واجهات GraphQL

GraphQL مع TypeScript

20 دقيقة الدرس 30 من 35

GraphQL مع TypeScript

أنشئ خوادم وعملاء GraphQL آمنة من حيث النوع باستخدام TypeScript للحصول على أمان النوع من النهاية إلى النهاية.

محللات آمنة من حيث النوع

حدد أنواع TypeScript للمحللات الخاصة بك:

// types/graphql.ts export interface User { id: string; name: string; email: string; posts?: Post[]; } export interface Post { id: string; title: string; content: string; authorId: string; } export interface Context { db: Database; user?: User; } export interface CreateUserInput { name: string; email: string; }
// resolvers.ts import { Resolvers } from './generated/graphql'; import { Context } from './types/graphql'; export const resolvers: Resolvers<Context> = { Query: { user: async (_, { id }, { db }) => { const user = await db.user.findUnique({ where: { id } }); if (!user) throw new Error('لم يتم العثور على المستخدم'); return user; }, users: async (_, __, { db }) => { return db.user.findMany(); }, }, Mutation: { createUser: async (_, { input }, { db }) => { return db.user.create({ data: { name: input.name, email: input.email, }, }); }, }, User: { posts: async (parent, _, { db }) => { return db.post.findMany({ where: { authorId: parent.id }, }); }, }, Post: { author: async (parent, _, { db }) => { const author = await db.user.findUnique({ where: { id: parent.authorId }, }); if (!author) throw new Error('لم يتم العثور على الكاتب'); return author; }, }, };

الأنواع المُولَّدة

استخدم GraphQL Code Generator لتوليد الأنواع التلقائي:

# codegen.yml للخادم schema: ./schema.graphql generates: src/generated/graphql.ts: plugins: - typescript - typescript-resolvers config: contextType: ../types/graphql#Context mappers: User: ../types/graphql#User Post: ../types/graphql#Post
// أنواع المحللات المُولَّدة export type ResolversTypes = { User: ResolverTypeWrapper<User>; String: ResolverTypeWrapper<Scalars['String']>; Post: ResolverTypeWrapper<Post>; Query: ResolverTypeWrapper<{}>; Mutation: ResolverTypeWrapper<{}>; CreateUserInput: CreateUserInput; }; export type UserResolvers< ContextType = Context, ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'] > = { id?: Resolver<ResolversTypes['String'], ParentType, ContextType>; name?: Resolver<ResolversTypes['String'], ParentType, ContextType>; email?: Resolver<ResolversTypes['String'], ParentType, ContextType>; posts?: Resolver<Array<ResolversTypes['Post']>, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>; };
نصيحة: ربط أنواع GraphQL بنماذج قاعدة البيانات باستخدام تكوين mappers لضمان اتساق النوع.

Nexus - نهج الكود أولاً

أنشئ مخطط GraphQL الخاص بك بالكود باستخدام Nexus:

import { objectType, extendType, stringArg, nonNull, inputObjectType } from 'nexus'; // تعريف نوع User export const User = objectType({ name: 'User', definition(t) { t.nonNull.id('id'); t.nonNull.string('name'); t.nonNull.string('email'); t.nonNull.list.nonNull.field('posts', { type: 'Post', resolve: (parent, _, ctx) => { return ctx.db.post.findMany({ where: { authorId: parent.id }, }); }, }); }, }); // تعريف نوع Post export const Post = objectType({ name: 'Post', definition(t) { t.nonNull.id('id'); t.nonNull.string('title'); t.nonNull.string('content'); t.nonNull.field('author', { type: 'User', resolve: (parent, _, ctx) => { return ctx.db.user.findUniqueOrThrow({ where: { id: parent.authorId }, }); }, }); }, }); // تعريف أنواع الإدخال export const CreateUserInput = inputObjectType({ name: 'CreateUserInput', definition(t) { t.nonNull.string('name'); t.nonNull.string('email'); }, }); // تعريف الاستعلامات export const UserQuery = extendType({ type: 'Query', definition(t) { t.field('user', { type: 'User', args: { id: nonNull(stringArg()), }, resolve: (_, { id }, ctx) => { return ctx.db.user.findUnique({ where: { id } }); }, }); t.nonNull.list.nonNull.field('users', { type: 'User', resolve: (_, __, ctx) => { return ctx.db.user.findMany(); }, }); }, }); // تعريف الطفرات export const UserMutation = extendType({ type: 'Mutation', definition(t) { t.nonNull.field('createUser', { type: 'User', args: { input: nonNull('CreateUserInput'), }, resolve: (_, { input }, ctx) => { return ctx.db.user.create({ data: { name: input.name, email: input.email, }, }); }, }); }, });
// schema.ts import { makeSchema } from 'nexus'; import * as types from './graphql'; import path from 'path'; export const schema = makeSchema({ types, outputs: { schema: path.join(__dirname, './generated/schema.graphql'), typegen: path.join(__dirname, './generated/nexus-typegen.ts'), }, contextType: { module: path.join(__dirname, './context.ts'), export: 'Context', }, });

TypeGraphQL

استخدم الديكورات لتعريف المخطط مع TypeGraphQL:

import { ObjectType, Field, ID, Resolver, Query, Mutation, Arg, Ctx } from 'type-graphql'; @ObjectType() class User { @Field(() => ID) id: string; @Field() name: string; @Field() email: string; @Field(() => [Post]) posts: Post[]; } @ObjectType() class Post { @Field(() => ID) id: string; @Field() title: string; @Field() content: string; @Field(() => User) author: User; } @InputType() class CreateUserInput { @Field() name: string; @Field() email: string; } @Resolver(User) class UserResolver { @Query(() => User, { nullable: true }) async user(@Arg('id') id: string, @Ctx() ctx: Context): Promise<User | null> { return ctx.db.user.findUnique({ where: { id } }); } @Query(() => [User]) async users(@Ctx() ctx: Context): Promise<User[]> { return ctx.db.user.findMany(); } @Mutation(() => User) async createUser( @Arg('input') input: CreateUserInput, @Ctx() ctx: Context ): Promise<User> { return ctx.db.user.create({ data: { name: input.name, email: input.email, }, }); } @FieldResolver(() => [Post]) async posts(@Root() user: User, @Ctx() ctx: Context): Promise<Post[]> { return ctx.db.post.findMany({ where: { authorId: user.id }, }); } }
// server.ts import 'reflect-metadata'; import { ApolloServer } from 'apollo-server'; import { buildSchema } from 'type-graphql'; async function bootstrap() { const schema = await buildSchema({ resolvers: [UserResolver, PostResolver], }); const server = new ApolloServer({ schema, context: ({ req }) => ({ db: prisma, user: getUserFromToken(req.headers.authorization), }), }); const { url } = await server.listen(4000); console.log(`الخادم جاهز على ${url}`); } bootstrap();

استعلامات العميل الآمنة من حيث النوع

استخدم الأنواع المُولَّدة في كود العميل الخاص بك:

import { useGetUserQuery, GetUserQuery, GetUserQueryVariables } from './generated/graphql'; function UserProfile({ userId }: { userId: string }) { // خطاف استعلام مُكتَّب بالكامل const { data, loading, error } = useGetUserQuery({ variables: { id: userId }, }); // TypeScript يعرف الشكل الدقيق للبيانات const userName: string | undefined = data?.user?.name; const userPosts: Array<{ id: string; title: string }> | undefined = data?.user?.posts; return ( <div> {userName && <h1>{userName}</h1>} {userPosts?.map((post) => ( <div key={post.id}>{post.title}</div> ))} </div> ); }

أمان النوع من النهاية إلى النهاية

حقق أماناً كاملاً للنوع من قاعدة البيانات إلى واجهة المستخدم:

// 1. مخطط قاعدة البيانات (Prisma) model User { id String @id @default(uuid()) name String email String @unique posts Post[] } // 2. مخطط GraphQL (مُولَّد أو الكود أولاً) type User { id: ID! name: String! email: String! posts: [Post!]! } // 3. محللات TypeScript (محققة من النوع) const resolvers: Resolvers<Context> = { Query: { user: (_, { id }, { db }) => db.user.findUnique({ where: { id } }), }, }; // 4. أنواع العميل المُولَّدة const { data } = useGetUserQuery({ variables: { id: '123' } }); // 5. عرض واجهة مستخدم آمن من حيث النوع const name: string | undefined = data?.user?.name;
تحذير: حافظ على تكوين codegen متزامناً مع تغييرات المخطط. قم دائماً بتشغيل codegen بعد تعديل ملفات GraphQL.
تمرين:
  1. قم بإعداد GraphQL Code Generator مع إضافات TypeScript و React Apollo
  2. أنشئ عمليات GraphQL (استعلامات وطفرات) في ملفات .graphql منفصلة
  3. ولّد أنواع TypeScript وخطافات React
  4. أنشئ مكون React باستخدام الخطافات المُولَّدة مع أمان النوع الكامل
  5. نفذ محللات من جانب الخادم مع أنواع المحللات المُولَّدة