@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.
- package/CHANGELOG.md +70 -26
- package/README.md +274 -51
- package/package.json +14 -6
- package/src/bin/generate-migrations.ts +288 -0
- package/src/collections/indexeddb-collection.ts +97 -397
- package/src/context/useDrizzleIndexedDB.ts +2 -1
- package/src/function-migrator.ts +191 -170
- package/src/idb-interceptor.ts +75 -0
- package/src/idb-operations.ts +41 -0
- package/src/idb-types.ts +135 -0
- package/src/index.ts +57 -9
- package/src/instrumented-idb-database.ts +188 -0
- package/src/native-idb-database.ts +355 -0
- package/src/proxy/idb-proxy-client.ts +345 -0
- package/src/proxy/idb-proxy-server.ts +313 -0
- package/src/proxy/idb-proxy-transport.ts +174 -0
- package/src/proxy/idb-proxy-types.ts +77 -0
- package/src/proxy/idb-sync-adapter.ts +95 -0
- package/src/proxy/index.ts +37 -0
- package/src/snapshot-migrator.ts +0 -420
- package/src/utils.ts +0 -15
|
@@ -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
|
+
}
|