@weft-js/db 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +556 -22
  2. package/package.json +3 -3
package/README.md CHANGED
@@ -1,38 +1,572 @@
1
- # @weft-js/db
1
+ <p align="center">
2
+ <a href="https://github.com/Ravonus/weft">
3
+ <img src="https://raw.githubusercontent.com/Ravonus/weft/main/site/public/logo.svg" width="88" height="88" alt="Weft">
4
+ </a>
5
+ </p>
2
6
 
3
- > **Early release (0.1.x).** APIs can change between minor versions.
7
+ <h1 align="center">@weft-js/db</h1>
4
8
 
5
- Database layers and list sources for [`@weft-js/start`](../start). Each driver is an optional peer dependency, and each
6
- subpath loads only its own.
9
+ <p align="center">
10
+ <strong>Prisma, plain SQL and six list sources for <code>@weft-js/start</code>, behind one <code>ctx.db</code>.</strong><br>
11
+ Every driver is an optional peer, and each subpath loads only its own.
12
+ </p>
7
13
 
8
- You rarely install this by hand. `npm create @weft-js` and `weft add db` pick the layer and write the files:
14
+ <p align="center">
15
+ <a href="https://www.npmjs.com/package/@weft-js/db"><img src="https://img.shields.io/npm/v/@weft-js/db?color=145c3c&label=npm" alt="npm version"></a>
16
+ <a href="https://github.com/Ravonus/weft/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-145c3c" alt="MIT license"></a>
17
+ <a href="https://github.com/Ravonus/weft/actions"><img src="https://img.shields.io/github/actions/workflow/status/Ravonus/weft/quality.yml?branch=main&label=checks" alt="checks"></a>
18
+ </p>
19
+
20
+ <p align="center">
21
+ <a href="#quick-start">Quick start</a> ·
22
+ <a href="https://github.com/Ravonus/weft/tree/main/docs">Docs</a> ·
23
+ <a href="https://github.com/Ravonus/weft/tree/main/examples">Examples</a> ·
24
+ <a href="https://github.com/Ravonus/weft/blob/main/CHANGELOG.md">Changelog</a> ·
25
+ <a href="https://github.com/Ravonus/weft">GitHub</a>
26
+ </p>
27
+
28
+ > [!IMPORTANT]
29
+ > **Early release (0.1.x).** APIs can change between minor versions. Pin the version and read the
30
+ > [changelog](https://github.com/Ravonus/weft/blob/main/CHANGELOG.md) before you upgrade.
31
+
32
+ [`@weft-js/start`](https://www.npmjs.com/package/@weft-js/start) <sup>[source](https://github.com/Ravonus/weft/tree/main/packages/start)</sup> comes with Drizzle on SQLite, Postgres
33
+ and MySQL. This package holds everything else a Start app might keep its data in: a Prisma layer, a no-ORM layer that is
34
+ just a `sql` tag over the driver, and list sources that page, sort, filter and search a table or collection safely. All
35
+ of them plug into the same machinery Start's own clients use, so migrations apply on the next request, `weft db
36
+ generate` knows where they live, and Q011 still checks your tags.
37
+
38
+ ## Highlights
39
+
40
+ - **Two data layers beside Drizzle.** `plugin-native` gives `ctx.db.sql` with no ORM; `plugin-prisma` hands you your own
41
+ Prisma client with migrations and tag checks added.
42
+ - **Values are always bound.** The Native `sql` tag turns every `${value}` into a driver parameter (`$1` on Postgres,
43
+ `?` elsewhere). There is no way to splice a value into the text.
44
+ - **Migrations without a deploy step.** Native's `migrations/*.sql` and Prisma's `prisma/migrations` apply before the
45
+ first statement a request runs, once per file, under a lock on Postgres and MySQL.
46
+ - **List sources with an allowlist.** `sqlSource`, `kyselySource`, `prismaSource`, `mongoSource`, `dynamoSource` and
47
+ `firestoreSource` read only the `columns` you name, and refuse any sort, filter or identifier outside them (Q002).
48
+ - **Only the driver you use loads.** `@weft-js/db/plugin-native/sqlite` never touches `postgres`, Prisma or MongoDB. A
49
+ test walks every entry's imports to keep it that way.
50
+ - **A clear error for a missing driver.** Import `@weft-js/db/kysely` without `kysely` installed and it says
51
+ `Run: npm install kysely`, instead of a module-resolution stack.
52
+ - **Outside writes for live queries.** `mongoChanges` and `watchMongo` turn a MongoDB change stream into tag pokes, and
53
+ `postgresTrigger` writes a NOTIFY trigger for any Postgres table.
54
+
55
+ ## Contents
56
+
57
+ - [Install](#install)
58
+ - [Quick start](#quick-start)
59
+ - [Choosing a layer](#choosing-a-layer)
60
+ - [The Native layer](#the-native-layer)
61
+ - [The Prisma layer](#the-prisma-layer)
62
+ - [Drizzle (built into Start)](#drizzle-built-into-start)
63
+ - [Migrations and `weft db`](#migrations-and-weft-db)
64
+ - [Auth on these layers](#auth-on-these-layers)
65
+ - [List sources](#list-sources)
66
+ - [Changes from MongoDB and Postgres](#changes-from-mongodb-and-postgres)
67
+ - [Registering in `weft.config.ts`](#registering-in-weftconfigts)
68
+ - [API reference](#api-reference)
69
+ - [Troubleshooting](#troubleshooting)
70
+ - [Related packages](#related-packages)
71
+ - [Contributing](#contributing)
72
+ - [License](#license)
73
+
74
+ ## Install
75
+
76
+ You rarely install this by hand. `npm create @weft-js` and `weft add db` pick the layer, install the right drivers and
77
+ write the files:
9
78
 
10
79
  ```bash
80
+ # a new app
11
81
  npm create @weft-js my-app -- --db postgres --layer native
12
- npx weft add db sqlite --layer prisma # in an existing app
82
+ pnpm create @weft-js my-app --db sqlite --layer prisma
83
+ bun create @weft-js my-app --db mysql --layer native
84
+
85
+ # an existing app with no database yet
86
+ npx weft add db sqlite --layer prisma
87
+ npx weft add db postgres --layer native
13
88
  ```
14
89
 
15
90
  `--db` takes `sqlite`, `postgres`, `mysql` or `none`. `--layer` takes `drizzle` (the default, which needs nothing from
16
- this package), `prisma` or `native`.
91
+ this package), `prisma` or `native`. `weft add db` refuses an app that already has a database, since changing engine
92
+ or layer changes your queries.
93
+
94
+ By hand, install the package next to `@weft-js/start` and the driver for what you use:
95
+
96
+ ```bash
97
+ npm install @weft-js/db @weft-js/start @weft-js/core
98
+ npm install postgres # plugin-native/postgres
99
+ npm install mysql2 # plugin-native/mysql
100
+ npm install kysely # kysely list source, plugin-native/auth
101
+ # Dynamodb
102
+ npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
103
+ ```
104
+
105
+ SQLite needs no driver: it uses `node:sqlite` on Node and Deno and `bun:sqlite` on Bun. The package requires Node 26 or
106
+ later (the `engines` field).
107
+
108
+ ## Quick start
109
+
110
+ A todo list on Postgres with no ORM. The client:
111
+
112
+ ```ts
113
+ // src/db/index.server.ts
114
+ import { createDb } from '@weft-js/db/plugin-native/postgres'
115
+ import { env } from '../env.server'
116
+
117
+ export const db = createDb({ url: env.DATABASE_URL })
118
+ ```
119
+
120
+ Put it on `ctx`:
121
+
122
+ ```ts
123
+ // src/context.server.ts
124
+ import { db } from '#/db/index.server'
125
+
126
+ export default () => ({ db })
127
+
128
+ declare module '@weft-js/start' {
129
+ interface Context {
130
+ db: typeof db
131
+ }
132
+ }
133
+ ```
134
+
135
+ Start a migration with `npx weft db generate todos`, which creates `migrations/0000_todos.sql`, and write the SQL:
136
+
137
+ ```sql
138
+ -- migrations/0000_todos.sql
139
+ create table todos (
140
+ id varchar(36) primary key,
141
+ title varchar(200) not null,
142
+ done boolean not null default false,
143
+ created_at bigint not null
144
+ );
145
+ ```
146
+
147
+ Use it from server functions. The migration applies before the first statement runs:
148
+
149
+ ```ts
150
+ // src/server/todos.ts
151
+ import { mutation, query } from '@weft-js/start'
152
+ import { z } from 'zod'
153
+
154
+ interface Todo {
155
+ id: string
156
+ title: string
157
+ done: boolean
158
+ }
159
+
160
+ export const listTodos = query({
161
+ tags: ['todos'],
162
+ run: async ({ ctx }) => {
163
+ const rows = await ctx.db.sql`
164
+ select id, title, done from todos order by created_at`
165
+ return rows as unknown as Todo[]
166
+ },
167
+ })
168
+
169
+ export const addTodo = mutation({
170
+ input: z.object({ title: z.string().trim().min(1).max(200) }),
171
+ invalidates: ['todos'],
172
+ run: async ({ input, ctx }) => {
173
+ const id = crypto.randomUUID()
174
+ await ctx.db.sql`
175
+ insert into todos (id, title, done, created_at)
176
+ values (${id}, ${input.title}, ${false}, ${Date.now()})`
177
+ },
178
+ })
179
+ ```
180
+
181
+ Finally register the layer, so the CLI knows where migrations live:
182
+
183
+ ```ts
184
+ // weft.config.ts
185
+ import { defineConfig } from '@weft-js/core/config'
186
+ import { native } from '@weft-js/db/plugin-native/plugin'
187
+
188
+ export default defineConfig({ plugins: [native()] })
189
+ ```
190
+
191
+ ## Choosing a layer
192
+
193
+ | Layer | `ctx.db` is | Schema lives in | Migrations | Comes from |
194
+ | ---------- | ------------------------------------------ | --------------------------------- | ---------------------------------- | ----------------------------- |
195
+ | `drizzle` | a Drizzle client | `src/db/schema.ts` | `drizzle/`, generated by drizzle-kit | `@weft-js/start/db/*` |
196
+ | `native` | `{ sql, query, transaction, driver }` | your SQL | `migrations/NNNN_name.sql`, written by you | `@weft-js/db/plugin-native` |
197
+ | `prisma` | your Prisma client, unchanged | `prisma/schema.prisma` | `prisma/migrations/`, generated by Prisma | `@weft-js/db/plugin-prisma` |
198
+
199
+ All three run on SQLite, Postgres and MySQL, connect on the first statement (so `weft build` and a page with no
200
+ queries need no database), and answer a request with 503 and Q016 when the database is down, then recover by
201
+ themselves.
202
+
203
+ ## The Native layer
204
+
205
+ No ORM, no schema file, no codegen. `createDb` returns a client with four members:
206
+
207
+ | Member | What it does |
208
+ | --------------------------- | -------------------------------------------------------------------------------------------------- |
209
+ | `` sql`…${v}…` `` | Runs one statement. Every `${v}` is a bound parameter. Resolves to rows as plain objects. |
210
+ | `query(text, params)` | One statement you built, with its values bound. For tools that write SQL; in app code use `sql`. |
211
+ | `transaction(fn)` | Runs `fn({ sql })` on one connection, committed when it resolves and rolled back if it throws. |
212
+ | `driver` | The real driver: the postgres.js `Sql`, the mysql2 pool, or SQLite's connection opener. |
213
+
214
+ ```ts
215
+ await ctx.db.transaction(async (tx) => {
216
+ await tx.sql`update accounts
217
+ set balance = balance - ${amount} where id = ${from}`
218
+ await tx.sql`update accounts
219
+ set balance = balance + ${amount} where id = ${to}`
220
+ })
221
+ ```
222
+
223
+ ### Engines
224
+
225
+ | Import | Driver | Options |
226
+ | --------------------------------------- | ----------------------------------------- | --------------------------------------------------------- |
227
+ | `@weft-js/db/plugin-native/sqlite` | `node:sqlite` (Node, Deno) or `bun:sqlite` | `url` (default `file:.data/app.db`, or `:memory:`), `migrate` |
228
+ | `@weft-js/db/plugin-native/postgres` | `postgres` (postgres.js) | `url`, `migrate`, `max` (pool size, default 10) |
229
+ | `@weft-js/db/plugin-native/mysql` | `mysql2` | `url`, `migrate`, `max` (pool size, default 10) |
230
+
231
+ `migrate` defaults to `true`. Set it to `false` for a client that should never apply migrations, such as a script.
232
+
233
+ SQLite opens one connection with WAL journaling, `foreign_keys = on` and a 5 s busy timeout. A transaction holds a gate,
234
+ so statements from other requests wait for it instead of landing inside it. On Deno, booleans are passed as `0` and `1`,
235
+ since `node:sqlite` there refuses them.
236
+
237
+ > [!NOTE]
238
+ > Rows come back as the driver returns them. SQLite and MySQL return booleans as `0`/`1`, so coerce where it matters:
239
+ > `rows.map((t) => ({ ...t, done: Boolean(t.done) }))`. MySQL has no `returning`.
240
+
241
+ ### Your own driver
242
+
243
+ `createNative(driver, { migrate })` from `@weft-js/db/plugin-native` wraps anything that implements `Driver`:
244
+ `dialect`, `raw`, `query(text, params)`, `exec(text)`, `transaction(fn)` and `locked(fn)` (hold off other instances
245
+ while migrations run).
246
+
247
+ ## The Prisma layer
248
+
249
+ `withWeft(client, dialect)` returns the same Prisma client, with what Weft needs: a migration runner and statement
250
+ tracking for Q011. Everything else is Prisma's own API, so Prisma's docs apply.
251
+
252
+ ```ts
253
+ // src/db/index.server.ts
254
+ import { PrismaPg } from '@prisma/adapter-pg'
255
+ import { withWeft } from '@weft-js/db/plugin-prisma'
256
+ import { PrismaClient } from '../generated/prisma/client'
257
+ import { env } from '../env.server'
258
+
259
+ const adapter = new PrismaPg({ connectionString: env.DATABASE_URL })
260
+ export const db = withWeft(
261
+ new PrismaClient({
262
+ adapter,
263
+ log: [{ emit: 'event', level: 'query' }],
264
+ }),
265
+ 'postgresql',
266
+ )
267
+ ```
268
+
269
+ ```ts
270
+ // src/server/todos.ts
271
+ export const listTodos = query({
272
+ tags: ['todos'],
273
+ run: ({ ctx }) =>
274
+ ctx.db.todo.findMany({ orderBy: { createdAt: 'asc' } }),
275
+ })
276
+ ```
17
277
 
18
- ## What's in it
278
+ - `dialect` is `'sqlite'`, `'postgresql'` or `'mysql'`. A scaffold uses `@prisma/adapter-libsql`, `@prisma/adapter-pg`
279
+ or `@prisma/adapter-mariadb` for them.
280
+ - `log: [{ emit: 'event', level: 'query' }]` lets Weft see each statement, which is how Q011 knows which tables a
281
+ function touched.
282
+ - Migrations in `prisma/migrations` apply inside an interactive transaction, under an advisory lock on Postgres and a
283
+ named lock on MySQL, because `prisma migrate deploy` isn't in a production bundle.
284
+ - `withWeft(client, dialect, { migrate: false })` turns that off.
19
285
 
20
- | Import | What it is |
21
- |---|---|
22
- | `@weft-js/db/plugin-native/<engine>` | `createDb({ url })`: a `sql` tag over the driver, with no ORM. Values always go to the driver as parameters. `<engine>` is `sqlite`, `postgres` or `mysql` |
23
- | `@weft-js/db/plugin-native/auth` | `nativeAuth(db)`: a Better Auth database adapter over that client |
24
- | `@weft-js/db/plugin-prisma` | `withWeft(client, dialect)`: the same Prisma client, with what Weft needs to run its migrations and check its tags |
25
- | `@weft-js/db/sql`, `/kysely`, `/prisma`, `/mongo`, `/dynamodb`, `/firestore` | List sources: the rows behind a `query` that takes `listInput` from `@weft-js/start/list`, paged, filtered, sorted and searched |
286
+ ## Drizzle (built into Start)
26
287
 
27
- The layers and the list sources each have a `/plugin` subpath for `weft.config.ts`, so `weft check` and the plugin lock know it's there.
288
+ Drizzle needs nothing from this package: `createDb` comes from `@weft-js/start/db/sqlite`, `/postgres` or `/mysql`,
289
+ with `drizzle.config.ts` and `src/db/schema.ts`. The
290
+ [`@weft-js/start` README](https://github.com/Ravonus/weft/tree/main/packages/start#database) covers it. This package
291
+ still adds list sources and change feeds you can use with a Drizzle app.
28
292
 
29
- ## Docs
293
+ ## Migrations and `weft db`
294
+
295
+ Every layer keeps its migrations in a folder the server reads, in order, and records applied ones in a
296
+ `__weft_migrations` table. In dev the folder is read on each request; a build embeds it. There is no `weft db
297
+ migrate`: a new migration applies before the next statement any request runs.
298
+
299
+ | Command | `drizzle` | `native` | `prisma` |
300
+ | --------------------------- | ------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------ |
301
+ | `weft db generate [name]` | `drizzle-kit generate` from `src/db/schema.ts` | a new, empty `migrations/NNNN_<name>.sql` to write | `prisma generate`, then a migration in `prisma/migrations` |
302
+ | Where migrations live | `drizzle/` | `migrations/` | `prisma/migrations/<stamp>_<name>/migration.sql` |
303
+ | `weft db live` | adds a `weft_live` migration with change triggers | prints the trigger SQL for you to add | prints the trigger SQL for you to add |
304
+ | `weft db listen` | runs the change feed as its own process | same | same |
305
+
306
+ ```bash
307
+ npx weft db generate add_due_dates
308
+ ```
309
+
310
+ - **Native.** The new file holds one comment line for you to replace with SQL. Files run once each, in file-name order,
311
+ and each is recorded in `__weft_migrations` by name.
312
+ - **Prisma.** The first `generate` diffs from empty and writes `00000000000000_init/migration.sql`, with no database
313
+ needed. Later ones run `prisma migrate dev --create-only`, which compares against a database.
314
+ - **Drizzle.** `generate` also records a schema hash; Q015 warns when `src/db/schema.ts` has changed since.
315
+
316
+ > [!WARNING]
317
+ > A Native file that holds only comments runs no SQL but is still recorded as applied. If the dev server handles a
318
+ > request that touches the database before you write the SQL, your later edit to that file won't run. Write the SQL
319
+ > first, or start a new file with `weft db generate`.
320
+
321
+ A failed migration answers 503 with Q016 and the driver's error in the server log. Fix the file and the next statement
322
+ tries again. On MySQL, DDL commits as it goes, so a failed migration can't roll back: check what applied before you
323
+ retry.
324
+
325
+ ## Auth on these layers
326
+
327
+ Better Auth needs a store. Start's `createAuth` takes it as `options.database`:
328
+
329
+ ```ts
330
+ // src/auth.server.ts, Native layer
331
+ import { createAuth } from '@weft-js/start/auth/better-auth'
332
+ import { nativeAuth } from '@weft-js/db/plugin-native/auth'
333
+ import { db } from '#/db/index.server'
334
+ import { env } from './env.server'
335
+
336
+ export default createAuth({
337
+ db,
338
+ secret: env.BETTER_AUTH_SECRET,
339
+ options: { database: nativeAuth(db) },
340
+ })
341
+ ```
342
+
343
+ `nativeAuth(db)` is a Kysely dialect over the Native client's own connection, so it needs `kysely` installed but no
344
+ second driver. It runs without transactions, which Better Auth treats as optional. On the Prisma layer, use Better
345
+ Auth's own adapter: `prismaAdapter(db, { provider: 'postgresql' })` from `better-auth/adapters/prisma`. `weft add auth`
346
+ writes the right one, with the tables in your migrations or Prisma schema.
347
+
348
+ ## List sources
349
+
350
+ A list source is one table or collection, read and written the same way whatever holds it. Pair it with `listInput` from
351
+ `@weft-js/start/list` and a query serves paged, sorted, filtered and searched rows that warp's `Table` reads directly.
352
+
353
+ ```ts
354
+ // src/server/tasks.ts
355
+ import { query } from '@weft-js/start'
356
+ import { listInput } from '@weft-js/start/list'
357
+ import { sqlSource } from '@weft-js/db/sql'
358
+ import { db } from '#/db/index.server'
359
+
360
+ const Tasks = sqlSource(db, {
361
+ table: 'tasks',
362
+ columns: {
363
+ id: 'id',
364
+ title: 'title',
365
+ status: 'status',
366
+ rank: 'rank_no',
367
+ }, // public name -> stored name
368
+ search: ['title'],
369
+ })
370
+
371
+ export const tasks = query({
372
+ tags: ['tasks'],
373
+ input: listInput({
374
+ sort: ['rank', 'title'],
375
+ filter: ['status'],
376
+ search: ['title'],
377
+ pageSize: { default: 25, max: 100 },
378
+ }),
379
+ run: ({ input }) => Tasks.list(input),
380
+ })
381
+ ```
382
+
383
+ Every name a client sends is checked twice: by `listInput`'s allowlist, then against the source's `columns`. Only
384
+ `columns` are selected, returned or written, so a `secret` column in the same table never leaves the server. A filter
385
+ value is a scalar or a list of them; an object is refused, which is what stops `{ $ne: null }` reaching a document store.
386
+
387
+ ### The spec
388
+
389
+ | Field | Type | Default | What it does |
390
+ | --------- | -------------------------- | -------------------------- | ------------------------------------------------------ |
391
+ | `columns` | `Record<string, string>` | required | Public name to stored name. The only names that exist. |
392
+ | `id` | `string` | `'id'` | The public name of the row's id |
393
+ | `search` | `string[]` | | Public names `search` scans |
394
+ | `write` | `string[]` | every column but the id | Public names `create` and `update` may set |
395
+
396
+ `listInput(spec)` takes `sort` and `filter` (both required, even empty), `search`, `defaultSort: { field, dir }` and
397
+ `pageSize: { default, max }` (25 and 100 unless set). A client sends `{ mode, page, pageSize, cursor, sort, search,
398
+ filter }`, and gets back `{ items, total?, next?, prev? }`.
399
+
400
+ Every source has the same methods: `list(params)`, `get(id)`, `create(data)`, `update(id, patch)` (null when there's
401
+ no such row) and `remove(id)`. `limits` says what a source can't do, so a page can leave that control out.
402
+
403
+ ### Sources
404
+
405
+ | Import | Function | Target | Notes |
406
+ | ----------------------- | --------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------- |
407
+ | `@weft-js/db/sql` | `sqlSource(target, { table, ...spec })` | anything with `query(text, params)`: a Native `ctx.db`, or a driver wrapper | Identifiers must be plain names. `create` needs the id in `data`. |
408
+ | `@weft-js/db/kysely` | `kyselySource(db, { table, returning?, ...spec })` | any `Kysely<DB>` | Set `returning: false` on MySQL. Needs `kysely`. |
409
+ | `@weft-js/db/prisma` | `prismaSource(ctx.db.task, { insensitive?, ...spec })` | a Prisma model delegate | `insensitive` (default `true`) adds `mode: 'insensitive'` to search, for Postgres and Mongo |
410
+ | `@weft-js/db/mongo` | `mongoSource(collection, spec)` | a MongoDB driver `Collection` | Filters are `$eq`/`$in` on checked scalars; search is an escaped `$regex` |
411
+ | `@weft-js/db/dynamodb` | `dynamoSource({ client, table }, { partition?, sortKey?, index?, ...spec })` | a `DynamoDBDocumentClient` | Cursor mode only. A `Scan` by default; a sorted `Query` when `partition` and `sortKey` name an index |
412
+ | `@weft-js/db/firestore` | `firestoreSource(collection, spec)` | an Admin SDK `CollectionReference` | The document id is the row id. Search is a prefix on the first search column |
413
+ | `@weft-js/start/list/drizzle` | `drizzleSource({ db, table }, spec)` | a Drizzle client and table | Lives in Start; `columns` maps to the table's property names |
414
+
415
+ ```ts
416
+ // Prisma: the model delegate is the target
417
+ import { prismaSource } from '@weft-js/db/prisma'
418
+
419
+ export const tasks = query({
420
+ input: listInput({
421
+ sort: ['title'],
422
+ filter: ['status'],
423
+ search: ['title'],
424
+ }),
425
+ run: ({ input, ctx }) =>
426
+ prismaSource(ctx.db.task, {
427
+ columns: { id: 'id', title: 'title', status: 'status' },
428
+ search: ['title'],
429
+ }).list(input),
430
+ })
431
+ ```
432
+
433
+ ```ts
434
+ // DynamoDB: one partition, sorted by an index's sort key
435
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
436
+ import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'
437
+ import { dynamoSource } from '@weft-js/db/dynamodb'
438
+
439
+ const client = DynamoDBDocumentClient.from(new DynamoDBClient({}))
440
+ const Orders = dynamoSource(
441
+ { client, table: 'orders' },
442
+ {
443
+ columns: {
444
+ id: 'pk',
445
+ customer: 'customer',
446
+ placed: 'placedAt',
447
+ total: 'total',
448
+ },
449
+ partition: { column: 'customer', value: 'c_42' },
450
+ sortKey: 'placed',
451
+ index: 'by_customer',
452
+ },
453
+ )
454
+ ```
455
+
456
+ ## Changes from MongoDB and Postgres
457
+
458
+ Start's `dbChanges` covers Postgres, SQLite and MySQL through triggers. For the rest:
459
+
460
+ **MongoDB as a change feed.** `mongoChanges(db, collections)` is a `Source` for `realtime({ changes })`. It opens one
461
+ change stream, carries ids only, and pokes `todos` and `todos:<id>` for a write to `todos`. It needs a replica set: a
462
+ standalone `mongod` reports `off` with the fix (`mongod --replSet rs0`, then `rs.initiate()`). After a lost stream every
463
+ collection is poked, since a poke is cheaper than a stale page.
464
+
465
+ ```ts
466
+ // src/realtime.server.ts
467
+ import { realtime } from '@weft-js/start'
468
+ import { mongoChanges } from '@weft-js/db/mongo'
469
+ import { mongo } from '#/db/index.server'
470
+
471
+ export default realtime({
472
+ changes: mongoChanges(mongo, ['todos', 'projects']),
473
+ })
474
+ ```
475
+
476
+ **One collection, poked directly.** `watchMongo(collection, { tag?, poke?, retry? })` watches one collection and pokes
477
+ its tag through the realtime hub, retrying every second (by default) after a failure. It returns a function that stops
478
+ it.
479
+
480
+ **A Postgres trigger by hand.** `postgresTrigger({ table, id?, tag? })` from `@weft-js/db/sql` returns SQL for a row
481
+ trigger that publishes `tag` and `tag:<id>` on the Postgres bus. Run the text once in a migration. Tags only, never
482
+ rows.
483
+
484
+ ## Registering in `weft.config.ts`
485
+
486
+ Each layer and each list source has a `/plugin` subpath. Adding it to `plugins` tells the CLI where migrations live (for
487
+ the layers), records it in `.weft/plugins.lock.json`, and lets `weft check` name it.
488
+
489
+ ```ts
490
+ // weft.config.ts
491
+ import { defineConfig } from '@weft-js/core/config'
492
+ import { native } from '@weft-js/db/plugin-native/plugin'
493
+ import { sqlSource } from '@weft-js/db/sql/plugin'
494
+
495
+ export default defineConfig({ plugins: [native(), sqlSource()] })
496
+ ```
497
+
498
+ | Plugin subpath | Export | Permissions it asks for |
499
+ | --------------------------------------- | ----------------- | ------------------------------------------------------------ |
500
+ | `@weft-js/db/plugin-native/plugin` | `native` | read and write `migrations/**` |
501
+ | `@weft-js/db/plugin-prisma/plugin` | `prisma` | read `prisma/**`, write `prisma/migrations/**`, spawn `node` |
502
+ | `@weft-js/db/sql/plugin` | `sqlSource` | `net: driver:query` |
503
+ | `@weft-js/db/kysely/plugin` | `kyselySource` | `net: driver:kysely` |
504
+ | `@weft-js/db/prisma/plugin` | `prismaSource` | `net: driver:prisma` |
505
+ | `@weft-js/db/mongo/plugin` | `mongoSource` | `net: driver:mongodb` |
506
+ | `@weft-js/db/dynamodb/plugin` | `dynamoSource` | `net: driver:aws-sdk` |
507
+ | `@weft-js/db/firestore/plugin` | `firestoreSource` | `net: driver:firebase-admin` |
508
+
509
+ `weft add db` and `npm create @weft-js` add the layer plugin and approve it. A plugin you add by hand needs
510
+ `weft plugins approve <name>` before the next `weft check` passes (P109).
511
+
512
+ ## API reference
513
+
514
+ | Import | Exports | Driver it loads |
515
+ | --------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------ |
516
+ | `@weft-js/db/plugin-native` | `createNative(driver, { migrate })`; types `NativeDb`, `Driver`, `Sql`, `Row` | none |
517
+ | `@weft-js/db/plugin-native/sqlite` | `createDb({ url?, migrate? })` | `node:sqlite` / `bun:sqlite` |
518
+ | `@weft-js/db/plugin-native/postgres` | `createDb({ url, migrate?, max? })` | `postgres` |
519
+ | `@weft-js/db/plugin-native/mysql` | `createDb({ url, migrate?, max? })` | `mysql2` |
520
+ | `@weft-js/db/plugin-native/auth` | `nativeAuth(db)`: Better Auth's `database` option | `kysely` |
521
+ | `@weft-js/db/plugin-prisma` | `withWeft(client, dialect, { migrate? })` | none (your client) |
522
+ | `@weft-js/db/sql` | `sqlSource`, `postgresTrigger`, `adapter`; types `SqlTarget`, `SqlSpec` | none |
523
+ | `@weft-js/db/kysely` | `kyselySource`, `adapter`; type `KyselySpec` | `kysely` |
524
+ | `@weft-js/db/prisma` | `prismaSource`, `adapter`; types `PrismaDelegate`, `PrismaSpec` | none |
525
+ | `@weft-js/db/mongo` | `mongoSource`, `mongoChanges`, `watchMongo`, `changeTags`, `adapter` | none (your client) |
526
+ | `@weft-js/db/dynamodb` | `dynamoSource`, `adapter`; types `DynamoTarget`, `DynamoSpec` | `@aws-sdk/lib-dynamodb` |
527
+ | `@weft-js/db/firestore` | `firestoreSource`, `adapter`; types `FirestoreCollection`, … | none (your client) |
528
+ | `…/plugin` for each of the above | the plugin for `weft.config.ts` | none |
529
+
530
+ Each `adapter` is the source as a `db.adapter@1` slot provider (`{ name, source(target, spec) }`).
531
+
532
+ ## Troubleshooting
533
+
534
+ | Code / symptom | Cause | Fix |
535
+ | --------------------------------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------- |
536
+ | `… needs the \`kysely\` driver, which isn't installed` | The subpath's optional peer is missing | Install the package the message names |
537
+ | Q002 | A sort, filter, column or table outside the allowlist, or not a plain identifier | Add it to the source's `columns` and to `listInput` |
538
+ | Q011 | A function touched a table its `tags`/`invalidates` don't name | Add the table's name; on Prisma, keep `log: [{ emit: 'event', level: 'query' }]` |
539
+ | Q015 | `src/db/schema.ts` changed since the last `weft db generate` | Run `weft db generate` |
540
+ | Q016 (503) | The database is down, or a migration failed | Start it, check `DATABASE_URL`, or fix the migration; no restart needed |
541
+ | `this app has no database: \`weft add db\` adds one` | `weft db generate` found no layer | `weft add db`, or register the layer plugin in `weft.config.ts` |
542
+ | `prisma is not installed in this project` | The Prisma layer's `generate` couldn't find the `prisma` CLI | Install `prisma` as a dev dependency |
543
+ | `another instance held the migration lock for 60s` | Prisma on MySQL: another instance is migrating | Let it finish; the next request tries again |
544
+ | A live query shows `off` on MongoDB | A standalone `mongod` has no change streams | Run it as a replica set (one member is enough) |
545
+ | P109 after adding a plugin | The plugin lock doesn't approve it yet | `weft plugins approve <name>` |
546
+
547
+ ## Related packages
548
+
549
+ | Package | What it is |
550
+ | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
551
+ | [`@weft-js/start`](https://www.npmjs.com/package/@weft-js/start) <sup>[source](https://github.com/Ravonus/weft/tree/main/packages/start)</sup> | File routes, server functions, SSR, Drizzle, auth and realtime; this package's host |
552
+ | [`@weft-js/core`](https://www.npmjs.com/package/@weft-js/core) <sup>[source](https://github.com/Ravonus/weft/tree/main/packages/core)</sup> | The reactive runtime and the plugin API these plugins use |
553
+ | [`@weft-js/cli`](https://www.npmjs.com/package/@weft-js/cli) <sup>[source](https://github.com/Ravonus/weft/tree/main/packages/cli)</sup> | `weft add db`, `weft db generate`, `weft db live` and `weft db listen` |
554
+ | [`@weft-js/create`](https://www.npmjs.com/package/@weft-js/create) <sup>[source](https://github.com/Ravonus/weft/tree/main/packages/create)</sup> | `npm create @weft-js --db … --layer …` |
555
+ | [`@weft-js/warp`](https://www.npmjs.com/package/@weft-js/warp) <sup>[source](https://github.com/Ravonus/weft/tree/main/packages/warp)</sup> | UI components; `Table` reads what a list source returns |
556
+
557
+ ## Contributing
558
+
559
+ See [CONTRIBUTING.md](https://github.com/Ravonus/weft/blob/main/CONTRIBUTING.md). From the repository root:
560
+
561
+ ```bash
562
+ pnpm install
563
+ pnpm --filter @weft-js/db test
564
+ ```
30
565
 
31
- `weft docs start` covers the database: `ctx.db`, migrations with `weft db generate`, and change feeds.
566
+ The suites run on SQLite in memory. Set `WEFT_TEST_POSTGRES`, `WEFT_TEST_MYSQL` or `WEFT_TEST_MONGO` (a replica set)
567
+ to a throwaway database to run the same tests against a real server. The tests drop and rewrite data there, so never
568
+ point them at one you care about.
32
569
 
33
- ## Links
570
+ ## License
34
571
 
35
- - [Source](https://github.com/Ravonus/weft/tree/main/packages/db)
36
- - [Weft on GitHub](https://github.com/Ravonus/weft): the framework, the docs and every package
37
- - [Changelog](https://github.com/Ravonus/weft/blob/main/CHANGELOG.md)
38
- - [Issues](https://github.com/Ravonus/weft/issues)
572
+ [MIT](https://github.com/Ravonus/weft/blob/main/LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weft-js/db",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "weft's data layers and list sources in one package: each driver is an optional peer, and a subpath loads only its own",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -39,14 +39,14 @@
39
39
  "@aws-sdk/client-dynamodb": "^3.0.0",
40
40
  "@aws-sdk/lib-dynamodb": "^3.0.0",
41
41
  "@prisma/client": ">=6",
42
- "@weft-js/start": "^0.1.2",
42
+ "@weft-js/start": "^0.1.3",
43
43
  "better-auth": "^1.7.0",
44
44
  "firebase-admin": ">=12",
45
45
  "kysely": "^0.29.0",
46
46
  "mongodb": ">=6",
47
47
  "mysql2": "^3.24.0",
48
48
  "postgres": ">=3",
49
- "@weft-js/core": "^0.1.2"
49
+ "@weft-js/core": "^0.1.3"
50
50
  },
51
51
  "peerDependenciesMeta": {
52
52
  "@aws-sdk/client-dynamodb": {