@numaris-microfront/graphql-client
v0.2.4
Published
Cliente AppSync (query/mutate/subscribe realtime, sin Amplify) para la API GraphQL de la plataforma Numaris, con el schema real incluido.
Maintainers
Readme
@numaris-microfront/graphql-client
Cliente para hablar con la API GraphQL (AWS AppSync) de la plataforma Numaris: lecturas, escrituras y suscripciones en tiempo real, sin Amplify. Si tu widget necesita datos reales —no solo eventos de otros widgets vía el Event Bus— este es el paquete que instalás.
Qué es, en una frase
Transporte + cache, nada más. Es funciones async puras (createAppSyncClient) más un
cache de queries con claves explícitas y un hook de React (useQuery) para leerlo. No trae
ninguna query de dominio — esas las escribís vos, contra el schema que este paquete incluye.
Instalación
npm install @numaris-microfront/graphql-clientreact (^19.2.0) es peer dependency, y solo hace falta si usás useQuery. El resto del
paquete (createAppSyncClient, la cache, retryQuery) no depende de React en absoluto.
El schema real, incluido en el paquete
node_modules/@numaris-microfront/graphql-client/schema.graphqlEs una copia commiteada del schema real de api-poc-microfronts (no una transcripción a
mano), versionada junto con este paquete. Ábrilo para ver campos, tipos y argumentos exactos
antes de escribir una query — no lo adivines ni lo pidas por otro lado.
Dos cosas de ese schema que conviene saber antes de leerlo:
Device.telemetryes un resolver de CAMPO, no una query aparte. AppSync solo lo ejecuta si tu query lo pide explícitamente (devices { imei name telemetry { ... } }); pedirdevices { imei name }no paga ese costo. Diseñá tus queries pidiendo solo lo que usás.- El schema evoluciona en el backend, no acá. Si una query te devuelve un error de validación por un campo que el schema local dice que existe, es señal de que tu copia quedó atrás de lo desplegado — pedí una actualización por el canal de soporte (ver más abajo).
Instalación 2: conseguir el endpoint y las credenciales
A diferencia del schema, el endpoint de AppSync y los datos de Cognito son específicos del entorno (dev/staging/prod) y no viajan en este paquete — son configuración, no código. Necesitás pedirlos al equipo Numaris (ver "Cómo pedir acceso" al final de este README):
| Dato | Para qué |
|---|---|
| Endpoint GraphQL (https://…appsync-api…/graphql) | AppSyncClientConfig.endpoint |
| Endpoint realtime (wss://…appsync-realtime-api…/graphql) | Lo arma subscribe solo, a partir del endpoint GraphQL — no lo necesitás vos |
| User Pool ID, App Client ID, region | Para obtener un token real de Cognito (ver la sección de autenticación) |
No hay auth por API key. El único modo disponible al front es AMAZON_COGNITO_USER_POOLS
— todo request necesita un token de Cognito válido.
Uso
import { createAppSyncClient } from '@numaris-microfront/graphql-client';
const client = createAppSyncClient({
endpoint: import.meta.env.VITE_APPSYNC_ENDPOINT, // del .env de TU widget, no del host
getToken: () => session.getToken(), // te lo pasa el host por WidgetMountProps — ver más abajo
organizationId: session.organizationId, // opcional: viaja como header `x-org-id`
});
// Lectura. Reintenta automáticamente ante error de red o 5xx (hasta 3 intentos, backoff exponencial).
const data = await client.query<{ devices: { imei: string; name: string }[] }>({
query: `query { devices { imei name } }`,
});
// Escritura. NUNCA reintenta — un error de validación no es transitorio, y reintentar puede
// duplicar un efecto (por ejemplo, mandar un comando dos veces a un vehículo real).
await client.mutate({
query: `mutation($imei: String!) { registerDevice(imei: $imei) { imei } }`,
variables: { imei: '123456789012345' },
});
// Suscripción realtime (WebSocket, protocolo propio de AppSync). A diferencia de query/mutate,
// abre una conexión que VIVE hasta que llamás al Unsubscribe devuelto — creá el cliente UNA
// vez para toda la vida de la suscripción, no en cada reporte.
const unsubscribe = client.subscribe(
{ query: `subscription($channel: String!) { onUpdateTelemetry(channel: $channel) { imei } } ` },
{
onData: (data) => console.log(data),
onError: (error) => console.error(error),
onReconnected: () => {
// Se reconectó DESPUÉS de haber estado conectada (nunca en la primera conexión). Lo que
// pasó mientras estuvo caída no lo cubre ningún mensaje futuro de la suscripción — típicamente
// acá volvés a pedir el listado completo por query.
},
},
);getToken(), nunca un token guardado
El patrón se repite en todo este ecosistema por una razón concreta: los tokens de Cognito
expiran (una hora por default) y un cliente creado al montar tu componente, con un token
capturado en ese momento, se queda con uno muerto — el síntoma es una pantalla que anduvo bien
toda la demo y empieza a dar 401 después. getToken() se llama en cada request.
Si tu widget vive dentro de un host tipo Numaris Shell, getToken te lo da el contrato de
montaje (PocSession.getToken, en @numaris-microfront/contracts) — nunca lo obtenés vos
mismo, y tu widget no debería importar ningún SDK de autenticación.
x-org-id: selección, no autorización
Si pasás organizationId, viaja como header x-org-id. El servidor lo revalida contra la
membresía real del usuario (la reclama del token) — no es una credencial, es la organización
que el usuario eligió entre las que ya tiene permitidas. Pedir datos de una organización a la
que no pertenecés falla en el servidor sin importar qué mandes en este header.
El cache y useQuery
import { createQueryCache, useQuery } from '@numaris-microfront/graphql-client';
const cache = createQueryCache(); // uno por widget, nunca compartido entre widgets
function DeviceList() {
const { status, data, error, refetch } = useQuery(cache, 'devices', () =>
client.query({ query: `query { devices { imei name } }` }),
);
if (status === 'loading') return <Spinner />;
if (status === 'error') return <ErrorState error={error} onRetry={refetch} />;
return <ul>{data.devices.map((d) => <li key={d.imei}>{d.name}</li>)}</ul>;
}Reglas de diseño de este cache, no accidentes:
- La clave la elegís vos, explícita (
'devices'arriba) — nunca se deriva del campo que leés de la respuesta. Usar el mismo string para las dos cosas hace que un desalineamiento devuelvaundefineden silencio en vez de un error. - No copies el resultado a
useState. El componente deriva del cache (useSyncExternalStorepor debajo); una copia enuseStatees una segunda fuente de verdad que diverge la primera vez que alguien invalida o refetchea. refetch()pasa porstatus: 'loading'(para un spinner).refetchSilently()no — es para una actualización de fondo que el usuario no pidió (por ejemplo, reconectar un WebSocket) y que no debería prender ningún spinner.- El cache no se comparte entre widgets. Cada uno crea el suyo. Si tu integración necesita compartir datos entre dos partes de tu propia app, es una decisión tuya — pero nunca a través de este paquete ni del Event Bus de Numaris (ese es solo para eventos de dominio).
Autenticar TU PROPIO backend con el mismo Cognito
Si además de (o en vez de) hablar con la API de Numaris tu widget necesita hablar con un backend propio, podés reusar el mismo token de Cognito que ya tenés — no hace falta un sistema de auth aparte.
El patrón:
Tu widget nunca "sabe" que existe Cognito ni importa
aws-amplify— solo recibe un string (getToken()) del host y lo reenvía como header a donde haga falta:const response = await fetch('https://tu-backend.example.com/api/algo', { headers: { Authorization: await session.getToken() }, });Tu backend valida ese JWT server-side, sin necesitar ningún secreto compartido con Numaris — Cognito publica sus claves públicas de verificación:
https://cognito-idp.<region>.amazonaws.com/<user-pool-id>/.well-known/jwks.jsonVerificá: la firma contra ese JWKS,
iss(issuer) igual ahttps://cognito-idp.<region>.amazonaws.com/<user-pool-id>,aud/client_idigual al App Client ID que te dieron, yexpno vencido. La mayoría de los lenguajes tienen una librería que hace esto sin escribir la verificación a mano — por ejemploaws-jwt-verifyen Node/TypeScript.Si tu backend necesita saber a qué organización pertenece el usuario, leé el claim
cognito:groupsdel token decodificado (después de verificarlo) — ahí viajan los grupostenant:<organizationId>de los que el usuario es miembro. No confíes en unorganizationIdque tu propio frontend te mande sin cruzarlo contra ese claim: el claim es lo único que el servidor de identidad firmó.
Lo que NO tenés que hacer: ni tu widget ni tu backend necesitan el App Client Secret (el client público de esta plataforma no tiene uno) ni credenciales de AWS — la verificación del JWT es pública y no requiere llamar a ningún API de AWS en el camino caliente.
Si tu backend es completamente independiente y no necesita saber nada de la organización activa de Numaris, también podés ignorar el token por completo y usar tu propio esquema de auth — el contrato de montaje no te obliga a nada más que recibirlo.
Cómo pedir acceso a un entorno real
El endpoint, los IDs de Cognito y un usuario de prueba para desarrollo no son públicos — son
configuración de un entorno concreto (dev/staging), gestionada por Terraform en
Easytrack-Next/api-poc-microfronts. Pedilos por el canal de soporte/onboarding que te haya
indicado tu contacto en Numaris (ver la guía de "cómo desarrollar un widget" para el proceso
completo). Mientras tanto, desarrollá y probá tu widget contra datos de fixture — ninguna parte
de este paquete requiere red para eso, solo lo necesitás cuando pasás a integrar de verdad.
