iT邦幫忙

2023 iThome 鐵人賽

DAY 21
0
Modern Web

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

21.【從前端到全端,Nextjs+Nestjs】利用GraphQL Schema First方法打造Nest.js的GraphQL服務

  • 分享至 

  • xImage
  •  

文章重點

  • 介紹了GraphQL的兩種實作方法:Schema First和Code First,並對兩者進行了比較。
  • 展示如何在NestJS專案中採用Schema First,並通過GraphQL Schema,並自動產生TypeScript類型。
  • 創建Nx指令來自動化產生types的過程,簡化了類型同步的工作。

本文

在深入探討具體的實作步驟之前,我們先簡單了解GraphQL的兩種主要實作方法: Schema First和Code First。

  1. Schema First: 在Schema First方法中,我們首先定義了一個GraphQL Schema,該Schema定義了所有的類型、查詢和變異。然後基於這個Schema,開發人員編寫對應的解析器來實現每個字段的功能。

  2. Code First: 從撰寫功能開始。GraphQL的Schema是從這些類和裝飾器自動生成的

上一篇我們簡單的使用Code First來創建我們GraphQL,這篇我將使用schema First來創建我們的GraphQL

我們先安裝所需的依賴:

pnpm add -D ts-morph

我們先使用schema first,我們把先前的schema複製到apps\iron-ecommerce-server\src\graphql\schemas\common並且我們重新命名為main.graphql:
https://ithelp.ithome.com.tw/upload/images/20231007/20108931OoMlKg3m4t.png

scalar DateTime

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
	addCartItem(productId: ID!, quantity: Int!): [CartItem!]!
	removeCartItem(productId: ID!): [CartItem!]!
	updateCartItem(productId: ID!, quantity: Int!): [CartItem!]!
	checkout(cartItems: [CartItemInput!]!): Order
}

為了讓TypeScript能夠理解GraphQL Schema,我們建立一個腳本來自動從GraphQL Schema產生TypeScript類型,創建apps\iron-ecommerce-server\src\graphql\generate-typings.ts:

import { GraphQLDefinitionsFactory } from "@nestjs/graphql";

const definitionsFactory = new GraphQLDefinitionsFactory();

definitionsFactory.generate({
	typePaths: ["apps/iron-ecommerce-server/src/graphql/schemas/common/**/*.graphql"],
	path: "apps/iron-ecommerce-server/src/graphql/types/graphql-types.ts",
	outputAs: "class",
	emitTypenameField: true
});

我們在apps\iron-ecommerce-server\project.json檔案中,我們建立一個自訂指令,用於執行類型生成腳本:

		},
		"gen-gql-type": {
			"executor": "nx:run-commands",
			"options": {
				"command": "ts-node apps/iron-ecommerce-server/src/graphql/generate-typings.ts"
			}
		}

https://ithelp.ithome.com.tw/upload/images/20231007/20108931H61YzkyBOV.png

並且我們需要在tsconfig.base.json中進行適當的配置,以確保ts-node可以正確執行,打開tsconfig.base.json
https://ithelp.ithome.com.tw/upload/images/20231007/20108931vVFmzKcDOY.png

設置完成後,我們執行pnpm exec nx run iron-ecommerce-server:gen-gql-type
https://ithelp.ithome.com.tw/upload/images/20231007/20108931WkYPK8exD1.png
並且能看到創建出我們的type了
https://ithelp.ithome.com.tw/upload/images/20231007/20108931tQAQNGqRrX.png

現在我們創建一個新的GqlSchemaFirstConfigService,我們先修正設置,並創建一個新的Service

///// apps\iron-ecommerce-server\src\common\configs\config.interface.ts

export interface Config {
	nest: NestConfig;
	cors: CorsConfig;
	graphql: GraphqlConfig;
}

export interface NestConfig {
	port: number;
}

export interface CorsConfig {
	enabled: boolean;
}

export interface GraphqlConfig {
	codefirst: {
		playgroundEnabled: boolean;
		debug: boolean;
		schemaDestination: string;
		sortSchema: boolean;
	};
	schemafirst: {
		playgroundEnabled: boolean;
		typePaths: string[];
		definitions: {
			path: string;
		};
	};
}


///// apps\iron-ecommerce-server\src\common\configs\config.ts

import type { Config } from "./config.interface";

const config: Config = {
	nest: {
		port: 3000
	},
	cors: {
		enabled: true
	},
	graphql: {
		codefirst: {
			playgroundEnabled: true,
			debug: true,
			schemaDestination: "apps/iron-ecommerce-server/src/graphql/schemas/schema.graphql",
			sortSchema: true
		},
		schemafirst: {
			playgroundEnabled: true,
			typePaths: ["apps/iron-ecommerce-server/src/graphql/**/*.graphql"],
			definitions: {
				path: "apps/iron-ecommerce-server/src/graphql/types/graphql-types.ts"
			}
		}
	}
};

export default (): Config => config;

我們創建service

///// apps\iron-ecommerce-server\src\graphql\gql-config.service.ts

import { GraphqlConfig } from "../common/configs/config.interface";
import { ApolloDriverConfig } from "@nestjs/apollo";
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { GqlOptionsFactory } from "@nestjs/graphql";

@Injectable()
export class GqlCodeFirstConfigService implements GqlOptionsFactory {
	constructor(private configService: ConfigService) {}
	createGqlOptions(): ApolloDriverConfig {
		const graphqlConfig = this.configService.get<GraphqlConfig>("graphql");
		return {
			// schema options
			autoSchemaFile: graphqlConfig.codefirst.schemaDestination,
			sortSchema: graphqlConfig.codefirst.sortSchema,
			buildSchemaOptions: {
				numberScalarMode: "integer"
			},
			// subscription
			installSubscriptionHandlers: true,
			includeStacktraceInErrorResponses: graphqlConfig.codefirst.debug,
			playground: graphqlConfig.codefirst.playgroundEnabled,
			context: ({ req }) => ({ req })
		};
	}
}

@Injectable()
export class GqlSchemaFirstConfigService implements GqlOptionsFactory {
	constructor(private configService: ConfigService) {}
	createGqlOptions(): ApolloDriverConfig {
		const graphqlConfig = this.configService.get<GraphqlConfig>("graphql");

		return {
			typePaths: graphqlConfig.schemafirst.typePaths,
			definitions: graphqlConfig.schemafirst.definitions,
			playground: graphqlConfig.schemafirst.playgroundEnabled
		};
	}
}


這裡我們在module將code first的service更換成schema first的service:

import { GqlSchemaFirstConfigService } from "./gql-config.service";
import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo";
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { GraphQLModule } from "@nestjs/graphql";

@Module({
	imports: [
		GraphQLModule.forRootAsync<ApolloDriverConfig>({
			driver: ApolloDriver,
			useClass: GqlSchemaFirstConfigService
		}),
		ConfigModule
	],
	providers: [GqlSchemaFirstConfigService],
	exports: [GraphQLModule]
})
export class GraphQLSetupModule {}


接下來,我們試著執行pnpm exec nx run iron-ecommerce-server:serve 並打開http://localhost:3000/graphql,我們能看到我們schema和documents
https://ithelp.ithome.com.tw/upload/images/20231007/20108931riPgEMYiqk.png

我們試著執行可以看到出現"Cannot return null for non-nullable field Query.getProducts."錯誤,因為我們目前還未實現我們內部的功能
https://ithelp.ithome.com.tw/upload/images/20231007/20108931n62Grig0ys.png


總結

透過本文,我們詳細探討如何在NestJS專案中採用Schema First方法來建構GraphQL服務。並且展示如何透過一個自動腳本以及Nx的自訂指令來自動從GraphQL Schema產生TypeScript類型。

雖然已經建構了基本的架構,但內部的功能尚未實作。在下一篇文章中,我們將開始實現具體的功能,以完善我們的GraphQL服務。


上一篇
20.【從前端到全端,Nextjs+Nestjs】在Nx中設置NestJS GraphQL server
下一篇
22.【從前端到全端,Nextjs+Nestjs】使用假資料創建Resolver並執行GraphQL
系列文
由前向後,從前端邁向全端30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言