@firtoz/drizzle-indexeddb 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI tool to generate IndexedDB migrations from Drizzle schema snapshots
4
+ * Run after `drizzle-kit generate` to create executable migration files
5
+ *
6
+ * Usage:
7
+ * bun drizzle-indexeddb-generate
8
+ */
9
+
10
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
11
+ import { join, resolve } from "node:path";
12
+ import type {
13
+ JournalEntry,
14
+ Journal,
15
+ Snapshot,
16
+ TableDefinition,
17
+ ColumnDefinition,
18
+ IndexDefinition,
19
+ } from "@firtoz/drizzle-utils";
20
+ import type { Migration, MigrationOperation } from "../function-migrator";
21
+
22
+ interface GenerateOptions {
23
+ drizzleDir?: string;
24
+ outputDir?: string;
25
+ }
26
+
27
+ function generateMigration(
28
+ _entry: JournalEntry,
29
+ snapshot: Snapshot,
30
+ prevSnapshot: Snapshot | null,
31
+ ): Migration {
32
+ const operations: MigrationOperation[] = [];
33
+
34
+ const currentTables: Record<string, TableDefinition> = snapshot.tables || {};
35
+ const previousTables: Record<string, TableDefinition> =
36
+ prevSnapshot?.tables || {};
37
+
38
+ // Find new tables
39
+ for (const [tableName, tableDef] of Object.entries(currentTables)) {
40
+ if (!previousTables[tableName]) {
41
+ // Find primary key
42
+ const pkColumn = Object.values(
43
+ tableDef.columns as Record<string, ColumnDefinition>,
44
+ ).find((col) => col.primaryKey);
45
+
46
+ const indexes: Array<{
47
+ name: string;
48
+ keyPath: string | string[];
49
+ unique?: boolean;
50
+ }> = [];
51
+
52
+ // Collect indexes
53
+ for (const [indexName, indexDef] of Object.entries(
54
+ tableDef.indexes || {},
55
+ )) {
56
+ indexes.push({
57
+ name: indexName,
58
+ keyPath:
59
+ indexDef.columns.length === 1
60
+ ? indexDef.columns[0]
61
+ : indexDef.columns,
62
+ unique: indexDef.isUnique,
63
+ });
64
+ }
65
+
66
+ operations.push({
67
+ type: "createTable",
68
+ name: tableName,
69
+ keyPath: pkColumn?.name,
70
+ autoIncrement: pkColumn?.autoincrement ?? false,
71
+ indexes: indexes.length > 0 ? indexes : undefined,
72
+ });
73
+ } else {
74
+ // Table exists, check for index changes
75
+ const prevTableDef: TableDefinition = previousTables[tableName];
76
+ const newIndexes: Record<string, IndexDefinition> =
77
+ tableDef.indexes || {};
78
+ const oldIndexes: Record<string, IndexDefinition> =
79
+ prevTableDef.indexes || {};
80
+
81
+ // Add new indexes
82
+ for (const [indexName, indexDef] of Object.entries(newIndexes)) {
83
+ if (!oldIndexes[indexName]) {
84
+ operations.push({
85
+ type: "createIndex",
86
+ tableName,
87
+ indexName,
88
+ keyPath:
89
+ indexDef.columns.length === 1
90
+ ? indexDef.columns[0]
91
+ : indexDef.columns,
92
+ unique: indexDef.isUnique,
93
+ });
94
+ }
95
+ }
96
+
97
+ // Delete removed indexes
98
+ for (const indexName of Object.keys(oldIndexes)) {
99
+ if (!newIndexes[indexName]) {
100
+ operations.push({
101
+ type: "deleteIndex",
102
+ tableName,
103
+ indexName,
104
+ });
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ // Find deleted tables
111
+ for (const tableName of Object.keys(previousTables)) {
112
+ if (!currentTables[tableName]) {
113
+ operations.push({
114
+ type: "deleteTable",
115
+ name: tableName,
116
+ });
117
+ }
118
+ }
119
+
120
+ return operations;
121
+ }
122
+
123
+ function migrationToCode(
124
+ migration: Migration,
125
+ idx: number,
126
+ tag: string,
127
+ ): string {
128
+ const migrationName = tag.replace(/^\d+_/, "").replace(/_/g, " ");
129
+ const lines: string[] = [];
130
+
131
+ lines.push(
132
+ `import type { Migration } from "@firtoz/drizzle-indexeddb";`,
133
+ ``,
134
+ `/**`,
135
+ ` * Migration: ${migrationName}`,
136
+ ` * Generated from: ${tag}`,
137
+ ` */`,
138
+ `export const migrate_${idx.toString().padStart(4, "0")}: Migration = ${JSON.stringify(migration, null, "\t")};`,
139
+ );
140
+
141
+ return lines.join("\n");
142
+ }
143
+
144
+ export function generateIndexedDBMigrations(
145
+ options: GenerateOptions = {},
146
+ ): void {
147
+ const cwd = process.cwd();
148
+ const drizzleDir = resolve(cwd, options.drizzleDir || "./drizzle");
149
+ const metaDir = join(drizzleDir, "meta");
150
+ const journalPath = join(metaDir, "_journal.json");
151
+ const outputDir = resolve(
152
+ cwd,
153
+ options.outputDir || join(drizzleDir, "indexeddb-migrations"),
154
+ );
155
+
156
+ const startTime = performance.now();
157
+ console.log(`[drizzle-indexeddb] Starting migration generation...`);
158
+
159
+ // Read the journal
160
+ if (!existsSync(journalPath)) {
161
+ console.error(
162
+ `[drizzle-indexeddb] Error: Journal not found at ${journalPath}`,
163
+ );
164
+ console.error(
165
+ `[drizzle-indexeddb] Make sure to run 'drizzle-kit generate' first`,
166
+ );
167
+ process.exit(1);
168
+ }
169
+
170
+ const journalContent = readFileSync(journalPath, "utf-8");
171
+ const journal: Journal = JSON.parse(journalContent);
172
+
173
+ console.log(`[drizzle-indexeddb] Found ${journal.entries.length} migrations`);
174
+
175
+ // Create output directory
176
+ if (!existsSync(outputDir)) {
177
+ mkdirSync(outputDir, { recursive: true });
178
+ console.log(`[drizzle-indexeddb] Created output directory: ${outputDir}`);
179
+ }
180
+
181
+ const migrationImports: string[] = [];
182
+ const migrationNames: string[] = [];
183
+
184
+ // Load all snapshots and generate migrations
185
+ const snapshots: Snapshot[] = [];
186
+
187
+ for (const entry of journal.entries) {
188
+ const fileName = `${entry.idx.toString().padStart(4, "0")}_snapshot.json`;
189
+ const snapshotPath = join(metaDir, fileName);
190
+
191
+ // Load snapshot
192
+ if (!existsSync(snapshotPath)) {
193
+ console.error(
194
+ `[drizzle-indexeddb] Error: Snapshot not found at ${snapshotPath}`,
195
+ );
196
+ process.exit(1);
197
+ }
198
+
199
+ const snapshotContent = readFileSync(snapshotPath, "utf-8");
200
+ const snapshot: Snapshot = JSON.parse(snapshotContent);
201
+ snapshots.push(snapshot);
202
+
203
+ // Generate migration
204
+ const prevSnapshot = entry.idx > 0 ? snapshots[entry.idx - 1] : null;
205
+ const migration = generateMigration(entry, snapshot, prevSnapshot);
206
+ const migrationCode = migrationToCode(migration, entry.idx, entry.tag);
207
+
208
+ const migrationFileName = `${entry.tag}.ts`;
209
+ const migrationPath = join(outputDir, migrationFileName);
210
+ writeFileSync(migrationPath, migrationCode, "utf-8");
211
+
212
+ // Add to index imports
213
+ const migrationName = `migrate_${entry.idx.toString().padStart(4, "0")}`;
214
+ migrationImports.push(`import { ${migrationName} } from './${entry.tag}';`);
215
+ migrationNames.push(migrationName);
216
+ }
217
+
218
+ // Generate index.ts for migrations
219
+ const indexContent = `import type { Migration } from "@firtoz/drizzle-indexeddb";
220
+
221
+ ${migrationImports.join("\n")}
222
+
223
+ export const migrations: Migration[] = [
224
+ \t${migrationNames.join(",\n\t")}
225
+ ];
226
+
227
+ export default migrations;
228
+ `;
229
+
230
+ writeFileSync(join(outputDir, "index.ts"), indexContent, "utf-8");
231
+ console.log(`[drizzle-indexeddb] ✓ Generated ${join(outputDir, "index.ts")}`);
232
+
233
+ const endTime = performance.now();
234
+ const totalTime = endTime - startTime;
235
+
236
+ console.log(`[drizzle-indexeddb] Migrations: ${migrationNames.join(", ")}`);
237
+ console.log(
238
+ `[drizzle-indexeddb] ✓ Complete! Generated ${journal.entries.length} migrations in ${totalTime.toFixed(2)}ms`,
239
+ );
240
+ }
241
+
242
+ // CLI entry point
243
+ function main(): void {
244
+ const args = process.argv.slice(2);
245
+ const command = args[0];
246
+
247
+ if (command === "generate" || command === undefined) {
248
+ // Parse options
249
+ const options: GenerateOptions = {};
250
+
251
+ for (let i = 1; i < args.length; i++) {
252
+ const arg = args[i];
253
+ if (arg === "--drizzle-dir" && args[i + 1]) {
254
+ options.drizzleDir = args[++i];
255
+ } else if (arg === "--output-dir" && args[i + 1]) {
256
+ options.outputDir = args[++i];
257
+ }
258
+ }
259
+
260
+ generateIndexedDBMigrations(options);
261
+ } else if (command === "--help" || command === "-h") {
262
+ console.log(`
263
+ drizzle-indexeddb-generate - Generate IndexedDB migrations from Drizzle schema
264
+
265
+ Usage:
266
+ bun drizzle-indexeddb-generate [options]
267
+
268
+ Options:
269
+ --drizzle-dir <path> Path to Drizzle directory (default: ./drizzle)
270
+ --output-dir <path> Path to output directory (default: ./drizzle/indexeddb-migrations)
271
+ -h, --help Show this help message
272
+
273
+ Examples:
274
+ bun drizzle-indexeddb-generate
275
+ bun drizzle-indexeddb-generate --drizzle-dir ./db/drizzle
276
+ bun drizzle-indexeddb-generate --output-dir ./src/migrations
277
+ `);
278
+ } else {
279
+ console.error(`Unknown command: ${command}`);
280
+ console.error(`Run 'bun drizzle-indexeddb-generate --help' for usage`);
281
+ process.exit(1);
282
+ }
283
+ }
284
+
285
+ // Only run CLI when executed directly (not when imported)
286
+ if (import.meta.main) {
287
+ main();
288
+ }