مكتبة عميل Azure Cosmos DB ل JavaScript - الإصدار 4.0.0

/TypeScript

أحدث شارة npmBuild Status

Azure Cosmos DB هي قاعدة بيانات متعددة النماذج موزعة عالميًا تدعم الوثائق والقيم المفتاحية والعمود العريض وقواعد البيانات الرسومية. هذه الحزمة مخصصة لتطبيقات JavaScript/TypeScript للتفاعل مع قواعد بيانات واجهة برمجة تطبيقات SQL ومستندات JSON التي تحتوي عليها:

  • إنشاء قواعد بيانات قاعدة بيانات Cosmos وتعديل إعداداتها
  • إنشاء حاويات وتعديلها لتخزين مجموعات مستندات JSON
  • إنشاء العناصر (مستندات JSON) وقراءتها وتحديثها وحذفها في الحاويات الخاصة بك
  • الاستعلام عن المستندات في قاعدة البيانات باستخدام بناء جملة يشبه SQL

الروابط الرئيسية:

الشروع في العمل

المتطلبات الأساسية

حساب Azure Subscription وCosmos DB SQL API

يجب أن يكون لديك اشتراك Azureوحساب Cosmos DB (SQL API) لاستخدام هذه الحزمة.

إذا كنت بحاجة إلى حساب واجهة برمجة تطبيقات Cosmos DB SQL، فيمكنك استخدام Azure Cloud Shell لإنشاء حساب باستخدام أمر Azure CLI هذا:

az cosmosdb create --resource-group <resource-group-name> --name <cosmos-database-account-name>

أو يمكنك إنشاء حساب في مدخل Microsoft Azure

NodeJS

يتم توزيع هذه الحزمة عبر npm والتي تأتي مثبتة مسبقًا مع NodeJS. يجب أن تستخدم Node v10 أو أعلى.

كورس

تحتاج إلى إعداد قواعد مشاركة الموارد عبر الأصل (CORS) لحساب قاعدة بيانات Cosmos الخاص بك إذا كنت بحاجة إلى التطوير للمتصفحات. اتبع الإرشادات الواردة في المستند المرتبط لإنشاء قواعد CORS جديدة لقاعدة بيانات Cosmos الخاصة بك.

تثبيت هذه الحزمة

npm install @azure/cosmos

الحصول على بيانات اعتماد الحساب

ستحتاج إلى نقطة نهايةومفتاح حساب Cosmos DB. يمكنك العثور عليها في مدخل Azure أو استخدام مقتطف Azure CLI أدناه. يُنسق المقتطف لـBash shell.

az cosmosdb show --resource-group <your-resource-group> --name <your-account-name> --query documentEndpoint --output tsv
az cosmosdb keys list --resource-group <your-resource-group> --name <your-account-name> --query primaryMasterKey --output tsv

قم بإنشاء مثيل لـ CosmosClient

يبدأ التفاعل مع قاعدة بيانات Cosmos بمثيل من فئة CosmosClient

const { CosmosClient } = require("@azure/cosmos");

const endpoint = "https://your-account.documents.azure.com";
const key = "<database account masterkey>";
const client = new CosmosClient({ endpoint, key });

async function main() {
  // The rest of the README samples are designed to be pasted into this function body
}

main().catch((error) => {
  console.error(error);
});

من أجل البساطة، قمنا بتضمين key وendpoint مباشرة في التعليمات البرمجية ولكن من المحتمل أن ترغب في تحميلها من ملف غير موجود في التحكم في المصدر باستخدام مشروع مثل dotenv أو التحميل من متغيرات البيئة

في بيئات الإنتاج، يجب تخزين أسرار مثل المفاتيح في Azure Key Vault

المفاهيم الرئيسية

بمجرد تهيئة CosmosClient، يمكنك التفاعل مع أنواع الموارد الأساسية في قاعدة بيانات Cosmos:

  • قاعدة البيانات: حساب Cosmos DB يمكن أن يحتوي على قواعد بيانات متعددة. عندما تنشئ قاعدة بيانات فأنت تحدد واجهة برمجة التطبيقات التي تريد استخدامها عندما تتعامل مع هذه الوثائق: SQL, MongoDB, Gremlin, Cassandra, أو Azure Table. استخدم كائن قاعدة البيانات لإدارة حاوياته.

  • حاوية: الحاوية هي مجموعة من وثائق JSON. إنشاء (إدراج) وقراءة وتحديث وحذف العناصر في حاوية باستخدام أساليب على كائن الحاوية.

  • العنصر: العنصر هو مستند JSON مخزن في حاوية. يجب أن يتضمن كل عنصر مفتاح id بقيمة تحدد العنصر داخل الحاوية بشكل فريد. إذا لم تقم بتوفير id، فستقوم SDK بإنشاء واحد تلقائيًا.

لمزيد من المعلومات حول هذه الموارد، راجع استخدام قواعد بيانات Azure Cosmos والحاويات والعناصر.

أمثلة

توفر الأقسام التالية العديد من مقتطفات التعليمات البرمجية التي تغطي بعض مهام قاعدة بيانات Cosmos الأكثر شيوعًا، بما في ذلك:

إنشاء قاعدة بيانات

بعد مصادقة CosmosClient، يمكنك العمل مع أي مورد في الحساب. تنشئ القصاصة البرمجية أدناه قاعدة بيانات واجهة برمجة تطبيقات NOSQL.

const { database } = await client.databases.createIfNotExists({ id: "Test Database" });
console.log(database.id);

إنشاء حاوية

ينشئ هذا المثال حاوية بإعدادات افتراضية

const { container } = await database.containers.createIfNotExists({ id: "Test Database" });
console.log(container.id);

استخدام مفاتيح القسم

يوضح هذا المثال أنواعا مختلفة من مفاتيح الأقسام المدعومة.

await container.item("id", "1").read();        // string type
await container.item("id", 2).read();          // number type
await container.item("id", true).read();       // boolean type
await container.item("id", {}).read();         // None type
await container.item("id", undefined).read();  // None type
await container.item("id", null).read();       // null type

إذا كان مفتاح القسم يتكون من قيمة واحدة، يمكن توفيره إما كقيمة حرفية، أو صفيف.

await container.item("id", "1").read();
await container.item("id", ["1"]).read();

إذا كان مفتاح القسم يتكون من أكثر من قيمة واحدة، فيجب توفيره كصفيف.

await container.item("id", ["a", "b"]).read();
await container.item("id", ["a", 2]).read();
await container.item("id", [{}, {}]).read();
await container.item("id", ["a", {}]).read();
await container.item("id", [2, null]).read();

إدراج العناصر

لإدراج عناصر في حاوية، مرر كائنًا يحتوي على بياناتك إلى Items.upsert. تتطلب خدمة Azure Cosmos DB أن يكون id لكل عنصر مفتاح. إذا لم تقم بتوفير واحد، فستقوم SDK بإنشاء id تلقائيًا.

يدرج هذا المثال عدة عناصر في الحاوية

const cities = [
  { id: "1", name: "Olympia", state: "WA", isCapitol: true },
  { id: "2", name: "Redmond", state: "WA", isCapitol: false },
  { id: "3", name: "Chicago", state: "IL", isCapitol: false }
];
for (const city of cities) {
  await container.items.create(city);
}

قراءة عنصر

لقراءة عنصر واحد من حاوية، استخدم Item.read. هذه عملية أقل تكلفة من استخدام SQL للاستعلام عن طريق id.

await container.item("1", "1").read();

CRUD على الحاوية بمفتاح قسم هرمي

إنشاء حاوية بمفتاح قسم هرمي

const containerDefinition = {
  id: "Test Database",
  partitionKey: {
    paths: ["/name", "/address/zip"],
    version: PartitionKeyDefinitionVersion.V2,
    kind: PartitionKeyKind.MultiHash,
  },
}
const { container } = await database.containers.createIfNotExists(containerDefinition);
console.log(container.id);

إدراج عنصر بمفتاح قسم هرمي معرف على أنه - ["/name", "/address/zip"]

const item = {
  id: 1,
  name: 'foo',
  address: {
    zip: 100
  },
  active: true
}
await container.items.create(item);

لقراءة عنصر واحد من حاوية مع تعريف مفتاح القسم الهرمي على أنه - ["/name", "/address/zip"],

await container.item("1", ["foo", 100]).read();

الاستعلام عن عنصر بمفتاح قسم هرمي مع تعريف مفتاح القسم الهرمي على أنه - ["/name", "/address/zip"],

const { resources } = await container.items
  .query("SELECT * from c WHERE c.active = true", {
          partitionKey: ["foo", 100],
        })
  .fetchAll();
for (const item of resources) {
  console.log(`${item.name}, ${item.address.zip} `);
}

حذف عنصر

لحذف عناصر من حاوية، استخدم Item.delete.

// Delete the first item returned by the query above
await container.item("1").delete();

الاستعلام عن قاعدة البيانات

تدعم قاعدة بيانات Cosmos DB SQL API الاستعلام عن العناصر الموجودة في حاوية باستخدام Items.query باستخدام بناء جملة يشبه SQL:

const { resources } = await container.items
  .query("SELECT * from c WHERE c.isCapitol = true")
  .fetchAll();
for (const city of resources) {
  console.log(`${city.name}, ${city.state} is a capitol `);
}

تنفيذ استعلامات المعلمات عن طريق تمرير كائن يحتوي على المعلمات وقيمها إلى Items.query:

const { resources } = await container.items
  .query({
    query: "SELECT * from c WHERE c.isCapitol = @isCapitol",
    parameters: [{ name: "@isCapitol", value: true }]
  })
  .fetchAll();
for (const city of resources) {
  console.log(`${city.name}, ${city.state} is a capitol `);
}

لمزيد من المعلومات حول الاستعلام عن قواعد بيانات قاعدة بيانات Cosmos باستخدام واجهة برمجة تطبيقات SQL، راجع الاستعلام عن بيانات قاعدة بيانات Azure Cosmos باستخدام استعلامات SQL.

تغيير نموذج سحب الموجز

يمكن جلب موجز التغيير لمفتاح قسم أو نطاق موجز أو حاوية بأكملها.

لمعالجة موجز التغيير، قم بإنشاء مثيل ل ChangeFeedPullModelIterator. عند إنشاء ChangeFeedPullModelIterator، يجب تحديد قيمة مطلوبة changeFeedStartFrom داخل ChangeFeedIteratorOptions والتي تتكون من موضع البداية لقراءة التغييرات والمورد (مفتاح قسم أو FeedRange) الذي سيتم جلب التغييرات له. يمكنك اختياريا استخدام maxItemCount في ChangeFeedIteratorOptions لتعيين الحد الأقصى لعدد العناصر المستلمة لكل صفحة.

ملاحظة: إذا لم يتم تحديد أي changeFeedStartFrom قيمة، فسيتم إحضار ملف التغيير لحاوية بأكملها من الآن().

هناك أربعة مواضع بداية لموجز التغيير:

  • Beginning
// Signals the iterator to read changefeed from the beginning of time.
const options = {
  changeFeedStartFrom: ChangeFeedStartFrom.Beginning();
}
const iterator = container.getChangeFeedIterator(options);
  • Time
// Signals the iterator to read changefeed from a particular point of time.
const time = new Date("2023/09/11") // some sample date
const options = {
  changeFeedStartFrom: ChangeFeedStartFrom.Time(time);
}
  • Now
// Signals the iterator to read changefeed from this moment onward.
const options = {
  changeFeedStartFrom: ChangeFeedStartFrom.Now();
}
  • Continuation
// Signals the iterator to read changefeed from a saved point.
const continuationToken = "some continuation token recieved from previous request";
const options = {
  changeFeedStartFrom: ChangeFeedStartFrom.Continuation(continuationToken);
}

فيما يلي مثال على إحضار موجز التغيير لمفتاح القسم

const partitionKey = "some-partition-Key-value";
const options = {
  changeFeedStartFrom: ChangeFeedStartFrom.Beginning(partitionKey),
};

const iterator = container.items.getChangeFeedIterator(options);

while (iterator.hasMoreResults) {
  const response = await iterator.readNext();
  // process this response
}

نظرا لأن موجز التغيير هو بشكل فعال قائمة لا نهائية من العناصر التي تشمل جميع عمليات الكتابة والتحديثات المستقبلية، فإن قيمة hasMoreResults دائما trueهي . عندما تحاول قراءة موجز التغيير ولا تتوفر تغييرات جديدة، تتلقى استجابة بالحالة NotModified .

يمكن العثور على إرشادات استخدام وأمثلة أكثر تفصيلا لموجز التغيير هنا.

معالجة الأخطاء

ينشئ SDK أنواعا مختلفة من الأخطاء التي يمكن أن تحدث أثناء العملية.

  1. ErrorResponse يتم طرح إذا كانت استجابة عملية ترجع رمز >خطأ =400.
  2. TimeoutError يتم طرح إذا تم استدعاء Abort داخليا بسبب المهلة.
  3. AbortError يتم طرح إذا تسبب أي مستخدم مرر إشارة في إجهاض.
  4. RestError يتم طرحه في حالة فشل استدعاء النظام الأساسي بسبب مشكلات في الشبكة.
  5. الأخطاء التي تم إنشاؤها بواسطة أي devDependencies. على سبيل المثال. @azure/identity يمكن أن تطرح CredentialUnavailableErrorالحزمة .

فيما يلي مثال لمعالجة الأخطاء من النوع ErrorResponseو TimeoutErrorAbortErrorو و.RestError

try {
  // some code
} catch (err) {
  if (err instanceof ErrorResponse) {
    // some specific error handling.
  } else if (err instanceof RestError) {
    // some specific error handling.
  }
  // handle other type of errors in similar way.
  else {
    // for any other error.
  }
}

من المهم معالجة هذه الأخطاء بشكل صحيح للتأكد من أن تطبيقك يمكنه الاسترداد بأمان من أي حالات فشل والاستمرار في العمل كما هو متوقع. يمكن العثور على مزيد من التفاصيل حول بعض هذه الأخطاء والحلول الممكنة لها هنا.

استكشاف الأخطاء وإصلاحها

عام

عند التفاعل مع أخطاء قاعدة بيانات Cosmos التي تم إرجاعها بواسطة الخدمة تتوافق مع نفس رموز حالة HTTP التي تم إرجاعها لطلبات واجهة برمجة تطبيقات REST:

رموز حالة HTTP لـ Azure Cosmos DB

النزاعات

على سبيل المثال، إذا حاولت إنشاء عنصر باستخدام عنصر id قيد الاستخدام بالفعل في قاعدة بيانات Cosmos DB، يتم إرجاع خطأ 409، يشير إلى التعارض. في المثال التالي، تتم معالجة الخطأ على نحو آمن من خلال معرفة الاستثناء وعرض معلومات إضافية حول الخطأ.

try {
  await containers.items.create({ id: "existing-item-id" });
} catch (error) {
  if (error.code === 409) {
    console.log("There was a conflict with an existing item");
  }
}

تحويل الشفرات

تم تصميم Azure SDKs لدعم بناء جملة ES5 JavaScript وإصدارات LTS من Node.js. إذا كنت بحاجة إلى دعم لأوقات تشغيل JavaScript السابقة مثل Internet Explorer أو Node 6، فستحتاج إلى نقل التعليمات البرمجية لحزمة SDK كجزء من عملية الإنشاء.

التعامل مع الأخطاء العابرة مع عمليات إعادة المحاولة

في أثناء العمل مع Cosmos DB، قد تواجه حالات فشل عابرة بسبب حدود المعدل التي تفرضها الخدمة، أو مشكلات عابرة أخرى مثل انقطاع الشبكة. للحصول على معلومات حول معالجة هذه الأنواع من الأعطال، راجع نمط إعادة المحاولة في دليل أنماط تصميم السحابة، ونمط قاطع الدائرة ذي الصلة.

تسجيل الدخول

قد يساعد تمكين التسجيل في الكشف عن معلومات مفيدة حول حالات الفشل. للاطلاع على سجل لطلبات HTTP واستجاباته، قم بتعيين AZURE_LOG_LEVELمتغير البيئة إلى info. بدلا من ذلك، يمكن تمكين التسجيل في وقت التشغيل عن طريق استدعاء setLogLevel في @azure/logger. أثناء استخدام AZURE_LOG_LEVEL تأكد من تعيينه قبل تهيئة مكتبة التسجيل. قم بتمريره بشكل مثالي عبر سطر الأوامر، إذا كان استخدام مكتبات مثل dotenv التأكد من تهيئة مثل هذه المكتبات قبل تسجيل المكتبة.

const { setLogLevel } = require("@azure/logger");
setLogLevel("info");

للحصول على إرشادات أكثر تفصيلا حول كيفية تمكين السجلات، يمكنك إلقاء نظرة على مستندات حزمة @azure/المسجل.

التشخيص

توفر ميزة تشخيص Cosmos رؤى محسنة حول جميع عمليات العميل الخاصة بك. تتم إضافة كائن CosmosDiagnostics إلى استجابة جميع عمليات العميل. مثل

  • نقطة البحث عن عملية reponse - item.read()، ، container.create()database.delete()
  • عملية الاستعلام reponse -queryIterator.fetchAll()،
  • عمليات مجمعة ودفعية -item.batch().
  • كائنات استجابة الخطأ/الاستثناء.

تتم إضافة كائن CosmosDiagnostics إلى استجابة جميع عمليات العميل. هناك 3 مستويات تشخيص Cosmos ومعلومات وتصحيح الأخطاء وتصحيح الأخطاء غير آمنة. حيث يتم استخدام المعلومات فقط لأنظمة الإنتاج وتصحيح الأخطاء وتصحيح الأخطاء غير الآمنة أثناء التطوير وتصحيح الأخطاء، لأنها تستهلك موارد أعلى بكثير. يمكن تعيين مستوى تشخيص Cosmos بطريقتين

  • برمجيا
  const client = new CosmosClient({ endpoint, key, diagnosticLevel: CosmosDbDiagnosticLevel.debug });
  • استخدام متغيرات البيئة. (مستوى التشخيص الذي تم تعيينه بواسطة متغير البيئة له أولوية أعلى على تعيينه من خلال خيارات العميل.)
  export AZURE_COSMOSDB_DIAGNOSTICS_LEVEL="debug"

يحتوي تشخيص Cosmos على ثلاثة أعضاء

  • نوع ClientSideRequestStatistics: يحتوي على تفاصيل تشخيص مجمعة، بما في ذلك عمليات البحث عن بيانات التعريف وإعادة المحاولة ونقاط النهاية التي تم الاتصال بها وإحصائيات الطلب والاستجابة مثل حجم الحمولة والمدة. (يتم جمعها دائما، ويمكن استخدامها في أنظمة الإنتاج.)

  • DiagnosticNode: هي بنية تشبه الشجرة تلتقط معلومات تشخيصية مفصلة. على har غرار التسجيل الموجود في المستعرضات. يتم تعطيل هذه الميزة بشكل افتراضي وهي مخصصة لتصحيح أخطاء البيئات غير الإنتاجية فقط. (تم جمعها على مستوى التشخيص لتصحيح الأخطاء وتصحيح الأخطاء غير الآمنة)

  • ClientConfig: يلتقط المعلومات الأساسية المتعلقة بإعدادات تكوين العميل أثناء تهيئة العميل. (تم جمعها على مستوى التشخيص لتصحيح الأخطاء وتصحيح الأخطاء غير الآمنة)

يرجى التأكد من عدم تعيين مستوى التشخيص أبدا إلى debug-unsafe في بيئة الإنتاج، لأنه يلتقط هذا المستوى CosmosDiagnostics حمولات الطلب والاستجابة وإذا اخترت تسجيله (يتم تسجيله افتراضيا بواسطة @azure/logger في verbose المستوى). قد يتم التقاط هذه الحمولات في متلقي السجل الخاص بك.

استهلاك التشخيصات

  • منذ diagnostics تتم إضافة إلى كافة كائنات الاستجابة. يمكنك الوصول CosmosDiagnostic برمجيا كما يلي.
  // For point look up operations
  const { container, diagnostics: containerCreateDiagnostic } =
    await database.containers.createIfNotExists({
      id: containerId,
      partitionKey: {
        paths: ["/key1"],
      },
  });

  // For Batch operations
   const operations: OperationInput[] = [
    {
      operationType: BulkOperationType.Create,
      resourceBody: { id: 'A', key: "A", school: "high" },
    },
  ];
  const response = await container.items.batch(operations, "A"); 
  
  // For query operations
  const queryIterator = container.items.query("select * from c");
  const { resources, diagnostics } = await queryIterator.fetchAll();

  // While error handling
  try {
    // Some operation that might fail
  } catch (err) {
    const diagnostics = err.diagnostics
  }
  • يمكنك أيضا تسجيل الدخول diagnostics باستخدام @azure/logger، يتم تسجيل التشخيص دائما باستخدام @azure/logger على verbose المستوى. لذلك إذا قمت بتعيين مستوى التشخيص إلى debug أو debug-unsafe والمستوى @azure/logger إلى verbose، diagnostics فسيتم تسجيله.

الخطوات التالية

المزيد من نماذج التعليمات البرمجية

تتوفر لك عدة عينات في مستودع SDK GitHub. توفر هذه العينات أمثلة على التعليمات البرمجية للسيناريوهات الإضافية التي تتم مواجهتها عادة في أثناء العمل مع Cosmos DB:

  • عمليات قاعدة البيانات
  • عمليات الحاوية
  • عمليات العنصر
  • تكوين الفهرسة
  • قراءة موجز تغيير حاوية
  • الإجراءات المخزنة
  • تغيير إعدادات معدل النقل قاعدة البيانات/الحاوية
  • عمليات الكتابة متعددة المناطق

التقييدات

الميزات أدناه غير مدعومة حاليا. للحصول على خيارات البدائل، تحقق من قسم الحلول البديلة أدناه.

قيود مستوى البيانات:

  • الاستعلامات باستخدام COUNT من استعلام فرعي DISTINCT
  • الوصول المباشر إلى وضع TCP
  • لا تدعم الاستعلامات المجمعة عبر الأقسام، مثل الفرز والعد والتميز، الرموز المميزة للاستمرار. استعلامات قابلة للبث، مثل SELECT * FROM WHERE ، دعم الرموز المميزة للمتابعة. راجع قسم "الحل البديل" لتنفيذ الاستعلامات غير القابلة للبث دون رمز مميز للمتابعة.
  • موجز التغيير: المعالج
  • موجز التغيير: قراءة قيم مفاتيح أقسام متعددة
  • تغيير نموذج سحب الموجز لجميع الإصدارات ووضع الحذف #27058
  • تغيير دعم نموذج سحب الموجز لمفاتيح الأقسام الهرمية الجزئية #27059
  • ORDER BY عبر الأقسام للأنووع المختلطة
  • قيود وحدة التحكم:

    • الحصول على مقاييس CollectionSizeUsage و DatabaseUsage و DocumentUsage
    • إنشاء فهرس جغرافي مكاني
    • تحديث معدل نقل التحجيم التلقائي

    الحلول البديلة

    الرمز المميز للمتابعة لاستعلامات الأقسام المتقاطعة

    يمكنك تحقيق استعلامات الأقسام المتقاطعة مع دعم الرمز المميز للاستمرار باستخدام نمط السيارة الجانبية. يمكن لهذا النمط أيضًا أن يمكّن التطبيقات من أن تتكون من مكونات وتقنيات غير متجانسة.

    تنفيذ استعلام عبر الأقسام غير القابلة للتقسيم

    لتنفيذ استعلامات غير قابلة للبث دون استخدام الرموز المميزة للمتابعة، يمكنك إنشاء مكرر استعلام بمواصفات وخيارات الاستعلام المطلوبة. يوضح نموذج التعليمات البرمجية التالي كيفية استخدام مكرر استعلام لجلب جميع النتائج دون الحاجة إلى رمز استمرار:

    const querySpec = {
      query: "SELECT * FROM c WHERE c.status = @status",
      parameters: [{ name: "@status", value: "active" }],
    };
    const queryOptions = {
      maxItemCount: 10, // maximum number of items to return per page
      enableCrossPartitionQuery: true,
    };
    const querIterator = await container.items.query(querySpec, queryOptions);
    while (querIterator.hasMoreResults()) {
      const { resources: result } = await querIterator.fetchNext();
      //Do something with result
    }
    

    يمكن أيضا استخدام هذا الأسلوب للاستعلامات القابلة للبث.

    التحكم بعمليات وحدة التحكم

    عادة، يمكنك استخدام مدخل Microsoft Azure أوواجهة برمجة تطبيقات REST لموفر موارد Azure Cosmos DB أو Azure CLI أو PowerShell لقيود مستوى التحكم غير المدعومة.

    وثائق إضافية

    للحصول على وثائق أكثر شمولًا حول خدمة قاعدة بيانات Cosmos، راجع وثائق قاعدة بيانات Azure Cosmos على docs.microsoft.com.

    المساهمة

    إذا كنت ترغب في المساهمة في هذه المكتبة، يرجى قراءة دليل المساهمة لمعرفة المزيد حول كيفية إنشاء التعليمات البرمجية واختبارها.

    مرات الظهور