brizzle 0.2.9 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +23 -2
  2. package/dist/index.js +1340 -434
  3. package/package.json +18 -1
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { program } from "commander";
5
5
  import { createRequire } from "module";
6
6
 
7
7
  // src/generators/model.ts
8
- import * as path4 from "path";
8
+ import * as path5 from "path";
9
9
 
10
10
  // src/lib/types.ts
11
11
  var VALID_FIELD_TYPES = [
@@ -27,11 +27,71 @@ var VALID_FIELD_TYPES = [
27
27
 
28
28
  // src/lib/config.ts
29
29
  import * as fs from "fs";
30
+ import * as path2 from "path";
31
+
32
+ // src/lib/logger.ts
30
33
  import * as path from "path";
34
+ var log = {
35
+ /** Log file creation (green) */
36
+ create: (filePath) => {
37
+ const relative2 = path.relative(process.cwd(), filePath);
38
+ console.log(` \x1B[32mcreate\x1B[0m ${relative2}`);
39
+ },
40
+ /** Log file overwrite with --force (yellow) */
41
+ force: (filePath) => {
42
+ const relative2 = path.relative(process.cwd(), filePath);
43
+ console.log(` \x1B[33mforce\x1B[0m ${relative2}`);
44
+ },
45
+ /** Log file skip (exists, no --force) (yellow) */
46
+ skip: (filePath) => {
47
+ const relative2 = path.relative(process.cwd(), filePath);
48
+ console.log(` \x1B[33mskip\x1B[0m ${relative2}`);
49
+ },
50
+ /** Log file/directory removal (red) */
51
+ remove: (filePath) => {
52
+ const relative2 = path.relative(process.cwd(), filePath);
53
+ console.log(` \x1B[31mremove\x1B[0m ${relative2}`);
54
+ },
55
+ /** Log file not found (yellow) */
56
+ notFound: (filePath) => {
57
+ const relative2 = path.relative(process.cwd(), filePath);
58
+ console.log(` \x1B[33mnot found\x1B[0m ${relative2}`);
59
+ },
60
+ /** Log dry-run file creation (cyan) */
61
+ wouldCreate: (filePath) => {
62
+ const relative2 = path.relative(process.cwd(), filePath);
63
+ console.log(`\x1B[36mwould create\x1B[0m ${relative2}`);
64
+ },
65
+ /** Log dry-run file overwrite (cyan) */
66
+ wouldForce: (filePath) => {
67
+ const relative2 = path.relative(process.cwd(), filePath);
68
+ console.log(` \x1B[36mwould force\x1B[0m ${relative2}`);
69
+ },
70
+ /** Log dry-run file removal (cyan) */
71
+ wouldRemove: (filePath) => {
72
+ const relative2 = path.relative(process.cwd(), filePath);
73
+ console.log(`\x1B[36mwould remove\x1B[0m ${relative2}`);
74
+ },
75
+ /** Log error message (red) */
76
+ error: (message) => {
77
+ console.error(`\x1B[31mError:\x1B[0m ${message}`);
78
+ },
79
+ /** Log warning message (yellow) */
80
+ warn: (message) => {
81
+ console.warn(`\x1B[33mWarning:\x1B[0m ${message}`);
82
+ },
83
+ /** Log info message (no color) */
84
+ info: (message) => {
85
+ console.log(message);
86
+ }
87
+ };
88
+
89
+ // src/lib/config.ts
31
90
  var cachedProjectConfig = null;
32
91
  function detectDialect() {
33
- const configPath = path.join(process.cwd(), "drizzle.config.ts");
92
+ const configPath = path2.join(process.cwd(), "drizzle.config.ts");
34
93
  if (!fs.existsSync(configPath)) {
94
+ log.warn("drizzle.config.ts not found, defaulting to sqlite dialect");
35
95
  return "sqlite";
36
96
  }
37
97
  const content = fs.readFileSync(configPath, "utf-8");
@@ -52,9 +112,9 @@ function detectProjectConfig() {
52
112
  return cachedProjectConfig;
53
113
  }
54
114
  const cwd = process.cwd();
55
- const useSrc = fs.existsSync(path.join(cwd, "src", "app"));
115
+ const useSrc = fs.existsSync(path2.join(cwd, "src", "app"));
56
116
  let alias = "@";
57
- const tsconfigPath = path.join(cwd, "tsconfig.json");
117
+ const tsconfigPath = path2.join(cwd, "tsconfig.json");
58
118
  if (fs.existsSync(tsconfigPath)) {
59
119
  try {
60
120
  const content = fs.readFileSync(tsconfigPath, "utf-8");
@@ -76,7 +136,7 @@ function detectProjectConfig() {
76
136
  let dbPath = useSrc ? "src/db" : "db";
77
137
  const possibleDbPaths = useSrc ? ["src/db", "src/lib/db", "src/server/db"] : ["db", "lib/db", "server/db"];
78
138
  for (const possiblePath of possibleDbPaths) {
79
- if (fs.existsSync(path.join(cwd, possiblePath))) {
139
+ if (fs.existsSync(path2.join(cwd, possiblePath))) {
80
140
  dbPath = possiblePath;
81
141
  break;
82
142
  }
@@ -95,55 +155,23 @@ function getSchemaImport() {
95
155
  }
96
156
  function getAppPath() {
97
157
  const config = detectProjectConfig();
98
- return path.join(process.cwd(), config.appPath);
158
+ return path2.join(process.cwd(), config.appPath);
99
159
  }
100
160
  function getDbPath() {
101
161
  const config = detectProjectConfig();
102
- return path.join(process.cwd(), config.dbPath);
162
+ return path2.join(process.cwd(), config.dbPath);
163
+ }
164
+ function detectPackageManager() {
165
+ const cwd = process.cwd();
166
+ if (fs.existsSync(path2.join(cwd, "bun.lockb"))) return "bun";
167
+ if (fs.existsSync(path2.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
168
+ if (fs.existsSync(path2.join(cwd, "yarn.lock"))) return "yarn";
169
+ return "npm";
170
+ }
171
+ function getRunCommand() {
172
+ const pm = detectPackageManager();
173
+ return pm === "npm" ? "npm run" : pm;
103
174
  }
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
175
 
148
176
  // src/lib/strings.ts
149
177
  import pluralizeLib from "pluralize";
@@ -188,7 +216,107 @@ function createModelContext(name) {
188
216
  };
189
217
  }
190
218
 
219
+ // src/lib/files.ts
220
+ import * as fs2 from "fs";
221
+ import * as path3 from "path";
222
+ function writeFile(filePath, content, options = {}) {
223
+ const exists = fs2.existsSync(filePath);
224
+ if (exists && !options.force) {
225
+ log.skip(filePath);
226
+ return false;
227
+ }
228
+ if (options.dryRun) {
229
+ if (exists && options.force) {
230
+ log.wouldForce(filePath);
231
+ } else {
232
+ log.wouldCreate(filePath);
233
+ }
234
+ return true;
235
+ }
236
+ const dir = path3.dirname(filePath);
237
+ if (!fs2.existsSync(dir)) {
238
+ fs2.mkdirSync(dir, { recursive: true });
239
+ }
240
+ fs2.writeFileSync(filePath, content);
241
+ if (exists && options.force) {
242
+ log.force(filePath);
243
+ } else {
244
+ log.create(filePath);
245
+ }
246
+ return true;
247
+ }
248
+ function deleteDirectory(dirPath, options = {}) {
249
+ if (!fs2.existsSync(dirPath)) {
250
+ log.notFound(dirPath);
251
+ return false;
252
+ }
253
+ if (options.dryRun) {
254
+ log.wouldRemove(dirPath);
255
+ return true;
256
+ }
257
+ fs2.rmSync(dirPath, { recursive: true });
258
+ log.remove(dirPath);
259
+ return true;
260
+ }
261
+ function fileExists(filePath) {
262
+ return fs2.existsSync(filePath);
263
+ }
264
+ function readFile(filePath) {
265
+ return fs2.readFileSync(filePath, "utf-8");
266
+ }
267
+ function escapeRegExp(str) {
268
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
269
+ }
270
+ function modelExistsInSchema(tableName) {
271
+ const schemaPath = path3.join(getDbPath(), "schema.ts");
272
+ if (!fs2.existsSync(schemaPath)) {
273
+ return false;
274
+ }
275
+ const content = fs2.readFileSync(schemaPath, "utf-8");
276
+ const pattern = new RegExp(
277
+ `(?:sqliteTable|pgTable|mysqlTable)\\s*\\(\\s*["']${escapeRegExp(tableName)}["']`
278
+ );
279
+ return pattern.test(content);
280
+ }
281
+ function removeModelFromSchemaContent(content, tableName) {
282
+ const lines = content.split("\n");
283
+ const tablePattern = new RegExp(
284
+ `^export\\s+const\\s+\\w+\\s*=\\s*(?:sqliteTable|pgTable|mysqlTable)\\s*\\(\\s*["']${escapeRegExp(tableName)}["']`
285
+ );
286
+ let startIdx = -1;
287
+ let endIdx = -1;
288
+ let braceCount = 0;
289
+ let foundOpenBrace = false;
290
+ for (let i = 0; i < lines.length; i++) {
291
+ if (startIdx === -1) {
292
+ if (tablePattern.test(lines[i])) {
293
+ startIdx = i;
294
+ } else {
295
+ continue;
296
+ }
297
+ }
298
+ for (const char of lines[i]) {
299
+ if (char === "{") {
300
+ braceCount++;
301
+ foundOpenBrace = true;
302
+ } else if (char === "}") {
303
+ braceCount--;
304
+ }
305
+ }
306
+ if (foundOpenBrace && braceCount === 0) {
307
+ endIdx = i;
308
+ break;
309
+ }
310
+ }
311
+ if (startIdx === -1 || endIdx === -1) {
312
+ return content;
313
+ }
314
+ lines.splice(startIdx, endIdx - startIdx + 1);
315
+ return lines.join("\n").replace(/\n{3,}/g, "\n\n");
316
+ }
317
+
191
318
  // src/lib/validation.ts
319
+ import * as path4 from "path";
192
320
  var SQL_RESERVED_WORDS = [
193
321
  // SQL keywords
194
322
  "select",
@@ -319,9 +447,7 @@ function validateFieldDefinition(fieldDef) {
319
447
  const values = enumValues.split(",");
320
448
  for (const value of values) {
321
449
  if (!value) {
322
- throw new Error(
323
- `Enum field "${name}" has an empty value. Values must not be empty.`
324
- );
450
+ throw new Error(`Enum field "${name}" has an empty value. Values must not be empty.`);
325
451
  }
326
452
  if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(value)) {
327
453
  throw new Error(
@@ -331,9 +457,24 @@ function validateFieldDefinition(fieldDef) {
331
457
  }
332
458
  const unique = new Set(values);
333
459
  if (unique.size !== values.length) {
334
- throw new Error(
335
- `Enum field "${name}" has duplicate values.`
336
- );
460
+ throw new Error(`Enum field "${name}" has duplicate values.`);
461
+ }
462
+ }
463
+ }
464
+ function validateReferences(fields) {
465
+ const schemaPath = path4.join(getDbPath(), "schema.ts");
466
+ if (!fileExists(schemaPath)) {
467
+ return;
468
+ }
469
+ const schemaContent = readFile(schemaPath);
470
+ for (const field of fields) {
471
+ if (field.isReference && field.referenceTo) {
472
+ const tableName = toSnakeCase(pluralize(field.referenceTo));
473
+ if (!schemaContent.includes(`"${tableName}"`)) {
474
+ log.warn(
475
+ `Referenced model "${field.referenceTo}" (table "${tableName}") not found in schema. Make sure to create it before running migrations.`
476
+ );
477
+ }
337
478
  }
338
479
  }
339
480
  }
@@ -380,7 +521,7 @@ function parseFields(fields) {
380
521
  });
381
522
  }
382
523
 
383
- // src/lib/drizzle.ts
524
+ // src/lib/drizzle/types.ts
384
525
  var SQLITE_TYPE_MAP = {
385
526
  string: "text",
386
527
  text: "text",
@@ -438,16 +579,8 @@ function drizzleType(field, dialect = "sqlite") {
438
579
  const typeMap = dialect === "postgresql" ? POSTGRESQL_TYPE_MAP : dialect === "mysql" ? MYSQL_TYPE_MAP : SQLITE_TYPE_MAP;
439
580
  return typeMap[field.type] || "text";
440
581
  }
441
- function getDrizzleImport(dialect) {
442
- switch (dialect) {
443
- case "postgresql":
444
- return "drizzle-orm/pg-core";
445
- case "mysql":
446
- return "drizzle-orm/mysql-core";
447
- default:
448
- return "drizzle-orm/sqlite-core";
449
- }
450
- }
582
+
583
+ // src/lib/drizzle/columns.ts
451
584
  function getTableFunction(dialect) {
452
585
  switch (dialect) {
453
586
  case "postgresql":
@@ -506,6 +639,18 @@ function getTimestampColumns(dialect, noTimestamps = false) {
506
639
  .$defaultFn(() => new Date())`;
507
640
  }
508
641
  }
642
+
643
+ // src/lib/drizzle/imports.ts
644
+ function getDrizzleImport(dialect) {
645
+ switch (dialect) {
646
+ case "postgresql":
647
+ return "drizzle-orm/pg-core";
648
+ case "mysql":
649
+ return "drizzle-orm/mysql-core";
650
+ default:
651
+ return "drizzle-orm/sqlite-core";
652
+ }
653
+ }
509
654
  function extractImportsFromSchema(content) {
510
655
  const importMatch = content.match(/import\s*\{([^}]+)\}\s*from\s*["']drizzle-orm\/[^"']+["']/);
511
656
  if (!importMatch) {
@@ -571,103 +716,197 @@ function getRequiredImports(fields, dialect, options = {}) {
571
716
  return Array.from(types);
572
717
  }
573
718
 
574
- // src/lib/files.ts
575
- import * as fs2 from "fs";
576
- import * as path3 from "path";
577
- function writeFile(filePath, content, options = {}) {
578
- const exists = fs2.existsSync(filePath);
579
- if (exists && !options.force) {
580
- log.skip(filePath);
581
- return false;
582
- }
583
- if (options.dryRun) {
584
- if (exists && options.force) {
585
- log.wouldForce(filePath);
586
- } else {
587
- log.wouldCreate(filePath);
588
- }
589
- return true;
590
- }
591
- const dir = path3.dirname(filePath);
592
- if (!fs2.existsSync(dir)) {
593
- fs2.mkdirSync(dir, { recursive: true });
594
- }
595
- fs2.writeFileSync(filePath, content);
596
- if (exists && options.force) {
597
- log.force(filePath);
598
- } else {
599
- log.create(filePath);
600
- }
601
- return true;
719
+ // src/lib/forms.ts
720
+ function createFieldContext(field, camelName, withDefault) {
721
+ return {
722
+ field,
723
+ label: toPascalCase(field.name),
724
+ optionalLabel: field.nullable ? ` <span className="text-zinc-400 dark:text-zinc-500">(optional)</span>` : "",
725
+ required: field.nullable ? "" : " required",
726
+ defaultValue: withDefault ? ` defaultValue={${camelName}.${field.name}}` : ""
727
+ };
602
728
  }
603
- function deleteDirectory(dirPath, options = {}) {
604
- if (!fs2.existsSync(dirPath)) {
605
- log.notFound(dirPath);
606
- return false;
607
- }
608
- if (options.dryRun) {
609
- log.wouldRemove(dirPath);
610
- return true;
611
- }
612
- fs2.rmSync(dirPath, { recursive: true });
613
- log.remove(dirPath);
614
- return true;
729
+ function generateTextareaField(ctx) {
730
+ const { field, label, optionalLabel, required, defaultValue } = ctx;
731
+ const rows = field.type === "json" ? 6 : 4;
732
+ const placeholder = field.type === "json" ? ` placeholder="{}"` : "";
733
+ return ` <div>
734
+ <label htmlFor="${field.name}" className="block text-sm font-medium text-zinc-700 dark:text-zinc-300">
735
+ ${label}${optionalLabel}
736
+ </label>
737
+ <textarea
738
+ id="${field.name}"
739
+ name="${field.name}"
740
+ rows={${rows}}
741
+ className="mt-1.5 block w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-zinc-900 placeholder:text-zinc-400 focus:border-zinc-400 focus:outline-none dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-50 dark:placeholder:text-zinc-500 dark:focus:border-zinc-500 resize-none"${defaultValue}${placeholder}${required}
742
+ />
743
+ </div>`;
615
744
  }
616
- function fileExists(filePath) {
617
- return fs2.existsSync(filePath);
745
+ function generateCheckboxField(ctx, camelName, withDefault) {
746
+ const { field, label } = ctx;
747
+ const defaultChecked = withDefault ? ` defaultChecked={${camelName}.${field.name}}` : "";
748
+ return ` <div className="flex items-center gap-2">
749
+ <input
750
+ type="checkbox"
751
+ id="${field.name}"
752
+ name="${field.name}"
753
+ className="h-4 w-4 rounded border-zinc-300 text-zinc-900 focus:ring-0 focus:ring-offset-0 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-50"${defaultChecked}
754
+ />
755
+ <label htmlFor="${field.name}" className="text-sm font-medium text-zinc-700 dark:text-zinc-300">
756
+ ${label}
757
+ </label>
758
+ </div>`;
618
759
  }
619
- function readFile(filePath) {
620
- return fs2.readFileSync(filePath, "utf-8");
760
+ function generateNumberField(ctx, step) {
761
+ const { field, label, optionalLabel, required, defaultValue } = ctx;
762
+ const stepAttr = step ? `
763
+ step="${step}"` : "";
764
+ return ` <div>
765
+ <label htmlFor="${field.name}" className="block text-sm font-medium text-zinc-700 dark:text-zinc-300">
766
+ ${label}${optionalLabel}
767
+ </label>
768
+ <input
769
+ type="number"${stepAttr}
770
+ id="${field.name}"
771
+ name="${field.name}"
772
+ className="mt-1.5 block w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-zinc-900 placeholder:text-zinc-400 focus:border-zinc-400 focus:outline-none dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-50 dark:placeholder:text-zinc-500 dark:focus:border-zinc-500"${defaultValue}${required}
773
+ />
774
+ </div>`;
621
775
  }
622
- function escapeRegExp(str) {
623
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
776
+ function generateDateField(ctx, camelName, withDefault) {
777
+ const { field, label, optionalLabel, required } = ctx;
778
+ const dateDefault = withDefault ? ` defaultValue={${camelName}.${field.name}?.toISOString().split("T")[0]}` : "";
779
+ return ` <div>
780
+ <label htmlFor="${field.name}" className="block text-sm font-medium text-zinc-700 dark:text-zinc-300">
781
+ ${label}${optionalLabel}
782
+ </label>
783
+ <input
784
+ type="date"
785
+ id="${field.name}"
786
+ name="${field.name}"
787
+ className="mt-1.5 block w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-zinc-900 placeholder:text-zinc-400 focus:border-zinc-400 focus:outline-none dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-50 dark:placeholder:text-zinc-500 dark:focus:border-zinc-500"${dateDefault}${required}
788
+ />
789
+ </div>`;
624
790
  }
625
- function modelExistsInSchema(tableName) {
626
- const schemaPath = path3.join(getDbPath(), "schema.ts");
627
- if (!fs2.existsSync(schemaPath)) {
628
- return false;
629
- }
630
- const content = fs2.readFileSync(schemaPath, "utf-8");
631
- const pattern = new RegExp(
632
- `(?:sqliteTable|pgTable|mysqlTable)\\s*\\(\\s*["']${escapeRegExp(tableName)}["']`
633
- );
634
- return pattern.test(content);
791
+ function generateDatetimeField(ctx, camelName, withDefault) {
792
+ const { field, label, optionalLabel, required } = ctx;
793
+ const dateDefault = withDefault ? ` defaultValue={${camelName}.${field.name}?.toISOString().slice(0, 16)}` : "";
794
+ return ` <div>
795
+ <label htmlFor="${field.name}" className="block text-sm font-medium text-zinc-700 dark:text-zinc-300">
796
+ ${label}${optionalLabel}
797
+ </label>
798
+ <input
799
+ type="datetime-local"
800
+ id="${field.name}"
801
+ name="${field.name}"
802
+ className="mt-1.5 block w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-zinc-900 placeholder:text-zinc-400 focus:border-zinc-400 focus:outline-none dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-50 dark:placeholder:text-zinc-500 dark:focus:border-zinc-500"${dateDefault}${required}
803
+ />
804
+ </div>`;
635
805
  }
636
- function removeModelFromSchemaContent(content, tableName) {
637
- const lines = content.split("\n");
638
- const tablePattern = new RegExp(
639
- `^export\\s+const\\s+\\w+\\s*=\\s*(?:sqliteTable|pgTable|mysqlTable)\\s*\\(\\s*["']${escapeRegExp(tableName)}["']`
640
- );
641
- let startIdx = -1;
642
- let endIdx = -1;
643
- let braceCount = 0;
644
- let foundOpenBrace = false;
645
- for (let i = 0; i < lines.length; i++) {
646
- if (startIdx === -1) {
647
- if (tablePattern.test(lines[i])) {
648
- startIdx = i;
649
- } else {
650
- continue;
806
+ function generateSelectField(ctx) {
807
+ const { field, label, optionalLabel, required, defaultValue } = ctx;
808
+ const options = field.enumValues.map(
809
+ (v) => ` <option value="${escapeString(v)}">${toPascalCase(v)}</option>`
810
+ ).join("\n");
811
+ return ` <div>
812
+ <label htmlFor="${field.name}" className="block text-sm font-medium text-zinc-700 dark:text-zinc-300">
813
+ ${label}${optionalLabel}
814
+ </label>
815
+ <select
816
+ id="${field.name}"
817
+ name="${field.name}"
818
+ className="mt-1.5 block w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-zinc-900 focus:border-zinc-400 focus:outline-none dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-50 dark:focus:border-zinc-500"${defaultValue}${required}
819
+ >
820
+ ${options}
821
+ </select>
822
+ </div>`;
823
+ }
824
+ function generateTextField(ctx) {
825
+ const { field, label, optionalLabel, required, defaultValue } = ctx;
826
+ return ` <div>
827
+ <label htmlFor="${field.name}" className="block text-sm font-medium text-zinc-700 dark:text-zinc-300">
828
+ ${label}${optionalLabel}
829
+ </label>
830
+ <input
831
+ type="text"
832
+ id="${field.name}"
833
+ name="${field.name}"
834
+ className="mt-1.5 block w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-zinc-900 placeholder:text-zinc-400 focus:border-zinc-400 focus:outline-none dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-50 dark:placeholder:text-zinc-500 dark:focus:border-zinc-500"${defaultValue}${required}
835
+ />
836
+ </div>`;
837
+ }
838
+ function generateFormField(field, camelName, withDefault = false) {
839
+ const ctx = createFieldContext(field, camelName, withDefault);
840
+ switch (field.type) {
841
+ case "text":
842
+ case "json":
843
+ return generateTextareaField(ctx);
844
+ case "boolean":
845
+ case "bool":
846
+ return generateCheckboxField(ctx, camelName, withDefault);
847
+ case "integer":
848
+ case "int":
849
+ case "bigint":
850
+ return generateNumberField(ctx);
851
+ case "float":
852
+ return generateNumberField(ctx, "any");
853
+ case "decimal":
854
+ return generateNumberField(ctx, "0.01");
855
+ case "date":
856
+ return generateDateField(ctx, camelName, withDefault);
857
+ case "datetime":
858
+ case "timestamp":
859
+ return generateDatetimeField(ctx, camelName, withDefault);
860
+ default:
861
+ if (field.isEnum && field.enumValues) {
862
+ return generateSelectField(ctx);
651
863
  }
864
+ return generateTextField(ctx);
865
+ }
866
+ }
867
+ function formDataValue(field) {
868
+ const getValue = `formData.get("${field.name}")`;
869
+ const asString = `${getValue} as string`;
870
+ if (field.nullable) {
871
+ if (field.type === "boolean" || field.type === "bool") {
872
+ return `${getValue} === "on" ? true : null`;
652
873
  }
653
- for (const char of lines[i]) {
654
- if (char === "{") {
655
- braceCount++;
656
- foundOpenBrace = true;
657
- } else if (char === "}") {
658
- braceCount--;
659
- }
874
+ if (field.type === "integer" || field.type === "int" || field.type === "bigint") {
875
+ return `${getValue} ? parseInt(${asString}) : null`;
660
876
  }
661
- if (foundOpenBrace && braceCount === 0) {
662
- endIdx = i;
663
- break;
877
+ if (field.type === "float") {
878
+ return `${getValue} ? parseFloat(${asString}) : null`;
879
+ }
880
+ if (field.type === "decimal") {
881
+ return `${getValue} ? ${asString} : null`;
882
+ }
883
+ if (field.type === "datetime" || field.type === "timestamp" || field.type === "date") {
884
+ return `${getValue} ? new Date(${asString}) : null`;
885
+ }
886
+ if (field.type === "json") {
887
+ return `${getValue} ? JSON.parse(${asString}) : null`;
664
888
  }
889
+ return `${getValue} ? ${asString} : null`;
665
890
  }
666
- if (startIdx === -1 || endIdx === -1) {
667
- return content;
891
+ if (field.type === "boolean" || field.type === "bool") {
892
+ return `${getValue} === "on"`;
668
893
  }
669
- lines.splice(startIdx, endIdx - startIdx + 1);
670
- return lines.join("\n").replace(/\n{3,}/g, "\n\n");
894
+ if (field.type === "integer" || field.type === "int" || field.type === "bigint") {
895
+ return `parseInt(${asString})`;
896
+ }
897
+ if (field.type === "float") {
898
+ return `parseFloat(${asString})`;
899
+ }
900
+ if (field.type === "decimal") {
901
+ return asString;
902
+ }
903
+ if (field.type === "datetime" || field.type === "timestamp" || field.type === "date") {
904
+ return `new Date(${asString})`;
905
+ }
906
+ if (field.type === "json") {
907
+ return `JSON.parse(${asString})`;
908
+ }
909
+ return asString;
671
910
  }
672
911
 
673
912
  // src/generators/model.ts
@@ -676,14 +915,21 @@ function generateModel(name, fieldArgs, options = {}) {
676
915
  const ctx = createModelContext(name);
677
916
  const fields = parseFields(fieldArgs);
678
917
  const dialect = detectDialect();
918
+ validateReferences(fields);
679
919
  if (modelExistsInSchema(ctx.tableName) && !options.force) {
680
920
  throw new Error(
681
921
  `Model "${ctx.pascalName}" already exists in schema. Use --force to regenerate.`
682
922
  );
683
923
  }
684
- const schemaPath = path4.join(getDbPath(), "schema.ts");
924
+ const schemaPath = path5.join(getDbPath(), "schema.ts");
685
925
  if (!fileExists(schemaPath)) {
686
- const schemaContent = generateSchemaContent(ctx.camelPlural, ctx.tableName, fields, dialect, options);
926
+ const schemaContent = generateSchemaContent(
927
+ ctx.camelPlural,
928
+ ctx.tableName,
929
+ fields,
930
+ dialect,
931
+ options
932
+ );
687
933
  writeFile(schemaPath, schemaContent, options);
688
934
  } else if (modelExistsInSchema(ctx.tableName)) {
689
935
  replaceInSchema(schemaPath, ctx.camelPlural, ctx.tableName, fields, dialect, options);
@@ -790,12 +1036,14 @@ function generateEnumField(field, columnName, dialect) {
790
1036
  switch (dialect) {
791
1037
  case "postgresql":
792
1038
  return ` ${field.name}: ${field.name}Enum("${columnName}")${modifiers},`;
793
- case "mysql":
1039
+ case "mysql": {
794
1040
  const mysqlValues = values.map((v) => `"${escapeString(v)}"`).join(", ");
795
1041
  return ` ${field.name}: mysqlEnum("${columnName}", [${mysqlValues}])${modifiers},`;
796
- default:
1042
+ }
1043
+ default: {
797
1044
  const sqliteValues = values.map((v) => `"${escapeString(v)}"`).join(", ");
798
1045
  return ` ${field.name}: text("${columnName}", { enum: [${sqliteValues}] })${modifiers},`;
1046
+ }
799
1047
  }
800
1048
  }
801
1049
  function appendToSchema(schemaPath, modelName, tableName, fields, dialect, options) {
@@ -819,23 +1067,23 @@ function replaceInSchema(schemaPath, modelName, tableName, fields, dialect, opti
819
1067
  }
820
1068
 
821
1069
  // src/generators/actions.ts
822
- import * as path5 from "path";
1070
+ import * as path6 from "path";
823
1071
  function generateActions(name, options = {}) {
824
1072
  validateModelName(name);
825
1073
  const ctx = createModelContext(name);
826
- const actionsPath = path5.join(
827
- getAppPath(),
828
- ctx.kebabPlural,
829
- "actions.ts"
830
- );
831
- const content = generateActionsContent(ctx, options);
1074
+ const dialect = detectDialect();
1075
+ const actionsPath = path6.join(getAppPath(), ctx.kebabPlural, "actions.ts");
1076
+ const content = generateActionsContent(ctx, options, dialect);
832
1077
  writeFile(actionsPath, content, options);
833
1078
  }
834
- function generateActionsContent(ctx, options = {}) {
1079
+ function generateActionsContent(ctx, options = {}, dialect = "sqlite") {
835
1080
  const { pascalName, pascalPlural, camelPlural, kebabPlural } = ctx;
836
1081
  const dbImport = getDbImport();
837
1082
  const schemaImport = getSchemaImport();
838
1083
  const idType = options.uuid ? "string" : "number";
1084
+ if (dialect === "mysql") {
1085
+ return generateMySqlActions(ctx, dbImport, schemaImport, idType);
1086
+ }
839
1087
  return `"use server";
840
1088
 
841
1089
  import { db } from "${dbImport}";
@@ -890,49 +1138,77 @@ export async function delete${pascalName}(id: ${idType}) {
890
1138
  }
891
1139
  `;
892
1140
  }
1141
+ function generateMySqlActions(ctx, dbImport, schemaImport, idType) {
1142
+ const { pascalName, pascalPlural, camelPlural, kebabPlural } = ctx;
1143
+ return `"use server";
893
1144
 
894
- // src/generators/scaffold.ts
895
- import * as path6 from "path";
896
- function generateScaffold(name, fieldArgs, options = {}) {
897
- validateModelName(name);
898
- const ctx = createModelContext(name);
899
- const fields = parseFields(fieldArgs);
900
- const prefix = options.dryRun ? "[dry-run] " : "";
901
- log.info(`
902
- ${prefix}Scaffolding ${ctx.pascalName}...
903
- `);
904
- generateModel(ctx.singularName, fieldArgs, options);
905
- generateActions(ctx.singularName, options);
906
- generatePages(ctx, fields, options);
907
- log.info(`
908
- Next steps:`);
909
- log.info(` 1. Run 'pnpm db:push' to update the database`);
910
- log.info(` 2. Run 'pnpm dev' and visit /${ctx.kebabPlural}`);
911
- }
912
- function generatePages(ctx, fields, options = {}) {
913
- const { pascalName, pascalPlural, camelName, kebabPlural } = ctx;
914
- const basePath = path6.join(getAppPath(), kebabPlural);
915
- writeFile(
916
- path6.join(basePath, "page.tsx"),
917
- generateIndexPage(pascalName, pascalPlural, camelName, kebabPlural, fields),
918
- options
919
- );
920
- writeFile(
921
- path6.join(basePath, "new", "page.tsx"),
922
- generateNewPage(pascalName, camelName, kebabPlural, fields),
923
- options
924
- );
925
- writeFile(
926
- path6.join(basePath, "[id]", "page.tsx"),
927
- generateShowPage(pascalName, pascalPlural, camelName, kebabPlural, fields, options),
928
- options
929
- );
930
- writeFile(
931
- path6.join(basePath, "[id]", "edit", "page.tsx"),
932
- generateEditPage(pascalName, camelName, kebabPlural, fields, options),
933
- options
934
- );
1145
+ import { db } from "${dbImport}";
1146
+ import { ${camelPlural} } from "${schemaImport}";
1147
+ import { eq, desc } from "drizzle-orm";
1148
+ import { revalidatePath } from "next/cache";
1149
+
1150
+ export type ${pascalName} = typeof ${camelPlural}.$inferSelect;
1151
+ export type New${pascalName} = typeof ${camelPlural}.$inferInsert;
1152
+
1153
+ export async function get${pascalPlural}() {
1154
+ return db.select().from(${camelPlural}).orderBy(desc(${camelPlural}.createdAt));
1155
+ }
1156
+
1157
+ export async function get${pascalName}(id: ${idType}) {
1158
+ const result = await db
1159
+ .select()
1160
+ .from(${camelPlural})
1161
+ .where(eq(${camelPlural}.id, id))
1162
+ .limit(1);
1163
+
1164
+ return result[0] ?? null;
1165
+ }
1166
+
1167
+ export async function create${pascalName}(data: Omit<New${pascalName}, "id" | "createdAt" | "updatedAt">) {
1168
+ const inserted = await db.insert(${camelPlural}).values(data).$returningId();
1169
+ const result = await db
1170
+ .select()
1171
+ .from(${camelPlural})
1172
+ .where(eq(${camelPlural}.id, inserted[0].id))
1173
+ .limit(1);
1174
+
1175
+ revalidatePath("/${kebabPlural}");
1176
+
1177
+ return result[0];
1178
+ }
1179
+
1180
+ export async function update${pascalName}(
1181
+ id: ${idType},
1182
+ data: Partial<Omit<New${pascalName}, "id" | "createdAt" | "updatedAt">>
1183
+ ) {
1184
+ await db
1185
+ .update(${camelPlural})
1186
+ .set({ ...data, updatedAt: new Date() })
1187
+ .where(eq(${camelPlural}.id, id));
1188
+
1189
+ const result = await db
1190
+ .select()
1191
+ .from(${camelPlural})
1192
+ .where(eq(${camelPlural}.id, id))
1193
+ .limit(1);
1194
+
1195
+ revalidatePath("/${kebabPlural}");
1196
+
1197
+ return result[0];
1198
+ }
1199
+
1200
+ export async function delete${pascalName}(id: ${idType}) {
1201
+ await db.delete(${camelPlural}).where(eq(${camelPlural}.id, id));
1202
+
1203
+ revalidatePath("/${kebabPlural}");
1204
+ }
1205
+ `;
935
1206
  }
1207
+
1208
+ // src/generators/scaffold.ts
1209
+ import * as path7 from "path";
1210
+
1211
+ // src/generators/pages/index-page.ts
936
1212
  function generateIndexPage(pascalName, pascalPlural, camelName, kebabPlural, fields) {
937
1213
  const displayField = fields[0]?.name || "id";
938
1214
  return `import Link from "next/link";
@@ -993,6 +1269,8 @@ export default async function ${pascalPlural}Page() {
993
1269
  }
994
1270
  `;
995
1271
  }
1272
+
1273
+ // src/generators/pages/new-page.ts
996
1274
  function generateNewPage(pascalName, camelName, kebabPlural, fields) {
997
1275
  return `import { redirect } from "next/navigation";
998
1276
  import Link from "next/link";
@@ -1036,7 +1314,9 @@ ${fields.map((f) => generateFormField(f, camelName)).join("\n\n")}
1036
1314
  }
1037
1315
  `;
1038
1316
  }
1039
- function generateShowPage(pascalName, _pascalPlural, camelName, kebabPlural, fields, options = {}) {
1317
+
1318
+ // src/generators/pages/show-page.ts
1319
+ function generateShowPage(pascalName, camelName, kebabPlural, fields, options = {}) {
1040
1320
  const idHandling = options.uuid ? `const ${camelName} = await get${pascalName}(id);` : `const numericId = Number(id);
1041
1321
 
1042
1322
  if (isNaN(numericId)) {
@@ -1097,6 +1377,8 @@ ${fields.map(
1097
1377
  }
1098
1378
  `;
1099
1379
  }
1380
+
1381
+ // src/generators/pages/edit-page.ts
1100
1382
  function generateEditPage(pascalName, camelName, kebabPlural, fields, options = {}) {
1101
1383
  const idHandling = options.uuid ? `const ${camelName} = await get${pascalName}(id);` : `const numericId = Number(id);
1102
1384
 
@@ -1134,219 +1416,73 @@ ${fields.map((f) => ` ${f.name}: ${formDataValue(f)},`).join("\n")}
1134
1416
 
1135
1417
  return (
1136
1418
  <div className="mx-auto max-w-xl px-6 py-12">
1137
- <h1 className="mb-8 text-2xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-50">Edit ${pascalName}</h1>
1138
-
1139
- <form action={handleUpdate} className="space-y-5">
1140
- ${fields.map((f) => generateFormField(f, camelName, true)).join("\n\n")}
1141
-
1142
- <div className="flex gap-3 pt-4">
1143
- <button
1144
- type="submit"
1145
- className="flex h-10 items-center rounded-full bg-zinc-900 px-4 text-sm font-medium text-white transition-colors hover:bg-zinc-700 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-200"
1146
- >
1147
- Update ${pascalName}
1148
- </button>
1149
- <Link
1150
- href="/${kebabPlural}"
1151
- className="flex h-10 items-center rounded-full border border-zinc-200 px-4 text-sm font-medium text-zinc-600 transition-colors hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800"
1152
- >
1153
- Cancel
1154
- </Link>
1155
- </div>
1156
- </form>
1157
- </div>
1158
- );
1159
- }
1160
- `;
1161
- }
1162
- function createFieldContext(field, camelName, withDefault) {
1163
- return {
1164
- field,
1165
- label: toPascalCase(field.name),
1166
- optionalLabel: field.nullable ? ` <span className="text-zinc-400 dark:text-zinc-500">(optional)</span>` : "",
1167
- required: field.nullable ? "" : " required",
1168
- defaultValue: withDefault ? ` defaultValue={${camelName}.${field.name}}` : ""
1169
- };
1170
- }
1171
- function generateTextareaField(ctx) {
1172
- const { field, label, optionalLabel, required, defaultValue } = ctx;
1173
- const rows = field.type === "json" ? 6 : 4;
1174
- const placeholder = field.type === "json" ? ` placeholder="{}"` : "";
1175
- return ` <div>
1176
- <label htmlFor="${field.name}" className="block text-sm font-medium text-zinc-700 dark:text-zinc-300">
1177
- ${label}${optionalLabel}
1178
- </label>
1179
- <textarea
1180
- id="${field.name}"
1181
- name="${field.name}"
1182
- rows={${rows}}
1183
- className="mt-1.5 block w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-zinc-900 placeholder:text-zinc-400 focus:border-zinc-400 focus:outline-none dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-50 dark:placeholder:text-zinc-500 dark:focus:border-zinc-500 resize-none"${defaultValue}${placeholder}${required}
1184
- />
1185
- </div>`;
1186
- }
1187
- function generateCheckboxField(ctx, camelName, withDefault) {
1188
- const { field, label } = ctx;
1189
- const defaultChecked = withDefault ? ` defaultChecked={${camelName}.${field.name}}` : "";
1190
- return ` <div className="flex items-center gap-2">
1191
- <input
1192
- type="checkbox"
1193
- id="${field.name}"
1194
- name="${field.name}"
1195
- className="h-4 w-4 rounded border-zinc-300 text-zinc-900 focus:ring-0 focus:ring-offset-0 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-50"${defaultChecked}
1196
- />
1197
- <label htmlFor="${field.name}" className="text-sm font-medium text-zinc-700 dark:text-zinc-300">
1198
- ${label}
1199
- </label>
1200
- </div>`;
1201
- }
1202
- function generateNumberField(ctx, step) {
1203
- const { field, label, optionalLabel, required, defaultValue } = ctx;
1204
- const stepAttr = step ? `
1205
- step="${step}"` : "";
1206
- return ` <div>
1207
- <label htmlFor="${field.name}" className="block text-sm font-medium text-zinc-700 dark:text-zinc-300">
1208
- ${label}${optionalLabel}
1209
- </label>
1210
- <input
1211
- type="number"${stepAttr}
1212
- id="${field.name}"
1213
- name="${field.name}"
1214
- className="mt-1.5 block w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-zinc-900 placeholder:text-zinc-400 focus:border-zinc-400 focus:outline-none dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-50 dark:placeholder:text-zinc-500 dark:focus:border-zinc-500"${defaultValue}${required}
1215
- />
1216
- </div>`;
1217
- }
1218
- function generateDateField(ctx, camelName, withDefault) {
1219
- const { field, label, optionalLabel, required } = ctx;
1220
- const dateDefault = withDefault ? ` defaultValue={${camelName}.${field.name}?.toISOString().split("T")[0]}` : "";
1221
- return ` <div>
1222
- <label htmlFor="${field.name}" className="block text-sm font-medium text-zinc-700 dark:text-zinc-300">
1223
- ${label}${optionalLabel}
1224
- </label>
1225
- <input
1226
- type="date"
1227
- id="${field.name}"
1228
- name="${field.name}"
1229
- className="mt-1.5 block w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-zinc-900 placeholder:text-zinc-400 focus:border-zinc-400 focus:outline-none dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-50 dark:placeholder:text-zinc-500 dark:focus:border-zinc-500"${dateDefault}${required}
1230
- />
1231
- </div>`;
1232
- }
1233
- function generateDatetimeField(ctx, camelName, withDefault) {
1234
- const { field, label, optionalLabel, required } = ctx;
1235
- const dateDefault = withDefault ? ` defaultValue={${camelName}.${field.name}?.toISOString().slice(0, 16)}` : "";
1236
- return ` <div>
1237
- <label htmlFor="${field.name}" className="block text-sm font-medium text-zinc-700 dark:text-zinc-300">
1238
- ${label}${optionalLabel}
1239
- </label>
1240
- <input
1241
- type="datetime-local"
1242
- id="${field.name}"
1243
- name="${field.name}"
1244
- className="mt-1.5 block w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-zinc-900 placeholder:text-zinc-400 focus:border-zinc-400 focus:outline-none dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-50 dark:placeholder:text-zinc-500 dark:focus:border-zinc-500"${dateDefault}${required}
1245
- />
1246
- </div>`;
1247
- }
1248
- function generateSelectField(ctx) {
1249
- const { field, label, optionalLabel, required, defaultValue } = ctx;
1250
- const options = field.enumValues.map((v) => ` <option value="${escapeString(v)}">${toPascalCase(v)}</option>`).join("\n");
1251
- return ` <div>
1252
- <label htmlFor="${field.name}" className="block text-sm font-medium text-zinc-700 dark:text-zinc-300">
1253
- ${label}${optionalLabel}
1254
- </label>
1255
- <select
1256
- id="${field.name}"
1257
- name="${field.name}"
1258
- className="mt-1.5 block w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-zinc-900 focus:border-zinc-400 focus:outline-none dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-50 dark:focus:border-zinc-500"${defaultValue}${required}
1259
- >
1260
- ${options}
1261
- </select>
1262
- </div>`;
1263
- }
1264
- function generateTextField(ctx) {
1265
- const { field, label, optionalLabel, required, defaultValue } = ctx;
1266
- return ` <div>
1267
- <label htmlFor="${field.name}" className="block text-sm font-medium text-zinc-700 dark:text-zinc-300">
1268
- ${label}${optionalLabel}
1269
- </label>
1270
- <input
1271
- type="text"
1272
- id="${field.name}"
1273
- name="${field.name}"
1274
- className="mt-1.5 block w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-zinc-900 placeholder:text-zinc-400 focus:border-zinc-400 focus:outline-none dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-50 dark:placeholder:text-zinc-500 dark:focus:border-zinc-500"${defaultValue}${required}
1275
- />
1276
- </div>`;
1277
- }
1278
- function generateFormField(field, camelName, withDefault = false) {
1279
- const ctx = createFieldContext(field, camelName, withDefault);
1280
- switch (field.type) {
1281
- case "text":
1282
- case "json":
1283
- return generateTextareaField(ctx);
1284
- case "boolean":
1285
- case "bool":
1286
- return generateCheckboxField(ctx, camelName, withDefault);
1287
- case "integer":
1288
- case "int":
1289
- case "bigint":
1290
- return generateNumberField(ctx);
1291
- case "float":
1292
- return generateNumberField(ctx, "any");
1293
- case "decimal":
1294
- return generateNumberField(ctx, "0.01");
1295
- case "date":
1296
- return generateDateField(ctx, camelName, withDefault);
1297
- case "datetime":
1298
- case "timestamp":
1299
- return generateDatetimeField(ctx, camelName, withDefault);
1300
- default:
1301
- if (field.isEnum && field.enumValues) {
1302
- return generateSelectField(ctx);
1303
- }
1304
- return generateTextField(ctx);
1305
- }
1306
- }
1307
- function formDataValue(field) {
1308
- const getValue = `formData.get("${field.name}")`;
1309
- const asString = `${getValue} as string`;
1310
- if (field.nullable) {
1311
- if (field.type === "boolean" || field.type === "bool") {
1312
- return `${getValue} === "on" ? true : null`;
1313
- }
1314
- if (field.type === "integer" || field.type === "int" || field.type === "bigint") {
1315
- return `${getValue} ? parseInt(${asString}) : null`;
1316
- }
1317
- if (field.type === "float") {
1318
- return `${getValue} ? parseFloat(${asString}) : null`;
1319
- }
1320
- if (field.type === "decimal") {
1321
- return `${getValue} ? ${asString} : null`;
1322
- }
1323
- if (field.type === "datetime" || field.type === "timestamp" || field.type === "date") {
1324
- return `${getValue} ? new Date(${asString}) : null`;
1325
- }
1326
- if (field.type === "json") {
1327
- return `${getValue} ? JSON.parse(${asString}) : null`;
1328
- }
1329
- return `${getValue} ? ${asString} : null`;
1330
- }
1331
- if (field.type === "boolean" || field.type === "bool") {
1332
- return `${getValue} === "on"`;
1333
- }
1334
- if (field.type === "integer" || field.type === "int" || field.type === "bigint") {
1335
- return `parseInt(${asString})`;
1336
- }
1337
- if (field.type === "float") {
1338
- return `parseFloat(${asString})`;
1339
- }
1340
- if (field.type === "decimal") {
1341
- return asString;
1342
- }
1343
- if (field.type === "datetime" || field.type === "timestamp" || field.type === "date") {
1344
- return `new Date(${asString})`;
1345
- }
1346
- if (field.type === "json") {
1347
- return `JSON.parse(${asString})`;
1348
- }
1349
- return asString;
1419
+ <h1 className="mb-8 text-2xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-50">Edit ${pascalName}</h1>
1420
+
1421
+ <form action={handleUpdate} className="space-y-5">
1422
+ ${fields.map((f) => generateFormField(f, camelName, true)).join("\n\n")}
1423
+
1424
+ <div className="flex gap-3 pt-4">
1425
+ <button
1426
+ type="submit"
1427
+ className="flex h-10 items-center rounded-full bg-zinc-900 px-4 text-sm font-medium text-white transition-colors hover:bg-zinc-700 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-200"
1428
+ >
1429
+ Update ${pascalName}
1430
+ </button>
1431
+ <Link
1432
+ href="/${kebabPlural}"
1433
+ className="flex h-10 items-center rounded-full border border-zinc-200 px-4 text-sm font-medium text-zinc-600 transition-colors hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800"
1434
+ >
1435
+ Cancel
1436
+ </Link>
1437
+ </div>
1438
+ </form>
1439
+ </div>
1440
+ );
1441
+ }
1442
+ `;
1443
+ }
1444
+
1445
+ // src/generators/scaffold.ts
1446
+ function generateScaffold(name, fieldArgs, options = {}) {
1447
+ validateModelName(name);
1448
+ const ctx = createModelContext(name);
1449
+ const fields = parseFields(fieldArgs);
1450
+ const prefix = options.dryRun ? "[dry-run] " : "";
1451
+ log.info(`
1452
+ ${prefix}Scaffolding ${ctx.pascalName}...
1453
+ `);
1454
+ generateModel(ctx.singularName, fieldArgs, options);
1455
+ generateActions(ctx.singularName, options);
1456
+ generatePages(ctx, fields, options);
1457
+ const run = getRunCommand();
1458
+ log.info(`
1459
+ Next steps:`);
1460
+ log.info(` 1. Run '${run} db:push' to update the database`);
1461
+ log.info(` 2. Run '${run} dev' and visit /${ctx.kebabPlural}`);
1462
+ }
1463
+ function generatePages(ctx, fields, options = {}) {
1464
+ const { pascalName, pascalPlural, camelName, kebabPlural } = ctx;
1465
+ const basePath = path7.join(getAppPath(), kebabPlural);
1466
+ writeFile(
1467
+ path7.join(basePath, "page.tsx"),
1468
+ generateIndexPage(pascalName, pascalPlural, camelName, kebabPlural, fields),
1469
+ options
1470
+ );
1471
+ writeFile(
1472
+ path7.join(basePath, "new", "page.tsx"),
1473
+ generateNewPage(pascalName, camelName, kebabPlural, fields),
1474
+ options
1475
+ );
1476
+ writeFile(
1477
+ path7.join(basePath, "[id]", "page.tsx"),
1478
+ generateShowPage(pascalName, camelName, kebabPlural, fields, options),
1479
+ options
1480
+ );
1481
+ writeFile(
1482
+ path7.join(basePath, "[id]", "edit", "page.tsx"),
1483
+ generateEditPage(pascalName, camelName, kebabPlural, fields, options),
1484
+ options
1485
+ );
1350
1486
  }
1351
1487
 
1352
1488
  // src/generators/resource.ts
@@ -1359,14 +1495,15 @@ ${prefix}Generating resource ${ctx.pascalName}...
1359
1495
  `);
1360
1496
  generateModel(ctx.singularName, fieldArgs, options);
1361
1497
  generateActions(ctx.singularName, options);
1498
+ const run = getRunCommand();
1362
1499
  log.info(`
1363
1500
  Next steps:`);
1364
- log.info(` 1. Run 'pnpm db:push' to update the database`);
1501
+ log.info(` 1. Run '${run} db:push' to update the database`);
1365
1502
  log.info(` 2. Create pages in app/${ctx.kebabPlural}/`);
1366
1503
  }
1367
1504
 
1368
1505
  // src/generators/api.ts
1369
- import * as path7 from "path";
1506
+ import * as path8 from "path";
1370
1507
  function generateApi(name, fieldArgs, options = {}) {
1371
1508
  validateModelName(name);
1372
1509
  const ctx = createModelContext(name);
@@ -1376,20 +1513,21 @@ ${prefix}Generating API ${ctx.pascalName}...
1376
1513
  `);
1377
1514
  generateModel(ctx.singularName, fieldArgs, options);
1378
1515
  generateRoutes(ctx.camelPlural, ctx.kebabPlural, options);
1516
+ const run = getRunCommand();
1379
1517
  log.info(`
1380
1518
  Next steps:`);
1381
- log.info(` 1. Run 'pnpm db:push' to update the database`);
1519
+ log.info(` 1. Run '${run} db:push' to update the database`);
1382
1520
  log.info(` 2. API available at /api/${ctx.kebabPlural}`);
1383
1521
  }
1384
1522
  function generateRoutes(camelPlural, kebabPlural, options) {
1385
- const basePath = path7.join(getAppPath(), "api", kebabPlural);
1523
+ const basePath = path8.join(getAppPath(), "api", kebabPlural);
1386
1524
  writeFile(
1387
- path7.join(basePath, "route.ts"),
1525
+ path8.join(basePath, "route.ts"),
1388
1526
  generateCollectionRoute(camelPlural, kebabPlural),
1389
1527
  options
1390
1528
  );
1391
1529
  writeFile(
1392
- path7.join(basePath, "[id]", "route.ts"),
1530
+ path8.join(basePath, "[id]", "route.ts"),
1393
1531
  generateMemberRoute(camelPlural, kebabPlural, options),
1394
1532
  options
1395
1533
  );
@@ -1537,7 +1675,7 @@ export async function DELETE(request: Request, { params }: Params) {
1537
1675
  }
1538
1676
 
1539
1677
  // src/generators/destroy.ts
1540
- import * as path8 from "path";
1678
+ import * as path9 from "path";
1541
1679
  import { confirm, isCancel } from "@clack/prompts";
1542
1680
  async function destroy(name, type, buildPath, options = {}) {
1543
1681
  validateModelName(name);
@@ -1563,19 +1701,756 @@ function removeFromSchema(tableName, options) {
1563
1701
  if (!modelExistsInSchema(tableName)) {
1564
1702
  return;
1565
1703
  }
1566
- const schemaPath = path8.join(getDbPath(), "schema.ts");
1704
+ const schemaPath = path9.join(getDbPath(), "schema.ts");
1567
1705
  const content = readFile(schemaPath);
1568
1706
  const cleaned = removeModelFromSchemaContent(content, tableName);
1569
1707
  writeFile(schemaPath, cleaned, { force: true, dryRun: options.dryRun });
1570
1708
  }
1571
1709
  async function destroyScaffold(name, options = {}) {
1572
- return destroy(name, "scaffold", (ctx) => path8.join(getAppPath(), ctx.kebabPlural), options);
1710
+ return destroy(name, "scaffold", (ctx) => path9.join(getAppPath(), ctx.kebabPlural), options);
1573
1711
  }
1574
1712
  async function destroyResource(name, options = {}) {
1575
- return destroy(name, "resource", (ctx) => path8.join(getAppPath(), ctx.kebabPlural), options);
1713
+ return destroy(name, "resource", (ctx) => path9.join(getAppPath(), ctx.kebabPlural), options);
1576
1714
  }
1577
1715
  async function destroyApi(name, options = {}) {
1578
- return destroy(name, "API", (ctx) => path8.join(getAppPath(), "api", ctx.kebabPlural), options);
1716
+ return destroy(name, "API", (ctx) => path9.join(getAppPath(), "api", ctx.kebabPlural), options);
1717
+ }
1718
+
1719
+ // src/generators/init.ts
1720
+ import * as path11 from "path";
1721
+
1722
+ // src/generators/init/prompts.ts
1723
+ import {
1724
+ intro,
1725
+ outro,
1726
+ select,
1727
+ confirm as confirm2,
1728
+ note,
1729
+ cancel,
1730
+ isCancel as isCancel2,
1731
+ log as clackLog,
1732
+ text,
1733
+ spinner
1734
+ } from "@clack/prompts";
1735
+ import * as path10 from "path";
1736
+ import * as fs3 from "fs";
1737
+ import { exec } from "child_process";
1738
+ import { promisify } from "util";
1739
+
1740
+ // src/generators/init/drivers.ts
1741
+ var DRIVERS = {
1742
+ // SQLite drivers
1743
+ "better-sqlite3": {
1744
+ name: "better-sqlite3",
1745
+ driver: "better-sqlite3",
1746
+ dialect: "sqlite",
1747
+ package: "better-sqlite3",
1748
+ drizzleKitDriver: "better-sqlite",
1749
+ envVar: "DATABASE_URL",
1750
+ envExample: "./sqlite.db",
1751
+ hint: "Local file-based SQLite",
1752
+ requiresUrl: false
1753
+ },
1754
+ libsql: {
1755
+ name: "Turso / LibSQL",
1756
+ driver: "libsql",
1757
+ dialect: "sqlite",
1758
+ package: "@libsql/client",
1759
+ drizzleKitDriver: "turso",
1760
+ envVar: "DATABASE_URL",
1761
+ envExample: "libsql://your-database.turso.io",
1762
+ hint: "Turso edge database or local LibSQL",
1763
+ requiresUrl: true
1764
+ },
1765
+ "bun:sqlite": {
1766
+ name: "Bun SQLite",
1767
+ driver: "bun:sqlite",
1768
+ dialect: "sqlite",
1769
+ package: "",
1770
+ drizzleKitDriver: "bun:sqlite",
1771
+ envVar: "DATABASE_URL",
1772
+ envExample: "./sqlite.db",
1773
+ hint: "Bun's built-in SQLite driver",
1774
+ requiresUrl: false
1775
+ },
1776
+ // PostgreSQL drivers
1777
+ postgres: {
1778
+ name: "postgres.js",
1779
+ driver: "postgres",
1780
+ dialect: "postgresql",
1781
+ package: "postgres",
1782
+ envVar: "DATABASE_URL",
1783
+ envExample: "postgres://user:password@localhost:5432/mydb",
1784
+ hint: "Recommended for most PostgreSQL setups",
1785
+ requiresUrl: true
1786
+ },
1787
+ pg: {
1788
+ name: "node-postgres (pg)",
1789
+ driver: "pg",
1790
+ dialect: "postgresql",
1791
+ package: "pg",
1792
+ envVar: "DATABASE_URL",
1793
+ envExample: "postgres://user:password@localhost:5432/mydb",
1794
+ hint: "Traditional Node.js PostgreSQL driver",
1795
+ requiresUrl: true
1796
+ },
1797
+ neon: {
1798
+ name: "Neon Serverless",
1799
+ driver: "neon",
1800
+ dialect: "postgresql",
1801
+ package: "@neondatabase/serverless",
1802
+ drizzleKitDriver: "neon-http",
1803
+ envVar: "DATABASE_URL",
1804
+ envExample: "postgres://user:password@ep-xxx.us-east-1.aws.neon.tech/neondb",
1805
+ hint: "Neon serverless PostgreSQL",
1806
+ requiresUrl: true
1807
+ },
1808
+ "vercel-postgres": {
1809
+ name: "Vercel Postgres",
1810
+ driver: "vercel-postgres",
1811
+ dialect: "postgresql",
1812
+ package: "@vercel/postgres",
1813
+ drizzleKitDriver: "vercel-postgres",
1814
+ envVar: "POSTGRES_URL",
1815
+ envExample: "postgres://...",
1816
+ hint: "Vercel's managed PostgreSQL",
1817
+ requiresUrl: true
1818
+ },
1819
+ // MySQL drivers
1820
+ mysql2: {
1821
+ name: "mysql2",
1822
+ driver: "mysql2",
1823
+ dialect: "mysql",
1824
+ package: "mysql2",
1825
+ envVar: "DATABASE_URL",
1826
+ envExample: "mysql://user:password@localhost:3306/mydb",
1827
+ hint: "Standard MySQL/MariaDB driver",
1828
+ requiresUrl: true
1829
+ },
1830
+ planetscale: {
1831
+ name: "PlanetScale",
1832
+ driver: "planetscale",
1833
+ dialect: "mysql",
1834
+ package: "@planetscale/database",
1835
+ drizzleKitDriver: "planetscale",
1836
+ envVar: "DATABASE_URL",
1837
+ envExample: 'mysql://xxx:pscale_pw_xxx@aws.connect.psdb.cloud/mydb?ssl={"rejectUnauthorized":true}',
1838
+ hint: "PlanetScale serverless MySQL",
1839
+ requiresUrl: true
1840
+ }
1841
+ };
1842
+ function getDriversForDialect(dialect) {
1843
+ return Object.values(DRIVERS).filter((d) => d.dialect === dialect);
1844
+ }
1845
+ function getDriverConfig(driver) {
1846
+ return DRIVERS[driver];
1847
+ }
1848
+ function isValidDriver(driver) {
1849
+ return driver in DRIVERS;
1850
+ }
1851
+ function isDriverForDialect(driver, dialect) {
1852
+ return DRIVERS[driver].dialect === dialect;
1853
+ }
1854
+
1855
+ // src/generators/init/prompts.ts
1856
+ var execAsync = promisify(exec);
1857
+ function checkNextJsProject() {
1858
+ const cwd = process.cwd();
1859
+ const hasNextConfig = fs3.existsSync(path10.join(cwd, "next.config.js")) || fs3.existsSync(path10.join(cwd, "next.config.mjs")) || fs3.existsSync(path10.join(cwd, "next.config.ts"));
1860
+ const packageJsonPath = path10.join(cwd, "package.json");
1861
+ if (fs3.existsSync(packageJsonPath)) {
1862
+ try {
1863
+ const content = fs3.readFileSync(packageJsonPath, "utf-8");
1864
+ const pkg = JSON.parse(content);
1865
+ const hasNextDep = pkg.dependencies?.next || pkg.devDependencies?.next;
1866
+ return hasNextConfig || !!hasNextDep;
1867
+ } catch {
1868
+ return hasNextConfig;
1869
+ }
1870
+ }
1871
+ return hasNextConfig;
1872
+ }
1873
+ function checkExistingFiles(dbPath) {
1874
+ const cwd = process.cwd();
1875
+ return {
1876
+ drizzleConfig: fileExists(path10.join(cwd, "drizzle.config.ts")),
1877
+ dbIndex: fileExists(path10.join(cwd, dbPath, "index.ts")),
1878
+ schema: fileExists(path10.join(cwd, dbPath, "schema.ts")),
1879
+ envExample: fileExists(path10.join(cwd, ".env.example")),
1880
+ dockerCompose: fileExists(path10.join(cwd, "docker-compose.yml"))
1881
+ };
1882
+ }
1883
+ async function runInitPrompts() {
1884
+ intro("Welcome to Brizzle - Drizzle ORM Setup Wizard");
1885
+ if (!checkNextJsProject()) {
1886
+ clackLog.warn("This doesn't appear to be a Next.js project. Brizzle is optimized for Next.js.");
1887
+ const proceed = await confirm2({
1888
+ message: "Continue anyway?",
1889
+ initialValue: true
1890
+ });
1891
+ if (isCancel2(proceed) || !proceed) {
1892
+ cancel("Setup cancelled.");
1893
+ return null;
1894
+ }
1895
+ }
1896
+ const projectConfig = detectProjectConfig();
1897
+ const defaultDbPath = projectConfig.dbPath;
1898
+ const dialectResult = await select({
1899
+ message: "Select your database dialect:",
1900
+ options: [
1901
+ { value: "sqlite", label: "SQLite", hint: "Lightweight, file-based database" },
1902
+ { value: "postgresql", label: "PostgreSQL", hint: "Advanced, robust database" },
1903
+ { value: "mysql", label: "MySQL", hint: "Popular, widely-supported database" }
1904
+ ]
1905
+ });
1906
+ if (isCancel2(dialectResult)) {
1907
+ cancel("Setup cancelled.");
1908
+ return null;
1909
+ }
1910
+ const dialect = dialectResult;
1911
+ const driverOptions = getDriversForDialect(dialect).map((d) => ({
1912
+ value: d.driver,
1913
+ label: d.name,
1914
+ hint: d.hint
1915
+ }));
1916
+ const driverResult = await select({
1917
+ message: "Select your database driver:",
1918
+ options: driverOptions
1919
+ });
1920
+ if (isCancel2(driverResult)) {
1921
+ cancel("Setup cancelled.");
1922
+ return null;
1923
+ }
1924
+ const driver = driverResult;
1925
+ const dbPath = await text({
1926
+ message: "Where should database files be located?",
1927
+ placeholder: defaultDbPath,
1928
+ defaultValue: defaultDbPath,
1929
+ validate: (value) => {
1930
+ if (!value) return "Path is required";
1931
+ if (value.includes("..")) return "Path cannot contain '..'";
1932
+ return void 0;
1933
+ }
1934
+ });
1935
+ if (isCancel2(dbPath)) {
1936
+ cancel("Setup cancelled.");
1937
+ return null;
1938
+ }
1939
+ const createEnvFile = await confirm2({
1940
+ message: "Create/update .env.example with database connection template?",
1941
+ initialValue: true
1942
+ });
1943
+ if (isCancel2(createEnvFile)) {
1944
+ cancel("Setup cancelled.");
1945
+ return null;
1946
+ }
1947
+ let createDockerCompose = false;
1948
+ if (dialect !== "sqlite") {
1949
+ const dockerResult = await confirm2({
1950
+ message: "Generate docker-compose.yml for local database?",
1951
+ initialValue: false
1952
+ });
1953
+ if (isCancel2(dockerResult)) {
1954
+ cancel("Setup cancelled.");
1955
+ return null;
1956
+ }
1957
+ createDockerCompose = dockerResult;
1958
+ }
1959
+ const installDeps = await confirm2({
1960
+ message: "Install dependencies now?",
1961
+ initialValue: true
1962
+ });
1963
+ if (isCancel2(installDeps)) {
1964
+ cancel("Setup cancelled.");
1965
+ return null;
1966
+ }
1967
+ return {
1968
+ dialect,
1969
+ driver,
1970
+ dbPath,
1971
+ createEnvFile,
1972
+ createDockerCompose,
1973
+ installDeps
1974
+ };
1975
+ }
1976
+ async function promptOverwrite(filePath) {
1977
+ const result = await confirm2({
1978
+ message: `${filePath} already exists. Overwrite?`,
1979
+ initialValue: false
1980
+ });
1981
+ if (isCancel2(result)) {
1982
+ return null;
1983
+ }
1984
+ return result;
1985
+ }
1986
+ async function installDependencies(options) {
1987
+ const driverConfig = DRIVERS[options.driver];
1988
+ const pm = detectPackageManager();
1989
+ const packages = [];
1990
+ if (driverConfig.package) {
1991
+ packages.push(driverConfig.package);
1992
+ }
1993
+ packages.push("drizzle-orm");
1994
+ const devPackages = ["drizzle-kit"];
1995
+ const installCmd = pm === "npm" ? "npm install" : `${pm} add`;
1996
+ const devFlag = pm === "npm" ? "--save-dev" : "-D";
1997
+ const s = spinner();
1998
+ try {
1999
+ s.start(`Installing ${packages.join(", ")}...`);
2000
+ await execAsync(`${installCmd} ${packages.join(" ")}`, {
2001
+ cwd: process.cwd()
2002
+ });
2003
+ s.stop(`Installed ${packages.join(", ")}`);
2004
+ s.start(`Installing ${devPackages.join(", ")} (dev)...`);
2005
+ await execAsync(`${installCmd} ${devFlag} ${devPackages.join(" ")}`, {
2006
+ cwd: process.cwd()
2007
+ });
2008
+ s.stop(`Installed ${devPackages.join(", ")}`);
2009
+ return true;
2010
+ } catch {
2011
+ s.stop("Installation failed");
2012
+ clackLog.error(
2013
+ `Failed to install dependencies. Please run manually:
2014
+ ${installCmd} ${packages.join(" ")}
2015
+ ${installCmd} ${devFlag} ${devPackages.join(" ")}`
2016
+ );
2017
+ return false;
2018
+ }
2019
+ }
2020
+ function addScriptsToPackageJson() {
2021
+ const cwd = process.cwd();
2022
+ const packageJsonPath = path10.join(cwd, "package.json");
2023
+ if (!fs3.existsSync(packageJsonPath)) {
2024
+ clackLog.warn("package.json not found, skipping script addition");
2025
+ return false;
2026
+ }
2027
+ try {
2028
+ const content = fs3.readFileSync(packageJsonPath, "utf-8");
2029
+ const pkg = JSON.parse(content);
2030
+ if (!pkg.scripts) {
2031
+ pkg.scripts = {};
2032
+ }
2033
+ const scriptsToAdd = {
2034
+ "db:generate": "drizzle-kit generate",
2035
+ "db:migrate": "drizzle-kit migrate",
2036
+ "db:push": "drizzle-kit push",
2037
+ "db:studio": "drizzle-kit studio"
2038
+ };
2039
+ let scriptsAdded = 0;
2040
+ for (const [name, command] of Object.entries(scriptsToAdd)) {
2041
+ if (!pkg.scripts[name]) {
2042
+ pkg.scripts[name] = command;
2043
+ scriptsAdded++;
2044
+ }
2045
+ }
2046
+ if (scriptsAdded > 0) {
2047
+ fs3.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + "\n");
2048
+ clackLog.info(`Added ${scriptsAdded} script(s) to package.json`);
2049
+ }
2050
+ return true;
2051
+ } catch {
2052
+ clackLog.warn("Failed to add scripts to package.json");
2053
+ return false;
2054
+ }
2055
+ }
2056
+ function showSummary(options, filesCreated, depsInstalled) {
2057
+ const driverConfig = DRIVERS[options.driver];
2058
+ if (filesCreated.length > 0) {
2059
+ note(filesCreated.map((f) => ` ${f}`).join("\n"), "Files created");
2060
+ }
2061
+ if (!depsInstalled) {
2062
+ const packages = [];
2063
+ if (driverConfig.package) {
2064
+ packages.push(driverConfig.package);
2065
+ }
2066
+ packages.push("drizzle-orm");
2067
+ const pm = detectPackageManager();
2068
+ const installCmd = pm === "npm" ? "npm install" : `${pm} add`;
2069
+ const devFlag = pm === "npm" ? "--save-dev" : "-D";
2070
+ note(
2071
+ `${installCmd} ${packages.join(" ")}
2072
+ ${installCmd} ${devFlag} drizzle-kit`,
2073
+ "Install dependencies"
2074
+ );
2075
+ }
2076
+ const nextSteps = [];
2077
+ let step = 1;
2078
+ if (options.createDockerCompose) {
2079
+ nextSteps.push(`${step}. Start your database: docker compose up -d`);
2080
+ step++;
2081
+ }
2082
+ if (options.createEnvFile) {
2083
+ nextSteps.push(`${step}. Copy environment file: cp .env.example .env`);
2084
+ step++;
2085
+ }
2086
+ nextSteps.push(`${step}. Set ${driverConfig.envVar} in your .env file`);
2087
+ step++;
2088
+ const run = getRunCommand();
2089
+ nextSteps.push(`${step}. Scaffold your first CRUD:`);
2090
+ nextSteps.push(` npx brizzle scaffold post title:string body:text`);
2091
+ step++;
2092
+ nextSteps.push(`${step}. Generate and run migrations:`);
2093
+ nextSteps.push(` ${run} db:generate`);
2094
+ nextSteps.push(` ${run} db:migrate`);
2095
+ note(nextSteps.join("\n"), "Next steps");
2096
+ outro("Happy coding with Drizzle ORM!");
2097
+ }
2098
+
2099
+ // src/generators/init/templates.ts
2100
+ function generateDrizzleConfig(options) {
2101
+ const config = getDriverConfig(options.driver);
2102
+ return `import { defineConfig } from "drizzle-kit";
2103
+
2104
+ export default defineConfig({
2105
+ schema: "./${options.dbPath}/schema.ts",
2106
+ out: "./${options.dbPath}/migrations",
2107
+ dialect: "${options.dialect}",
2108
+ dbCredentials: {
2109
+ url: process.env.${config.envVar}!,
2110
+ },
2111
+ });
2112
+ `;
2113
+ }
2114
+ function generateDbClient(options) {
2115
+ switch (options.driver) {
2116
+ case "better-sqlite3":
2117
+ return generateBetterSqliteClient(options);
2118
+ case "libsql":
2119
+ return generateLibsqlClient(options);
2120
+ case "bun:sqlite":
2121
+ return generateBunSqliteClient(options);
2122
+ case "postgres":
2123
+ return generatePostgresJsClient(options);
2124
+ case "pg":
2125
+ return generatePgClient(options);
2126
+ case "neon":
2127
+ return generateNeonClient(options);
2128
+ case "vercel-postgres":
2129
+ return generateVercelPgClient(options);
2130
+ case "mysql2":
2131
+ return generateMysql2Client(options);
2132
+ case "planetscale":
2133
+ return generatePlanetscaleClient(options);
2134
+ default:
2135
+ throw new Error(`Unsupported driver: ${options.driver}`);
2136
+ }
2137
+ }
2138
+ function generateBetterSqliteClient(options) {
2139
+ const config = getDriverConfig(options.driver);
2140
+ return `import Database from "better-sqlite3";
2141
+ import { drizzle } from "drizzle-orm/better-sqlite3";
2142
+ import * as schema from "./schema";
2143
+
2144
+ const sqlite = new Database(process.env.${config.envVar} || "sqlite.db");
2145
+
2146
+ export const db = drizzle(sqlite, { schema });
2147
+ `;
2148
+ }
2149
+ function generateLibsqlClient(options) {
2150
+ const config = getDriverConfig(options.driver);
2151
+ return `import { createClient } from "@libsql/client";
2152
+ import { drizzle } from "drizzle-orm/libsql";
2153
+ import * as schema from "./schema";
2154
+
2155
+ const client = createClient({
2156
+ url: process.env.${config.envVar}!,
2157
+ authToken: process.env.DATABASE_AUTH_TOKEN,
2158
+ });
2159
+
2160
+ export const db = drizzle(client, { schema });
2161
+ `;
2162
+ }
2163
+ function generateBunSqliteClient(options) {
2164
+ const config = getDriverConfig(options.driver);
2165
+ return `import { Database } from "bun:sqlite";
2166
+ import { drizzle } from "drizzle-orm/bun-sqlite";
2167
+ import * as schema from "./schema";
2168
+
2169
+ const sqlite = new Database(process.env.${config.envVar} || "sqlite.db");
2170
+
2171
+ export const db = drizzle(sqlite, { schema });
2172
+ `;
2173
+ }
2174
+ function generatePostgresJsClient(options) {
2175
+ const config = getDriverConfig(options.driver);
2176
+ return `import postgres from "postgres";
2177
+ import { drizzle } from "drizzle-orm/postgres-js";
2178
+ import * as schema from "./schema";
2179
+
2180
+ const client = postgres(process.env.${config.envVar}!);
2181
+
2182
+ export const db = drizzle(client, { schema });
2183
+ `;
2184
+ }
2185
+ function generatePgClient(options) {
2186
+ const config = getDriverConfig(options.driver);
2187
+ return `import { Pool } from "pg";
2188
+ import { drizzle } from "drizzle-orm/node-postgres";
2189
+ import * as schema from "./schema";
2190
+
2191
+ const pool = new Pool({
2192
+ connectionString: process.env.${config.envVar},
2193
+ });
2194
+
2195
+ export const db = drizzle(pool, { schema });
2196
+ `;
2197
+ }
2198
+ function generateNeonClient(options) {
2199
+ const config = getDriverConfig(options.driver);
2200
+ return `import { neon } from "@neondatabase/serverless";
2201
+ import { drizzle } from "drizzle-orm/neon-http";
2202
+ import * as schema from "./schema";
2203
+
2204
+ const sql = neon(process.env.${config.envVar}!);
2205
+
2206
+ export const db = drizzle(sql, { schema });
2207
+ `;
2208
+ }
2209
+ function generateVercelPgClient(_options) {
2210
+ return `import { sql } from "@vercel/postgres";
2211
+ import { drizzle } from "drizzle-orm/vercel-postgres";
2212
+ import * as schema from "./schema";
2213
+
2214
+ export const db = drizzle(sql, { schema });
2215
+ `;
2216
+ }
2217
+ function generateMysql2Client(options) {
2218
+ const config = getDriverConfig(options.driver);
2219
+ return `import mysql from "mysql2/promise";
2220
+ import { drizzle } from "drizzle-orm/mysql2";
2221
+ import * as schema from "./schema";
2222
+
2223
+ const connection = await mysql.createConnection(process.env.${config.envVar}!);
2224
+
2225
+ export const db = drizzle(connection, { schema, mode: "default" });
2226
+ `;
2227
+ }
2228
+ function generatePlanetscaleClient(options) {
2229
+ const config = getDriverConfig(options.driver);
2230
+ return `import { connect } from "@planetscale/database";
2231
+ import { drizzle } from "drizzle-orm/planetscale-serverless";
2232
+ import * as schema from "./schema";
2233
+
2234
+ const connection = connect({
2235
+ url: process.env.${config.envVar},
2236
+ });
2237
+
2238
+ export const db = drizzle(connection, { schema, mode: "planetscale" });
2239
+ `;
2240
+ }
2241
+ function generateSchema(dialect) {
2242
+ const drizzleImport = getDrizzleImport(dialect);
2243
+ const tableFunction = getTableFunction(dialect);
2244
+ return `import { ${tableFunction} } from "${drizzleImport}";
2245
+
2246
+ // Scaffold your first CRUD: npx brizzle scaffold post title:string body:text
2247
+ `;
2248
+ }
2249
+ function generateEnvExample(options) {
2250
+ const config = getDriverConfig(options.driver);
2251
+ let content = `# Database
2252
+ `;
2253
+ if (options.createDockerCompose) {
2254
+ if (options.dialect === "postgresql") {
2255
+ content += `${config.envVar}="postgres://postgres:postgres@localhost:5432/myapp"
2256
+ `;
2257
+ } else if (options.dialect === "mysql") {
2258
+ content += `${config.envVar}="mysql://root:root@localhost:3306/myapp"
2259
+ `;
2260
+ } else {
2261
+ content += `${config.envVar}="${config.envExample}"
2262
+ `;
2263
+ }
2264
+ } else {
2265
+ content += `${config.envVar}="${config.envExample}"
2266
+ `;
2267
+ }
2268
+ if (options.driver === "libsql") {
2269
+ content += `DATABASE_AUTH_TOKEN="your-auth-token"
2270
+ `;
2271
+ }
2272
+ return content;
2273
+ }
2274
+ function generatePostgresDockerCompose() {
2275
+ return `services:
2276
+ db:
2277
+ image: postgres:16-alpine
2278
+ environment:
2279
+ POSTGRES_USER: postgres
2280
+ POSTGRES_PASSWORD: postgres
2281
+ POSTGRES_DB: myapp
2282
+ ports:
2283
+ - "5432:5432"
2284
+ volumes:
2285
+ - postgres_data:/var/lib/postgresql/data
2286
+
2287
+ volumes:
2288
+ postgres_data:
2289
+ `;
2290
+ }
2291
+ function generateMysqlDockerCompose() {
2292
+ return `services:
2293
+ db:
2294
+ image: mysql:8
2295
+ environment:
2296
+ MYSQL_ROOT_PASSWORD: root
2297
+ MYSQL_DATABASE: myapp
2298
+ ports:
2299
+ - "3306:3306"
2300
+ volumes:
2301
+ - mysql_data:/var/lib/mysql
2302
+
2303
+ volumes:
2304
+ mysql_data:
2305
+ `;
2306
+ }
2307
+ function generateDockerCompose(dialect) {
2308
+ switch (dialect) {
2309
+ case "postgresql":
2310
+ return generatePostgresDockerCompose();
2311
+ case "mysql":
2312
+ return generateMysqlDockerCompose();
2313
+ default:
2314
+ return null;
2315
+ }
2316
+ }
2317
+
2318
+ // src/generators/init.ts
2319
+ var VALID_DIALECTS = ["sqlite", "postgresql", "mysql"];
2320
+ function validateDialect(dialect) {
2321
+ if (!VALID_DIALECTS.includes(dialect)) {
2322
+ throw new Error(`Invalid dialect "${dialect}". Must be: ${VALID_DIALECTS.join(", ")}`);
2323
+ }
2324
+ return dialect;
2325
+ }
2326
+ function validateDriver(driver, dialect) {
2327
+ if (!isValidDriver(driver)) {
2328
+ throw new Error(`Invalid driver "${driver}".`);
2329
+ }
2330
+ if (!isDriverForDialect(driver, dialect)) {
2331
+ throw new Error(`Driver "${driver}" is not compatible with dialect "${dialect}".`);
2332
+ }
2333
+ return driver;
2334
+ }
2335
+ async function generateInit(commandOptions = {}) {
2336
+ let options;
2337
+ if (commandOptions.dialect && commandOptions.driver) {
2338
+ const dialect = validateDialect(commandOptions.dialect);
2339
+ const driver = validateDriver(commandOptions.driver, dialect);
2340
+ const projectConfig = detectProjectConfig();
2341
+ options = {
2342
+ dialect,
2343
+ driver,
2344
+ dbPath: projectConfig.dbPath,
2345
+ createEnvFile: true,
2346
+ createDockerCompose: false,
2347
+ installDeps: commandOptions.install !== false,
2348
+ force: commandOptions.force,
2349
+ dryRun: commandOptions.dryRun
2350
+ };
2351
+ } else if (commandOptions.dialect || commandOptions.driver) {
2352
+ throw new Error("Both --dialect and --driver must be provided for non-interactive mode.");
2353
+ } else {
2354
+ const promptResult = await runInitPrompts();
2355
+ if (!promptResult) {
2356
+ return;
2357
+ }
2358
+ options = {
2359
+ ...promptResult,
2360
+ force: commandOptions.force,
2361
+ dryRun: commandOptions.dryRun
2362
+ };
2363
+ }
2364
+ const filesCreated = await createFiles(options);
2365
+ if (options.dryRun) {
2366
+ log.info("\nDry run complete. No files were written.\n");
2367
+ return;
2368
+ }
2369
+ if (filesCreated.length === 0) {
2370
+ log.info("\nNo files were created. All files already exist.\n");
2371
+ return;
2372
+ }
2373
+ addScriptsToPackageJson();
2374
+ let depsInstalled = false;
2375
+ if (options.installDeps) {
2376
+ depsInstalled = await installDependencies(options);
2377
+ }
2378
+ showSummary(options, filesCreated, depsInstalled);
2379
+ }
2380
+ async function createFiles(options) {
2381
+ const cwd = process.cwd();
2382
+ const filesCreated = [];
2383
+ const existing = checkExistingFiles(options.dbPath);
2384
+ const drizzleConfigPath = path11.join(cwd, "drizzle.config.ts");
2385
+ if (await shouldWriteFile(drizzleConfigPath, existing.drizzleConfig, options)) {
2386
+ const content = generateDrizzleConfig(options);
2387
+ if (writeFile(drizzleConfigPath, content, { force: true, dryRun: options.dryRun })) {
2388
+ filesCreated.push("drizzle.config.ts");
2389
+ }
2390
+ }
2391
+ const dbIndexPath = path11.join(cwd, options.dbPath, "index.ts");
2392
+ if (await shouldWriteFile(dbIndexPath, existing.dbIndex, options)) {
2393
+ const content = generateDbClient(options);
2394
+ if (writeFile(dbIndexPath, content, { force: true, dryRun: options.dryRun })) {
2395
+ filesCreated.push(`${options.dbPath}/index.ts`);
2396
+ }
2397
+ }
2398
+ const schemaPath = path11.join(cwd, options.dbPath, "schema.ts");
2399
+ if (await shouldWriteFile(schemaPath, existing.schema, options)) {
2400
+ const content = generateSchema(options.dialect);
2401
+ if (writeFile(schemaPath, content, { force: true, dryRun: options.dryRun })) {
2402
+ filesCreated.push(`${options.dbPath}/schema.ts`);
2403
+ }
2404
+ }
2405
+ if (options.createEnvFile) {
2406
+ const envExamplePath = path11.join(cwd, ".env.example");
2407
+ const newEnvContent = generateEnvExample(options);
2408
+ const config = getDriverConfig(options.driver);
2409
+ if (existing.envExample && !options.force) {
2410
+ const existingContent = readFile(envExamplePath);
2411
+ if (!existingContent.includes(config.envVar)) {
2412
+ const mergedContent = existingContent.trimEnd() + "\n\n" + newEnvContent;
2413
+ if (writeFile(envExamplePath, mergedContent, { force: true, dryRun: options.dryRun })) {
2414
+ filesCreated.push(".env.example (updated)");
2415
+ }
2416
+ } else {
2417
+ log.skip(envExamplePath);
2418
+ }
2419
+ } else if (await shouldWriteFile(envExamplePath, existing.envExample, options)) {
2420
+ if (writeFile(envExamplePath, newEnvContent, { force: true, dryRun: options.dryRun })) {
2421
+ filesCreated.push(".env.example");
2422
+ }
2423
+ }
2424
+ }
2425
+ if (options.createDockerCompose) {
2426
+ const dockerComposePath = path11.join(cwd, "docker-compose.yml");
2427
+ if (await shouldWriteFile(dockerComposePath, existing.dockerCompose, options)) {
2428
+ const content = generateDockerCompose(options.dialect);
2429
+ if (content) {
2430
+ if (writeFile(dockerComposePath, content, { force: true, dryRun: options.dryRun })) {
2431
+ filesCreated.push("docker-compose.yml");
2432
+ }
2433
+ }
2434
+ }
2435
+ }
2436
+ return filesCreated;
2437
+ }
2438
+ async function shouldWriteFile(filePath, exists, options) {
2439
+ if (!exists) {
2440
+ return true;
2441
+ }
2442
+ if (options.force || options.dryRun) {
2443
+ return true;
2444
+ }
2445
+ const result = await promptOverwrite(path11.basename(filePath));
2446
+ if (result === null) {
2447
+ log.info("Aborted.");
2448
+ process.exit(0);
2449
+ }
2450
+ if (!result) {
2451
+ log.skip(filePath);
2452
+ }
2453
+ return result;
1579
2454
  }
1580
2455
 
1581
2456
  // src/index.ts
@@ -1679,7 +2554,12 @@ program.command("destroy <type> <name>").alias("d").description(
1679
2554
  brizzle d api product --dry-run`
1680
2555
  ).option("-f, --force", "Skip confirmation prompt").option("-n, --dry-run", "Preview changes without deleting files").action(async (type, name, opts) => {
1681
2556
  try {
1682
- switch (type) {
2557
+ const validTypes = ["scaffold", "resource", "api"];
2558
+ if (!validTypes.includes(type)) {
2559
+ throw new Error(`Unknown type "${type}". Use: scaffold, resource, or api`);
2560
+ }
2561
+ const destroyType = type;
2562
+ switch (destroyType) {
1683
2563
  case "scaffold":
1684
2564
  await destroyScaffold(name, opts);
1685
2565
  break;
@@ -1689,8 +2569,6 @@ program.command("destroy <type> <name>").alias("d").description(
1689
2569
  case "api":
1690
2570
  await destroyApi(name, opts);
1691
2571
  break;
1692
- default:
1693
- throw new Error(`Unknown type "${type}". Use: scaffold, resource, or api`);
1694
2572
  }
1695
2573
  } catch (error) {
1696
2574
  handleError(error);
@@ -1700,7 +2578,9 @@ program.command("config").description("Show detected project configuration").act
1700
2578
  const config = detectProjectConfig();
1701
2579
  const dialect = detectDialect();
1702
2580
  console.log("\nDetected project configuration:\n");
1703
- console.log(` Project structure: ${config.useSrc ? "src/ (e.g., src/app/, src/db/)" : "root (e.g., app/, db/)"}`);
2581
+ console.log(
2582
+ ` Project structure: ${config.useSrc ? "src/ (e.g., src/app/, src/db/)" : "root (e.g., app/, db/)"}`
2583
+ );
1704
2584
  console.log(` Path alias: ${config.alias}/`);
1705
2585
  console.log(` App directory: ${config.appPath}/`);
1706
2586
  console.log(` DB directory: ${config.dbPath}/`);
@@ -1711,4 +2591,30 @@ program.command("config").description("Show detected project configuration").act
1711
2591
  console.log(` Schema: ${config.alias}/${config.dbPath.replace(/^src\//, "")}/schema`);
1712
2592
  console.log();
1713
2593
  });
2594
+ program.command("init").description(
2595
+ `Initialize Drizzle ORM in your Next.js project
2596
+
2597
+ Interactive setup wizard that configures:
2598
+ - Database dialect (SQLite, PostgreSQL, MySQL)
2599
+ - Database driver selection
2600
+ - drizzle.config.ts
2601
+ - Database client export (db/index.ts)
2602
+ - Schema file (db/schema.ts)
2603
+ - Environment variables template
2604
+
2605
+ Examples:
2606
+ brizzle init
2607
+ brizzle init --dry-run
2608
+ brizzle init --dialect postgresql --driver postgres
2609
+ brizzle init --dialect sqlite --driver better-sqlite3 --no-install`
2610
+ ).option("-f, --force", "Overwrite existing files without prompting").option("-n, --dry-run", "Preview changes without writing files").option(
2611
+ "-d, --dialect <dialect>",
2612
+ "Database dialect for non-interactive mode (sqlite, postgresql, mysql)"
2613
+ ).option("--driver <driver>", "Database driver for non-interactive mode").option("--no-install", "Skip automatic dependency installation").action(async (opts) => {
2614
+ try {
2615
+ await generateInit(opts);
2616
+ } catch (error) {
2617
+ handleError(error);
2618
+ }
2619
+ });
1714
2620
  program.parse();