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 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
- ## 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. 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
- // d1-kyt/migrations/0001_create_user_table.ts
13
-
14
- import { defineTable, createIndex } from 'd1-kyt/migrate';
15
-
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
- }));
22
-
23
- export const migration = () => [
24
- ...User.sql,
25
- createIndex(User, ['externalId'], { unique: true }),
26
- createIndex(User, ['email'], { unique: true }),
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
- 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.
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
- import { defineTable, type TableType } from 'd1-kyt/migrate';
98
+ // Disable everything
99
+ defineTable('events', { uuid: v.string() }, {
100
+ primaryKey: false, createdAt: false, updatedAt: false,
101
+ })
35
102
 
36
- const User = defineTable('User', (col) => ({
37
- preferences: col.json<{ theme: string; notifications: boolean }>('{ theme: string; notifications: boolean }'),
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
- 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 }
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
- 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`.
121
+ ### `init`
46
122
 
47
- ## Query Builder
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
- // src/queries.ts
147
+ // db/config.ts (or d1-kyt/config.ts)
148
+ import { defineConfig } from 'd1-kyt/config';
51
149
 
52
- import { createQueryBuilder } from 'd1-kyt';
53
- import type { DB } from './db/generated';
150
+ export default defineConfig({
151
+ migrationsDir: 'db/migrations',
152
+ namingStrategy: 'sequential', // or 'timestamp'
153
+ });
154
+ ```
54
155
 
55
- const db = createQueryBuilder<DB>();
156
+ ---
56
157
 
57
- export const getUsers = () => db.selectFrom('User').selectAll().compile();
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
- export const getUser = (id: number) =>
60
- db.selectFrom('User').selectAll().where('id', '=', id).compile();
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
- export const insertUser = (email: string, name: string) =>
63
- db.insertInto('User').values({ email, name }).returning(['id']).compile();
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.getUsers());
207
+ const users = await queryAll(c.env.DB, q.listUsers());
79
208
  return c.json(users);
80
209
  });
81
210
 
82
- app.get('/users/:id', async (c) => {
83
- const user = await queryFirst(c.env.DB, q.getUser(c.req.param('id')));
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
- ## Customizing Auto Columns
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
- Use `createUseTable` for type-safe references to existing tables:
225
+ ## Partial indexes
116
226
 
117
227
  ```typescript
118
- import type { DB } from '../../db/generated';
119
- import { createUseTable, addColumn, createIndex } from 'd1-kyt/migrate';
120
-
121
- const useTable = createUseTable<DB>();
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
- ## Install
234
+ ---
133
235
 
134
- ```bash
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
- `d1-kyt typegen` requires a `DATABASE_URL` environment variable pointing to a local SQLite database. kysely-codegen connects to this database to infer types.
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
- ```bash
155
- # Apply migrations to local D1 database first
156
- wrangler d1 migrations apply <db-name> --local
244
+ ---
157
245
 
158
- # Then generate types (local D1 databases live in .wrangler/)
159
- DATABASE_URL=.wrangler/state/v3/d1/<db-id>/db.sqlite d1-kyt typegen
160
- ```
246
+ ## API reference
161
247
 
162
- ### Configuration
248
+ ### `d1-kyt/schema`
163
249
 
164
- ```typescript
165
- // d1-kyt/config.ts
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
- import { defineConfig } from 'd1-kyt/config';
258
+ ### `d1-kyt` (main)
168
259
 
169
- export default defineConfig({
170
- migrationsDir: 'db/migrations',
171
- dbDir: 'db',
172
- namingStrategy: 'sequential', // or 'timestamp'
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
- ## Conventions
268
+ ### `d1-kyt/config`
177
269
 
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`
270
+ | Export | Description |
271
+ |---|---|
272
+ | `defineConfig(config)` | Define `config.ts` (typed helper) |
182
273
 
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 |
274
+ ---
198
275
 
199
276
  ## License
200
277