d1-kyt 0.4.5 → 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 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
- ## Migration DSL
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/migrations/0001_create_user_table.ts
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
- import { defineTable, createIndex } from 'd1-kyt/migrate';
62
+ export const usersEmailIdx = defineIndex(users, ['email'], { unique: true });
15
63
 
16
- const User = defineTable('User', (col) => ({
17
- externalId: col.text().notNull(),
18
- email: col.text().notNull(),
19
- name: col.text(),
20
- preferences: col.json<{ theme: string; notifications: boolean }>('{ theme: string; notifications: boolean }'),
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
- export const migration = () => [
24
- ...User.sql,
25
- createIndex(User, ['externalId'], { unique: true }),
26
- createIndex(User, ['email'], { unique: true }),
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
- Column types: `col.text()`, `col.integer()`, `col.real()`, `col.blob()`, `col.json<T>(overrideType?)`.
31
- `col.json<T>()` stores JSON as SQLite TEXT. The generic type parameter annotates the column's shape at compile time — use `TableType<typeof Table>` to derive a fully-typed row type from the DSL. Pass a TypeScript type string as the optional second argument to write that type into the kysely-codegen override registry; without it, `"unknown"` is written.
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 { defineTable, type TableType } from 'd1-kyt/migrate';
152
+ import { users } from './db/schema';
35
153
 
36
- const User = defineTable('User', (col) => ({
37
- preferences: col.json<{ theme: string; notifications: boolean }>('{ theme: string; notifications: boolean }'),
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
- type UserRow = TableType<typeof User>;
41
- // { preferences: { theme: string; notifications: boolean }; id: unknown; createdAt: unknown; updatedAt: unknown }
42
- // generated.ts emits: preferences: { theme: string; notifications: boolean }
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
- The type string passed to `col.json()` is written into `d1-kyt/kysely-codegen.json` so kysely-codegen emits the real shape in `generated.ts` instead of `unknown`. `generated.ts` remains the single source of truth — auto columns (`id`, `createdAt`, `updatedAt`) are typed there, not in `TableType`.
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/generated';
184
+ import type { DB } from './db';
54
185
 
55
186
  const db = createQueryBuilder<DB>();
56
187
 
57
- export const getUsers = () => db.selectFrom('User').selectAll().compile();
188
+ export const listUsers = () =>
189
+ db.selectFrom('users').selectAll().compile();
58
190
 
59
- export const getUser = (id: number) =>
60
- db.selectFrom('User').selectAll().where('id', '=', id).compile();
191
+ export const getUserByEmail = (email: string) =>
192
+ db.selectFrom('users').selectAll().where('email', '=', email).compile();
61
193
 
62
- export const insertUser = (email: string, name: string) =>
63
- db.insertInto('User').values({ email, name }).returning(['id']).compile();
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.getUsers());
209
+ const users = await queryAll(c.env.DB, q.listUsers());
79
210
  return c.json(users);
80
211
  });
81
212
 
82
- app.get('/users/:id', async (c) => {
83
- const user = await queryFirst(c.env.DB, q.getUser(c.req.param('id')));
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,108 +222,79 @@ app.post('/users', async (c) => {
91
222
  });
92
223
  ```
93
224
 
94
- ## Customizing Auto Columns
225
+ ---
226
+
227
+ ## Partial indexes
95
228
 
96
229
  ```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
- });
230
+ defineIndex(users, ['email'], {
231
+ unique: true,
232
+ where: '"active" = 1', // raw SQL string
233
+ })
111
234
  ```
112
235
 
113
- ## Later Migrations
236
+ ---
114
237
 
115
- Use `createUseTable` for type-safe references to existing tables:
116
-
117
- ```typescript
118
- import type { DB } from '../../db/generated';
119
- import { createUseTable, addColumn, createIndex } from 'd1-kyt/migrate';
238
+ ## Conventions
120
239
 
121
- const useTable = createUseTable<DB>();
122
- const User = useTable('User');
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
- export const migration = () => [
125
- addColumn(User, 'age', (col) => col.integer()),
126
- createIndex(User, ['age']),
127
- ];
128
- ```
246
+ ---
129
247
 
130
- ## Install
248
+ ## API reference
131
249
 
132
- ```bash
133
- npm install d1-kyt kysely
134
- npm install -D kysely-codegen
135
- ```
250
+ ### `d1-kyt/schema`
136
251
 
137
- ## CLI
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) |
138
259
 
139
- ```bash
140
- d1-kyt init # creates d1-kyt/ folder with config
141
- d1-kyt migrate:create <name> # creates d1-kyt/migrations/0001_<name>.ts
142
- d1-kyt migrate:build # compiles *.ts → db/migrations/*.sql
143
- d1-kyt typegen # runs kysely-codegen
144
- ```
260
+ ### `d1-kyt` (main)
145
261
 
146
- Reads `wrangler.jsonc` to detect `migrations_dir` automatically.
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 |
147
269
 
148
- ### Type Generation
270
+ ### `d1-kyt/config`
149
271
 
150
- `d1-kyt typegen` requires a `DATABASE_URL` environment variable pointing to a local SQLite database. kysely-codegen connects to this database to infer types.
272
+ | Export | Description |
273
+ |---|---|
274
+ | `defineConfig(config)` | Define `config.ts` (typed helper) |
151
275
 
152
- ```bash
153
- # Apply migrations to local D1 database first
154
- wrangler d1 migrations apply <db-name> --local
276
+ ---
155
277
 
156
- # Then generate types (local D1 databases live in .wrangler/)
157
- DATABASE_URL=.wrangler/state/v3/d1/<db-id>/db.sqlite d1-kyt typegen
158
- ```
278
+ ## Legacy migrate API
159
279
 
160
- ### Configuration
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.
161
281
 
162
282
  ```typescript
163
- // d1-kyt/config.ts
283
+ // d1-kyt/migrations/0001_create_users.ts
284
+ import { defineTable, createIndex } from 'd1-kyt/migrate';
164
285
 
165
- import { defineConfig } from 'd1-kyt/config';
286
+ const users = defineTable('users', (col) => ({
287
+ email: col.text().notNull(),
288
+ name: col.text(),
289
+ }));
166
290
 
167
- export default defineConfig({
168
- migrationsDir: 'db/migrations',
169
- dbDir: 'db',
170
- namingStrategy: 'sequential', // or 'timestamp'
171
- });
291
+ export const migration = () => [
292
+ ...users.sql,
293
+ createIndex(users, ['email'], { unique: true }),
294
+ ];
172
295
  ```
173
296
 
174
- ## Conventions
175
-
176
- - Auto `id`, `createdAt`, `updatedAt` on every table (configurable)
177
- - Auto trigger for `updatedAt`
178
- - Index naming: `{table}_{cols}_idx`, `{table}_{cols}_uq`
179
- - Trigger naming: `{table}_{col}_trg`
180
-
181
- ## API
182
-
183
- | Function | Description |
184
- | -------------------------------------- | --------------------------------- |
185
- | `defineTable(name, fn, opts?)` | Define new table |
186
- | `createUseTable<DB>()` | Factory for typed table refs |
187
- | `useTable<T>(name)` | Reference table (manual typing) |
188
- | `createIndex(table, cols, opts?)` | Create index |
189
- | `addColumn(table, col, fn)` | Add column |
190
- | `dropTable(table, updatedAtCol?)` | Drop table + trigger |
191
- | `dropIndex(name)` | Drop index |
192
- | `queryAll(db, query)` | Execute, return all rows |
193
- | `queryFirst(db, query)` | Execute, return first row or null |
194
- | `queryRun(db, query)` | Execute mutation |
195
- | `queryBatch(db, queries)` | Execute batch |
297
+ ---
196
298
 
197
299
  ## License
198
300