Datos
Bases de datos, colecciones y registros con esquemas y reglas, por una sola puerta REST/JSON.
Descripción general
Datos es la base de datos de tu aplicación, servida por una sola puerta REST/JSON en https://data.inovacc.dev. Creas bases de datos, les das colecciones, y lees y escribes registros con simples llamadas HTTP. Una colección es tipada, con campos declarados por un esquema, o libre, y contiene documentos JSON sin esquema. Quién puede hacer qué se decide en cada solicitud, primero por los permisos de la credencial y luego, registro por registro, por las reglas que declara la colección.
El problema que resuelve es operar datos persistentes sin operar una base de datos: sin servidor, sin pool de conexiones, sin herramienta de migraciones, sin respaldos que programar. Tu aplicación conserva su propio código y almacena aquí sus datos; dónde viven los datos lo decide Inovacc y nunca se muestra. La misma API atiende a tus servidores, a tus páginas web (con una clave de navegador) y a las personas con sesión iniciada en tu app, y un oyente en tiempo real envía los cambios a medida que ocurren.
Usa Datos para el estado de la aplicación: notas de usuarios, pedidos, configuraciones, catálogos, cualquier cosa con forma de registros. Almacena contenido binario grande con Archivos, consulta lo que sucedió con el Registro de actividad, y notifica a otros sistemas con Eventos.
Conceptos
Bases de datos. Una base de datos es una clave que elige tu aplicación, como shop o crm/v2 (una / se escribe %2F en una ruta). Una base de datos pertenece a la aplicación cuya credencial la creó; la base de datos de otra aplicación responde 404, como si no existiera. Una credencial a nivel de organización ve todas las bases de datos de la organización.
Colecciones: con esquema o libres. Una colección con esquema declara hasta 64 campos, cada uno con un tipo: text, integer, number, bool, datetime, json, relation (un id de registro en una colección target) o blob (el SHA-256 de un archivo en la misma base de datos). Un campo puede ser required, unique, indexed o fulltext (en text). Una colección libre no declara nada: cada registro es un documento JSON, consultado por ruta con puntos (profile.city), y se le puede dar un esquema más adelante cuando todos los documentos encajen. Escribir en una colección que no existe crea una libre, cuando tu credencial puede cambiar esquemas.
Registros y versiones. Un registro es {id, version, created_at, updated_at, ...fields}. Su id lo genera el servicio (rec_ más 26 caracteres ordenables) o lo eliges tú. Cada cambio sube version en uno; las actualizaciones y eliminaciones deben enviar If-Match: <version>, de modo que dos escritores nunca se sobrescriban en silencio.
Reglas. Una colección declara cinco reglas, list, view, create, update y delete. Cada una es null (nadie), "" (cualquier credencial de servidor de tu organización), public (cualquiera, incluidas las claves de navegador), users (las personas con sesión iniciada de la aplicación propietaria, y los servidores), o una expresión como owner = @auth.id. Las reglas se aplican a toda credencial, incluida la de un administrador.
Credenciales. Una clave secreta es para servidores. Una clave publicable (apb_...) es para páginas web: funciona solo desde los orígenes que lista su aplicación y solo donde una regla la admite. El token de acceso de una persona con sesión iniciada lleva solo permisos de registros y archivos.
Cómo funciona
Toda llamada lleva Authorization: Bearer <credential>. No hay encabezado de organización: tu organización, y la aplicación, se obtienen de la credencial, y nada en una ruta, una consulta o un encabezado puede nombrar a otra. Una solicitud sin una credencial válida, o a una ruta que no es una ruta del servicio, recibe un único 401 idéntico.
Para cada llamada el servicio primero verifica el permiso de la credencial para la acción (leer, escribir, crear, actualizar, eliminar o cambios de esquema) sobre la base de datos. Cualquier cosa distinta de "permitir" es 403 forbidden sin detalle. Luego la regla de la colección decide por registro: una lista devuelve solo los registros que admite la regla list; leer un registro que la regla view oculta es 404, igual que un registro inexistente; una creación verifica los datos nuevos; una actualización verifica tanto el registro almacenado como los datos nuevos.
Las escrituras llevan versión. POST /records crea y responde 201 con ETag: "1". PATCH cambia los campos nombrados (en una colección libre es un parche de combinación JSON), PUT reemplaza el registro, y PUT con If-None-Match: * lo crea en el id que elijas. Un If-Match incorrecto es 409 version_conflict; uno ausente es 428.
Las listas toman filter (field op value unidos por && y ||), sort, limit (hasta 200) y un cursor opaco, y devuelven next_cursor hasta la última página; q busca en los campos fulltext. Un lote aplica hasta 100 creaciones, actualizaciones y eliminaciones en una sola transacción: todas tienen éxito, o ninguna.
Un oyente en tiempo real es un WebSocket en GET .../collections/{c}/listen: envía ready, luego eventos added, modified y removed para los registros que tu regla te permite listar. Cada llamada se registra en el Registro de actividad.
Primeros pasos
Necesitas una clave secreta con permiso para crear bases de datos y colecciones (Autenticación).
- Crea una base de datos.
PUT /v1/databases/notessin cuerpo. La respuesta es 201 con{key, created_at}; repetirlo responde 200. - Declara una colección.
PUT /v1/databases/notes/collections/itemscon los camposownerybodyy reglas (consulta los ejemplos). La respuesta es la colección conschema_version1. - Escribe un registro.
POST .../collections/items/recordscon los campos. La respuesta es 201 con el registro, suidyversion1. - Actualízalo.
PATCH .../records/{id}conIf-Match: 1. La respuesta tieneversion2; enviarIf-Match: 1de nuevo es 409version_conflict. - Lista.
GET .../records?filter=owner%20%3D%20%22me%22&limit=10devuelve tu registro ynext_cursor: null.
Casos de uso
Datos por usuario en una aplicación web. Una aplicación de notas declara una colección cuyas cinco reglas dicen owner = @auth.id. La página llama a Datos directamente con el token de la persona con sesión iniciada; cada persona lista y edita solo sus propias notas, y la nota de otra persona es un 404. Ningún código de servidor lo hace cumplir: lo hacen las reglas.
Un catálogo público. Una tienda en línea mantiene products como una colección libre con list y view en public y las escrituras cerradas. El escaparate la lee desde el navegador con una clave publicable; la administración la escribe desde un servidor con una clave secreta.
Paneles en vivo. Una pantalla de operaciones abre un oyente sobre orders filtrado por los pedidos abiertos de hoy. Ejecuta la consulta después de ready, y luego aplica los eventos added, modified y removed a medida que llegan, sin sondeo.
Límites y precios
| Límite | Valor |
|---|---|
| Cuerpo de solicitud JSON | 1 MiB |
| Un registro | 900 KiB |
Un campo json | 64 KiB |
| Campos por colección; colecciones por base de datos | 64; 64 |
| Registros por página | 200 (por defecto 50) |
| Operaciones por lote | 100, en una sola transacción |
| Filtro | 2,048 caracteres, 40 valores, profundidad de anidamiento 8 |
| Cadena de consulta | 16 KiB |
Búsqueda de texto completo q | 16 términos, 256 caracteres |
| Oyentes por colección; un evento de oyente | 1,000; 512 KiB |
| Tasa | 600 solicitudes por minuto por titular de credencial, por ubicación (429 rate_limited) |
Precios: a consultar.
Errores
| Estado | Código | Qué significa y qué hacer |
|---|---|---|
| 400 | invalid_body, invalid_name, unknown_field, missing_field, invalid_value | El cuerpo o un nombre incumple las reglas; corrígelo. |
| 400 | record_too_large, field_too_large, reserved_field | Reduce el registro; no envíes id, version, created_at, updated_at en un cuerpo. |
| 400 | invalid_schema, invalid_rule, schema_change_unsupported | La definición de la colección no es válida, o se cambió un campo en el mismo lugar. |
| 400 | invalid_filter, invalid_sort, invalid_cursor, invalid_limit, invalid_search | Corrige la consulta de la lista; empieza de nuevo sin el cursor. |
| 400 | invalid_if_match, batch_too_large, cross_shard_batch | Revisa el encabezado o divide el lote. |
| 401 | invalid_credentials | Envía una credencial válida. |
| 403 | forbidden | Los permisos de la credencial o la regla de la colección lo rechazan. |
| 403 | origin_not_allowed, secret_key_in_browser | Una clave de navegador desde un origen no listado, o una clave secreta en un navegador. |
| 404 | database_not_found, collection_not_found, record_not_found | No existe, o no tienes permiso para verlo. |
| 409 | version_conflict | Alguien cambió el registro; léelo de nuevo y reintenta. |
| 409 | already_exists, unique_violation, not_empty, schema_violation | El id o el valor ya está en uso; vacíala antes de eliminar; los documentos no encajan en el esquema. |
| 412, 428 | precondition_failed, precondition_required | El registro ya existe; envía If-Match. |
| 413 | payload_too_large | El cuerpo supera 1 MiB. |
| 429 | rate_limited, too_many_listeners | Reduce el ritmo; cierra los oyentes que no necesites. |
| 503 | service_unavailable | Reintenta más tarde; no se escribió nada. |
Buenas prácticas
- Envía siempre
If-Matchcon la versión que leíste, y ante 409version_conflictvuelve a leer antes de reintentar. - Cierra toda colección por defecto y ábrela regla por regla; una colección nacida de una escritura admite cualquier credencial de servidor hasta que la restrinjas.
- Nunca pongas una clave secreta en una página web: usa una clave publicable y reglas
publico@auth. - Elige tus propios ids de registro para las importaciones, de modo que una importación reintentada no cree nada dos veces (409
already_exists). - Usa un lote cuando varias escrituras deban tener éxito juntas.
- Indexa los campos por los que filtras y ordenas, y sigue
next_cursoren lugar de usar páginas grandes. - Con un oyente, espera a
readyantes de consultar, y luego descarta los eventos que no sean más recientes que lo que devolvió la consulta.
Relacionados
- Archivos: contenido binario junto a tus registros
- Registro de actividad: qué se escribió, y quién lo hizo
- Eventos: avisa a otros sistemas de un cambio
- Autenticación, Errores, Límites
Autenticación
Toda llamada lleva tu clave; tu organización viene de ella. Consulta la guía de autenticación.
AuthorizationEndpoints
GET /v1/databases
PUT /v1/databases/{database}
PUT /v1/databases/{database}/collections/{collection}
POST /v1/databases/{database}/collections/{collection}/records
POST /v1/databases/{database}/batch
Ejemplo
Lenguaje
⋮
cURL
curl "https://data.inovacc.dev/v1/databases" \
-H "Authorization: Bearer $INOVACC_API_KEY"curl "https://data.inovacc.dev/v1/databases" \
-H "Authorization: Bearer $INOVACC_API_KEY"
TypeScript
const url = "https://data.inovacc.dev/v1/databases";
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.INOVACC_API_KEY}`,
},
});
console.log(response.status, await response.text());const url = "https://data.inovacc.dev/v1/databases";
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.INOVACC_API_KEY}`,
},
});
console.log(response.status, await response.text());
Python
import os
import urllib.request
request = urllib.request.Request(
"https://data.inovacc.dev/v1/databases",
method="GET",
headers={
"User-Agent": "inovacc-python-sample",
"Authorization": "Bearer " + os.environ["INOVACC_API_KEY"],
},
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())import os
import urllib.request
request = urllib.request.Request(
"https://data.inovacc.dev/v1/databases",
method="GET",
headers={
"User-Agent": "inovacc-python-sample",
"Authorization": "Bearer " + os.environ["INOVACC_API_KEY"],
},
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Go
package main
import (
"fmt"
"io"
"net/http"
"os"
)
const url = "https://data.inovacc.dev/v1/databases"
func main() {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("INOVACC_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(out))
}package main
import (
"fmt"
"io"
"net/http"
"os"
)
const url = "https://data.inovacc.dev/v1/databases"
func main() {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("INOVACC_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(out))
}
Rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
fn main() -> Result<(), Box<dyn std::error::Error>> {
let api_key = std::env::var("INOVACC_API_KEY")?;
let response = reqwest::blocking::Client::new()
.get("https://data.inovacc.dev/v1/databases")
.bearer_auth(api_key)
.send()?;
println!("{} {}", response.status(), response.text()?);
Ok(())
}// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
fn main() -> Result<(), Box<dyn std::error::Error>> {
let api_key = std::env::var("INOVACC_API_KEY")?;
let response = reqwest::blocking::Client::new()
.get("https://data.inovacc.dev/v1/databases")
.bearer_auth(api_key)
.send()?;
println!("{} {}", response.status(), response.text()?);
Ok(())
}
JavaScript
const url = "https://data.inovacc.dev/v1/databases";
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.INOVACC_API_KEY}`,
},
});
console.log(response.status, await response.text());const url = "https://data.inovacc.dev/v1/databases";
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.INOVACC_API_KEY}`,
},
});
console.log(response.status, await response.text());
PHP
<?php
$curl = curl_init('https://data.inovacc.dev/v1/databases');
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INOVACC_API_KEY'),
],
]);
$response = curl_exec($curl);
echo curl_getinfo($curl, CURLINFO_HTTP_CODE), ' ', $response, "\n";
curl_close($curl);<?php
$curl = curl_init('https://data.inovacc.dev/v1/databases');
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INOVACC_API_KEY'),
],
]);
$response = curl_exec($curl);
echo curl_getinfo($curl, CURLINFO_HTTP_CODE), ' ', $response, "\n";
curl_close($curl);
Ruby
require "net/http"
require "uri"
uri = URI('https://data.inovacc.dev/v1/databases')
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('INOVACC_API_KEY')}"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
http.request(request)
end
puts "#{response.code} #{response.body}"require "net/http"
require "uri"
uri = URI('https://data.inovacc.dev/v1/databases')
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('INOVACC_API_KEY')}"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
http.request(request)
end
puts "#{response.code} #{response.body}"
Java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Main {
public static void main(String[] args) throws Exception {
String apiKey = System.getenv("INOVACC_API_KEY");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://data.inovacc.dev/v1/databases"))
.header("Authorization", "Bearer " + apiKey)
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Main {
public static void main(String[] args) throws Exception {
String apiKey = System.getenv("INOVACC_API_KEY");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://data.inovacc.dev/v1/databases"))
.header("Authorization", "Bearer " + apiKey)
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
C#
var url = "https://data.inovacc.dev/v1/databases";
var apiKey = Environment.GetEnvironmentVariable("INOVACC_API_KEY");
using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Add("Authorization", $"Bearer {apiKey}");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");var url = "https://data.inovacc.dev/v1/databases";
var apiKey = Environment.GetEnvironmentVariable("INOVACC_API_KEY");
using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Add("Authorization", $"Bearer {apiKey}");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
Kotlin
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
fun main() {
val apiKey = System.getenv("INOVACC_API_KEY")
val request = HttpRequest.newBuilder()
.uri(URI.create("https://data.inovacc.dev/v1/databases"))
.header("Authorization", "Bearer " + apiKey)
.GET()
.build()
val response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString())
println("${response.statusCode()} ${response.body()}")
}import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
fun main() {
val apiKey = System.getenv("INOVACC_API_KEY")
val request = HttpRequest.newBuilder()
.uri(URI.create("https://data.inovacc.dev/v1/databases"))
.header("Authorization", "Bearer " + apiKey)
.GET()
.build()
val response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString())
println("${response.statusCode()} ${response.body()}")
}
Swift
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
let apiKey = ProcessInfo.processInfo.environment["INOVACC_API_KEY"] ?? ""
let url = URL(string: "https://data.inovacc.dev/v1/databases")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
print(status, String(decoding: data, as: UTF8.self))import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
let apiKey = ProcessInfo.processInfo.environment["INOVACC_API_KEY"] ?? ""
let url = URL(string: "https://data.inovacc.dev/v1/databases")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
print(status, String(decoding: data, as: UTF8.self))
Detalles de la fuente
- Id
- identity/component-taxonomy/capabilities#data
- Repositorio
- identity
- Ruta
- contracts/component-taxonomy/catalog/capabilities.json
- Commit
- 08ef3cd75d82
- Id
- identity/component-taxonomy/quickstart#data
- Repositorio
- identity
- Ruta
- contracts/component-taxonomy/catalog/capabilities.json
- Commit
- 08ef3cd75d82