brizzle 0.2.8 → 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.
- package/README.md +40 -1
- package/dist/index.js +1360 -454
- 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
|
|
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 =
|
|
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(
|
|
115
|
+
const useSrc = fs.existsSync(path2.join(cwd, "src", "app"));
|
|
56
116
|
let alias = "@";
|
|
57
|
-
const tsconfigPath =
|
|
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(
|
|
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
|
|
158
|
+
return path2.join(process.cwd(), config.appPath);
|
|
99
159
|
}
|
|
100
160
|
function getDbPath() {
|
|
101
161
|
const config = detectProjectConfig();
|
|
102
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
442
|
-
|
|
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/
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
|
617
|
-
|
|
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
|
|
620
|
-
|
|
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
|
|
623
|
-
|
|
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
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
|
637
|
-
const
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
);
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
-
|
|
654
|
-
|
|
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 (
|
|
662
|
-
|
|
663
|
-
|
|
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 (
|
|
667
|
-
return
|
|
891
|
+
if (field.type === "boolean" || field.type === "bool") {
|
|
892
|
+
return `${getValue} === "on"`;
|
|
668
893
|
}
|
|
669
|
-
|
|
670
|
-
|
|
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 =
|
|
924
|
+
const schemaPath = path5.join(getDbPath(), "schema.ts");
|
|
685
925
|
if (!fileExists(schemaPath)) {
|
|
686
|
-
const schemaContent = generateSchemaContent(
|
|
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
|
-
|
|
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
|
|
1070
|
+
import * as path6 from "path";
|
|
823
1071
|
function generateActions(name, options = {}) {
|
|
824
1072
|
validateModelName(name);
|
|
825
1073
|
const ctx = createModelContext(name);
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
|
|
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
|
-
|
|
895
|
-
import
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
${
|
|
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}`);
|
|
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));
|
|
911
1155
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
);
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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}");
|
|
935
1204
|
}
|
|
1205
|
+
`;
|
|
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";
|
|
@@ -945,31 +1221,31 @@ export default async function ${pascalPlural}Page() {
|
|
|
945
1221
|
return (
|
|
946
1222
|
<div className="mx-auto max-w-3xl px-6 py-12">
|
|
947
1223
|
<div className="mb-10 flex items-center justify-between">
|
|
948
|
-
<h1 className="text-2xl font-semibold text-
|
|
1224
|
+
<h1 className="text-2xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-50">${pascalPlural}</h1>
|
|
949
1225
|
<Link
|
|
950
1226
|
href="/${kebabPlural}/new"
|
|
951
|
-
className="rounded-
|
|
1227
|
+
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"
|
|
952
1228
|
>
|
|
953
1229
|
New ${pascalName}
|
|
954
1230
|
</Link>
|
|
955
1231
|
</div>
|
|
956
1232
|
|
|
957
1233
|
{${camelName}s.length === 0 ? (
|
|
958
|
-
<p className="text-
|
|
1234
|
+
<p className="text-zinc-500 dark:text-zinc-400">No ${camelName}s yet.</p>
|
|
959
1235
|
) : (
|
|
960
|
-
<div className="divide-y divide-
|
|
1236
|
+
<div className="divide-y divide-zinc-100 dark:divide-zinc-800">
|
|
961
1237
|
{${camelName}s.map((${camelName}) => (
|
|
962
1238
|
<div
|
|
963
1239
|
key={${camelName}.id}
|
|
964
1240
|
className="flex items-center justify-between py-4"
|
|
965
1241
|
>
|
|
966
|
-
<Link href={\`/${kebabPlural}/\${${camelName}.id}\`} className="font-medium text-
|
|
1242
|
+
<Link href={\`/${kebabPlural}/\${${camelName}.id}\`} className="font-medium text-zinc-900 hover:text-zinc-600 dark:text-zinc-50 dark:hover:text-zinc-300">
|
|
967
1243
|
{${camelName}.${displayField}}
|
|
968
1244
|
</Link>
|
|
969
1245
|
<div className="flex gap-4 text-sm">
|
|
970
1246
|
<Link
|
|
971
1247
|
href={\`/${kebabPlural}/\${${camelName}.id}/edit\`}
|
|
972
|
-
className="text-
|
|
1248
|
+
className="text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-50"
|
|
973
1249
|
>
|
|
974
1250
|
Edit
|
|
975
1251
|
</Link>
|
|
@@ -979,7 +1255,7 @@ export default async function ${pascalPlural}Page() {
|
|
|
979
1255
|
await delete${pascalName}(${camelName}.id);
|
|
980
1256
|
}}
|
|
981
1257
|
>
|
|
982
|
-
<button type="submit" className="text-
|
|
1258
|
+
<button type="submit" className="text-zinc-500 hover:text-red-600 dark:text-zinc-400 dark:hover:text-red-400">
|
|
983
1259
|
Delete
|
|
984
1260
|
</button>
|
|
985
1261
|
</form>
|
|
@@ -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";
|
|
@@ -1011,7 +1289,7 @@ ${fields.map((f) => ` ${f.name}: ${formDataValue(f)},`).join("\n")}
|
|
|
1011
1289
|
|
|
1012
1290
|
return (
|
|
1013
1291
|
<div className="mx-auto max-w-xl px-6 py-12">
|
|
1014
|
-
<h1 className="mb-8 text-2xl font-semibold text-
|
|
1292
|
+
<h1 className="mb-8 text-2xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-50">New ${pascalName}</h1>
|
|
1015
1293
|
|
|
1016
1294
|
<form action={handleCreate} className="space-y-5">
|
|
1017
1295
|
${fields.map((f) => generateFormField(f, camelName)).join("\n\n")}
|
|
@@ -1019,13 +1297,13 @@ ${fields.map((f) => generateFormField(f, camelName)).join("\n\n")}
|
|
|
1019
1297
|
<div className="flex gap-3 pt-4">
|
|
1020
1298
|
<button
|
|
1021
1299
|
type="submit"
|
|
1022
|
-
className="rounded-
|
|
1300
|
+
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"
|
|
1023
1301
|
>
|
|
1024
1302
|
Create ${pascalName}
|
|
1025
1303
|
</button>
|
|
1026
1304
|
<Link
|
|
1027
1305
|
href="/${kebabPlural}"
|
|
1028
|
-
className="rounded-
|
|
1306
|
+
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"
|
|
1029
1307
|
>
|
|
1030
1308
|
Cancel
|
|
1031
1309
|
</Link>
|
|
@@ -1036,7 +1314,9 @@ ${fields.map((f) => generateFormField(f, camelName)).join("\n\n")}
|
|
|
1036
1314
|
}
|
|
1037
1315
|
`;
|
|
1038
1316
|
}
|
|
1039
|
-
|
|
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)) {
|
|
@@ -1063,33 +1343,33 @@ export default async function ${pascalName}Page({
|
|
|
1063
1343
|
return (
|
|
1064
1344
|
<div className="mx-auto max-w-xl px-6 py-12">
|
|
1065
1345
|
<div className="mb-8 flex items-center justify-between">
|
|
1066
|
-
<h1 className="text-2xl font-semibold text-
|
|
1346
|
+
<h1 className="text-2xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-50">${pascalName}</h1>
|
|
1067
1347
|
<div className="flex gap-3">
|
|
1068
1348
|
<Link
|
|
1069
1349
|
href={\`/${kebabPlural}/\${${camelName}.id}/edit\`}
|
|
1070
|
-
className="rounded-
|
|
1350
|
+
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"
|
|
1071
1351
|
>
|
|
1072
1352
|
Edit
|
|
1073
1353
|
</Link>
|
|
1074
1354
|
<Link
|
|
1075
1355
|
href="/${kebabPlural}"
|
|
1076
|
-
className="rounded-
|
|
1356
|
+
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"
|
|
1077
1357
|
>
|
|
1078
1358
|
Back
|
|
1079
1359
|
</Link>
|
|
1080
1360
|
</div>
|
|
1081
1361
|
</div>
|
|
1082
1362
|
|
|
1083
|
-
<dl className="divide-y divide-
|
|
1363
|
+
<dl className="divide-y divide-zinc-100 dark:divide-zinc-800">
|
|
1084
1364
|
${fields.map(
|
|
1085
1365
|
(f) => ` <div className="py-3">
|
|
1086
|
-
<dt className="text-sm text-
|
|
1087
|
-
<dd className="mt-1 text-
|
|
1366
|
+
<dt className="text-sm text-zinc-500 dark:text-zinc-400">${toPascalCase(f.name)}</dt>
|
|
1367
|
+
<dd className="mt-1 text-zinc-900 dark:text-zinc-50">{${camelName}.${f.name}}</dd>
|
|
1088
1368
|
</div>`
|
|
1089
1369
|
).join("\n")}${options.noTimestamps ? "" : `
|
|
1090
1370
|
<div className="py-3">
|
|
1091
|
-
<dt className="text-sm text-
|
|
1092
|
-
<dd className="mt-1 text-
|
|
1371
|
+
<dt className="text-sm text-zinc-500 dark:text-zinc-400">Created At</dt>
|
|
1372
|
+
<dd className="mt-1 text-zinc-900 dark:text-zinc-50">{${camelName}.createdAt.toLocaleString()}</dd>
|
|
1093
1373
|
</div>`}
|
|
1094
1374
|
</dl>
|
|
1095
1375
|
</div>
|
|
@@ -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
|
|
|
@@ -1131,222 +1413,76 @@ ${fields.map((f) => ` ${f.name}: ${formDataValue(f)},`).join("\n")}
|
|
|
1131
1413
|
|
|
1132
1414
|
redirect("/${kebabPlural}");
|
|
1133
1415
|
}
|
|
1134
|
-
|
|
1135
|
-
return (
|
|
1136
|
-
<div className="mx-auto max-w-xl px-6 py-12">
|
|
1137
|
-
<h1 className="mb-8 text-2xl font-semibold text-
|
|
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="rounded-
|
|
1146
|
-
>
|
|
1147
|
-
Update ${pascalName}
|
|
1148
|
-
</button>
|
|
1149
|
-
<Link
|
|
1150
|
-
href="/${kebabPlural}"
|
|
1151
|
-
className="rounded-
|
|
1152
|
-
>
|
|
1153
|
-
Cancel
|
|
1154
|
-
</Link>
|
|
1155
|
-
</div>
|
|
1156
|
-
</form>
|
|
1157
|
-
</div>
|
|
1158
|
-
);
|
|
1159
|
-
}
|
|
1160
|
-
`;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
const stepAttr = step ? `
|
|
1205
|
-
step="${step}"` : "";
|
|
1206
|
-
return ` <div>
|
|
1207
|
-
<label htmlFor="${field.name}" className="block text-sm font-medium text-gray-700">
|
|
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-gray-200 px-3 py-2 text-gray-900 placeholder:text-gray-400 focus:border-gray-400 focus:outline-none focus:ring-0"${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-gray-700">
|
|
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-gray-200 px-3 py-2 text-gray-900 placeholder:text-gray-400 focus:border-gray-400 focus:outline-none focus:ring-0"${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-gray-700">
|
|
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-gray-200 px-3 py-2 text-gray-900 placeholder:text-gray-400 focus:border-gray-400 focus:outline-none focus:ring-0"${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-gray-700">
|
|
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-gray-200 px-3 py-2 text-gray-900 focus:border-gray-400 focus:outline-none focus:ring-0"${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-gray-700">
|
|
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-gray-200 px-3 py-2 text-gray-900 placeholder:text-gray-400 focus:border-gray-400 focus:outline-none focus:ring-0"${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;
|
|
1416
|
+
|
|
1417
|
+
return (
|
|
1418
|
+
<div className="mx-auto max-w-xl px-6 py-12">
|
|
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 '
|
|
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
|
|
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 '
|
|
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 =
|
|
1523
|
+
const basePath = path8.join(getAppPath(), "api", kebabPlural);
|
|
1386
1524
|
writeFile(
|
|
1387
|
-
|
|
1525
|
+
path8.join(basePath, "route.ts"),
|
|
1388
1526
|
generateCollectionRoute(camelPlural, kebabPlural),
|
|
1389
1527
|
options
|
|
1390
1528
|
);
|
|
1391
1529
|
writeFile(
|
|
1392
|
-
|
|
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
|
|
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 =
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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
|
-
|
|
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(
|
|
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();
|