brizzle 0.2.3

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