d1-kyt 0.4.6 → 0.5.0
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 +218 -118
- package/dist/cli.js +178 -224
- package/dist/cli.js.map +1 -1
- package/dist/migrate.d.ts +2 -3
- package/dist/migrate.d.ts.map +1 -1
- package/dist/migrate.js +4 -5
- package/dist/migrate.js.map +1 -1
- package/dist/schema-diff.d.ts +85 -0
- package/dist/schema-diff.d.ts.map +1 -0
- package/dist/schema-diff.js +346 -0
- package/dist/schema-diff.js.map +1 -0
- package/dist/schema.d.ts +128 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +148 -0
- package/dist/schema.js.map +1 -0
- package/package.json +9 -6
package/README.md
CHANGED
|
@@ -4,83 +4,214 @@ Opinionated [Cloudflare D1](https://developers.cloudflare.com/d1/) + [Kysely](ht
|
|
|
4
4
|
|
|
5
5
|
**ky**(sely) + **t**(oolkit) = **kyt**
|
|
6
6
|
|
|
7
|
-
> **Not an ORM.** Thin wrapper with helpers that relies on Kysely's type inference. No magic, no runtime overhead.
|
|
7
|
+
> **Not an ORM.** Thin wrapper with helpers that relies on Kysely's type inference and Valibot schemas. No magic, no runtime overhead.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install d1-kyt kysely valibot
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Workflow
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
schema.ts → schema:diff → .sql migration → wrangler apply → types from schema
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
1. Define your schema with Valibot types in `schema.ts`
|
|
22
|
+
2. Run `d1-kyt schema:diff <name>` — diffs against a snapshot, writes a `.sql` migration
|
|
23
|
+
3. Apply with `wrangler d1 migrations apply <db> --local`
|
|
24
|
+
4. Use `$inferSelect` / `$inferInsert` from your schema for type-safe queries
|
|
25
|
+
|
|
26
|
+
No code generation step required — types come directly from the schema file.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# In your Cloudflare Workers project:
|
|
34
|
+
d1-kyt init
|
|
35
|
+
|
|
36
|
+
# Edit the generated schema file, then:
|
|
37
|
+
d1-kyt schema:diff create_users
|
|
38
|
+
|
|
39
|
+
# Apply to local D1:
|
|
40
|
+
wrangler d1 migrations apply <db-name> --local
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`init` auto-detects the right directory. If your wrangler config has `migrations_dir = "db/migrations"`, it places files in `db/`. Otherwise it uses `d1-kyt/`.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Schema
|
|
10
48
|
|
|
11
49
|
```typescript
|
|
12
|
-
// d1-kyt/
|
|
50
|
+
// db/schema.ts (or d1-kyt/schema.ts)
|
|
51
|
+
import { defineTable, defineIndex, defineTrigger } from 'd1-kyt/schema';
|
|
52
|
+
import * as v from 'valibot';
|
|
53
|
+
|
|
54
|
+
export const users = defineTable('users', {
|
|
55
|
+
email: v.string(), // TEXT NOT NULL
|
|
56
|
+
name: v.optional(v.string()), // TEXT (nullable)
|
|
57
|
+
age: v.optional(v.pipe(v.number(), v.integer())), // INTEGER (nullable)
|
|
58
|
+
prefs: v.optional(v.object({ theme: v.string() })), // TEXT JSON (nullable)
|
|
59
|
+
role: v.optional(v.string(), 'user'), // TEXT DEFAULT 'user'
|
|
60
|
+
});
|
|
13
61
|
|
|
14
|
-
|
|
62
|
+
export const usersEmailIdx = defineIndex(users, ['email'], { unique: true });
|
|
15
63
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}));
|
|
64
|
+
export const auditTrigger = defineTrigger('users_audit_trg', {
|
|
65
|
+
timing: 'AFTER', event: 'INSERT', on: users,
|
|
66
|
+
body: `INSERT INTO audit (action, at) VALUES ('insert', datetime('now'));`,
|
|
67
|
+
});
|
|
68
|
+
```
|
|
22
69
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
70
|
+
### Valibot → SQL type mapping
|
|
71
|
+
|
|
72
|
+
| Valibot schema | SQL type | Nullable |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `v.string()` | TEXT | NOT NULL |
|
|
75
|
+
| `v.number()` | REAL | NOT NULL |
|
|
76
|
+
| `v.pipe(v.number(), v.integer(), ...)` | INTEGER | NOT NULL |
|
|
77
|
+
| `v.boolean()` | INTEGER | NOT NULL |
|
|
78
|
+
| `v.object({...})` or `v.array(...)` | TEXT (JSON) | NOT NULL |
|
|
79
|
+
| `v.optional(X)` | type of X | NULL |
|
|
80
|
+
| `v.nullable(X)` | type of X | NULL |
|
|
81
|
+
| `v.optional(X, defaultVal)` | type of X + DEFAULT | NULL |
|
|
82
|
+
|
|
83
|
+
### Auto columns
|
|
84
|
+
|
|
85
|
+
Every table gets `id`, `createdAt`, `updatedAt` by default, plus an `AFTER UPDATE` trigger for `updatedAt`. Control via options:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// Disable everything
|
|
89
|
+
defineTable('events', { uuid: v.string() }, {
|
|
90
|
+
primaryKey: false, createdAt: false, updatedAt: false,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Custom names (snake_case)
|
|
94
|
+
defineTable('users', { email: v.string() }, {
|
|
95
|
+
primaryKeyColumn: 'user_id',
|
|
96
|
+
createdAtColumn: 'created_at',
|
|
97
|
+
updatedAtColumn: 'updated_at',
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## CLI
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
d1-kyt init [--dir <dir>] # scaffold config + schema template
|
|
107
|
+
d1-kyt schema:diff <name> [--dir <dir>] # diff schema → write .sql migration
|
|
108
|
+
d1-kyt schema:diff <name> --schema <path> # use a custom schema file path
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### `init`
|
|
112
|
+
|
|
113
|
+
Creates (skips if already exists):
|
|
114
|
+
- `<dir>/config.ts` — migrationsDir + namingStrategy
|
|
115
|
+
- `<dir>/schema.ts` — schema template to fill in
|
|
116
|
+
- `<dir>/schema.snapshot.jsonc` — diff baseline (**commit this to git**)
|
|
117
|
+
|
|
118
|
+
Directory resolution:
|
|
119
|
+
1. `--dir <path>` if provided
|
|
120
|
+
2. Parent of wrangler `migrations_dir` (e.g. `db/` when `migrations_dir = "db/migrations"`)
|
|
121
|
+
3. `d1-kyt/` as fallback
|
|
122
|
+
|
|
123
|
+
### `schema:diff <name>`
|
|
124
|
+
|
|
125
|
+
Reads your `schema.ts`, diffs against `schema.snapshot.jsonc`, writes a numbered `.sql` file to your `migrationsDir`, and updates the snapshot. **Commit the `.sql` and the snapshot together** — they are the source of truth for migration history.
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
d1-kyt schema:diff create_users # generates 0001_create_users.sql
|
|
129
|
+
d1-kyt schema:diff add_email_index # generates 0002_add_email_index.sql
|
|
130
|
+
d1-kyt schema:diff --dir db add_posts # use db/config.ts, db/schema.ts
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Config
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// db/config.ts (or d1-kyt/config.ts)
|
|
137
|
+
import { defineConfig } from 'd1-kyt/config';
|
|
138
|
+
|
|
139
|
+
export default defineConfig({
|
|
140
|
+
migrationsDir: 'db/migrations',
|
|
141
|
+
namingStrategy: 'sequential', // or 'timestamp'
|
|
142
|
+
});
|
|
28
143
|
```
|
|
29
144
|
|
|
30
|
-
|
|
31
|
-
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Type inference
|
|
148
|
+
|
|
149
|
+
Types come directly from your schema — no code generation step required:
|
|
32
150
|
|
|
33
151
|
```typescript
|
|
34
|
-
import {
|
|
152
|
+
import { users } from './db/schema';
|
|
35
153
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
154
|
+
// Full row returned by SELECT
|
|
155
|
+
type UserRow = typeof users.$inferSelect;
|
|
156
|
+
// { id: number; email: string; name: string | undefined; age: number | undefined;
|
|
157
|
+
// prefs: { theme: string } | undefined; role: string | undefined;
|
|
158
|
+
// createdAt: string; updatedAt: string }
|
|
39
159
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
//
|
|
160
|
+
// Input for INSERT
|
|
161
|
+
type NewUser = typeof users.$inferInsert;
|
|
162
|
+
// { email: string; name?: string | undefined; age?: number | undefined; ... id?: number }
|
|
43
163
|
```
|
|
44
164
|
|
|
45
|
-
|
|
165
|
+
### Building a DB type for Kysely
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
// db/index.ts
|
|
169
|
+
import { users } from './schema';
|
|
170
|
+
|
|
171
|
+
export type DB = {
|
|
172
|
+
users: typeof users.$inferSelect;
|
|
173
|
+
// ... add other tables
|
|
174
|
+
};
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
46
178
|
|
|
47
179
|
## Query Builder
|
|
48
180
|
|
|
49
181
|
```typescript
|
|
50
182
|
// src/queries.ts
|
|
51
|
-
|
|
52
183
|
import { createQueryBuilder } from 'd1-kyt';
|
|
53
|
-
import type { DB } from './db
|
|
184
|
+
import type { DB } from './db';
|
|
54
185
|
|
|
55
186
|
const db = createQueryBuilder<DB>();
|
|
56
187
|
|
|
57
|
-
export const
|
|
188
|
+
export const listUsers = () =>
|
|
189
|
+
db.selectFrom('users').selectAll().compile();
|
|
58
190
|
|
|
59
|
-
export const
|
|
60
|
-
db.selectFrom('
|
|
191
|
+
export const getUserByEmail = (email: string) =>
|
|
192
|
+
db.selectFrom('users').selectAll().where('email', '=', email).compile();
|
|
61
193
|
|
|
62
|
-
export const insertUser = (email: string, name
|
|
63
|
-
db.insertInto('
|
|
194
|
+
export const insertUser = (email: string, name?: string) =>
|
|
195
|
+
db.insertInto('users').values({ email, name }).returning(['id']).compile();
|
|
64
196
|
```
|
|
65
197
|
|
|
66
198
|
## Execute Queries
|
|
67
199
|
|
|
68
200
|
```typescript
|
|
69
201
|
// src/app.ts
|
|
70
|
-
|
|
71
|
-
import Hono from 'hono';
|
|
202
|
+
import { Hono } from 'hono';
|
|
72
203
|
import { queryAll, queryFirst, queryRun } from 'd1-kyt';
|
|
73
204
|
import * as q from './queries';
|
|
74
205
|
|
|
75
206
|
const app = new Hono();
|
|
76
207
|
|
|
77
208
|
app.get('/users', async (c) => {
|
|
78
|
-
const users = await queryAll(c.env.DB, q.
|
|
209
|
+
const users = await queryAll(c.env.DB, q.listUsers());
|
|
79
210
|
return c.json(users);
|
|
80
211
|
});
|
|
81
212
|
|
|
82
|
-
app.get('/users/:
|
|
83
|
-
const user = await queryFirst(c.env.DB, q.
|
|
213
|
+
app.get('/users/:email', async (c) => {
|
|
214
|
+
const user = await queryFirst(c.env.DB, q.getUserByEmail(c.req.param('email')));
|
|
84
215
|
return user ? c.json(user) : c.notFound();
|
|
85
216
|
});
|
|
86
217
|
|
|
@@ -91,110 +222,79 @@ app.post('/users', async (c) => {
|
|
|
91
222
|
});
|
|
92
223
|
```
|
|
93
224
|
|
|
94
|
-
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Partial indexes
|
|
95
228
|
|
|
96
229
|
```typescript
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}), { primaryKey: false, createdAt: false, updatedAt: false });
|
|
102
|
-
|
|
103
|
-
// Custom column names (snake_case)
|
|
104
|
-
const User = defineTable('user', (col) => ({
|
|
105
|
-
email: col.text().notNull(),
|
|
106
|
-
}), {
|
|
107
|
-
primaryKeyColumn: 'user_id',
|
|
108
|
-
createdAtColumn: 'created_at',
|
|
109
|
-
updatedAtColumn: 'updated_at',
|
|
110
|
-
});
|
|
230
|
+
defineIndex(users, ['email'], {
|
|
231
|
+
unique: true,
|
|
232
|
+
where: '"active" = 1', // raw SQL string
|
|
233
|
+
})
|
|
111
234
|
```
|
|
112
235
|
|
|
113
|
-
|
|
236
|
+
---
|
|
114
237
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
```typescript
|
|
118
|
-
import type { DB } from '../../db/generated';
|
|
119
|
-
import { createUseTable, addColumn, createIndex } from 'd1-kyt/migrate';
|
|
238
|
+
## Conventions
|
|
120
239
|
|
|
121
|
-
|
|
122
|
-
|
|
240
|
+
- Auto `id INTEGER PRIMARY KEY AUTOINCREMENT`, `createdAt TEXT`, `updatedAt TEXT` on every table (all configurable/disableable)
|
|
241
|
+
- Auto `AFTER UPDATE` trigger to keep `updatedAt` current
|
|
242
|
+
- Index naming: `{table}_{cols}_idx` / `{table}_{cols}_uq`
|
|
243
|
+
- Trigger naming: `{table}_{col}_trg`
|
|
244
|
+
- `schema.snapshot.jsonc` is the diff source of truth — always commit it alongside migration SQL files
|
|
123
245
|
|
|
124
|
-
|
|
125
|
-
addColumn(User, 'age', (col) => col.integer()),
|
|
126
|
-
createIndex(User, ['age']),
|
|
127
|
-
// partial index — where accepts a typed column accessor or a raw SQL string
|
|
128
|
-
createIndex(User, ['age'], { where: col => `${col('age')} IS NOT NULL` }),
|
|
129
|
-
];
|
|
130
|
-
```
|
|
246
|
+
---
|
|
131
247
|
|
|
132
|
-
##
|
|
248
|
+
## API reference
|
|
133
249
|
|
|
134
|
-
|
|
135
|
-
npm install d1-kyt kysely
|
|
136
|
-
npm install -D kysely-codegen
|
|
137
|
-
```
|
|
250
|
+
### `d1-kyt/schema`
|
|
138
251
|
|
|
139
|
-
|
|
252
|
+
| Export | Description |
|
|
253
|
+
|---|---|
|
|
254
|
+
| `defineTable(name, columns, opts?)` | Define a table; returns `SchemaTable` with `$inferSelect` / `$inferInsert` |
|
|
255
|
+
| `defineIndex(table, columns, opts?)` | Define an index (columns are type-checked against the table) |
|
|
256
|
+
| `defineTrigger(name, opts)` | Define a custom trigger attached to a table |
|
|
257
|
+
| `sqlTypeFromSchema(schema)` | Inspect a Valibot schema → `{ type, notNull, default?, isJson }` |
|
|
258
|
+
| `TableOptions` | Options type for auto columns (re-exported) |
|
|
140
259
|
|
|
141
|
-
|
|
142
|
-
d1-kyt init # creates d1-kyt/ folder with config
|
|
143
|
-
d1-kyt migrate:create <name> # creates d1-kyt/migrations/0001_<name>.ts
|
|
144
|
-
d1-kyt migrate:build # compiles *.ts → db/migrations/*.sql
|
|
145
|
-
d1-kyt typegen # runs kysely-codegen
|
|
146
|
-
```
|
|
260
|
+
### `d1-kyt` (main)
|
|
147
261
|
|
|
148
|
-
|
|
262
|
+
| Export | Description |
|
|
263
|
+
|---|---|
|
|
264
|
+
| `createQueryBuilder<DB>()` | Kysely instance (compile-only, no execution) |
|
|
265
|
+
| `queryAll(db, query)` | Execute query, return all rows |
|
|
266
|
+
| `queryFirst(db, query)` | Execute query, return first row or null |
|
|
267
|
+
| `queryRun(db, query)` | Execute mutation, return run metadata |
|
|
268
|
+
| `queryBatch(db, queries)` | Execute multiple queries as a D1 batch |
|
|
149
269
|
|
|
150
|
-
###
|
|
270
|
+
### `d1-kyt/config`
|
|
151
271
|
|
|
152
|
-
|
|
272
|
+
| Export | Description |
|
|
273
|
+
|---|---|
|
|
274
|
+
| `defineConfig(config)` | Define `config.ts` (typed helper) |
|
|
153
275
|
|
|
154
|
-
|
|
155
|
-
# Apply migrations to local D1 database first
|
|
156
|
-
wrangler d1 migrations apply <db-name> --local
|
|
276
|
+
---
|
|
157
277
|
|
|
158
|
-
|
|
159
|
-
DATABASE_URL=.wrangler/state/v3/d1/<db-id>/db.sqlite d1-kyt typegen
|
|
160
|
-
```
|
|
278
|
+
## Legacy migrate API
|
|
161
279
|
|
|
162
|
-
|
|
280
|
+
The imperative migration DSL (`d1-kyt/migrate`) is still available but superseded by the schema-first approach above. It will be removed in a future major version.
|
|
163
281
|
|
|
164
282
|
```typescript
|
|
165
|
-
// d1-kyt/
|
|
283
|
+
// d1-kyt/migrations/0001_create_users.ts
|
|
284
|
+
import { defineTable, createIndex } from 'd1-kyt/migrate';
|
|
166
285
|
|
|
167
|
-
|
|
286
|
+
const users = defineTable('users', (col) => ({
|
|
287
|
+
email: col.text().notNull(),
|
|
288
|
+
name: col.text(),
|
|
289
|
+
}));
|
|
168
290
|
|
|
169
|
-
export
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
});
|
|
291
|
+
export const migration = () => [
|
|
292
|
+
...users.sql,
|
|
293
|
+
createIndex(users, ['email'], { unique: true }),
|
|
294
|
+
];
|
|
174
295
|
```
|
|
175
296
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
- Auto `id`, `createdAt`, `updatedAt` on every table (configurable)
|
|
179
|
-
- Auto trigger for `updatedAt`
|
|
180
|
-
- Index naming: `{table}_{cols}_idx`, `{table}_{cols}_uq`
|
|
181
|
-
- Trigger naming: `{table}_{col}_trg`
|
|
182
|
-
|
|
183
|
-
## API
|
|
184
|
-
|
|
185
|
-
| Function | Description |
|
|
186
|
-
| -------------------------------------- | --------------------------------- |
|
|
187
|
-
| `defineTable(name, fn, opts?)` | Define new table |
|
|
188
|
-
| `createUseTable<DB>()` | Factory for typed table refs |
|
|
189
|
-
| `useTable<T>(name)` | Reference table (manual typing) |
|
|
190
|
-
| `createIndex(table, cols, opts?)` | Create index |
|
|
191
|
-
| `addColumn(table, col, fn)` | Add column |
|
|
192
|
-
| `dropTable(table, updatedAtCol?)` | Drop table + trigger |
|
|
193
|
-
| `dropIndex(name)` | Drop index |
|
|
194
|
-
| `queryAll(db, query)` | Execute, return all rows |
|
|
195
|
-
| `queryFirst(db, query)` | Execute, return first row or null |
|
|
196
|
-
| `queryRun(db, query)` | Execute mutation |
|
|
197
|
-
| `queryBatch(db, queries)` | Execute batch |
|
|
297
|
+
---
|
|
198
298
|
|
|
199
299
|
## License
|
|
200
300
|
|