TypeScript مع GraphQL
GraphQL وTypeScript يشكلان تطابقاً مثالياً. نهج GraphQL القائم على المخطط يوفر أماناً تلقائياً للأنواع، بينما يضمن TypeScript أن استعلاماتك ومحللاتك آمنة من حيث النوع. في هذا الدرس، سنستكشف الاستعلامات المكتوبة، توليد الكود، المحللات المكتوبة، وأنماط تكامل GraphQL العملية.
لماذا GraphQL + TypeScript؟
GraphQL وTypeScript يكملان بعضهما البعض بشكل مثالي:
- أنواع مدفوعة بالمخطط: مخططات GraphQL تولد أنواع TypeScript تلقائياً
- أمان من البداية للنهاية: الأنواع تتدفق من المخطط إلى المحللات إلى استعلامات العميل
- الإكمال التلقائي: بيئات التطوير توفر اقتراحات استعلامات GraphQL بناءً على المخطط
- ثقة في إعادة الهيكلة: تغييرات المخطط تظهر أخطاء الأنواع فوراً
- التوثيق: الأنواع تعمل كتوثيق حي
أساسيات GraphQL
أولاً، لنفهم أساسيات GraphQL:
# GraphQL Schema (schema.graphql)
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
publishedAt: DateTime
}
type Query {
user(id: ID!): User
users: [User!]!
post(id: ID!): Post
}
type Mutation {
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
}
input CreatePostInput {
title: String!
content: String!
authorId: ID!
}
input UpdatePostInput {
title: String
content: String
}
توليد كود GraphQL
استخدم graphql-codegen لتوليد أنواع TypeScript من مخططك:
// تثبيت التبعيات
npm install --save-dev @graphql-codegen/cli \
@graphql-codegen/typescript \
@graphql-codegen/typescript-operations \
@graphql-codegen/typescript-resolvers
// codegen.yml
schema: "./schema.graphql"
documents: "./src/**/*.graphql"
generates:
./src/generated/types.ts:
plugins:
- typescript
- typescript-operations
- typescript-resolvers
config:
useIndexSignature: true
enumsAsTypes: true
avoidOptionals: false
maybeValue: T | null
هذا يولد أنواع TypeScript شاملة:
// generated/types.ts (مقتطف)
export type User = {
__typename?: 'User';
id: Scalars['ID'];
name: Scalars['String'];
email: Scalars['String'];
posts: Array<Post>;
};
export type Post = {
__typename?: 'Post';
id: Scalars['ID'];
title: Scalars['String'];
content: Scalars['String'];
author: User;
publishedAt?: Maybe<Scalars['DateTime']>;
};
export type QueryUserArgs = {
id: Scalars['ID'];
};
export type MutationCreatePostArgs = {
input: CreatePostInput;
};
محللات GraphQL المكتوبة
أنشئ محللات آمنة من حيث النوع باستخدام الأنواع المولدة:
import { Resolvers, User, Post } from './generated/types';
// نماذج قاعدة البيانات (مثال)
interface UserModel {
id: string;
name: string;
email: string;
}
interface PostModel {
id: string;
title: string;
content: string;
authorId: string;
publishedAt: Date | null;
}
// نوع السياق
interface Context {
db: {
users: UserModel[];
posts: PostModel[];
};
userId?: string;
}
// محللات مكتوبة
const resolvers: Resolvers<Context> = {
Query: {
user: async (_, { id }, context) => {
const user = context.db.users.find(u => u.id === id);
if (!user) return null;
return user;
},
users: async (_, __, context) => {
return context.db.users;
},
post: async (_, { id }, context) => {
const post = context.db.posts.find(p => p.id === id);
if (!post) return null;
return post;
}
},
Mutation: {
createPost: async (_, { input }, context) => {
if (!context.userId) {
throw new Error('Not authenticated');
}
const post: PostModel = {
id: generateId(),
title: input.title,
content: input.content,
authorId: input.authorId,
publishedAt: new Date()
};
context.db.posts.push(post);
return post;
}
},
User: {
posts: async (user, _, context) => {
return context.db.posts.filter(p => p.authorId === user.id);
}
},
Post: {
author: async (post, _, context) => {
const author = context.db.users.find(u => u.id === post.authorId);
if (!author) throw new Error('Author not found');
return author;
}
}
};
ملاحظة: نوع Resolvers المولد يضمن أن جميع المحللات تطابق المخطط، مع أنواع معاملات صحيحة، أنواع إرجاع، وسياق.
عميل GraphQL آمن من حيث النوع (graphql-request)
استخدم graphql-request للاستعلامات المكتوبة للعميل:
npm install graphql graphql-request
import { GraphQLClient, gql } from 'graphql-request';
import { User, Post, CreatePostInput } from './generated/types';
const client = new GraphQLClient('http://localhost:4000/graphql', {
headers: {
authorization: 'Bearer token'
}
});
// استعلام مكتوب
async function getUser(id: string): Promise<User | null> {
const query = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
publishedAt
}
}
}
`;
const data = await client.request<{ user: User | null }>(query, { id });
return data.user;
}
// طفرة مكتوبة
async function createPost(input: CreatePostInput): Promise<Post> {
const mutation = gql`
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
content
author {
id
name
}
publishedAt
}
}
`;
const data = await client.request<{ createPost: Post }>(mutation, { input });
return data.createPost;
}
// الاستخدام
async function demo() {
const user = await getUser('123');
if (user) {
console.log(`User: ${user.name}`);
console.log(`Posts: ${user.posts.length}`);
}
const newPost = await createPost({
title: 'TypeScript + GraphQL',
content: 'Amazing combination!',
authorId: '123'
});
console.log(`Created post: ${newPost.title}`);
}
متقدم: عمليات GraphQL المكتوبة
ولد أنواعاً لعمليات محددة باستخدام graphql-codegen:
// queries.graphql
query GetUserWithPosts($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
content
publishedAt
}
}
}
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
content
author {
id
name
}
}
}
// codegen.yml (محسّن)
generates:
./src/generated/types.ts:
plugins:
- typescript
- typescript-operations
config:
namingConvention:
typeNames: pascal-case
enumValues: upper-case
هذا يولد أنواعاً خاصة بالعمليات:
// generated/types.ts (إضافي)
export type GetUserWithPostsQuery = {
__typename?: 'Query';
user?: {
__typename?: 'User';
id: string;
name: string;
email: string;
posts: Array<{
__typename?: 'Post';
id: string;
title: string;
content: string;
publishedAt?: string | null;
}>;
} | null;
};
export type GetUserWithPostsQueryVariables = {
id: string;
};
export type CreatePostMutation = {
__typename?: 'Mutation';
createPost: {
__typename?: 'Post';
id: string;
title: string;
content: string;
author: {
__typename?: 'User';
id: string;
name: string;
};
};
};
export type CreatePostMutationVariables = {
input: CreatePostInput;
};
// الاستخدام مع أنواع العمليات
import { GetUserWithPostsQuery, GetUserWithPostsQueryVariables } from './generated/types';
async function getUserWithPosts(id: string): Promise<GetUserWithPostsQuery['user']> {
const query = gql`
query GetUserWithPosts($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
content
publishedAt
}
}
}
`;
const variables: GetUserWithPostsQueryVariables = { id };
const data = await client.request<GetUserWithPostsQuery>(query, variables);
return data.user;
}
TypeScript + Apollo Server
استخدم Apollo Server مع TypeScript لواجهة GraphQL API آمنة من حيث النوع بالكامل:
npm install apollo-server graphql
npm install --save-dev @graphql-tools/schema
import { ApolloServer } from 'apollo-server';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { readFileSync } from 'fs';
import { Resolvers } from './generated/types';
// تحميل المخطط
const typeDefs = readFileSync('./schema.graphql', 'utf-8');
// محللات آمنة من حيث النوع
const resolvers: Resolvers = {
Query: {
user: async (_, { id }, context) => {
return context.dataSources.users.findById(id);
}
}
};
// إنشاء المخطط
const schema = makeExecutableSchema({
typeDefs,
resolvers
});
// نوع السياق
interface Context {
dataSources: {
users: UserDataSource;
posts: PostDataSource;
};
userId?: string;
}
// إنشاء الخادم
const server = new ApolloServer({
schema,
context: ({ req }): Context => ({
dataSources: {
users: new UserDataSource(),
posts: new PostDataSource()
},
userId: extractUserId(req.headers.authorization)
})
});
server.listen().then(({ url }) => {
console.log(`Server ready at ${url}`);
});
معالجة الأخطاء
import { GraphQLError } from 'graphql';
const resolvers: Resolvers = {
Query: {
user: async (_, { id }, context) => {
const user = await context.dataSources.users.findById(id);
if (!user) {
throw new GraphQLError('User not found', {
extensions: {
code: 'USER_NOT_FOUND',
userId: id
}
});
}
return user;
}
},
Mutation: {
createPost: async (_, { input }, context) => {
if (!context.userId) {
throw new GraphQLError('Authentication required', {
extensions: { code: 'UNAUTHENTICATED' }
});
}
try {
return await context.dataSources.posts.create(input);
} catch (error) {
throw new GraphQLError('Failed to create post', {
extensions: {
code: 'CREATE_POST_FAILED',
originalError: error
}
});
}
}
}
};
نصيحة: استخدم امتدادات أخطاء GraphQL لتوفير معلومات أخطاء منظمة مع كتابة صحيحة.
دعم الاشتراكات
// schema.graphql
type Subscription {
postCreated: Post!
userUpdated(userId: ID!): User!
}
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
const resolvers: Resolvers = {
Mutation: {
createPost: async (_, { input }, context) => {
const post = await context.dataSources.posts.create(input);
// نشر حدث الاشتراك
pubsub.publish('POST_CREATED', { postCreated: post });
return post;
}
},
Subscription: {
postCreated: {
subscribe: () => pubsub.asyncIterator(['POST_CREATED'])
},
userUpdated: {
subscribe: (_, { userId }) => {
return pubsub.asyncIterator([`USER_UPDATED_${userId}`]);
}
}
}
};
تمرين: أنشئ GraphQL API باستخدام TypeScript:
- عرف مخططاً لمدونة (منشورات، تعليقات، مؤلفون)
- ولد أنواع TypeScript باستخدام graphql-codegen
- نفذ محللات مكتوبة مع Apollo Server
- أنشئ عميلاً مكتوباً باستخدام graphql-request
- أضف دعم الاشتراكات للتحديثات في الوقت الفعلي
- نفذ معالجة مناسبة للأخطاء مع رموز أخطاء مخصصة
الخلاصة
في هذا الدرس، تعلمت كيفية دمج TypeScript مع GraphQL لأمان الأنواع من البداية للنهاية. غطينا توليد الأنواع بناءً على المخطط، المحللات المكتوبة، استعلامات العميل الآمنة من حيث النوع، ومعالجة الأخطاء. هذا المزيج يوفر تجربة مطور لا مثيل لها وثقة في واجهات GraphQL APIs الخاصة بك.