npm.io
0.0.9 • Published yesterday

@playfast/reform-db

Licence
MIT
Version
0.0.9
Deps
0
Size
46 kB
Vulns
0
Weekly
348

@playfast/reform-db

Type-safe, PGlite-backed reactive SQL state for Reform.


Define tables once, read them reactively as Reform Sources, and write through ordinary Procedures — all with column-level type-safety inferred through Kysely. Built for on-device / local-first apps (Electron, Electrobun, browser) where the database lives next to the UI and writes are instant.

Install

npm install @playfast/reform-db kysely
# PGlite is an optional peer — only needed for the real driver (Db.pglite/Db.memory)
npm install @electric-sql/pglite

effect, kysely, and @playfast/reform are peer dependencies.

Quick Start

import { Db, DbColumn, DbQuery, DbSchema, DbTable, Migration } from '@playfast/reform-db'
import { Procedure } from '@playfast/reform'
import { Layer } from 'effect'

// 1. Define tables — each column carries its SQL type + a runtime Schema.
const Environments = DbTable.make('environments', {
  columns: {
    id: DbColumn.text({ primaryKey: true }),
    name: DbColumn.text(),
    status: DbColumn.literal(['idle', 'running', 'done']),
    createdAt: DbColumn.timestamp(),
  },
  primaryKey: 'id',
})

// 2. One schema → one Kysely Database type, shared by reads AND writes.
const AppDb = DbSchema.make('app', { tables: [Environments] })

// 3. A reactive read — `yield* ActiveEnvironments` yields AsyncData<Row[]>.
const ActiveEnvironments = DbQuery.make('activeEnvironments', { output: Environments.Row })
const ActiveEnvironmentsLive = DbQuery.live(ActiveEnvironments, {
  schema: AppDb,
  inputs: [],
  build: (_inputs, db) =>
    db.selectFrom('environments').selectAll().where('status', '=', 'running').orderBy('createdAt'),
})

// 4. A write — a Procedure using Db.exec, typed against the SAME schema.
const CreateEnvironmentLive = Procedure.live(CreateEnvironment, {
  run: ({ name }) =>
    Db.exec(
      AppDb.kysely
        .insertInto('environments')
        .values({ id: ulid(), name, status: 'idle', createdAt: Date.now() }),
    ),
})

// 5. Wire the driver at the root (renderer-side, instant writes).
const DataLayer = Layer.mergeAll(ActiveEnvironmentsLive, CreateEnvironmentLive).pipe(
  Layer.provideMerge(Db.pglite({ dataDir: 'idb://app', migrations: Migration.fromSchema(AppDb) })),
)

Use Db.memory() (ephemeral PGlite) in tests and @playfast/proof — the driver is an interface, so nothing else changes.

Key Concepts

Piece Role
DbColumn A column carrying both its SQL type and a runtime Schema (text, integer, real, timestamp, boolean, literal, json).
DbTable A table definition (make — columns + primary key); exposes a .Row schema.
DbSchema A set of tables → one Kysely Database type and a .kysely builder, shared by reads and writes.
DbQuery A reactive, SQL-backed Source (make + live) yielding AsyncData<Rows>.
Db The driver service: Db.pglite / Db.memory layers + the Db.exec write helper + dump() for snapshotting.
Migration CREATE TABLE DDL derived from a schema via Migration.fromSchema.

There is no DbMutation primitive: a write is a side effect inside an ordinary Reform Procedure. Direct DB writes are a local-app concern, so writes reuse the existing mutation path rather than adding a parallel concept.

How it fits Reform

Reform's React binding is useSyncExternalStore-shaped: a Store<A> exposes getSnapshot / subscribe / getVersion. A PGlite live query exposes the same shape. The core seam, SyncedStore (in @playfast/reform), bridges any external reactive source into a Reform Store. reform-db builds DbQuery on top of it, so a SQL query renders exactly like an AsyncCalc — its value is an AsyncData<Rows>.

PGlite live query ──► Db driver ──► SyncedStore ──► Reform render host

Docs

License

MIT

Keywords