التوجيهات المخصصة
توسيع GraphQL بالتوجيهات المخصصة
توفر التوجيهات طريقة لإضافة منطق مخصص إلى مخطط GraphQL الخاص بك دون تعديل الأنواع أو الحقول. إنها تمكن الميزات القابلة لإعادة الاستخدام مثل التفويض والتخزين المؤقت والتنسيق والتحقق من الصحة.
ما هي توجيهات المخطط؟
توجيهات المخطط هي تعليقات توضيحية مسبوقة بـ @ تعدل سلوك الحقول أو الأنواع أو الوسائط. يتضمن GraphQL توجيهات مدمجة مثل @deprecated و @skip و @include.
type User {
id: ID!
name: String!
email: String! @deprecated(reason: "استخدم contactEmail بدلاً من ذلك")
contactEmail: String!
posts: [Post!]!
}
# يمكن للعميل تخطي الحقول بشكل مشروط
query GetUser($includeEmail: Boolean!) {
user(id: "1") {
id
name
email @include(if: $includeEmail)
posts @skip(if: false)
}
}
الإعلان عن التوجيهات المخصصة
يتم تعريف التوجيهات المخصصة في مخططك باستخدام الكلمة الأساسية directive. تحدد أين يمكن تطبيقها باستخدام مواقع التوجيه.
directive @auth(
requires: Role = USER
) on OBJECT | FIELD_DEFINITION
directive @rateLimit(
limit: Int = 10,
window: Int = 60
) on FIELD_DEFINITION
directive @cache(
maxAge: Int = 3600
) on FIELD_DEFINITION
directive @upper on FIELD_DEFINITION
directive @length(
min: Int,
max: Int
) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
enum Role {
USER
ADMIN
MODERATOR
}
تنفيذ توجيه @auth
يقيد توجيه @auth الوصول إلى الحقول بناءً على أدوار المستخدم.
import { SchemaDirectiveVisitor } from '@graphql-tools/utils';
import { defaultFieldResolver } from 'graphql';
class AuthDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { requires } = this.args;
const { resolve = defaultFieldResolver } = field;
field.resolve = async function (source, args, context, info) {
const user = context.user;
if (!user) {
throw new Error('المصادقة مطلوبة');
}
if (requires && user.role !== requires) {
throw new Error(`الدور ${requires} مطلوب`);
}
return resolve.call(this, source, args, context, info);
};
}
}
// تطبيق على المخطط
const schema = makeExecutableSchema({
typeDefs,
resolvers,
schemaDirectives: {
auth: AuthDirective
}
});
استخدام @auth في المخطط
type Query {
# عام - لا يتطلب مصادقة
posts: [Post!]!
# يتطلب المصادقة
me: User! @auth
# يتطلب دور ADMIN
users: [User!]! @auth(requires: ADMIN)
allOrders: [Order!]! @auth(requires: ADMIN)
}
type Mutation {
# أي مستخدم مصادق عليه
createPost(title: String!, content: String!): Post! @auth
# المسؤولون فقط
deleteUser(id: ID!): Boolean! @auth(requires: ADMIN)
# المشرفون أو المسؤولون فقط
approvePost(id: ID!): Post! @auth(requires: MODERATOR)
}
type User @auth {
id: ID!
email: String!
# حتى إذا تم الاستعلام عن نوع المستخدم، يحتاج الحقل الحساس إلى حماية إضافية
ssn: String! @auth(requires: ADMIN)
}
تنفيذ توجيه @rateLimit
يمنع تحديد المعدل إساءة الاستخدام عن طريق الحد من عدد الطلبات لكل نافذة زمنية.
import { SchemaDirectiveVisitor } from '@graphql-tools/utils';
import { defaultFieldResolver } from 'graphql';
class RateLimitDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { limit, window } = this.args;
const { resolve = defaultFieldResolver } = field;
const requests = new Map();
field.resolve = async function (source, args, context, info) {
const userId = context.user?.id || context.ip;
const key = `${userId}:${info.parentType}.${info.fieldName}`;
const now = Date.now();
// الحصول على سجل الطلبات
let history = requests.get(key) || [];
// إزالة الطلبات القديمة خارج النافذة
history = history.filter(time => now - time < window * 1000);
// التحقق من الحد
if (history.length >= limit) {
throw new Error(
`تم تجاوز حد المعدل. الحد الأقصى ${limit} طلبات لكل ${window} ثانية`
);
}
// إضافة الطلب الحالي
history.push(now);
requests.set(key, history);
return resolve.call(this, source, args, context, info);
};
}
}
type Mutation {
# بحد أقصى 5 منشورات في الدقيقة
createPost(title: String!, content: String!): Post!
@auth
@rateLimit(limit: 5, window: 60)
# بحد أقصى 3 إعادات تعيين كلمة المرور في الساعة
resetPassword(email: String!): Boolean!
@rateLimit(limit: 3, window: 3600)
}
تنفيذ توجيه @cache
توجيهات التخزين المؤقت تحسن الأداء عن طريق تخزين الحسابات المكلفة.
class CacheDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { maxAge } = this.args;
const { resolve = defaultFieldResolver } = field;
const cache = new Map();
field.resolve = async function (source, args, context, info) {
const key = JSON.stringify({
field: info.fieldName,
args,
sourceId: source?.id
});
// التحقق من ذاكرة التخزين المؤقت
const cached = cache.get(key);
if (cached && Date.now() - cached.time < maxAge * 1000) {
return cached.value;
}
// تنفيذ المحلل
const result = await resolve.call(this, source, args, context, info);
// التخزين في ذاكرة التخزين المؤقت
cache.set(key, { value: result, time: Date.now() });
return result;
};
}
}
type Query {
# التخزين المؤقت لمدة ساعة واحدة
popularPosts: [Post!]! @cache(maxAge: 3600)
# التخزين المؤقت لمدة 5 دقائق
stats: SiteStats! @cache(maxAge: 300)
}
type User {
id: ID!
name: String!
# التخزين المؤقت لمنشورات المستخدم لمدة 10 دقائق
posts: [Post!]! @cache(maxAge: 600)
}
توجيهات المحول
توجيهات المحول تعدل قيم الحقول قبل إرجاعها إلى العميل.
class UpperDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field;
field.resolve = async function (...args) {
const result = await resolve.apply(this, args);
if (typeof result === 'string') {
return result.toUpperCase();
}
return result;
};
}
}
# الاستخدام
type User {
name: String! @upper # "john doe" يصبح "JOHN DOE"
email: String!
}
توجيهات التحقق من الصحة
توجيهات التحقق من الصحة تفرض قيودًا على وسائط الإدخال.
class LengthDirective extends SchemaDirectiveVisitor {
visitArgumentDefinition(argument) {
const { min, max } = this.args;
// التفاف محلل الحقل
const field = argument.astNode.parent;
const { resolve = defaultFieldResolver } = field;
field.resolve = async function (source, args, context, info) {
const value = args[argument.name];
if (min && value.length < min) {
throw new Error(
`${argument.name} يجب أن يكون على الأقل ${min} حرفًا`
);
}
if (max && value.length > max) {
throw new Error(
`${argument.name} يجب أن يكون بحد أقصى ${max} حرفًا`
);
}
return resolve.call(this, source, args, context, info);
};
}
}
# الاستخدام
type Mutation {
createUser(
username: String! @length(min: 3, max: 20)
password: String! @length(min: 8, max: 100)
): User!
}
@auth @rateLimit @cache يوفر المصادقة وتحديد المعدل والتخزين المؤقت في حقل واحد.
- أنشئ توجيه
@logيسجل كل وصول إلى الحقل مع الطابع الزمني ومعلومات المستخدم - نفذ توجيه
@sanitizeالذي يزيل علامات HTML من إدخالات السلسلة - قم ببناء توجيه
@costالذي يتتبع تعقيد الاستعلام ويرفض الاستعلامات المكلفة - أنشئ توجيه
@maskالذي يخفي جزئيًا البيانات الحساسة (مثل أرقام بطاقات الائتمان) - ادمج
@authو@rateLimitو@cacheفي حقل واحد واختبر السلوك