nextjs-drizzle-gen 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1437 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { program } from "commander";
5
+
6
+ // src/generators/model.ts
7
+ import * as path2 from "path";
8
+
9
+ // src/utils.ts
10
+ import * as fs from "fs";
11
+ import * as path from "path";
12
+ var VALID_FIELD_TYPES = [
13
+ "string",
14
+ "text",
15
+ "integer",
16
+ "int",
17
+ "bigint",
18
+ "boolean",
19
+ "bool",
20
+ "datetime",
21
+ "timestamp",
22
+ "date",
23
+ "float",
24
+ "decimal",
25
+ "json",
26
+ "uuid"
27
+ ];
28
+ function detectDialect() {
29
+ const configPath = path.join(process.cwd(), "drizzle.config.ts");
30
+ if (!fs.existsSync(configPath)) {
31
+ return "sqlite";
32
+ }
33
+ const content = fs.readFileSync(configPath, "utf-8");
34
+ const match = content.match(/dialect:\s*["'](\w+)["']/);
35
+ if (match) {
36
+ const dialect = match[1];
37
+ if (["postgresql", "postgres", "pg"].includes(dialect)) {
38
+ return "postgresql";
39
+ }
40
+ if (["mysql", "mysql2"].includes(dialect)) {
41
+ return "mysql";
42
+ }
43
+ }
44
+ return "sqlite";
45
+ }
46
+ var cachedProjectConfig = null;
47
+ function detectProjectConfig() {
48
+ if (cachedProjectConfig) {
49
+ return cachedProjectConfig;
50
+ }
51
+ const cwd = process.cwd();
52
+ const useSrc = fs.existsSync(path.join(cwd, "src", "app"));
53
+ let alias = "@";
54
+ const tsconfigPath = path.join(cwd, "tsconfig.json");
55
+ if (fs.existsSync(tsconfigPath)) {
56
+ try {
57
+ const content = fs.readFileSync(tsconfigPath, "utf-8");
58
+ const cleanContent = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
59
+ const tsconfig = JSON.parse(cleanContent);
60
+ const paths = tsconfig?.compilerOptions?.paths;
61
+ if (paths) {
62
+ for (const key of Object.keys(paths)) {
63
+ const match = key.match(/^(@\w*|~)\//);
64
+ if (match) {
65
+ alias = match[1];
66
+ break;
67
+ }
68
+ }
69
+ }
70
+ } catch {
71
+ }
72
+ }
73
+ let dbPath = useSrc ? "src/db" : "db";
74
+ const possibleDbPaths = useSrc ? ["src/db", "src/lib/db", "src/server/db"] : ["db", "lib/db", "server/db"];
75
+ for (const possiblePath of possibleDbPaths) {
76
+ if (fs.existsSync(path.join(cwd, possiblePath))) {
77
+ dbPath = possiblePath;
78
+ break;
79
+ }
80
+ }
81
+ const appPath = useSrc ? "src/app" : "app";
82
+ cachedProjectConfig = { useSrc, alias, dbPath, appPath };
83
+ return cachedProjectConfig;
84
+ }
85
+ function getDbImport() {
86
+ const config = detectProjectConfig();
87
+ const importPath = config.dbPath.replace(/^src\//, "");
88
+ return `${config.alias}/${importPath}`;
89
+ }
90
+ function getSchemaImport() {
91
+ return `${getDbImport()}/schema`;
92
+ }
93
+ function getAppPath() {
94
+ const config = detectProjectConfig();
95
+ return path.join(process.cwd(), config.appPath);
96
+ }
97
+ function getDbPath() {
98
+ const config = detectProjectConfig();
99
+ return path.join(process.cwd(), config.dbPath);
100
+ }
101
+ var log = {
102
+ create: (filePath) => {
103
+ const relative2 = path.relative(process.cwd(), filePath);
104
+ console.log(` \x1B[32mcreate\x1B[0m ${relative2}`);
105
+ },
106
+ force: (filePath) => {
107
+ const relative2 = path.relative(process.cwd(), filePath);
108
+ console.log(` \x1B[33mforce\x1B[0m ${relative2}`);
109
+ },
110
+ skip: (filePath) => {
111
+ const relative2 = path.relative(process.cwd(), filePath);
112
+ console.log(` \x1B[33mskip\x1B[0m ${relative2}`);
113
+ },
114
+ remove: (filePath) => {
115
+ const relative2 = path.relative(process.cwd(), filePath);
116
+ console.log(` \x1B[31mremove\x1B[0m ${relative2}`);
117
+ },
118
+ notFound: (filePath) => {
119
+ const relative2 = path.relative(process.cwd(), filePath);
120
+ console.log(` \x1B[33mnot found\x1B[0m ${relative2}`);
121
+ },
122
+ wouldCreate: (filePath) => {
123
+ const relative2 = path.relative(process.cwd(), filePath);
124
+ console.log(`\x1B[36mwould create\x1B[0m ${relative2}`);
125
+ },
126
+ wouldForce: (filePath) => {
127
+ const relative2 = path.relative(process.cwd(), filePath);
128
+ console.log(` \x1B[36mwould force\x1B[0m ${relative2}`);
129
+ },
130
+ wouldRemove: (filePath) => {
131
+ const relative2 = path.relative(process.cwd(), filePath);
132
+ console.log(`\x1B[36mwould remove\x1B[0m ${relative2}`);
133
+ },
134
+ error: (message) => {
135
+ console.error(`\x1B[31mError:\x1B[0m ${message}`);
136
+ },
137
+ info: (message) => {
138
+ console.log(message);
139
+ }
140
+ };
141
+ function validateModelName(name) {
142
+ if (!name) {
143
+ throw new Error("Model name is required");
144
+ }
145
+ if (!/^[A-Za-z][A-Za-z0-9]*$/.test(name)) {
146
+ throw new Error(
147
+ `Invalid model name "${name}". Must start with a letter and contain only letters and numbers.`
148
+ );
149
+ }
150
+ const reserved = ["model", "schema", "db", "database", "table"];
151
+ if (reserved.includes(name.toLowerCase())) {
152
+ throw new Error(`"${name}" is a reserved word and cannot be used as a model name.`);
153
+ }
154
+ }
155
+ function validateFieldDefinition(fieldDef) {
156
+ const parts = fieldDef.split(":");
157
+ let name = parts[0];
158
+ let type = parts[1] || "string";
159
+ if (name.endsWith("?")) {
160
+ name = name.slice(0, -1);
161
+ }
162
+ if (type.endsWith("?")) {
163
+ type = type.slice(0, -1);
164
+ }
165
+ if (!name) {
166
+ throw new Error(`Invalid field definition "${fieldDef}". Field name is required.`);
167
+ }
168
+ if (!/^[a-z][a-zA-Z0-9]*$/.test(name)) {
169
+ throw new Error(
170
+ `Invalid field name "${name}". Must be camelCase (start with lowercase letter).`
171
+ );
172
+ }
173
+ if (type && !type.startsWith("references") && type !== "enum" && type !== "unique") {
174
+ if (!VALID_FIELD_TYPES.includes(type)) {
175
+ throw new Error(
176
+ `Invalid field type "${type}". Valid types: ${VALID_FIELD_TYPES.join(", ")}, enum`
177
+ );
178
+ }
179
+ }
180
+ if (type === "enum") {
181
+ const enumValues = parts[2];
182
+ if (!enumValues || enumValues === "unique") {
183
+ throw new Error(
184
+ `Enum field "${name}" requires values. Example: ${name}:enum:draft,published,archived`
185
+ );
186
+ }
187
+ }
188
+ }
189
+ function parseFields(fields) {
190
+ return fields.map((field) => {
191
+ validateFieldDefinition(field);
192
+ const parts = field.split(":");
193
+ let name = parts[0];
194
+ let type = parts[1] || "string";
195
+ const nullable = name.endsWith("?") || type.endsWith("?");
196
+ if (name.endsWith("?")) {
197
+ name = name.slice(0, -1);
198
+ }
199
+ if (type.endsWith("?")) {
200
+ type = type.slice(0, -1);
201
+ }
202
+ const unique = parts.includes("unique");
203
+ if (type === "references") {
204
+ return {
205
+ name,
206
+ type: "integer",
207
+ isReference: true,
208
+ referenceTo: parts[2],
209
+ isEnum: false,
210
+ nullable,
211
+ unique
212
+ };
213
+ }
214
+ if (type === "enum") {
215
+ const enumValues = parts[2]?.split(",") || [];
216
+ return {
217
+ name,
218
+ type: "enum",
219
+ isReference: false,
220
+ isEnum: true,
221
+ enumValues,
222
+ nullable,
223
+ unique
224
+ };
225
+ }
226
+ return { name, type, isReference: false, isEnum: false, nullable, unique };
227
+ });
228
+ }
229
+ function toPascalCase(str) {
230
+ return str.replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toUpperCase());
231
+ }
232
+ function toCamelCase(str) {
233
+ const pascal = toPascalCase(str);
234
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
235
+ }
236
+ function toSnakeCase(str) {
237
+ return str.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
238
+ }
239
+ function toKebabCase(str) {
240
+ return toSnakeCase(str).replace(/_/g, "-");
241
+ }
242
+ function pluralize(str) {
243
+ if (str.endsWith("y") && !/[aeiou]y$/.test(str)) {
244
+ return str.slice(0, -1) + "ies";
245
+ }
246
+ if (str.endsWith("s") || str.endsWith("x") || str.endsWith("ch") || str.endsWith("sh")) {
247
+ return str + "es";
248
+ }
249
+ return str + "s";
250
+ }
251
+ function singularize(str) {
252
+ if (str.endsWith("ies")) {
253
+ return str.slice(0, -3) + "y";
254
+ }
255
+ if (str.endsWith("es") && (str.endsWith("xes") || str.endsWith("ches") || str.endsWith("shes") || str.endsWith("sses"))) {
256
+ return str.slice(0, -2);
257
+ }
258
+ if (str.endsWith("s") && !str.endsWith("ss")) {
259
+ return str.slice(0, -1);
260
+ }
261
+ return str;
262
+ }
263
+ function createModelContext(name) {
264
+ const singularName = singularize(name);
265
+ const pluralName = pluralize(singularName);
266
+ return {
267
+ name,
268
+ singularName,
269
+ pluralName,
270
+ pascalName: toPascalCase(singularName),
271
+ pascalPlural: toPascalCase(pluralName),
272
+ camelName: toCamelCase(singularName),
273
+ camelPlural: toCamelCase(pluralName),
274
+ snakeName: toSnakeCase(singularName),
275
+ snakePlural: toSnakeCase(pluralName),
276
+ kebabName: toKebabCase(singularName),
277
+ kebabPlural: toKebabCase(pluralName),
278
+ tableName: pluralize(toSnakeCase(singularName))
279
+ };
280
+ }
281
+ var SQLITE_TYPE_MAP = {
282
+ string: "text",
283
+ text: "text",
284
+ integer: "integer",
285
+ int: "integer",
286
+ bigint: "integer",
287
+ // SQLite doesn't distinguish
288
+ boolean: 'integer({ mode: "boolean" })',
289
+ bool: 'integer({ mode: "boolean" })',
290
+ datetime: 'integer({ mode: "timestamp" })',
291
+ timestamp: 'integer({ mode: "timestamp" })',
292
+ date: 'integer({ mode: "timestamp" })',
293
+ float: "real",
294
+ decimal: "text",
295
+ // SQLite has no native decimal
296
+ json: "text",
297
+ // Store as JSON string
298
+ uuid: "text"
299
+ // Store as text
300
+ };
301
+ var POSTGRESQL_TYPE_MAP = {
302
+ string: "text",
303
+ text: "text",
304
+ integer: "integer",
305
+ int: "integer",
306
+ bigint: "bigint",
307
+ boolean: "boolean",
308
+ bool: "boolean",
309
+ datetime: "timestamp",
310
+ timestamp: "timestamp",
311
+ date: "date",
312
+ float: "doublePrecision",
313
+ decimal: "numeric",
314
+ json: "jsonb",
315
+ uuid: "uuid"
316
+ };
317
+ var MYSQL_TYPE_MAP = {
318
+ string: "varchar",
319
+ text: "text",
320
+ integer: "int",
321
+ int: "int",
322
+ bigint: "bigint",
323
+ boolean: "boolean",
324
+ bool: "boolean",
325
+ datetime: "datetime",
326
+ timestamp: "timestamp",
327
+ date: "date",
328
+ float: "double",
329
+ decimal: "decimal",
330
+ json: "json",
331
+ uuid: "varchar"
332
+ // Store as varchar(36)
333
+ };
334
+ function drizzleType(field, dialect = "sqlite") {
335
+ const typeMap = dialect === "postgresql" ? POSTGRESQL_TYPE_MAP : dialect === "mysql" ? MYSQL_TYPE_MAP : SQLITE_TYPE_MAP;
336
+ return typeMap[field.type] || "text";
337
+ }
338
+ function getDrizzleImport(dialect) {
339
+ switch (dialect) {
340
+ case "postgresql":
341
+ return "drizzle-orm/pg-core";
342
+ case "mysql":
343
+ return "drizzle-orm/mysql-core";
344
+ default:
345
+ return "drizzle-orm/sqlite-core";
346
+ }
347
+ }
348
+ function getTableFunction(dialect) {
349
+ switch (dialect) {
350
+ case "postgresql":
351
+ return "pgTable";
352
+ case "mysql":
353
+ return "mysqlTable";
354
+ default:
355
+ return "sqliteTable";
356
+ }
357
+ }
358
+ function getIdColumn(dialect, useUuid = false) {
359
+ if (useUuid) {
360
+ switch (dialect) {
361
+ case "postgresql":
362
+ return 'id: uuid("id").primaryKey().defaultRandom()';
363
+ case "mysql":
364
+ return 'id: varchar("id", { length: 36 }).primaryKey().$defaultFn(() => crypto.randomUUID())';
365
+ default:
366
+ return 'id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID())';
367
+ }
368
+ }
369
+ switch (dialect) {
370
+ case "postgresql":
371
+ return 'id: serial("id").primaryKey()';
372
+ case "mysql":
373
+ return 'id: int("id").primaryKey().autoincrement()';
374
+ default:
375
+ return 'id: integer("id").primaryKey({ autoIncrement: true })';
376
+ }
377
+ }
378
+ function getTimestampColumns(dialect, noTimestamps = false) {
379
+ if (noTimestamps) {
380
+ return null;
381
+ }
382
+ switch (dialect) {
383
+ case "postgresql":
384
+ return `createdAt: timestamp("created_at")
385
+ .notNull()
386
+ .defaultNow(),
387
+ updatedAt: timestamp("updated_at")
388
+ .notNull()
389
+ .defaultNow()`;
390
+ case "mysql":
391
+ return `createdAt: datetime("created_at")
392
+ .notNull()
393
+ .$defaultFn(() => new Date()),
394
+ updatedAt: datetime("updated_at")
395
+ .notNull()
396
+ .$defaultFn(() => new Date())`;
397
+ default:
398
+ return `createdAt: integer("created_at", { mode: "timestamp" })
399
+ .notNull()
400
+ .$defaultFn(() => new Date()),
401
+ updatedAt: integer("updated_at", { mode: "timestamp" })
402
+ .notNull()
403
+ .$defaultFn(() => new Date())`;
404
+ }
405
+ }
406
+ function getRequiredImports(fields, dialect, options = {}) {
407
+ const types = /* @__PURE__ */ new Set();
408
+ types.add(getTableFunction(dialect));
409
+ if (options.uuid) {
410
+ if (dialect === "postgresql") {
411
+ types.add("uuid");
412
+ } else if (dialect === "mysql") {
413
+ types.add("varchar");
414
+ } else {
415
+ types.add("text");
416
+ }
417
+ } else {
418
+ if (dialect === "postgresql") {
419
+ types.add("serial");
420
+ } else if (dialect === "mysql") {
421
+ types.add("int");
422
+ } else {
423
+ types.add("integer");
424
+ }
425
+ }
426
+ if (!options.noTimestamps) {
427
+ if (dialect === "postgresql") {
428
+ types.add("timestamp");
429
+ } else if (dialect === "mysql") {
430
+ types.add("datetime");
431
+ }
432
+ }
433
+ const hasEnums = fields.some((f) => f.isEnum);
434
+ if (hasEnums) {
435
+ if (dialect === "postgresql") {
436
+ types.add("pgEnum");
437
+ } else if (dialect === "mysql") {
438
+ types.add("mysqlEnum");
439
+ }
440
+ }
441
+ for (const field of fields) {
442
+ if (field.isEnum) continue;
443
+ const drizzleTypeDef = drizzleType(field, dialect);
444
+ const baseType = drizzleTypeDef.split("(")[0];
445
+ types.add(baseType);
446
+ }
447
+ if (dialect !== "mysql") {
448
+ types.add("text");
449
+ }
450
+ return Array.from(types);
451
+ }
452
+ function writeFile(filePath, content, options = {}) {
453
+ const exists = fs.existsSync(filePath);
454
+ if (exists && !options.force) {
455
+ log.skip(filePath);
456
+ return false;
457
+ }
458
+ if (options.dryRun) {
459
+ if (exists && options.force) {
460
+ log.wouldForce(filePath);
461
+ } else {
462
+ log.wouldCreate(filePath);
463
+ }
464
+ return true;
465
+ }
466
+ const dir = path.dirname(filePath);
467
+ if (!fs.existsSync(dir)) {
468
+ fs.mkdirSync(dir, { recursive: true });
469
+ }
470
+ fs.writeFileSync(filePath, content);
471
+ if (exists && options.force) {
472
+ log.force(filePath);
473
+ } else {
474
+ log.create(filePath);
475
+ }
476
+ return true;
477
+ }
478
+ function deleteDirectory(dirPath, options = {}) {
479
+ if (!fs.existsSync(dirPath)) {
480
+ log.notFound(dirPath);
481
+ return false;
482
+ }
483
+ if (options.dryRun) {
484
+ log.wouldRemove(dirPath);
485
+ return true;
486
+ }
487
+ fs.rmSync(dirPath, { recursive: true });
488
+ log.remove(dirPath);
489
+ return true;
490
+ }
491
+ function fileExists(filePath) {
492
+ return fs.existsSync(filePath);
493
+ }
494
+ function readFile(filePath) {
495
+ return fs.readFileSync(filePath, "utf-8");
496
+ }
497
+ function modelExistsInSchema(tableName) {
498
+ const schemaPath = path.join(process.cwd(), "db", "schema.ts");
499
+ if (!fs.existsSync(schemaPath)) {
500
+ return false;
501
+ }
502
+ const content = fs.readFileSync(schemaPath, "utf-8");
503
+ const pattern = new RegExp(`sqliteTable\\s*\\(\\s*["']${tableName}["']`);
504
+ return pattern.test(content);
505
+ }
506
+
507
+ // src/generators/model.ts
508
+ function generateModel(name, fieldArgs, options = {}) {
509
+ validateModelName(name);
510
+ const ctx = createModelContext(name);
511
+ const fields = parseFields(fieldArgs);
512
+ const dialect = detectDialect();
513
+ if (modelExistsInSchema(ctx.tableName) && !options.force) {
514
+ throw new Error(
515
+ `Model "${ctx.pascalName}" already exists in schema. Use --force to regenerate.`
516
+ );
517
+ }
518
+ const schemaPath = path2.join(getDbPath(), "schema.ts");
519
+ if (fileExists(schemaPath) && !modelExistsInSchema(ctx.tableName)) {
520
+ appendToSchema(schemaPath, ctx.camelPlural, ctx.tableName, fields, dialect, options);
521
+ } else if (!fileExists(schemaPath)) {
522
+ const schemaContent = generateSchemaContent(ctx.camelPlural, ctx.tableName, fields, dialect, options);
523
+ writeFile(schemaPath, schemaContent, options);
524
+ } else {
525
+ throw new Error(
526
+ `Cannot regenerate model "${ctx.pascalName}" - manual removal from schema required.`
527
+ );
528
+ }
529
+ }
530
+ function generateSchemaContent(modelName, tableName, fields, dialect, options = {}) {
531
+ const imports = getRequiredImports(fields, dialect, options);
532
+ const drizzleImport = getDrizzleImport(dialect);
533
+ const enumDefinitions = generateEnumDefinitions(fields, dialect);
534
+ const tableDefinition = generateTableDefinition(modelName, tableName, fields, dialect, options);
535
+ return `import { ${imports.join(", ")} } from "${drizzleImport}";
536
+ ${enumDefinitions}
537
+ ${tableDefinition}
538
+ `;
539
+ }
540
+ function generateEnumDefinitions(fields, dialect) {
541
+ if (dialect !== "postgresql") {
542
+ return "";
543
+ }
544
+ const enumFields = fields.filter((f) => f.isEnum && f.enumValues);
545
+ if (enumFields.length === 0) {
546
+ return "";
547
+ }
548
+ return enumFields.map((field) => {
549
+ const enumName = `${field.name}Enum`;
550
+ const values = field.enumValues.map((v) => `"${v}"`).join(", ");
551
+ return `
552
+ export const ${enumName} = pgEnum("${toSnakeCase(field.name)}", [${values}]);`;
553
+ }).join("\n");
554
+ }
555
+ function generateTableDefinition(modelName, tableName, fields, dialect, options = {}) {
556
+ const tableFunction = getTableFunction(dialect);
557
+ const idColumn = getIdColumn(dialect, options.uuid);
558
+ const timestampColumns = getTimestampColumns(dialect, options.noTimestamps);
559
+ const fieldDefinitions = generateFieldDefinitions(fields, dialect);
560
+ const lines = [` ${idColumn},`];
561
+ if (fieldDefinitions) {
562
+ lines.push(fieldDefinitions);
563
+ }
564
+ if (timestampColumns) {
565
+ lines.push(` ${timestampColumns},`);
566
+ }
567
+ return `export const ${modelName} = ${tableFunction}("${tableName}", {
568
+ ${lines.join("\n")}
569
+ });`;
570
+ }
571
+ function generateFieldDefinitions(fields, dialect) {
572
+ return fields.map((field) => {
573
+ const columnName = toSnakeCase(field.name);
574
+ const modifiers = getFieldModifiers(field);
575
+ if (field.isEnum && field.enumValues) {
576
+ return generateEnumField(field, columnName, dialect);
577
+ }
578
+ const drizzleTypeDef = drizzleType(field, dialect);
579
+ if (field.isReference && field.referenceTo) {
580
+ const intType = dialect === "mysql" ? "int" : "integer";
581
+ return ` ${field.name}: ${intType}("${columnName}").references(() => ${toCamelCase(pluralize(field.referenceTo))}.id)${modifiers},`;
582
+ }
583
+ if (dialect === "mysql" && drizzleTypeDef === "varchar") {
584
+ const length = field.type === "uuid" ? 36 : 255;
585
+ return ` ${field.name}: varchar("${columnName}", { length: ${length} })${modifiers},`;
586
+ }
587
+ if (drizzleTypeDef.includes("(")) {
588
+ const [typeName] = drizzleTypeDef.split("(");
589
+ const typeOptions = drizzleTypeDef.match(/\(.*\)/)?.[0] ?? "";
590
+ return ` ${field.name}: ${typeName}("${columnName}", ${typeOptions})${modifiers},`;
591
+ }
592
+ return ` ${field.name}: ${drizzleTypeDef}("${columnName}")${modifiers},`;
593
+ }).join("\n");
594
+ }
595
+ function getFieldModifiers(field) {
596
+ const modifiers = [];
597
+ if (!field.nullable) {
598
+ modifiers.push(".notNull()");
599
+ }
600
+ if (field.unique) {
601
+ modifiers.push(".unique()");
602
+ }
603
+ return modifiers.join("");
604
+ }
605
+ function generateEnumField(field, columnName, dialect) {
606
+ const values = field.enumValues;
607
+ const modifiers = getFieldModifiers(field);
608
+ switch (dialect) {
609
+ case "postgresql":
610
+ return ` ${field.name}: ${field.name}Enum("${columnName}")${modifiers},`;
611
+ case "mysql":
612
+ const mysqlValues = values.map((v) => `"${v}"`).join(", ");
613
+ return ` ${field.name}: mysqlEnum("${columnName}", [${mysqlValues}])${modifiers},`;
614
+ default:
615
+ const sqliteValues = values.map((v) => `"${v}"`).join(", ");
616
+ return ` ${field.name}: text("${columnName}", { enum: [${sqliteValues}] })${modifiers},`;
617
+ }
618
+ }
619
+ function appendToSchema(schemaPath, modelName, tableName, fields, dialect, options) {
620
+ const existingContent = readFile(schemaPath);
621
+ const enumDefinitions = generateEnumDefinitions(fields, dialect);
622
+ const tableDefinition = generateTableDefinition(modelName, tableName, fields, dialect, options);
623
+ const newContent = existingContent + enumDefinitions + "\n" + tableDefinition + "\n";
624
+ writeFile(schemaPath, newContent, { force: true, dryRun: options.dryRun });
625
+ }
626
+
627
+ // src/generators/actions.ts
628
+ import * as path3 from "path";
629
+ function generateActions(name, options = {}) {
630
+ validateModelName(name);
631
+ const ctx = createModelContext(name);
632
+ const actionsPath = path3.join(
633
+ getAppPath(),
634
+ ctx.kebabPlural,
635
+ "actions.ts"
636
+ );
637
+ const content = generateActionsContent(ctx);
638
+ writeFile(actionsPath, content, options);
639
+ }
640
+ function generateActionsContent(ctx) {
641
+ const { pascalName, pascalPlural, camelPlural, kebabPlural } = ctx;
642
+ const dbImport = getDbImport();
643
+ const schemaImport = getSchemaImport();
644
+ return `"use server";
645
+
646
+ import { db } from "${dbImport}";
647
+ import { ${camelPlural} } from "${schemaImport}";
648
+ import { eq, desc } from "drizzle-orm";
649
+ import { revalidatePath } from "next/cache";
650
+
651
+ export type ${pascalName} = typeof ${camelPlural}.$inferSelect;
652
+ export type New${pascalName} = typeof ${camelPlural}.$inferInsert;
653
+
654
+ export async function get${pascalPlural}() {
655
+ return db.select().from(${camelPlural}).orderBy(desc(${camelPlural}.createdAt));
656
+ }
657
+
658
+ export async function get${pascalName}(id: number) {
659
+ const result = await db
660
+ .select()
661
+ .from(${camelPlural})
662
+ .where(eq(${camelPlural}.id, id))
663
+ .limit(1);
664
+ return result[0] ?? null;
665
+ }
666
+
667
+ export async function create${pascalName}(data: Omit<New${pascalName}, "id" | "createdAt" | "updatedAt">) {
668
+ const result = await db.insert(${camelPlural}).values(data).returning();
669
+ revalidatePath("/${kebabPlural}");
670
+ return result[0];
671
+ }
672
+
673
+ export async function update${pascalName}(
674
+ id: number,
675
+ data: Partial<Omit<New${pascalName}, "id" | "createdAt" | "updatedAt">>
676
+ ) {
677
+ const result = await db
678
+ .update(${camelPlural})
679
+ .set({ ...data, updatedAt: new Date() })
680
+ .where(eq(${camelPlural}.id, id))
681
+ .returning();
682
+ revalidatePath("/${kebabPlural}");
683
+ return result[0];
684
+ }
685
+
686
+ export async function delete${pascalName}(id: number) {
687
+ await db.delete(${camelPlural}).where(eq(${camelPlural}.id, id));
688
+ revalidatePath("/${kebabPlural}");
689
+ }
690
+ `;
691
+ }
692
+
693
+ // src/generators/scaffold.ts
694
+ import * as path4 from "path";
695
+ function generateScaffold(name, fieldArgs, options = {}) {
696
+ validateModelName(name);
697
+ const ctx = createModelContext(name);
698
+ const fields = parseFields(fieldArgs);
699
+ const prefix = options.dryRun ? "[dry-run] " : "";
700
+ log.info(`
701
+ ${prefix}Scaffolding ${ctx.pascalName}...
702
+ `);
703
+ generateModel(ctx.singularName, fieldArgs, options);
704
+ generateActions(ctx.singularName, options);
705
+ generatePages(ctx, fields, options);
706
+ log.info(`
707
+ Next steps:`);
708
+ log.info(` 1. Run 'pnpm db:push' to update the database`);
709
+ log.info(` 2. Run 'pnpm dev' and visit /${ctx.kebabPlural}`);
710
+ }
711
+ function generatePages(ctx, fields, options = {}) {
712
+ const { pascalName, pascalPlural, camelName, kebabPlural } = ctx;
713
+ const basePath = path4.join(getAppPath(), kebabPlural);
714
+ writeFile(
715
+ path4.join(basePath, "page.tsx"),
716
+ generateIndexPage(pascalName, pascalPlural, camelName, kebabPlural, fields),
717
+ options
718
+ );
719
+ writeFile(
720
+ path4.join(basePath, "new", "page.tsx"),
721
+ generateNewPage(pascalName, camelName, kebabPlural, fields),
722
+ options
723
+ );
724
+ writeFile(
725
+ path4.join(basePath, "[id]", "page.tsx"),
726
+ generateShowPage(pascalName, pascalPlural, camelName, kebabPlural, fields),
727
+ options
728
+ );
729
+ writeFile(
730
+ path4.join(basePath, "[id]", "edit", "page.tsx"),
731
+ generateEditPage(pascalName, camelName, kebabPlural, fields),
732
+ options
733
+ );
734
+ }
735
+ function generateIndexPage(pascalName, pascalPlural, camelName, kebabPlural, fields) {
736
+ const displayField = fields[0]?.name || "id";
737
+ return `import Link from "next/link";
738
+ import { get${pascalPlural} } from "./actions";
739
+ import { delete${pascalName} } from "./actions";
740
+
741
+ export default async function ${pascalPlural}Page() {
742
+ const ${camelName}s = await get${pascalPlural}();
743
+
744
+ return (
745
+ <div className="mx-auto max-w-3xl px-6 py-12">
746
+ <div className="mb-10 flex items-center justify-between">
747
+ <h1 className="text-2xl font-semibold text-gray-900">${pascalPlural}</h1>
748
+ <Link
749
+ href="/${kebabPlural}/new"
750
+ className="rounded-lg bg-gray-900 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-gray-800"
751
+ >
752
+ New ${pascalName}
753
+ </Link>
754
+ </div>
755
+
756
+ {${camelName}s.length === 0 ? (
757
+ <p className="text-gray-500">No ${camelName}s yet.</p>
758
+ ) : (
759
+ <div className="divide-y divide-gray-100">
760
+ {${camelName}s.map((${camelName}) => (
761
+ <div
762
+ key={${camelName}.id}
763
+ className="flex items-center justify-between py-4"
764
+ >
765
+ <Link href={\`/${kebabPlural}/\${${camelName}.id}\`} className="font-medium text-gray-900 hover:text-gray-600">
766
+ {${camelName}.${displayField}}
767
+ </Link>
768
+ <div className="flex gap-4 text-sm">
769
+ <Link
770
+ href={\`/${kebabPlural}/\${${camelName}.id}/edit\`}
771
+ className="text-gray-500 hover:text-gray-900"
772
+ >
773
+ Edit
774
+ </Link>
775
+ <form
776
+ action={async () => {
777
+ "use server";
778
+ await delete${pascalName}(${camelName}.id);
779
+ }}
780
+ >
781
+ <button type="submit" className="text-gray-500 hover:text-red-600">
782
+ Delete
783
+ </button>
784
+ </form>
785
+ </div>
786
+ </div>
787
+ ))}
788
+ </div>
789
+ )}
790
+ </div>
791
+ );
792
+ }
793
+ `;
794
+ }
795
+ function generateNewPage(pascalName, camelName, kebabPlural, fields) {
796
+ return `import { redirect } from "next/navigation";
797
+ import Link from "next/link";
798
+ import { create${pascalName} } from "../actions";
799
+
800
+ export default function New${pascalName}Page() {
801
+ async function handleCreate(formData: FormData) {
802
+ "use server";
803
+ await create${pascalName}({
804
+ ${fields.map((f) => ` ${f.name}: ${formDataValue(f)},`).join("\n")}
805
+ });
806
+ redirect("/${kebabPlural}");
807
+ }
808
+
809
+ return (
810
+ <div className="mx-auto max-w-xl px-6 py-12">
811
+ <h1 className="mb-8 text-2xl font-semibold text-gray-900">New ${pascalName}</h1>
812
+
813
+ <form action={handleCreate} className="space-y-5">
814
+ ${fields.map((f) => generateFormField(f, camelName)).join("\n\n")}
815
+
816
+ <div className="flex gap-3 pt-4">
817
+ <button
818
+ type="submit"
819
+ className="rounded-lg bg-gray-900 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-gray-800"
820
+ >
821
+ Create ${pascalName}
822
+ </button>
823
+ <Link
824
+ href="/${kebabPlural}"
825
+ className="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-50"
826
+ >
827
+ Cancel
828
+ </Link>
829
+ </div>
830
+ </form>
831
+ </div>
832
+ );
833
+ }
834
+ `;
835
+ }
836
+ function generateShowPage(pascalName, _pascalPlural, camelName, kebabPlural, fields) {
837
+ return `import { notFound } from "next/navigation";
838
+ import Link from "next/link";
839
+ import { get${pascalName} } from "../actions";
840
+
841
+ export default async function ${pascalName}Page({
842
+ params,
843
+ }: {
844
+ params: Promise<{ id: string }>;
845
+ }) {
846
+ const { id } = await params;
847
+ const ${camelName} = await get${pascalName}(parseInt(id));
848
+
849
+ if (!${camelName}) {
850
+ notFound();
851
+ }
852
+
853
+ return (
854
+ <div className="mx-auto max-w-xl px-6 py-12">
855
+ <div className="mb-8 flex items-center justify-between">
856
+ <h1 className="text-2xl font-semibold text-gray-900">${pascalName}</h1>
857
+ <div className="flex gap-3">
858
+ <Link
859
+ href={\`/${kebabPlural}/\${${camelName}.id}/edit\`}
860
+ className="rounded-lg bg-gray-900 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-gray-800"
861
+ >
862
+ Edit
863
+ </Link>
864
+ <Link
865
+ href="/${kebabPlural}"
866
+ className="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-50"
867
+ >
868
+ Back
869
+ </Link>
870
+ </div>
871
+ </div>
872
+
873
+ <dl className="divide-y divide-gray-100">
874
+ ${fields.map(
875
+ (f) => ` <div className="py-3">
876
+ <dt className="text-sm text-gray-500">${toPascalCase(f.name)}</dt>
877
+ <dd className="mt-1 text-gray-900">{${camelName}.${f.name}}</dd>
878
+ </div>`
879
+ ).join("\n")}
880
+ <div className="py-3">
881
+ <dt className="text-sm text-gray-500">Created At</dt>
882
+ <dd className="mt-1 text-gray-900">{${camelName}.createdAt.toLocaleString()}</dd>
883
+ </div>
884
+ </dl>
885
+ </div>
886
+ );
887
+ }
888
+ `;
889
+ }
890
+ function generateEditPage(pascalName, camelName, kebabPlural, fields) {
891
+ return `import { notFound, redirect } from "next/navigation";
892
+ import Link from "next/link";
893
+ import { get${pascalName}, update${pascalName} } from "../../actions";
894
+
895
+ export default async function Edit${pascalName}Page({
896
+ params,
897
+ }: {
898
+ params: Promise<{ id: string }>;
899
+ }) {
900
+ const { id } = await params;
901
+ const ${camelName} = await get${pascalName}(parseInt(id));
902
+
903
+ if (!${camelName}) {
904
+ notFound();
905
+ }
906
+
907
+ async function handleUpdate(formData: FormData) {
908
+ "use server";
909
+ await update${pascalName}(parseInt(id), {
910
+ ${fields.map((f) => ` ${f.name}: ${formDataValue(f)},`).join("\n")}
911
+ });
912
+ redirect("/${kebabPlural}");
913
+ }
914
+
915
+ return (
916
+ <div className="mx-auto max-w-xl px-6 py-12">
917
+ <h1 className="mb-8 text-2xl font-semibold text-gray-900">Edit ${pascalName}</h1>
918
+
919
+ <form action={handleUpdate} className="space-y-5">
920
+ ${fields.map((f) => generateFormField(f, camelName, true)).join("\n\n")}
921
+
922
+ <div className="flex gap-3 pt-4">
923
+ <button
924
+ type="submit"
925
+ className="rounded-lg bg-gray-900 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-gray-800"
926
+ >
927
+ Update ${pascalName}
928
+ </button>
929
+ <Link
930
+ href="/${kebabPlural}"
931
+ className="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-50"
932
+ >
933
+ Cancel
934
+ </Link>
935
+ </div>
936
+ </form>
937
+ </div>
938
+ );
939
+ }
940
+ `;
941
+ }
942
+ function generateFormField(field, camelName, withDefault = false) {
943
+ const label = toPascalCase(field.name);
944
+ const defaultValue = withDefault ? ` defaultValue={${camelName}.${field.name}}` : "";
945
+ const inputClasses = "mt-1.5 block w-full rounded-lg border border-gray-200 px-3 py-2 text-gray-900 placeholder:text-gray-400 focus:border-gray-400 focus:outline-none focus:ring-0";
946
+ const selectClasses = "mt-1.5 block w-full rounded-lg border border-gray-200 px-3 py-2 text-gray-900 focus:border-gray-400 focus:outline-none focus:ring-0";
947
+ const optionalLabel = field.nullable ? ` <span className="text-gray-400">(optional)</span>` : "";
948
+ const required = field.nullable ? "" : " required";
949
+ if (field.type === "text" || field.type === "json") {
950
+ const rows = field.type === "json" ? 6 : 4;
951
+ const placeholder = field.type === "json" ? ` placeholder="{}"` : "";
952
+ return ` <div>
953
+ <label htmlFor="${field.name}" className="block text-sm font-medium text-gray-700">
954
+ ${label}${optionalLabel}
955
+ </label>
956
+ <textarea
957
+ id="${field.name}"
958
+ name="${field.name}"
959
+ rows={${rows}}
960
+ className="${inputClasses} resize-none"${defaultValue}${placeholder}${required}
961
+ />
962
+ </div>`;
963
+ }
964
+ if (field.type === "boolean" || field.type === "bool") {
965
+ const defaultChecked = withDefault ? ` defaultChecked={${camelName}.${field.name}}` : "";
966
+ return ` <div className="flex items-center gap-2">
967
+ <input
968
+ type="checkbox"
969
+ id="${field.name}"
970
+ name="${field.name}"
971
+ className="h-4 w-4 rounded border-gray-300 text-gray-900 focus:ring-0 focus:ring-offset-0"${defaultChecked}
972
+ />
973
+ <label htmlFor="${field.name}" className="text-sm font-medium text-gray-700">
974
+ ${label}
975
+ </label>
976
+ </div>`;
977
+ }
978
+ if (field.type === "integer" || field.type === "int" || field.type === "bigint") {
979
+ return ` <div>
980
+ <label htmlFor="${field.name}" className="block text-sm font-medium text-gray-700">
981
+ ${label}${optionalLabel}
982
+ </label>
983
+ <input
984
+ type="number"
985
+ id="${field.name}"
986
+ name="${field.name}"
987
+ className="${inputClasses}"${defaultValue}${required}
988
+ />
989
+ </div>`;
990
+ }
991
+ if (field.type === "float" || field.type === "decimal") {
992
+ const step = field.type === "decimal" ? "0.01" : "any";
993
+ return ` <div>
994
+ <label htmlFor="${field.name}" className="block text-sm font-medium text-gray-700">
995
+ ${label}${optionalLabel}
996
+ </label>
997
+ <input
998
+ type="number"
999
+ step="${step}"
1000
+ id="${field.name}"
1001
+ name="${field.name}"
1002
+ className="${inputClasses}"${defaultValue}${required}
1003
+ />
1004
+ </div>`;
1005
+ }
1006
+ if (field.type === "date") {
1007
+ const dateDefault = withDefault ? ` defaultValue={${camelName}.${field.name}?.toISOString().split("T")[0]}` : "";
1008
+ return ` <div>
1009
+ <label htmlFor="${field.name}" className="block text-sm font-medium text-gray-700">
1010
+ ${label}${optionalLabel}
1011
+ </label>
1012
+ <input
1013
+ type="date"
1014
+ id="${field.name}"
1015
+ name="${field.name}"
1016
+ className="${inputClasses}"${dateDefault}${required}
1017
+ />
1018
+ </div>`;
1019
+ }
1020
+ if (field.type === "datetime" || field.type === "timestamp") {
1021
+ const dateDefault = withDefault ? ` defaultValue={${camelName}.${field.name}?.toISOString().slice(0, 16)}` : "";
1022
+ return ` <div>
1023
+ <label htmlFor="${field.name}" className="block text-sm font-medium text-gray-700">
1024
+ ${label}${optionalLabel}
1025
+ </label>
1026
+ <input
1027
+ type="datetime-local"
1028
+ id="${field.name}"
1029
+ name="${field.name}"
1030
+ className="${inputClasses}"${dateDefault}${required}
1031
+ />
1032
+ </div>`;
1033
+ }
1034
+ if (field.isEnum && field.enumValues) {
1035
+ const options = field.enumValues.map((v) => ` <option value="${v}">${toPascalCase(v)}</option>`).join("\n");
1036
+ return ` <div>
1037
+ <label htmlFor="${field.name}" className="block text-sm font-medium text-gray-700">
1038
+ ${label}${optionalLabel}
1039
+ </label>
1040
+ <select
1041
+ id="${field.name}"
1042
+ name="${field.name}"
1043
+ className="${selectClasses}"${defaultValue}${required}
1044
+ >
1045
+ ${options}
1046
+ </select>
1047
+ </div>`;
1048
+ }
1049
+ return ` <div>
1050
+ <label htmlFor="${field.name}" className="block text-sm font-medium text-gray-700">
1051
+ ${label}${optionalLabel}
1052
+ </label>
1053
+ <input
1054
+ type="text"
1055
+ id="${field.name}"
1056
+ name="${field.name}"
1057
+ className="${inputClasses}"${defaultValue}${required}
1058
+ />
1059
+ </div>`;
1060
+ }
1061
+ function formDataValue(field) {
1062
+ const getValue = `formData.get("${field.name}")`;
1063
+ const asString = `${getValue} as string`;
1064
+ if (field.nullable) {
1065
+ if (field.type === "integer" || field.type === "int" || field.type === "bigint") {
1066
+ return `${getValue} ? parseInt(${asString}) : null`;
1067
+ }
1068
+ if (field.type === "float") {
1069
+ return `${getValue} ? parseFloat(${asString}) : null`;
1070
+ }
1071
+ if (field.type === "decimal") {
1072
+ return `${getValue} ? ${asString} : null`;
1073
+ }
1074
+ if (field.type === "datetime" || field.type === "timestamp" || field.type === "date") {
1075
+ return `${getValue} ? new Date(${asString}) : null`;
1076
+ }
1077
+ if (field.type === "json") {
1078
+ return `${getValue} ? JSON.parse(${asString}) : null`;
1079
+ }
1080
+ return `${getValue} ? ${asString} : null`;
1081
+ }
1082
+ if (field.type === "boolean" || field.type === "bool") {
1083
+ return `${getValue} === "on"`;
1084
+ }
1085
+ if (field.type === "integer" || field.type === "int" || field.type === "bigint") {
1086
+ return `parseInt(${asString})`;
1087
+ }
1088
+ if (field.type === "float") {
1089
+ return `parseFloat(${asString})`;
1090
+ }
1091
+ if (field.type === "datetime" || field.type === "timestamp" || field.type === "date") {
1092
+ return `new Date(${asString})`;
1093
+ }
1094
+ if (field.type === "json") {
1095
+ return `JSON.parse(${asString})`;
1096
+ }
1097
+ return asString;
1098
+ }
1099
+
1100
+ // src/generators/resource.ts
1101
+ function generateResource(name, fieldArgs, options = {}) {
1102
+ validateModelName(name);
1103
+ const ctx = createModelContext(name);
1104
+ const prefix = options.dryRun ? "[dry-run] " : "";
1105
+ log.info(`
1106
+ ${prefix}Generating resource ${ctx.pascalName}...
1107
+ `);
1108
+ generateModel(ctx.singularName, fieldArgs, options);
1109
+ generateActions(ctx.singularName, options);
1110
+ log.info(`
1111
+ Next steps:`);
1112
+ log.info(` 1. Run 'pnpm db:push' to update the database`);
1113
+ log.info(` 2. Create pages in app/${ctx.kebabPlural}/`);
1114
+ }
1115
+
1116
+ // src/generators/api.ts
1117
+ import * as path5 from "path";
1118
+ function generateApi(name, fieldArgs, options = {}) {
1119
+ validateModelName(name);
1120
+ const ctx = createModelContext(name);
1121
+ const prefix = options.dryRun ? "[dry-run] " : "";
1122
+ log.info(`
1123
+ ${prefix}Generating API ${ctx.pascalName}...
1124
+ `);
1125
+ generateModel(ctx.singularName, fieldArgs, options);
1126
+ generateRoutes(ctx.camelPlural, ctx.kebabPlural, options);
1127
+ log.info(`
1128
+ Next steps:`);
1129
+ log.info(` 1. Run 'pnpm db:push' to update the database`);
1130
+ log.info(` 2. API available at /api/${ctx.kebabPlural}`);
1131
+ }
1132
+ function generateRoutes(camelPlural, kebabPlural, options) {
1133
+ const basePath = path5.join(getAppPath(), "api", kebabPlural);
1134
+ writeFile(
1135
+ path5.join(basePath, "route.ts"),
1136
+ generateCollectionRoute(camelPlural),
1137
+ options
1138
+ );
1139
+ writeFile(
1140
+ path5.join(basePath, "[id]", "route.ts"),
1141
+ generateMemberRoute(camelPlural),
1142
+ options
1143
+ );
1144
+ }
1145
+ function generateCollectionRoute(camelPlural) {
1146
+ const dbImport = getDbImport();
1147
+ const schemaImport = getSchemaImport();
1148
+ return `import { db } from "${dbImport}";
1149
+ import { ${camelPlural} } from "${schemaImport}";
1150
+ import { desc } from "drizzle-orm";
1151
+ import { NextResponse } from "next/server";
1152
+
1153
+ export async function GET() {
1154
+ try {
1155
+ const data = await db
1156
+ .select()
1157
+ .from(${camelPlural})
1158
+ .orderBy(desc(${camelPlural}.createdAt));
1159
+
1160
+ return NextResponse.json(data);
1161
+ } catch {
1162
+ return NextResponse.json(
1163
+ { error: "Failed to fetch records" },
1164
+ { status: 500 }
1165
+ );
1166
+ }
1167
+ }
1168
+
1169
+ export async function POST(request: Request) {
1170
+ try {
1171
+ const body = await request.json();
1172
+ const result = await db.insert(${camelPlural}).values(body).returning();
1173
+
1174
+ return NextResponse.json(result[0], { status: 201 });
1175
+ } catch {
1176
+ return NextResponse.json(
1177
+ { error: "Failed to create record" },
1178
+ { status: 500 }
1179
+ );
1180
+ }
1181
+ }
1182
+ `;
1183
+ }
1184
+ function generateMemberRoute(camelPlural) {
1185
+ const dbImport = getDbImport();
1186
+ const schemaImport = getSchemaImport();
1187
+ return `import { db } from "${dbImport}";
1188
+ import { ${camelPlural} } from "${schemaImport}";
1189
+ import { eq } from "drizzle-orm";
1190
+ import { NextResponse } from "next/server";
1191
+
1192
+ type Params = { params: Promise<{ id: string }> };
1193
+
1194
+ export async function GET(request: Request, { params }: Params) {
1195
+ try {
1196
+ const { id } = await params;
1197
+ const result = await db
1198
+ .select()
1199
+ .from(${camelPlural})
1200
+ .where(eq(${camelPlural}.id, parseInt(id)))
1201
+ .limit(1);
1202
+
1203
+ if (!result[0]) {
1204
+ return NextResponse.json(
1205
+ { error: "Record not found" },
1206
+ { status: 404 }
1207
+ );
1208
+ }
1209
+
1210
+ return NextResponse.json(result[0]);
1211
+ } catch {
1212
+ return NextResponse.json(
1213
+ { error: "Failed to fetch record" },
1214
+ { status: 500 }
1215
+ );
1216
+ }
1217
+ }
1218
+
1219
+ export async function PATCH(request: Request, { params }: Params) {
1220
+ try {
1221
+ const { id } = await params;
1222
+ const body = await request.json();
1223
+ const result = await db
1224
+ .update(${camelPlural})
1225
+ .set({ ...body, updatedAt: new Date() })
1226
+ .where(eq(${camelPlural}.id, parseInt(id)))
1227
+ .returning();
1228
+
1229
+ if (!result[0]) {
1230
+ return NextResponse.json(
1231
+ { error: "Record not found" },
1232
+ { status: 404 }
1233
+ );
1234
+ }
1235
+
1236
+ return NextResponse.json(result[0]);
1237
+ } catch {
1238
+ return NextResponse.json(
1239
+ { error: "Failed to update record" },
1240
+ { status: 500 }
1241
+ );
1242
+ }
1243
+ }
1244
+
1245
+ export async function DELETE(request: Request, { params }: Params) {
1246
+ try {
1247
+ const { id } = await params;
1248
+ await db.delete(${camelPlural}).where(eq(${camelPlural}.id, parseInt(id)));
1249
+
1250
+ return new NextResponse(null, { status: 204 });
1251
+ } catch {
1252
+ return NextResponse.json(
1253
+ { error: "Failed to delete record" },
1254
+ { status: 500 }
1255
+ );
1256
+ }
1257
+ }
1258
+ `;
1259
+ }
1260
+
1261
+ // src/generators/destroy.ts
1262
+ import * as path6 from "path";
1263
+ function destroyScaffold(name, options = {}) {
1264
+ validateModelName(name);
1265
+ const ctx = createModelContext(name);
1266
+ const config = detectProjectConfig();
1267
+ const prefix = options.dryRun ? "[dry-run] " : "";
1268
+ log.info(`
1269
+ ${prefix}Destroying scaffold ${ctx.pascalName}...
1270
+ `);
1271
+ const basePath = path6.join(getAppPath(), ctx.kebabPlural);
1272
+ deleteDirectory(basePath, options);
1273
+ log.info(`
1274
+ Note: Schema in ${config.dbPath}/schema.ts was not modified.`);
1275
+ log.info(` Remove the table definition manually if needed.`);
1276
+ }
1277
+ function destroyResource(name, options = {}) {
1278
+ validateModelName(name);
1279
+ const ctx = createModelContext(name);
1280
+ const config = detectProjectConfig();
1281
+ const prefix = options.dryRun ? "[dry-run] " : "";
1282
+ log.info(`
1283
+ ${prefix}Destroying resource ${ctx.pascalName}...
1284
+ `);
1285
+ const basePath = path6.join(getAppPath(), ctx.kebabPlural);
1286
+ deleteDirectory(basePath, options);
1287
+ log.info(`
1288
+ Note: Schema in ${config.dbPath}/schema.ts was not modified.`);
1289
+ log.info(` Remove the table definition manually if needed.`);
1290
+ }
1291
+ function destroyApi(name, options = {}) {
1292
+ validateModelName(name);
1293
+ const ctx = createModelContext(name);
1294
+ const config = detectProjectConfig();
1295
+ const prefix = options.dryRun ? "[dry-run] " : "";
1296
+ log.info(`
1297
+ ${prefix}Destroying API ${ctx.pascalName}...
1298
+ `);
1299
+ const basePath = path6.join(getAppPath(), "api", ctx.kebabPlural);
1300
+ deleteDirectory(basePath, options);
1301
+ log.info(`
1302
+ Note: Schema in ${config.dbPath}/schema.ts was not modified.`);
1303
+ log.info(` Remove the table definition manually if needed.`);
1304
+ }
1305
+
1306
+ // src/index.ts
1307
+ function handleError(error) {
1308
+ if (error instanceof Error) {
1309
+ log.error(error.message);
1310
+ } else {
1311
+ log.error(String(error));
1312
+ }
1313
+ process.exit(1);
1314
+ }
1315
+ program.name("drizzle-gen").description("Rails-like generators for Next.js + Drizzle").version("0.1.0");
1316
+ program.command("model <name> [fields...]").description(
1317
+ `Generate a Drizzle schema model
1318
+
1319
+ Examples:
1320
+ drizzle-gen model user name:string email:string:unique
1321
+ drizzle-gen model post title:string body:text published:boolean
1322
+ drizzle-gen model order total:decimal status:enum:pending,paid,shipped
1323
+ drizzle-gen model token value:uuid --uuid --no-timestamps
1324
+ drizzle-gen model comment content:text? author:string`
1325
+ ).option("-f, --force", "Overwrite existing files").option("-n, --dry-run", "Preview changes without writing files").option("-u, --uuid", "Use UUID for primary key instead of auto-increment").option("--no-timestamps", "Skip createdAt/updatedAt fields").action((name, fields, opts) => {
1326
+ try {
1327
+ generateModel(name, fields, {
1328
+ ...opts,
1329
+ noTimestamps: opts.timestamps === false
1330
+ });
1331
+ } catch (error) {
1332
+ handleError(error);
1333
+ }
1334
+ });
1335
+ program.command("actions <name>").description(
1336
+ `Generate server actions for an existing model
1337
+
1338
+ Examples:
1339
+ drizzle-gen actions user
1340
+ drizzle-gen actions post --force`
1341
+ ).option("-f, --force", "Overwrite existing files").option("-n, --dry-run", "Preview changes without writing files").action((name, opts) => {
1342
+ try {
1343
+ generateActions(name, opts);
1344
+ } catch (error) {
1345
+ handleError(error);
1346
+ }
1347
+ });
1348
+ program.command("resource <name> [fields...]").description(
1349
+ `Generate model and actions (no views)
1350
+
1351
+ Examples:
1352
+ drizzle-gen resource user name:string email:string:unique
1353
+ drizzle-gen resource session token:uuid userId:references:user --uuid`
1354
+ ).option("-f, --force", "Overwrite existing files").option("-n, --dry-run", "Preview changes without writing files").option("-u, --uuid", "Use UUID for primary key instead of auto-increment").option("--no-timestamps", "Skip createdAt/updatedAt fields").action((name, fields, opts) => {
1355
+ try {
1356
+ generateResource(name, fields, {
1357
+ ...opts,
1358
+ noTimestamps: opts.timestamps === false
1359
+ });
1360
+ } catch (error) {
1361
+ handleError(error);
1362
+ }
1363
+ });
1364
+ program.command("scaffold <name> [fields...]").description(
1365
+ `Generate model, actions, and pages (full CRUD)
1366
+
1367
+ Examples:
1368
+ drizzle-gen scaffold post title:string body:text published:boolean
1369
+ drizzle-gen scaffold product name:string price:float description:text?
1370
+ drizzle-gen scaffold order status:enum:pending,processing,shipped,delivered`
1371
+ ).option("-f, --force", "Overwrite existing files").option("-n, --dry-run", "Preview changes without writing files").option("-u, --uuid", "Use UUID for primary key instead of auto-increment").option("--no-timestamps", "Skip createdAt/updatedAt fields").action((name, fields, opts) => {
1372
+ try {
1373
+ generateScaffold(name, fields, {
1374
+ ...opts,
1375
+ noTimestamps: opts.timestamps === false
1376
+ });
1377
+ } catch (error) {
1378
+ handleError(error);
1379
+ }
1380
+ });
1381
+ program.command("api <name> [fields...]").description(
1382
+ `Generate model and API route handlers (REST)
1383
+
1384
+ Examples:
1385
+ drizzle-gen api product name:string price:float
1386
+ drizzle-gen api webhook url:string secret:string:unique --uuid`
1387
+ ).option("-f, --force", "Overwrite existing files").option("-n, --dry-run", "Preview changes without writing files").option("-u, --uuid", "Use UUID for primary key instead of auto-increment").option("--no-timestamps", "Skip createdAt/updatedAt fields").action((name, fields, opts) => {
1388
+ try {
1389
+ generateApi(name, fields, {
1390
+ ...opts,
1391
+ noTimestamps: opts.timestamps === false
1392
+ });
1393
+ } catch (error) {
1394
+ handleError(error);
1395
+ }
1396
+ });
1397
+ program.command("destroy <type> <name>").alias("d").description(
1398
+ `Remove generated files (scaffold, resource, api)
1399
+
1400
+ Examples:
1401
+ drizzle-gen destroy scaffold post
1402
+ drizzle-gen d api product --dry-run`
1403
+ ).option("-n, --dry-run", "Preview changes without deleting files").action((type, name, opts) => {
1404
+ try {
1405
+ switch (type) {
1406
+ case "scaffold":
1407
+ destroyScaffold(name, opts);
1408
+ break;
1409
+ case "resource":
1410
+ destroyResource(name, opts);
1411
+ break;
1412
+ case "api":
1413
+ destroyApi(name, opts);
1414
+ break;
1415
+ default:
1416
+ throw new Error(`Unknown type "${type}". Use: scaffold, resource, or api`);
1417
+ }
1418
+ } catch (error) {
1419
+ handleError(error);
1420
+ }
1421
+ });
1422
+ program.command("config").description("Show detected project configuration").action(() => {
1423
+ const config = detectProjectConfig();
1424
+ const dialect = detectDialect();
1425
+ console.log("\nDetected project configuration:\n");
1426
+ console.log(` Project structure: ${config.useSrc ? "src/ (e.g., src/app/, src/db/)" : "root (e.g., app/, db/)"}`);
1427
+ console.log(` Path alias: ${config.alias}/`);
1428
+ console.log(` App directory: ${config.appPath}/`);
1429
+ console.log(` DB directory: ${config.dbPath}/`);
1430
+ console.log(` Database dialect: ${dialect}`);
1431
+ console.log();
1432
+ console.log("Imports will use:");
1433
+ console.log(` DB: ${config.alias}/${config.dbPath.replace(/^src\//, "")}`);
1434
+ console.log(` Schema: ${config.alias}/${config.dbPath.replace(/^src\//, "")}/schema`);
1435
+ console.log();
1436
+ });
1437
+ program.parse();