واجهات GraphQL

المقاييس المخصصة

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

إنشاء أنواع مقياس مخصصة

تمثل أنواع المقياس القيم الورقية البدائية في GraphQL. بينما يوفر GraphQL مقاييس مدمجة مثل String وInt وFloat وBoolean وID، يمكنك إنشاء مقاييس مخصصة لأنواع البيانات المتخصصة مثل DateTime وEmail وURL وJSON.

المقاييس المدمجة

يتضمن GraphQL خمسة أنواع مقياس افتراضية تشكل أساس مخططك.

المقاييس المدمجة:
scalar String   # تسلسل أحرف UTF-8
scalar Int      # عدد صحيح 32 بت مع إشارة
scalar Float    # قيمة نقطة عائمة مزدوجة الدقة مع إشارة
scalar Boolean  # صحيح أو خطأ
scalar ID       # معرف فريد (يتم تسلسله كسلسلة)

type User {
  id: ID!           # مقياس ID مدمج
  name: String!     # مقياس String مدمج
  age: Int          # مقياس Int مدمج
  rating: Float     # مقياس Float مدمج
  isActive: Boolean # مقياس Boolean مدمج
}

متى يتم إنشاء مقاييس مخصصة

المقاييس المخصصة مفيدة عندما تحتاج إلى منطق محدد للتحقق من الصحة أو التسلسل أو التحليل لنوع بيانات يُستخدم عبر مخططك.

حالات الاستخدام للمقاييس المخصصة:
  • DateTime: سلاسل التاريخ/الوقت بتنسيق ISO 8601 مع دعم المنطقة الزمنية
  • Email: عناوين البريد الإلكتروني المصادق عليها
  • URL: عناوين URL المصادق عليها مع البروتوكول
  • JSON: كائنات JSON عشوائية
  • PhoneNumber: أرقام هواتف مصادق عليها
  • Currency: قيم نقدية بدقة
  • UUID: معرفات فريدة عالميًا

الإعلان عن المقاييس المخصصة

يتم الإعلان عن المقاييس المخصصة في مخططك تمامًا مثل الأنواع المدمجة.

إعلان المخطط:
scalar DateTime
scalar Email
scalar URL
scalar JSON
scalar PhoneNumber

type User {
  id: ID!
  name: String!
  email: Email!           # مقياس Email مخصص
  website: URL            # مقياس URL مخصص
  phoneNumber: PhoneNumber
  createdAt: DateTime!    # مقياس DateTime مخصص
  metadata: JSON          # مقياس JSON مخصص
}

type Post {
  id: ID!
  title: String!
  publishedAt: DateTime
  author: User!
}

تنفيذ مقياس DateTime

يتعامل مقياس DateTime مع تحليل التاريخ/الوقت والتحقق من الصحة والتسلسل.

تنفيذ مقياس DateTime (Node.js):
import { GraphQLScalarType, Kind } from 'graphql';

const DateTimeScalar = new GraphQLScalarType({
  name: 'DateTime',
  description: 'سلسلة تاريخ-وقت ISO 8601 (مثل 2024-01-15T10:30:00Z)',

  // التسلسل إلى العميل (DB -> العميل)
  serialize(value) {
    if (value instanceof Date) {
      return value.toISOString();
    }
    if (typeof value === 'string') {
      return new Date(value).toISOString();
    }
    throw new Error('يجب أن يكون DateTime كائن Date أو سلسلة ISO');
  },

  // التحليل من إدخال العميل (العميل -> الخادم)
  parseValue(value) {
    if (typeof value === 'string') {
      const date = new Date(value);
      if (isNaN(date.getTime())) {
        throw new Error('تنسيق DateTime غير صالح');
      }
      return date;
    }
    throw new Error('يجب أن يكون DateTime سلسلة');
  },

  // التحليل من استعلام حرفي
  parseLiteral(ast) {
    if (ast.kind === Kind.STRING) {
      const date = new Date(ast.value);
      if (isNaN(date.getTime())) {
        throw new Error('تنسيق DateTime غير صالح');
      }
      return date;
    }
    return null;
  }
});

// تسجيل المحلل
const resolvers = {
  DateTime: DateTimeScalar
};
شرح الطرق الثلاث:
  • serialize: يحول القيمة الداخلية إلى تنسيق يواجه العميل (استجابة)
  • parseValue: يحلل إدخال متغير من العميل (متغيرات في التحويرات)
  • parseLiteral: يحلل الاستعلامات الحرفية المضمنة (قيم مشفرة في الاستعلامات)

تنفيذ مقياس Email

يتحقق مقياس Email من صحة عناوين البريد الإلكتروني باستخدام أنماط regex.

تنفيذ مقياس Email:
import { GraphQLScalarType, Kind } from 'graphql';

const EMAIL_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;

const EmailScalar = new GraphQLScalarType({
  name: 'Email',
  description: 'عنوان بريد إلكتروني صالح',

  serialize(value) {
    if (typeof value !== 'string') {
      throw new Error('يجب أن يكون Email سلسلة');
    }
    if (!EMAIL_REGEX.test(value)) {
      throw new Error('تنسيق بريد إلكتروني غير صالح');
    }
    return value.toLowerCase();
  },

  parseValue(value) {
    if (typeof value !== 'string') {
      throw new Error('يجب أن يكون Email سلسلة');
    }
    if (!EMAIL_REGEX.test(value)) {
      throw new Error('تنسيق بريد إلكتروني غير صالح');
    }
    return value.toLowerCase();
  },

  parseLiteral(ast) {
    if (ast.kind === Kind.STRING) {
      if (!EMAIL_REGEX.test(ast.value)) {
        throw new Error('تنسيق بريد إلكتروني غير صالح');
      }
      return ast.value.toLowerCase();
    }
    return null;
  }
});

const resolvers = {
  Email: EmailScalar
};

تنفيذ مقياس URL

تنفيذ مقياس URL:
import { GraphQLScalarType, Kind } from 'graphql';

const URLScalar = new GraphQLScalarType({
  name: 'URL',
  description: 'عنوان URL صالح مع البروتوكول',

  serialize(value) {
    try {
      const url = new URL(value);
      return url.href;
    } catch {
      throw new Error('تنسيق URL غير صالح');
    }
  },

  parseValue(value) {
    try {
      const url = new URL(value);
      // التأكد من أن البروتوكول هو http أو https
      if (!['http:', 'https:'].includes(url.protocol)) {
        throw new Error('يجب أن يستخدم URL بروتوكول http أو https');
      }
      return url.href;
    } catch {
      throw new Error('تنسيق URL غير صالح');
    }
  },

  parseLiteral(ast) {
    if (ast.kind === Kind.STRING) {
      try {
        const url = new URL(ast.value);
        if (!['http:', 'https:'].includes(url.protocol)) {
          throw new Error('يجب أن يستخدم URL بروتوكول http أو https');
        }
        return url.href;
      } catch {
        throw new Error('تنسيق URL غير صالح');
      }
    }
    return null;
  }
});

تنفيذ مقياس JSON

يسمح مقياس JSON بكائنات JSON عشوائية كقيم حقول.

تنفيذ مقياس JSON:
import { GraphQLScalarType, Kind } from 'graphql';

const JSONScalar = new GraphQLScalarType({
  name: 'JSON',
  description: 'قيمة JSON عشوائية',

  serialize(value) {
    return value; // بالفعل كائن JS
  },

  parseValue(value) {
    return value; // العميل يرسل JSON
  },

  parseLiteral(ast) {
    switch (ast.kind) {
      case Kind.STRING:
      case Kind.BOOLEAN:
        return ast.value;
      case Kind.INT:
      case Kind.FLOAT:
        return parseFloat(ast.value);
      case Kind.OBJECT:
        return parseObject(ast);
      case Kind.LIST:
        return ast.values.map(n => JSONScalar.parseLiteral(n));
      default:
        return null;
    }
  }
});

function parseObject(ast) {
  const value = Object.create(null);
  ast.fields.forEach(field => {
    value[field.name.value] = JSONScalar.parseLiteral(field.value);
  });
  return value;
}

استخدام مكتبة graphql-scalars

بدلاً من تنفيذ المقاييس من الصفر، استخدم مكتبة graphql-scalars التي توفر تطبيقات جاهزة للإنتاج.

التثبيت والاستخدام graphql-scalars:
# التثبيت
npm install graphql-scalars

# الاستخدام
import {
  DateTimeResolver,
  EmailAddressResolver,
  URLResolver,
  JSONResolver,
  PhoneNumberResolver,
  UUIDResolver
} from 'graphql-scalars';

const resolvers = {
  DateTime: DateTimeResolver,
  Email: EmailAddressResolver,
  URL: URLResolver,
  JSON: JSONResolver,
  PhoneNumber: PhoneNumberResolver,
  UUID: UUIDResolver
};
المقاييس المتاحة في graphql-scalars:
# المقاييس الشائعة
DateTime, Date, Time, Duration
EmailAddress, PhoneNumber, URL
UUID, GUID, HexColorCode, RGB, RGBA
JSON, JSONObject
BigInt, Long, Byte
PositiveInt, NonNegativeInt, NegativeInt
PositiveFloat, NonNegativeFloat, NegativeFloat

# متقدم
MAC, IPv4, IPv6
Port, Latitude, Longitude
PostalCode, Currency
SafeInt, Void

أفضل ممارسات التحقق من الصحة

مثال شامل للتحقق من الصحة:
import { GraphQLScalarType, Kind } from 'graphql';

const PhoneNumberScalar = new GraphQLScalarType({
  name: 'PhoneNumber',
  description: 'رقم هاتف دولي (تنسيق E.164)',

  serialize(value) {
    return validateAndFormat(value);
  },

  parseValue(value) {
    return validateAndFormat(value);
  },

  parseLiteral(ast) {
    if (ast.kind === Kind.STRING) {
      return validateAndFormat(ast.value);
    }
    return null;
  }
});

function validateAndFormat(value) {
  if (typeof value !== 'string') {
    throw new Error('يجب أن يكون PhoneNumber سلسلة');
  }

  // إزالة المسافات والتنسيق
  const cleaned = value.replace(/[\s()-]/g, '');

  // تنسيق E.164: +[رمز البلد][الرقم]
  const e164Regex = /^\+[1-9]\d{1,14}$/;

  if (!e164Regex.test(cleaned)) {
    throw new Error(
      'يجب أن يكون PhoneNumber بتنسيق E.164 (مثل +14155552671)'
    );
  }

  return cleaned;
}
تحذير: المقاييس المخصصة تتحقق من الصحة في كل إدخال/إخراج. منطق التحقق المعقد يمكن أن يؤثر على الأداء. احتفظ بالتحقق بكفاءة وفكر في تخزين نتائج التحقق مؤقتًا للقيم المستخدمة بشكل متكرر.
تمرين:
  1. أنشئ مقياس HexColor يتحقق من صحة رموز الألوان السداسية (#RGB أو #RRGGBB)
  2. نفذ مقياس Currency يخزن المبالغ بدقة عشرية 2
  3. قم ببناء مقياس Markdown يتحقق من صحة بناء جملة Markdown
  4. أنشئ مقياس Latitude و Longitude مع التحقق من النطاق (-90 إلى 90، -180 إلى 180)
  5. استخدم مكتبة graphql-scalars لإضافة مقاييس DateTime وEmail وURL إلى مخططك