Utiliser le Runtime SDK
Objectif
Comprendre le Runtime SDK, la seule interface qu’une application React personnalisée ou un contrôle personnalisé utilise pour atteindre la plateforme Parsware, et en utiliser correctement chaque partie.
Prérequis
- Un projet préparé comme dans
Créer une application React personnalisée ou
Créer un contrôle personnalisé, avec
@parsware/runtime-sdkinstallé depuishttps://docs.parsware.com/sdk/parsware-runtime-sdk-1.0.0.tgz. @parsware/runtime-sdkdéclaré singleton partagé dans la configuration Module Federation du projet. Sans cela, l’application reçoit sa propre copie vide du SDK au lieu de celle du Shell.
La règle unique
Un artefact hébergé n’atteint la plateforme que par le Runtime SDK. Le Shell lie le runtime à l’environnement où il a ouvert l’application et à la personne qui l’utilise. Il n’y a ni identifiant d’environnement à passer, ni jeton à stocker, ni adresse à configurer : le même build fonctionne dans chaque environnement où il est installé. Une application qui appelle elle-même les API HTTP de la plateforme doit résoudre ces trois points, et se trompe la première fois qu’elle est installée ailleurs.
Obtenir le runtime
Une application React personnalisée le lit depuis un hook :
import { useParswareRuntime } from '@parsware/runtime-sdk';
export default function App() {
const runtime = useParswareRuntime();
// …
}
Un contrôle personnalisé reçoit la même surface dans sa prop context :
export default function Control({ value, onChange, fieldMetadata, context }: ParswareControlProps) {
context.log('debug', 'mounted');
}
Hors du Shell, dans un test unitaire, enveloppez le composant dans ParswareRuntimeProvider avec un
factice : <ParswareRuntimeProvider runtime={stub}>.
Ce que contient le runtime
| Membre | Ce que c’est |
|---|---|
environmentId, environmentName | L’environnement où l’application a été ouverte. Le nom peut être affiché. |
artifactId | Le nom logique de l’application ou du contrôle. |
user | La personne qui l’utilise : id, name, email, tenantId, roles. |
scopes | Ce que l’application a demandé, réduit à ce que contient la session de la personne. |
data | Tables, enregistrements, requêtes, requêtes enregistrées, API personnalisées, rapports et fichiers d’enregistrement, liés à l’environnement. |
storage | Stockage d’objets : les compartiments (buckets) et leurs fichiers. |
log | Journalisation structurée, envoyée à la plateforme avec l’application et l’environnement. |
Qui l’utilise, et ce qu’il peut faire
runtime.user sert à afficher et à saluer : un nom, un rôle d’après lequel choisir une mise en
page. Ce n’est pas une vérification de droits.
Chaque appel est fait au nom de la personne qui utilise l’application. Son rôle de sécurité décide ce que chacun peut lire et modifier, exactement comme dans une application pilotée par modèle : une application ne peut jamais faire plus que la personne qui l’a ouverte, et deux personnes qui utilisent la même application voient chacune ce qui lui est permis.
runtime.scopes est la liste requestedScopes de l’application réduite à la session de la
personne. Lisez-la pour laisser de côté ce qui ne marchera pas :
const canSave = runtime.scopes.includes('storage.write');
Lire des enregistrements
Une page d’une table
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 …
Une page, jamais la table entière. { pageSize: 1 } est la façon économique de ne lire qu’un
nombre.
Une requête : colonnes, liens et totaux
runQuery prend le même document de requête que celui qu’envoient les grilles de la plateforme :
elle peut donc choisir des colonnes, suivre des recherches et additionner :
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
}
Avec un bloc aggregate, elle répond par des groupes plutôt que des lignes
(result.kind === 'Groups'). Demandez les totaux au serveur au lieu de rapatrier chaque ligne pour
les additionner dans le navigateur.
Une requête enregistrée, par son nom
Quand la question appartient à la solution plutôt qu’au code de l’application, enregistrez-la dans la solution et exécutez-la par son nom :
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
}
La définition voyage alors avec la solution et peut être améliorée sans livrer de nouveau build.
Les options ne peuvent que la restreindre : un filter supplémentaire est combiné par ET avec
celui enregistré, et page ou top la limitent. Ce qu’elle lit, regroupe et compte reste celui de
la requête enregistrée.
Recherches et choix
searchLookup(table, { search })trouve des candidats pour une recherche, avec les colonnes qu’affiche la vue de recherche de la table cible.resolveLookup(table, id)retransforme un identifiant stocké en texte à afficher.- Une colonne de choix arrive sous la forme du numéro de l’option, avec son libellé à côté.
valuescontient le numéro stocké etformattedValuesle libellé qu’une personne lit, comme arrive le nom d’une recherche. Les résultats groupés le portent dansformattedGroups.listOptionSets()donne la liste complète, pour un sélecteur.
Modifier des enregistrements
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 ne prend que les colonnes modifiées. Renvoyer un enregistrement entier réécrit des
valeurs que quelqu’un d’autre a pu modifier depuis votre lecture. Les étapes de plug-in serveur
s’exécutent sur ces écritures comme sur toute autre ; les règles métier côté client d’un formulaire
non, puisqu’il n’y a pas de formulaire.
Appeler une API personnalisée
const overview = await runtime.data.callApi('GET', 'psl/v1/overview', { season: '2026' });
Le chemin est l’adresse de l’API sous l’environnement : préfixe de l’éditeur, version, route. Pour
GET et DELETE, les entrées vont dans la chaîne de requête ; sinon dans le corps JSON. Un refus
lève une erreur portant le message de l’API elle-même. Une application et un système extérieur qui
appellent la même API lisent la même réponse.
Stockage d’objets
Pour les fichiers qui n’appartiennent à aucun enregistrement en particulier : un export produit par l’application, un document qu’on lui a remis, un lot qu’elle s’apprête à traiter.
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);
- Un compartiment appartient au locataire, pas à l’environnement. Deux environnements d’un même locataire le partagent : mettez l’environnement dans la clé si leurs fichiers doivent rester séparés.
- Les noms de compartiment sont partagés entre tous les locataires, comme dans S3. Choisissez un nom qui vous est propre.
- Demandez
storage.readetstorage.writedans lesrequestedScopesde l’application. - Un fichier propre à un enregistrement va plutôt sur l’enregistrement (section suivante).
Les fichiers et images d’un enregistrement
Une colonne File ou Image contient un petit fichier qui appartient à cet enregistrement :
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');
- Le téléversement renseigne la colonne lui-même. Ne mettez pas le champ dans
createRecordni dansupdateRecord. - L’enregistrement doit exister d’abord. Créez-le, puis téléversez.
- Il n’y a pas de lien à mettre dans
<img src>. Le fichier est autorisé comme son enregistrement : affichez le Blob avecURL.createObjectURL(blob).
Les tables et leurs définitions
listEntities(), listFields(entityId), listViews(entityId), getViewDefinition(…),
listForms(entityId), getFormDefinition(…), listRelationships(), listOptionSets() et
getEntityStatus(entityId) décrivent l’environnement, pour une application qui dessine sa propre
grille ou son propre formulaire. getRecordPermissions(table) dit si la personne peut créer, lire,
mettre à jour ou supprimer dans une table, pour que l’application laisse de côté un bouton avant
qu’il ne soit refusé.
canImport dit si elle peut y charger une feuille de calcul, ce qui demande, en plus de créer, le
privilège Importer des données.
Journalisation
runtime.log('info', 'Visitor list saved', { rows: 8 });
runtime.log('error', 'Could not load visitors', { message });
Les niveaux sont debug, info, warn et error. L’application et l’environnement sont ajoutés
pour vous. Ne journalisez jamais de valeurs d’enregistrement, de données personnelles ni quoi que
ce soit venant d’un jeton. La console du navigateur est invisible pour qui assure le support de
l’application ; ceci ne l’est pas.
Gérer les échecs
Chaque appel renvoie une promesse qui peut être rejetée : pas de droit, un enregistrement supprimé entre-temps, un réseau coupé. Affichez le message, et cessez de modifier l’état une fois l’application fermée :
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]);
Ce que le SDK ne fait pas
- Il n’envoie pas de notifications. Il n’y a pas de surface de notification ; n’en simulez pas avec un appel HTTP direct.
- Il n’enregistre pas la valeur d’un contrôle personnalisé. Un contrôle valide par
onChange, et c’est le formulaire qui enregistre. - Il ne donne pas de jetons. Il n’y a rien à lire ni à stocker.
- Il n’atteint pas d’autres environnements. Déplacer du travail entre environnements, c’est exporter puis importer une solution.
Dépannage
useParswareRuntime()lève une erreur : le composant a été affiché hors d’un fournisseur de runtime. Dans le Shell cela ne peut pas arriver ; dans un test, enveloppez-le dansParswareRuntimeProvider.- Le runtime est là mais vide, ou les hooks échouent :
@parsware/runtime-sdkou React n’est passingleton: truedans le blocsharedde Module Federation, et l’application a embarqué sa propre copie. - Un appel est refusé : la personne qui utilise l’application n’a pas le droit de le faire ;
c’est son rôle de sécurité qui décide. Vérifiez
runtime.scopesetgetRecordPermissions, et laissez l’action de côté. - Une colonne de choix affiche un nombre : lisez
formattedValues, pasvalues, pour son libellé. Cannot find module …/dist/typessous Vitest : le SDK vient d’un ancien téléchargement. Installez l’actuel.
Scénarios associés
- Créer une application React personnalisée.
- Créer un contrôle personnalisé.
- Obtenir un jeton d’accès.