Documentación oficial del SDK

StowDB Wiki

Referencia completa para consultar y modificar collections de StowDB sin escribir SQL ni construir JSON manualmente.

CRUDFiltrosJoinsAgregaciones34 secciones

Esta documentación corresponde a la implementación actual de stowdb.js.

1. Descripción

stowdb.js es un SDK para consultar y modificar collections de StowDB sin escribir SQL ni construir manualmente el JSON enviado al backend.

La librería expone estas clases principales:

  • StowDB: configura la conexión y permite crear documentos.
  • StowQuery: construye consultas encadenando métodos.
  • StowCatalog: valida collections, campos, tipos, relaciones y capacidades.
  • StowError: expone errores estructurados del SDK.

La clase StowDB queda disponible globalmente mediante:

globalThis.StowDB = StowDB;

2. Instalación en una página HTML

StowDB no se distribuye mediante npm. Para producción, carga una versión fija desde el CDN oficial:

<script
    src="https://stowdb.miramaxtech.com/sdk/v1/stowdb.js"
    integrity="sha384-RcSZc4US9vOBliImnF3ioCIoO9a22UFflk5aLpGzSWN6TRbqATZMoyrM7nbt4PvW"
    crossorigin="anonymous">
</script>

También puedes descargar stowdb.js desde el portal y alojarlo junto a tu página:

<script src="./stowdb.js"></script>
<script>
    const db = new StowDB(
        "https://stowdb.miramaxtech.com/rest",
        "vw_live_API_KEY",
        "PROJECT_KEY"
    );
</script>

El endpoint debe escribirse sin /get, /set, /update o /delete. Una barra final es eliminada automáticamente.

3. Crear el cliente

const db = new StowDB(endpoint, apiKey, projectKey, options);

Parámetros

ParámetroTipoDescripción
endpointstringURL base de la API, por ejemplo https://stowdb.miramaxtech.com/rest.
apiKeystringAPI key usada como Bearer token.
projectKeystringClave del proyecto enviada en X-Project-Key.

Los tres valores son obligatorios y no pueden estar vacíos.

Opciones de conexión

const db = new StowDB(endpoint, apiKey, projectKey, {
    timeout: 30000,
    retries: 2,
    retryDelay: 500,
    retryBackoff: 2,
    retryMutations: false
});
OpciónPredeterminadoDescripción
timeout30000Tiempo máximo en milisegundos. 0 desactiva el timeout.
retries2Reintentos para errores temporales, entre 0 y 10.
retryDelay500Espera inicial entre reintentos, en milisegundos.
retryBackoff2Multiplicador de la espera para cada intento.
retryMutationsfalsePermite reintentar create, update y delete.
idempotencyKeynullClave enviada como Idempotency-Key para hacer una escritura repetible de forma segura.
catalogSin catálogoDefinición de collections, campos, tipos, relaciones y capacidades.
strictCatalogfalseValida collections, campos, operadores y casts antes de enviar la petición.
autoCastfalseConvierte automáticamente las filas según los tipos del catálogo.
catalogCacheTtl300000Duración de la caché del catálogo en milisegundos.
onRequestnullHook ejecutado antes de cada intento.
onResponsenullHook ejecutado después de una respuesta correcta.
onRetrynullHook ejecutado antes de reintentar.
onErrornullHook ejecutado cuando la petición falla definitivamente.

Por seguridad, las escrituras no se reintentan de forma predeterminada porque una desconexión no garantiza que el servidor no haya aplicado la operación.

Ejemplo con catálogo:

const db = new StowDB(endpoint, apiKey, projectKey, {
    catalog,
    strictCatalog: true,
    autoCast: true
});

Cabeceras enviadas

Content-Type: application/json
Authorization: Bearer API_KEY
X-Project-Key: PROJECT_KEY

Endpoints utilizados

OperaciónEndpoint
ConsultaPOST /get
CreaciónPOST /set
ActualizaciónPOST /update
EliminaciónPOST /delete

4. Consultar una collection

const result = await db
    .from("orders")
    .get();

collection() es un alias de from():

const result = await db
    .collection("orders")
    .get();

Los nombres de collections deben comenzar con letra o _ y solo pueden contener letras, números y _.

5. Seleccionar campos

select(...fields)

const result = await db
    .from("orders")
    .select("order_id", "status", "total")
    .get();

También acepta un array:

const fields = ["order_id", "status", "total"];

const result = await db
    .from("orders")
    .select(fields)
    .get();

Los campos sin collection son calificados automáticamente:

"status" // se convierte en "orders.status"

También se puede proporcionar la referencia completa:

.select("orders.order_id", "customers.first_name")

Alias para campos normales

const result = await db
    .from("orders")
    .select(
        { field: "order_id", as: "id" },
        { field: "total", as: "amount" }
    )
    .get();

También puede usarse selectAs():

const result = await db
    .from("orders")
    .selectAs("order_id", "id")
    .selectAs("total", "amount")
    .get();

Si no se llama a select(), el backend devuelve el documento completo según las reglas del parser.

6. DISTINCT

distinct(enabled = true)

const result = await db
    .from("orders")
    .select("status")
    .distinct()
    .get();

Para desactivarlo explícitamente:

.distinct(false)

El argumento debe ser booleano.

7. Condiciones WHERE

where(field, operator, value)

const result = await db
    .from("orders")
    .where("status", "=", "Paid")
    .get();

Varias llamadas a where() se combinan con AND:

const result = await db
    .from("orders")
    .where("status", "=", "Paid")
    .where("total", ">", 500)
    .get();

Equivalente conceptual:

WHERE status = 'Paid' AND total > 500

Operadores

El SDK acepta cualquier operador no vacío y lo envía en minúsculas. La disponibilidad final depende del parser del backend.

Operadores usados por StowDB:

OperadorEjemplo
=.where("status", "=", "Paid")
!= o <>.where("status", "!=", "Cancelled")
>.where("total", ">", 500)
>=.where("total", ">=", 500)
<.where("quantity", "<", 10)
<=.where("quantity", "<=", 10)
like.where("product", "like", "%Laptop%")
not like.where("product", "not like", "%Laptop%")
in.where("status", "in", ["Paid", "Delivered"])
not in.where("status", "not in", ["Cancelled"])
betweenPreferir whereBetween().
isPreferir whereNull().
is notPreferir whereNotNull().

No insertes valores directamente en el operador. Siempre pasa el valor en el tercer argumento.

8. Condiciones OR

orWhere(field, operator, value)

const result = await db
    .from("orders")
    .where("status", "=", "Paid")
    .orWhere("status", "=", "Delivered")
    .get();

Equivalente conceptual:

WHERE status = 'Paid' OR status = 'Delivered'

Se pueden encadenar condiciones OR:

const result = await db
    .from("orders")
    .where("order_id", "like", "%abc%")
    .orWhere("customer_id", "like", "%abc%")
    .orWhere("product", "like", "%abc%")
    .orWhere("status", "like", "%abc%")
    .get();

Grupos booleanos anidados

const result = await db
    .from("orders")
    .whereGroup(group => {
        group
            .where("status", "=", "Paid")
            .orWhere("status", "=", "Delivered");
    })
    .whereGroup(group => {
        group
            .where("total", ">", 500)
            .andWhere("quantity", ">", 1);
    })
    .get();

Los grupos pueden contener whereGroup() y orWhereGroup() adicionales. El parser del backend debe reconocer los objetos and y or generados.

9. IN y NOT IN

whereIn(field, values)

const result = await db
    .from("orders")
    .whereIn("status", ["Paid", "Delivered"])
    .get();

whereNotIn(field, values)

const result = await db
    .from("orders")
    .whereNotIn("status", ["Cancelled", "Pending"])
    .get();

values debe ser un array compatible con el parser del backend.

10. BETWEEN

whereBetween(field, minimum, maximum)

const result = await db
    .from("orders")
    .whereBetween("total", 100, 1000)
    .get();

Ejemplo con fechas:

const result = await db
    .from("appointments")
    .whereBetween("date", "2026-07-01", "2026-07-15")
    .get();

El tratamiento como texto, número o fecha depende del tipo reconocido por el parser.

11. Valores NULL

whereNull(field)

const result = await db
    .from("orders")
    .whereNull("delivery_date")
    .get();

whereNotNull(field)

const result = await db
    .from("orders")
    .whereNotNull("delivery_date")
    .get();

12. Ordenamiento

orderBy(field, direction = "asc", type = "text")

const result = await db
    .from("orders")
    .orderBy("total", "desc", "numeric")
    .get();

Múltiples campos:

const result = await db
    .from("orders")
    .orderBy("status", "asc")
    .orderBy("total", "desc", "numeric")
    .get();

direction solo puede ser asc o desc.

El tercer argumento indica al backend cómo ordenar el valor JSON. Ejemplos habituales:

  • text
  • numeric
  • date
  • timestamp

La lista exacta de tipos permitidos depende del parser del backend.

Si no se llama a orderBy(), el SDK no envía order_by.

13. Paginación

limit(value)

Define el máximo de documentos devueltos.

.limit(20)

Rango permitido por el SDK: 1 a 500.

Valor predeterminado: 200.

offset(value)

Define cuántos documentos omitir antes de devolver resultados.

.offset(40)

Rango permitido por el SDK: 0 a 1,000,000.

Valor predeterminado: 0.

Ejemplo de paginación

const pageSize = 20;
const page = 3;

const result = await db
    .from("orders")
    .select("order_id", "status", "total")
    .limit(pageSize)
    .offset((page - 1) * pageSize)
    .get();

Para detectar una página siguiente se puede solicitar un registro adicional:

const pageSize = 20;

const result = await db
    .from("orders")
    .limit(pageSize + 1)
    .offset(0)
    .get();

const rows = result.response ?? [];
const hasNextPage = rows.length > pageSize;
const visibleRows = rows.slice(0, pageSize);

Paginación por cursor

paginate() usa paginación keyset: agrega una condición > o < sobre un campo estable y evita el costo creciente de OFFSET.

const firstPage = await db
    .from("orders")
    .select("order_id", "status", "total")
    .paginate({
        field: "order_id",
        pageSize: 20,
        direction: "asc",
        type: "text"
    });

Respuesta:

firstPage.response;
firstPage.pagination.hasNextPage;
firstPage.pagination.nextCursor;

Página siguiente:

const nextPage = await db
    .from("orders")
    .select("order_id", "status", "total")
    .paginate({
        field: "order_id",
        pageSize: 20,
        cursor: firstPage.pagination.nextCursor
    });

Reglas:

  • El campo debe ser estable, único y estar incluido en el resultado.
  • El cursor está vinculado al campo y dirección usados para crearlo.
  • Una instancia de consulta solo puede ejecutar paginate() una vez.
  • El tamaño permitido es de 1 a 499; el SDK solicita un registro adicional para detectar la siguiente página.
  • El cursor es opaco para el consumidor y no debe modificarse.

14. Ejecutar una consulta

get()

Envía la consulta a POST /get y devuelve una promesa:

try {
    const result = await db
        .from("orders")
        .where("status", "=", "Paid")
        .limit(20)
        .get();

    console.table(result.response);
} catch (error) {
    console.error(error.message);
}

15. Crear documentos

create(collection, data)

const result = await db.create("orders", {
    order_id: crypto.randomUUID(),
    customer_id: "3e1a3863-1d3e-4e15-b367-7cf50259cb62",
    product: "Laptop",
    quantity: 2,
    total: 2400.50,
    status: "Pending"
});

El segundo argumento debe ser un objeto. No puede ser null ni un array.

Payload generado:

{
  "collection": "orders",
  "data": {
    "order_id": "...",
    "customer_id": "...",
    "product": "Laptop",
    "quantity": 2,
    "total": 2400.5,
    "status": "Pending"
  }
}

16. Actualizar documentos

update(values, unset = [])

Una actualización exige al menos una condición where().

const result = await db
    .from("orders")
    .where("order_id", "=", "910680b7-a63a-4753-93a8-e69f9c4f6402")
    .limit(1)
    .update({
        total: 0,
        status: "Cancelled"
    });

Para eliminar propiedades del documento JSON durante la actualización:

const result = await db
    .from("orders")
    .where("order_id", "=", "910680b7-a63a-4753-93a8-e69f9c4f6402")
    .limit(1)
    .update(
        { status: "Cancelled" },
        ["temporary_field", "legacy_value"]
    );

Reglas importantes:

  • values debe ser un objeto.
  • La consulta debe contener al menos un where().
  • limit() controla el máximo de documentos actualizados.
  • Si no se especifica limit(), el valor heredado es 200.
  • Para actualizar un documento único se recomienda .limit(1) y un identificador único.

17. Eliminar documentos

delete()

La eliminación exige al menos una condición where().

const result = await db
    .from("orders")
    .where("order_id", "=", "9b4a7989-a8ed-4b15-88e5-be633b2bb75a")
    .limit(1)
    .delete();

Varias condiciones:

const result = await db
    .from("orders")
    .where("status", "=", "Cancelled")
    .where("product", "=", "Laptop")
    .limit(100)
    .delete();

En la implementación actual del backend, delete() puede representar un borrado lógico mediante deleted_at, no necesariamente un DELETE físico.

Reglas importantes:

  • No se permite eliminar sin where().
  • limit() controla el máximo de documentos afectados.
  • Para eliminar un único documento se recomienda .limit(1).

18. Joins

Los campos de un join deben usar el formato completo collection.field.

innerJoin(collection, leftField, rightField, alias = null)

const result = await db
    .from("orders")
    .select(
        "orders.order_id",
        "orders.status",
        "customers.first_name",
        "customers.last_name"
    )
    .innerJoin(
        "customers",
        "orders.customer_id",
        "customers.customer_id"
    )
    .get();

leftJoin(collection, leftField, rightField, alias = null)

const result = await db
    .from("orders")
    .select("orders.order_id", "products.name")
    .leftJoin(
        "products",
        "orders.order_id",
        "products.order_id"
    )
    .get();

rightJoin(collection, leftField, rightField, alias = null)

const result = await db
    .from("orders")
    .rightJoin(
        "products",
        "orders.order_id",
        "products.order_id"
    )
    .get();

fullJoin(collection, leftField, rightField, alias = null)

const result = await db
    .from("orders")
    .fullJoin(
        "products",
        "orders.order_id",
        "products.order_id"
    )
    .get();

join(type, collection, leftField, operator, rightField, alias = null)

Método general para controlar el tipo y operador:

const result = await db
    .from("orders")
    .join(
        "inner",
        "customers",
        "orders.customer_id",
        "=",
        "customers.customer_id"
    )
    .get();

Tipos permitidos por el SDK:

  • inner
  • left
  • right
  • full

Alias de collection

const result = await db
    .from("orders")
    .select("orders.order_id", "p.name")
    .leftJoin(
        "products",
        "orders.order_id",
        "p.order_id",
        "p"
    )
    .get();

El parser del backend debe admitir el alias y la relación generada.

Varios joins

const result = await db
    .from("orders")
    .select(
        "orders.order_id",
        "customers.first_name",
        "products.name"
    )
    .innerJoin(
        "customers",
        "orders.customer_id",
        "customers.customer_id"
    )
    .leftJoin(
        "products",
        "orders.order_id",
        "products.order_id"
    )
    .get();

Varias condiciones ON

const result = await db
    .from("orders")
    .leftJoin("products", join => {
        join
            .on("orders.order_id", "=", "products.order_id")
            .andOn("products.stock", ">", 0);
    })
    .get();

Para combinar todas las condiciones con OR:

.leftJoin("products", join => {
    join
        .on("orders.order_id", "=", "products.order_id")
        .orOn("orders.product", "=", "products.name");
})

Grupos ON arbitrariamente anidados

.leftJoin("products", join => {
    join
        .on("orders.order_id", "=", "products.order_id")
        .orOnGroup(group => {
            group
                .on("orders.product", "=", "products.name")
                .andOn("products.stock", ">", { value: 0 });
        });
})

Están disponibles onGroup(), andOnGroup() y orOnGroup(). Los grupos pueden anidarse y el parser SELECT conserva sus paréntesis y lógica.

Relaciones automáticas con with()

Cuando hay un catálogo cargado, el SDK encuentra el camino más corto entre collections y construye los joins:

const result = await db
    .from("orders")
    .select(
        "orders.order_id",
        "customers.first_name",
        "products.name"
    )
    .with("customers", "products")
    .get();

with() evita repetir collections que ya fueron incorporadas y lanza CATALOG_ERROR si no existe una relación.

19. Agrupación

groupBy(...fields)

const result = await db
    .from("orders")
    .select("status")
    .sum("total", "total_sales")
    .groupBy("status")
    .get();

También acepta un array:

.groupBy(["status", "product"])

Los campos agrupados deben incluirse normalmente en select() cuando también se muestran en el resultado.

20. Funciones de agregación

count(field = "*", alias = "count")

const result = await db
    .from("orders")
    .count("*", "order_count")
    .get();

countDistinct(field, alias = null)

const result = await db
    .from("orders")
    .countDistinct("customer_id", "customer_count")
    .get();

sum(field, alias = null)

const result = await db
    .from("orders")
    .sum("total", "total_sales")
    .get();

avg(field, alias = null)

const result = await db
    .from("orders")
    .avg("total", "average_order")
    .get();

min(field, alias = null, type = "text")

const result = await db
    .from("orders")
    .min("total", "minimum_total", "numeric")
    .get();

max(field, alias = null, type = "text")

const result = await db
    .from("orders")
    .max("total", "maximum_total", "numeric")
    .get();

Si no se proporciona alias, el SDK lo genera con el formato:

operacion_campo

Ejemplo:

.sum("total") // alias: sum_total

Los alias deben comenzar con letra o _ y solo pueden contener letras, números y _.

21. HAVING

having(alias, operator, value)

having() trabaja con el alias de una agregación:

const result = await db
    .from("orders")
    .select("status")
    .sum("total", "total_sales")
    .groupBy("status")
    .having("total_sales", ">", 10000)
    .orderBy("total_sales", "desc")
    .get();

22. Consulta agregada completa

const result = await db
    .from("orders")
    .select("customers.city")
    .innerJoin(
        "customers",
        "orders.customer_id",
        "customers.customer_id"
    )
    .whereIn("orders.status", ["Paid", "Delivered"])
    .countDistinct("orders.customer_id", "customer_count")
    .sum("orders.total", "total_sales")
    .groupBy("customers.city")
    .having("customer_count", ">", 1)
    .orderBy("total_sales", "desc")
    .limit(50)
    .offset(0)
    .get();

console.table(result.response);

23. Inspeccionar el payload sin ejecutar

toPayload()

const query = db
    .from("orders")
    .select("order_id", "status")
    .where("status", "=", "Paid")
    .limit(20);

console.log(query.toPayload());

Resultado aproximado:

{
  "from": "orders",
  "distinct": false,
  "limit": 20,
  "offset": 0,
  "select": [
    "orders.order_id",
    "orders.status"
  ],
  "where": [
    {
      "orders.status": ["=", "Paid"]
    }
  ]
}

toUpdatePayload(values, unset = [])

const query = db
    .from("orders")
    .where("order_id", "=", "abc")
    .limit(1);

console.log(query.toUpdatePayload({ status: "Paid" }));

toDeletePayload()

const query = db
    .from("orders")
    .where("order_id", "=", "abc")
    .limit(1);

console.log(query.toDeletePayload());

24. Respuesta del backend

El SDK devuelve el objeto JSON recibido. Una respuesta habitual de consulta tiene esta forma:

{
  "ok": true,
  "response": [
    {
      "order_id": "5742459c-3b47-46df-9f88-583d17953305",
      "status": "Paid",
      "total": "3181.02"
    }
  ],
  "query": {
    "sql": "...",
    "parameters": {}
  }
}

Para recuperar las filas:

const rows = Array.isArray(result.response)
    ? result.response
    : [];

La forma exacta puede variar según el endpoint. Por defecto, el SDK devuelve response tal como llega; los métodos siguientes permiten transformarlo explícitamente.

Convertir tipos de las filas

const result = await db
    .from("orders")
    .select("order_id", "quantity", "total", "active")
    .casts({
        quantity: "integer",
        total: "number",
        active: "boolean"
    })
    .get();

También puede declararse campo por campo:

const result = await db
    .from("orders")
    .cast("total", "number")
    .cast("created_at", "date")
    .get();

Tipos disponibles:

TipoResultado
stringString
numberNúmero finito
integerNúmero entero
booleanBooleano estricto
dateInstancia de Date
jsonObjeto o array JSON
bigintBigInt

Los valores null y undefined se conservan. Los campos que no estén presentes se ignoran.

BigInt no puede serializarse directamente con JSON.stringify().

Conversión personalizada

const result = await db
    .from("orders")
    .cast("total", value => Number(value) * 1.07)
    .get();

La función recibe (value, row, index) y puede ser asíncrona.

Transformar cada fila

const result = await db
    .from("orders")
    .casts({
        quantity: "integer",
        total: "number"
    })
    .transform((row, index) => ({
        ...row,
        index,
        description: `${row.quantity} × ${row.product}`
    }))
    .get();

transform() se ejecuta después de los casts y debe devolver un valor. Puede devolver una promesa.

El SDK crea nuevas filas para la respuesta transformada; no modifica directamente los objetos recibidos del backend.

25. Manejo de errores

try {
    const result = await db
        .from("orders")
        .where("status", "=", "Paid")
        .get();

    console.log(result.response);
} catch (error) {
    console.error(error.message);
}

El SDK lanza errores cuando:

  • No puede conectarse al endpoint.
  • El backend devuelve JSON inválido.
  • El código HTTP no es exitoso.
  • La respuesta contiene ok: false.
  • La respuesta contiene success: false.
  • Un argumento local es inválido.

Mensajes habituales:

Unable to connect to StowDB: ...
StowDB returned invalid JSON (HTTP 500).
StowDB request error: ...

El error original de conexión o JSON se conserva en error.cause cuando el navegador lo soporta.

Los errores del SDK son instancias de VanillaError:

try {
    await db.from("orders").get();
} catch (error) {
    console.log(error.code);
    console.log(error.status);
    console.log(error.details);
}
CódigoSignificado
NETWORK_ERRORFalló la conexión o lectura de la respuesta.
TIMEOUTSe agotó el tiempo configurado.
REQUEST_ABORTEDLa aplicación canceló la petición.
HTTP_ERROREl servidor respondió con un código HTTP fallido.
INVALID_RESPONSEEl servidor no devolvió JSON válido.
BACKEND_ERROREl JSON contiene ok: false o success: false.
TRANSFORM_ERRORFalló un cast o la transformación de una fila.
CATALOG_ERRORCollection, campo, relación o tipo inválido.
CAPABILITY_ERROREl catálogo indica que el backend no soporta la operación.
CURSOR_ERROREl cursor es inválido o incompatible con la consulta.
IDEMPOTENCY_ERRORUna escritura reintentable no tiene una clave de idempotencia.
REQUEST_SERIALIZATION_ERROREl payload no puede convertirse a JSON.

Cancelar una petición

const controller = new AbortController();

const request = db
    .from("orders")
    .get({ signal: controller.signal });

controller.abort();

await request;

Opciones para una petición específica

const result = await db
    .from("orders")
    .get({
        timeout: 10000,
        retries: 1,
        retryDelay: 250
    });

Las opciones específicas reemplazan únicamente los valores correspondientes de la configuración global.

Para autorizar reintentos de una escritura explícitamente:

await db.create("orders", document, {
    retries: 1,
    retryMutations: true
});

Actualización con opciones:

await db
    .from("orders")
    .where("order_id", "=", orderId)
    .limit(1)
    .update(
        { status: "Paid" },
        [],
        { timeout: 15000 }
    );

Idempotencia de escrituras

Una escritura solo puede reintentarse cuando incluye una clave de idempotencia:

await db.create("orders", document, {
    retries: 2,
    retryMutations: true,
    idempotencyKey: crypto.randomUUID()
});

El SDK envía:

Idempotency-Key: valor-unico

Si se solicitan reintentos de mutaciones sin la clave, se genera IDEMPOTENCY_ERROR antes de enviar la petición.

El backend incluye:

  • vanilla_idempotency_schema.sql: almacenamiento aislado por tenant/proyecto.
  • IdempotencyStore.php: reserva, recuperación, finalización, liberación y purga de claves.

update_execute_endpoint.php y delete_execute_endpoint.php ya integran el almacenamiento dentro de la misma transacción. El endpoint /set debe aplicar el mismo patrón para que CREATE sea idempotente en producción.

Hooks y métricas

const db = new StowDB(endpoint, apiKey, projectKey, {
    onRequest: event => console.log("request", event),
    onResponse: event => console.log("response", event),
    onRetry: event => console.log("retry", event),
    onError: event => console.error("error", event)
});

Los errores lanzados por un hook no cambian el resultado de la petición.

console.log(db.metrics());
db.resetMetrics();

Las métricas contienen requests, intentos, éxitos, fallos, reintentos, duración total, promedio y desglose por operación.

26. CORS en el backend

Para usar el SDK desde un navegador, los endpoints deben aceptar las cabeceras enviadas por StowDB:

header('Access-Control-Allow-Origin: http://localhost');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header(
    'Access-Control-Allow-Headers: ' .
    'Authorization, Content-Type, X-Project-Key, X-API-Key, Idempotency-Key'
);

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;
}

El servidor debe responder correctamente a la petición OPTIONS previa al POST.

27. Uso en navegadores y variables de entorno

El navegador no proporciona process.env. Este código no funciona directamente en un HTML normal:

const apiKey = process.env.VANILLA_API_KEY;

En una aplicación compilada, usa el mecanismo de variables de entorno de Vite, Next.js u otra herramienta. Ten presente que cualquier secreto enviado al frontend puede ser visto por el usuario.

Para herramientas administrativas locales se pueden usar inputs protegidos:

<input id="apiKey" type="password">

No guardes API keys administrativas en repositorios públicos ni en JavaScript distribuido a usuarios no confiables.

28. Método avanzado request()

request(operation, payload) es público y puede utilizarse directamente:

const controller = new AbortController();

const result = await db.request("select", {
    from: "orders",
    distinct: false,
    limit: 20,
    offset: 0
}, {
    timeout: 10000,
    signal: controller.signal
});

Operaciones aceptadas:

  • select
  • create
  • update
  • delete

Se recomienda usar los métodos de alto nivel porque validan argumentos y construyen el payload.

29. Referencia rápida

Cliente

MétodoResultado
new StowDB(endpoint, apiKey, projectKey, options)Crea el cliente y configura las peticiones.
from(collection)Crea una consulta.
collection(collection)Alias de from().
create(collection, data)Inserta un documento.
request(operation, payload)Envía una operación manual.
useCatalog(definition)Carga o reemplaza el catálogo.
fetchCatalog(options)Recupera el catálogo desde /catalog.
clearCatalogCache()Elimina la caché del catálogo del proyecto.
catalogVersion()Devuelve la versión del catálogo.
getCatalog()Devuelve la instancia VanillaCatalog.
collections()Lista las collections del catálogo.
describe(collection)Describe campos y tipos.
relations()Lista las relaciones declaradas.
capabilities()Devuelve operadores y casts soportados.
metrics()Devuelve métricas acumuladas de peticiones.
resetMetrics()Reinicia las métricas del cliente.

Consulta

MétodoDescripción
select(...fields)Selecciona campos.
selectAs(field, alias)Selecciona un campo con alias.
distinct(enabled)Activa o desactiva DISTINCT.
where(field, operator, value)Agrega una condición AND.
orWhere(field, operator, value)Agrega o amplía un grupo OR.
whereGroup(callback)Agrega un grupo booleano AND.
orWhereGroup(callback)Agrega un grupo booleano OR.
whereIn(field, values)Condición IN.
whereNotIn(field, values)Condición NOT IN.
whereBetween(field, min, max)Condición BETWEEN.
whereNull(field)Condición IS NULL.
whereNotNull(field)Condición IS NOT NULL.
join(...)Join configurable.
innerJoin(...)INNER JOIN.
leftJoin(...)LEFT JOIN.
rightJoin(...)RIGHT JOIN.
fullJoin(...)FULL JOIN.
with(...collections)Construye joins usando el catálogo.
groupBy(...fields)Agrupa resultados.
having(alias, operator, value)Filtra agregaciones.
count(field, alias)COUNT.
countDistinct(field, alias)COUNT DISTINCT.
sum(field, alias)SUM.
avg(field, alias)AVG.
min(field, alias, type)MIN.
max(field, alias, type)MAX.
orderBy(field, direction, type)Ordena resultados.
cast(field, type)Convierte un campo de cada fila.
casts(definitions)Declara varias conversiones.
transform(callback)Transforma cada fila devuelta.
typed(enabled)Activa el tipado automático para la consulta.
limit(value)Máximo de resultados o documentos afectados.
offset(value)Desplazamiento de consulta.
get()Ejecuta SELECT.
paginate(options, requestOptions)Ejecuta paginación keyset por cursor.
update(values, unset)Actualiza documentos.
delete()Elimina documentos.
toPayload()Devuelve el payload SELECT.
toUpdatePayload(values, unset)Devuelve el payload UPDATE.
toDeletePayload()Devuelve el payload DELETE.

30. Ejemplos rápidos

Buscar una orden

const result = await db
    .from("orders")
    .select("order_id", "status", "total")
    .where("order_id", "=", "5742459c-3b47-46df-9f88-583d17953305")
    .limit(1)
    .get();

Buscar órdenes pagadas

const result = await db
    .from("orders")
    .where("status", "=", "Paid")
    .orderBy("total", "desc", "numeric")
    .limit(20)
    .get();

Buscar texto en varios campos

const term = "Laptop";

const result = await db
    .from("orders")
    .where("order_id", "like", `%${term}%`)
    .orWhere("product", "like", `%${term}%`)
    .orWhere("status", "like", `%${term}%`)
    .limit(20)
    .get();

Total vendido por estado

const result = await db
    .from("orders")
    .select("status")
    .sum("total", "total_sales")
    .groupBy("status")
    .orderBy("total_sales", "desc")
    .get();

Actualizar una orden

const result = await db
    .from("orders")
    .where("order_id", "=", "5742459c-3b47-46df-9f88-583d17953305")
    .limit(1)
    .update({ status: "Delivered" });

Eliminar órdenes canceladas

const result = await db
    .from("orders")
    .where("status", "=", "Cancelled")
    .limit(100)
    .delete();

31. Catálogo, tipos y relaciones

El catálogo permite que el SDK conozca la estructura pública del proyecto sin conocer la tabla interna de StowDB.

const catalog = {
    version: 1,
    capabilities: {
        operators: [
            "=", "!=", ">", ">=", "<", "<=",
            "like", "in", "between", "is", "is not"
        ],
        casts: [
            "text", "numeric", "integer",
            "boolean", "date", "timestamp", "uuid"
        ]
    },
    collections: {
        orders: {
            label: "Orders",
            fields: {
                order_id: { type: "uuid", nullable: false },
                customer_id: { type: "uuid", nullable: false },
                total: { type: "numeric", nullable: false },
                status: {
                    type: "string",
                    nullable: false,
                    enum: ["Pending", "Paid", "Delivered", "Cancelled"]
                }
            }
        },
        customers: {
            fields: {
                customer_id: { type: "uuid", nullable: false },
                city: { type: "string", nullable: true }
            }
        }
    },
    relations: [{
        name: "orders_customer",
        from_collection: "orders",
        from_field: "customer_id",
        to_collection: "customers",
        to_field: "customer_id",
        join_type: "left"
    }]
};

Tipos de catálogo permitidos:

  • string
  • integer
  • numeric
  • boolean
  • date
  • timestamp
  • uuid
  • array
  • object

Cargar el catálogo en el constructor

const db = new StowDB(endpoint, apiKey, projectKey, {
    catalog,
    strictCatalog: true,
    autoCast: true
});

Cargarlo posteriormente

db.useCatalog(catalog);

Recuperarlo del backend

const catalog = await db.fetchCatalog();

Esta llamada usa POST /catalog. El endpoint debe devolver la definición en catalog, response o data.

El proyecto incluye dos archivos de referencia para desplegar esta capacidad:

  • vanilla_catalog_schema.sql: tabla de catálogo aislada por tenant y proyecto.
  • catalog_endpoint.php: endpoint autenticado que devuelve únicamente el catálogo del proyecto actual.

Versión y caché

db.catalogVersion();

await db.fetchCatalog({
    cacheTtl: 300000,
    refresh: false
});

db.clearCatalogCache();

La caché está aislada por endpoint y project key. La versión positiva del documento se conserva al serializar el catálogo.

Consultar el catálogo

db.collections();
db.describe("orders");
db.relations();
db.capabilities();
db.relationPath("orders", "customers");

Validación estricta

Con strictCatalog: true, el SDK valida antes del request:

  • Collections conocidas.
  • Campos conocidos.
  • Compatibilidad del operador con el tipo.
  • Operadores declarados por el backend.
  • Casts declarados por el backend.
  • Valores array para IN y NOT IN.
  • Valores null para IS e IS NOT.

Los errores utilizan CATALOG_ERROR o CAPABILITY_ERROR.

Tipado automático

Puede activarse globalmente con autoCast: true o por consulta:

const result = await db
    .from("orders")
    .select("order_id", "total")
    .typed()
    .get();

numeric se convierte en number, integer en entero, boolean en booleano, fechas en Date, objetos y arrays en JSON.

Generar TypeScript

const declarations = db
    .getCatalog()
    .toTypeScript({ namespace: "VanillaSchema" });

También puede usarse la herramienta de línea de comandos:

node generate_vanilla_types.mjs collection_catalog.json StowDB-schema.d.ts

Genera interfaces, enums como uniones literales, nullability, CollectionName y DocumentOf<C>.

32. Uso como módulo

Navegador tradicional

<script src="https://stowdb.miramaxtech.com/sdk/v1/stowdb.js"></script>

ES Modules en navegador o Node.js

import StowDB, {
    VanillaError,
    VanillaCatalog
} from "./StowDB.mjs";

En el navegador también puede importarse directamente desde https://stowdb.miramaxtech.com/sdk/v1/stowdb.mjs.

CommonJS

const {
    StowDB,
    VanillaError,
    VanillaCatalog
} = require("./StowDB.cjs");

stowdb.js mantiene las variables globales para compatibilidad y también exporta mediante module.exports cuando se ejecuta como CommonJS.

Distribución oficial

El SDK se entrega exclusivamente mediante el portal de StowDB y el CDN oficial. El manifiesto package.json está marcado como privado para impedir su publicación accidental en npm.

URLs estables de la versión 1:

  • https://stowdb.miramaxtech.com/sdk/v1/StowDB-sdk-v1.0.0.zip
  • https://stowdb.miramaxtech.com/sdk/v1/stowdb.js
  • https://stowdb.miramaxtech.com/sdk/v1/stowdb.mjs
  • https://stowdb.miramaxtech.com/sdk/v1/stowdb.cjs
  • https://StowDB.miramaxtech.com/sdk/v1/StowDB.d.ts
  • https://StowDB.miramaxtech.com/sdk/v1/SHA256SUMS.txt

Usa siempre /sdk/v1/ en producción. /sdk/latest/ se reserva para pruebas porque puede cambiar sin modificar la URL.

Parsers compatibles

Las versiones incluidas de parse_select.php, parse_update.php y parse_delete.php aceptan:

  • Grupos AND y OR anidados hasta diez niveles.
  • Máximo de cien condiciones reales por WHERE.
  • IN, NOT IN, BETWEEN, IS NULL, IS NOT NULL, LIKE y comparadores.
  • CONTAINS y NOT CONTAINS mediante JSONB @> parametrizado.
  • Tipado automático de números, booleanos y fechas.

parse_select.php añade aliases de salida, aliases de collections, joins INNER, LEFT, RIGHT, FULL y CROSS, múltiples joins, grupos ON arbitrariamente anidados, agregaciones, GROUP BY, HAVING, DISTINCT, ordenamiento y paginación.

33. Condiciones operativas

Las limitaciones implementables del SDK están cubiertas. Permanecen condiciones que dependen del despliegue o de la naturaleza de los datos:

  • POST /catalog y sus tablas deben desplegarse para que el navegador pueda enumerar collections del proyecto.
  • El catálogo debe reflejar las relaciones y capacidades reales del parser desplegado.
  • La paginación keyset necesita un campo estable, único, seleccionable y comparable.
  • La idempotencia requiere desplegar su tabla e integrar IdempotencyStore en los endpoints de escritura.
  • El portal y las rutas /sdk/v1/ y /sdk/latest/ deben publicarse en IIS.
  • Las API keys enviadas al navegador siguen siendo visibles para el usuario de ese navegador.

34. Recomendaciones

  • Usa IDs únicos en filtros de actualización y eliminación.
  • Usa .limit(1) cuando esperas afectar un solo documento.
  • Evita límites grandes en el frontend.
  • Selecciona únicamente los campos necesarios.
  • Usa tipos numéricos reales en JSON para cantidades y totales.
  • Crea índices apropiados en el backend para filtros frecuentes.
  • No expongas API keys con permisos administrativos en aplicaciones públicas.
  • Captura siempre los errores con try/catch.