d1-kyt 0.4.6 → 0.5.2
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 +208 -131
- package/dist/cli.js +182 -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,212 @@ 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. Defaults to `db/`. If your wrangler config has a `migrations_dir` with a parent folder (e.g. `src/migrations/`), it uses that parent instead.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Schema
|
|
10
48
|
|
|
11
49
|
```typescript
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
email:
|
|
19
|
-
name:
|
|
20
|
-
|
|
21
|
-
}))
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
50
|
+
// db/schema.ts
|
|
51
|
+
import { defineTable, defineIndex, defineTrigger } from 'd1-kyt/schema';
|
|
52
|
+
import { createQueryBuilder } from 'd1-kyt';
|
|
53
|
+
import * as v from 'valibot';
|
|
54
|
+
|
|
55
|
+
export const users = defineTable('users', {
|
|
56
|
+
email: v.string(), // TEXT NOT NULL
|
|
57
|
+
name: v.optional(v.string()), // TEXT (nullable)
|
|
58
|
+
age: v.optional(v.pipe(v.number(), v.integer())), // INTEGER (nullable)
|
|
59
|
+
prefs: v.optional(v.object({ theme: v.string() })), // TEXT JSON (nullable)
|
|
60
|
+
role: v.optional(v.string(), 'user'), // TEXT DEFAULT 'user'
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const usersEmailIdx = defineIndex(users, ['email'], { unique: true });
|
|
64
|
+
|
|
65
|
+
export const auditTrigger = defineTrigger('users_audit_trg', {
|
|
66
|
+
timing: 'AFTER', event: 'INSERT', on: users,
|
|
67
|
+
body: `INSERT INTO audit (action, at) VALUES ('insert', datetime('now'));`,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Add each table here as you define it.
|
|
71
|
+
export type DB = {
|
|
72
|
+
users: typeof users.$inferSelect;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Compile-only Kysely query builder — stateless, no connection held.
|
|
76
|
+
// Use with queryAll/queryFirst/queryRun to execute against D1.
|
|
77
|
+
export const db = createQueryBuilder<DB>();
|
|
28
78
|
```
|
|
29
79
|
|
|
30
|
-
|
|
31
|
-
|
|
80
|
+
### Valibot → SQL type mapping
|
|
81
|
+
|
|
82
|
+
| Valibot schema | SQL type | Nullable |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `v.string()` | TEXT | NOT NULL |
|
|
85
|
+
| `v.number()` | REAL | NOT NULL |
|
|
86
|
+
| `v.pipe(v.number(), v.integer(), ...)` | INTEGER | NOT NULL |
|
|
87
|
+
| `v.boolean()` | INTEGER | NOT NULL |
|
|
88
|
+
| `v.object({...})` or `v.array(...)` | TEXT (JSON) | NOT NULL |
|
|
89
|
+
| `v.optional(X)` | type of X | NULL |
|
|
90
|
+
| `v.nullable(X)` | type of X | NULL |
|
|
91
|
+
| `v.optional(X, defaultVal)` | type of X + DEFAULT | NULL |
|
|
92
|
+
|
|
93
|
+
### Auto columns
|
|
94
|
+
|
|
95
|
+
Every table gets `id`, `createdAt`, `updatedAt` by default, plus an `AFTER UPDATE` trigger for `updatedAt`. Control via options:
|
|
32
96
|
|
|
33
97
|
```typescript
|
|
34
|
-
|
|
98
|
+
// Disable everything
|
|
99
|
+
defineTable('events', { uuid: v.string() }, {
|
|
100
|
+
primaryKey: false, createdAt: false, updatedAt: false,
|
|
101
|
+
})
|
|
35
102
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
103
|
+
// Custom names (snake_case)
|
|
104
|
+
defineTable('users', { email: v.string() }, {
|
|
105
|
+
primaryKeyColumn: 'user_id',
|
|
106
|
+
createdAtColumn: 'created_at',
|
|
107
|
+
updatedAtColumn: 'updated_at',
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## CLI
|
|
39
114
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
115
|
+
```bash
|
|
116
|
+
d1-kyt init [--dir <dir>] # scaffold config + schema template
|
|
117
|
+
d1-kyt schema:diff <name> [--dir <dir>] # diff schema → write .sql migration
|
|
118
|
+
d1-kyt schema:diff <name> --schema <path> # use a custom schema file path
|
|
43
119
|
```
|
|
44
120
|
|
|
45
|
-
|
|
121
|
+
### `init`
|
|
46
122
|
|
|
47
|
-
|
|
123
|
+
Creates (skips if already exists):
|
|
124
|
+
- `<dir>/config.ts` — migrationsDir + namingStrategy
|
|
125
|
+
- `<dir>/schema.ts` — schema template to fill in
|
|
126
|
+
- `<dir>/schema.snapshot.jsonc` — diff baseline (**commit this to git**)
|
|
127
|
+
|
|
128
|
+
Directory resolution:
|
|
129
|
+
1. `--dir <path>` if provided
|
|
130
|
+
2. `db/` if it contains a `config.ts` (default)
|
|
131
|
+
3. `d1-kyt/` if it contains a `config.ts` (legacy)
|
|
132
|
+
4. Parent of wrangler `migrations_dir` if not the project root
|
|
133
|
+
|
|
134
|
+
### `schema:diff <name>`
|
|
135
|
+
|
|
136
|
+
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.
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
d1-kyt schema:diff create_users # generates 0001_create_users.sql
|
|
140
|
+
d1-kyt schema:diff add_email_index # generates 0002_add_email_index.sql
|
|
141
|
+
d1-kyt schema:diff --dir db add_posts # use db/config.ts, db/schema.ts
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Config
|
|
48
145
|
|
|
49
146
|
```typescript
|
|
50
|
-
//
|
|
147
|
+
// db/config.ts (or d1-kyt/config.ts)
|
|
148
|
+
import { defineConfig } from 'd1-kyt/config';
|
|
51
149
|
|
|
52
|
-
|
|
53
|
-
|
|
150
|
+
export default defineConfig({
|
|
151
|
+
migrationsDir: 'db/migrations',
|
|
152
|
+
namingStrategy: 'sequential', // or 'timestamp'
|
|
153
|
+
});
|
|
154
|
+
```
|
|
54
155
|
|
|
55
|
-
|
|
156
|
+
---
|
|
56
157
|
|
|
57
|
-
|
|
158
|
+
## Type inference
|
|
159
|
+
|
|
160
|
+
Types come directly from your schema — no code generation step required:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import { users } from './db/schema';
|
|
58
164
|
|
|
59
|
-
|
|
60
|
-
|
|
165
|
+
// Full row returned by SELECT
|
|
166
|
+
type UserRow = typeof users.$inferSelect;
|
|
167
|
+
// { id: number; email: string; name: string | undefined; age: number | undefined;
|
|
168
|
+
// prefs: { theme: string } | undefined; role: string | undefined;
|
|
169
|
+
// createdAt: string; updatedAt: string }
|
|
61
170
|
|
|
62
|
-
|
|
63
|
-
|
|
171
|
+
// Input for INSERT
|
|
172
|
+
type NewUser = typeof users.$inferInsert;
|
|
173
|
+
// { email: string; name?: string | undefined; age?: number | undefined; ... id?: number }
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Query Builder
|
|
179
|
+
|
|
180
|
+
`db` is a compile-only Kysely instance exported from your schema file. It holds no connection — it just builds typed SQL that you pass to `queryAll`/`queryFirst`/`queryRun` for execution.
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// src/queries.ts
|
|
184
|
+
import { db } from './db/schema';
|
|
185
|
+
|
|
186
|
+
export const listUsers = () =>
|
|
187
|
+
db.selectFrom('users').selectAll().compile();
|
|
188
|
+
|
|
189
|
+
export const getUserByEmail = (email: string) =>
|
|
190
|
+
db.selectFrom('users').selectAll().where('email', '=', email).compile();
|
|
191
|
+
|
|
192
|
+
export const insertUser = (email: string, name?: string) =>
|
|
193
|
+
db.insertInto('users').values({ email, name }).returning(['id']).compile();
|
|
64
194
|
```
|
|
65
195
|
|
|
66
196
|
## Execute Queries
|
|
67
197
|
|
|
68
198
|
```typescript
|
|
69
199
|
// src/app.ts
|
|
70
|
-
|
|
71
|
-
import Hono from 'hono';
|
|
200
|
+
import { Hono } from 'hono';
|
|
72
201
|
import { queryAll, queryFirst, queryRun } from 'd1-kyt';
|
|
73
202
|
import * as q from './queries';
|
|
74
203
|
|
|
75
204
|
const app = new Hono();
|
|
76
205
|
|
|
77
206
|
app.get('/users', async (c) => {
|
|
78
|
-
const users = await queryAll(c.env.DB, q.
|
|
207
|
+
const users = await queryAll(c.env.DB, q.listUsers());
|
|
79
208
|
return c.json(users);
|
|
80
209
|
});
|
|
81
210
|
|
|
82
|
-
app.get('/users/:
|
|
83
|
-
const user = await queryFirst(c.env.DB, q.
|
|
211
|
+
app.get('/users/:email', async (c) => {
|
|
212
|
+
const user = await queryFirst(c.env.DB, q.getUserByEmail(c.req.param('email')));
|
|
84
213
|
return user ? c.json(user) : c.notFound();
|
|
85
214
|
});
|
|
86
215
|
|
|
@@ -91,110 +220,58 @@ app.post('/users', async (c) => {
|
|
|
91
220
|
});
|
|
92
221
|
```
|
|
93
222
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
```typescript
|
|
97
|
-
// Disable all auto columns
|
|
98
|
-
const Event = defineTable('Event', (col) => ({
|
|
99
|
-
uuid: col.text().notNull(),
|
|
100
|
-
name: col.text().notNull(),
|
|
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
|
-
});
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
## Later Migrations
|
|
223
|
+
---
|
|
114
224
|
|
|
115
|
-
|
|
225
|
+
## Partial indexes
|
|
116
226
|
|
|
117
227
|
```typescript
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const User = useTable('User');
|
|
123
|
-
|
|
124
|
-
export const migration = () => [
|
|
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
|
-
];
|
|
228
|
+
defineIndex(users, ['email'], {
|
|
229
|
+
unique: true,
|
|
230
|
+
where: '"active" = 1', // raw SQL string
|
|
231
|
+
})
|
|
130
232
|
```
|
|
131
233
|
|
|
132
|
-
|
|
234
|
+
---
|
|
133
235
|
|
|
134
|
-
|
|
135
|
-
npm install d1-kyt kysely
|
|
136
|
-
npm install -D kysely-codegen
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
## CLI
|
|
140
|
-
|
|
141
|
-
```bash
|
|
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
|
-
```
|
|
147
|
-
|
|
148
|
-
Reads `wrangler.jsonc` to detect `migrations_dir` automatically.
|
|
149
|
-
|
|
150
|
-
### Type Generation
|
|
236
|
+
## Conventions
|
|
151
237
|
|
|
152
|
-
|
|
238
|
+
- Auto `id INTEGER PRIMARY KEY AUTOINCREMENT`, `createdAt TEXT`, `updatedAt TEXT` on every table (all configurable/disableable)
|
|
239
|
+
- Auto `AFTER UPDATE` trigger to keep `updatedAt` current
|
|
240
|
+
- Index naming: `{table}_{cols}_idx` / `{table}_{cols}_uq`
|
|
241
|
+
- Trigger naming: `{table}_{col}_trg`
|
|
242
|
+
- `schema.snapshot.jsonc` is the diff source of truth — always commit it alongside migration SQL files
|
|
153
243
|
|
|
154
|
-
|
|
155
|
-
# Apply migrations to local D1 database first
|
|
156
|
-
wrangler d1 migrations apply <db-name> --local
|
|
244
|
+
---
|
|
157
245
|
|
|
158
|
-
|
|
159
|
-
DATABASE_URL=.wrangler/state/v3/d1/<db-id>/db.sqlite d1-kyt typegen
|
|
160
|
-
```
|
|
246
|
+
## API reference
|
|
161
247
|
|
|
162
|
-
###
|
|
248
|
+
### `d1-kyt/schema`
|
|
163
249
|
|
|
164
|
-
|
|
165
|
-
|
|
250
|
+
| Export | Description |
|
|
251
|
+
|---|---|
|
|
252
|
+
| `defineTable(name, columns, opts?)` | Define a table; returns `SchemaTable` with `$inferSelect` / `$inferInsert` |
|
|
253
|
+
| `defineIndex(table, columns, opts?)` | Define an index (columns are type-checked against the table) |
|
|
254
|
+
| `defineTrigger(name, opts)` | Define a custom trigger attached to a table |
|
|
255
|
+
| `sqlTypeFromSchema(schema)` | Inspect a Valibot schema → `{ type, notNull, default?, isJson }` |
|
|
256
|
+
| `TableOptions` | Options type for auto columns (re-exported) |
|
|
166
257
|
|
|
167
|
-
|
|
258
|
+
### `d1-kyt` (main)
|
|
168
259
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
260
|
+
| Export | Description |
|
|
261
|
+
|---|---|
|
|
262
|
+
| `createQueryBuilder<DB>()` | Kysely instance (compile-only, no execution) |
|
|
263
|
+
| `queryAll(db, query)` | Execute query, return all rows |
|
|
264
|
+
| `queryFirst(db, query)` | Execute query, return first row or null |
|
|
265
|
+
| `queryRun(db, query)` | Execute mutation, return run metadata |
|
|
266
|
+
| `queryBatch(db, queries)` | Execute multiple queries as a D1 batch |
|
|
175
267
|
|
|
176
|
-
|
|
268
|
+
### `d1-kyt/config`
|
|
177
269
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
- Trigger naming: `{table}_{col}_trg`
|
|
270
|
+
| Export | Description |
|
|
271
|
+
|---|---|
|
|
272
|
+
| `defineConfig(config)` | Define `config.ts` (typed helper) |
|
|
182
273
|
|
|
183
|
-
|
|
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 |
|
|
274
|
+
---
|
|
198
275
|
|
|
199
276
|
## License
|
|
200
277
|
|