@next-model/local-storage-connector
v1.2.1
Published
Browser localStorage connector for next-model. Inherits from MemoryConnector.
Downloads
293
Maintainers
Readme
@next-model/local-storage-connector
Browser localStorage connector for @next-model/core.
LocalStorageConnector extends MemoryConnector, so every behaviour you get from the in-memory store (filter operators, ordering, aggregates, transactions with rollback, schema DSL, JS-expression execute) carries over verbatim. The only differences are persistence and id management.
Installation
pnpm add @next-model/core @next-model/local-storage-connector
# or: npm install @next-model/core @next-model/local-storage-connector@next-model/core is declared as a peerDependency — install it alongside the connector so pnpm/npm materialise it into your app's node_modules.
Constructing the connector
import { LocalStorageConnector } from '@next-model/local-storage-connector';
// Browser: picks up globalThis.localStorage automatically
const connector = new LocalStorageConnector();
// With an injected store (Node tests, sandboxed environments)
import { MemoryLocalStorage } from '@next-model/local-storage-connector/dist/__mocks__/MemoryLocalStorage.js';
const connector = new LocalStorageConnector({ localStorage: new MemoryLocalStorage() });
// Namespacing keys
const connector = new LocalStorageConnector({
prefix: 'app:',
suffix: ':v2',
});When neither localStorage (option) nor globalThis.localStorage is available, the constructor throws — letting you fail fast in environments where the Web Storage API is not present.
Attaching a typed schema
Pass a DatabaseSchema (from @next-model/core's defineSchema(...)) so Model({ connector, tableName: 'users' }) can infer per-table props at the type level. Both call shapes work:
import { defineSchema } from '@next-model/core';
const schema = defineSchema({
users: { columns: { id: { type: 'integer', primary: true }, email: { type: 'string' } } },
});
// Single-arg form (preferred):
const connector = new LocalStorageConnector({ prefix: 'app:', schema });
// Legacy two-arg form (still supported):
const connector = new LocalStorageConnector({ prefix: 'app:' }, { schema });Existing call sites without a schema keep working unchanged.
Materialising tables with ensureSchema()
Inherited from MemoryConnector: when a schema is attached, connector.ensureSchema() iterates every declared table and initialises it idempotently. Returns { created, existing }. Call once on app boot:
const connector = new LocalStorageConnector({ prefix: 'app:' }, { schema });
const { created } = await connector.ensureSchema();Wiring a Model
import { Model } from '@next-model/core';
import { LocalStorageConnector } from '@next-model/local-storage-connector';
const connector = new LocalStorageConnector();
class Note extends Model({
tableName: 'notes',
connector,
init: (props: { title: string; body: string }) => props,
}) {}
await Note.create({ title: 'Hello', body: 'world' });
await Note.count(); // 1Feature → connector specifics
Storage layout
Each table is stored under ${prefix}${tableName}${suffix} as a JSON-serialised array. A sidecar key ${prefix}${tableName}${suffix}__nextId tracks the next auto-increment id so deleted rows never have their id reused.
Reads load the table on first access of a request; writes serialise back after every mutation. There is no in-memory cache, so swapping localStorage between tabs (e.g. browser sync) is observed on the next call.
Filter operators, aggregates, ordering
Inherited from MemoryConnector. All operators ($and, $or, $not, $in, $notIn, $null, $notNull, $between, $notBetween, $gt/$gte/$lt/$lte, $like, $async, $raw) and the same ordering / limit / skip semantics apply. $like uses the MemoryConnector's pattern matcher, not SQL LIKE.
execute(query, bindings)
Inherited from MemoryConnector: query is a function (storage, ...bindings) => any[] that walks the in-memory storage map directly. The JS-source-string form that older versions accepted has been removed — passing a string now throws UnsupportedOperationError with a migration hint. SQL connectors keep accepting SQL strings.
Transactions
Inherited from MemoryConnector: transaction(fn) snapshots storage and lastIds (via structuredClone), runs fn, and either commits in place or rolls back to the snapshot on throw. The whole localStorage tree is not synchronised across tabs during the transaction, so concurrent writers from another tab can race a rollback.
batchInsert
Each row is pushed to the in-memory table array, then the table is serialised back to localStorage. Auto-increment ids come from the per-table __nextId counter (incremented even after deletes).
Schema DSL
createTable(name, blueprint) initialises an empty table array; dropTable(name) removes both the data array and the __nextId counter; hasTable(name) checks whether the data key exists. Column types declared in the blueprint are validated up-front via defineTable but not enforced at runtime — localStorage has no schema layer.
Changelog
See HISTORY.md.
