Saltar al contenido principal

Usar el Runtime SDK

Objetivo​

Entender el Runtime SDK, la única interfaz que una aplicación React personalizada o un control personalizado usa para llegar a la plataforma Parsware, y usar bien cada parte de él.

Requisitos previos​

  • Un proyecto preparado como en Crear una aplicación React personalizada o Crear un control personalizado, con @parsware/runtime-sdk instalado desde https://docs.parsware.com/sdk/parsware-runtime-sdk-1.0.0.tgz.
  • @parsware/runtime-sdk declarado como singleton compartido en la configuración de Module Federation del proyecto. Sin ello, la aplicación recibe su propia copia vacía del SDK en lugar de la del Shell.

La única regla​

Un artefacto alojado llega a la plataforma solo a través del Runtime SDK. El Shell vincula el runtime al entorno en el que abrió la aplicación y a la persona que la usa. No hay id de entorno que pasar, ni token que guardar, ni dirección que configurar, así que la misma compilación funciona en todos los entornos donde se instale. Una aplicación que llama ella misma a las API HTTP de la plataforma tiene que resolver las tres cosas, y se equivoca la primera vez que se instala en otro sitio.

Obtener el runtime​

Una aplicación React personalizada lo lee de un hook:

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

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

Un control personalizado recibe la misma superficie como su prop context:

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

Fuera del Shell, en una prueba unitaria, envuelva el componente en ParswareRuntimeProvider con un simulado: <ParswareRuntimeProvider runtime={stub}>.

Qué lleva el runtime​

MiembroQué es
environmentId, environmentNameEl entorno en el que se abrió la aplicación. El nombre se puede mostrar.
artifactIdEl nombre lógico de la aplicación o del control.
userLa persona que lo usa: id, name, email, tenantId, roles.
scopesLo que pidió la aplicación, reducido a lo que contiene la sesión de la persona.
dataTablas, registros, consultas, consultas guardadas, API personalizadas, informes y archivos de registro, vinculados al entorno.
storageAlmacenamiento de objetos: buckets y los archivos que contienen.
logRegistro estructurado, enviado a la plataforma con la aplicación y el entorno adjuntos.

Quién la usa y qué puede hacer​

runtime.user sirve para mostrar y saludar: un nombre, un rol según el cual elegir un diseño. No es una comprobación de permisos.

Cada llamada se hace como la persona que usa la aplicación. Su rol de seguridad decide qué puede leer y cambiar cada una, exactamente como en una aplicación basada en modelo, así que una aplicación nunca puede hacer más que la persona que la abrió, y dos personas que usan la misma aplicación ven cada una lo que se le permite.

runtime.scopes son los requestedScopes de la aplicación reducidos a la sesión de la persona. Léalos para dejar fuera lo que no funcionará:

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

Leer registros​

Una página de una tabla​

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 …

Una página, nunca la tabla entera. { pageSize: 1 } es la forma barata de leer solo un recuento.

Una consulta: columnas, vínculos y totales​

runQuery toma el mismo documento de consulta que envían las cuadrículas de la propia plataforma, así que puede elegir columnas, seguir búsquedas y sumar:

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
}

Con un bloque aggregate responde con grupos en lugar de filas (result.kind === 'Groups'). Pida los totales al servidor en lugar de traer todas las filas para sumarlas en el navegador.

Una consulta guardada, por su nombre​

Cuando la pregunta pertenece a la solución y no al código de la aplicación, guárdela en la solución y ejecútela por su nombre:

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 definición viaja entonces con la solución y puede mejorarse sin publicar una nueva compilación. Las opciones solo pueden restringirla: un filter adicional se combina con AND con el guardado, y page o top la limitan. Lo que lee, agrupa y cuenta sigue siendo lo de la consulta guardada.

Búsquedas y opciones​

  • searchLookup(table, { search }) encuentra candidatos para una búsqueda, con las columnas que muestra la vista de búsqueda de la tabla de destino. resolveLookup(table, id) convierte un id guardado en el texto que se muestra.
  • Una columna de opciones llega como el número de la opción, con su etiqueta al lado. values lleva el número guardado y formattedValues la etiqueta que lee una persona, igual que llega el nombre de una búsqueda. Los resultados agrupados la llevan en formattedGroups. listOptionSets() da la lista completa, para un selector.

Cambiar registros​

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 recibe solo las columnas que cambian. Devolver un registro entero reescribe valores que otra persona puede haber cambiado desde que usted los leyó. Los pasos de complemento del servidor se ejecutan en estas escrituras como en cualquier otra; las reglas de negocio de cliente de un formulario no, porque no hay formulario.

Llamar a una API personalizada​

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

La ruta es la dirección de la API bajo el entorno: prefijo del editor, versión, ruta. Para GET y DELETE las entradas van en la cadena de consulta; en los demás casos, en el cuerpo JSON. Un rechazo lanza el propio mensaje de la API. Una aplicación y un sistema externo que llaman a la misma API leen la misma respuesta.

Almacenamiento de objetos​

Para archivos que no pertenecen a un único registro: una exportación que hizo la aplicación, un documento que alguien le entregó, un lote que está a punto de procesar.

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 bucket pertenece al inquilino, no al entorno. Dos entornos de un mismo inquilino lo comparten, así que ponga el entorno en la clave si sus archivos deben quedar separados.
  • Los nombres de bucket se comparten entre todos los inquilinos, como en S3. Elija uno propio de usted.
  • Pida storage.read y storage.write en los requestedScopes de la aplicación.
  • Un archivo que es de un único registro va en el registro (siguiente sección).

Archivos e imágenes de un registro​

Una columna File o Image contiene un archivo pequeño que pertenece a ese registro:

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');
  • La carga establece la columna por sí misma. No ponga el campo en createRecord ni en updateRecord.
  • El registro debe existir primero. Créelo y después cargue.
  • No hay enlace que poner en <img src>. El archivo se autoriza como su registro, así que muestre el Blob mediante URL.createObjectURL(blob).

Las tablas y sus definiciones​

listEntities(), listFields(entityId), listViews(entityId), getViewDefinition(…), listForms(entityId), getFormDefinition(…), listRelationships(), listOptionSets() y getEntityStatus(entityId) describen el entorno, para una aplicación que dibuja su propia cuadrícula o formulario. getRecordPermissions(table) dice si la persona puede crear, leer, actualizar o eliminar en una tabla, para que la aplicación deje fuera un botón antes de que lo rechacen. canImport dice si puede cargar una hoja de cálculo en ella, lo que además de crear requiere el privilegio Importar datos.

Registro​

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

Los niveles son debug, info, warn y error. La aplicación y el entorno se adjuntan automáticamente. Nunca registre valores de registros, datos personales ni nada de un token. La consola del navegador es invisible para quien da soporte a la aplicación; esto no.

Gestionar errores​

Cada llamada devuelve una promesa que puede rechazarse: sin permiso, un registro eliminado mientras tanto, la red caída. Muestre el mensaje y deje de cambiar el estado una vez cerrada la aplicación:

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

Lo que el SDK no hace​

  • No envía notificaciones. No hay superficie de notificaciones; no la simule con una llamada HTTP directa.
  • No guarda el valor de un control personalizado. Un control confirma mediante onChange y el formulario guarda.
  • No entrega tokens. No hay nada que leer ni nada que guardar.
  • No llega a otros entornos. Mover trabajo entre entornos es exportar e importar una solución.

Solución de problemas​

  • useParswareRuntime() lanza un error: el componente se representó fuera de un proveedor de runtime. En el Shell no puede ocurrir; en una prueba, envuélvalo en ParswareRuntimeProvider.
  • El runtime está pero vacío, o fallan los hooks: @parsware/runtime-sdk o React no son singleton: true en el bloque shared de Module Federation, así que la aplicación empaquetó su propia copia.
  • Se rechaza una llamada: la persona que usa la aplicación no puede hacer eso; decide su rol de seguridad. Compruebe runtime.scopes y getRecordPermissions, y deje fuera la acción.
  • Una columna de opciones muestra un número: lea formattedValues, no values, para su etiqueta.
  • Cannot find module …/dist/types en Vitest: el SDK se instaló desde una descarga anterior. Instale la actual.

Escenarios relacionados​

  • Crear una aplicación React personalizada.
  • Crear un control personalizado.
  • Obtener un token de acceso.