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