nextjs-drizzle-gen 0.1.3 → 0.2.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 +7 -0
- package/dist/index.js +179 -122
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
# nextjs-drizzle-gen
|
|
2
2
|
|
|
3
|
+
[](https://github.com/mantaskaveckas/nextjs-drizzle-gen/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/nextjs-drizzle-gen)
|
|
5
|
+
[](https://www.npmjs.com/package/nextjs-drizzle-gen)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
3
8
|
Rails-like generators for Next.js + Drizzle ORM projects. Generate models, server actions, CRUD pages, and API routes with a single command.
|
|
4
9
|
|
|
10
|
+
[**Documentation**](https://mantaskaveckas.github.io/nextjs-drizzle-gen/)
|
|
11
|
+
|
|
5
12
|
## Installation
|
|
6
13
|
|
|
7
14
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -4,11 +4,9 @@
|
|
|
4
4
|
import { program } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/generators/model.ts
|
|
7
|
-
import * as
|
|
7
|
+
import * as path4 from "path";
|
|
8
8
|
|
|
9
|
-
// src/
|
|
10
|
-
import * as fs from "fs";
|
|
11
|
-
import * as path from "path";
|
|
9
|
+
// src/lib/types.ts
|
|
12
10
|
var VALID_FIELD_TYPES = [
|
|
13
11
|
"string",
|
|
14
12
|
"text",
|
|
@@ -25,6 +23,11 @@ var VALID_FIELD_TYPES = [
|
|
|
25
23
|
"json",
|
|
26
24
|
"uuid"
|
|
27
25
|
];
|
|
26
|
+
|
|
27
|
+
// src/lib/config.ts
|
|
28
|
+
import * as fs from "fs";
|
|
29
|
+
import * as path from "path";
|
|
30
|
+
var cachedProjectConfig = null;
|
|
28
31
|
function detectDialect() {
|
|
29
32
|
const configPath = path.join(process.cwd(), "drizzle.config.ts");
|
|
30
33
|
if (!fs.existsSync(configPath)) {
|
|
@@ -43,7 +46,6 @@ function detectDialect() {
|
|
|
43
46
|
}
|
|
44
47
|
return "sqlite";
|
|
45
48
|
}
|
|
46
|
-
var cachedProjectConfig = null;
|
|
47
49
|
function detectProjectConfig() {
|
|
48
50
|
if (cachedProjectConfig) {
|
|
49
51
|
return cachedProjectConfig;
|
|
@@ -98,37 +100,40 @@ function getDbPath() {
|
|
|
98
100
|
const config = detectProjectConfig();
|
|
99
101
|
return path.join(process.cwd(), config.dbPath);
|
|
100
102
|
}
|
|
103
|
+
|
|
104
|
+
// src/lib/logger.ts
|
|
105
|
+
import * as path2 from "path";
|
|
101
106
|
var log = {
|
|
102
107
|
create: (filePath) => {
|
|
103
|
-
const relative2 =
|
|
108
|
+
const relative2 = path2.relative(process.cwd(), filePath);
|
|
104
109
|
console.log(` \x1B[32mcreate\x1B[0m ${relative2}`);
|
|
105
110
|
},
|
|
106
111
|
force: (filePath) => {
|
|
107
|
-
const relative2 =
|
|
112
|
+
const relative2 = path2.relative(process.cwd(), filePath);
|
|
108
113
|
console.log(` \x1B[33mforce\x1B[0m ${relative2}`);
|
|
109
114
|
},
|
|
110
115
|
skip: (filePath) => {
|
|
111
|
-
const relative2 =
|
|
116
|
+
const relative2 = path2.relative(process.cwd(), filePath);
|
|
112
117
|
console.log(` \x1B[33mskip\x1B[0m ${relative2}`);
|
|
113
118
|
},
|
|
114
119
|
remove: (filePath) => {
|
|
115
|
-
const relative2 =
|
|
120
|
+
const relative2 = path2.relative(process.cwd(), filePath);
|
|
116
121
|
console.log(` \x1B[31mremove\x1B[0m ${relative2}`);
|
|
117
122
|
},
|
|
118
123
|
notFound: (filePath) => {
|
|
119
|
-
const relative2 =
|
|
124
|
+
const relative2 = path2.relative(process.cwd(), filePath);
|
|
120
125
|
console.log(` \x1B[33mnot found\x1B[0m ${relative2}`);
|
|
121
126
|
},
|
|
122
127
|
wouldCreate: (filePath) => {
|
|
123
|
-
const relative2 =
|
|
128
|
+
const relative2 = path2.relative(process.cwd(), filePath);
|
|
124
129
|
console.log(`\x1B[36mwould create\x1B[0m ${relative2}`);
|
|
125
130
|
},
|
|
126
131
|
wouldForce: (filePath) => {
|
|
127
|
-
const relative2 =
|
|
132
|
+
const relative2 = path2.relative(process.cwd(), filePath);
|
|
128
133
|
console.log(` \x1B[36mwould force\x1B[0m ${relative2}`);
|
|
129
134
|
},
|
|
130
135
|
wouldRemove: (filePath) => {
|
|
131
|
-
const relative2 =
|
|
136
|
+
const relative2 = path2.relative(process.cwd(), filePath);
|
|
132
137
|
console.log(`\x1B[36mwould remove\x1B[0m ${relative2}`);
|
|
133
138
|
},
|
|
134
139
|
error: (message) => {
|
|
@@ -138,6 +143,62 @@ var log = {
|
|
|
138
143
|
console.log(message);
|
|
139
144
|
}
|
|
140
145
|
};
|
|
146
|
+
|
|
147
|
+
// src/lib/strings.ts
|
|
148
|
+
function toPascalCase(str) {
|
|
149
|
+
return str.replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toUpperCase());
|
|
150
|
+
}
|
|
151
|
+
function toCamelCase(str) {
|
|
152
|
+
const pascal = toPascalCase(str);
|
|
153
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
154
|
+
}
|
|
155
|
+
function toSnakeCase(str) {
|
|
156
|
+
return str.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
157
|
+
}
|
|
158
|
+
function toKebabCase(str) {
|
|
159
|
+
return toSnakeCase(str).replace(/_/g, "-");
|
|
160
|
+
}
|
|
161
|
+
function pluralize(str) {
|
|
162
|
+
if (str.endsWith("y") && !/[aeiou]y$/.test(str)) {
|
|
163
|
+
return str.slice(0, -1) + "ies";
|
|
164
|
+
}
|
|
165
|
+
if (str.endsWith("s") || str.endsWith("x") || str.endsWith("ch") || str.endsWith("sh")) {
|
|
166
|
+
return str + "es";
|
|
167
|
+
}
|
|
168
|
+
return str + "s";
|
|
169
|
+
}
|
|
170
|
+
function singularize(str) {
|
|
171
|
+
if (str.endsWith("ies")) {
|
|
172
|
+
return str.slice(0, -3) + "y";
|
|
173
|
+
}
|
|
174
|
+
if (str.endsWith("es") && (str.endsWith("xes") || str.endsWith("ches") || str.endsWith("shes") || str.endsWith("sses"))) {
|
|
175
|
+
return str.slice(0, -2);
|
|
176
|
+
}
|
|
177
|
+
if (str.endsWith("s") && !str.endsWith("ss")) {
|
|
178
|
+
return str.slice(0, -1);
|
|
179
|
+
}
|
|
180
|
+
return str;
|
|
181
|
+
}
|
|
182
|
+
function createModelContext(name) {
|
|
183
|
+
const singularName = singularize(name);
|
|
184
|
+
const pluralName = pluralize(singularName);
|
|
185
|
+
return {
|
|
186
|
+
name,
|
|
187
|
+
singularName,
|
|
188
|
+
pluralName,
|
|
189
|
+
pascalName: toPascalCase(singularName),
|
|
190
|
+
pascalPlural: toPascalCase(pluralName),
|
|
191
|
+
camelName: toCamelCase(singularName),
|
|
192
|
+
camelPlural: toCamelCase(pluralName),
|
|
193
|
+
snakeName: toSnakeCase(singularName),
|
|
194
|
+
snakePlural: toSnakeCase(pluralName),
|
|
195
|
+
kebabName: toKebabCase(singularName),
|
|
196
|
+
kebabPlural: toKebabCase(pluralName),
|
|
197
|
+
tableName: pluralize(toSnakeCase(singularName))
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/lib/validation.ts
|
|
141
202
|
function validateModelName(name) {
|
|
142
203
|
if (!name) {
|
|
143
204
|
throw new Error("Model name is required");
|
|
@@ -186,6 +247,8 @@ function validateFieldDefinition(fieldDef) {
|
|
|
186
247
|
}
|
|
187
248
|
}
|
|
188
249
|
}
|
|
250
|
+
|
|
251
|
+
// src/lib/parsing.ts
|
|
189
252
|
function parseFields(fields) {
|
|
190
253
|
return fields.map((field) => {
|
|
191
254
|
validateFieldDefinition(field);
|
|
@@ -226,58 +289,8 @@ function parseFields(fields) {
|
|
|
226
289
|
return { name, type, isReference: false, isEnum: false, nullable, unique };
|
|
227
290
|
});
|
|
228
291
|
}
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
}
|
|
292
|
+
|
|
293
|
+
// src/lib/drizzle.ts
|
|
281
294
|
var SQLITE_TYPE_MAP = {
|
|
282
295
|
string: "text",
|
|
283
296
|
text: "text",
|
|
@@ -449,8 +462,12 @@ function getRequiredImports(fields, dialect, options = {}) {
|
|
|
449
462
|
}
|
|
450
463
|
return Array.from(types);
|
|
451
464
|
}
|
|
465
|
+
|
|
466
|
+
// src/lib/files.ts
|
|
467
|
+
import * as fs2 from "fs";
|
|
468
|
+
import * as path3 from "path";
|
|
452
469
|
function writeFile(filePath, content, options = {}) {
|
|
453
|
-
const exists =
|
|
470
|
+
const exists = fs2.existsSync(filePath);
|
|
454
471
|
if (exists && !options.force) {
|
|
455
472
|
log.skip(filePath);
|
|
456
473
|
return false;
|
|
@@ -463,11 +480,11 @@ function writeFile(filePath, content, options = {}) {
|
|
|
463
480
|
}
|
|
464
481
|
return true;
|
|
465
482
|
}
|
|
466
|
-
const dir =
|
|
467
|
-
if (!
|
|
468
|
-
|
|
483
|
+
const dir = path3.dirname(filePath);
|
|
484
|
+
if (!fs2.existsSync(dir)) {
|
|
485
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
469
486
|
}
|
|
470
|
-
|
|
487
|
+
fs2.writeFileSync(filePath, content);
|
|
471
488
|
if (exists && options.force) {
|
|
472
489
|
log.force(filePath);
|
|
473
490
|
} else {
|
|
@@ -476,7 +493,7 @@ function writeFile(filePath, content, options = {}) {
|
|
|
476
493
|
return true;
|
|
477
494
|
}
|
|
478
495
|
function deleteDirectory(dirPath, options = {}) {
|
|
479
|
-
if (!
|
|
496
|
+
if (!fs2.existsSync(dirPath)) {
|
|
480
497
|
log.notFound(dirPath);
|
|
481
498
|
return false;
|
|
482
499
|
}
|
|
@@ -484,23 +501,25 @@ function deleteDirectory(dirPath, options = {}) {
|
|
|
484
501
|
log.wouldRemove(dirPath);
|
|
485
502
|
return true;
|
|
486
503
|
}
|
|
487
|
-
|
|
504
|
+
fs2.rmSync(dirPath, { recursive: true });
|
|
488
505
|
log.remove(dirPath);
|
|
489
506
|
return true;
|
|
490
507
|
}
|
|
491
508
|
function fileExists(filePath) {
|
|
492
|
-
return
|
|
509
|
+
return fs2.existsSync(filePath);
|
|
493
510
|
}
|
|
494
511
|
function readFile(filePath) {
|
|
495
|
-
return
|
|
512
|
+
return fs2.readFileSync(filePath, "utf-8");
|
|
496
513
|
}
|
|
497
514
|
function modelExistsInSchema(tableName) {
|
|
498
|
-
const schemaPath =
|
|
499
|
-
if (!
|
|
515
|
+
const schemaPath = path3.join(getDbPath(), "schema.ts");
|
|
516
|
+
if (!fs2.existsSync(schemaPath)) {
|
|
500
517
|
return false;
|
|
501
518
|
}
|
|
502
|
-
const content =
|
|
503
|
-
const pattern = new RegExp(
|
|
519
|
+
const content = fs2.readFileSync(schemaPath, "utf-8");
|
|
520
|
+
const pattern = new RegExp(
|
|
521
|
+
`(?:sqliteTable|pgTable|mysqlTable)\\s*\\(\\s*["']${tableName}["']`
|
|
522
|
+
);
|
|
504
523
|
return pattern.test(content);
|
|
505
524
|
}
|
|
506
525
|
|
|
@@ -515,7 +534,7 @@ function generateModel(name, fieldArgs, options = {}) {
|
|
|
515
534
|
`Model "${ctx.pascalName}" already exists in schema. Use --force to regenerate.`
|
|
516
535
|
);
|
|
517
536
|
}
|
|
518
|
-
const schemaPath =
|
|
537
|
+
const schemaPath = path4.join(getDbPath(), "schema.ts");
|
|
519
538
|
if (fileExists(schemaPath) && !modelExistsInSchema(ctx.tableName)) {
|
|
520
539
|
appendToSchema(schemaPath, ctx.camelPlural, ctx.tableName, fields, dialect, options);
|
|
521
540
|
} else if (!fileExists(schemaPath)) {
|
|
@@ -625,22 +644,23 @@ function appendToSchema(schemaPath, modelName, tableName, fields, dialect, optio
|
|
|
625
644
|
}
|
|
626
645
|
|
|
627
646
|
// src/generators/actions.ts
|
|
628
|
-
import * as
|
|
647
|
+
import * as path5 from "path";
|
|
629
648
|
function generateActions(name, options = {}) {
|
|
630
649
|
validateModelName(name);
|
|
631
650
|
const ctx = createModelContext(name);
|
|
632
|
-
const actionsPath =
|
|
651
|
+
const actionsPath = path5.join(
|
|
633
652
|
getAppPath(),
|
|
634
653
|
ctx.kebabPlural,
|
|
635
654
|
"actions.ts"
|
|
636
655
|
);
|
|
637
|
-
const content = generateActionsContent(ctx);
|
|
656
|
+
const content = generateActionsContent(ctx, options);
|
|
638
657
|
writeFile(actionsPath, content, options);
|
|
639
658
|
}
|
|
640
|
-
function generateActionsContent(ctx) {
|
|
659
|
+
function generateActionsContent(ctx, options = {}) {
|
|
641
660
|
const { pascalName, pascalPlural, camelPlural, kebabPlural } = ctx;
|
|
642
661
|
const dbImport = getDbImport();
|
|
643
662
|
const schemaImport = getSchemaImport();
|
|
663
|
+
const idType = options.uuid ? "string" : "number";
|
|
644
664
|
return `"use server";
|
|
645
665
|
|
|
646
666
|
import { db } from "${dbImport}";
|
|
@@ -655,7 +675,7 @@ export async function get${pascalPlural}() {
|
|
|
655
675
|
return db.select().from(${camelPlural}).orderBy(desc(${camelPlural}.createdAt));
|
|
656
676
|
}
|
|
657
677
|
|
|
658
|
-
export async function get${pascalName}(id:
|
|
678
|
+
export async function get${pascalName}(id: ${idType}) {
|
|
659
679
|
const result = await db
|
|
660
680
|
.select()
|
|
661
681
|
.from(${camelPlural})
|
|
@@ -671,7 +691,7 @@ export async function create${pascalName}(data: Omit<New${pascalName}, "id" | "c
|
|
|
671
691
|
}
|
|
672
692
|
|
|
673
693
|
export async function update${pascalName}(
|
|
674
|
-
id:
|
|
694
|
+
id: ${idType},
|
|
675
695
|
data: Partial<Omit<New${pascalName}, "id" | "createdAt" | "updatedAt">>
|
|
676
696
|
) {
|
|
677
697
|
const result = await db
|
|
@@ -683,7 +703,7 @@ export async function update${pascalName}(
|
|
|
683
703
|
return result[0];
|
|
684
704
|
}
|
|
685
705
|
|
|
686
|
-
export async function delete${pascalName}(id:
|
|
706
|
+
export async function delete${pascalName}(id: ${idType}) {
|
|
687
707
|
await db.delete(${camelPlural}).where(eq(${camelPlural}.id, id));
|
|
688
708
|
revalidatePath("/${kebabPlural}");
|
|
689
709
|
}
|
|
@@ -691,7 +711,7 @@ export async function delete${pascalName}(id: number) {
|
|
|
691
711
|
}
|
|
692
712
|
|
|
693
713
|
// src/generators/scaffold.ts
|
|
694
|
-
import * as
|
|
714
|
+
import * as path6 from "path";
|
|
695
715
|
function generateScaffold(name, fieldArgs, options = {}) {
|
|
696
716
|
validateModelName(name);
|
|
697
717
|
const ctx = createModelContext(name);
|
|
@@ -710,25 +730,25 @@ Next steps:`);
|
|
|
710
730
|
}
|
|
711
731
|
function generatePages(ctx, fields, options = {}) {
|
|
712
732
|
const { pascalName, pascalPlural, camelName, kebabPlural } = ctx;
|
|
713
|
-
const basePath =
|
|
733
|
+
const basePath = path6.join(getAppPath(), kebabPlural);
|
|
714
734
|
writeFile(
|
|
715
|
-
|
|
735
|
+
path6.join(basePath, "page.tsx"),
|
|
716
736
|
generateIndexPage(pascalName, pascalPlural, camelName, kebabPlural, fields),
|
|
717
737
|
options
|
|
718
738
|
);
|
|
719
739
|
writeFile(
|
|
720
|
-
|
|
740
|
+
path6.join(basePath, "new", "page.tsx"),
|
|
721
741
|
generateNewPage(pascalName, camelName, kebabPlural, fields),
|
|
722
742
|
options
|
|
723
743
|
);
|
|
724
744
|
writeFile(
|
|
725
|
-
|
|
726
|
-
generateShowPage(pascalName, pascalPlural, camelName, kebabPlural, fields),
|
|
745
|
+
path6.join(basePath, "[id]", "page.tsx"),
|
|
746
|
+
generateShowPage(pascalName, pascalPlural, camelName, kebabPlural, fields, options),
|
|
727
747
|
options
|
|
728
748
|
);
|
|
729
749
|
writeFile(
|
|
730
|
-
|
|
731
|
-
generateEditPage(pascalName, camelName, kebabPlural, fields),
|
|
750
|
+
path6.join(basePath, "[id]", "edit", "page.tsx"),
|
|
751
|
+
generateEditPage(pascalName, camelName, kebabPlural, fields, options),
|
|
732
752
|
options
|
|
733
753
|
);
|
|
734
754
|
}
|
|
@@ -833,7 +853,12 @@ ${fields.map((f) => generateFormField(f, camelName)).join("\n\n")}
|
|
|
833
853
|
}
|
|
834
854
|
`;
|
|
835
855
|
}
|
|
836
|
-
function generateShowPage(pascalName, _pascalPlural, camelName, kebabPlural, fields) {
|
|
856
|
+
function generateShowPage(pascalName, _pascalPlural, camelName, kebabPlural, fields, options = {}) {
|
|
857
|
+
const idHandling = options.uuid ? `const ${camelName} = await get${pascalName}(id);` : `const numericId = Number(id);
|
|
858
|
+
if (isNaN(numericId)) {
|
|
859
|
+
notFound();
|
|
860
|
+
}
|
|
861
|
+
const ${camelName} = await get${pascalName}(numericId);`;
|
|
837
862
|
return `import { notFound } from "next/navigation";
|
|
838
863
|
import Link from "next/link";
|
|
839
864
|
import { get${pascalName} } from "../actions";
|
|
@@ -844,7 +869,7 @@ export default async function ${pascalName}Page({
|
|
|
844
869
|
params: Promise<{ id: string }>;
|
|
845
870
|
}) {
|
|
846
871
|
const { id } = await params;
|
|
847
|
-
|
|
872
|
+
${idHandling}
|
|
848
873
|
|
|
849
874
|
if (!${camelName}) {
|
|
850
875
|
notFound();
|
|
@@ -887,7 +912,13 @@ ${fields.map(
|
|
|
887
912
|
}
|
|
888
913
|
`;
|
|
889
914
|
}
|
|
890
|
-
function generateEditPage(pascalName, camelName, kebabPlural, fields) {
|
|
915
|
+
function generateEditPage(pascalName, camelName, kebabPlural, fields, options = {}) {
|
|
916
|
+
const idHandling = options.uuid ? `const ${camelName} = await get${pascalName}(id);` : `const numericId = Number(id);
|
|
917
|
+
if (isNaN(numericId)) {
|
|
918
|
+
notFound();
|
|
919
|
+
}
|
|
920
|
+
const ${camelName} = await get${pascalName}(numericId);`;
|
|
921
|
+
const updateId = options.uuid ? "id" : "numericId";
|
|
891
922
|
return `import { notFound, redirect } from "next/navigation";
|
|
892
923
|
import Link from "next/link";
|
|
893
924
|
import { get${pascalName}, update${pascalName} } from "../../actions";
|
|
@@ -898,7 +929,7 @@ export default async function Edit${pascalName}Page({
|
|
|
898
929
|
params: Promise<{ id: string }>;
|
|
899
930
|
}) {
|
|
900
931
|
const { id } = await params;
|
|
901
|
-
|
|
932
|
+
${idHandling}
|
|
902
933
|
|
|
903
934
|
if (!${camelName}) {
|
|
904
935
|
notFound();
|
|
@@ -906,7 +937,7 @@ export default async function Edit${pascalName}Page({
|
|
|
906
937
|
|
|
907
938
|
async function handleUpdate(formData: FormData) {
|
|
908
939
|
"use server";
|
|
909
|
-
await update${pascalName}(
|
|
940
|
+
await update${pascalName}(${updateId}, {
|
|
910
941
|
${fields.map((f) => ` ${f.name}: ${formDataValue(f)},`).join("\n")}
|
|
911
942
|
});
|
|
912
943
|
redirect("/${kebabPlural}");
|
|
@@ -1114,7 +1145,7 @@ Next steps:`);
|
|
|
1114
1145
|
}
|
|
1115
1146
|
|
|
1116
1147
|
// src/generators/api.ts
|
|
1117
|
-
import * as
|
|
1148
|
+
import * as path7 from "path";
|
|
1118
1149
|
function generateApi(name, fieldArgs, options = {}) {
|
|
1119
1150
|
validateModelName(name);
|
|
1120
1151
|
const ctx = createModelContext(name);
|
|
@@ -1130,19 +1161,19 @@ Next steps:`);
|
|
|
1130
1161
|
log.info(` 2. API available at /api/${ctx.kebabPlural}`);
|
|
1131
1162
|
}
|
|
1132
1163
|
function generateRoutes(camelPlural, kebabPlural, options) {
|
|
1133
|
-
const basePath =
|
|
1164
|
+
const basePath = path7.join(getAppPath(), "api", kebabPlural);
|
|
1134
1165
|
writeFile(
|
|
1135
|
-
|
|
1136
|
-
generateCollectionRoute(camelPlural),
|
|
1166
|
+
path7.join(basePath, "route.ts"),
|
|
1167
|
+
generateCollectionRoute(camelPlural, kebabPlural),
|
|
1137
1168
|
options
|
|
1138
1169
|
);
|
|
1139
1170
|
writeFile(
|
|
1140
|
-
|
|
1141
|
-
generateMemberRoute(camelPlural),
|
|
1171
|
+
path7.join(basePath, "[id]", "route.ts"),
|
|
1172
|
+
generateMemberRoute(camelPlural, kebabPlural, options),
|
|
1142
1173
|
options
|
|
1143
1174
|
);
|
|
1144
1175
|
}
|
|
1145
|
-
function generateCollectionRoute(camelPlural) {
|
|
1176
|
+
function generateCollectionRoute(camelPlural, kebabPlural) {
|
|
1146
1177
|
const dbImport = getDbImport();
|
|
1147
1178
|
const schemaImport = getSchemaImport();
|
|
1148
1179
|
return `import { db } from "${dbImport}";
|
|
@@ -1158,7 +1189,8 @@ export async function GET() {
|
|
|
1158
1189
|
.orderBy(desc(${camelPlural}.createdAt));
|
|
1159
1190
|
|
|
1160
1191
|
return NextResponse.json(data);
|
|
1161
|
-
} catch {
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
console.error("GET /api/${kebabPlural} failed:", error);
|
|
1162
1194
|
return NextResponse.json(
|
|
1163
1195
|
{ error: "Failed to fetch records" },
|
|
1164
1196
|
{ status: 500 }
|
|
@@ -1172,7 +1204,14 @@ export async function POST(request: Request) {
|
|
|
1172
1204
|
const result = await db.insert(${camelPlural}).values(body).returning();
|
|
1173
1205
|
|
|
1174
1206
|
return NextResponse.json(result[0], { status: 201 });
|
|
1175
|
-
} catch {
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
console.error("POST /api/${kebabPlural} failed:", error);
|
|
1209
|
+
if (error instanceof SyntaxError) {
|
|
1210
|
+
return NextResponse.json(
|
|
1211
|
+
{ error: "Invalid JSON in request body" },
|
|
1212
|
+
{ status: 400 }
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1176
1215
|
return NextResponse.json(
|
|
1177
1216
|
{ error: "Failed to create record" },
|
|
1178
1217
|
{ status: 500 }
|
|
@@ -1181,9 +1220,18 @@ export async function POST(request: Request) {
|
|
|
1181
1220
|
}
|
|
1182
1221
|
`;
|
|
1183
1222
|
}
|
|
1184
|
-
function generateMemberRoute(camelPlural) {
|
|
1223
|
+
function generateMemberRoute(camelPlural, kebabPlural, options = {}) {
|
|
1185
1224
|
const dbImport = getDbImport();
|
|
1186
1225
|
const schemaImport = getSchemaImport();
|
|
1226
|
+
const idValidation = options.uuid ? "" : `
|
|
1227
|
+
const numericId = Number(id);
|
|
1228
|
+
if (isNaN(numericId)) {
|
|
1229
|
+
return NextResponse.json(
|
|
1230
|
+
{ error: "Invalid ID format" },
|
|
1231
|
+
{ status: 400 }
|
|
1232
|
+
);
|
|
1233
|
+
}`;
|
|
1234
|
+
const idValue = options.uuid ? "id" : "numericId";
|
|
1187
1235
|
return `import { db } from "${dbImport}";
|
|
1188
1236
|
import { ${camelPlural} } from "${schemaImport}";
|
|
1189
1237
|
import { eq } from "drizzle-orm";
|
|
@@ -1193,11 +1241,11 @@ type Params = { params: Promise<{ id: string }> };
|
|
|
1193
1241
|
|
|
1194
1242
|
export async function GET(request: Request, { params }: Params) {
|
|
1195
1243
|
try {
|
|
1196
|
-
const { id } = await params
|
|
1244
|
+
const { id } = await params;${idValidation}
|
|
1197
1245
|
const result = await db
|
|
1198
1246
|
.select()
|
|
1199
1247
|
.from(${camelPlural})
|
|
1200
|
-
.where(eq(${camelPlural}.id,
|
|
1248
|
+
.where(eq(${camelPlural}.id, ${idValue}))
|
|
1201
1249
|
.limit(1);
|
|
1202
1250
|
|
|
1203
1251
|
if (!result[0]) {
|
|
@@ -1208,7 +1256,8 @@ export async function GET(request: Request, { params }: Params) {
|
|
|
1208
1256
|
}
|
|
1209
1257
|
|
|
1210
1258
|
return NextResponse.json(result[0]);
|
|
1211
|
-
} catch {
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
console.error("GET /api/${kebabPlural}/[id] failed:", error);
|
|
1212
1261
|
return NextResponse.json(
|
|
1213
1262
|
{ error: "Failed to fetch record" },
|
|
1214
1263
|
{ status: 500 }
|
|
@@ -1218,12 +1267,12 @@ export async function GET(request: Request, { params }: Params) {
|
|
|
1218
1267
|
|
|
1219
1268
|
export async function PATCH(request: Request, { params }: Params) {
|
|
1220
1269
|
try {
|
|
1221
|
-
const { id } = await params
|
|
1270
|
+
const { id } = await params;${idValidation}
|
|
1222
1271
|
const body = await request.json();
|
|
1223
1272
|
const result = await db
|
|
1224
1273
|
.update(${camelPlural})
|
|
1225
1274
|
.set({ ...body, updatedAt: new Date() })
|
|
1226
|
-
.where(eq(${camelPlural}.id,
|
|
1275
|
+
.where(eq(${camelPlural}.id, ${idValue}))
|
|
1227
1276
|
.returning();
|
|
1228
1277
|
|
|
1229
1278
|
if (!result[0]) {
|
|
@@ -1234,7 +1283,14 @@ export async function PATCH(request: Request, { params }: Params) {
|
|
|
1234
1283
|
}
|
|
1235
1284
|
|
|
1236
1285
|
return NextResponse.json(result[0]);
|
|
1237
|
-
} catch {
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
console.error("PATCH /api/${kebabPlural}/[id] failed:", error);
|
|
1288
|
+
if (error instanceof SyntaxError) {
|
|
1289
|
+
return NextResponse.json(
|
|
1290
|
+
{ error: "Invalid JSON in request body" },
|
|
1291
|
+
{ status: 400 }
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1238
1294
|
return NextResponse.json(
|
|
1239
1295
|
{ error: "Failed to update record" },
|
|
1240
1296
|
{ status: 500 }
|
|
@@ -1244,11 +1300,12 @@ export async function PATCH(request: Request, { params }: Params) {
|
|
|
1244
1300
|
|
|
1245
1301
|
export async function DELETE(request: Request, { params }: Params) {
|
|
1246
1302
|
try {
|
|
1247
|
-
const { id } = await params
|
|
1248
|
-
await db.delete(${camelPlural}).where(eq(${camelPlural}.id,
|
|
1303
|
+
const { id } = await params;${idValidation}
|
|
1304
|
+
await db.delete(${camelPlural}).where(eq(${camelPlural}.id, ${idValue}));
|
|
1249
1305
|
|
|
1250
1306
|
return new NextResponse(null, { status: 204 });
|
|
1251
|
-
} catch {
|
|
1307
|
+
} catch (error) {
|
|
1308
|
+
console.error("DELETE /api/${kebabPlural}/[id] failed:", error);
|
|
1252
1309
|
return NextResponse.json(
|
|
1253
1310
|
{ error: "Failed to delete record" },
|
|
1254
1311
|
{ status: 500 }
|
|
@@ -1259,7 +1316,7 @@ export async function DELETE(request: Request, { params }: Params) {
|
|
|
1259
1316
|
}
|
|
1260
1317
|
|
|
1261
1318
|
// src/generators/destroy.ts
|
|
1262
|
-
import * as
|
|
1319
|
+
import * as path8 from "path";
|
|
1263
1320
|
function destroyScaffold(name, options = {}) {
|
|
1264
1321
|
validateModelName(name);
|
|
1265
1322
|
const ctx = createModelContext(name);
|
|
@@ -1268,7 +1325,7 @@ function destroyScaffold(name, options = {}) {
|
|
|
1268
1325
|
log.info(`
|
|
1269
1326
|
${prefix}Destroying scaffold ${ctx.pascalName}...
|
|
1270
1327
|
`);
|
|
1271
|
-
const basePath =
|
|
1328
|
+
const basePath = path8.join(getAppPath(), ctx.kebabPlural);
|
|
1272
1329
|
deleteDirectory(basePath, options);
|
|
1273
1330
|
log.info(`
|
|
1274
1331
|
Note: Schema in ${config.dbPath}/schema.ts was not modified.`);
|
|
@@ -1282,7 +1339,7 @@ function destroyResource(name, options = {}) {
|
|
|
1282
1339
|
log.info(`
|
|
1283
1340
|
${prefix}Destroying resource ${ctx.pascalName}...
|
|
1284
1341
|
`);
|
|
1285
|
-
const basePath =
|
|
1342
|
+
const basePath = path8.join(getAppPath(), ctx.kebabPlural);
|
|
1286
1343
|
deleteDirectory(basePath, options);
|
|
1287
1344
|
log.info(`
|
|
1288
1345
|
Note: Schema in ${config.dbPath}/schema.ts was not modified.`);
|
|
@@ -1296,7 +1353,7 @@ function destroyApi(name, options = {}) {
|
|
|
1296
1353
|
log.info(`
|
|
1297
1354
|
${prefix}Destroying API ${ctx.pascalName}...
|
|
1298
1355
|
`);
|
|
1299
|
-
const basePath =
|
|
1356
|
+
const basePath = path8.join(getAppPath(), "api", ctx.kebabPlural);
|
|
1300
1357
|
deleteDirectory(basePath, options);
|
|
1301
1358
|
log.info(`
|
|
1302
1359
|
Note: Schema in ${config.dbPath}/schema.ts was not modified.`);
|