@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.
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/columns/foreign-key.d.ts +33 -0
- package/dist/columns/foreign-key.d.ts.map +1 -0
- package/dist/columns/helpers.d.ts +59 -0
- package/dist/columns/helpers.d.ts.map +1 -0
- package/dist/columns/index.d.ts +5 -0
- package/dist/columns/index.d.ts.map +1 -0
- package/dist/columns/primary-key.d.ts +16 -0
- package/dist/columns/primary-key.d.ts.map +1 -0
- package/dist/columns/timestamps.d.ts +65 -0
- package/dist/columns/timestamps.d.ts.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/node/connection/d1-helpers.d.ts +64 -0
- package/dist/node/connection/d1-helpers.d.ts.map +1 -0
- package/dist/node/connection/index.d.ts +3 -0
- package/dist/node/connection/index.d.ts.map +1 -0
- package/dist/node/connection/wrangler-config.d.ts +21 -0
- package/dist/node/connection/wrangler-config.d.ts.map +1 -0
- package/dist/node/index.d.ts +4 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/migrate.d.ts +22 -0
- package/dist/node/migrate.d.ts.map +1 -0
- package/dist/node/seeding/index.d.ts +3 -0
- package/dist/node/seeding/index.d.ts.map +1 -0
- package/dist/node/seeding/json-seeder.d.ts +59 -0
- package/dist/node/seeding/json-seeder.d.ts.map +1 -0
- package/dist/node/seeding/reset.d.ts +28 -0
- package/dist/node/seeding/reset.d.ts.map +1 -0
- package/dist/node/snapshot/index.d.ts +2 -0
- package/dist/node/snapshot/index.d.ts.map +1 -0
- package/dist/node/snapshot/snapshot.d.ts +63 -0
- package/dist/node/snapshot/snapshot.d.ts.map +1 -0
- package/dist/node.d.ts +28 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +310 -0
- 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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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
|
+
}
|