在本教學中,我們將建立並測試自己的GraphQL API,首先聚焦於Products相關的API。我們將建立所需的文件並設定相應的結構。
我們從定義Product模型開始,之後建立ProductService服務來模擬產品資料的產生和管理
///// apps\iron-ecommerce-server\src\api\products\products.model.ts
import { Field, ObjectType } from "@nestjs/graphql";
@ObjectType()
export class Product {
@Field()
id: string;
@Field()
name: string;
@Field()
price: number;
@Field()
description: string;
@Field()
imageUrl: string;
}
///// apps\iron-ecommerce-server\src\api\products\products.service.ts
import { Product } from "./products.model";
import { faker } from "@faker-js/faker";
import { Injectable } from "@nestjs/common";
export function generateProducts(count: number): Product[] {
const products: Product[] = [];
for (let i = 1; i <= count; i++) {
products.push({
id: i.toString(),
name: faker.commerce.productName(),
price: parseFloat(faker.commerce.price()),
description: faker.commerce.productDescription(),
imageUrl: faker.image.url()
});
}
return products;
}
@Injectable()
export class ProductService {
private readonly products: Product[] = generateProducts(10);
findAll(): Product[] {
return this.products;
}
findOne(id: string): Product | null {
return this.products.find((product) => product.id === id) || null;
}
}
///// apps\iron-ecommerce-server\src\api\products\products.resolver.ts
import { Product } from "./products.model";
import { ProductService } from "./products.service";
import { Args, Query, Resolver } from "@nestjs/graphql";
@Resolver(() => Product)
export class ProductsResolver {
constructor(private readonly productService: ProductService) {}
@Query(() => [Product])
async getProducts(): Promise<Product[]> {
return await this.productService.findAll();
}
@Query(() => Product, { nullable: true })
async getProduct(@Args("id") id: string): Promise<Product | null> {
return await this.productService.findOne(id);
}
}
///// apps\iron-ecommerce-server\src\api\products\products.module.ts
import { ProductsResolver } from "./products.resolver";
import { ProductService } from "./products.service";
import { Module } from "@nestjs/common";
@Module({
imports: [],
providers: [ProductsResolver, ProductService]
})
export class ProductsModule {}
接下來,我們執行啟動server並測試,執行pnpm exec nx run iron-ecommerce-server:serve
。並且我們使用query查詢data:
{
getProducts {
id
description
}
}
能看到我們能獲取到我們所有的data
現在我們簡易實現了Product相關的query,現在我們來創建mutation功能
首先,我們要修改一下我們的schema,打開apps\iron-ecommerce-server\src\graphql\schemas\common\main.graphql
:
scalar DateTime
input NewProductInput {
name: String!
price: Float!
description: String!
imageUrl: String!
}
input UpdateProductInput {
id: ID!
name: String!
price: Float!
description: String!
imageUrl: String!
}
type Product {
id: ID!
name: String!
price: Float!
description: String!
imageUrl: String!
}
type CartItem {
productId: ID!
productName: String!
price: Float!
quantity: Int!
}
type User {
id: ID!
name: String!
email: String!
}
type AuthPayload {
user: User!
token: String!
}
input UserInput {
name: String!
email: String!
password: String!
}
input CartItemInput {
productId: ID!
quantity: Int!
}
type Order {
id: ID!
items: [CartItem!]!
orderDate: DateTime!
}
type Query {
getProducts: [Product!]!
getProduct(id: ID!): Product
getUserProfile: User
getCartItems: [CartItem!]!
}
type Mutation {
loginUser(username: String!, password: String!): AuthPayload
registerUser(input: UserInput!): AuthPayload
addProduct(input: NewProductInput!): Product
updateProduct(input: UpdateProductInput!): Product
deleteProduct(id: ID!): Boolean
addCartItem(productId: ID!, quantity: Int!): [CartItem!]!
removeCartItem(productId: ID!): [CartItem!]!
updateCartItem(productId: ID!, quantity: Int!): [CartItem!]!
checkout(cartItems: [CartItemInput!]!): Order
}
修改完schema後,執行指令以創建型別
pnpm exec nx run iron-ecommerce-server:gen-gql-type
我們首先創建GraphQL的InputType,CreateProductInput
和UpdateProductInput
並在service創建addProduct
、updateProduct
以及deleteProduct
功能
// apps\iron-ecommerce-server\src\api\products\products.model.ts
import { Field, InputType, Int, ObjectType } from "@nestjs/graphql";
@ObjectType()
export class Product {
@Field()
id: string;
@Field()
name: string;
@Field()
price: number;
@Field()
description: string;
@Field()
imageUrl: string;
}
@InputType()
export class NewProductInput {
@Field()
name: string;
@Field(() => Int)
price: number;
@Field()
description: string;
@Field()
imageUrl: string;
}
@InputType()
export class UpdateProductInput {
@Field()
id: string;
@Field()
name: string;
@Field(() => Int)
price: number;
@Field()
description: string;
@Field()
imageUrl: string;
}
///// apps\iron-ecommerce-server\src\api\products\products.service.ts
import { Product } from "./products.model";
import { faker } from "@faker-js/faker";
import { Injectable } from "@nestjs/common";
export function generateProducts(count: number): Product[] {
const products: Product[] = [];
for (let i = 1; i <= count; i++) {
products.push({
id: i.toString(),
name: faker.commerce.productName(),
price: parseFloat(faker.commerce.price()),
description: faker.commerce.productDescription(),
imageUrl: faker.image.url()
});
}
return products;
}
@Injectable()
export class ProductService {
private readonly products: Product[] = generateProducts(10);
private nextId: number = this.products.length + 1;
findAll(): Product[] {
return this.products;
}
findOne(id: string): Product | null {
return this.products.find((product) => product.id === id) || null;
}
addProduct(newProduct: Omit<Product, "id">): Product {
const product: Product = {
id: (this.nextId++).toString(),
...newProduct
};
this.products.push(product);
return product;
}
updateProduct(updatedProduct: Product): Product | null {
const index = this.products.findIndex((product) => product.id === updatedProduct.id);
if (index === -1) return null;
this.products[index] = updatedProduct;
return updatedProduct;
}
deleteProduct(id: string): boolean {
const index = this.products.findIndex((product) => product.id === id);
if (index === -1) return false;
this.products.splice(index, 1);
return true;
}
}
現在我們開始實現我們的Resolver:
///// apps\iron-ecommerce-server\src\api\products\products.resolver.ts
import { NewProductInput, Product, UpdateProductInput } from "./products.model";
import { ProductService } from "./products.service";
import { Args, Mutation, Query, Resolver } from "@nestjs/graphql";
@Resolver(() => Product)
export class ProductsResolver {
constructor(private readonly productService: ProductService) {}
@Query(() => [Product])
async getProducts(): Promise<Product[]> {
return await this.productService.findAll();
}
@Query(() => Product, { nullable: true })
async getProduct(@Args("id") id: string): Promise<Product | null> {
return await this.productService.findOne(id);
}
@Mutation(() => Product)
async addProduct(@Args("input") newProduct: NewProductInput): Promise<Product> {
return await this.productService.addProduct(newProduct);
}
@Mutation(() => Product, { nullable: true })
async updateProduct(@Args("input") updatedProduct: UpdateProductInput): Promise<Product | null> {
return await this.productService.updateProduct(updatedProduct);
}
@Mutation(() => Boolean)
async deleteProduct(@Args("id") id: string): Promise<boolean> {
return await this.productService.deleteProduct(id);
}
}
接下來我們進行測試,首先使用query獲取所有的data:
{
getProducts {
id
name
description
}
}
///// Return Data
{
"data": {
"getProducts": [
{
"id": "1",
"name": "Incredible Bronze Pizza",
"description": "New ABC 13 9370, 13.3, 5th Gen CoreA5-8250U, 8GB RAM, 256GB SSD, power UHD Graphics, OS 10 Home, OS Office A & J 2016",
"imageUrl": "https://picsum.photos/seed/jtEhxzH/640/480"
},
{
"id": "2",
"name": "Refined Rubber Car",
"description": "The beautiful range of Apple Naturalé that has an exciting mix of natural ingredients. With the Goodness of 100% Natural Ingredients",
"imageUrl": "https://loremflickr.com/640/480?lock=1301813913452544"
},
{
"id": "3",
"name": "Awesome Rubber Towels",
"description": "Andy shoes are designed to keeping in mind durability as well as trends, the most stylish range of shoes & sandals",
"imageUrl": "https://loremflickr.com/640/480?lock=5239937867710464"
},
{
"id": "4",
"name": "Modern Fresh Mouse",
"description": "Ergonomic executive chair upholstered in bonded black leather and PVC padded seat and back for all-day comfort and support",
"imageUrl": "https://loremflickr.com/640/480?lock=7796565974450176"
},
{
"id": "5",
"name": "Practical Plastic Hat",
"description": "New range of formal shirts are designed keeping you in mind. With fits and styling that will make you stand apart",
"imageUrl": "https://picsum.photos/seed/FA9CKfkfl0/640/480"
},
{
"id": "6",
"name": "Refined Plastic Gloves",
"description": "The Nagasaki Lander is the trademarked name of several series of Nagasaki sport bikes, that started with the 1984 ABC800J",
"imageUrl": "https://loremflickr.com/640/480?lock=2819936273563648"
},
{
"id": "7",
"name": "Ergonomic Concrete Car",
"description": "The automobile layout consists of a front-engine design, with transaxle-type transmissions mounted at the rear of the engine and four wheel drive",
"imageUrl": "https://picsum.photos/seed/7FDTs6fVz/640/480"
},
{
"id": "8",
"name": "Refined Fresh Shoes",
"description": "The Football Is Good For Training And Recreational Purposes",
"imageUrl": "https://loremflickr.com/640/480?lock=2485948505915392"
},
{
"id": "9",
"name": "Handcrafted Steel Gloves",
"description": "The Apollotech B340 is an affordable wireless mouse with reliable connectivity, 12 months battery life and modern design",
"imageUrl": "https://loremflickr.com/640/480?lock=6466009309380608"
},
{
"id": "10",
"name": "Oriental Frozen Shirt",
"description": "New ABC 13 9370, 13.3, 5th Gen CoreA5-8250U, 8GB RAM, 256GB SSD, power UHD Graphics, OS 10 Home, OS Office A & J 2016",
"imageUrl": "https://loremflickr.com/640/480?lock=5812486552944640"
}
]
}
}
接下來,我們測試建立、更新和刪除產品的功能。我們分別執行下面的GraphQL變更操作,然後重新查詢產品列表,最後觀察操作結果
///// create
mutation {
addProduct(input: {
name: "New-Product",
price: 20.0,
description: "This is New-Product",
imageUrl: "http://example.com/new-product.jpg"
}) {
id
name
price
description
imageUrl
}
}
///// update
mutation {
updateProduct(input: {
id: "2",
name: "Updated Product Name",
price: 25.0,
description: "Updated Product Description",
imageUrl: "http://example.com/updated-product.jpg"
}) {
id
name
price
description
imageUrl
}
}
///// delete
mutation {
deleteProduct(id: "5")
}
我們能看到資料中新建了id為11的product、更新了id為2的product以及刪除id為5的product。
{
"data": {
"getProducts": [
{
"id": "1",
"name": "Incredible Bronze Pizza",
"description": "New ABC 13 9370, 13.3, 5th Gen CoreA5-8250U, 8GB RAM, 256GB SSD, power UHD Graphics, OS 10 Home, OS Office A & J 2016",
"imageUrl": "https://picsum.photos/seed/jtEhxzH/640/480"
},
{
"id": "2",
"name": "Updated Product Name",
"description": "Updated Product Description",
"imageUrl": "http://example.com/updated-product.jpg"
},
{
"id": "3",
"name": "Awesome Rubber Towels",
"description": "Andy shoes are designed to keeping in mind durability as well as trends, the most stylish range of shoes & sandals",
"imageUrl": "https://loremflickr.com/640/480?lock=5239937867710464"
},
{
"id": "4",
"name": "Modern Fresh Mouse",
"description": "Ergonomic executive chair upholstered in bonded black leather and PVC padded seat and back for all-day comfort and support",
"imageUrl": "https://loremflickr.com/640/480?lock=7796565974450176"
},
{
"id": "6",
"name": "Refined Plastic Gloves",
"description": "The Nagasaki Lander is the trademarked name of several series of Nagasaki sport bikes, that started with the 1984 ABC800J",
"imageUrl": "https://loremflickr.com/640/480?lock=2819936273563648"
},
{
"id": "7",
"name": "Ergonomic Concrete Car",
"description": "The automobile layout consists of a front-engine design, with transaxle-type transmissions mounted at the rear of the engine and four wheel drive",
"imageUrl": "https://picsum.photos/seed/7FDTs6fVz/640/480"
},
{
"id": "8",
"name": "Refined Fresh Shoes",
"description": "The Football Is Good For Training And Recreational Purposes",
"imageUrl": "https://loremflickr.com/640/480?lock=2485948505915392"
},
{
"id": "9",
"name": "Handcrafted Steel Gloves",
"description": "The Apollotech B340 is an affordable wireless mouse with reliable connectivity, 12 months battery life and modern design",
"imageUrl": "https://loremflickr.com/640/480?lock=6466009309380608"
},
{
"id": "10",
"name": "Oriental Frozen Shirt",
"description": "New ABC 13 9370, 13.3, 5th Gen CoreA5-8250U, 8GB RAM, 256GB SSD, power UHD Graphics, OS 10 Home, OS Office A & J 2016",
"imageUrl": "https://loremflickr.com/640/480?lock=5812486552944640"
},
{
"id": "11",
"name": "New-Product",
"description": "This is New-Product",
"imageUrl": "http://example.com/new-product.jpg"
}
]
}
}
透過本文,我們成功建立了產品管理的GraphQL API,實現了產品資料的查詢、建立、更新和刪除功能。下一篇我們將會替換掉假資料,並使用prsima創建並做為橋樑來連接我們的資料庫