Documentação

Introdução

Receitas curtas e completas para você copiar.


Uma lista de tarefas com persistência

const app = new sorla();
app.useDb('todo');
app.db.createCollection('tasks', {
  required: ['title'],
  properties: { title: { type: 'string', minLength: 1 }, done: { type: 'boolean', default: false } }
});
const tasks = app.db.tasks;

// carrega o que foi salvo e depois salva a cada mudança
tasks.insertMany(JSON.parse(localStorage.getItem('tasks') ?? '[]'));
tasks.on('change', () => localStorage.setItem('tasks', JSON.stringify(tasks.find({}).toArray())));

// renderiza sempre que a lista muda
tasks.watch({}, { sort: { done: 1, title: 1 } }).subscribe(list => {
  document.querySelector('#list').innerHTML = list
    .map(t => `<li data-id="${t._id}" class="${t.done ? 'done' : ''}">${t.title}</li>`).join('');
});

document.querySelector('#list').addEventListener('click', e => {
  const id = e.target.dataset.id;
  const task = tasks.findOne({ _id: id });
  tasks.updateOne({ _id: id }, { $set: { done: !task.done } });
});

Busca enquanto você digita

const escape = text => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
input.addEventListener('input', () => {
  const pattern = { $regex: escape(input.value), $options: 'i' };
  const results = products.find({ $or: [{ name: pattern }, { tags: pattern }] }).limit(20).toArray();
  render(results);
});

Um paginador

function page(query, { page = 1, size = 20, sort = { _id: 1 } } = {}) {
  const total = products.count(query);
  const rows = products.find(query).sort(sort).skip((page - 1) * size).limit(size).toArray();
  return { rows, total, pages: Math.ceil(total / size) };
}

Cache de respostas da API

async function loadProducts() {
  if (products.count()) return products.find({}).toArray();
  const rows = await (await fetch('/api/products')).json();
  products.insertMany(rows);
  return products.find({}).toArray();
}

Mesclando dados do servidor (linhas novas e existentes)

function merge(rows) {
  const existing = new Set(products.find({ _id: { $in: rows.map(r => r._id) } }).ids());
  return products.bulkWrite(rows.map(row => existing.has(row._id)
    ? { replaceOne: { filter: { _id: row._id }, replacement: row } }
    : { insertOne: { document: row } }));
}

Desfazer a última mudança

const undo = [];
products.on('update', ({ ids, previous }) => undo.push({ ids, previous }));
function undoLast() {
  const last = undo.pop();
  if (!last) return;
  products.bulkWrite(last.ids.map((id, i) => ({ replaceOne: { filter: { _id: id }, replacement: last.previous[i] } })));
}

(Desfazer também emite um evento update; pule o registro enquanto desfaz se você precisar de uma pilha de refazer.)


Agrupar e somar

function sumBy(collection, field, value, query = {}) {
  const totals = {};
  for (const doc of collection.find(query)) totals[doc[field]] = (totals[doc[field]] ?? 0) + doc[value];
  return totals;
}
sumBy(orders, 'city', 'total', { status: 'done' });   // { Recife: 1520, Curitiba: 830 }

Exportar e importar JSON ou CSV

const json = JSON.stringify(orders.find({}).toArray());

const toCsv = rows => {
  const fields = Object.keys(rows[0] ?? {});
  const cell = v => `"${String(v ?? '').replace(/"/g, '""')}"`;
  return [fields.join(','), ...rows.map(r => fields.map(f => cell(r[f])).join(','))].join('\n');
};
const csv = toCsv(orders.find({}, { _id: 0 }).toArray());

Um formulário com validação em três linhas

app.db.createCollection('contacts', { required: ['name', 'email'], properties: { name: { type: 'string', minLength: 2 }, email: { type: 'string', format: 'email' } } });
app.db.contacts.bindForm('#contact', { generate: true, onSuccess: () => alert('Thanks!') });

Uma instância compartilhada entre módulos

// db.js
import sorla from 'sorla-db';
export const app = new sorla();
app.useDb('app');
for (const name of ['users', 'orders', 'products']) app.db.createCollection(name);
export const { users, orders, products } = app.db;

// em qualquer lugar
import { orders } from './db.js';

Perguntas frequentes

Os dados são salvos quando a página recarrega? Não, o sorla-db vive na memória. Salve com um ouvinte de eventos (veja a receita da lista de tarefas) ou recarregue do seu servidor; para grandes volumes de dados, envie-os em fluxo com Server-Sent Events.

Posso usar no servidor? Sim: ele roda no Node 18+, o que é útil para testes, scripts, caches e para responder às mesmas consultas que o seu frontend envia (veja o servidor WebSocket em Sincronização com o servidor).

Quão parecido ele é com o MongoDB? A linguagem de consulta, os operadores de atualização, o bulkWrite, as projeções e os resultados seguem o MongoDB. Diferenças: { field: null } corresponde só a null (não a campos ausentes), campos ausentes e null ficam por último na ordenação por padrão, não há pipelines de agregação nem índices secundários, e _id pode ser qualquer valor.

Por que recebo cópias em vez dos objetos armazenados? Para que nada fora da coleção altere os dados dela por acidente. Os cursores copiam só o que você lê, então as cópias custam pouco; ids() e count() não copiam nada.

Posso armazenar instâncias de classes ou funções? Não: os documentos precisam ser dados clonáveis (objetos, arrays, strings, números, booleanos, null, Date).

Como adiciono um índice em outro campo? Só o _id é indexado. Consultas em outros campos percorrem a coleção, o que leva milissegundos para dezenas de milhares de documentos. Se você busca muito por outra chave, use-a como _id.

E a concorrência? O JavaScript executa uma coisa de cada vez, então toda chamada é atômica. insertMany, updateMany e cada operação do bulkWrite validam antes de escrever.

Funciona com o meu framework? Sim: React, Vue, Svelte e Angular têm integrações, e qualquer coisa que entenda Observables ou callbacks simples funciona com watch(). Veja Consultas vivas.

Qual é o tamanho? Cerca de 54 kB minificado para o bundle do navegador, com formulários, validação de schema, consultas vivas e os dois transportes de sincronização incluídos.