Naar hoofdinhoud gaan

De Runtime SDK gebruiken

Doel​

De Runtime SDK begrijpen — de ene interface waarmee een aangepaste React-app of een aangepast besturingselement het Parsware-platform bereikt — en elk onderdeel ervan goed gebruiken.

Vereisten​

  • Een project dat is opgezet zoals in Een aangepaste React-app bouwen of Een aangepast besturingselement bouwen, met @parsware/runtime-sdk geïnstalleerd vanaf https://docs.parsware.com/sdk/parsware-runtime-sdk-1.0.0.tgz.
  • @parsware/runtime-sdk als gedeelde singleton gedeclareerd in de Module Federation-configuratie van het project. Zonder dat krijgt de app een eigen, lege kopie van de SDK in plaats van die van de shell.

De ene regel​

Een gehost artefact bereikt het platform alleen via de Runtime SDK. De shell koppelt de runtime aan de omgeving waarin zij de app opende en aan de persoon die haar gebruikt. Er is geen omgevings-id om door te geven, geen token om te bewaren en geen adres om te configureren, dus dezelfde build werkt in elke omgeving waarin ze is geïnstalleerd. Een app die zelf de HTTP-API's van het platform aanroept, moet alle drie zelf oplossen en doet het fout zodra ze ergens anders wordt geïnstalleerd.

De runtime krijgen​

Een aangepaste React-app leest hem uit een hook:

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

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

Een aangepast besturingselement krijgt hetzelfde oppervlak als zijn prop context:

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

Buiten de shell, in een unittest, omsluit je de component met ParswareRuntimeProvider en een stub: <ParswareRuntimeProvider runtime={stub}>.

Wat de runtime bevat​

OnderdeelWat het is
environmentId, environmentNameDe omgeving waarin de app werd geopend. De naam mag je tonen.
artifactIdDe logische naam van de app of het besturingselement.
userDe persoon die het gebruikt: id, name, email, tenantId, roles.
scopesWat de app vroeg, ingeperkt tot wat de sessie van de persoon bevat.
dataTabellen, records, query's, opgeslagen query's, aangepaste API's, rapporten en recordbestanden, gekoppeld aan de omgeving.
storageObjectopslag: buckets en de bestanden erin.
logGestructureerd logboek, dat met app en omgeving naar het platform gaat.

Wie haar gebruikt, en wat die mag​

runtime.user is om te tonen en te begroeten: een naam, een rol om een indeling op te kiezen. Het is geen controle van rechten.

Elke aanroep gebeurt als de persoon die de app gebruikt. Hun beveiligingsrol bepaalt wat elke aanroep mag lezen en wijzigen, precies zoals in een modelgestuurde app, dus een app kan nooit meer dan de persoon die haar opende, en twee mensen die dezelfde app gebruiken zien elk wat zij mogen zien.

runtime.scopes zijn de requestedScopes van de app, ingeperkt tot de sessie van de persoon. Lees ze om weg te laten wat niet zal werken:

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

Records lezen​

Een pagina van één tabel​

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 …

Een pagina, nooit de hele tabel. { pageSize: 1 } is de goedkope manier om alleen een aantal te lezen.

Een query: kolommen, koppelingen en totalen​

runQuery neemt hetzelfde querydocument dat de rasters van het platform zelf versturen, en kan dus kolommen kiezen, zoekvelden volgen en optellen:

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
}

Met een aggregate-blok antwoordt ze met groepen in plaats van rijen (result.kind === 'Groups'). Vraag de server om totalen in plaats van elke rij op te halen om ze in de browser op te tellen.

Een opgeslagen query, op naam​

Hoort de vraag bij de oplossing en niet bij de code van de app, sla haar dan op in de oplossing en voer haar uit op naam:

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
}

De definitie reist dan mee met de oplossing en kan worden verbeterd zonder een nieuwe build uit te brengen. De opties kunnen haar alleen beperken: een extra filter wordt met EN gecombineerd met de opgeslagen filter, en page of top begrenzen haar. Wat ze leest, groepeert en telt, blijft dat van de opgeslagen query.

Zoekvelden en keuzes​

  • searchLookup(table, { search }) vindt kandidaten voor een zoekveld, met de kolommen die de zoekweergave van de doeltabel toont. resolveLookup(table, id) maakt van een opgeslagen id weer de tekst om te tonen.
  • Een keuzekolom komt als het nummer van de optie, met het label ernaast. values bevat het opgeslagen nummer en formattedValues het label dat iemand leest, zoals de naam van een zoekveld meekomt. Gegroepeerde resultaten dragen het in formattedGroups. listOptionSets() geeft de hele lijst, voor een keuzelijst.

Records wijzigen​

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 neemt alleen de kolommen die veranderen. Een hele record terugsturen overschrijft waarden die iemand anders sinds jouw lezen kan hebben gewijzigd. Plug-instappen op de server draaien bij deze schrijfacties zoals bij elke andere; de bedrijfsregels aan clientzijde van een formulier niet, want er is geen formulier.

Een aangepaste API aanroepen​

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

Het pad is het adres van de API onder de omgeving: voorvoegsel van de uitgever, versie, route. Bij GET en DELETE gaan de invoerwaarden in de querystring, anders in de JSON-body. Een weigering geeft een fout met de eigen melding van de API. Een app en een extern systeem die dezelfde API aanroepen, lezen hetzelfde antwoord.

Objectopslag​

Voor bestanden die niet bij één record horen: een export die de app maakte, een document dat iemand haar gaf, een batch die ze zo gaat verwerken.

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);
  • Een bucket hoort bij de tenant, niet bij de omgeving. Twee omgevingen van één tenant delen hem, dus zet de omgeving in de sleutel als hun bestanden gescheiden moeten blijven.
  • Bucketnamen worden over alle tenants gedeeld, zoals in S3. Kies er een die echt van jou is.
  • Vraag storage.read en storage.write aan in de requestedScopes van de app.
  • Een bestand dat bij één record hoort, gaat in plaats daarvan op de record (volgende sectie).

Bestanden en afbeeldingen van een record​

Een kolom van het type File of Image bevat een klein bestand dat bij die record hoort:

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');
  • Het uploaden vult de kolom zelf. Zet het veld niet in createRecord of updateRecord.
  • De record moet eerst bestaan. Maak hem aan, upload daarna.
  • Er is geen link voor <img src>. Het bestand wordt geautoriseerd als zijn record, dus toon de Blob via URL.createObjectURL(blob).

Tabellen en hun definities​

listEntities(), listFields(entityId), listViews(entityId), getViewDefinition(…), listForms(entityId), getFormDefinition(…), listRelationships(), listOptionSets() en getEntityStatus(entityId) beschrijven de omgeving, voor een app die haar eigen raster of formulier tekent. getRecordPermissions(table) zegt of de persoon in een tabel mag aanmaken, lezen, bijwerken of verwijderen, zodat de app een knop kan weglaten voordat die wordt geweigerd. canImport zegt of de persoon er een spreadsheet in mag laden; daarvoor is naast aanmaken ook het recht Gegevens importeren nodig.

Logboek​

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

De niveaus zijn debug, info, warn en error. App en omgeving worden voor je toegevoegd. Log nooit recordwaarden, persoonsgegevens of iets uit een token. De browserconsole is onzichtbaar voor wie de app ondersteunt; dit niet.

Fouten afhandelen​

Elke aanroep geeft een promise terug die kan worden afgewezen: geen recht, een record die intussen is verwijderd, een netwerk dat plat ligt. Toon de melding, en wijzig geen status meer zodra de app is gesloten:

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]);

Wat de SDK niet doet​

  • Ze verstuurt geen meldingen. Er is geen meldingsoppervlak; doe er geen na met een directe HTTP-aanroep.
  • Ze slaat de waarde van een aangepast besturingselement niet op. Een besturingselement legt vast via onChange, en het formulier slaat op.
  • Ze geeft geen tokens. Er is niets te lezen en niets te bewaren.
  • Ze bereikt geen andere omgevingen. Werk tussen omgevingen verplaatsen is een oplossing exporteren en importeren.

Problemen oplossen​

  • useParswareRuntime() geeft een fout — de component is buiten een runtime-provider gerenderd. In de shell kan dat niet; in een test omsluit je haar met ParswareRuntimeProvider.
  • De runtime is er maar is leeg, of hooks falen — @parsware/runtime-sdk of React is in het shared-blok van Module Federation niet singleton: true, dus de app heeft een eigen kopie gebundeld.
  • Een aanroep wordt geweigerd — de persoon die de app gebruikt, mag dat niet; hun beveiligingsrol beslist. Controleer runtime.scopes en getRecordPermissions, en laat de actie weg.
  • Een keuzekolom toont een getal — lees formattedValues, niet values, voor het label.
  • Cannot find module …/dist/types onder Vitest — de SDK is geïnstalleerd uit een oudere download. Installeer de huidige.

Gerelateerde scenario's​

  • Een aangepaste React-app bouwen.
  • Een aangepast besturingselement bouwen.
  • Een toegangstoken verkrijgen.