إنتقل إلى المحتوى الرئيسي

استخدام Runtime SDK

الهدف​

فهم Runtime SDK، الواجهة الوحيدة التي يصل بها تطبيق React مخصص أو عنصر تحكم مخصص إلى منصة Parsware، واستخدام كل جزء منه استخدامًا صحيحًا.

المتطلبات المسبقة​

  • مشروع مُهيَّأ كما في بناء تطبيق React مخصص أو بناء عنصر تحكم مخصص، مع تثبيت @parsware/runtime-sdk من https://docs.parsware.com/sdk/parsware-runtime-sdk-1.0.0.tgz.
  • إعلان @parsware/runtime-sdk بوصفه singleton مشتركًا في ضبط Module Federation للمشروع. ومن دون ذلك يحصل التطبيق على نسخته الفارغة من SDK بدلًا من نسخة الواجهة.

القاعدة الوحيدة​

لا يصل العنصر المستضاف إلى المنصة إلا عبر Runtime SDK. تربط الواجهة runtime بالبيئة التي فتحت فيها التطبيق وبالشخص الذي يستخدمه. لا معرّف بيئة يُمرَّر، ولا رمز يُخزَّن، ولا عنوان يُضبط، فيعمل البناء نفسه في كل بيئة يُثبَّت فيها. والتطبيق الذي يستدعي واجهات HTTP البرمجية للمنصة بنفسه عليه أن يحل الأمور الثلاثة، ويخطئ فيها أول مرة يُثبَّت في مكان آخر.

الحصول على runtime​

يقرؤه تطبيق React المخصص من hook:

import { useParswareRuntime } from '@parsware/runtime-sdk';

export default function App() {
const runtime = useParswareRuntime();
// …
}

ويتلقى عنصر التحكم المخصص السطح نفسه في الخاصية context:

export default function Control({ value, onChange, fieldMetadata, context }: ParswareControlProps) {
context.log('debug', 'mounted');
}

خارج الواجهة، في اختبار وحدة، غلّف المكوّن بـ ParswareRuntimeProvider مع runtime بديل: <ParswareRuntimeProvider runtime={stub}>.

ما يحمله runtime​

العضوما هو
environmentId، environmentNameالبيئة التي فُتح فيها التطبيق. يمكن عرض الاسم.
artifactIdالاسم المنطقي للتطبيق أو عنصر التحكم.
userالشخص الذي يستخدمه: id، name، email، tenantId، roles.
scopesما طلبه التطبيق، مقصورًا على ما تحمله جلسة ذلك الشخص.
dataالجداول، والسجلات، والاستعلامات، والاستعلامات المحفوظة، والواجهات البرمجية المخصصة، والتقارير، وملفات السجلات، مربوطة بالبيئة.
storageتخزين الكائنات: الحاويات (buckets) والملفات التي فيها.
logتسجيل منظَّم يُرسَل إلى المنصة مع التطبيق والبيئة.

من يستخدمه، وما المسموح له​

runtime.user للعرض والترحيب: اسم، ودور يُختار على أساسه تخطيط. وليس تحققًا من الصلاحيات.

كل استدعاء يتم باسم الشخص الذي يستخدم التطبيق. دوره الأمني يقرر ما يحق لكل استدعاء قراءته وتغييره، تمامًا كما في التطبيق الذي يعتمد على النموذج؛ فلا يستطيع التطبيق أبدًا أن يفعل أكثر مما يستطيعه من فتحه، وشخصان يستخدمان التطبيق نفسه يرى كل منهما ما يُسمح له به.

runtime.scopes هي requestedScopes الخاصة بالتطبيق مقصورة على جلسة الشخص. اقرأها لتستبعد ما لن يعمل:

const canSave = runtime.scopes.includes('storage.write');

قراءة السجلات​

صفحة من جدول واحد​

import { FilterConnector, RecordFilterOperator } from '@parsware/runtime-sdk';

const page = await runtime.data.queryRecords('par_order', {
pageNumber: 1,
pageSize: 25,
sorts: [{ field: 'par_createdon', descending: true }],
filterGroup: {
connector: FilterConnector.And,
conditions: [{ field: 'par_status', operator: RecordFilterOperator.Equals, value: 1 }],
},
});
// page.items, page.totalCount, page.hasNextPage …

صفحة، لا الجدول كله أبدًا. و{ pageSize: 1 } هي الطريقة الرخيصة لقراءة عدد فقط.

استعلام: أعمدة وروابط ومجاميع​

يأخذ runQuery مستند الاستعلام نفسه الذي ترسله جداول المنصة نفسها، فيستطيع اختيار الأعمدة وتتبّع حقول البحث والجمع:

const result = await runtime.data.runQuery('nw_visitor', {
select: ['nw_name', 'nw_company', 'new_purpose'],
order: [{ field: 'nw_name', descending: false }],
page: { number: 1, size: 50 },
});
for (const record of result.records?.items ?? []) {
record.values.nw_name; // 'Ada Lovelace'
record.values.new_purpose; // 270150000 — what is stored
record.formattedValues?.new_purpose; // 'Meeting' — what a person reads
}

ومع كتلة aggregate يجيب بمجموعات بدلًا من صفوف (result.kind === 'Groups'). اطلب المجاميع من الخادم بدلًا من جلب كل صف لجمعه في المتصفح.

استعلام محفوظ، باسمه​

حين يكون السؤال من شأن الحل لا من شأن شيفرة التطبيق، احفظه في الحل وشغّله باسمه:

const totals = await runtime.data.runSavedQuery('nw_visitorsbycompany');
for (const group of totals.groups?.groups ?? []) {
group.groups.nw_company; // 'Analytical Engines'
group.measures.visitors; // 4
}

عندئذ ينتقل التعريف مع الحل ويمكن تحسينه دون نشر بناء جديد. ولا تستطيع الخيارات إلا تضييقه: يُضَم filter الإضافي إلى المحفوظ بـ AND، ويحدّه page أو top. أما ما يقرؤه ويجمّعه ويعدّه فيبقى ما في الاستعلام المحفوظ.

حقول البحث والخيارات​

  • searchLookup(table, { search }) يجد المرشحين لحقل بحث، مع الأعمدة التي يعرضها عرض البحث في الجدول الهدف. و**resolveLookup(table, id)** يعيد المعرّف المحفوظ إلى النص الذي يُعرض.
  • يصل عمود الخيارات رقمَ الخيار، وتسميته بجانبه. تحمل values الرقم المحفوظ وتحمل formattedValues التسمية التي يقرؤها الشخص، كما يصل اسم حقل البحث. وتحملها النتائج المجمّعة في formattedGroups. وlistOptionSets() يعطي القائمة كاملة، لأداة اختيار.

تغيير السجلات​

const created = await runtime.data.createRecord('par_order', { par_name: 'ORD-1001' });
await runtime.data.updateRecord('par_order', created.id, { par_status: 2 });
await runtime.data.deleteRecord('par_order', created.id);

يأخذ updateRecord الأعمدة التي تتغير فقط. إعادة إرسال السجل كاملًا تكتب فوق قيم ربما غيّرها شخص آخر منذ أن قرأتها. تعمل خطوات المكوّن الإضافي على الخادم عند هذه الكتابات كما عند غيرها؛ أما قواعد العمل من جهة العميل في النموذج فلا، إذ لا نموذج هنا.

استدعاء واجهة برمجية مخصصة​

const overview = await runtime.data.callApi('GET', 'psl/v1/overview', { season: '2026' });

المسار هو عنوان الواجهة البرمجية تحت البيئة: بادئة الناشر، ثم الإصدار، ثم المسار. في GET وDELETE تذهب المدخلات في سلسلة الاستعلام، وفي غيرهما في جسم JSON. والرفض يرمي خطأ برسالة الواجهة البرمجية نفسها. والتطبيق والنظام الخارجي اللذان يستدعيان الواجهة البرمجية نفسها يقرآن الإجابة نفسها.

تخزين الكائنات​

للملفات التي لا تخص سجلًا بعينه: تصدير صنعه التطبيق، أو مستند سلّمه إليه أحدهم، أو دفعة يوشك على معالجتها.

await runtime.storage.createBucket('nw-frontdesk'); // or returns the existing one
const saved = await runtime.storage.put('nw-frontdesk', 'visitors/2026-10-14.csv',
new Blob([csv], { type: 'text/csv' }), '2026-10-14.csv');
const files = await runtime.storage.list('nw-frontdesk', 'visitors/');
const blob = await runtime.storage.get('nw-frontdesk', saved.key);
await runtime.storage.remove('nw-frontdesk', saved.key);
  • الحاوية ملك للمستأجر لا للبيئة. تتشاركها بيئتان للمستأجر نفسه، فضع البيئة في المفتاح إن وجب أن تبقى ملفاتهما منفصلة.
  • أسماء الحاويات مشتركة بين كل المستأجرين، كما في S3. اختر اسمًا خاصًا بك.
  • اطلب storage.read وstorage.write في requestedScopes الخاصة بالتطبيق.
  • الملف الذي يخص سجلًا واحدًا يوضع على السجل نفسه بدلًا من ذلك (القسم التالي).

ملفات السجل وصوره​

يحمل عمود من نوع File أو Image ملفًا صغيرًا يخص ذلك السجل:

const info = await runtime.data.getRecordFile('par_contact', id, 'par_photo'); // null when empty
await runtime.data.uploadRecordFile('par_contact', id, 'par_photo', file);
const blob = await runtime.data.downloadRecordFile('par_contact', id, 'par_photo');
await runtime.data.deleteRecordFile('par_contact', id, 'par_photo');
  • الرفع يضبط العمود بنفسه. لا تضع الحقل في createRecord أو updateRecord.
  • يجب أن يوجد السجل أولًا. أنشئه ثم ارفع.
  • لا رابط يوضع في <img src>. الملف مخوَّل كسجله، فاعرض الـ Blob عبر URL.createObjectURL(blob).

الجداول وتعريفاتها​

listEntities() وlistFields(entityId) وlistViews(entityId) وgetViewDefinition(…) وlistForms(entityId) وgetFormDefinition(…) وlistRelationships() وlistOptionSets() وgetEntityStatus(entityId) تصف البيئة، لتطبيق يرسم جدوله أو نموذجه الخاص. وgetRecordPermissions(table) يبيّن هل يحق للشخص الإنشاء أو القراءة أو التحديث أو الحذف في جدول، فيستبعد التطبيق زرًا قبل أن يُرفض. وcanImport يبيّن هل يحق له تحميل جدول بيانات فيه، وهذا يتطلب امتياز استيراد بيانات إضافة إلى الإنشاء.

التسجيل​

runtime.log('info', 'Visitor list saved', { rows: 8 });
runtime.log('error', 'Could not load visitors', { message });

المستويات هي debug وinfo وwarn وerror. يُرفَق التطبيق والبيئة تلقائيًا. لا تسجّل أبدًا قيم السجلات أو البيانات الشخصية أو أي شيء من رمز. وحدة تحكم المتصفح لا يراها من يدعم التطبيق؛ أما هذا فيراه.

معالجة الإخفاقات​

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

useEffect(() => {
let cancelled = false;
runtime.data.runQuery('nw_visitor', { page: { number: 1, size: 50 } })
.then((r) => { if (!cancelled) setRows(r.records?.items ?? []); })
.catch((e: unknown) => { if (!cancelled) setError(e instanceof Error ? e.message : String(e)); });
return () => { cancelled = true; };
}, [runtime]);

ما لا يفعله SDK​

  • لا يرسل إشعارات. لا يوجد سطح للإشعارات؛ فلا تزيّفه باستدعاء HTTP مباشر.
  • لا يحفظ قيمة عنصر التحكم المخصص. يعتمد عنصر التحكم القيمة عبر onChange، والنموذج هو الذي يحفظ.
  • لا يسلّم رموزًا. لا شيء يُقرأ ولا شيء يُخزَّن.
  • لا يصل إلى بيئات أخرى. نقل العمل بين البيئات هو تصدير حل واستيراده.

استكشاف الأخطاء​

  • useParswareRuntime() يرمي خطأ — عُرض المكوّن خارج مزوّد runtime. لا يحدث هذا في الواجهة؛ وفي الاختبار غلّفه بـ ParswareRuntimeProvider.
  • runtime موجود لكنه فارغ، أو تفشل hooks — @parsware/runtime-sdk أو React ليس singleton: true في كتلة shared من Module Federation، فحزّم التطبيق نسخته الخاصة.
  • رُفض استدعاء — لا يحق للشخص الذي يستخدم التطبيق فعل ذلك؛ دوره الأمني هو الذي يقرر. تحقق من runtime.scopes وgetRecordPermissions، واستبعد الإجراء.
  • عمود خيارات يعرض رقمًا — اقرأ formattedValues لا values للحصول على تسميته.
  • Cannot find module …/dist/types في Vitest — ثُبّت SDK من تنزيل أقدم. ثبّت التنزيل الحالي.

سيناريوهات ذات صلة​

  • بناء تطبيق React مخصص.
  • بناء عنصر تحكم مخصص.
  • الحصول على رمز وصول.