在深入探討具體的實作步驟之前,我們先簡單了解GraphQL的兩種主要實作方法: Schema First和Code First。
Schema First: 在Schema First方法中,我們首先定義了一個GraphQL Schema,該Schema定義了所有的類型、查詢和變異。然後基於這個Schema,開發人員編寫對應的解析器來實現每個字段的功能。
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
:
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"
}
}
並且我們需要在tsconfig.base.json中進行適當的配置,以確保ts-node可以正確執行,打開tsconfig.base.json
設置完成後,我們執行pnpm exec nx run iron-ecommerce-server:gen-gql-type
並且能看到創建出我們的type了
現在我們創建一個新的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
我們試著執行可以看到出現"Cannot return null for non-nullable field Query.getProducts."錯誤,因為我們目前還未實現我們內部的功能
透過本文,我們詳細探討如何在NestJS專案中採用Schema First方法來建構GraphQL服務。並且展示如何透過一個自動腳本以及Nx的自訂指令來自動從GraphQL Schema產生TypeScript類型。
雖然已經建構了基本的架構,但內部的功能尚未實作。在下一篇文章中,我們將開始實現具體的功能,以完善我們的GraphQL服務。