واجهات GraphQL

التوجيهات المخصصة

18 دقيقة الدرس 17 من 35

توسيع 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
}
ملاحظة: تتضمن مواقع التوجيه: FIELD_DEFINITION، OBJECT، INTERFACE، UNION، ENUM، SCALAR، INPUT_OBJECT، INPUT_FIELD_DEFINITION، ARGUMENT_DEFINITION، SCHEMA، والمزيد.

تنفيذ توجيه @auth

يقيد توجيه @auth الوصول إلى الحقول بناءً على أدوار المستخدم.

تنفيذ توجيه المصادقة (Apollo):
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 يوفر المصادقة وتحديد المعدل والتخزين المؤقت في حقل واحد.
تحذير: يهم ترتيب تنفيذ التوجيه عند السلسلة. عمومًا: التحقق من الصحة ← المصادقة ← تحديد المعدل ← التخزين المؤقت ← التحويل. اختبر مجموعات التوجيه بدقة.
تمرين:
  1. أنشئ توجيه @log يسجل كل وصول إلى الحقل مع الطابع الزمني ومعلومات المستخدم
  2. نفذ توجيه @sanitize الذي يزيل علامات HTML من إدخالات السلسلة
  3. قم ببناء توجيه @cost الذي يتتبع تعقيد الاستعلام ويرفض الاستعلامات المكلفة
  4. أنشئ توجيه @mask الذي يخفي جزئيًا البيانات الحساسة (مثل أرقام بطاقات الائتمان)
  5. ادمج @auth و @rateLimit و @cache في حقل واحد واختبر السلوك