واجهات GraphQL
بناء مشروع GraphQL API (الجزء 1)
بناء مشروع GraphQL API (الجزء 1)
في هذا الدرس، سنبدأ في بناء GraphQL API كامل لمنصة مدونة. سيوضح هذا المشروع العملي تصميم المخطط والمصادقة وعمليات CRUD في تطبيق واقعي.
نظرة عامة على المشروع
سنبني API لمنصة مدونة بالميزات التالية:
- مصادقة وتفويض المستخدمين
- إنشاء وقراءة وتحديث وحذف منشورات المدونة
- نظام التعليقات مع الردود المتداخلة
- ملفات تعريف المستخدمين مع الصور الرمزية
- البحث والتصفية في المنشورات
- ترقيم الصفحات لمجموعات البيانات الكبيرة
إعداد المشروع
أولاً، لنقم بتهيئة مشروع Node.js مع التبعيات الضرورية:
mkdir graphql-blog-api\ncd graphql-blog-api\nnpm init -y\n\n# تثبيت التبعيات الأساسية\nnpm install apollo-server graphql\n\n# تثبيت قاعدة البيانات و ORM\nnpm install mongoose\n\n# تثبيت المصادقة\nnpm install bcryptjs jsonwebtoken\n\n# تثبيت الأدوات المساعدة\nnpm install dotenv validator
هيكل المشروع
أنشئ هيكل المجلدات التالي:
graphql-blog-api/\n├── src/\n│ ├── models/ # نماذج قاعدة البيانات\n│ │ ├── User.js\n│ │ ├── Post.js\n│ │ └── Comment.js\n│ ├── schema/ # مخطط GraphQL\n│ │ ├── typeDefs.js\n│ │ └── resolvers.js\n│ ├── utils/ # وظائف مساعدة\n│ │ ├── auth.js\n│ │ └── validators.js\n│ ├── context.js # إعداد السياق\n│ └── index.js # نقطة دخول الخادم\n├── .env # متغيرات البيئة\n└── package.json
نماذج قاعدة البيانات
لنقم بإنشاء نماذج MongoDB باستخدام Mongoose:
// src/models/User.js\nconst mongoose = require('mongoose');\n\nconst userSchema = new mongoose.Schema({\n username: {\n type: String,\n required: true,\n unique: true,\n trim: true,\n minlength: 3\n },\n email: {\n type: String,\n required: true,\n unique: true,\n lowercase: true\n },\n password: {\n type: String,\n required: true,\n minlength: 6\n },\n name: {\n type: String,\n required: true\n },\n avatar: String,\n bio: String,\n createdAt: {\n type: Date,\n default: Date.now\n }\n});\n\nmodule.exports = mongoose.model('User', userSchema);// src/models/Post.js\nconst mongoose = require('mongoose');\n\nconst postSchema = new mongoose.Schema({\n title: {\n type: String,\n required: true,\n trim: true\n },\n content: {\n type: String,\n required: true\n },\n excerpt: String,\n author: {\n type: mongoose.Schema.Types.ObjectId,\n ref: 'User',\n required: true\n },\n tags: [String],\n published: {\n type: Boolean,\n default: false\n },\n createdAt: {\n type: Date,\n default: Date.now\n },\n updatedAt: {\n type: Date,\n default: Date.now\n }\n});\n\nmodule.exports = mongoose.model('Post', postSchema);// src/models/Comment.js\nconst mongoose = require('mongoose');\n\nconst commentSchema = new mongoose.Schema({\n content: {\n type: String,\n required: true\n },\n author: {\n type: mongoose.Schema.Types.ObjectId,\n ref: 'User',\n required: true\n },\n post: {\n type: mongoose.Schema.Types.ObjectId,\n ref: 'Post',\n required: true\n },\n parentComment: {\n type: mongoose.Schema.Types.ObjectId,\n ref: 'Comment'\n },\n createdAt: {\n type: Date,\n default: Date.now\n }\n});\n\nmodule.exports = mongoose.model('Comment', commentSchema);تصميم مخطط GraphQL
الآن لنحدد مخطط GraphQL مع الأنواع والاستعلامات والطفرات:
// src/schema/typeDefs.js\nconst { gql } = require('apollo-server');\n\nconst typeDefs = gql`\n type User {\n id: ID!\n username: String!\n email: String!\n name: String!\n avatar: String\n bio: String\n posts: [Post!]!\n createdAt: String!\n }\n\n type Post {\n id: ID!\n title: String!\n content: String!\n excerpt: String\n author: User!\n tags: [String!]!\n published: Boolean!\n comments: [Comment!]!\n createdAt: String!\n updatedAt: String!\n }\n\n type Comment {\n id: ID!\n content: String!\n author: User!\n post: Post!\n parentComment: Comment\n replies: [Comment!]!\n createdAt: String!\n }\n\n type AuthPayload {\n token: String!\n user: User!\n }\n\n type Query {\n # استعلامات المستخدم\n me: User\n user(id: ID!): User\n users: [User!]!\n\n # استعلامات المنشورات\n post(id: ID!): Post\n posts(limit: Int, offset: Int): [Post!]!\n postsByUser(userId: ID!): [Post!]!\n searchPosts(query: String!): [Post!]!\n\n # استعلامات التعليقات\n comments(postId: ID!): [Comment!]!\n }\n\n type Mutation {\n # المصادقة\n register(username: String!, email: String!, password: String!, name: String!): AuthPayload!\n login(email: String!, password: String!): AuthPayload!\n\n # طفرات المستخدم\n updateProfile(name: String, avatar: String, bio: String): User!\n\n # طفرات المنشورات\n createPost(title: String!, content: String!, excerpt: String, tags: [String!]): Post!\n updatePost(id: ID!, title: String, content: String, excerpt: String, tags: [String!]): Post!\n deletePost(id: ID!): Boolean!\n publishPost(id: ID!): Post!\n\n # طفرات التعليقات\n createComment(postId: ID!, content: String!, parentCommentId: ID): Comment!\n deleteComment(id: ID!): Boolean!\n }\n`;\n\nmodule.exports = typeDefs;أفضل ممارسات تصميم المخطط: لاحظ كيف نستخدم الأنواع غير الفارغة (!) للحقول المطلوبة، وأنواع منفصلة لحمولات المصادقة، ونشمل العلاقات بين الأنواع. المخطط منظم حسب المجال (User, Post, Comment) للوضوح.
أدوات المصادقة
أنشئ وظائف مساعدة للمصادقة والتفويض:
// src/utils/auth.js\nconst jwt = require('jsonwebtoken');\nconst bcrypt = require('bcryptjs');\n\nconst JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';\n\n// توليد رمز JWT\nfunction generateToken(userId) {\n return jwt.sign({ userId }, JWT_SECRET, { expiresIn: '7d' });\n}\n\n// التحقق من رمز JWT\nfunction verifyToken(token) {\n try {\n return jwt.verify(token, JWT_SECRET);\n } catch (error) {\n return null;\n }\n}\n\n// تشفير كلمة المرور\nasync function hashPassword(password) {\n return bcrypt.hash(password, 10);\n}\n\n// مقارنة كلمة المرور\nasync function comparePassword(password, hashedPassword) {\n return bcrypt.compare(password, hashedPassword);\n}\n\nmodule.exports = {\n generateToken,\n verifyToken,\n hashPassword,\n comparePassword\n};إعداد السياق
قم بإعداد السياق لتضمين المستخدم المصادق عليه:
// src/context.js\nconst { verifyToken } = require('./utils/auth');\nconst User = require('./models/User');\n\nasync function context({ req }) {\n // الحصول على الرمز من العناوين\n const token = req.headers.authorization?.replace('Bearer ', '');\n \n if (!token) {\n return { user: null };\n }\n\n // التحقق من الرمز والحصول على المستخدم\n const decoded = verifyToken(token);\n \n if (!decoded) {\n return { user: null };\n }\n\n try {\n const user = await User.findById(decoded.userId);\n return { user };\n } catch (error) {\n return { user: null };\n }\n}\n\nmodule.exports = context;المحللات الأساسية
لنقم بإنشاء محللات المصادقة للبدء:
// src/schema/resolvers.js\nconst { AuthenticationError, UserInputError } = require('apollo-server');\nconst User = require('../models/User');\nconst Post = require('../models/Post');\nconst Comment = require('../models/Comment');\nconst { generateToken, hashPassword, comparePassword } = require('../utils/auth');\n\nconst resolvers = {\n Query: {\n me: async (_, __, { user }) => {\n if (!user) {\n throw new AuthenticationError('غير مصادق عليه');\n }\n return user;\n },\n\n user: async (_, { id }) => {\n return User.findById(id);\n },\n\n users: async () => {\n return User.find();\n }\n },\n\n Mutation: {\n register: async (_, { username, email, password, name }) => {\n // التحقق من وجود المستخدم بالفعل\n const existingUser = await User.findOne({\n $or: [{ email }, { username }]\n });\n\n if (existingUser) {\n throw new UserInputError('المستخدم موجود بالفعل');\n }\n\n // تشفير كلمة المرور\n const hashedPassword = await hashPassword(password);\n\n // إنشاء المستخدم\n const user = await User.create({\n username,\n email,\n password: hashedPassword,\n name\n });\n\n // توليد الرمز\n const token = generateToken(user.id);\n\n return { token, user };\n },\n\n login: async (_, { email, password }) => {\n // العثور على المستخدم\n const user = await User.findOne({ email });\n\n if (!user) {\n throw new AuthenticationError('بيانات اعتماد غير صالحة');\n }\n\n // التحقق من كلمة المرور\n const valid = await comparePassword(password, user.password);\n\n if (!valid) {\n throw new AuthenticationError('بيانات اعتماد غير صالحة');\n }\n\n // توليد الرمز\n const token = generateToken(user.id);\n\n return { token, user };\n }\n },\n\n User: {\n posts: async (user) => {\n return Post.find({ author: user.id });\n }\n }\n};\n\nmodule.exports = resolvers;نصيحة: المحللات منظمة حسب نوع العملية (Query, Mutation) وتتضمن محللات الحقول للبيانات ذات الصلة. هذا النهج المعياري يجعل الكود أسهل في الصيانة والاختبار.
تكوين البيئة
أنشئ ملف .env للتكوين:
# .env\nMONGODB_URI=mongodb://localhost:27017/graphql-blog\nJWT_SECRET=your-super-secret-jwt-key-change-in-production\nPORT=4000
تحذير: لا تقم أبداً بإرسال ملف .env إلى نظام التحكم في الإصدار. استخدم دائماً قيماً قوية وفريدة لـ JWT_SECRET في بيئات الإنتاج.
الخطوات التالية
في الدرس التالي، سنكمل المشروع من خلال:
- تنفيذ محللات المنشورات والتعليقات المتبقية
- إضافة إعداد الخادم مع Apollo Server
- تنفيذ الترقيم والتصفية
- إضافة التحقق من صحة الإدخال
- اختبار API الكامل
تمرين: قم بإعداد هيكل المشروع وتثبيت جميع التبعيات. أنشئ نماذج قاعدة البيانات وراجع تصميم المخطط. حاول التنبؤ بشكل المحللات المتبقية بناءً على المخطط الذي حددناه. في الدرس التالي، سننفذها معاً.