پرش به مطلب اصلی

استفاده از 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 در پیکربندی Module Federation پروژه به‌عنوان یک singleton مشترک اعلام شده باشد. بدون آن، برنامه به‌جای نسخهٔ پوسته، نسخهٔ خالی خودش از SDK را می‌گیرد.

آن یک قاعده​

یک آرتیفکت میزبانی‌شده فقط از راه Runtime SDK به پلتفرم می‌رسد. پوسته runtime را به محیطی که برنامه را در آن باز کرده و به کسی که از آن استفاده می‌کند متصل می‌کند. نه شناسهٔ محیطی برای فرستادن هست، نه توکنی برای نگه‌داشتن و نه نشانی‌ای برای پیکربندی، پس همان بیلد در هر محیطی که نصب شود کار می‌کند. برنامه‌ای که خودش اپی‌آی‌های HTTP پلتفرم را صدا بزند باید هر سه را خودش حل کند، و نخستین باری که جای دیگری نصب شود اشتباهشان می‌کند.

گرفتن runtime​

یک برنامهٔ سفارشی React آن را از یک هوک می‌خواند:

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

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

یک کنترل سفارشی همین سطح را به‌صورت prop context می‌گیرد:

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

بیرون از پوسته، در یک آزمون واحد، کامپوننت را با یک runtime ساختگی درون ParswareRuntimeProvider بگذارید: <ParswareRuntimeProvider runtime={stub}>.

runtime چه چیزهایی دارد​

عضوچیست
environmentId، environmentNameمحیطی که برنامه در آن باز شده. نمایش نام بی‌خطر است.
artifactIdنام منطقی برنامه یا کنترل.
userکسی که از آن استفاده می‌کند: id، name، email، tenantId، roles.
scopesآنچه برنامه خواسته، محدودشده به آنچه نشست آن شخص دارد.
dataجدول‌ها، رکوردها، پرس‌وجوها، پرس‌وجوهای ذخیره‌شده، اپی‌آی‌های سفارشی، گزارش‌ها و فایل‌های رکورد، متصل به محیط.
storageفضای ذخیره‌سازی اشیا: باکت‌ها و فایل‌های درونشان.
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 برمی‌گرداند که ممکن است رد شود: نبودِ مجوز، رکوردی که در این میان حذف شده، شبکه‌ای که قطع است. پیام را نشان دهید، و پس از بسته‌شدن برنامه دیگر state را تغییر ندهید:

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 هست اما خالی است، یا هوک‌ها شکست می‌خورند — @parsware/runtime-sdk یا React در بلوک shared از Module Federation به‌صورت singleton: true نیست، پس برنامه نسخهٔ خودش را بسته‌بندی کرده است.
  • یک فراخوانی رد می‌شود — کسی که از برنامه استفاده می‌کند اجازهٔ آن کار را ندارد؛ نقش امنیتی او تصمیم می‌گیرد. runtime.scopes و getRecordPermissions را بررسی کنید و آن کار را کنار بگذارید.
  • ستون گزینه‌ای یک عدد نشان می‌دهد — برای برچسبش formattedValues را بخوانید، نه values.
  • Cannot find module …/dist/types زیر Vitest — SDK از یک دانلود قدیمی‌تر نصب شده است. نسخهٔ کنونی را نصب کنید.

سناریوهای مرتبط​

  • ساخت یک برنامهٔ سفارشی React.
  • ساخت یک کنترل سفارشی.
  • گرفتن یک توکن دسترسی.