@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.
@@ -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
+ }