@weft-js/db 0.1.1 → 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.
- package/README.md +558 -17
- package/package.json +13 -4
- package/plugins/dynamodb.mjs +10 -2
- package/plugins/firestore.mjs +10 -2
- package/plugins/kysely.mjs +10 -2
- package/plugins/mongo.mjs +10 -2
- package/plugins/native.mjs +16 -4
- package/plugins/prisma-client.mjs +29 -6
- package/plugins/prisma.mjs +10 -2
- package/plugins/sql.mjs +10 -2
- package/src/driver.ts +11 -4
- package/src/dynamodb.ts +165 -49
- package/src/firestore.ts +89 -31
- package/src/kysely.ts +108 -32
- package/src/mongo.ts +125 -30
- package/src/native/auth.ts +37 -9
- package/src/native/index.ts +36 -23
- package/src/native/mysql.ts +22 -5
- package/src/native/postgres.ts +28 -10
- package/src/native/sqlite.ts +20 -6
- package/src/prisma-client.ts +53 -24
- package/src/prisma.ts +102 -28
- package/src/sql.ts +141 -43
package/README.md
CHANGED
|
@@ -1,31 +1,572 @@
|
|
|
1
|
-
|
|
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
|
-
>
|
|
7
|
+
<h1 align="center">@weft-js/db</h1>
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
```
|
|
277
|
+
|
|
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.
|
|
17
285
|
|
|
18
|
-
##
|
|
286
|
+
## Drizzle (built into Start)
|
|
19
287
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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.
|
|
292
|
+
|
|
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
|
+
```
|
|
26
565
|
|
|
27
|
-
The
|
|
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.
|
|
28
569
|
|
|
29
|
-
##
|
|
570
|
+
## License
|
|
30
571
|
|
|
31
|
-
|
|
572
|
+
[MIT](https://github.com/Ravonus/weft/blob/main/LICENSE)
|
package/package.json
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weft-js/db",
|
|
3
|
-
"version": "0.1.
|
|
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
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Ravonus/weft.git",
|
|
9
|
+
"directory": "packages/db"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/Ravonus/weft/tree/main/packages/db#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Ravonus/weft/issues"
|
|
14
|
+
},
|
|
6
15
|
"type": "module",
|
|
7
16
|
"exports": {
|
|
8
17
|
"./sql": "./src/sql.ts",
|
|
@@ -30,14 +39,14 @@
|
|
|
30
39
|
"@aws-sdk/client-dynamodb": "^3.0.0",
|
|
31
40
|
"@aws-sdk/lib-dynamodb": "^3.0.0",
|
|
32
41
|
"@prisma/client": ">=6",
|
|
33
|
-
"@weft-js/start": "^0.1.
|
|
42
|
+
"@weft-js/start": "^0.1.3",
|
|
34
43
|
"better-auth": "^1.7.0",
|
|
35
44
|
"firebase-admin": ">=12",
|
|
36
45
|
"kysely": "^0.29.0",
|
|
37
46
|
"mongodb": ">=6",
|
|
38
47
|
"mysql2": "^3.24.0",
|
|
39
48
|
"postgres": ">=3",
|
|
40
|
-
"@weft-js/core": "^0.1.
|
|
49
|
+
"@weft-js/core": "^0.1.3"
|
|
41
50
|
},
|
|
42
51
|
"peerDependenciesMeta": {
|
|
43
52
|
"@aws-sdk/client-dynamodb": {
|
|
@@ -83,4 +92,4 @@
|
|
|
83
92
|
"README.md",
|
|
84
93
|
"LICENSE"
|
|
85
94
|
]
|
|
86
|
-
}
|
|
95
|
+
}
|
package/plugins/dynamodb.mjs
CHANGED
|
@@ -9,8 +9,16 @@ export const dynamoSource = definePlugin({
|
|
|
9
9
|
weft: '*',
|
|
10
10
|
permissions: { net: ['driver:aws-sdk'] },
|
|
11
11
|
contribute: () => ({
|
|
12
|
-
provides: [
|
|
13
|
-
|
|
12
|
+
provides: [
|
|
13
|
+
provide(slot, {
|
|
14
|
+
key: 'dynamodb',
|
|
15
|
+
sides: { server: { module: '@weft-js/db/dynamodb', export: 'adapter' } },
|
|
16
|
+
}),
|
|
17
|
+
],
|
|
18
|
+
agents: {
|
|
19
|
+
title: 'Lists (dynamodb)',
|
|
20
|
+
text: 'Build a source once, in a server module, with `columns`: only those are selected, returned or written. Then `query({ input: listInput({ sort, filter, search }), run: ({ input }) => source.list(input) })`. warp `Table` reads what comes back. Cursor mode only, and a sort needs a partition and an index.',
|
|
21
|
+
},
|
|
14
22
|
}),
|
|
15
23
|
})
|
|
16
24
|
|