@chromav/sizzler 0.1.1

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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/dist/columns/foreign-key.d.ts +33 -0
  4. package/dist/columns/foreign-key.d.ts.map +1 -0
  5. package/dist/columns/helpers.d.ts +59 -0
  6. package/dist/columns/helpers.d.ts.map +1 -0
  7. package/dist/columns/index.d.ts +5 -0
  8. package/dist/columns/index.d.ts.map +1 -0
  9. package/dist/columns/primary-key.d.ts +16 -0
  10. package/dist/columns/primary-key.d.ts.map +1 -0
  11. package/dist/columns/timestamps.d.ts +65 -0
  12. package/dist/columns/timestamps.d.ts.map +1 -0
  13. package/dist/index.d.ts +31 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +51 -0
  16. package/dist/node/connection/d1-helpers.d.ts +64 -0
  17. package/dist/node/connection/d1-helpers.d.ts.map +1 -0
  18. package/dist/node/connection/index.d.ts +3 -0
  19. package/dist/node/connection/index.d.ts.map +1 -0
  20. package/dist/node/connection/wrangler-config.d.ts +21 -0
  21. package/dist/node/connection/wrangler-config.d.ts.map +1 -0
  22. package/dist/node/index.d.ts +4 -0
  23. package/dist/node/index.d.ts.map +1 -0
  24. package/dist/node/migrate.d.ts +22 -0
  25. package/dist/node/migrate.d.ts.map +1 -0
  26. package/dist/node/seeding/index.d.ts +3 -0
  27. package/dist/node/seeding/index.d.ts.map +1 -0
  28. package/dist/node/seeding/json-seeder.d.ts +59 -0
  29. package/dist/node/seeding/json-seeder.d.ts.map +1 -0
  30. package/dist/node/seeding/reset.d.ts +28 -0
  31. package/dist/node/seeding/reset.d.ts.map +1 -0
  32. package/dist/node/snapshot/index.d.ts +2 -0
  33. package/dist/node/snapshot/index.d.ts.map +1 -0
  34. package/dist/node/snapshot/snapshot.d.ts +63 -0
  35. package/dist/node/snapshot/snapshot.d.ts.map +1 -0
  36. package/dist/node.d.ts +28 -0
  37. package/dist/node.d.ts.map +1 -0
  38. package/dist/node.js +310 -0
  39. package/package.json +79 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Eddy Recio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,169 @@
1
+ # sizzler 🔥
2
+
3
+ **Sensible defaults for Drizzle + SQLite. Now we're sizzling.**
4
+
5
+ Stop bikeshedding IDs, timestamps, and foreign keys. Define your schema, seed your database, and get back to building.
6
+
7
+ ```typescript
8
+ import { primaryKey, foreignKey, createdAt, updatedAt, table, text, enums } from "sizzler";
9
+
10
+ const status = ["pending", "in_progress", "done"] as const;
11
+
12
+ export const tasks = table("tasks", {
13
+ id: primaryKey(), // ULID — sortable, client-generatable
14
+ title: text("title").notNull(),
15
+ status: enums("status", status), // type-safe, defaults to first value
16
+ assigneeId: foreignKey("assignee_id", users.id),
17
+ createdAt: createdAt(), // unixepoch() default
18
+ updatedAt: updatedAt(),
19
+ });
20
+ ```
21
+
22
+ Works with D1, Turso, libsql, better-sqlite3 — anywhere Drizzle's SQLite adapter runs.
23
+
24
+ ## What's in the box
25
+
26
+ ### 1. Schema factories
27
+
28
+ Sensible defaults so you don't have to think about it:
29
+
30
+ | Factory | What you get |
31
+ |---------|--------------|
32
+ | `primaryKey()` | Text ULID — sortable by time, generate client-side, no DB roundtrip |
33
+ | `foreignKey(name, ref)` | Text FK to match ULID PKs (pass `"integer"` for numeric refs) |
34
+ | `createdAt()` / `updatedAt()` | Timestamps with `unixepoch()` default |
35
+ | `timestamp(name)` | Bare timestamp, no default |
36
+ | `boolean(name)` | Integer 0/1 (SQLite has no boolean type) |
37
+ | `enums(name, values)` | Type-safe text column, defaults to first value |
38
+ | `jsonArray<T>(name)` | Typed JSON storage |
39
+ | `timestampMs(name)` | Integer timestamp in milliseconds (for BetterAuth compatibility) |
40
+
41
+ Every factory returns a standard Drizzle column builder. Chain `.notNull()`, `.unique()`, whatever — then use `drizzle-kit` like normal.
42
+
43
+ ### 2. Local dev helpers
44
+
45
+ The `sizzler/node` entry point gets you connected and seeded fast:
46
+
47
+ ```typescript
48
+ import { computeD1Filename, getLocalD1Path, createJSONSeeder, resetDatabase } from "sizzler/node";
49
+
50
+ // Compute the exact filename miniflare uses for a D1 database
51
+ const filename = computeD1Filename("3c9d1256-e9bd-4617-ad46-22315915a680");
52
+ // => "feed28b9d46e13dcb8b4ff9ebe14a54b3b84fc08efd83b1de4251b30f50c9f10.sqlite"
53
+
54
+ // Find your local D1 database (uses computeD1Filename internally)
55
+ const dbPath = getLocalD1Path();
56
+
57
+ // Load seed data with automatic transforms
58
+ const seeder = createJSONSeeder({
59
+ baseDir: "./seeds",
60
+ transformers: commonTransformers.dateFields(["createdAt", "updatedAt"]),
61
+ });
62
+ const users = seeder.loadJSON("users.json");
63
+
64
+ // Reset and reseed (requires explicit confirmation)
65
+ await resetDatabase(db, schema, { confirm: true });
66
+ ```
67
+
68
+ No more hunting for SQLite files in `.wrangler/`. No more reverse-engineering miniflare's hashed filenames. No more manual date parsing in seed scripts.
69
+
70
+ ### 3. Golden snapshots
71
+
72
+ Also on `sizzler/node`: helpers for creating and restoring self-contained SQLite
73
+ "golden" snapshots — the foundation of fast, reproducible E2E tests (build the DB
74
+ once, restore the file per run instead of migrating + seeding every time).
75
+
76
+ ```typescript
77
+ import { createSnapshot, restoreSnapshot } from "sizzler/node";
78
+
79
+ // Create a golden snapshot from a live DB: checkpoints the WAL into the main
80
+ // file (so the .sqlite is self-contained), copies it, and verifies it has data.
81
+ createSnapshot(liveDbPath, "test-data/golden/test.sqlite", {
82
+ verify: { countQuery: "SELECT COUNT(*) as count FROM user", min: 1 },
83
+ });
84
+
85
+ // Restore it onto a target DB atomically (temp file + rename — no torn/missing
86
+ // window), with a corruption guard and optional post-restore verification.
87
+ restoreSnapshot("test-data/golden/test.sqlite", liveDbPath, {
88
+ minBytes: 100 * 1024,
89
+ verify: { countQuery: "SELECT COUNT(*) as count FROM user", min: 1 },
90
+ });
91
+ ```
92
+
93
+ Lower-level primitives are exported too: `checkpointDatabase`, `removeWalFiles`,
94
+ `removeOldSnapshot`, `verifySnapshot`, and `countRows`. These require the optional
95
+ `better-sqlite3` peer dependency (node-only — never imported in a Worker bundle),
96
+ and pair with `computeD1Filename`/`parseWranglerConfig` to locate the miniflare D1
97
+ file to snapshot or restore.
98
+
99
+ > ⚠️ **Restore semantics:** `restoreSnapshot` creates a new inode at the target
100
+ > path. Run it *before the server process opens the DB* (e.g. a `pretest` step),
101
+ > not mid-session against a live server — a running worker's mmap won't see the
102
+ > swapped file. (For mid-session resets, reset *within* the live DB connection
103
+ > instead.)
104
+
105
+ ## Installation
106
+
107
+ ```bash
108
+ npm install sizzler drizzle-orm
109
+ ```
110
+
111
+ ## The boilerplate it replaces
112
+
113
+ <details>
114
+ <summary>See before/after</summary>
115
+
116
+ **Before:**
117
+
118
+ ```typescript
119
+ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
120
+ import { sql } from "drizzle-orm";
121
+ import { ulid } from "ulid";
122
+
123
+ export const tasks = sqliteTable("tasks", {
124
+ id: text("id").primaryKey().notNull().$defaultFn(() => ulid()),
125
+ title: text("title").notNull(),
126
+ status: text("status", { enum: ["pending", "in_progress", "done"] }).default("pending"),
127
+ assigneeId: text("assignee_id").references(() => users.id),
128
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
129
+ updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
130
+ });
131
+ ```
132
+
133
+ **After:**
134
+
135
+ ```typescript
136
+ import { primaryKey, foreignKey, createdAt, updatedAt, table, text, enums } from "sizzler";
137
+
138
+ const status = ["pending", "in_progress", "done"] as const;
139
+
140
+ export const tasks = table("tasks", {
141
+ id: primaryKey(),
142
+ title: text("title").notNull(),
143
+ status: enums("status", status),
144
+ assigneeId: foreignKey("assignee_id", users.id),
145
+ createdAt: createdAt(),
146
+ updatedAt: updatedAt(),
147
+ });
148
+ ```
149
+
150
+ </details>
151
+
152
+ ## Why ULIDs?
153
+
154
+ The primary key choice is the most opinionated default. Here's the reasoning:
155
+
156
+ - **Sortable** — First 10 chars encode a millisecond timestamp. `ORDER BY id` works like `ORDER BY created_at`.
157
+ - **Client-generatable** — Know the ID before the insert hits the database. Essential for optimistic UI.
158
+ - **No coordination** — Works across edge locations, replicas, offline-first apps.
159
+
160
+ Integer autoincrement requires a roundtrip to learn the ID. ULIDs let client and server agree on the ID upfront.
161
+
162
+ ## Documentation
163
+
164
+ - [API Reference](docs/api-reference.md) — Full signatures and examples for all factories
165
+ - [Node.js Utilities](docs/node-utilities.md) — D1 path resolution, JSON seeding, database reset
166
+
167
+ ## License
168
+
169
+ MIT
@@ -0,0 +1,33 @@
1
+ import type { SQLiteColumn, SQLiteIntegerBuilder, SQLiteTextBuilder, UpdateDeleteAction } from "drizzle-orm/sqlite-core";
2
+ type ActionConfig = {
3
+ onUpdate?: UpdateDeleteAction;
4
+ onDelete?: UpdateDeleteAction;
5
+ };
6
+ /**
7
+ * Creates a foreign key column with configurable cascade actions.
8
+ *
9
+ * Defaults to `"text"` type to match sizzler's ULID-based `primaryKey()`.
10
+ * Use `"integer"` for auto-increment or numeric foreign keys.
11
+ *
12
+ * @param name - Column name
13
+ * @param col - Reference column from another table
14
+ * @param type - Column type: `"text"` (default) or `"integer"`
15
+ * @param actions - Optional cascade actions for onUpdate/onDelete
16
+ * @returns A column configured as a foreign key
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const posts = table("posts", {
21
+ * id: primaryKey(),
22
+ * // Text foreign key (default — matches ULID primary keys)
23
+ * authorId: foreignKey("author_id", users.id),
24
+ * // With cascade options
25
+ * parentId: foreignKey("parent_id", posts.id, "text", { onDelete: "cascade" }),
26
+ * // Integer foreign key (for numeric references)
27
+ * legacyCategoryId: foreignKey("category_id", categories.id, "integer"),
28
+ * });
29
+ * ```
30
+ */
31
+ export declare const foreignKey: <T extends "integer" | "text" = "text">(name: string, col: SQLiteColumn, type?: T, actions?: ActionConfig) => T extends "integer" ? SQLiteIntegerBuilder : SQLiteTextBuilder<[string, ...string[]]>;
32
+ export {};
33
+ //# sourceMappingURL=foreign-key.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"foreign-key.d.ts","sourceRoot":"","sources":["../../src/columns/foreign-key.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EACnB,MAAM,yBAAyB,CAAC;AAGjC,KAAK,YAAY,GAAG;IAClB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC/B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,SAAS,SAAS,GAAG,MAAM,GAAG,MAAM,EAC9D,MAAM,MAAM,EACZ,KAAK,YAAY,EACjB,OAAM,CAAe,EACrB,UAAU,YAAY,KACrB,CAAC,SAAS,SAAS,GAAG,oBAAoB,GAAG,iBAAiB,CAAC,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,CAKzC,CAAC"}
@@ -0,0 +1,59 @@
1
+ import { index as sqliteIndex, integer as sqliteInteger, real as sqliteReal, text as sqliteText } from "drizzle-orm/sqlite-core";
2
+ type EnumValues = readonly [string, ...string[]];
3
+ export declare const table: import("drizzle-orm/sqlite-core").SQLiteTableFn<undefined>;
4
+ export declare const index: typeof sqliteIndex;
5
+ export declare const integer: typeof sqliteInteger;
6
+ export declare const real: typeof sqliteReal;
7
+ export declare const text: typeof sqliteText;
8
+ /**
9
+ * Creates a boolean column stored as integer (0/1).
10
+ *
11
+ * @param name - Column name
12
+ * @returns An integer column in boolean mode
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const users = table("users", {
17
+ * isActive: boolean("is_active"),
18
+ * });
19
+ * ```
20
+ */
21
+ export declare const boolean: (name: string) => import("drizzle-orm/sqlite-core").SQLiteBooleanBuilder;
22
+ /**
23
+ * Creates an enum column with type-safe values.
24
+ * Stored as text in SQLite with validation at the application level.
25
+ *
26
+ * @param name - Column name
27
+ * @param values - Tuple of allowed string values
28
+ * @param defaultValue - Optional default value (defaults to first value)
29
+ * @returns A text column with enum type
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * const taskPriority = ["low", "medium", "high"] as const;
34
+ *
35
+ * const tasks = table("tasks", {
36
+ * priority: enums("priority", taskPriority, "medium"),
37
+ * });
38
+ * ```
39
+ */
40
+ export declare const enums: <T extends EnumValues>(name: string, values: T, defaultValue?: T[number]) => import("drizzle-orm").HasDefault<import("drizzle-orm/sqlite-core").SQLiteTextBuilder<import("drizzle-orm").Writable<T>>>;
41
+ /**
42
+ * Creates a JSON array column with type parameter.
43
+ *
44
+ * @param name - Column name
45
+ * @returns A text column storing JSON with typed array access
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * const posts = table("posts", {
50
+ * // Array of user IDs who liked the post
51
+ * likedBy: jsonArray<string[]>("liked_by"),
52
+ * // Array of tag objects
53
+ * tags: jsonArray<{ id: string; name: string }[]>("tags"),
54
+ * });
55
+ * ```
56
+ */
57
+ export declare const jsonArray: <T = string[]>(name: string) => import("drizzle-orm").$Type<import("drizzle-orm/sqlite-core").SQLiteTextJsonBuilder, T>;
58
+ export {};
59
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/columns/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,IAAI,WAAW,EACpB,OAAO,IAAI,aAAa,EACxB,IAAI,IAAI,UAAU,EAElB,IAAI,IAAI,UAAU,EACnB,MAAM,yBAAyB,CAAC;AAEjC,KAAK,UAAU,GAAG,SAAS,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC;AAGjD,eAAO,MAAM,KAAK,4DAAc,CAAC;AACjC,eAAO,MAAM,KAAK,oBAAc,CAAC;AACjC,eAAO,MAAM,OAAO,sBAAgB,CAAC;AACrC,eAAO,MAAM,IAAI,mBAAa,CAAC;AAC/B,eAAO,MAAM,IAAI,mBAAa,CAAC;AAE/B;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,MAAM,2DAAuC,CAAC;AAE5E;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,KAAK,GAAI,CAAC,SAAS,UAAU,EAAE,MAAM,MAAM,EAAE,QAAQ,CAAC,EAAE,eAAe,CAAC,CAAC,MAAM,CAAC,6HAGxC,CAAC;AAEtD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,SAAS,GAAI,CAAC,GAAG,MAAM,EAAE,EAAE,MAAM,MAAM,4FAA4C,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { foreignKey } from "./foreign-key";
2
+ export { boolean, enums, index, integer, jsonArray, real, table, text, } from "./helpers";
3
+ export { primaryKey } from "./primary-key";
4
+ export { createdAt, timestamp, timestamp_ms, updatedAt } from "./timestamps";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/columns/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EACL,OAAO,EACP,KAAK,EACL,KAAK,EACL,OAAO,EACP,SAAS,EACT,IAAI,EAEJ,KAAK,EACL,IAAI,GACL,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Creates a primary key column with auto-generated ULID.
3
+ *
4
+ * @param name - Column name (default: "id")
5
+ * @returns A text column configured as primary key with ULID generation
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const users = table("users", {
10
+ * id: primaryKey(),
11
+ * name: text("name"),
12
+ * });
13
+ * ```
14
+ */
15
+ export declare const primaryKey: (name?: string) => import("drizzle-orm").HasRuntimeDefault<import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm").IsPrimaryKey<import("drizzle-orm/sqlite-core").SQLiteTextBuilder<[string, ...string[]]>>>>>;
16
+ //# sourceMappingURL=primary-key.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"primary-key.d.ts","sourceRoot":"","sources":["../../src/columns/primary-key.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,UAAU,GAAI,aAAW,6NAIT,CAAC"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Creates a timestamp column storing Unix epoch seconds.
3
+ *
4
+ * @param columnName - Column name
5
+ * @returns An integer column in timestamp mode
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const events = table("events", {
10
+ * occurredAt: timestamp("occurred_at"),
11
+ * });
12
+ * ```
13
+ */
14
+ export declare const timestamp: (columnName: string) => import("drizzle-orm/sqlite-core").SQLiteTimestampBuilder;
15
+ /**
16
+ * Creates a timestamp column storing Unix epoch milliseconds.
17
+ *
18
+ * Use this for BetterAuth plugin tables (api keys, etc.) which expect
19
+ * `timestamp_ms` mode. Cobaltplan's own tables use `timestamp` (seconds).
20
+ *
21
+ * @param columnName - Column name
22
+ * @returns An integer column in timestamp_ms mode
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const apikey = table("apikey", {
27
+ * expiresAt: timestampMs("expires_at"),
28
+ * });
29
+ * ```
30
+ */
31
+ export declare const timestamp_ms: (columnName: string) => import("drizzle-orm/sqlite-core").SQLiteTimestampBuilder;
32
+ /**
33
+ * Creates a created_at timestamp column with automatic default.
34
+ * Uses SQLite's unixepoch() for the default value.
35
+ *
36
+ * @returns A non-null timestamp column defaulting to current time
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * const users = table("users", {
41
+ * id: primaryKey(),
42
+ * createdAt: createdAt(),
43
+ * });
44
+ * ```
45
+ */
46
+ export declare const createdAt: () => import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm/sqlite-core").SQLiteTimestampBuilder>>;
47
+ /**
48
+ * Creates an updated_at timestamp column with automatic default.
49
+ * Uses SQLite's unixepoch() for the default value.
50
+ *
51
+ * Note: Auto-update on modification requires application-level handling
52
+ * or a database trigger.
53
+ *
54
+ * @returns A non-null timestamp column defaulting to current time
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * const users = table("users", {
59
+ * id: primaryKey(),
60
+ * updatedAt: updatedAt(),
61
+ * });
62
+ * ```
63
+ */
64
+ export declare const updatedAt: () => import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm/sqlite-core").SQLiteTimestampBuilder>>;
65
+ //# sourceMappingURL=timestamps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timestamps.d.ts","sourceRoot":"","sources":["../../src/columns/timestamps.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,SAAS,GAAI,YAAY,MAAM,6DAA+C,CAAC;AAE5F;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,YAAY,GAAI,YAAY,MAAM,6DAAkD,CAAC;AAElG;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,SAAS,iIAAsE,CAAC;AAE7F;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,SAAS,iIAAsE,CAAC"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Sizzler
3
+ *
4
+ * Workers-safe column factories and utilities for Drizzle ORM with SQLite/D1/Turso.
5
+ *
6
+ * For Node.js utilities (seeding, connection helpers), import from:
7
+ * ```ts
8
+ * import { getLocalD1Path, createJSONSeeder } from "sizzler/node";
9
+ * ```
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import {
14
+ * primaryKey,
15
+ * foreignKey,
16
+ * createdAt,
17
+ * updatedAt,
18
+ * table,
19
+ * text,
20
+ * } from "sizzler";
21
+ *
22
+ * const users = table("users", {
23
+ * id: primaryKey(),
24
+ * name: text("name").notNull(),
25
+ * createdAt: createdAt(),
26
+ * updatedAt: updatedAt(),
27
+ * });
28
+ * ```
29
+ */
30
+ export { boolean, createdAt, enums, foreignKey, index, integer, jsonArray, primaryKey, real, table, text, timestamp, timestamp_ms, updatedAt, } from "./columns";
31
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAGH,OAAO,EACL,OAAO,EACP,SAAS,EACT,KAAK,EACL,UAAU,EACV,KAAK,EACL,OAAO,EACP,SAAS,EACT,UAAU,EACV,IAAI,EAEJ,KAAK,EACL,IAAI,EACJ,SAAS,EACT,YAAY,EACZ,SAAS,GACV,MAAM,WAAW,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,51 @@
1
+ // src/columns/foreign-key.ts
2
+ import { integer, text } from "drizzle-orm/sqlite-core";
3
+ var foreignKey = (name, col, type = "text", actions) => type === "integer" ? integer(name).references(() => col, actions) : text(name).references(() => col, actions);
4
+
5
+ // src/columns/helpers.ts
6
+ import {
7
+ index as sqliteIndex,
8
+ integer as sqliteInteger,
9
+ real as sqliteReal,
10
+ sqliteTable,
11
+ text as sqliteText
12
+ } from "drizzle-orm/sqlite-core";
13
+ var table = sqliteTable;
14
+ var index = sqliteIndex;
15
+ var integer2 = sqliteInteger;
16
+ var real = sqliteReal;
17
+ var text2 = sqliteText;
18
+ var boolean = (name) => integer2(name, { mode: "boolean" });
19
+ var enums = (name, values, defaultValue) => text2(name, {
20
+ enum: values
21
+ }).default(defaultValue ? defaultValue : values[0]);
22
+ var jsonArray = (name) => text2(name, { mode: "json" }).$type();
23
+
24
+ // src/columns/primary-key.ts
25
+ import { text as text3 } from "drizzle-orm/sqlite-core";
26
+ import { ulid } from "ulid";
27
+ var primaryKey = (name = "id") => text3(name).primaryKey().notNull().$defaultFn(() => ulid());
28
+
29
+ // src/columns/timestamps.ts
30
+ import { sql } from "drizzle-orm";
31
+ import { integer as integer3 } from "drizzle-orm/sqlite-core";
32
+ var timestamp = (columnName) => integer3(columnName, { mode: "timestamp" });
33
+ var timestamp_ms = (columnName) => integer3(columnName, { mode: "timestamp_ms" });
34
+ var createdAt = () => timestamp("created_at").notNull().default(sql`(unixepoch())`);
35
+ var updatedAt = () => timestamp("updated_at").notNull().default(sql`(unixepoch())`);
36
+ export {
37
+ boolean,
38
+ createdAt,
39
+ enums,
40
+ foreignKey,
41
+ index,
42
+ integer2 as integer,
43
+ jsonArray,
44
+ primaryKey,
45
+ real,
46
+ table,
47
+ text2 as text,
48
+ timestamp,
49
+ timestamp_ms,
50
+ updatedAt
51
+ };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Compute the SQLite filename that miniflare uses for a D1 database.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * Miniflare (Cloudflare's local dev runtime) stores each D1 database as a
7
+ * SQLite file with a hashed name — NOT the human-readable `database_id` from
8
+ * `wrangler.jsonc`. Without this function, scripts that need to create or
9
+ * locate the database file (migrations, golden DB restore, golden DB create)
10
+ * would have to start a dev server first and scan the directory for whatever
11
+ * file appeared — which is fragile and slow.
12
+ *
13
+ * ## How the hash works
14
+ *
15
+ * Miniflare's `durableObjectNamespaceIdFromName()` algorithm:
16
+ *
17
+ * 1. Derive a `key` by SHA-256 hashing the Durable Object unique key
18
+ * (always `"miniflare-D1DatabaseObject"` for D1).
19
+ * 2. HMAC-SHA256 the `database_id` with that key, take the first 16 bytes
20
+ * → this is the `nameHmac` (identifies the namespace + name).
21
+ * 3. HMAC-SHA256 the `nameHmac` with the same key, take the first 16 bytes
22
+ * → this is the `hmac` (integrity check).
23
+ * 4. Concatenate `nameHmac + hmac` (32 bytes) and hex-encode → 64-char filename.
24
+ * 5. Append `.sqlite`.
25
+ *
26
+ * For our `database_id: "3c9d1256-e9bd-4617-ad46-22315915a680"`, this always
27
+ * produces: `feed28b9d46e13dcb8b4ff9ebe14a54b3b84fc08efd83b1de4251b30f50c9f10.sqlite`
28
+ *
29
+ * ## Where it's used
30
+ *
31
+ * - `drizzle/migrate-local.ts` — creates the DB file with the correct name
32
+ * - `scripts/golden-db-restore.ts` — copies snapshot to the correct filename
33
+ * - `scripts/golden-db-create.ts` — finds the DB file to snapshot
34
+ * - `getLocalD1Path()` below — locates the DB file for seed scripts
35
+ *
36
+ * @param databaseId - The `database_id` from wrangler.jsonc
37
+ * @returns The hashed filename (e.g., `feed28b9...sqlite`)
38
+ *
39
+ * @see https://github.com/cloudflare/workers-sdk — miniflare source
40
+ */
41
+ export declare function computeD1Filename(databaseId: string): string;
42
+ /**
43
+ * Get the local D1 database file path for development or testing.
44
+ *
45
+ * By default, looks in `.wrangler/state/v3/d1/miniflare-D1DatabaseObject/`.
46
+ * When TEST_STATE_DIR is set (e.g., TEST_STATE_DIR=.wrangler-test), looks there instead.
47
+ *
48
+ * @param bindingName - The D1 binding name from wrangler.json (default: "DB")
49
+ * @param projectRoot - Project root directory (default: process.cwd())
50
+ * @returns The absolute path to the SQLite database file
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * import Database from "better-sqlite3";
55
+ * import { drizzle } from "drizzle-orm/better-sqlite3";
56
+ * import { getLocalD1Path } from "sizzler/node";
57
+ *
58
+ * const dbPath = getLocalD1Path();
59
+ * const sqlite = new Database(dbPath);
60
+ * const db = drizzle({ client: sqlite });
61
+ * ```
62
+ */
63
+ export declare function getLocalD1Path(bindingName?: string, projectRoot?: string): string;
64
+ //# sourceMappingURL=d1-helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d1-helpers.d.ts","sourceRoot":"","sources":["../../../src/node/connection/d1-helpers.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAa5D;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,cAAc,CAAC,WAAW,SAAO,EAAE,WAAW,GAAE,MAAsB,GAAG,MAAM,CA2C9F"}
@@ -0,0 +1,3 @@
1
+ export { computeD1Filename, getLocalD1Path } from "./d1-helpers";
2
+ export { parseWranglerConfig, type WranglerConfig } from "./wrangler-config";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/node/connection/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,21 @@
1
+ export interface WranglerConfig {
2
+ d1_databases?: Array<{
3
+ binding: string;
4
+ database_id: string;
5
+ }>;
6
+ }
7
+ /**
8
+ * Parse wrangler.json/wrangler.jsonc configuration with JSONC support.
9
+ * Handles comments and trailing commas.
10
+ *
11
+ * @param projectRoot - Project root directory (default: process.cwd())
12
+ * @returns Parsed Wrangler configuration
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const config = parseWranglerConfig();
17
+ * const dbId = config.d1_databases?.[0]?.database_id;
18
+ * ```
19
+ */
20
+ export declare function parseWranglerConfig(projectRoot?: string): WranglerConfig;
21
+ //# sourceMappingURL=wrangler-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wrangler-config.d.ts","sourceRoot":"","sources":["../../../src/node/connection/wrangler-config.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,cAAc;IAC7B,YAAY,CAAC,EAAE,KAAK,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC,CAAC;CACJ;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,GAAE,MAAsB,GAAG,cAAc,CA4CvF"}
@@ -0,0 +1,4 @@
1
+ export { getLocalD1Path, parseWranglerConfig, type WranglerConfig, } from "./connection";
2
+ export { commonTransformers, createJSONSeeder, type JSONSeederOptions, resetDatabase, } from "./seeding";
3
+ export { type CreateSnapshotOptions, checkpointDatabase, countRows, createSnapshot, type RestoreSnapshotOptions, removeOldSnapshot, removeWalFiles, restoreSnapshot, type SnapshotVerify, verifySnapshot, } from "./snapshot";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,KAAK,cAAc,GACpB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,iBAAiB,EACtB,aAAa,GACd,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,KAAK,qBAAqB,EAC1B,kBAAkB,EAClB,SAAS,EACT,cAAc,EACd,KAAK,sBAAsB,EAC3B,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,KAAK,cAAc,EACnB,cAAc,GACf,MAAM,YAAY,CAAC"}
@@ -0,0 +1,22 @@
1
+ export interface RunLocalMigrationsOptions {
2
+ /** D1 binding name in the wrangler config (e.g. "DB", "WORKGEN_DB"). */
3
+ bindingName: string;
4
+ /** Project root holding the wrangler config + `.wrangler` state. Defaults to `process.cwd()`. */
5
+ projectRoot?: string;
6
+ /** drizzle-kit migrations folder. Defaults to `<projectRoot>/drizzle/migrations`. */
7
+ migrationsFolder?: string;
8
+ /** Progress logger. Defaults to `console.log`. */
9
+ log?: (message: string) => void;
10
+ }
11
+ /**
12
+ * Apply drizzle-kit migrations to the local miniflare D1 file for a binding.
13
+ *
14
+ * The file is named by hashing the binding's `database_id` — the exact name
15
+ * miniflare (and the @cloudflare/vite-plugin dev server) uses — so the dev
16
+ * server reads what this writes. Always targets that hashed name (never a stray
17
+ * file) and creates it if missing. Honors `TEST_STATE_DIR` for test isolation.
18
+ *
19
+ * Requires the optional `better-sqlite3` + `drizzle-orm` peer deps.
20
+ */
21
+ export declare function runLocalMigrations(options: RunLocalMigrationsOptions): void;
22
+ //# sourceMappingURL=migrate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../src/node/migrate.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,yBAAyB;IACxC,wEAAwE;IACxE,WAAW,EAAE,MAAM,CAAC;IACpB,iGAAiG;IACjG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kDAAkD;IAClD,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,IAAI,CA0B3E"}
@@ -0,0 +1,3 @@
1
+ export { commonTransformers, createJSONSeeder, type JSONSeederOptions, } from "./json-seeder";
2
+ export { resetDatabase } from "./reset";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/node/seeding/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,iBAAiB,GACvB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,59 @@
1
+ export interface JSONSeederOptions {
2
+ baseDir: string;
3
+ transformers?: Record<string, (value: unknown) => unknown>;
4
+ }
5
+ /**
6
+ * Utility for seeding databases from structured JSON files.
7
+ * Handles date parsing, transformations, and nested directory structures.
8
+ *
9
+ * @param options - Configuration options
10
+ * @returns Object with loadJSON, loadJSONDirectory, and fileExists methods
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const seeder = createJSONSeeder({
15
+ * baseDir: path.join(__dirname, "seed-data"),
16
+ * transformers: {
17
+ * ...commonTransformers.dateFields(["createdAt", "updatedAt"]),
18
+ * },
19
+ * });
20
+ *
21
+ * const users = seeder.loadJSON<User[]>("users.json");
22
+ * const posts = seeder.loadJSONDirectory<Post>("posts/");
23
+ * ```
24
+ */
25
+ export declare function createJSONSeeder(options: JSONSeederOptions): {
26
+ loadJSON: <T = unknown>(relativePath: string) => T;
27
+ loadJSONDirectory: <T = unknown>(relativePath: string) => T[];
28
+ fileExists: (relativePath: string) => boolean;
29
+ };
30
+ /**
31
+ * Common transformers for typical database seeding scenarios.
32
+ */
33
+ export declare const commonTransformers: {
34
+ /**
35
+ * Convert ISO date strings to Date objects.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * const seeder = createJSONSeeder({
40
+ * baseDir: "./seed-data",
41
+ * transformers: commonTransformers.dateFields(["createdAt", "updatedAt"]),
42
+ * });
43
+ * ```
44
+ */
45
+ dateFields: (fieldNames: string[]) => Record<string, (value: unknown) => unknown>;
46
+ /**
47
+ * Convert string values to numbers.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * const seeder = createJSONSeeder({
52
+ * baseDir: "./seed-data",
53
+ * transformers: commonTransformers.numberFields(["age", "count"]),
54
+ * });
55
+ * ```
56
+ */
57
+ numberFields: (fieldNames: string[]) => Record<string, (value: unknown) => unknown>;
58
+ };
59
+ //# sourceMappingURL=json-seeder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-seeder.d.ts","sourceRoot":"","sources":["../../../src/node/seeding/json-seeder.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC;CAC5D;AAyBD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB;eAMvC,CAAC,0BAA0B,MAAM,KAAG,CAAC;wBAW5B,CAAC,0BAA0B,MAAM,KAAG,CAAC,EAAE;+BAUhC,MAAM,KAAG,OAAO;EASnD;AAED;;GAEG;AACH,eAAO,MAAM,kBAAkB;IAC7B;;;;;;;;;;OAUG;6BACsB,MAAM,EAAE,4BACY,OAAO,KAAK,OAAO;IAOhE;;;;;;;;;;OAUG;+BACwB,MAAM,EAAE,4BACU,OAAO,KAAK,OAAO;CAMjE,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";
2
+ /**
3
+ * Reset database with enhanced error handling and logging.
4
+ * Requires explicit confirmation to prevent accidental data loss.
5
+ *
6
+ * @param db - Drizzle database instance
7
+ * @param schema - Database schema object
8
+ * @param options - Configuration options
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { resetDatabase } from "sizzler/node";
13
+ *
14
+ * await resetDatabase(db, schema, {
15
+ * confirm: true,
16
+ * verbose: true,
17
+ * });
18
+ * ```
19
+ */
20
+ export declare function resetDatabase(db: BaseSQLiteDatabase<"sync" | "async", unknown>, schema: Record<string, unknown>, options?: {
21
+ confirm?: boolean;
22
+ verbose?: boolean;
23
+ logger?: {
24
+ log: (...args: unknown[]) => void;
25
+ error: (...args: unknown[]) => void;
26
+ };
27
+ }): Promise<void>;
28
+ //# sourceMappingURL=reset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reset.d.ts","sourceRoot":"","sources":["../../../src/node/seeding/reset.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAGlE;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,aAAa,CACjC,EAAE,EAAE,kBAAkB,CAAC,MAAM,GAAG,OAAO,EAAE,OAAO,CAAC,EACjD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,GAAE;IACP,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE;QAAE,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;QAAC,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;KAAE,CAAC;CAChF,iBAwBP"}
@@ -0,0 +1,2 @@
1
+ export { type CreateSnapshotOptions, checkpointDatabase, countRows, createSnapshot, type RestoreSnapshotOptions, removeOldSnapshot, removeWalFiles, restoreSnapshot, type SnapshotVerify, verifySnapshot, } from "./snapshot";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/node/snapshot/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,qBAAqB,EAC1B,kBAAkB,EAClB,SAAS,EACT,cAAc,EACd,KAAK,sBAAsB,EAC3B,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,KAAK,cAAc,EACnB,cAAc,GACf,MAAM,YAAY,CAAC"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * SQLite "golden snapshot" utilities — node-only helpers for creating and
3
+ * restoring self-contained database snapshots (e.g. for E2E test fixtures).
4
+ *
5
+ * Pairs with {@link computeD1Filename}/{@link parseWranglerConfig} so callers
6
+ * can locate a miniflare D1 file and snapshot/restore it. Requires
7
+ * `better-sqlite3` (an optional peer dependency); these run in node tooling
8
+ * only, never in a Worker or browser bundle.
9
+ */
10
+ export interface SnapshotVerify {
11
+ /** A query selecting a single integer column aliased `count`. */
12
+ countQuery: string;
13
+ /** Minimum acceptable count (default 1). */
14
+ min?: number;
15
+ }
16
+ /** Remove the `-wal` and `-shm` sidecar files for a SQLite database, if present. */
17
+ export declare function removeWalFiles(dbPath: string): void;
18
+ /**
19
+ * Checkpoint a database file's WAL into its main file and close it, so the
20
+ * `.sqlite` file is self-contained and safe to copy without its sidecars.
21
+ */
22
+ export declare function checkpointDatabase(dbPath: string): void;
23
+ /**
24
+ * Count rows from a read-only query against a SQLite file. The query must
25
+ * select a single integer column aliased `count`. Does not mutate WAL/SHM.
26
+ */
27
+ export declare function countRows(dbPath: string, countQuery: string): number;
28
+ /**
29
+ * Verify a snapshot by running a count query, then clean up any WAL/SHM files
30
+ * created by opening it read-only. Returns the count.
31
+ */
32
+ export declare function verifySnapshot(snapshotPath: string, countQuery: string): number;
33
+ /** Delete an existing snapshot and its sidecars; ensure the parent dir exists. */
34
+ export declare function removeOldSnapshot(snapshotPath: string): void;
35
+ export interface CreateSnapshotOptions {
36
+ /** Verify the created snapshot has the expected data; throws if not. */
37
+ verify?: SnapshotVerify;
38
+ }
39
+ /**
40
+ * Create a self-contained snapshot of a live SQLite database.
41
+ *
42
+ * Checkpoints the source's WAL into its main file first (so the copy contains
43
+ * all committed data without needing the `-wal` sidecar), removes any stale
44
+ * snapshot, copies the main file, and optionally verifies the result.
45
+ */
46
+ export declare function createSnapshot(sourceDbPath: string, snapshotPath: string, options?: CreateSnapshotOptions): void;
47
+ export interface RestoreSnapshotOptions {
48
+ /** Reject the snapshot if smaller than this many bytes (corruption guard). */
49
+ minBytes?: number;
50
+ /** Verify the restored database after the swap; throws if not. */
51
+ verify?: SnapshotVerify;
52
+ }
53
+ /**
54
+ * Restore a snapshot onto a target database path atomically.
55
+ *
56
+ * Validates the snapshot exists (and meets `minBytes`), removes the target's
57
+ * stale WAL/SHM, copies the snapshot to a uniquely-named temp file, then
58
+ * atomically renames it into place — leaving no window where the target is
59
+ * missing or half-written, and no chance of SQLite replaying a stale journal
60
+ * against fresh data. Optionally verifies the restored data.
61
+ */
62
+ export declare function restoreSnapshot(snapshotPath: string, targetPath: string, options?: RestoreSnapshotOptions): void;
63
+ //# sourceMappingURL=snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../../../src/node/snapshot/snapshot.ts"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAEH,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,oFAAoF;AACpF,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAInD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAOvD;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAQpE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAM/E;AAED,kFAAkF;AAClF,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAO5D;AAED,MAAM,WAAW,qBAAqB;IACpC,wEAAwE;IACxE,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,GAAG,IAAI,CAkBpH;AAED,MAAM,WAAW,sBAAsB;IACrC,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,sBAA2B,GAAG,IAAI,CA+BpH"}
package/dist/node.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Node.js utilities for Sizzler.
3
+ *
4
+ * These utilities require Node.js APIs (fs, path) and should NOT be imported
5
+ * in Cloudflare Workers or browser code.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import {
10
+ * getLocalD1Path,
11
+ * createJSONSeeder,
12
+ * resetDatabase,
13
+ * } from "sizzler/node";
14
+ *
15
+ * // Get local D1 database path
16
+ * const dbPath = getLocalD1Path();
17
+ *
18
+ * // Create a JSON seeder
19
+ * const seeder = createJSONSeeder({
20
+ * baseDir: "./seed-data",
21
+ * });
22
+ * ```
23
+ */
24
+ export { computeD1Filename, getLocalD1Path, parseWranglerConfig, type WranglerConfig, } from "./node/connection";
25
+ export { type RunLocalMigrationsOptions, runLocalMigrations } from "./node/migrate";
26
+ export { commonTransformers, createJSONSeeder, type JSONSeederOptions, resetDatabase, } from "./node/seeding";
27
+ export { type CreateSnapshotOptions, checkpointDatabase, countRows, createSnapshot, type RestoreSnapshotOptions, removeOldSnapshot, removeWalFiles, restoreSnapshot, type SnapshotVerify, verifySnapshot, } from "./node/snapshot";
28
+ //# sourceMappingURL=node.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,KAAK,cAAc,GACpB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,KAAK,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAGpF,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,iBAAiB,EACtB,aAAa,GACd,MAAM,gBAAgB,CAAC;AAIxB,OAAO,EACL,KAAK,qBAAqB,EAC1B,kBAAkB,EAClB,SAAS,EACT,cAAc,EACd,KAAK,sBAAsB,EAC3B,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,KAAK,cAAc,EACnB,cAAc,GACf,MAAM,iBAAiB,CAAC"}
package/dist/node.js ADDED
@@ -0,0 +1,310 @@
1
+ // src/node/connection/d1-helpers.ts
2
+ import { createHash, createHmac } from "crypto";
3
+ import { readdirSync } from "fs";
4
+ import { join as join2 } from "path";
5
+
6
+ // src/node/connection/wrangler-config.ts
7
+ import { readFileSync } from "fs";
8
+ import { join } from "path";
9
+ function parseWranglerConfig(projectRoot = process.cwd()) {
10
+ let wranglerPath = join(projectRoot, "wrangler.json");
11
+ try {
12
+ readFileSync(wranglerPath, "utf8");
13
+ } catch {
14
+ wranglerPath = join(projectRoot, "wrangler.jsonc");
15
+ }
16
+ const content = readFileSync(wranglerPath, "utf8");
17
+ const cleanedContent = content.split("\n").map((line) => {
18
+ let inString = false;
19
+ let escaped = false;
20
+ for (let i = 0; i < line.length; i++) {
21
+ const char = line[i];
22
+ if (escaped) {
23
+ escaped = false;
24
+ continue;
25
+ }
26
+ if (char === "\\" && inString) {
27
+ escaped = true;
28
+ continue;
29
+ }
30
+ if (char === '"') {
31
+ inString = !inString;
32
+ continue;
33
+ }
34
+ if (!inString && line.substring(i, i + 2) === "//") {
35
+ return line.slice(0, i).trim();
36
+ }
37
+ }
38
+ return line;
39
+ }).join("\n").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,(\s*[}\]])/g, "$1");
40
+ return JSON.parse(cleanedContent);
41
+ }
42
+
43
+ // src/node/connection/d1-helpers.ts
44
+ function computeD1Filename(databaseId) {
45
+ const uniqueKey = "miniflare-D1DatabaseObject";
46
+ const key = createHash("sha256").update(uniqueKey).digest();
47
+ const nameHmac = createHmac("sha256", key).update(databaseId).digest().subarray(0, 16);
48
+ const hmac = createHmac("sha256", key).update(nameHmac).digest().subarray(0, 16);
49
+ return Buffer.concat([nameHmac, hmac]).toString("hex") + ".sqlite";
50
+ }
51
+ function getLocalD1Path(bindingName = "DB", projectRoot = process.cwd()) {
52
+ const stateDir = process.env.TEST_STATE_DIR;
53
+ const wranglerConfig = parseWranglerConfig(projectRoot);
54
+ const dbConfig = wranglerConfig.d1_databases?.find((d) => d.binding === bindingName);
55
+ if (!dbConfig) {
56
+ throw new Error(`Database binding '${bindingName}' not found in wrangler.json/wrangler.jsonc`);
57
+ }
58
+ const d1Dir = stateDir ? join2(projectRoot, `${stateDir}/v3/d1/miniflare-D1DatabaseObject`) : join2(projectRoot, ".wrangler/state/v3/d1/miniflare-D1DatabaseObject");
59
+ const expectedFilename = computeD1Filename(dbConfig.database_id);
60
+ const expectedPath = join2(d1Dir, expectedFilename);
61
+ try {
62
+ const files = readdirSync(d1Dir).filter((file) => file.endsWith(".sqlite"));
63
+ if (files.length === 0) {
64
+ throw new Error(`No D1 database found in ${d1Dir}. Run migrations first (npm run db:migrate).`);
65
+ }
66
+ if (files.includes(expectedFilename)) {
67
+ return expectedPath;
68
+ }
69
+ return join2(d1Dir, files[0]);
70
+ } catch (error) {
71
+ if (error instanceof Error && error.message.includes("No D1 database")) {
72
+ throw error;
73
+ }
74
+ throw new Error(`No D1 database directory found at ${d1Dir}. Run migrations first (npm run db:migrate).`);
75
+ }
76
+ }
77
+
78
+ // src/node/migrate.ts
79
+ import { mkdirSync } from "fs";
80
+ import { join as join3 } from "path";
81
+ import Database from "better-sqlite3";
82
+ import { drizzle } from "drizzle-orm/better-sqlite3";
83
+ import { migrate } from "drizzle-orm/better-sqlite3/migrator";
84
+ function runLocalMigrations(options) {
85
+ const { bindingName, projectRoot = process.cwd(), log = console.log } = options;
86
+ const migrationsFolder = options.migrationsFolder ?? join3(projectRoot, "drizzle", "migrations");
87
+ const stateDir = process.env.TEST_STATE_DIR;
88
+ const d1Dir = stateDir ? join3(projectRoot, stateDir, "v3", "d1", "miniflare-D1DatabaseObject") : join3(projectRoot, ".wrangler", "state", "v3", "d1", "miniflare-D1DatabaseObject");
89
+ mkdirSync(d1Dir, { recursive: true });
90
+ const dbConfig = parseWranglerConfig(projectRoot).d1_databases?.find((d) => d.binding === bindingName);
91
+ if (!dbConfig) {
92
+ throw new Error(`No D1 database binding '${bindingName}' found in wrangler config`);
93
+ }
94
+ const dbPath = join3(d1Dir, computeD1Filename(dbConfig.database_id));
95
+ log(`\u{1F4C2} Database: ${dbPath}`);
96
+ const sqlite = new Database(dbPath);
97
+ try {
98
+ migrate(drizzle({ client: sqlite }), { migrationsFolder });
99
+ log("\u2705 Migrations applied successfully");
100
+ } finally {
101
+ sqlite.close();
102
+ }
103
+ }
104
+
105
+ // src/node/seeding/json-seeder.ts
106
+ import fs from "fs";
107
+ import path from "path";
108
+ function applyTransformers(data, transformers) {
109
+ if (Array.isArray(data)) {
110
+ return data.map((item) => applyTransformers(item, transformers));
111
+ }
112
+ if (data && typeof data === "object") {
113
+ const result = {};
114
+ for (const [key, value] of Object.entries(data)) {
115
+ if (transformers[key]) {
116
+ result[key] = transformers[key](value);
117
+ } else {
118
+ result[key] = applyTransformers(value, transformers);
119
+ }
120
+ }
121
+ return result;
122
+ }
123
+ return data;
124
+ }
125
+ function createJSONSeeder(options) {
126
+ const { baseDir, transformers = {} } = options;
127
+ function loadJSON(relativePath) {
128
+ const filePath = path.join(baseDir, relativePath);
129
+ const content = fs.readFileSync(filePath, "utf-8");
130
+ const data = JSON.parse(content);
131
+ return applyTransformers(data, transformers);
132
+ }
133
+ function loadJSONDirectory(relativePath) {
134
+ const dirPath = path.join(baseDir, relativePath);
135
+ const files = fs.readdirSync(dirPath).filter((file) => file.endsWith(".json"));
136
+ return files.map((file) => loadJSON(path.join(relativePath, file)));
137
+ }
138
+ function fileExists(relativePath) {
139
+ return fs.existsSync(path.join(baseDir, relativePath));
140
+ }
141
+ return {
142
+ loadJSON,
143
+ loadJSONDirectory,
144
+ fileExists
145
+ };
146
+ }
147
+ var commonTransformers = {
148
+ /**
149
+ * Convert ISO date strings to Date objects.
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * const seeder = createJSONSeeder({
154
+ * baseDir: "./seed-data",
155
+ * transformers: commonTransformers.dateFields(["createdAt", "updatedAt"]),
156
+ * });
157
+ * ```
158
+ */
159
+ dateFields: (fieldNames) => {
160
+ const transformers = {};
161
+ fieldNames.forEach((field) => {
162
+ transformers[field] = (value) => new Date(value);
163
+ });
164
+ return transformers;
165
+ },
166
+ /**
167
+ * Convert string values to numbers.
168
+ *
169
+ * @example
170
+ * ```ts
171
+ * const seeder = createJSONSeeder({
172
+ * baseDir: "./seed-data",
173
+ * transformers: commonTransformers.numberFields(["age", "count"]),
174
+ * });
175
+ * ```
176
+ */
177
+ numberFields: (fieldNames) => {
178
+ const transformers = {};
179
+ fieldNames.forEach((field) => {
180
+ transformers[field] = (value) => Number(value);
181
+ });
182
+ return transformers;
183
+ }
184
+ };
185
+
186
+ // src/node/seeding/reset.ts
187
+ import { reset } from "drizzle-seed";
188
+ async function resetDatabase(db, schema, options = {}) {
189
+ const { confirm = false, verbose = false, logger = console } = options;
190
+ if (!confirm) {
191
+ throw new Error("Database reset requires explicit confirmation. Set confirm: true");
192
+ }
193
+ try {
194
+ if (verbose) {
195
+ logger.log("Resetting database...");
196
+ }
197
+ await reset(db, schema);
198
+ if (verbose) {
199
+ logger.log("Database reset completed successfully.");
200
+ }
201
+ } catch (error) {
202
+ if (verbose) {
203
+ logger.error("Database reset failed:", error);
204
+ }
205
+ throw error;
206
+ }
207
+ }
208
+
209
+ // src/node/snapshot/snapshot.ts
210
+ import { copyFileSync, existsSync, mkdirSync as mkdirSync2, renameSync, rmSync, statSync } from "fs";
211
+ import { dirname } from "path";
212
+ import Database2 from "better-sqlite3";
213
+ function removeWalFiles(dbPath) {
214
+ for (const suffix of ["-wal", "-shm"]) {
215
+ rmSync(`${dbPath}${suffix}`, { force: true });
216
+ }
217
+ }
218
+ function checkpointDatabase(dbPath) {
219
+ const db = new Database2(dbPath);
220
+ try {
221
+ db.pragma("wal_checkpoint(TRUNCATE)");
222
+ } finally {
223
+ db.close();
224
+ }
225
+ }
226
+ function countRows(dbPath, countQuery) {
227
+ const db = new Database2(dbPath, { readonly: true });
228
+ try {
229
+ const row = db.prepare(countQuery).get();
230
+ return row?.count ?? 0;
231
+ } finally {
232
+ db.close();
233
+ }
234
+ }
235
+ function verifySnapshot(snapshotPath, countQuery) {
236
+ try {
237
+ return countRows(snapshotPath, countQuery);
238
+ } finally {
239
+ removeWalFiles(snapshotPath);
240
+ }
241
+ }
242
+ function removeOldSnapshot(snapshotPath) {
243
+ const dir = dirname(snapshotPath);
244
+ if (!existsSync(dir)) mkdirSync2(dir, { recursive: true });
245
+ if (existsSync(snapshotPath)) {
246
+ rmSync(snapshotPath, { force: true });
247
+ removeWalFiles(snapshotPath);
248
+ }
249
+ }
250
+ function createSnapshot(sourceDbPath, snapshotPath, options = {}) {
251
+ if (!existsSync(sourceDbPath)) {
252
+ throw new Error(`Cannot create snapshot: source database not found at ${sourceDbPath}`);
253
+ }
254
+ checkpointDatabase(sourceDbPath);
255
+ removeOldSnapshot(snapshotPath);
256
+ copyFileSync(sourceDbPath, snapshotPath);
257
+ if (options.verify) {
258
+ const { countQuery, min = 1 } = options.verify;
259
+ const count = verifySnapshot(snapshotPath, countQuery);
260
+ if (count < min) {
261
+ throw new Error(
262
+ `Snapshot verification failed: \`${countQuery}\` returned ${count} (expected >= ${min}).
263
+ Snapshot: ${snapshotPath}`
264
+ );
265
+ }
266
+ }
267
+ }
268
+ function restoreSnapshot(snapshotPath, targetPath, options = {}) {
269
+ const { minBytes = 1, verify } = options;
270
+ if (!existsSync(snapshotPath)) {
271
+ throw new Error(`Cannot restore: snapshot not found at ${snapshotPath}`);
272
+ }
273
+ const size = statSync(snapshotPath).size;
274
+ if (size < minBytes) {
275
+ throw new Error(
276
+ `Cannot restore: snapshot at ${snapshotPath} is ${size} bytes (expected >= ${minBytes}). Likely corrupt \u2014 recreate it.`
277
+ );
278
+ }
279
+ mkdirSync2(dirname(targetPath), { recursive: true });
280
+ removeWalFiles(targetPath);
281
+ const tmpPath = `${targetPath}.tmp.${process.pid}.${Date.now()}`;
282
+ copyFileSync(snapshotPath, tmpPath);
283
+ renameSync(tmpPath, targetPath);
284
+ if (verify) {
285
+ const { countQuery, min = 1 } = verify;
286
+ const count = verifySnapshot(targetPath, countQuery);
287
+ if (count < min) {
288
+ throw new Error(
289
+ `Restore verification failed: \`${countQuery}\` returned ${count} (expected >= ${min}).
290
+ Target: ${targetPath}`
291
+ );
292
+ }
293
+ }
294
+ }
295
+ export {
296
+ checkpointDatabase,
297
+ commonTransformers,
298
+ computeD1Filename,
299
+ countRows,
300
+ createJSONSeeder,
301
+ createSnapshot,
302
+ getLocalD1Path,
303
+ parseWranglerConfig,
304
+ removeOldSnapshot,
305
+ removeWalFiles,
306
+ resetDatabase,
307
+ restoreSnapshot,
308
+ runLocalMigrations,
309
+ verifySnapshot
310
+ };
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@chromav/sizzler",
3
+ "version": "0.1.1",
4
+ "description": "Sizzling column factories and utilities for Drizzle ORM with SQLite, D1, and Turso",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./node": {
14
+ "import": "./dist/node.js",
15
+ "types": "./dist/node.d.ts"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",
23
+ "dev": "tsup --watch",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
26
+ "test:coverage": "vitest run --coverage",
27
+ "typecheck": "tsc --noEmit"
28
+ },
29
+ "peerDependencies": {
30
+ "better-sqlite3": ">=11.0.0",
31
+ "drizzle-orm": ">=0.30.0 || >=1.0.0-beta.1",
32
+ "drizzle-seed": ">=0.1.0"
33
+ },
34
+ "peerDependenciesMeta": {
35
+ "better-sqlite3": {
36
+ "optional": true
37
+ },
38
+ "drizzle-seed": {
39
+ "optional": true
40
+ }
41
+ },
42
+ "dependencies": {
43
+ "ulid": "^3.0.2"
44
+ },
45
+ "devDependencies": {
46
+ "@types/better-sqlite3": "^7.6.13",
47
+ "better-sqlite3": "^12.0.0",
48
+ "drizzle-orm": "1.0.0-beta.22",
49
+ "drizzle-seed": "^1.0.0-beta.22",
50
+ "tsup": "^8.5.1",
51
+ "typescript": "^6.0.3",
52
+ "vitest": "^4.1.5"
53
+ },
54
+ "keywords": [
55
+ "sizzler",
56
+ "drizzle",
57
+ "orm",
58
+ "sqlite",
59
+ "d1",
60
+ "cloudflare",
61
+ "turso",
62
+ "column-factories",
63
+ "ulid"
64
+ ],
65
+ "author": "Cobaltplan",
66
+ "license": "MIT",
67
+ "publishConfig": {
68
+ "access": "public"
69
+ },
70
+ "repository": {
71
+ "type": "git",
72
+ "url": "git+https://github.com/chroma-v/kindling.git",
73
+ "directory": "packages/sizzler"
74
+ },
75
+ "homepage": "https://github.com/chroma-v/kindling/tree/main/packages/sizzler#readme",
76
+ "bugs": {
77
+ "url": "https://github.com/chroma-v/kindling/issues"
78
+ }
79
+ }