Construye con BQL
BlitzStore es una base de datos de grafos polimórfica. Define tus datos como , conéctalos con y consúltalo todo, incluidas las relaciones anidadas, en una sola petición JSON.
Conectar
Configura el acceso HTTP, elige la familia de tokens adecuada y mantén la auth de sesión de la aplicación separada de la auth del servidor de datos.
Autenticarse
El servidor de datos de BlitzStore solo acepta Authorization: Bearer <token>. No existe una cabecera API-Key. Cuatro familias de tokens usan ese mismo transporte:
bzt_*: grant tokens de operador con alcance para clientes (alcance de servidor, de base de datos o de namespace). Acúñalos desde el plano de control de BlitzGraph y úsalos en las llamadas al plano de datos (/query,/mutate,/admin).bzu_*: credenciales de sujeto para los usuarios finales de un portal, ligadas a una unidad dentro de un namespace y confinadas a la superficie de aplicación para la que se acuñaron. La API en crudo las rechaza conSURFACE_CONFINED.bzi_*: claves internas por instancia, sin restricciones, reservadas para una recuperación de emergencia explícita.bzc_*: capacidades de servicio de 60 segundos ligadas a una instancia, a una acción HTTP y a un alcance de base de datos o de namespace. Algunas rutas internas de servicio aceptan la acción correspondiente; la administración de claves y de blobs sigue siendo exclusiva debzi_*.
curl -X POST https://api.blitzgraph.com/query \ -H "Content-Type: application/json" \ -H "Authorization: Bearer bzt_your.token-here" \ -d '{"query":{"$kinds":"User","$fields":"*","$limit":10}}'
La rotación en caliente de las claves bzi_* se hace por /server/keys/upsert; los grant tokens nuevos se proyectan por /server/grants/upsert.
Los bearer access tokens de Better Auth no autentican directamente contra el servidor de datos. Autentican sesiones de personas o de agentes en rutas de aplicación de BlitzGraph como POST /agents/build-now, que a su vez emiten o reutilizan un grant de operador bzt_* con alcance para la cuenta de BlitzGraph que ha iniciado sesión. El servidor de datos solo llega a ver el bzt_* acuñado.
El usuario final de un portal va por otro carril: su sesión la acuña el servidor de datos como una credencial de sujeto bzu_*, confinada a la única superficie de aplicación para la que se emitió y rechazada en la API en crudo.
El flujo de conexión OAuth de MCP autoriza el grant de conexión emitido contra un Space, no contra un namespace o un servidor fijos. Cada llamada resuelve en vivo la ubicación actual del Space, así que mover un Space nunca invalida un grant existente. Las herramientas heredan el destino de namespace seleccionado; usa opts.defaultSubspace cuando una llamada concreta deba apuntar a otro subspace por defecto.
Plataforma
Los alcances de almacenamiento aíslan los datos; las consultas en vivo los mantienen sincronizados; los workflows duraderos orquestan el trabajo; los límites efectivos explican la capacidad; y BlitzStudio opera todas esas superficies.
Namespaces y subspaces
Un es el límite de aislamiento de nivel superior dentro de una base de datos. Cada uno tiene su propio esquema, sus datos y sus índices. Un es otro límite de aislamiento dentro de ese namespace: los datos, el esquema, los portales y los índices quedan acotados a él. Los usuarios del plan gratuito tienen un namespace y pueden crear varios subspaces (por ejemplo main, archive, drafts).
// HTTP envelope default { "query": { "$kinds": "Note", "$fields": "*" }, "opts": { "defaultSubspace": "archive" } } // Per-query override { "$kinds": "Note", "$subspace": "archive", "$fields": "*" } // Per-mutation override { "$setKinds": ["Note"], "content": "temp", "$subspace": "drafts" } // Omit $subspace → defaults to "main" { "$kinds": "Note", "$fields": "*" }
// POST /admin: create a subspace { "admin": { "$resource": "subspace", "$op": "create", "$rid": "ss:archive", "storage": "memory" } } // POST /admin: list subspaces { "admin": { "$resource": "subspace", "$op": "query" } }
Los datos están totalmente aislados entre subspaces. Una consulta sobre "main" nunca ve datos de "archive". Cada subspace puede tener su propio esquema, definido con más opts.defaultSubspace, o con ns.defaultSubspace("name").schema.import() en el SDK.
Para transferencias autoservicio con alcance de namespace, usa las rutas /namespace/bundle/* y /subspace/bundle/*. Emiten bundles .bzg estrictos y ejecutan las importaciones en segundo plano, así que puedes volver a subir el mismo bundle para reanudar una importación interrumpida que coincida.
El $history por unidad es ilimitado por defecto. Fija history.retentionDays o history.maxEventsPerUnit en la configuración del namespace para acotarlo; un barrido en segundo plano elimina los eventos que superan la ventana de retención o el tope, y las lecturas de $history solo ven las entradas que sobreviven.
Consultas en vivo
Una mantiene un resultado BQL ordenado sincronizado por SSE. El carril en crudo POST /query/live es para grants con alcance y claves internas. Las sesiones de de un portal solo se suscriben mediante una operación de consulta con nombre y validada en /_blitz/ops/query/{op}/live, así que un navegador nunca obtiene la capacidad de consulta en crudo.
for await (const frame of client.queryLive({
query: { $kinds: 'Ticket', $sort: ['priority', '$id'], $fields: '*' },
subspace: 'main',
resume: lastAppliedCheckpoint,
})) {
// Persist frame.checkpoint only after the whole snapshot/patch applies.
applyAtomically(frame)
}// Scoped grant / internal-key application const tickets = useLiveQuery({ $kinds: 'Ticket', $sort: ['$id'], $fields: '*' }) // Portal AppUser: generated named-operation ref const myTickets = useLiveOperation(myTicketsRef, { status: 'open' })
Un LIVE_CONTINUITY_GAP reintentable significa que el servidor ha acotado un hueco y lo está reparando. LIVE_RESUME_UNAVAILABLE es estable: un operador debe configurar BLITZSTORE_SECRET_LIVE_RESUME_KEYRING antes de que la reanudación esté disponible. El store del cliente espacia las reconexiones y recurre a una instantánea autorizada nueva cuando ya no se puede usar una repetición retenida.
Acciones y workflows
Disponible en BlitzGraph 0.61.0. Una Action es un paso tipado y reutilizable; un es una máquina de estados duradera que orquesta Actions, BQL, esperas, señales, workflows anidados y fan-out acotado. Las definiciones pasan por el linter antes de persistirse, y cada ejecución fija su clausura ejecutable y el digest del programa.
{ $op: 'create', $type: 'action', name: 'scoreLead',
type: 'pure', mode: 'inline',
inputSchema: { type: 'object', properties: { score: { type: 'number' } } },
outputSchema: { type: 'number' },
$ts: 'return (input as { score: number }).score satisfies number'
}{ $op: 'create', $type: 'action', name: 'protectedScore',
type: 'pure', mode: 'wasm',
module: 'scorer@sha256:<64 lowercase hex>',
inputSchema: { type: 'object' },
outputSchema: { type: 'number' }
}El código en línea acepta $js, $ts o $code explícito. TypeScript se guarda tal como se escribió, pero el servidor rechaza imports, módulos, JSX, decoradores, enums, namespaces y demás sintaxis que genere runtime antes de que una definición persista. Los diagnósticos de Studio usan el mismo manifiesto generado de perfil restringido; el linter del servidor sigue siendo la autoridad.
const uploaded = await client.uploadCodeModule('scorer', wasmBlob)
const pinned = uploaded.data.module // name@sha256:<digest>, abiVersion: 1
await client.listCodeModules()
await client.getCodeModule(pinned)
const originalBytes = await client.downloadCodeModule(pinned)
await client.deleteCodeModule(pinned) // rejected while an Action references itWASM solo está disponible para pure×wasm. Los componentes exportan process(string) → result<string, string>, no tienen imports del host y se ejecutan con memoria, salida, errores, logs, bytes de caché y plazos acotados. El servidor fija y valida la ABI de WASM en el programa de workflow duradero; las definiciones públicas de Action no aceptan un campo abiVersion.
const run = await client.runWorkflow('onboardCustomer', {
idempotencyKey: 'signup:customer-01J...',
trigger: { customerId: 'customer-01J...' },
})
const runId = run.data.runId
const status = await client.getTask(runId)
for await (const event of client.taskStream(runId)) {
console.log(event)
}La recuperación tras un fallo reanuda toda la pila de llamadas y el cursor de compensación pendiente. La pertenencia de los hijos mapeados se registra antes del despacho, así que los joins y las cancelaciones no pierden hijos tras un relevo de worker. MCP expone el mismo ciclo de vida con workflows.run, workflows.runStatus y las herramientas de tareas y eventos.
Límites efectivos
Un es el valor que el servidor aplica de verdad una vez resueltos los valores por defecto, la configuración, las anulaciones de entorno y el alcance de la petición. El catálogo y los detalles de rechazo son proyecciones de ese mismo objeto, así que los valores documentados y los aplicados no pueden divergir.
const { data: limits } = await client.effectiveLimits()
for (const limit of limits) {
console.log(limit.limitId, limit.effectiveValue, limit.unit)
}GET /limits/effective
// MCP tool: no input body
limits.effectiveBlitzStudio
BlitzStudio es la interfaz visual integrada para gestionar tus datos. Se conecta a cualquier instancia de BlitzStore y te da un espacio de trabajo completo con edición de esquema, exploración de datos y una consola de consultas en vivo.
Esquema
Define tu modelo de datos con , campos, , validaciones, campos calculados y hooks de mutación. Importa tu una vez y el mismo modelo alimenta /query, /mutate y BlitzStudio.
Estructura de la base de datos
BlitzStore separa los (tus unidades y conexiones) de las (el y los ).
POST /queryyPOST /mutateoperan sobre los datosPOST /definitions/import,/definitions/queryy/definitions/mutateoperan sobre las definiciones (mira )POST /adminusa JSON de administración en crudo, no un sobre{ body, opts }POST /data/importdevuelve JSON por defecto; añadeAccept: text/event-streampara recibir el progreso por SSE
Clases y campos
- Un es una definición de esquema (como una clase o una tabla)
- Una es una instancia almacenada de una o varias clases
- Las clases tienen tres tipos de campo:
- contiene valores, validaciones y comportamiento de cálculo
- define los puntos de conexión (mira )
- es un atajo para recorrer desde el otro lado
- El comportamiento de mutación que abarca varios campos va en los a nivel de clase
{
"kinds": {
"User": {
"dataFields": {
"name": { "valueType": "TEXT" },
"email": { "valueType": "EMAIL", "unique": true },
"age": { "valueType": "INTEGER" }
}
},
"Article": {
"dataFields": {
"title": { "valueType": "TEXT", "required": true, "fts": true },
"body": { "valueType": "TEXT", "fts": true },
"status": { "valueType": "TEXT" }
}
}
}
}Un campo FILE almacena un blob más su metainformación. El cliente de TS detecta automáticamente los valores File / Blob en mutate() y cambia a multipart; el cuerpo JSON lleva un marcador { "#file": "<key>" } que el servidor asocia con la parte con ese nombre. Al leer, el valor se materializa como FileValue con filename / mime / size / url / thumbnail_url.
// multipart body has parts: "mutation" (JSON) + named file parts { "$setKinds": ["Document"], "title": "Annual report", "attachment": { "#file": "file_0" } } // → server stores blob, returns FileValue with signed url
Cambiar el valueType de un campo entre INTEGER, DECIMAL, FLOAT y PERCENTAGE funciona. La mutación de esquema responde de inmediato; una tarea en segundo plano reescribe las filas almacenadas según la numericPolicy del campo: rejectOnLoss (por defecto), round, truncate o allowPrecisionLoss. NaN y ±Inf se rechazan siempre.
Polimorfismo y herencia
- Polimorfismo. Una puede tener varias a la vez y hacerlas evolucionar con el tiempo. Un User puede ganar o perder Admin sin volver a crearse.
- Herencia. Una clase puede declarar
extends; hereda los campos y los roles de esa clase padre, y las consultas$kinds: "Parent"coinciden con todos sus descendientes por defecto.
// Create a unit with two kinds at once { "$setKinds": ["User", "Admin"], "name": "Alice" } // Add a kind to an existing unit { "$id": "user_abc", "$setKinds": [{ "$op": "add", "$kinds": ["Moderator"] }] } // Remove a kind (at least one must remain) { "$id": "user_abc", "$setKinds": [{ "$op": "remove", "$kinds": ["Admin"] }] }
{
"kinds": {
"User": { "dataFields": { "email": { "valueType": "EMAIL" } } },
"Admin": { "extends": "User", "dataFields": { "level": { "valueType": "INTEGER" } } },
"SuperAdmin": { "extends": "Admin" }
}
}
// Matches User + Admin + SuperAdmin
{ "$kinds": "User" }
// Exact match, no descendants
{ "$kinds": "User", "$descendants": false }Validaciones y campos calculados
Las reglas de campo viven junto a cada . Usa los validadores integrados para comprobar tipos y rangos, validadores $js propios para las reglas de negocio y campos calculados cuando un valor deba derivarse en lugar de almacenarse. Los campos calculados también pueden leer valores meta como $id o $createdAt.
{
"kinds": {
"User": {
"dataFields": {
"age": {
"valueType": "INTEGER",
"validations": { "required": true, "min": 0, "max": 150 }
},
"email": { "valueType": "EMAIL" },
"backupEmail": {
"valueType": "TEXT",
"validations": {
"custom": [{
"on": ["create", "update"],
"$js": "$value.includes('@')",
"message": "must be a valid email",
"severity": "error"
}]
}
}
}
}
},
"opts": { "defaultSubspace": "main" }
}{
"kinds": {
"User": {
"idField": "email",
"dataFields": {
"firstName": { "valueType": "TEXT" },
"familyName": { "valueType": "TEXT" },
"email": { "valueType": "EMAIL", "unique": true },
"fullName": {
"valueType": "FLEX",
"computeType": "computed",
"$js": "firstName + ' ' + familyName"
},
"displayId": {
"valueType": "FLEX",
"computeType": "computed",
"$js": "$id"
},
"role": {
"valueType": "TEXT",
"computeType": "editable",
"$js": "'user'"
}
}
}
}
}- Los tipos de contenido como
EMAILya ejecutan la validación del sistema - Los validadores propios usan
$valuey pueden apuntar acreate, aupdateo a ambos severity: "error"bloquea la mutación
computeType: "computed"se ejecuta en cada consulta y rechaza las escriturascomputeType: "editable"+$jsactúa como valor por defecto al crear, que luego se puede sobrescribir- Los campos calculados pueden referirse a
$id,$iid,$version,$createdAt,$updatedAty$kinds
Hooks
Los hooks viven en la parte alta del esquema, en un mapa schema.hooks indexado por nombre, y apuntan a Units mediante { ops, $kinds, $filter }. Cada hook declara su categoría con type (unit.validate, unit.transform o unit.effect) y se ejecuta dentro del pipeline de mutación cuando su target coincide con el borrador de la Unit. Úsalos cuando una escritura deba derivar campos, validar el borrador final combinando varios campos o lanzar efectos secundarios después del commit.
{
"kinds": {
"BlogPost": {
"dataFields": {
"title": { "valueType": "TEXT" },
"slug": { "valueType": "TEXT" },
"status": { "valueType": "TEXT" },
"word_count": { "valueType": "INTEGER" }
}
}
},
// Hooks live as a sibling of kinds at the schema level (refactored 2026-05-09).
// Each hook declares its category via "type" and applicability via "target".
"hooks": {
// transform: derive or normalize fields before validation
"BlogPost.generateSlug": {
"type": "unit.transform",
"target": {
"ops": ["create", "update"],
"$kinds": { "$any": ["BlogPost"] }
},
"when": { "$js": "$this.title" },
"$js": "({ slug: $this.title.toLowerCase().replace(/\\s+/g, '-') })"
},
"BlogPost.defaultStatus": {
"type": "unit.transform",
"target": {
"ops": ["create"],
"$kinds": { "$any": ["BlogPost"] }
},
"when": { "$js": "!$this.status" },
"$js": "({ status: 'draft' })"
},
// validate: reject an invalid final draft state
"BlogPost.requireTitle": {
"type": "unit.validate",
"target": {
"ops": ["create", "update"],
"$kinds": { "$any": ["BlogPost"] }
},
"$js": "$this.title && $this.title.length > 0",
"message": "Title is required",
"severity": "error"
},
// effect: post-commit side effects (no pre-effect on schema.hooks)
"BlogPost.postLog": {
"type": "unit.effect",
"target": {
"ops": ["create"],
"$kinds": { "$any": ["BlogPost"] }
},
"$js": "true"
}
}
}{
"kinds": {
"Person": {
"dataFields": {
"name": { "valueType": "TEXT" }
}
}
},
"hooks": {
"Person.upcase": {
"type": "unit.transform",
"target": {
"ops": ["create", "update"],
"$kinds": { "$any": ["Person"] }
},
"remote": "upcase_name@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
}
}- El orden es transform → validate → effect
- Los hooks de validación ven el estado final de la transacción y también pueden inspeccionar
$input - Los hooks que apuntan a clases ancestras pueden incluir descendientes con
$descendants - Los hooks de transformación pueden devolver parches en línea o con un
returnexplícito - El cuerpo de un hook acepta
$js,$ts(con los tipos borrados) o un módulo WASMremotefijado
- Los hooks mutan
$this; las cascadas entre unidades todavía no están soportadas unit.effectse ejecuta después del commit; las comprobaciones que bloquean van enunit.validate- Los hooks de transformación sobre
linktodavía no están implementados - Los bucles de transformación que no convergen fallan por la protección de profundidad máxima
Cuando se borra una unidad y esa ruptura corta sus relaciones, cada ruptura llega a los hooks de update de la unidad superviviente como un evento unlink en $delta.arcs. Ese mismo flujo aparece también en $history y en la salida normalizada de la mutación, así que un superviviente puede reaccionar a perder una conexión.
{
"hooks": {
// Post survives when its author User is deleted; the broken arc
// arrives as an unlink entry in $delta.arcs on the survivor's update.
"Post.markUnlinkSeen": {
"type": "unit.transform",
"target": {
"ops": ["update"],
"$kinds": { "$any": ["Post"] }
},
"$js": "var arcs = ($delta && $delta.arcs) || []; return { unlinkSeen: arcs.some(a => a.$op === 'unlink') };"
}
}
}Relaciones
- Un vive en el lado de la relación y define quién puede conectarse y cuántos ()
- Un vive en el lado del jugador y nombra la clase de relación más el rol que juega en ella
- Las también son clases, así que pueden llevar sus propios campos de datos y consultarse directamente
target: "relation"devuelve las unidades de relación;target: "role"ytargetRolesproyectan hasta el extremo o los extremos opuestos
Hay dos formas que conviene recordar. Para una dependencia simple como libro → autor, pon el en un lado y deja el inverso como . Eso es una relación directa, no un túnel. La regla de Giulietta es solo una recomendación para elegir el propietario. Cuando la conexión necesita sus propios campos, crea una clase de relación intermedia y expón un enlace en crudo más un túnel proyectado en cada lado si eso mejora la ergonomía de las consultas.
// Direct relation, no tunnel. // One valid ownership choice: Book owns the roleField, Author keeps the reverse linkField. { "kinds": { "Author": { "dataFields": { "name": { "valueType": "TEXT" }, "slug": { "valueType": "TEXT", "unique": true } }, "linkFields": { "books": { "relation": "Book", "plays": "author" } } }, "Book": { "dataFields": { "title": { "valueType": "TEXT" }, "publishedYear": { "valueType": "INTEGER" } }, "roleFields": { "author": { "playedBy": ["Author"], "cardinality": "ONE", "required": true } } } } }
{
"kinds": {
"Company": {
"dataFields": {
"name": { "valueType": "TEXT" }
},
"linkFields": {
"memberships": {
"relation": "Membership",
"plays": "company",
"target": "relation"
},
"employees": {
"relation": "Membership",
"plays": "company",
"target": "role",
"targetRoles": ["employee"]
}
}
},
"Employee": {
"dataFields": {
"name": { "valueType": "TEXT" },
"email": { "valueType": "EMAIL" }
},
"linkFields": {
"memberships": {
"relation": "Membership",
"plays": "employee",
"target": "relation"
},
"companies": {
"relation": "Membership",
"plays": "employee",
"target": "role",
"targetRoles": ["company"]
}
}
},
"Membership": {
"dataFields": {
"title": { "valueType": "TEXT" },
"startDate": { "valueType": "DATE" }
},
"roleFields": {
"company": { "playedBy": ["Company"], "cardinality": "ONE" },
"employee": { "playedBy": ["Employee"], "cardinality": "ONE" }
}
}
}
}{
"$kinds": "Company",
"$fields": [
"name",
{
"$expand": "memberships",
"$fields": [
"title",
"startDate",
{ "$expand": "employee", "$fields": ["name", "email"] }
]
}
]
}required para exactamente 1.min y max.{ "#intervals": ... }. Los resultados no llevan clave de casting; su forma sigue el intervalFormat del campo.Un rol lo pueden jugar varias clases mediante playedBy: ["A", "B"]. Las creaciones anidadas deben indicar $setKinds para que el motor sepa qué clase crear; cuando playedBy tiene exactamente una clase, se infiere.
{
"kinds": {
"Employee": {
"dataFields": { "name": { "valueType": "TEXT" } }
},
"Agency": {
"dataFields": { "name": { "valueType": "TEXT" } }
},
"Project": {
"dataFields": { "name": { "valueType": "TEXT" } },
"linkFields": {
"assignments": {
"relation": "Assignment",
"plays": "project",
"target": "relation"
},
"assignees": {
"relation": "Assignment",
"plays": "project",
"target": "role",
"targetRoles": ["assignee"]
}
}
},
"Assignment": {
"roleFields": {
"project": { "playedBy": ["Project"], "cardinality": "ONE" },
"assignee": { "playedBy": ["Employee", "Agency"], "cardinality": "ONE" }
}
}
}
}
// Nested create: $setKinds disambiguates the polymorphic assignee
{
"$setKinds": ["Assignment"],
"project": "project_123",
"assignee": { "$setKinds": ["Agency"], "name": "Acme Staffing" }
}symmetric: true en un campo de rol colapsa A↔B y B↔A en una sola arista: el duplicado se rechaza con SYMMETRIC_RELATION_EXISTS. Por defecto, cada rol rechaza que el mismo jugador aparezca en dos roles distintos de la misma unidad de relación (SELF_RELATION_FORBIDDEN); actívalo rol a rol con allowSelfRelation: true.
{
"kinds": {
"Friendship": {
"roleFields": {
"friends": { "playedBy": ["User"], "cardinality": "MANY", "symmetric": true }
}
},
"Reference": {
"roleFields": {
"from": { "playedBy": ["Document"], "allowSelfRelation": true },
"to": { "playedBy": ["Document"], "allowSelfRelation": true }
}
}
}
}Operaciones de esquema
Cuatro endpoints gestionan las en tiempo de ejecución: import, export, query y mutate. Import y export usan cuerpos JSON directos con opts.defaultSubspace. Query y mutate usan los sobres query / mutation.
Todo cuerpo de importación de definiciones (/definitions/import, /schema/import, /portals/import, /automations/import) debe llevar $definitionContract: lee la huella desde GET /health (campo $definitionContract) o deja que el SDK de JS la estampe automáticamente; un cuerpo sin ella se rechaza con 422.
// POST /definitions/import { // Required: this build's definitions wire fingerprint. // Read it from GET /health ("$definitionContract"); the JS SDK stamps it for you. "$definitionContract": "<value from GET /health>", "schema": { "kinds": { "User": { "dataFields": { "name": { "valueType": "TEXT" }, "email": { "valueType": "EMAIL", "unique": true } }, "linkFields": { "posts": { "relation": "Post", "plays": "author" } } }, "Post": { "dataFields": { "title": { "valueType": "TEXT" } }, "roleFields": { "author": { "playedBy": ["User"], "cardinality": "ONE" } } } } }, "opts": { "defaultSubspace": "main" } }
// POST /definitions/export { "opts": { "defaultSubspace": "main" } } // → { "data": { "$bzv": "0.34.0", "$definitionContract": "…", "schema": { "kinds": { ... } }, "portals": { ... } } } // The exported "$definitionContract" round-trips straight back into /definitions/import. // For schema-only export: POST /schema/export with the same opts shape. // For per-field type metadata: GET /docs/fields/content-types.
Los endpoints de definiciones de arriba solo mueven el esquema y los portales. Quien opera a nivel de store puede mover un namespace entero con sus artefactos almacenados (/namespace/export y /namespace/import). Los usuarios con alcance de namespace usan en su lugar las rutas de bundle .bzg: el bundle lleva la misma carga verificada, las importaciones corren en segundo plano y volver a subir el mismo bundle reanuda una importación interrumpida que coincida.
// Preview bundle contents GET /namespace/bundle/plan // Download a .bzg bundle (writes pause only while the artifact is staged) POST /namespace/bundle/export { "include": { "definitions": true, "units": true, "files": false }, "subspaces": ["main"] } // Upload the bundle; import completes asynchronously POST /namespace/bundle/import?definitions=true&units=true&files=false Content-Type: multipart/form-data bundle=@main.bzg // → 202 { "importId": "01...", "resumed": false } // Poll GET /admin/import-status until terminal.importId matches.
El viaje de ida y vuelta de los FILE lo controla include.files. En las rutas de bundle vale false por defecto para evitar salidas de datos accidentales; ponlo a true para llevar todos los blobs, o a un objeto { "perSubspace": { ... } } para campos FILE concretos. Las rutas de artefactos a nivel de store ponen include.files a true por defecto porque la transferencia se queda en el servidor.
La autorización de los usuarios de aplicación también se escribe en las definiciones. Una clase sujeto se marca con userKind y un campo de identidad de correo. Las clases protegidas usan reglas permissions evaluadas con $me. Los portales usan reglas access para la admisión de app, de página o de endpoint.
{
"schema": {
"kinds": {
"Member": {
"userKind": true,
"idField": "email",
"emailField": "email",
"dataFields": {
"email": { "valueType": "EMAIL", "unique": true },
"name": { "valueType": "TEXT" }
}
},
"Admin": {
"extends": "Member",
"userKind": true
},
"Note": {
"dataFields": {
"title": { "valueType": "TEXT" },
"privateText": { "valueType": "TEXT" }
},
"roleFields": {
"owner": { "playedBy": ["Member"], "cardinality": "ONE" }
},
"permissions": {
"query": "owner",
"create": { "$me.$kinds": "Member" },
"fields": {
"privateText": { "query": { "$me.$kinds": "Admin" } }
}
}
}
}
},
"env": {
"domains": {
"notes-auth": {
"secrets": ["password_pepper", "resend_api_key"]
}
}
},
"portals": {
"apps": {
"notes": {
"slug": "notes",
"env": ["notes-auth"],
"access": { "$kinds": ["Member"] },
"auth": {
"providers": {
"emailPassword": {
"enabled": true,
"pepper": { "$secret": "env.notes-auth.password_pepper" }
}
},
"delivery": {
"provider": "resend",
"apiKey": { "$secret": "env.notes-auth.resend_api_key" },
"fromAddress": "Notes <no-reply@example.com>"
},
"signup": {
"mode": "open",
"blueprint": {
"$setKinds": ["Member"],
"$authAnchorKind": "Member",
"email": { "$auth": "email" },
"name": { "$input": "name" }
}
}
},
"pages": {
"/": {
"tsxSource": "export default function NotesPage() { return null }"
}
}
}
}
}
}// POST /definitions/query: all kinds { "query": { "$type": "kind" }, "opts": { "defaultSubspace": "main" } } // Query a specific kind by $did { "query": { "$type": "kind", "$did": "K:abc123" } } // Query fields of a kind { "query": { "$type": "dataField", "$filter": { "ownerKind": "User" } } }
// POST /definitions/mutate { "mutation": [ { "$op": "create", "$type": "kind", "name": "Product", "dataFields": { "title": { "valueType": "TEXT" }, "price": { "valueType": "INTEGER" } } }, { "$op": "create", "$type": "roleField", "ownerKind": "Order", "name": "customer", "playedBy": ["User"], "cardinality": "ONE" }, { "$op": "create", "$type": "linkField", "ownerKind": "User", "name": "orders", "relation": "Order", "plays": "customer", "target": "relation" } ], "opts": { "defaultSubspace": "main" } }
Consultas
Cada consulta es un objeto JSON que se envía a POST /query. Los resultados vuelven como JSON y su forma es predecible a partir de tu consulta.
Fundamentos
Usa $kinds para seleccionar por clase, $id para traer una unidad concreta y $fields para controlar qué vuelve.
// Returns all User units (array) { "$kinds": "User", "$fields": "*" }
// Returns one unit (object or null) { "$id": "abc123", "$fields": "*" }
{
"$kinds": "User",
"$fields": ["name", "email", "age"],
"$sort": [{ "$by": "age", "$order": "desc" }],
"$limit": 10,
"$offset": 20
}Para conjuntos de resultados grandes, prefiere $cursor antes que $offset. Pasa $cursor junto con $limit; la respuesta devuelve un token opaco meta.$nextCursor que reenvías tal cual como el siguiente $cursor. Si no aparece meta.$nextCursor, has llegado a la última página. Nunca analices ni construyas el token. La paginación por cursor requiere $limit, excluye $offset, solo vale en la consulta raíz y se rechaza con $groupBy y con agregados en la raíz.
// First page { "$kinds": "Task", "$limit": 50 } // → { "data": [ ... ], "meta": { "count": 50, "$nextCursor": "eyJ..." } } // Next page: replay meta.$nextCursor verbatim as $cursor { "$kinds": "Task", "$limit": 50, "$cursor": "<token from meta.$nextCursor>" } // No meta.$nextCursor in the response → last page
Filtros y búsqueda
Usa $filter para las condiciones a nivel de campo. Los valores directos son la forma abreviada de $eq. Usa $search para la búsqueda de texto completo en los campos marcados con fts: true. $fuzzy va en $filter, no en $search: no distingue mayúsculas, usa distancia 2 por defecto, admite $distance de 0 a 10 y ordena automáticamente las coincidencias más cercanas cuando no pasas $sort.
{
"$kinds": "User",
"$filter": {
"status": "active",
"age": { "$gte": 18 }
},
"$fields": ["name", "email"]
}{
"$kinds": "User",
"$filter": { "name": { "$fuzzy": "alcie", "$distance": 1 } },
"$fields": ["name"],
"$limit": 5
}La igualdad con null significa contenido vacío, no un null JSON almacenado: { "tags": null } se pliega a { "tags": { "$isEmpty": true } }. Usa $exists cuando necesites presencia, y usa $has, $any, $all o $exact para los campos MANY.
{
"$kinds": "Article",
"$search": "rust programming",
"$fields": ["title", "$score"],
"$limit": 10
}Proyecciones
$fields controla qué datos vuelven. Usa "*" para todos los campos, un array para campos concretos o $excludedFields para excluir. Los campos de arco en $fields devuelven los IDs sin expandir. Usa $expand para traer las unidades completas. Cuando varias clases compuestas definen el mismo nombre de campo de forma distinta, usa $definedOn para elegir la clase que lo define; una fila multiclase realmente ambigua devuelve un marcador $ambiguous en lugar de adivinar.
// All fields { "$kinds": "User", "$fields": "*" } // Specific fields only { "$kinds": "User", "$fields": ["name", "email"] } // All except some { "$kinds": "User", "$fields": "*", "$excludedFields": ["password"] } // Arc fields without $expand → raw IDs { "$kinds": "Post", "$fields": ["title", "author"] } // → { "title": "Hello", "author": "user_abc123" }
{
"$kinds": ["Bot", "User"],
"$fields": [
{ "$fetch": "name", "$definedOn": "User", "$as": "userName" },
{ "$fetch": "name", "$definedOn": "Bot", "$as": "botName" }
]
}Expandir relaciones
$expand recorre las relaciones e inserta las unidades conectadas. Sin N+1: BlitzStore agrupa todos los recorridos automáticamente. Puedes anidar $expand a cualquier profundidad, con sus propios $fields, $filter, $sort y $limit. Usa para incluir las marcas de tiempo de la conexión.
{
"$kinds": "Post",
"$fields": [
"title",
{ "$expand": "author", "$fields": ["name", "email"] },
{ "$expand": "comments", "$fields": ["text"] }
]
}{
"$kinds": "User",
"$fields": [
"name",
{
"$expand": "posts",
"$filter": { "status": "published" },
"$sort": [{ "$by": "title", "$order": "asc" }],
"$limit": 5,
"$fields": ["title", "status"]
}
]
}Usa $as para renombrar en la respuesta un campo expandido o virtual cuando el nombre del esquema no se lee bien en el cliente.
{
"$kinds": "User",
"$fields": [
{ "$expand": "posts", "$as": "articles", "$fields": ["title"] }
]
}Metadatos de arco
funciona como , pero envuelve cada resultado en un sobre de metadatos de con $arcCreatedAt (marca de tiempo ISO). Útil para ver cuándo se crearon las conexiones: amistades, membresías, trazas de auditoría.
{
"$kinds": "Team",
"$id": "team1",
"$fields": [
"name",
{ "$expandArc": "members", "$fields": ["name"] }
]
}
// Response wraps each member in an arc envelope:
// { "name": "Devs", "members": [
// { "$arcCreatedAt": "2026-03-15T10:30:00Z", "$unit": { "$id": "...", "name": "Alice" } },
// { "$arcCreatedAt": "2026-03-16T09:00:00Z", "$unit": { "$id": "...", "name": "Bob" } }
// ]}Dentro de también puedes filtrar y ordenar las relaciones por cuándo se creó cada conexión, usando $arcCreatedAt como clave de filtro y de orden. El predicado sobre la hora del arco se evalúa antes de traer las unidades conectadas, así que las conexiones que no coinciden se descartan pronto.
{
"$kinds": "Team",
"$id": "t1",
"$fields": [
{
"$expandArc": "members",
"$filter": { "$arcCreatedAt": { "$gte": { "#datetime": "2026-01-01T00:00:00.000Z" } } },
"$sort": "-$arcCreatedAt",
"$limit": 10
}
]
}- Solo operadores de comparación:
$gte,$gt,$lte,$lt,$eq,$neq - Como filtro,
$arcCreatedAtdebe ser una condición de nivel superior (o estar dentro de un$andde nivel superior) - Como clave de orden debe ser la clave principal (la primera)
- Solo es válido dentro de , nunca en un normal
Agregaciones
$groupBy agrupa los resultados por valores de campo, pero no hace falta para obtener una única fila resumen en la raíz. Agrega con operadores $agg como COUNT, SUM, AVG, MIN, MAX y más. Funciona en el nivel raíz y anidado dentro de $expand.
{
"$kinds": "Order",
"$aggFields": [
{ "%count": { "$agg": "COUNT" } },
{ "%total": { "$agg": "SUM", "$field": "amount" } },
{ "%avg": { "$agg": "AVG", "$field": "amount" } }
]
}$groupBy emite por sí solo las filas con las claves de grupo distintas; añade $aggFields cuando esas filas necesiten métricas. $groupBy.$filter es el HAVING, y el $sort, el $limit y el $offset del objeto $groupBy ordenan y paginan los grupos. El $sort / $limit / $offset a nivel de nodo acota las unidades candidatas antes de agrupar. Las claves de grupo pueden ser campos escalares almacenados o calculados, identidades de arco ONE, o expresiones $js/$ts/$code/$expr escritas por ti con un nombre $as. Las claves con valores MANY se rechazan de forma explícita en lugar de crear grupos ambiguos.
{
"$kinds": "Order",
"$groupBy": {
"$by": ["status"],
"$filter": { "total": { "$gte": 1000 } },
"$sort": [{ "$by": "total", "$order": "desc" }],
"$limit": 5
},
"$aggFields": [
{ "%count": { "$agg": "COUNT" } },
{ "%total": { "$agg": "SUM", "$field": "amount" } }
]
}{
"$kinds": "Order",
"$groupBy": {
"$by": [
"customer",
{ "$js": "status.trim().toLowerCase()", "$as": "normalizedStatus" }
]
},
"$aggFields": [
{ "%count": { "$agg": "COUNT" } }
]
}{
"$kinds": "Team",
"$fields": [
"name",
{
"$expand": "members",
"$groupBy": ["position"],
"$aggFields": [
{ "%count": { "$agg": "COUNT" } }
]
}
]
}{
"$kinds": { "$all": ["Human", "Spanish"] },
"$search": "senior backend rust",
"$filter": { "role": { "$in": ["engineer", "designer"] } },
"$fields": [
"name", "salary", "bonus",
{ "%total": { "$js": "salary + bonus" } },
{
"$expand": "projects",
"$sort": [{ "$by": "budget", "$order": "desc" }],
"$limit": 3,
"$fields": [
"title", "budget", "spent",
{ "%remaining": { "$js": "budget - spent" } }
]
}
]
}Mutaciones
Envía las mutaciones a POST /mutate. Las operaciones se infieren de la forma de tu JSON, o fija $op de forma explícita.
Crear, actualizar y borrar
La operación se infiere de tu entrada: $setKinds sin $id crea, $id con campos actualiza y $id a solas borra.
// Create: $setKinds without $id { "$setKinds": ["User"], "name": "Alice", "email": "alice@test.com" } // Update: $id with fields { "$id": "user_abc123", "email": "new@test.com" } // Delete: $id without fields { "$id": "user_abc123" } // Bulk delete: with $filter { "$op": "delete", "$kinds": "Task", "$filter": { "done": true } }
$setKinds sin $id$id con campos de datos$id sin campos de datosUpsert
$op: "upsert" actualiza cuando la identidad resuelve una unidad existente y crea cuando no. Como puede crear, $setKinds es obligatorio. La identidad puede venir de $id o de $filter, pero nunca de los dos.
// Kind has idField: "email" // First call creates { "$op": "upsert", "$id": "alice@test.com", "$setKinds": ["User"], "email": "alice@test.com", "name": "Alice" } // Second call updates same unit { "$op": "upsert", "$id": "alice@test.com", "$setKinds": ["User"], "name": "Alice V2" }
// 0 matches -> create { "$op": "upsert", "$setKinds": ["User"], "$filter": { "email": "bob@test.com" }, "email": "bob@test.com", "name": "Bob" } // 1 match -> update { "$op": "upsert", "$setKinds": ["User"], "$filter": { "email": "bob@test.com" }, "name": "Bob V2" }
$id para la identidad directa, o $filter para una búsqueda que coincida con 0 o 1 unidad.$id + $filter no es válido. Un $filter que coincida con 2 o más unidades tampoco lo es.Operaciones por lotes
Pasa un array para hacer mutaciones por lotes atómicas. O tienen éxito todas o se deshacen todas. Puedes mezclar creaciones, actualizaciones y borrados en el mismo lote. Usa $var para referenciar unidades entre operaciones, o anida las unidades hijas directamente dentro de su padre.
[
{ "$var": "_:user", "$setKinds": ["User"], "name": "Alice" },
{ "$var": "_:acct", "$setKinds": ["Account"], "provider": "github" },
{ "$setKinds": ["UserAccount"], "user": "_:user", "account": "_:acct" }
]{
"$setKinds": ["UserAccount"],
"user": { "$setKinds": ["User"], "name": "Alice" },
"account": { "$setKinds": ["Account"], "provider": "github" }
}// User → Tag → Group → Colors, all created atomically { "$setKinds": ["User"], "tags": [{ "$setKinds": ["Tag"], "group": { "$setKinds": ["Group"], "colors": [ { "$setKinds": ["Color"], "name": "Red" }, { "$setKinds": ["Color"], "name": "Blue" } ] } }] }
// Create + update in the same atomic batch [ { "$setKinds": ["User"], "name": "Bob", "status": "new" }, { "$id": "existing_user_id", "status": "updated" } ]
Importación de datos
POST /data/import es un carril rápido de solo creación para sembrar conjuntos de datos. Los elementos sin un $op explícito toman create por defecto; cualquier otro $op se rechaza. El constructor ordena topológicamente las referencias $var y trocea los arrays grandes, así que una sola carga puede llevar un conjunto de datos entero y respetar el orden de padres antes que hijos.
// Country has idField:"code". Send code as data; // $id is used to resolve same-import arcs. POST /data/import { "units": [ { "$id": "country:mex", "$kinds": ["Country"], "code": "MEX", "name": "Mexico" }, { "$id": "country:bra", "$kinds": ["Country"], "code": "BRA", "name": "Brazil" }, { "$id": "sticker:mex-logo", "$kinds": ["Sticker"], "code": "MEX1", "label": "Logo", "country": "country:mex" } ] }
// data_export emits query-like units with $id, $kinds, // data fields, and raw arc IDs. It omits $op, $var, $iid. POST /data/import { "units": [ { "$id": "user:alice", "$kinds": ["User"], "name": "Alice", "posts": ["post:hello"] }, { "$id": "post:hello", "$kinds": ["Post"], "title": "Hello", "author": "user:alice" } ] }
$id en la importación$id de un objeto de creación importado resuelve las referencias de arco dentro de la misma importación y se descarta antes de crear; envía los valores de idField como campos de datos normales.$id exportados o $var: "_:<name>" en el padre y referéncialo desde los hijos. El constructor reserva los ULID por adelantado y ordena topológicamente. Los ULID internos nunca los elige quien llama.Accept: text/event-stream a POST /data/import para recibir eventos de progreso por trozos; omítelo para obtener un JSON de una sola vez. opts.batchSize ajusta el troceado en el servidor (5000 por defecto): no lo dividas en el cliente.Límites de tamaño de lote
db.mutate() limita una sola llamada a 10.000 operaciones por defecto; definition_mutate limita a 500 elementos. Cuenta cada nodo de mutación raíz o anidado (create, update, upsert, delete, query), más cada destino explícito de arco link / unlink / replace. Una operación masiva acotada por filtro cuenta como una, coincida con las unidades que coincida: el tope acota el tamaño del lote de entrada, no el número de coincidencias. Los nodos se cuentan después de expandir $for / $if.
Superar el tope devuelve BATCH_TOO_LARGE, que dirige a quien envía datos hacia data_import por trozos o la importación de namespace, y a quien envía definiciones hacia lotes más pequeños. Sube el tope por llamada (hasta el tope duro del servidor) o por namespace (mutation.maxBatchSize / definitionMutation.maxBatchSize, solo al alza: la configuración del namespace no puede bajar el valor por defecto del servidor). maxTotalGeneratedItems (100.000) sigue siendo el techo absoluto de expansión de $for aunque subas maxBatchSize.
Control de flujo
$for genera elementos de mutación a partir de un bucle; $if los incluye de forma condicional. El control de flujo se expande antes de que corra el motor, así que el lote se mantiene plano y atómico. Ambos se pueden anidar y mezclar con elementos normales.
// Range source (inclusive) { "$for": { "$in": { "$range": [1, 3] }, "$as": "_:i" }, "$do": [ { "$setKinds": ["User"], "name": "user{_:i}", "index": "_:i" } ] } // Array source { "$for": { "$in": ["rust", "graph", "blitz"], "$as": "_:tag" }, "$do": [{ "$setKinds": ["Tag"], "name": "_:tag" }] }
// With optional else branch { "$if": { "$js": "1 > 2" }, "$then": [{ "$setKinds": ["Status"], "value": "then" }], "$else": [{ "$setKinds": ["Status"], "value": "else" }] }
Escribe {_:var} dentro de las cadenas para interpolar la variable del bucle. El total de iteraciones está acotado por los límites de consulta para evitar bucles descontrolados.
Operaciones de grafo
Gestiona las conexiones de con $op dentro de los campos de arco (mira para las marcas de tiempo). link añade conexiones, unlink las quita y replace las fija exactamente. La asignación directa (azúcar de DX) es la forma abreviada de replace.
// Link: add connections { "$id": "book1", "authors": { "$op": "link", "$id": "user1" } } // Unlink: remove a connection { "$id": "book1", "authors": { "$op": "unlink", "$id": "user1" } } // Replace: set exact connections { "$id": "book1", "authors": { "$op": "replace", "$id": ["user1", "user2"] } } // DX sugar: direct assignment = replace { "$id": "book1", "authors": ["user1", "user2"] }
{
"$id": "book1",
"authors": {
"$op": "link",
"$kinds": "User",
"$filter": { "role": "dev" }
}
}En los linkField con target: "role", las escrituras con forma de extremo directo se rechazan. Usa una actualización proyectada del extremo para las unidades relacionadas existentes, o crea el árbol de relación de forma explícita.
// Query projected endpoints through a tunnel link { "$kinds": "Candidate", "$fields": [ "name", { "$expand": "interviewers", "$fields": ["name", "department"] } ] } // Update the projected endpoints selected by the tunnel { "$id": "candidate_123", "interviewers": { "$op": "update", "$filter": { "department": "Ops" }, "department": "Platform" } }
{
"$id": "candidate_123",
"interviewers": {
"$setKinds": ["Interview"],
"date": "2026-04-11T09:00:00Z",
"interviewer": {
"$setKinds": ["Interviewer"],
"name": "Dana",
"department": "Platform"
}
}
}Expresiones
Usa $js para expresiones JavaScript en línea o $ts para TypeScript (los tipos se borran antes de ejecutar; la forma explícita es $code: { $lang, $body }). Funciona en las escrituras de campos de datos al crear y al actualizar (incluidos el upsert y las actualizaciones de arco acotadas). Al crear solo ve los campos hermanos; al actualizar ve la fila almacenada además de los hermanos, en orden topológico. La autorreferencia lee el valor almacenado antes de la escritura. Las variables de lote usan _:varname. Las dependencias circulares entre hermanos se rechazan. Los hooks ven la expresión tal como se escribió, no el valor evaluado. Comprueba la ausencia con isNull(x); los locales DECIMAL son objetos: usa decimal().
Una expresión que devuelve null limpia su destino, igual que un null literal. Los fallos en ejecución hacen fallar la mutación por defecto; al actualizar, onExprError: "skip" deja sin escribir solo el campo que falla y emite un aviso. Los objetos con forma de código dentro de los valores o arrays de una operación JSON son JSON literal, no expresiones ejecutables.
{
"$setKinds": ["Invoice"],
"subtotal": 100,
"tax": { "$js": "subtotal * 0.21" },
"total": { "$js": "subtotal + tax" }
}{
"$id": "line1",
"total": { "$js": "price * qty" },
"count": { "$js": "count + 1" }
}{
"$setKinds": ["Post"],
"title": "Hello World",
"slug": { "$js": "title.toLowerCase().replaceAll(' ', '-')" }
}{
"$setKinds": ["Invoice"],
"subtotal": 100,
"total": { "$ts": "const rate: number = 0.21; subtotal * (1 + rate)" }
}// Query an existing user, then create a post using their data [ { "$op": "query", "$var": "_:author", "$kinds": "User", "$id": "user1" }, { "$setKinds": ["Post"], "title": "Hello World", "authorName": { "$js": "_:author.name" }, "boost": { "$js": "_:author.karma * 0.1" }, "greeting": { "$js": "`Hello ${_:author.name}!`" } } ]
$js también funciona en las consultas como campos virtuales: valores calculados que solo existen en la respuesta, con el prefijo %.
{
"$kinds": "Order",
"$fields": [
"name", "price", "quantity",
{ "%line_total": { "$js": "price * quantity" } },
{ "%tax_label": { "$js": "'Tax: ' + (price * 0.21).toFixed(2)" } }
]
}Fuerza que un valor JSON tenga un tipo concreto al escribir. La forma canónica es la clave de objeto con el prefijo #: el espacio de nombres $ está reservado para los operadores y selectores de BQL.
{
"$setKinds": ["Invoice"],
"created": { "#datetime": "2024-01-15T10:30:00Z" },
"dueDate": { "#date": "2024-02-15" },
"openAt": { "#time": "09:00:00.000" },
"price": { "#decimal": "19.99" },
"eta": { "#duration": "1h30m" },
"active": { "#boolean": "true" },
"count": { "#integer": "42" },
"ratio": { "#float": "3.14" },
"owner": { "#unit": "01J..." },
"rawText": { "#text": ".name" }
}#datetime requiere zona horaria (Z o +02:00). #decimal conserva la base 10 exacta. #duration combina w/d/h/m/s/u/n y admite negativos. #text escapa una cadena que si no se interpretaría como azúcar de DX (por ejemplo ".name" dentro de una expresión nativa). Seis castings (unit / date / datetime / time / decimal / duration) también aceptan el atajo heredado de DX con prefijo en la cadena "<datetime>2024-01-15T10:30:00Z"; el resto solo admiten la forma de objeto.
Un campo INTERVAL almacena un conjunto de rangos. Consúltalo con $has (contención completa de un punto o de un subconjunto) y con $intersects (cualquier solapamiento). $contains es solo subcadena de TEXT.
// Write: a Schedule with two ranges. Compact notation is the // shortest spelling; a member array of nested tuples also works. { "$setKinds": ["Schedule"], "officeHours": { "#intervals": "[09:00:00.000,13:00:00.000) U [14:00:00.000,18:00:00.000)" } } // Read: results never echo the "#intervals" cast. The shape follows the // field's intervalFormat — "tuple" here, so bounds render as pairs: // { "officeHours": [["09:00:00.000","13:00:00.000"], ["14:00:00.000","18:00:00.000"]] } // Schedules whose office hours contain 10:30 { "$kinds": "Schedule", "$filter": { "officeHours": { "$has": { "#time": "10:30:00.000" } } } } // Schedules whose office hours overlap a meeting window { "$kinds": "Schedule", "$filter": { "officeHours": { "$intersects": { "#intervals": [[{ "#time": "12:30:00.000" }, { "#time": "15:30:00.000" }]] } } } }
$expr es un carril rápido verificado para funciones concretas de rutas calientes, con el nombre @namespace.fn. Dentro de una llamada con @, una cadena que empieza por . es azúcar de DX para referirse a un campo; envuélvela en #text para conservar la cadena literal. $js sigue siendo la superficie amplia para todo lo que aún no está cubierto.
{
"$kinds": "User",
"$fields": [
"name",
{ "%fullName": { "$expr": { "@text.concat": [".name", " ", ".familyName"] } } }
]
}Respuestas
Todas las respuestas siguen una estructura consistente. La forma de data es predecible a partir de tu consulta.
Forma de la respuesta
El campo data es un objeto o null cuando el selector es demostrablemente único: un $id escalar, un $iid escalar o un filtro de igualdad sobre un campo de datos unique (por ejemplo $filter: { "email": "x" } con email marcado como unique: true). La igualdad con null sobre un campo unique sigue siendo MANY. Las consultas guiadas solo por $kinds, por filtros no unique, por $id.$in, por un $or en la raíz o por $in devuelven un array, aunque el filtro coincida con una sola fila. $limit: 1 no cambia la forma de la respuesta.
{
"data": {
"$id": "abc123",
"$kinds": ["User"],
"name": "Alice",
"email": "alice@test.com"
},
"outcome": "completed"
}{
"data": [
{ "$id": "abc123", "$kinds": ["User"], "name": "Alice" },
{ "$id": "def456", "$kinds": ["User"], "name": "Bob" }
],
"outcome": "completed",
"meta": { "count": 2, "timing_ms": 1.23 }
}Añade $explain a una consulta o mutación para incluir el plan en la respuesta. "basic" devuelve la lista de pasos y el número de filas; "full" añade los tiempos por paso. Útil para afinar.
{ "$kinds": "User", "$filter": { "status": "active" }, "$explain": "full" }
// Response shape
{
"data": [ ... ],
"outcome": "completed",
"meta": { "count": 10, "timing_ms": 1.8 },
"explain": { "steps": [ ... ], "rows_scanned": 25000, "timing_ms": { ... } }
}Metadatos
Toda unidad incluye $id y $kinds por defecto. $meta solo es válido dentro de $fields. $meta no es una clave raíz de consulta; inclúyelo dentro de $fields para obtener todos los campos meta siempre activos. Los metadatos contextuales ($score, $history) hay que pedirlos de forma explícita. Los campos de marca de tiempo usan las claves públicas canónicas $createdAt y $updatedAt; ambos se serializan como cadenas ISO.
Resultados y avisos
outcome es el estado de la operación. Los resultados completed, committed y partial usan 200; el trabajo aceptado usa 202; el trabajo rechazado usa normalmente 422 (o 429 por contrapresión).
issueses una única lista plana de diagnósticos ordenada por severidadseverityes error, warning o infophasees request, execution, commit o post_commit
Una consulta cuyo $filter no ha coincidido con ninguna unidad (y que no era una búsqueda por identidad ni por $offset), o que ha usado un $in: [] vacío, sigue teniendo éxito con data: [], pero lleva un aviso que explica el resultado vacío. Cada aviso tiene un código estable; el contexto propio de cada aviso va en details.
{
"data": [],
"outcome": "completed",
"issues": [
{ "code": "FILTER_MATCHED_NOTHING", "severity": "warning", "phase": "execution", "field": "$filter", "message": "filter matched 0 units" }
],
"meta": { "count": 0, "timing_ms": 0.21, "issues": { "total": 1, "returned": 1, "omitted": 0, "errors": 0, "warnings": 1, "info": 0 } }
}{
"data": null,
"outcome": "rejected",
"issues": [
{ "code": "UNKNOWN_KIND", "severity": "error", "phase": "execution", "message": "unknown kind 'Userr'. Did you mean 'User'?" }
],
"meta": { "timing_ms": 0.12 }
}Ramifica según issue.code con el catálogo tipado que exporta @blitzgraph/client-core (SERVER_ISSUE_CODES, con particiones separadas de errores y de diagnósticos): el catálogo se genera desde el servidor, así que un código desconocido significa que el cliente está desactualizado.
Por defecto, las mutaciones acumulan todos los errores para que veas todos los problemas de una vez. Pon failFast: true en las opciones de mutación para parar en el primer error.