Das Runtime SDK verwenden
Ziel
Das Runtime SDK verstehen – die eine Schnittstelle, über die eine benutzerdefinierte React-App oder ein benutzerdefiniertes Steuerelement die Parsware-Plattform erreicht – und jeden Teil davon richtig verwenden.
Voraussetzungen
- Ein Projekt, eingerichtet wie in
Eine benutzerdefinierte React-App erstellen oder
Ein benutzerdefiniertes Steuerelement erstellen,
mit
@parsware/runtime-sdkinstalliert vonhttps://docs.parsware.com/sdk/parsware-runtime-sdk-1.0.0.tgz. @parsware/runtime-sdkin der Module-Federation-Konfiguration des Projekts als gemeinsames Singleton deklariert. Ohne das bekommt die App eine eigene, leere Kopie des SDK statt der der Shell.
Die eine Regel
Ein gehostetes Artefakt erreicht die Plattform nur über das Runtime SDK. Die Shell bindet die Runtime an die Umgebung, in der sie die App geöffnet hat, und an die Person, die sie benutzt. Es gibt keine Umgebungs-ID zu übergeben, kein Token zu speichern und keine Adresse zu konfigurieren, sodass derselbe Build in jeder Umgebung funktioniert, in der er installiert ist. Eine App, die die HTTP-APIs der Plattform selbst aufruft, muss alle drei Dinge selbst lösen und macht sie falsch, sobald sie zum ersten Mal woanders installiert wird.
Die Runtime bekommen
Eine benutzerdefinierte React-App liest sie aus einem Hook:
import { useParswareRuntime } from '@parsware/runtime-sdk';
export default function App() {
const runtime = useParswareRuntime();
// …
}
Ein benutzerdefiniertes Steuerelement bekommt dieselbe Oberfläche als Prop context:
export default function Control({ value, onChange, fieldMetadata, context }: ParswareControlProps) {
context.log('debug', 'mounted');
}
Außerhalb der Shell, in einem Unit-Test, umschließen Sie die Komponente mit
ParswareRuntimeProvider und einem Stub: <ParswareRuntimeProvider runtime={stub}>.
Was die Runtime enthält
| Element | Was es ist |
|---|---|
environmentId, environmentName | Die Umgebung, in der die App geöffnet wurde. Den Namen kann man anzeigen. |
artifactId | Der logische Name der App oder des Steuerelements. |
user | Die Person, die es benutzt: id, name, email, tenantId, roles. |
scopes | Was die App angefordert hat, beschnitten auf das, was die Sitzung der Person enthält. |
data | Tabellen, Datensätze, Abfragen, gespeicherte Abfragen, benutzerdefinierte APIs, Berichte und Datensatzdateien, an die Umgebung gebunden. |
storage | Objektspeicher: Buckets und die Dateien darin. |
log | Strukturierte Protokollierung, die mit App und Umgebung an die Plattform geht. |
Wer sie benutzt und was er darf
runtime.user ist zum Anzeigen und Begrüßen da: ein Name, eine Rolle, nach der man ein Layout
wählt. Es ist keine Berechtigungsprüfung.
Jeder Aufruf erfolgt als die Person, die die App benutzt. Ihre Sicherheitsrolle entscheidet, was jeder Aufruf lesen und ändern darf, genau wie in einer modellgesteuerten App. Eine App kann also nie mehr tun als die Person, die sie geöffnet hat, und zwei Personen, die dieselbe App benutzen, sehen jeweils, was sie sehen dürfen.
runtime.scopes sind die requestedScopes der App, beschnitten auf die Sitzung der Person. Lesen
Sie sie, um wegzulassen, was nicht funktionieren wird:
const canSave = runtime.scopes.includes('storage.write');
Datensätze lesen
Eine Seite einer Tabelle
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 …
Eine Seite, nie die ganze Tabelle. { pageSize: 1 } ist der günstige Weg, nur eine Anzahl zu
lesen.
Eine Abfrage: Spalten, Verknüpfungen und Summen
runQuery nimmt dasselbe Abfragedokument, das die Raster der Plattform selbst senden, und kann
deshalb Spalten wählen, Nachschlagefeldern folgen und aufsummieren:
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
}
Mit einem aggregate-Block antwortet sie mit Gruppen statt Zeilen (result.kind === 'Groups').
Bitten Sie den Server um Summen, statt jede Zeile zu holen und sie im Browser zu addieren.
Eine gespeicherte Abfrage, nach Namen
Gehört die Frage zur Lösung und nicht zum Code der App, speichern Sie sie in der Lösung und führen Sie sie über ihren Namen aus:
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
}
Die Definition reist dann mit der Lösung und lässt sich verbessern, ohne einen neuen Build
auszuliefern. Die Optionen können sie nur einschränken: Ein zusätzlicher filter wird mit dem
gespeicherten UND-verknüpft, und page oder top begrenzen sie. Was sie liest, gruppiert und
zählt, bleibt das der gespeicherten Abfrage.
Nachschlagefelder und Optionen
searchLookup(table, { search })findet Kandidaten für ein Nachschlagefeld, mit den Spalten, die die Nachschlageansicht der Zieltabelle zeigt.resolveLookup(table, id)macht aus einer gespeicherten ID wieder den anzuzeigenden Text.- Eine Optionsspalte kommt als Nummer der Option, mit ihrer Beschriftung daneben.
valuesenthält die gespeicherte Nummer undformattedValuesdie Beschriftung, die ein Mensch liest – so, wie der Name eines Nachschlagefelds mitkommt. Gruppierte Ergebnisse tragen sie informattedGroups.listOptionSets()liefert die ganze Liste, für eine Auswahl.
Datensätze ändern
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 nimmt nur die Spalten, die sich ändern. Einen ganzen Datensatz zurückzuschicken
überschreibt Werte, die jemand anderes inzwischen geändert haben kann. Server-Plug-in-Schritte
laufen bei diesen Schreibvorgängen wie bei jedem anderen; die clientseitigen Geschäftsregeln eines
Formulars nicht, denn es gibt kein Formular.
Eine benutzerdefinierte API aufrufen
const overview = await runtime.data.callApi('GET', 'psl/v1/overview', { season: '2026' });
Der Pfad ist die Adresse der API unterhalb der Umgebung: Herausgeberpräfix, Version, Route. Bei
GET und DELETE gehen die Eingaben in die Abfragezeichenfolge, sonst in den JSON-Body. Eine
Ablehnung wird mit der eigenen Meldung der API ausgelöst. Eine App und ein externes System, die
dieselbe API aufrufen, lesen dieselbe Antwort.
Objektspeicher
Für Dateien, die keinem einzelnen Datensatz gehören: ein Export, den die App erstellt hat, ein Dokument, das ihr jemand gegeben hat, ein Stapel, den sie gleich verarbeitet.
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);
- Ein Bucket gehört dem Mandanten, nicht der Umgebung. Zwei Umgebungen desselben Mandanten teilen ihn; nehmen Sie also die Umgebung in den Schlüssel auf, wenn ihre Dateien getrennt bleiben müssen.
- Bucket-Namen werden über alle Mandanten hinweg geteilt, wie bei S3. Wählen Sie einen, der eindeutig Ihrer ist.
- Fordern Sie
storage.readundstorage.writein denrequestedScopesder App an. - Eine Datei, die einem einzelnen Datensatz gehört, kommt stattdessen an den Datensatz (nächster Abschnitt).
Dateien und Bilder eines Datensatzes
Eine Spalte vom Typ File oder Image enthält eine kleine Datei, die diesem Datensatz gehört:
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');
- Das Hochladen setzt die Spalte selbst. Setzen Sie das Feld nicht in
createRecordoderupdateRecord. - Der Datensatz muss zuerst existieren. Legen Sie ihn an, dann laden Sie hoch.
- Es gibt keinen Link für
<img src>. Die Datei ist wie ihr Datensatz berechtigt; zeigen Sie den Blob also überURL.createObjectURL(blob)an.
Tabellen und ihre Definitionen
listEntities(), listFields(entityId), listViews(entityId), getViewDefinition(…),
listForms(entityId), getFormDefinition(…), listRelationships(), listOptionSets() und
getEntityStatus(entityId) beschreiben die Umgebung, für eine App, die ihr eigenes Raster oder
Formular zeichnet. getRecordPermissions(table) sagt, ob die Person in einer Tabelle anlegen,
lesen, aktualisieren oder löschen darf, sodass die App eine Schaltfläche weglassen kann, bevor sie
abgelehnt wird.
canImport sagt, ob sie eine Tabellenkalkulation hineinladen darf; dafür braucht es neben dem
Anlegen auch das Recht „Daten importieren“.
Protokollierung
runtime.log('info', 'Visitor list saved', { rows: 8 });
runtime.log('error', 'Could not load visitors', { message });
Die Stufen sind debug, info, warn und error. App und Umgebung werden für Sie angehängt.
Protokollieren Sie nie Datensatzwerte, personenbezogene Daten oder etwas aus einem Token. Die
Browserkonsole ist für den, der die App betreut, unsichtbar; das hier nicht.
Fehler behandeln
Jeder Aufruf gibt ein Promise zurück, das abgelehnt werden kann: keine Berechtigung, ein inzwischen gelöschter Datensatz, ein ausgefallenes Netzwerk. Zeigen Sie die Meldung an, und setzen Sie keinen Zustand mehr, sobald die App geschlossen ist:
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]);
Was das SDK nicht tut
- Es versendet keine Benachrichtigungen. Es gibt keine Benachrichtigungsoberfläche; täuschen Sie keine mit einem direkten HTTP-Aufruf vor.
- Es speichert nicht den Wert eines benutzerdefinierten Steuerelements. Ein Steuerelement
übergibt über
onChange, und das Formular speichert. - Es gibt keine Token heraus. Es gibt nichts zu lesen und nichts zu speichern.
- Es erreicht keine anderen Umgebungen. Arbeit zwischen Umgebungen zu bewegen ist ein Export und Import einer Lösung.
Fehlerbehebung
useParswareRuntime()wirft einen Fehler – die Komponente wurde außerhalb eines Runtime-Providers gerendert. In der Shell kann das nicht passieren; in einem Test umschließen Sie sie mitParswareRuntimeProvider.- Die Runtime ist da, aber leer, oder Hooks schlagen fehl –
@parsware/runtime-sdkoder React ist imshared-Block von Module Federation nichtsingleton: true, sodass die App ihre eigene Kopie gebündelt hat. - Ein Aufruf wird abgelehnt – die Person, die die App benutzt, darf das nicht; ihre
Sicherheitsrolle entscheidet. Prüfen Sie
runtime.scopesundgetRecordPermissionsund lassen Sie die Aktion weg. - Eine Optionsspalte zeigt eine Zahl – lesen Sie für die Beschriftung
formattedValues, nichtvalues. Cannot find module …/dist/typesunter Vitest – das SDK wurde aus einem älteren Download installiert. Installieren Sie den aktuellen.
Verwandte Szenarien
- Eine benutzerdefinierte React-App erstellen.
- Ein benutzerdefiniertes Steuerelement erstellen.
- Ein Zugriffstoken abrufen.