واجهات GraphQL

التحقق من صحة المدخلات في GraphQL

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

استراتيجيات التحقق من صحة المدخلات

التحقق من صحة المدخلات أمر بالغ الأهمية لسلامة البيانات والأمان في واجهات GraphQL API. يوفر GraphQL التحقق من صحة النوع تلقائيًا، ولكن يجب تنفيذ التحقق من منطق الأعمال في المحللات.

التحقق من صحة مستوى النوع

يتحقق GraphQL تلقائيًا من أن المدخلات تتطابق مع أنواع المخطط:

type Mutation { createUser(input: CreateUserInput!): User! } input CreateUserInput { username: String! email: String! age: Int role: UserRole! } enum UserRole { USER ADMIN MODERATOR } # يتحقق GraphQL تلقائيًا من: # - username هو سلسلة نصية # - email هو سلسلة نصية # - age هو عدد صحيح (إذا تم توفيره) # - role هو أحد قيم enum # - الحقول المطلوبة موجودة
ملاحظة: يحدث التحقق من صحة النوع قبل تشغيل المحللات. ترجع الأنواع غير الصالحة أخطاء فورًا دون تنفيذ منطق المحلل.

التحقق من صحة Scalar المخصصة

إنشاء Scalars مخصصة لمتطلبات التحقق المحددة:

const { GraphQLScalarType, GraphQLError } = require('graphql'); // Email scalar const EmailScalar = new GraphQLScalarType({ name: 'Email', description: 'Valid email address', serialize(value) { return value; // إرسال إلى العميل كسلسلة نصية }, parseValue(value) { // التحقق من صحة القيمة الواردة من المتغيرات if (typeof value !== 'string') { throw new GraphQLError('Email must be a string'); } const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(value)) { throw new GraphQLError('Invalid email format'); } return value.toLowerCase(); }, parseLiteral(ast) { // التحقق من صحة القيم المضمنة في الاستعلام if (ast.kind !== 'StringValue') { throw new GraphQLError('Email must be a string'); } const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(ast.value)) { throw new GraphQLError('Invalid email format'); } return ast.value.toLowerCase(); } }); // URL scalar const URLScalar = new GraphQLScalarType({ name: 'URL', description: 'Valid URL', parseValue(value) { try { new URL(value); return value; } catch (error) { throw new GraphQLError('Invalid URL format'); } } }); // استخدام في المخطط const typeDefs = gql` scalar Email scalar URL input CreateUserInput { username: String! email: Email! website: URL } `; const resolvers = { Email: EmailScalar, URL: URLScalar };

تكامل مكتبات التحقق

استخدم مكتبات مثل Joi أو Yup أو validator.js للتحقق المعقد:

const Joi = require('joi'); const { ValidationError } = require('./errors/CustomErrors'); // تعريف مخططات التحقق const createUserSchema = Joi.object({ username: Joi.string() .alphanum() .min(3) .max(30) .required() .messages({ 'string.alphanum': 'Username must contain only letters and numbers', 'string.min': 'Username must be at least 3 characters', 'string.max': 'Username cannot exceed 30 characters' }), email: Joi.string() .email() .required() .messages({ 'string.email': 'Please provide a valid email address' }), password: Joi.string() .min(8) .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/) .required() .messages({ 'string.min': 'Password must be at least 8 characters', 'string.pattern.base': 'Password must contain uppercase, lowercase, and number' }), age: Joi.number() .integer() .min(13) .max(120) .messages({ 'number.min': 'You must be at least 13 years old', 'number.max': 'Invalid age' }), bio: Joi.string() .max(500) .messages({ 'string.max': 'Bio cannot exceed 500 characters' }) }); // مساعد التحقق function validateInput(schema, input) { const { error, value } = schema.validate(input, { abortEarly: false }); if (error) { const fields = {}; error.details.forEach(detail => { fields[detail.path[0]] = detail.message; }); throw new ValidationError('Validation failed', fields); } return value; } // استخدام في المحلل const resolvers = { Mutation: { createUser: async (_, { input }) => { // التحقق من صحة المدخلات const validatedInput = validateInput(createUserSchema, input); // التحقق من التكرارات const existingUser = await User.findOne({ $or: [ { username: validatedInput.username }, { email: validatedInput.email } ] }); if (existingUser) { if (existingUser.username === validatedInput.username) { throw new ValidationError('Username already taken', { username: 'This username is already in use' }); } if (existingUser.email === validatedInput.email) { throw new ValidationError('Email already registered', { email: 'This email is already registered' }); } } // إنشاء المستخدم return await User.create(validatedInput); } } };
نصيحة: اضبط abortEarly: false في التحقق من Joi لجمع جميع أخطاء التحقق مرة واحدة، مما يوفر تجربة مستخدم أفضل.

التحقق على مستوى الحقل

التحقق من صحة الحقول الفردية باستخدام دوال المدقق القابلة لإعادة الاستخدام:

// validators/fieldValidators.js const validators = { username: (value) => { if (!value || value.length < 3) { return 'Username must be at least 3 characters'; } if (!/^[a-zA-Z0-9_]+$/.test(value)) { return 'Username can only contain letters, numbers, and underscores'; } if (value.length > 30) { return 'Username cannot exceed 30 characters'; } return null; }, email: (value) => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(value)) { return 'Invalid email format'; } return null; }, password: (value) => { if (value.length < 8) { return 'Password must be at least 8 characters'; } if (!/(?=.*[a-z])/.test(value)) { return 'Password must contain a lowercase letter'; } if (!/(?=.*[A-Z])/.test(value)) { return 'Password must contain an uppercase letter'; } if (!/(?=.*\d)/.test(value)) { return 'Password must contain a number'; } return null; }, phone: (value) => { const phoneRegex = /^\+?[1-9]\d{1,14}$/; if (!phoneRegex.test(value)) { return 'Invalid phone number format'; } return null; }, url: (value) => { try { new URL(value); return null; } catch { return 'Invalid URL format'; } } }; // التحقق من صحة حقول متعددة function validateFields(input, fieldValidators) { const errors = {}; Object.keys(fieldValidators).forEach(field => { if (input[field] !== undefined) { const error = fieldValidators[field](input[field]); if (error) { errors[field] = error; } } }); if (Object.keys(errors).length > 0) { throw new ValidationError('Validation failed', errors); } } module.exports = { validators, validateFields };

أنماط التحقق من النماذج

const { validators, validateFields } = require('./validators/fieldValidators'); const resolvers = { Mutation: { updateProfile: async (_, { input }, context) => { if (!context.user) { throw new AuthenticationError('Authentication required'); } // التحقق من الحقول المقدمة فقط const validationRules = {}; if (input.username) validationRules.username = validators.username; if (input.email) validationRules.email = validators.email; if (input.website) validationRules.url = validators.url; if (input.phone) validationRules.phone = validators.phone; validateFields(input, validationRules); // التحقق الإضافي من منطق الأعمال if (input.username) { const existing = await User.findOne({ username: input.username, _id: { $ne: context.user.id } }); if (existing) { throw new ValidationError('Username taken', { username: 'This username is already in use' }); } } // تحديث المستخدم return await User.findByIdAndUpdate( context.user.id, input, { new: true, runValidators: true } ); } } };
تحذير: تحقق دائمًا من صحة البيانات على جانب الخادم، حتى لو كان التحقق من جانب العميل موجودًا. لا تثق أبدًا في مدخلات العميل.

التحقق غير المتزامن

معالجة التحقق الذي يتطلب استدعاءات قاعدة البيانات أو واجهة برمجة التطبيقات الخارجية:

const asyncValidators = { uniqueUsername: async (username) => { const exists = await User.findOne({ username }); if (exists) { throw new ValidationError('Username unavailable', { username: 'This username is already taken' }); } }, uniqueEmail: async (email) => { const exists = await User.findOne({ email }); if (exists) { throw new ValidationError('Email registered', { email: 'This email is already registered' }); } }, validCouponCode: async (code) => { const coupon = await Coupon.findOne({ code, isActive: true }); if (!coupon) { throw new ValidationError('Invalid coupon', { couponCode: 'This coupon code is invalid or expired' }); } if (coupon.usageCount >= coupon.maxUsage) { throw new ValidationError('Coupon limit reached', { couponCode: 'This coupon has reached its usage limit' }); } return coupon; } }; // استخدام في المحلل const resolvers = { Mutation: { createUser: async (_, { input }) => { // التحقق المتزامن أولاً validateFields(input, { username: validators.username, email: validators.email, password: validators.password }); // التحقق غير المتزامن await asyncValidators.uniqueUsername(input.username); await asyncValidators.uniqueEmail(input.email); // إذا نجح التحقق من صحة الكل، أنشئ المستخدم return await User.create(input); } } };
تمرين:
  1. أنشئ Scalar مخصص PhoneNumber يتحقق من صحة أرقام الهواتف الدولية
  2. نفذ مخطط Joi لمنشور مدونة بعنوان (3-100 حرف)، محتوى (مطلوب)، علامات (بحد أقصى 5)، وفئة
  3. أنشئ مدققات غير متزامنة للتحقق مما إذا كان slug فريدًا وما إذا كانت الفئة موجودة
  4. أنشئ mutation للتسجيل مع تحقق شامل بما في ذلك قوة كلمة المرور والتحقق من البريد الإلكتروني وتوفر اسم المستخدم
  5. نفذ التحقق على مستوى الحقل الذي يرجع جميع الأخطاء مرة واحدة لتجربة مستخدم أفضل