iT邦幫忙

2023 iThome 鐵人賽

DAY 22
0
Modern Web

由前向後,從前端邁向全端系列 第 22

22.【從前端到全端,Nextjs+Nestjs】使用假資料創建Resolver並執行GraphQL

  • 分享至 

  • xImage
  •  

文章重點

  • 建構GraphQL API:實作產品相關的API,包括查詢、建立、更新和刪除功能
  • 建立模型與服務:建構Product模型,實作ProductService服務來操作資料,並使用faker來創建並模擬產品資料
  • 實作Resolver:透過ProductsResolver實現GraphQL的查詢與變更操作
  • 測試API:執行服務並透過GraphQL查詢測試API的功能,包括取得產品清單、新增產品、更新現有產品和刪除產品等操作

本文

在本教學中,我們將建立並測試自己的GraphQL API,首先聚焦於Products相關的API。我們將建立所需的文件並設定相應的結構。
https://ithelp.ithome.com.tw/upload/images/20231007/20108931wEdZTtRJQo.png

我們從定義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
https://ithelp.ithome.com.tw/upload/images/20231007/20108931ET6W1MWIfV.png

現在我們簡易實現了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,CreateProductInputUpdateProductInput並在service創建addProductupdateProduct以及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創建並做為橋樑來連接我們的資料庫


上一篇
21.【從前端到全端,Nextjs+Nestjs】利用GraphQL Schema First方法打造Nest.js的GraphQL服務
下一篇
23.【從前端到全端,Nextjs+Nestjs】利用 Prisma 和 Docker 快速搭建並操作 PostgreSQL 資料庫
系列文
由前向後,從前端邁向全端30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言