@typokit/db-drizzle 0.1.4
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/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +339 -0
- package/dist/index.js.map +1 -0
- package/package.json +29 -0
- package/src/index.test.ts +425 -0
- package/src/index.ts +426 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { SchemaTypeMap, GeneratedOutput, MigrationDraft } from "@typokit/types";
|
|
2
|
+
import type { DatabaseAdapter, DatabaseState } from "@typokit/core";
|
|
3
|
+
export type DrizzleDialect = "postgresql" | "sqlite";
|
|
4
|
+
export interface DrizzleAdapterOptions {
|
|
5
|
+
dialect?: DrizzleDialect;
|
|
6
|
+
outputDir?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Parse string union type like `"a" | "b" | "c"` into values */
|
|
9
|
+
export declare function parseUnionValues(typeStr: string): string[] | null;
|
|
10
|
+
export declare class DrizzleDatabaseAdapter implements DatabaseAdapter {
|
|
11
|
+
private readonly dialect;
|
|
12
|
+
private readonly outputDir;
|
|
13
|
+
constructor(options?: DrizzleAdapterOptions);
|
|
14
|
+
generate(types: SchemaTypeMap): GeneratedOutput[];
|
|
15
|
+
diff(types: SchemaTypeMap, currentState: DatabaseState): MigrationDraft;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,aAAa,EAEb,eAAe,EACf,cAAc,EAEf,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEpE,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,QAAQ,CAAC;AAErD,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA4BD,iEAAiE;AACjE,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAYjE;AA0UD,qBAAa,sBAAuB,YAAW,eAAe;IAC5D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,OAAO,CAAC,EAAE,qBAAqB;IAK3C,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,eAAe,EAAE;IAcjD,IAAI,CAAC,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,GAAG,cAAc;CAgBxE"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
// ─── Helpers ─────────────────────────────────────────────────
|
|
2
|
+
function toSnakeCase(str) {
|
|
3
|
+
return str
|
|
4
|
+
.replace(/([A-Z])/g, (_, letter, index) => index === 0 ? letter.toLowerCase() : `_${letter.toLowerCase()}`)
|
|
5
|
+
.replace(/__+/g, "_");
|
|
6
|
+
}
|
|
7
|
+
function pluralize(str) {
|
|
8
|
+
if (str.endsWith("s"))
|
|
9
|
+
return str;
|
|
10
|
+
if (str.endsWith("y") && !/[aeiou]y$/i.test(str))
|
|
11
|
+
return str.slice(0, -1) + "ies";
|
|
12
|
+
return str + "s";
|
|
13
|
+
}
|
|
14
|
+
function toTableName(typeName, jsdoc) {
|
|
15
|
+
if (jsdoc?.table)
|
|
16
|
+
return jsdoc.table;
|
|
17
|
+
return pluralize(toSnakeCase(typeName));
|
|
18
|
+
}
|
|
19
|
+
function toColumnName(propName) {
|
|
20
|
+
return toSnakeCase(propName);
|
|
21
|
+
}
|
|
22
|
+
/** Parse string union type like `"a" | "b" | "c"` into values */
|
|
23
|
+
export function parseUnionValues(typeStr) {
|
|
24
|
+
const trimmed = typeStr.trim();
|
|
25
|
+
if (!trimmed.includes("|"))
|
|
26
|
+
return null;
|
|
27
|
+
const parts = trimmed.split("|").map((p) => p.trim());
|
|
28
|
+
const values = [];
|
|
29
|
+
for (const part of parts) {
|
|
30
|
+
const match = /^"([^"]*)"$/.exec(part);
|
|
31
|
+
if (!match)
|
|
32
|
+
return null;
|
|
33
|
+
values.push(match[1]);
|
|
34
|
+
}
|
|
35
|
+
return values.length > 0 ? values : null;
|
|
36
|
+
}
|
|
37
|
+
function toEnumName(typeName, propName) {
|
|
38
|
+
return `${toSnakeCase(typeName)}_${toSnakeCase(propName)}`;
|
|
39
|
+
}
|
|
40
|
+
function toEnumVarName(typeName, propName) {
|
|
41
|
+
const base = typeName.charAt(0).toLowerCase() + typeName.slice(1);
|
|
42
|
+
return `${base}${propName.charAt(0).toUpperCase() + propName.slice(1)}Enum`;
|
|
43
|
+
}
|
|
44
|
+
function mapPgColumn(propName, prop, typeName) {
|
|
45
|
+
const col = toColumnName(propName);
|
|
46
|
+
const jsdoc = prop.jsdoc ?? {};
|
|
47
|
+
const imports = new Set();
|
|
48
|
+
let call;
|
|
49
|
+
let enumDef;
|
|
50
|
+
// Check for union → pgEnum
|
|
51
|
+
const unionValues = parseUnionValues(prop.type);
|
|
52
|
+
if (unionValues) {
|
|
53
|
+
const enumVarName = toEnumVarName(typeName, propName);
|
|
54
|
+
const enumDbName = toEnumName(typeName, propName);
|
|
55
|
+
imports.add("pgEnum");
|
|
56
|
+
enumDef = { varName: enumVarName, dbName: enumDbName, values: unionValues };
|
|
57
|
+
call = `${enumVarName}("${col}")`;
|
|
58
|
+
}
|
|
59
|
+
else if (prop.type === "string" &&
|
|
60
|
+
(jsdoc.id !== undefined || jsdoc.generated === "uuid")) {
|
|
61
|
+
imports.add("uuid");
|
|
62
|
+
call = `uuid("${col}")`;
|
|
63
|
+
}
|
|
64
|
+
else if (prop.type === "string" && jsdoc.maxLength) {
|
|
65
|
+
imports.add("varchar");
|
|
66
|
+
call = `varchar("${col}", { length: ${jsdoc.maxLength} })`;
|
|
67
|
+
}
|
|
68
|
+
else if (prop.type === "string" && jsdoc.format === "email") {
|
|
69
|
+
imports.add("varchar");
|
|
70
|
+
call = `varchar("${col}", { length: 255 })`;
|
|
71
|
+
}
|
|
72
|
+
else if (prop.type === "string") {
|
|
73
|
+
imports.add("text");
|
|
74
|
+
call = `text("${col}")`;
|
|
75
|
+
}
|
|
76
|
+
else if (prop.type === "number") {
|
|
77
|
+
imports.add("integer");
|
|
78
|
+
call = `integer("${col}")`;
|
|
79
|
+
}
|
|
80
|
+
else if (prop.type === "bigint") {
|
|
81
|
+
imports.add("bigint");
|
|
82
|
+
call = `bigint("${col}", { mode: "number" })`;
|
|
83
|
+
}
|
|
84
|
+
else if (prop.type === "boolean") {
|
|
85
|
+
imports.add("boolean");
|
|
86
|
+
call = `boolean("${col}")`;
|
|
87
|
+
}
|
|
88
|
+
else if (prop.type === "Date") {
|
|
89
|
+
imports.add("timestamp");
|
|
90
|
+
call = `timestamp("${col}")`;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// object, Record, unknown → jsonb
|
|
94
|
+
imports.add("jsonb");
|
|
95
|
+
call = `jsonb("${col}")`;
|
|
96
|
+
}
|
|
97
|
+
// Apply constraints
|
|
98
|
+
if (jsdoc.generated === "uuid") {
|
|
99
|
+
call += ".defaultRandom()";
|
|
100
|
+
}
|
|
101
|
+
if (jsdoc.generated === "now" || jsdoc.onUpdate === "now") {
|
|
102
|
+
call += ".defaultNow()";
|
|
103
|
+
}
|
|
104
|
+
if (jsdoc.id !== undefined) {
|
|
105
|
+
call += ".primaryKey()";
|
|
106
|
+
}
|
|
107
|
+
if (!prop.optional) {
|
|
108
|
+
call += ".notNull()";
|
|
109
|
+
}
|
|
110
|
+
if (jsdoc.unique !== undefined) {
|
|
111
|
+
call += ".unique()";
|
|
112
|
+
}
|
|
113
|
+
if (jsdoc.default !== undefined && jsdoc.generated === undefined) {
|
|
114
|
+
const defaultVal = jsdoc.default;
|
|
115
|
+
// Numeric defaults
|
|
116
|
+
if (/^\d+$/.test(defaultVal)) {
|
|
117
|
+
call += `.default(${defaultVal})`;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// Remove surrounding quotes if present
|
|
121
|
+
const cleaned = defaultVal.replace(/^["']|["']$/g, "");
|
|
122
|
+
call += `.default("${cleaned}")`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { drizzleCall: call, imports, enumDef };
|
|
126
|
+
}
|
|
127
|
+
// ─── SQLite Column Mapping ──────────────────────────────────
|
|
128
|
+
function mapSqliteColumn(propName, prop) {
|
|
129
|
+
const col = toColumnName(propName);
|
|
130
|
+
const jsdoc = prop.jsdoc ?? {};
|
|
131
|
+
const imports = new Set();
|
|
132
|
+
let call;
|
|
133
|
+
if (prop.type === "number" || prop.type === "bigint") {
|
|
134
|
+
imports.add("integer");
|
|
135
|
+
call = `integer("${col}")`;
|
|
136
|
+
}
|
|
137
|
+
else if (prop.type === "boolean") {
|
|
138
|
+
imports.add("integer");
|
|
139
|
+
call = `integer("${col}", { mode: "boolean" })`;
|
|
140
|
+
}
|
|
141
|
+
else if (prop.type === "Date" ||
|
|
142
|
+
prop.type === "object" ||
|
|
143
|
+
prop.type.startsWith("Record<") ||
|
|
144
|
+
parseUnionValues(prop.type)) {
|
|
145
|
+
imports.add("text");
|
|
146
|
+
call = `text("${col}")`;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
imports.add("text");
|
|
150
|
+
call = `text("${col}")`;
|
|
151
|
+
}
|
|
152
|
+
if (jsdoc.id !== undefined) {
|
|
153
|
+
call += ".primaryKey()";
|
|
154
|
+
}
|
|
155
|
+
if (!prop.optional) {
|
|
156
|
+
call += ".notNull()";
|
|
157
|
+
}
|
|
158
|
+
if (jsdoc.unique !== undefined) {
|
|
159
|
+
call += ".unique()";
|
|
160
|
+
}
|
|
161
|
+
if (jsdoc.default !== undefined) {
|
|
162
|
+
const defaultVal = jsdoc.default.replace(/^["']|["']$/g, "");
|
|
163
|
+
call += `.default("${defaultVal}")`;
|
|
164
|
+
}
|
|
165
|
+
return { drizzleCall: call, imports };
|
|
166
|
+
}
|
|
167
|
+
// ─── Code Generation ────────────────────────────────────────
|
|
168
|
+
function generatePgFile(typeName, meta) {
|
|
169
|
+
const tableName = toTableName(typeName, meta.jsdoc);
|
|
170
|
+
const tableVarName = tableName;
|
|
171
|
+
const allImports = new Set(["pgTable"]);
|
|
172
|
+
const enumDefs = [];
|
|
173
|
+
const columns = [];
|
|
174
|
+
for (const [propName, prop] of Object.entries(meta.properties)) {
|
|
175
|
+
const col = mapPgColumn(propName, prop, typeName);
|
|
176
|
+
for (const imp of col.imports)
|
|
177
|
+
allImports.add(imp);
|
|
178
|
+
if (col.enumDef)
|
|
179
|
+
enumDefs.push(col.enumDef);
|
|
180
|
+
columns.push({ name: propName, call: col.drizzleCall });
|
|
181
|
+
}
|
|
182
|
+
const importList = Array.from(allImports).sort().join(", ");
|
|
183
|
+
let code = `// AUTO-GENERATED by @typokit/db-drizzle from ${typeName} type\n`;
|
|
184
|
+
code += `// Do not edit manually — modify the source type instead\n\n`;
|
|
185
|
+
code += `import { ${importList} } from "drizzle-orm/pg-core";\n`;
|
|
186
|
+
// Enum definitions
|
|
187
|
+
for (const e of enumDefs) {
|
|
188
|
+
code += `\nexport const ${e.varName} = pgEnum("${e.dbName}", [${e.values.map((v) => `"${v}"`).join(", ")}]);\n`;
|
|
189
|
+
}
|
|
190
|
+
// Table definition
|
|
191
|
+
code += `\nexport const ${tableVarName} = pgTable("${tableName}", {\n`;
|
|
192
|
+
const columnLines = columns.map((c) => ` ${c.name}: ${c.call},`);
|
|
193
|
+
code += columnLines.join("\n") + "\n";
|
|
194
|
+
code += "});\n";
|
|
195
|
+
return {
|
|
196
|
+
filePath: `${tableName}.ts`,
|
|
197
|
+
content: code,
|
|
198
|
+
overwrite: true,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function generateSqliteFile(typeName, meta) {
|
|
202
|
+
const tableName = toTableName(typeName, meta.jsdoc);
|
|
203
|
+
const tableVarName = tableName;
|
|
204
|
+
const allImports = new Set(["sqliteTable"]);
|
|
205
|
+
const columns = [];
|
|
206
|
+
for (const [propName, prop] of Object.entries(meta.properties)) {
|
|
207
|
+
const col = mapSqliteColumn(propName, prop);
|
|
208
|
+
for (const imp of col.imports)
|
|
209
|
+
allImports.add(imp);
|
|
210
|
+
columns.push({ name: propName, call: col.drizzleCall });
|
|
211
|
+
}
|
|
212
|
+
const importList = Array.from(allImports).sort().join(", ");
|
|
213
|
+
let code = `// AUTO-GENERATED by @typokit/db-drizzle from ${typeName} type\n`;
|
|
214
|
+
code += `// Do not edit manually — modify the source type instead\n\n`;
|
|
215
|
+
code += `import { ${importList} } from "drizzle-orm/sqlite-core";\n`;
|
|
216
|
+
code += `\nexport const ${tableVarName} = sqliteTable("${tableName}", {\n`;
|
|
217
|
+
const columnLines = columns.map((c) => ` ${c.name}: ${c.call},`);
|
|
218
|
+
code += columnLines.join("\n") + "\n";
|
|
219
|
+
code += "});\n";
|
|
220
|
+
return {
|
|
221
|
+
filePath: `${tableName}.ts`,
|
|
222
|
+
content: code,
|
|
223
|
+
overwrite: true,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
// ─── Diff Logic ─────────────────────────────────────────────
|
|
227
|
+
function diffTypes(types, currentState) {
|
|
228
|
+
const changes = [];
|
|
229
|
+
for (const [typeName, meta] of Object.entries(types)) {
|
|
230
|
+
const tableName = toTableName(typeName, meta.jsdoc);
|
|
231
|
+
const existing = currentState.tables[tableName];
|
|
232
|
+
if (!existing) {
|
|
233
|
+
changes.push({ type: "add", entity: tableName });
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
for (const [propName, prop] of Object.entries(meta.properties)) {
|
|
237
|
+
const colName = toColumnName(propName);
|
|
238
|
+
const existingCol = existing.columns[colName];
|
|
239
|
+
if (!existingCol) {
|
|
240
|
+
changes.push({
|
|
241
|
+
type: "add",
|
|
242
|
+
entity: tableName,
|
|
243
|
+
field: colName,
|
|
244
|
+
details: { tsType: prop.type },
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
const nullable = prop.optional;
|
|
249
|
+
if (existingCol.nullable !== nullable) {
|
|
250
|
+
changes.push({
|
|
251
|
+
type: "modify",
|
|
252
|
+
entity: tableName,
|
|
253
|
+
field: colName,
|
|
254
|
+
details: {
|
|
255
|
+
nullableFrom: existingCol.nullable,
|
|
256
|
+
nullableTo: nullable,
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Detect removed columns
|
|
263
|
+
for (const colName of Object.keys(existing.columns)) {
|
|
264
|
+
const hasProp = Object.keys(meta.properties).some((p) => toColumnName(p) === colName);
|
|
265
|
+
if (!hasProp) {
|
|
266
|
+
changes.push({ type: "remove", entity: tableName, field: colName });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// Detect removed tables
|
|
271
|
+
for (const tableName of Object.keys(currentState.tables)) {
|
|
272
|
+
const hasType = Object.entries(types).some(([name, meta]) => toTableName(name, meta.jsdoc) === tableName);
|
|
273
|
+
if (!hasType) {
|
|
274
|
+
changes.push({ type: "remove", entity: tableName });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return changes;
|
|
278
|
+
}
|
|
279
|
+
function generateMigrationSql(changes, dialect) {
|
|
280
|
+
const lines = [];
|
|
281
|
+
for (const change of changes) {
|
|
282
|
+
if (change.type === "add" && !change.field) {
|
|
283
|
+
lines.push(`-- TODO: CREATE TABLE "${change.entity}" (define columns)`);
|
|
284
|
+
}
|
|
285
|
+
else if (change.type === "add" && change.field) {
|
|
286
|
+
lines.push(`ALTER TABLE "${change.entity}" ADD COLUMN "${change.field}" TEXT;`);
|
|
287
|
+
}
|
|
288
|
+
else if (change.type === "remove" && !change.field) {
|
|
289
|
+
lines.push(`DROP TABLE IF EXISTS "${change.entity}";`);
|
|
290
|
+
}
|
|
291
|
+
else if (change.type === "remove" && change.field) {
|
|
292
|
+
if (dialect === "postgresql") {
|
|
293
|
+
lines.push(`ALTER TABLE "${change.entity}" DROP COLUMN "${change.field}";`);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
lines.push(`-- SQLite: cannot DROP COLUMN "${change.field}" from "${change.entity}" — recreate table`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else if (change.type === "modify") {
|
|
300
|
+
lines.push(`-- TODO: ALTER TABLE "${change.entity}" modify column "${change.field}"`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return lines.join("\n");
|
|
304
|
+
}
|
|
305
|
+
// ─── Adapter ────────────────────────────────────────────────
|
|
306
|
+
export class DrizzleDatabaseAdapter {
|
|
307
|
+
dialect;
|
|
308
|
+
outputDir;
|
|
309
|
+
constructor(options) {
|
|
310
|
+
this.dialect = options?.dialect ?? "postgresql";
|
|
311
|
+
this.outputDir = options?.outputDir ?? "drizzle";
|
|
312
|
+
}
|
|
313
|
+
generate(types) {
|
|
314
|
+
const outputs = [];
|
|
315
|
+
const genFn = this.dialect === "postgresql" ? generatePgFile : generateSqliteFile;
|
|
316
|
+
for (const [typeName, meta] of Object.entries(types)) {
|
|
317
|
+
const output = genFn(typeName, meta);
|
|
318
|
+
output.filePath = `${this.outputDir}/${output.filePath}`;
|
|
319
|
+
outputs.push(output);
|
|
320
|
+
}
|
|
321
|
+
return outputs;
|
|
322
|
+
}
|
|
323
|
+
diff(types, currentState) {
|
|
324
|
+
const changes = diffTypes(types, currentState);
|
|
325
|
+
const destructive = changes.some((c) => c.type === "remove");
|
|
326
|
+
const sql = generateMigrationSql(changes, this.dialect);
|
|
327
|
+
const timestamp = new Date()
|
|
328
|
+
.toISOString()
|
|
329
|
+
.replace(/[-:T]/g, "")
|
|
330
|
+
.slice(0, 14);
|
|
331
|
+
return {
|
|
332
|
+
name: `${timestamp}_schema_update`,
|
|
333
|
+
sql,
|
|
334
|
+
destructive,
|
|
335
|
+
changes,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@typokit/db-drizzle",
|
|
3
|
+
"exports": {
|
|
4
|
+
".": {
|
|
5
|
+
"import": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts"
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"version": "0.1.4",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"src"
|
|
14
|
+
],
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@typokit/types": "0.1.4",
|
|
19
|
+
"@typokit/core": "0.1.4"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/KyleBastien/typokit",
|
|
24
|
+
"directory": "packages/db-drizzle"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "rstest run --passWithNoTests"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { describe, it, expect } from "@rstest/core";
|
|
2
|
+
import { DrizzleDatabaseAdapter, parseUnionValues } from "./index.js";
|
|
3
|
+
import type { SchemaTypeMap } from "@typokit/types";
|
|
4
|
+
import type { DatabaseState } from "@typokit/core";
|
|
5
|
+
|
|
6
|
+
describe("DrizzleDatabaseAdapter", () => {
|
|
7
|
+
describe("parseUnionValues", () => {
|
|
8
|
+
it("should parse string union types", () => {
|
|
9
|
+
const values = parseUnionValues('"active" | "suspended" | "deleted"');
|
|
10
|
+
expect(values).toEqual(["active", "suspended", "deleted"]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should return null for non-union types", () => {
|
|
14
|
+
expect(parseUnionValues("string")).toBeNull();
|
|
15
|
+
expect(parseUnionValues("number")).toBeNull();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should return null for mixed unions", () => {
|
|
19
|
+
expect(parseUnionValues('"a" | number')).toBeNull();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("generate() - PostgreSQL", () => {
|
|
24
|
+
const adapter = new DrizzleDatabaseAdapter({ dialect: "postgresql" });
|
|
25
|
+
|
|
26
|
+
it("should generate Drizzle schema from User type", () => {
|
|
27
|
+
const types: SchemaTypeMap = {
|
|
28
|
+
User: {
|
|
29
|
+
name: "User",
|
|
30
|
+
jsdoc: { table: "users" },
|
|
31
|
+
properties: {
|
|
32
|
+
id: {
|
|
33
|
+
type: "string",
|
|
34
|
+
optional: false,
|
|
35
|
+
jsdoc: { id: "", generated: "uuid" },
|
|
36
|
+
},
|
|
37
|
+
email: {
|
|
38
|
+
type: "string",
|
|
39
|
+
optional: false,
|
|
40
|
+
jsdoc: { format: "email", unique: "" },
|
|
41
|
+
},
|
|
42
|
+
displayName: {
|
|
43
|
+
type: "string",
|
|
44
|
+
optional: false,
|
|
45
|
+
jsdoc: { maxLength: "100" },
|
|
46
|
+
},
|
|
47
|
+
status: {
|
|
48
|
+
type: '"active" | "suspended" | "deleted"',
|
|
49
|
+
optional: false,
|
|
50
|
+
jsdoc: { default: "active" },
|
|
51
|
+
},
|
|
52
|
+
createdAt: {
|
|
53
|
+
type: "Date",
|
|
54
|
+
optional: false,
|
|
55
|
+
jsdoc: { generated: "now" },
|
|
56
|
+
},
|
|
57
|
+
updatedAt: {
|
|
58
|
+
type: "Date",
|
|
59
|
+
optional: false,
|
|
60
|
+
jsdoc: { onUpdate: "now" },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const outputs = adapter.generate(types);
|
|
67
|
+
expect(outputs).toHaveLength(1);
|
|
68
|
+
|
|
69
|
+
const output = outputs[0];
|
|
70
|
+
expect(output.filePath).toBe("drizzle/users.ts");
|
|
71
|
+
expect(output.overwrite).toBe(true);
|
|
72
|
+
|
|
73
|
+
const content = output.content;
|
|
74
|
+
|
|
75
|
+
// Header
|
|
76
|
+
expect(content).toContain("// AUTO-GENERATED by @typokit/db-drizzle");
|
|
77
|
+
|
|
78
|
+
// Imports
|
|
79
|
+
expect(content).toContain('from "drizzle-orm/pg-core"');
|
|
80
|
+
expect(content).toContain("pgTable");
|
|
81
|
+
expect(content).toContain("pgEnum");
|
|
82
|
+
expect(content).toContain("uuid");
|
|
83
|
+
expect(content).toContain("varchar");
|
|
84
|
+
expect(content).toContain("timestamp");
|
|
85
|
+
|
|
86
|
+
// Enum
|
|
87
|
+
expect(content).toContain("userStatusEnum");
|
|
88
|
+
expect(content).toContain('pgEnum("user_status"');
|
|
89
|
+
expect(content).toContain('"active", "suspended", "deleted"');
|
|
90
|
+
|
|
91
|
+
// Table
|
|
92
|
+
expect(content).toContain('pgTable("users"');
|
|
93
|
+
|
|
94
|
+
// Columns
|
|
95
|
+
expect(content).toContain('uuid("id")');
|
|
96
|
+
expect(content).toContain(".defaultRandom()");
|
|
97
|
+
expect(content).toContain(".primaryKey()");
|
|
98
|
+
|
|
99
|
+
expect(content).toContain('varchar("email", { length: 255 })');
|
|
100
|
+
expect(content).toContain(".unique()");
|
|
101
|
+
|
|
102
|
+
expect(content).toContain('varchar("display_name", { length: 100 })');
|
|
103
|
+
|
|
104
|
+
expect(content).toContain('userStatusEnum("status")');
|
|
105
|
+
expect(content).toContain('.default("active")');
|
|
106
|
+
|
|
107
|
+
expect(content).toContain('timestamp("created_at")');
|
|
108
|
+
expect(content).toContain(".defaultNow()");
|
|
109
|
+
|
|
110
|
+
expect(content).toContain('timestamp("updated_at")');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should handle all supported column types", () => {
|
|
114
|
+
const types: SchemaTypeMap = {
|
|
115
|
+
Product: {
|
|
116
|
+
name: "Product",
|
|
117
|
+
properties: {
|
|
118
|
+
id: {
|
|
119
|
+
type: "string",
|
|
120
|
+
optional: false,
|
|
121
|
+
jsdoc: { id: "", generated: "uuid" },
|
|
122
|
+
},
|
|
123
|
+
name: { type: "string", optional: false },
|
|
124
|
+
price: { type: "number", optional: false },
|
|
125
|
+
stock: { type: "bigint", optional: false },
|
|
126
|
+
active: { type: "boolean", optional: false },
|
|
127
|
+
metadata: { type: "Record<string, unknown>", optional: true },
|
|
128
|
+
description: { type: "string", optional: true },
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const outputs = adapter.generate(types);
|
|
134
|
+
expect(outputs).toHaveLength(1);
|
|
135
|
+
const content = outputs[0].content;
|
|
136
|
+
|
|
137
|
+
expect(content).toContain('uuid("id")');
|
|
138
|
+
expect(content).toContain('text("name")');
|
|
139
|
+
expect(content).toContain('integer("price")');
|
|
140
|
+
expect(content).toContain('bigint("stock", { mode: "number" })');
|
|
141
|
+
expect(content).toContain('boolean("active")');
|
|
142
|
+
expect(content).toContain('jsonb("metadata")');
|
|
143
|
+
expect(content).toContain('text("description")');
|
|
144
|
+
|
|
145
|
+
// Required fields have .notNull()
|
|
146
|
+
expect(content).toMatch(/text\("name"\)\.notNull\(\)/);
|
|
147
|
+
// Optional fields do NOT have .notNull()
|
|
148
|
+
expect(content).not.toMatch(/jsonb\("metadata"\)\.notNull\(\)/);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should use @table JSDoc tag for table name", () => {
|
|
152
|
+
const types: SchemaTypeMap = {
|
|
153
|
+
UserAccount: {
|
|
154
|
+
name: "UserAccount",
|
|
155
|
+
jsdoc: { table: "custom_accounts" },
|
|
156
|
+
properties: {
|
|
157
|
+
id: { type: "string", optional: false, jsdoc: { id: "" } },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const outputs = adapter.generate(types);
|
|
163
|
+
expect(outputs[0].content).toContain('pgTable("custom_accounts"');
|
|
164
|
+
expect(outputs[0].filePath).toBe("drizzle/custom_accounts.ts");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should pluralize table names from type names", () => {
|
|
168
|
+
const types: SchemaTypeMap = {
|
|
169
|
+
Category: {
|
|
170
|
+
name: "Category",
|
|
171
|
+
properties: {
|
|
172
|
+
id: { type: "string", optional: false, jsdoc: { id: "" } },
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const outputs = adapter.generate(types);
|
|
178
|
+
expect(outputs[0].content).toContain('pgTable("categories"');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("should generate enum for string union types", () => {
|
|
182
|
+
const types: SchemaTypeMap = {
|
|
183
|
+
Order: {
|
|
184
|
+
name: "Order",
|
|
185
|
+
properties: {
|
|
186
|
+
status: {
|
|
187
|
+
type: '"pending" | "shipped" | "delivered"',
|
|
188
|
+
optional: false,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const outputs = adapter.generate(types);
|
|
195
|
+
const content = outputs[0].content;
|
|
196
|
+
expect(content).toContain("pgEnum");
|
|
197
|
+
expect(content).toContain("orderStatusEnum");
|
|
198
|
+
expect(content).toContain('"pending", "shipped", "delivered"');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("should handle @default with numeric values", () => {
|
|
202
|
+
const types: SchemaTypeMap = {
|
|
203
|
+
Config: {
|
|
204
|
+
name: "Config",
|
|
205
|
+
properties: {
|
|
206
|
+
retryCount: {
|
|
207
|
+
type: "number",
|
|
208
|
+
optional: false,
|
|
209
|
+
jsdoc: { default: "3" },
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const outputs = adapter.generate(types);
|
|
216
|
+
expect(outputs[0].content).toContain(".default(3)");
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe("generate() - SQLite", () => {
|
|
221
|
+
const adapter = new DrizzleDatabaseAdapter({ dialect: "sqlite" });
|
|
222
|
+
|
|
223
|
+
it("should generate SQLite schema", () => {
|
|
224
|
+
const types: SchemaTypeMap = {
|
|
225
|
+
User: {
|
|
226
|
+
name: "User",
|
|
227
|
+
jsdoc: { table: "users" },
|
|
228
|
+
properties: {
|
|
229
|
+
id: { type: "string", optional: false, jsdoc: { id: "" } },
|
|
230
|
+
name: { type: "string", optional: false },
|
|
231
|
+
age: { type: "number", optional: false },
|
|
232
|
+
active: { type: "boolean", optional: false },
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const outputs = adapter.generate(types);
|
|
238
|
+
expect(outputs).toHaveLength(1);
|
|
239
|
+
const content = outputs[0].content;
|
|
240
|
+
|
|
241
|
+
expect(content).toContain('from "drizzle-orm/sqlite-core"');
|
|
242
|
+
expect(content).toContain("sqliteTable");
|
|
243
|
+
expect(content).toContain('text("id")');
|
|
244
|
+
expect(content).toContain('text("name")');
|
|
245
|
+
expect(content).toContain('integer("age")');
|
|
246
|
+
expect(content).toContain('integer("active", { mode: "boolean" })');
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe("diff()", () => {
|
|
251
|
+
const adapter = new DrizzleDatabaseAdapter({ dialect: "postgresql" });
|
|
252
|
+
|
|
253
|
+
it("should detect new tables", () => {
|
|
254
|
+
const types: SchemaTypeMap = {
|
|
255
|
+
User: {
|
|
256
|
+
name: "User",
|
|
257
|
+
jsdoc: { table: "users" },
|
|
258
|
+
properties: {
|
|
259
|
+
id: { type: "string", optional: false },
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
const state: DatabaseState = { tables: {} };
|
|
264
|
+
|
|
265
|
+
const draft = adapter.diff(types, state);
|
|
266
|
+
expect(draft.changes).toHaveLength(1);
|
|
267
|
+
expect(draft.changes[0]).toEqual({ type: "add", entity: "users" });
|
|
268
|
+
expect(draft.destructive).toBe(false);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("should detect new columns", () => {
|
|
272
|
+
const types: SchemaTypeMap = {
|
|
273
|
+
User: {
|
|
274
|
+
name: "User",
|
|
275
|
+
jsdoc: { table: "users" },
|
|
276
|
+
properties: {
|
|
277
|
+
id: { type: "string", optional: false },
|
|
278
|
+
email: { type: "string", optional: false },
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
const state: DatabaseState = {
|
|
283
|
+
tables: {
|
|
284
|
+
users: {
|
|
285
|
+
columns: {
|
|
286
|
+
id: { type: "text", nullable: false },
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const draft = adapter.diff(types, state);
|
|
293
|
+
expect(draft.changes).toHaveLength(1);
|
|
294
|
+
expect(draft.changes[0].type).toBe("add");
|
|
295
|
+
expect(draft.changes[0].field).toBe("email");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("should detect removed columns", () => {
|
|
299
|
+
const types: SchemaTypeMap = {
|
|
300
|
+
User: {
|
|
301
|
+
name: "User",
|
|
302
|
+
jsdoc: { table: "users" },
|
|
303
|
+
properties: {
|
|
304
|
+
id: { type: "string", optional: false },
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
const state: DatabaseState = {
|
|
309
|
+
tables: {
|
|
310
|
+
users: {
|
|
311
|
+
columns: {
|
|
312
|
+
id: { type: "text", nullable: false },
|
|
313
|
+
old_col: { type: "text", nullable: true },
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const draft = adapter.diff(types, state);
|
|
320
|
+
expect(draft.changes).toHaveLength(1);
|
|
321
|
+
expect(draft.changes[0]).toEqual({
|
|
322
|
+
type: "remove",
|
|
323
|
+
entity: "users",
|
|
324
|
+
field: "old_col",
|
|
325
|
+
});
|
|
326
|
+
expect(draft.destructive).toBe(true);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("should detect removed tables", () => {
|
|
330
|
+
const types: SchemaTypeMap = {};
|
|
331
|
+
const state: DatabaseState = {
|
|
332
|
+
tables: {
|
|
333
|
+
old_table: { columns: { id: { type: "text", nullable: false } } },
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const draft = adapter.diff(types, state);
|
|
338
|
+
expect(draft.changes).toHaveLength(1);
|
|
339
|
+
expect(draft.changes[0]).toEqual({ type: "remove", entity: "old_table" });
|
|
340
|
+
expect(draft.destructive).toBe(true);
|
|
341
|
+
expect(draft.sql).toContain("DROP TABLE");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("should detect nullable changes", () => {
|
|
345
|
+
const types: SchemaTypeMap = {
|
|
346
|
+
User: {
|
|
347
|
+
name: "User",
|
|
348
|
+
jsdoc: { table: "users" },
|
|
349
|
+
properties: {
|
|
350
|
+
email: { type: "string", optional: true },
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
const state: DatabaseState = {
|
|
355
|
+
tables: {
|
|
356
|
+
users: {
|
|
357
|
+
columns: {
|
|
358
|
+
email: { type: "text", nullable: false },
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const draft = adapter.diff(types, state);
|
|
365
|
+
expect(draft.changes).toHaveLength(1);
|
|
366
|
+
expect(draft.changes[0].type).toBe("modify");
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("should generate migration SQL", () => {
|
|
370
|
+
const types: SchemaTypeMap = {
|
|
371
|
+
User: {
|
|
372
|
+
name: "User",
|
|
373
|
+
jsdoc: { table: "users" },
|
|
374
|
+
properties: {
|
|
375
|
+
id: { type: "string", optional: false },
|
|
376
|
+
newField: { type: "string", optional: false },
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
const state: DatabaseState = {
|
|
381
|
+
tables: {
|
|
382
|
+
users: {
|
|
383
|
+
columns: {
|
|
384
|
+
id: { type: "text", nullable: false },
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const draft = adapter.diff(types, state);
|
|
391
|
+
expect(draft.sql).toContain(
|
|
392
|
+
'ALTER TABLE "users" ADD COLUMN "new_field" TEXT;',
|
|
393
|
+
);
|
|
394
|
+
expect(draft.name).toMatch(/^\d{14}_schema_update$/);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
describe("constructor options", () => {
|
|
399
|
+
it("should use custom output directory", () => {
|
|
400
|
+
const adapter = new DrizzleDatabaseAdapter({ outputDir: "db/schema" });
|
|
401
|
+
const types: SchemaTypeMap = {
|
|
402
|
+
Item: {
|
|
403
|
+
name: "Item",
|
|
404
|
+
properties: { id: { type: "string", optional: false } },
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const outputs = adapter.generate(types);
|
|
409
|
+
expect(outputs[0].filePath).toContain("db/schema/");
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("should default to postgresql dialect", () => {
|
|
413
|
+
const adapter = new DrizzleDatabaseAdapter();
|
|
414
|
+
const types: SchemaTypeMap = {
|
|
415
|
+
Item: {
|
|
416
|
+
name: "Item",
|
|
417
|
+
properties: { id: { type: "string", optional: false } },
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const outputs = adapter.generate(types);
|
|
422
|
+
expect(outputs[0].content).toContain("drizzle-orm/pg-core");
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
// @typokit/db-drizzle — Drizzle Schema Generation
|
|
2
|
+
import type {
|
|
3
|
+
SchemaTypeMap,
|
|
4
|
+
TypeMetadata,
|
|
5
|
+
GeneratedOutput,
|
|
6
|
+
MigrationDraft,
|
|
7
|
+
SchemaChange,
|
|
8
|
+
} from "@typokit/types";
|
|
9
|
+
import type { DatabaseAdapter, DatabaseState } from "@typokit/core";
|
|
10
|
+
|
|
11
|
+
export type DrizzleDialect = "postgresql" | "sqlite";
|
|
12
|
+
|
|
13
|
+
export interface DrizzleAdapterOptions {
|
|
14
|
+
dialect?: DrizzleDialect;
|
|
15
|
+
outputDir?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ─── Helpers ─────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function toSnakeCase(str: string): string {
|
|
21
|
+
return str
|
|
22
|
+
.replace(/([A-Z])/g, (_, letter, index) =>
|
|
23
|
+
index === 0 ? letter.toLowerCase() : `_${letter.toLowerCase()}`,
|
|
24
|
+
)
|
|
25
|
+
.replace(/__+/g, "_");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function pluralize(str: string): string {
|
|
29
|
+
if (str.endsWith("s")) return str;
|
|
30
|
+
if (str.endsWith("y") && !/[aeiou]y$/i.test(str))
|
|
31
|
+
return str.slice(0, -1) + "ies";
|
|
32
|
+
return str + "s";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function toTableName(typeName: string, jsdoc?: Record<string, string>): string {
|
|
36
|
+
if (jsdoc?.table) return jsdoc.table;
|
|
37
|
+
return pluralize(toSnakeCase(typeName));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toColumnName(propName: string): string {
|
|
41
|
+
return toSnakeCase(propName);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Parse string union type like `"a" | "b" | "c"` into values */
|
|
45
|
+
export function parseUnionValues(typeStr: string): string[] | null {
|
|
46
|
+
const trimmed = typeStr.trim();
|
|
47
|
+
if (!trimmed.includes("|")) return null;
|
|
48
|
+
|
|
49
|
+
const parts = trimmed.split("|").map((p) => p.trim());
|
|
50
|
+
const values: string[] = [];
|
|
51
|
+
for (const part of parts) {
|
|
52
|
+
const match = /^"([^"]*)"$/.exec(part);
|
|
53
|
+
if (!match) return null;
|
|
54
|
+
values.push(match[1]);
|
|
55
|
+
}
|
|
56
|
+
return values.length > 0 ? values : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function toEnumName(typeName: string, propName: string): string {
|
|
60
|
+
return `${toSnakeCase(typeName)}_${toSnakeCase(propName)}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toEnumVarName(typeName: string, propName: string): string {
|
|
64
|
+
const base = typeName.charAt(0).toLowerCase() + typeName.slice(1);
|
|
65
|
+
return `${base}${propName.charAt(0).toUpperCase() + propName.slice(1)}Enum`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── PostgreSQL Column Mapping ──────────────────────────────
|
|
69
|
+
|
|
70
|
+
interface ColumnInfo {
|
|
71
|
+
drizzleCall: string;
|
|
72
|
+
imports: Set<string>;
|
|
73
|
+
enumDef?: { varName: string; dbName: string; values: string[] };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function mapPgColumn(
|
|
77
|
+
propName: string,
|
|
78
|
+
prop: { type: string; optional: boolean; jsdoc?: Record<string, string> },
|
|
79
|
+
typeName: string,
|
|
80
|
+
): ColumnInfo {
|
|
81
|
+
const col = toColumnName(propName);
|
|
82
|
+
const jsdoc = prop.jsdoc ?? {};
|
|
83
|
+
const imports = new Set<string>();
|
|
84
|
+
let call: string;
|
|
85
|
+
let enumDef: ColumnInfo["enumDef"] | undefined;
|
|
86
|
+
|
|
87
|
+
// Check for union → pgEnum
|
|
88
|
+
const unionValues = parseUnionValues(prop.type);
|
|
89
|
+
if (unionValues) {
|
|
90
|
+
const enumVarName = toEnumVarName(typeName, propName);
|
|
91
|
+
const enumDbName = toEnumName(typeName, propName);
|
|
92
|
+
imports.add("pgEnum");
|
|
93
|
+
enumDef = { varName: enumVarName, dbName: enumDbName, values: unionValues };
|
|
94
|
+
call = `${enumVarName}("${col}")`;
|
|
95
|
+
} else if (
|
|
96
|
+
prop.type === "string" &&
|
|
97
|
+
(jsdoc.id !== undefined || jsdoc.generated === "uuid")
|
|
98
|
+
) {
|
|
99
|
+
imports.add("uuid");
|
|
100
|
+
call = `uuid("${col}")`;
|
|
101
|
+
} else if (prop.type === "string" && jsdoc.maxLength) {
|
|
102
|
+
imports.add("varchar");
|
|
103
|
+
call = `varchar("${col}", { length: ${jsdoc.maxLength} })`;
|
|
104
|
+
} else if (prop.type === "string" && jsdoc.format === "email") {
|
|
105
|
+
imports.add("varchar");
|
|
106
|
+
call = `varchar("${col}", { length: 255 })`;
|
|
107
|
+
} else if (prop.type === "string") {
|
|
108
|
+
imports.add("text");
|
|
109
|
+
call = `text("${col}")`;
|
|
110
|
+
} else if (prop.type === "number") {
|
|
111
|
+
imports.add("integer");
|
|
112
|
+
call = `integer("${col}")`;
|
|
113
|
+
} else if (prop.type === "bigint") {
|
|
114
|
+
imports.add("bigint");
|
|
115
|
+
call = `bigint("${col}", { mode: "number" })`;
|
|
116
|
+
} else if (prop.type === "boolean") {
|
|
117
|
+
imports.add("boolean");
|
|
118
|
+
call = `boolean("${col}")`;
|
|
119
|
+
} else if (prop.type === "Date") {
|
|
120
|
+
imports.add("timestamp");
|
|
121
|
+
call = `timestamp("${col}")`;
|
|
122
|
+
} else {
|
|
123
|
+
// object, Record, unknown → jsonb
|
|
124
|
+
imports.add("jsonb");
|
|
125
|
+
call = `jsonb("${col}")`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Apply constraints
|
|
129
|
+
if (jsdoc.generated === "uuid") {
|
|
130
|
+
call += ".defaultRandom()";
|
|
131
|
+
}
|
|
132
|
+
if (jsdoc.generated === "now" || jsdoc.onUpdate === "now") {
|
|
133
|
+
call += ".defaultNow()";
|
|
134
|
+
}
|
|
135
|
+
if (jsdoc.id !== undefined) {
|
|
136
|
+
call += ".primaryKey()";
|
|
137
|
+
}
|
|
138
|
+
if (!prop.optional) {
|
|
139
|
+
call += ".notNull()";
|
|
140
|
+
}
|
|
141
|
+
if (jsdoc.unique !== undefined) {
|
|
142
|
+
call += ".unique()";
|
|
143
|
+
}
|
|
144
|
+
if (jsdoc.default !== undefined && jsdoc.generated === undefined) {
|
|
145
|
+
const defaultVal = jsdoc.default;
|
|
146
|
+
// Numeric defaults
|
|
147
|
+
if (/^\d+$/.test(defaultVal)) {
|
|
148
|
+
call += `.default(${defaultVal})`;
|
|
149
|
+
} else {
|
|
150
|
+
// Remove surrounding quotes if present
|
|
151
|
+
const cleaned = defaultVal.replace(/^["']|["']$/g, "");
|
|
152
|
+
call += `.default("${cleaned}")`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { drizzleCall: call, imports, enumDef };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── SQLite Column Mapping ──────────────────────────────────
|
|
160
|
+
|
|
161
|
+
function mapSqliteColumn(
|
|
162
|
+
propName: string,
|
|
163
|
+
prop: { type: string; optional: boolean; jsdoc?: Record<string, string> },
|
|
164
|
+
): ColumnInfo {
|
|
165
|
+
const col = toColumnName(propName);
|
|
166
|
+
const jsdoc = prop.jsdoc ?? {};
|
|
167
|
+
const imports = new Set<string>();
|
|
168
|
+
let call: string;
|
|
169
|
+
|
|
170
|
+
if (prop.type === "number" || prop.type === "bigint") {
|
|
171
|
+
imports.add("integer");
|
|
172
|
+
call = `integer("${col}")`;
|
|
173
|
+
} else if (prop.type === "boolean") {
|
|
174
|
+
imports.add("integer");
|
|
175
|
+
call = `integer("${col}", { mode: "boolean" })`;
|
|
176
|
+
} else if (
|
|
177
|
+
prop.type === "Date" ||
|
|
178
|
+
prop.type === "object" ||
|
|
179
|
+
prop.type.startsWith("Record<") ||
|
|
180
|
+
parseUnionValues(prop.type)
|
|
181
|
+
) {
|
|
182
|
+
imports.add("text");
|
|
183
|
+
call = `text("${col}")`;
|
|
184
|
+
} else {
|
|
185
|
+
imports.add("text");
|
|
186
|
+
call = `text("${col}")`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (jsdoc.id !== undefined) {
|
|
190
|
+
call += ".primaryKey()";
|
|
191
|
+
}
|
|
192
|
+
if (!prop.optional) {
|
|
193
|
+
call += ".notNull()";
|
|
194
|
+
}
|
|
195
|
+
if (jsdoc.unique !== undefined) {
|
|
196
|
+
call += ".unique()";
|
|
197
|
+
}
|
|
198
|
+
if (jsdoc.default !== undefined) {
|
|
199
|
+
const defaultVal = jsdoc.default.replace(/^["']|["']$/g, "");
|
|
200
|
+
call += `.default("${defaultVal}")`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { drizzleCall: call, imports };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ─── Code Generation ────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
function generatePgFile(typeName: string, meta: TypeMetadata): GeneratedOutput {
|
|
209
|
+
const tableName = toTableName(typeName, meta.jsdoc);
|
|
210
|
+
const tableVarName = tableName;
|
|
211
|
+
|
|
212
|
+
const allImports = new Set<string>(["pgTable"]);
|
|
213
|
+
const enumDefs: Array<{ varName: string; dbName: string; values: string[] }> =
|
|
214
|
+
[];
|
|
215
|
+
const columns: Array<{ name: string; call: string }> = [];
|
|
216
|
+
|
|
217
|
+
for (const [propName, prop] of Object.entries(meta.properties)) {
|
|
218
|
+
const col = mapPgColumn(propName, prop, typeName);
|
|
219
|
+
for (const imp of col.imports) allImports.add(imp);
|
|
220
|
+
if (col.enumDef) enumDefs.push(col.enumDef);
|
|
221
|
+
columns.push({ name: propName, call: col.drizzleCall });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const importList = Array.from(allImports).sort().join(", ");
|
|
225
|
+
|
|
226
|
+
let code = `// AUTO-GENERATED by @typokit/db-drizzle from ${typeName} type\n`;
|
|
227
|
+
code += `// Do not edit manually — modify the source type instead\n\n`;
|
|
228
|
+
code += `import { ${importList} } from "drizzle-orm/pg-core";\n`;
|
|
229
|
+
|
|
230
|
+
// Enum definitions
|
|
231
|
+
for (const e of enumDefs) {
|
|
232
|
+
code += `\nexport const ${e.varName} = pgEnum("${e.dbName}", [${e.values.map((v) => `"${v}"`).join(", ")}]);\n`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Table definition
|
|
236
|
+
code += `\nexport const ${tableVarName} = pgTable("${tableName}", {\n`;
|
|
237
|
+
const columnLines = columns.map((c) => ` ${c.name}: ${c.call},`);
|
|
238
|
+
code += columnLines.join("\n") + "\n";
|
|
239
|
+
code += "});\n";
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
filePath: `${tableName}.ts`,
|
|
243
|
+
content: code,
|
|
244
|
+
overwrite: true,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function generateSqliteFile(
|
|
249
|
+
typeName: string,
|
|
250
|
+
meta: TypeMetadata,
|
|
251
|
+
): GeneratedOutput {
|
|
252
|
+
const tableName = toTableName(typeName, meta.jsdoc);
|
|
253
|
+
const tableVarName = tableName;
|
|
254
|
+
|
|
255
|
+
const allImports = new Set<string>(["sqliteTable"]);
|
|
256
|
+
const columns: Array<{ name: string; call: string }> = [];
|
|
257
|
+
|
|
258
|
+
for (const [propName, prop] of Object.entries(meta.properties)) {
|
|
259
|
+
const col = mapSqliteColumn(propName, prop);
|
|
260
|
+
for (const imp of col.imports) allImports.add(imp);
|
|
261
|
+
columns.push({ name: propName, call: col.drizzleCall });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const importList = Array.from(allImports).sort().join(", ");
|
|
265
|
+
|
|
266
|
+
let code = `// AUTO-GENERATED by @typokit/db-drizzle from ${typeName} type\n`;
|
|
267
|
+
code += `// Do not edit manually — modify the source type instead\n\n`;
|
|
268
|
+
code += `import { ${importList} } from "drizzle-orm/sqlite-core";\n`;
|
|
269
|
+
|
|
270
|
+
code += `\nexport const ${tableVarName} = sqliteTable("${tableName}", {\n`;
|
|
271
|
+
const columnLines = columns.map((c) => ` ${c.name}: ${c.call},`);
|
|
272
|
+
code += columnLines.join("\n") + "\n";
|
|
273
|
+
code += "});\n";
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
filePath: `${tableName}.ts`,
|
|
277
|
+
content: code,
|
|
278
|
+
overwrite: true,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ─── Diff Logic ─────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
function diffTypes(
|
|
285
|
+
types: SchemaTypeMap,
|
|
286
|
+
currentState: DatabaseState,
|
|
287
|
+
): SchemaChange[] {
|
|
288
|
+
const changes: SchemaChange[] = [];
|
|
289
|
+
|
|
290
|
+
for (const [typeName, meta] of Object.entries(types)) {
|
|
291
|
+
const tableName = toTableName(typeName, meta.jsdoc);
|
|
292
|
+
const existing = currentState.tables[tableName];
|
|
293
|
+
|
|
294
|
+
if (!existing) {
|
|
295
|
+
changes.push({ type: "add", entity: tableName });
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for (const [propName, prop] of Object.entries(meta.properties)) {
|
|
300
|
+
const colName = toColumnName(propName);
|
|
301
|
+
const existingCol = existing.columns[colName];
|
|
302
|
+
|
|
303
|
+
if (!existingCol) {
|
|
304
|
+
changes.push({
|
|
305
|
+
type: "add",
|
|
306
|
+
entity: tableName,
|
|
307
|
+
field: colName,
|
|
308
|
+
details: { tsType: prop.type },
|
|
309
|
+
});
|
|
310
|
+
} else {
|
|
311
|
+
const nullable = prop.optional;
|
|
312
|
+
if (existingCol.nullable !== nullable) {
|
|
313
|
+
changes.push({
|
|
314
|
+
type: "modify",
|
|
315
|
+
entity: tableName,
|
|
316
|
+
field: colName,
|
|
317
|
+
details: {
|
|
318
|
+
nullableFrom: existingCol.nullable,
|
|
319
|
+
nullableTo: nullable,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Detect removed columns
|
|
327
|
+
for (const colName of Object.keys(existing.columns)) {
|
|
328
|
+
const hasProp = Object.keys(meta.properties).some(
|
|
329
|
+
(p) => toColumnName(p) === colName,
|
|
330
|
+
);
|
|
331
|
+
if (!hasProp) {
|
|
332
|
+
changes.push({ type: "remove", entity: tableName, field: colName });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Detect removed tables
|
|
338
|
+
for (const tableName of Object.keys(currentState.tables)) {
|
|
339
|
+
const hasType = Object.entries(types).some(
|
|
340
|
+
([name, meta]) => toTableName(name, meta.jsdoc) === tableName,
|
|
341
|
+
);
|
|
342
|
+
if (!hasType) {
|
|
343
|
+
changes.push({ type: "remove", entity: tableName });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return changes;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function generateMigrationSql(
|
|
351
|
+
changes: SchemaChange[],
|
|
352
|
+
dialect: DrizzleDialect,
|
|
353
|
+
): string {
|
|
354
|
+
const lines: string[] = [];
|
|
355
|
+
|
|
356
|
+
for (const change of changes) {
|
|
357
|
+
if (change.type === "add" && !change.field) {
|
|
358
|
+
lines.push(`-- TODO: CREATE TABLE "${change.entity}" (define columns)`);
|
|
359
|
+
} else if (change.type === "add" && change.field) {
|
|
360
|
+
lines.push(
|
|
361
|
+
`ALTER TABLE "${change.entity}" ADD COLUMN "${change.field}" TEXT;`,
|
|
362
|
+
);
|
|
363
|
+
} else if (change.type === "remove" && !change.field) {
|
|
364
|
+
lines.push(`DROP TABLE IF EXISTS "${change.entity}";`);
|
|
365
|
+
} else if (change.type === "remove" && change.field) {
|
|
366
|
+
if (dialect === "postgresql") {
|
|
367
|
+
lines.push(
|
|
368
|
+
`ALTER TABLE "${change.entity}" DROP COLUMN "${change.field}";`,
|
|
369
|
+
);
|
|
370
|
+
} else {
|
|
371
|
+
lines.push(
|
|
372
|
+
`-- SQLite: cannot DROP COLUMN "${change.field}" from "${change.entity}" — recreate table`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
} else if (change.type === "modify") {
|
|
376
|
+
lines.push(
|
|
377
|
+
`-- TODO: ALTER TABLE "${change.entity}" modify column "${change.field}"`,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return lines.join("\n");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ─── Adapter ────────────────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
export class DrizzleDatabaseAdapter implements DatabaseAdapter {
|
|
388
|
+
private readonly dialect: DrizzleDialect;
|
|
389
|
+
private readonly outputDir: string;
|
|
390
|
+
|
|
391
|
+
constructor(options?: DrizzleAdapterOptions) {
|
|
392
|
+
this.dialect = options?.dialect ?? "postgresql";
|
|
393
|
+
this.outputDir = options?.outputDir ?? "drizzle";
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
generate(types: SchemaTypeMap): GeneratedOutput[] {
|
|
397
|
+
const outputs: GeneratedOutput[] = [];
|
|
398
|
+
const genFn =
|
|
399
|
+
this.dialect === "postgresql" ? generatePgFile : generateSqliteFile;
|
|
400
|
+
|
|
401
|
+
for (const [typeName, meta] of Object.entries(types)) {
|
|
402
|
+
const output = genFn(typeName, meta);
|
|
403
|
+
output.filePath = `${this.outputDir}/${output.filePath}`;
|
|
404
|
+
outputs.push(output);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return outputs;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
diff(types: SchemaTypeMap, currentState: DatabaseState): MigrationDraft {
|
|
411
|
+
const changes = diffTypes(types, currentState);
|
|
412
|
+
const destructive = changes.some((c) => c.type === "remove");
|
|
413
|
+
const sql = generateMigrationSql(changes, this.dialect);
|
|
414
|
+
const timestamp = new Date()
|
|
415
|
+
.toISOString()
|
|
416
|
+
.replace(/[-:T]/g, "")
|
|
417
|
+
.slice(0, 14);
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
name: `${timestamp}_schema_update`,
|
|
421
|
+
sql,
|
|
422
|
+
destructive,
|
|
423
|
+
changes,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
}
|