Skip to content

v9.6.0

Choose a tag to compare

@jamesgpearce jamesgpearce released this 30 Aug 03:20
· 20 commits to main since this release

In Summary

And more!

PostgreSQL, via pg

The new persister-pg module provides the PgPersister, which binds to PostgreSQL databases with the pg module - the de facto standard PostgreSQL driver for Node.js.

It joins the existing PostgresPersister and PglitePersister, and is the one to reach for with hosted services that offer a pg-compatible driver. Neon, for example, has a serverless driver whose Pool and Client objects can be passed straight to the createPgPersister function, so you can persist a Store from an edge runtime that cannot open a TCP connection.

import {Pool} from 'pg';
import {createStore} from 'tinybase';
import {createPgPersister} from 'tinybase/persisters/persister-pg';

const nodePgPool = new Pool({
  connectionString: 'postgres://localhost:5432/tinybase',
});
const nodePgStore = createStore().setTables({pets: {fido: {species: 'dog'}}});
const nodePgPersister = await createPgPersister(
  nodePgStore,
  nodePgPool,
  'my_tinybase',
);

await nodePgPersister.save();
console.log((await nodePgPool.query('SELECT * FROM my_tinybase;')).rows);
// -> [{_id: '_', store: '[{"pets":{"fido":{"species":"dog"}}},{}]'}]

await nodePgPool.query('UPDATE my_tinybase SET store = $1 WHERE _id = $2;', [
  '[{"pets":{"felix":{"species":"cat"}}},{}]',
  '_',
]);
await nodePgPersister.load();
console.log(nodePgStore.getTables());
// -> {pets: {felix: {species: 'cat'}}}

await nodePgPersister.destroy();
await nodePgPool.query('DROP TABLE my_tinybase;');
await nodePgPool.end();

Both JSON and tabular modes are supported, as is reactive auto-loading, and a MergeableStore can be persisted in JSON mode. There's more information in the documentation for the new persister-pg module.

Supabase

Also new is the persister-supabase module, which provides the SupabasePersister (as requested in issue #204). Pass the createSupabasePersister function the client you get back from Supabase's createClient function, and your Store is persisted to a table in your project:

import {createClient} from '@supabase/supabase-js';
import {createStore} from 'tinybase';
import {createSupabasePersister} from 'tinybase/persisters/persister-supabase';

const supabase = createClient('https://my-project.supabase.co', 'anon-key');
const store = createStore().setTables({pets: {fido: {species: 'dog'}}});
const persister = createSupabasePersister(store, supabase, 'my_tinybase');

await persister.save();
await persister.startAutoLoad();

Unlike the other PostgreSQL Persisters, this one goes through Supabase's REST API rather than connecting to the database. So it runs in a browser or edge runtime, your row-level security policies apply to what it reads and writes, and the startAutoLoad method hears about other clients' changes over Supabase Realtime instead of polling. In return, only the JSON serialization mode is available, since the REST API cannot run the arbitrary SQL that tabular mapping needs.

It also issues no DDL at all, unlike its siblings. Where they create tables, add and drop columns as Cells come and go, and install their own change-notification triggers, this one touches nothing but a single row. You create the table and its policies yourself and they stay exactly as you left them, and the persister-supabase module documentation has the SQL to do it.

SQLite, via better-sqlite3

The new persister-better-sqlite3 module provides the BetterSqlite3Persister, which binds to a local SQLite database with the popular synchronous better-sqlite3 module:

import Database from 'better-sqlite3';
import {createBetterSqlite3Persister} from 'tinybase/persisters/persister-better-sqlite3';

const betterDb = new Database(':memory:');
const betterStore = createStore().setTables({pets: {fido: {species: 'dog'}}});
const betterPersister = createBetterSqlite3Persister(
  betterStore,
  betterDb,
  'my_tinybase',
);

await betterPersister.save();
console.log(betterDb.prepare('SELECT * FROM my_tinybase;').all());
// -> [{_id: '_', store: '[{"pets":{"fido":{"species":"dog"}}},{}]'}]

await betterPersister.destroy();
betterDb.close();

Since better-sqlite3 does not signal when the database changes underneath it, automatic loading polls. There's more information in the documentation for the new persister-better-sqlite3 module.

SQLite In Capacitor

The new persister-capacitor-sqlite module provides the CapacitorSqlitePersister, which binds to a SQLite database in a Capacitor app via the @capacitor-community/sqlite plugin (as requested in issue #219). Both JSON and tabular modes work, as does persisting a MergeableStore in JSON mode.

import {CapacitorSQLite, SQLiteConnection} from '@capacitor-community/sqlite';
import {createStore} from 'tinybase';
import {createCapacitorSqlitePersister} from 'tinybase/persisters/persister-capacitor-sqlite';

const sqlite = new SQLiteConnection(CapacitorSQLite);
const db = await sqlite.createConnection('my.db', false, 'no-encryption', 1, false);
await db.open();

const store = createStore().setTables({pets: {fido: {species: 'dog'}}});
const persister = createCapacitorSqlitePersister(store, db, 'my_tinybase');
await persister.save();

Note that this module's tests run against a mocked plugin, since it needs a native iOS or Android runtime that a Node test suite cannot provide. Its SQL behavior is shared with the other SQLite Persisters and well covered by them, but the binding to the plugin itself is not exercised on a device, so please report anything that behaves differently in a real app.

MergeableStore Support For IndexedDB

The IndexedDbPersister can now persist a MergeableStore, closing a gap that had made IndexedDB the odd one out amongst the browser Persisters (as requested in issue #203). The SessionPersister, LocalPersister and OpfsPersister could all already do this; now the browser's most capable storage can too.

import {createMergeableStore} from 'tinybase';
import {createIndexedDbPersister} from 'tinybase/persisters/persister-indexed-db';

const store = createMergeableStore().setTables({pets: {fido: {species: 'dog'}}});
const persister = createIndexedDbPersister(store, 'petStore');

await persister.save();

A regular Store continues to use the 't' and 'v' object stores exactly as before, and a MergeableStore uses a new one called 'm', so databases written by previous versions are upgraded in place with their content intact.

SQL Placeholders

TinyBase used to generate PostgreSQL's numbered $1 placeholders for every database, and then rewrite them for the SQLite Persisters that objected. SQL is now built with the placeholder style each database family actually wants, so the stricter drivers work without anyone having to patch statements after the fact. That is what makes the new BetterSqlite3Persister possible at all; it also means the DurableObjectSqlStoragePersister no longer needs the workaround it carried, and that the PowerSyncPersister now works with PowerSync's Node SDK, which it previously did not.