brizzle 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +319 -124
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -146,6 +146,7 @@ var log = {
|
|
|
146
146
|
};
|
|
147
147
|
|
|
148
148
|
// src/lib/strings.ts
|
|
149
|
+
import pluralizeLib from "pluralize";
|
|
149
150
|
function toPascalCase(str) {
|
|
150
151
|
return str.replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toUpperCase());
|
|
151
152
|
}
|
|
@@ -159,26 +160,14 @@ function toSnakeCase(str) {
|
|
|
159
160
|
function toKebabCase(str) {
|
|
160
161
|
return toSnakeCase(str).replace(/_/g, "-");
|
|
161
162
|
}
|
|
163
|
+
function escapeString(str) {
|
|
164
|
+
return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
165
|
+
}
|
|
162
166
|
function pluralize(str) {
|
|
163
|
-
|
|
164
|
-
return str.slice(0, -1) + "ies";
|
|
165
|
-
}
|
|
166
|
-
if (str.endsWith("s") || str.endsWith("x") || str.endsWith("ch") || str.endsWith("sh")) {
|
|
167
|
-
return str + "es";
|
|
168
|
-
}
|
|
169
|
-
return str + "s";
|
|
167
|
+
return pluralizeLib.plural(str);
|
|
170
168
|
}
|
|
171
169
|
function singularize(str) {
|
|
172
|
-
|
|
173
|
-
return str.slice(0, -3) + "y";
|
|
174
|
-
}
|
|
175
|
-
if (str.endsWith("es") && (str.endsWith("xes") || str.endsWith("ches") || str.endsWith("shes") || str.endsWith("sses"))) {
|
|
176
|
-
return str.slice(0, -2);
|
|
177
|
-
}
|
|
178
|
-
if (str.endsWith("s") && !str.endsWith("ss")) {
|
|
179
|
-
return str.slice(0, -1);
|
|
180
|
-
}
|
|
181
|
-
return str;
|
|
170
|
+
return pluralizeLib.singular(str);
|
|
182
171
|
}
|
|
183
172
|
function createModelContext(name) {
|
|
184
173
|
const singularName = singularize(name);
|
|
@@ -200,6 +189,82 @@ function createModelContext(name) {
|
|
|
200
189
|
}
|
|
201
190
|
|
|
202
191
|
// src/lib/validation.ts
|
|
192
|
+
var SQL_RESERVED_WORDS = [
|
|
193
|
+
// SQL keywords
|
|
194
|
+
"select",
|
|
195
|
+
"from",
|
|
196
|
+
"where",
|
|
197
|
+
"insert",
|
|
198
|
+
"update",
|
|
199
|
+
"delete",
|
|
200
|
+
"drop",
|
|
201
|
+
"create",
|
|
202
|
+
"alter",
|
|
203
|
+
"index",
|
|
204
|
+
"table",
|
|
205
|
+
"column",
|
|
206
|
+
"database",
|
|
207
|
+
"schema",
|
|
208
|
+
"and",
|
|
209
|
+
"or",
|
|
210
|
+
"not",
|
|
211
|
+
"null",
|
|
212
|
+
"true",
|
|
213
|
+
"false",
|
|
214
|
+
"order",
|
|
215
|
+
"by",
|
|
216
|
+
"group",
|
|
217
|
+
"having",
|
|
218
|
+
"limit",
|
|
219
|
+
"offset",
|
|
220
|
+
"join",
|
|
221
|
+
"left",
|
|
222
|
+
"right",
|
|
223
|
+
"inner",
|
|
224
|
+
"outer",
|
|
225
|
+
"on",
|
|
226
|
+
"as",
|
|
227
|
+
"in",
|
|
228
|
+
"between",
|
|
229
|
+
"like",
|
|
230
|
+
"is",
|
|
231
|
+
"case",
|
|
232
|
+
"when",
|
|
233
|
+
"then",
|
|
234
|
+
"else",
|
|
235
|
+
"end",
|
|
236
|
+
"exists",
|
|
237
|
+
"distinct",
|
|
238
|
+
"all",
|
|
239
|
+
"any",
|
|
240
|
+
"union",
|
|
241
|
+
"intersect",
|
|
242
|
+
"except",
|
|
243
|
+
"primary",
|
|
244
|
+
"foreign",
|
|
245
|
+
"key",
|
|
246
|
+
"references",
|
|
247
|
+
"unique",
|
|
248
|
+
"default",
|
|
249
|
+
"check",
|
|
250
|
+
"constraint",
|
|
251
|
+
// Common type names that might conflict
|
|
252
|
+
"int",
|
|
253
|
+
"integer",
|
|
254
|
+
"float",
|
|
255
|
+
"double",
|
|
256
|
+
"decimal",
|
|
257
|
+
"numeric",
|
|
258
|
+
"boolean",
|
|
259
|
+
"bool",
|
|
260
|
+
"text",
|
|
261
|
+
"varchar",
|
|
262
|
+
"char",
|
|
263
|
+
"date",
|
|
264
|
+
"time",
|
|
265
|
+
"timestamp",
|
|
266
|
+
"datetime"
|
|
267
|
+
];
|
|
203
268
|
function validateModelName(name) {
|
|
204
269
|
if (!name) {
|
|
205
270
|
throw new Error("Model name is required");
|
|
@@ -232,6 +297,11 @@ function validateFieldDefinition(fieldDef) {
|
|
|
232
297
|
`Invalid field name "${name}". Must be camelCase (start with lowercase letter).`
|
|
233
298
|
);
|
|
234
299
|
}
|
|
300
|
+
if (SQL_RESERVED_WORDS.includes(name.toLowerCase())) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`Field name "${name}" is a SQL reserved word. Consider renaming to "${name}Value" or "${name}Field".`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
235
305
|
if (type && !type.startsWith("references") && type !== "enum" && type !== "unique") {
|
|
236
306
|
if (!VALID_FIELD_TYPES.includes(type)) {
|
|
237
307
|
throw new Error(
|
|
@@ -246,6 +316,25 @@ function validateFieldDefinition(fieldDef) {
|
|
|
246
316
|
`Enum field "${name}" requires values. Example: ${name}:enum:draft,published,archived`
|
|
247
317
|
);
|
|
248
318
|
}
|
|
319
|
+
const values = enumValues.split(",");
|
|
320
|
+
for (const value of values) {
|
|
321
|
+
if (!value) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
`Enum field "${name}" has an empty value. Values must not be empty.`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(value)) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
`Invalid enum value "${value}" for field "${name}". Values must start with a letter and contain only letters, numbers, underscores, or hyphens.`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const unique = new Set(values);
|
|
333
|
+
if (unique.size !== values.length) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`Enum field "${name}" has duplicate values.`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
249
338
|
}
|
|
250
339
|
}
|
|
251
340
|
|
|
@@ -458,7 +547,7 @@ function getRequiredImports(fields, dialect, options = {}) {
|
|
|
458
547
|
const baseType = drizzleTypeDef.split("(")[0];
|
|
459
548
|
types.add(baseType);
|
|
460
549
|
}
|
|
461
|
-
if (dialect
|
|
550
|
+
if (dialect === "sqlite" && hasEnums) {
|
|
462
551
|
types.add("text");
|
|
463
552
|
}
|
|
464
553
|
return Array.from(types);
|
|
@@ -512,6 +601,9 @@ function fileExists(filePath) {
|
|
|
512
601
|
function readFile(filePath) {
|
|
513
602
|
return fs2.readFileSync(filePath, "utf-8");
|
|
514
603
|
}
|
|
604
|
+
function escapeRegExp(str) {
|
|
605
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
606
|
+
}
|
|
515
607
|
function modelExistsInSchema(tableName) {
|
|
516
608
|
const schemaPath = path3.join(getDbPath(), "schema.ts");
|
|
517
609
|
if (!fs2.existsSync(schemaPath)) {
|
|
@@ -519,10 +611,46 @@ function modelExistsInSchema(tableName) {
|
|
|
519
611
|
}
|
|
520
612
|
const content = fs2.readFileSync(schemaPath, "utf-8");
|
|
521
613
|
const pattern = new RegExp(
|
|
522
|
-
`(?:sqliteTable|pgTable|mysqlTable)\\s*\\(\\s*["']${tableName}["']`
|
|
614
|
+
`(?:sqliteTable|pgTable|mysqlTable)\\s*\\(\\s*["']${escapeRegExp(tableName)}["']`
|
|
523
615
|
);
|
|
524
616
|
return pattern.test(content);
|
|
525
617
|
}
|
|
618
|
+
function removeModelFromSchemaContent(content, tableName) {
|
|
619
|
+
const lines = content.split("\n");
|
|
620
|
+
const tablePattern = new RegExp(
|
|
621
|
+
`^export\\s+const\\s+\\w+\\s*=\\s*(?:sqliteTable|pgTable|mysqlTable)\\s*\\(\\s*["']${escapeRegExp(tableName)}["']`
|
|
622
|
+
);
|
|
623
|
+
let startIdx = -1;
|
|
624
|
+
let endIdx = -1;
|
|
625
|
+
let braceCount = 0;
|
|
626
|
+
let foundOpenBrace = false;
|
|
627
|
+
for (let i = 0; i < lines.length; i++) {
|
|
628
|
+
if (startIdx === -1) {
|
|
629
|
+
if (tablePattern.test(lines[i])) {
|
|
630
|
+
startIdx = i;
|
|
631
|
+
} else {
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
for (const char of lines[i]) {
|
|
636
|
+
if (char === "{") {
|
|
637
|
+
braceCount++;
|
|
638
|
+
foundOpenBrace = true;
|
|
639
|
+
} else if (char === "}") {
|
|
640
|
+
braceCount--;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (foundOpenBrace && braceCount === 0) {
|
|
644
|
+
endIdx = i;
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
649
|
+
return content;
|
|
650
|
+
}
|
|
651
|
+
lines.splice(startIdx, endIdx - startIdx + 1);
|
|
652
|
+
return lines.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
653
|
+
}
|
|
526
654
|
|
|
527
655
|
// src/generators/model.ts
|
|
528
656
|
function generateModel(name, fieldArgs, options = {}) {
|
|
@@ -536,15 +664,13 @@ function generateModel(name, fieldArgs, options = {}) {
|
|
|
536
664
|
);
|
|
537
665
|
}
|
|
538
666
|
const schemaPath = path4.join(getDbPath(), "schema.ts");
|
|
539
|
-
if (fileExists(schemaPath)
|
|
540
|
-
appendToSchema(schemaPath, ctx.camelPlural, ctx.tableName, fields, dialect, options);
|
|
541
|
-
} else if (!fileExists(schemaPath)) {
|
|
667
|
+
if (!fileExists(schemaPath)) {
|
|
542
668
|
const schemaContent = generateSchemaContent(ctx.camelPlural, ctx.tableName, fields, dialect, options);
|
|
543
669
|
writeFile(schemaPath, schemaContent, options);
|
|
670
|
+
} else if (modelExistsInSchema(ctx.tableName)) {
|
|
671
|
+
replaceInSchema(schemaPath, ctx.camelPlural, ctx.tableName, fields, dialect, options);
|
|
544
672
|
} else {
|
|
545
|
-
|
|
546
|
-
`Cannot regenerate model "${ctx.pascalName}" - manual removal from schema required.`
|
|
547
|
-
);
|
|
673
|
+
appendToSchema(schemaPath, ctx.camelPlural, ctx.tableName, fields, dialect, options);
|
|
548
674
|
}
|
|
549
675
|
}
|
|
550
676
|
function generateSchemaContent(modelName, tableName, fields, dialect, options = {}) {
|
|
@@ -567,7 +693,7 @@ function generateEnumDefinitions(fields, dialect) {
|
|
|
567
693
|
}
|
|
568
694
|
return enumFields.map((field) => {
|
|
569
695
|
const enumName = `${field.name}Enum`;
|
|
570
|
-
const values = field.enumValues.map((v) => `"${v}"`).join(", ");
|
|
696
|
+
const values = field.enumValues.map((v) => `"${escapeString(v)}"`).join(", ");
|
|
571
697
|
return `
|
|
572
698
|
export const ${enumName} = pgEnum("${toSnakeCase(field.name)}", [${values}]);`;
|
|
573
699
|
}).join("\n");
|
|
@@ -576,7 +702,7 @@ function generateTableDefinition(modelName, tableName, fields, dialect, options
|
|
|
576
702
|
const tableFunction = getTableFunction(dialect);
|
|
577
703
|
const idColumn = getIdColumn(dialect, options.uuid);
|
|
578
704
|
const timestampColumns = getTimestampColumns(dialect, options.noTimestamps);
|
|
579
|
-
const fieldDefinitions = generateFieldDefinitions(fields, dialect);
|
|
705
|
+
const fieldDefinitions = generateFieldDefinitions(fields, dialect, options);
|
|
580
706
|
const lines = [` ${idColumn},`];
|
|
581
707
|
if (fieldDefinitions) {
|
|
582
708
|
lines.push(fieldDefinitions);
|
|
@@ -588,7 +714,7 @@ function generateTableDefinition(modelName, tableName, fields, dialect, options
|
|
|
588
714
|
${lines.join("\n")}
|
|
589
715
|
});`;
|
|
590
716
|
}
|
|
591
|
-
function generateFieldDefinitions(fields, dialect) {
|
|
717
|
+
function generateFieldDefinitions(fields, dialect, options = {}) {
|
|
592
718
|
return fields.map((field) => {
|
|
593
719
|
const columnName = toSnakeCase(field.name);
|
|
594
720
|
const modifiers = getFieldModifiers(field);
|
|
@@ -597,8 +723,8 @@ function generateFieldDefinitions(fields, dialect) {
|
|
|
597
723
|
}
|
|
598
724
|
const drizzleTypeDef = drizzleType(field, dialect);
|
|
599
725
|
if (field.isReference && field.referenceTo) {
|
|
600
|
-
const
|
|
601
|
-
return ` ${field.name}: ${
|
|
726
|
+
const refTarget = `${toCamelCase(pluralize(field.referenceTo))}.id`;
|
|
727
|
+
return ` ${field.name}: ${getReferenceType(dialect, columnName, options.uuid)}.references(() => ${refTarget})${modifiers},`;
|
|
602
728
|
}
|
|
603
729
|
if (dialect === "mysql" && drizzleTypeDef === "varchar") {
|
|
604
730
|
const length = field.type === "uuid" ? 36 : 255;
|
|
@@ -622,6 +748,24 @@ function getFieldModifiers(field) {
|
|
|
622
748
|
}
|
|
623
749
|
return modifiers.join("");
|
|
624
750
|
}
|
|
751
|
+
function getReferenceType(dialect, columnName, useUuid = false) {
|
|
752
|
+
if (useUuid) {
|
|
753
|
+
switch (dialect) {
|
|
754
|
+
case "postgresql":
|
|
755
|
+
return `uuid("${columnName}")`;
|
|
756
|
+
case "mysql":
|
|
757
|
+
return `varchar("${columnName}", { length: 36 })`;
|
|
758
|
+
default:
|
|
759
|
+
return `text("${columnName}")`;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
switch (dialect) {
|
|
763
|
+
case "mysql":
|
|
764
|
+
return `int("${columnName}")`;
|
|
765
|
+
default:
|
|
766
|
+
return `integer("${columnName}")`;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
625
769
|
function generateEnumField(field, columnName, dialect) {
|
|
626
770
|
const values = field.enumValues;
|
|
627
771
|
const modifiers = getFieldModifiers(field);
|
|
@@ -629,10 +773,10 @@ function generateEnumField(field, columnName, dialect) {
|
|
|
629
773
|
case "postgresql":
|
|
630
774
|
return ` ${field.name}: ${field.name}Enum("${columnName}")${modifiers},`;
|
|
631
775
|
case "mysql":
|
|
632
|
-
const mysqlValues = values.map((v) => `"${v}"`).join(", ");
|
|
776
|
+
const mysqlValues = values.map((v) => `"${escapeString(v)}"`).join(", ");
|
|
633
777
|
return ` ${field.name}: mysqlEnum("${columnName}", [${mysqlValues}])${modifiers},`;
|
|
634
778
|
default:
|
|
635
|
-
const sqliteValues = values.map((v) => `"${v}"`).join(", ");
|
|
779
|
+
const sqliteValues = values.map((v) => `"${escapeString(v)}"`).join(", ");
|
|
636
780
|
return ` ${field.name}: text("${columnName}", { enum: [${sqliteValues}] })${modifiers},`;
|
|
637
781
|
}
|
|
638
782
|
}
|
|
@@ -643,6 +787,14 @@ function appendToSchema(schemaPath, modelName, tableName, fields, dialect, optio
|
|
|
643
787
|
const newContent = existingContent + enumDefinitions + "\n" + tableDefinition + "\n";
|
|
644
788
|
writeFile(schemaPath, newContent, { force: true, dryRun: options.dryRun });
|
|
645
789
|
}
|
|
790
|
+
function replaceInSchema(schemaPath, modelName, tableName, fields, dialect, options) {
|
|
791
|
+
const existingContent = readFile(schemaPath);
|
|
792
|
+
const cleanedContent = removeModelFromSchemaContent(existingContent, tableName);
|
|
793
|
+
const enumDefinitions = generateEnumDefinitions(fields, dialect);
|
|
794
|
+
const tableDefinition = generateTableDefinition(modelName, tableName, fields, dialect, options);
|
|
795
|
+
const newContent = cleanedContent.trimEnd() + "\n" + enumDefinitions + "\n" + tableDefinition + "\n";
|
|
796
|
+
writeFile(schemaPath, newContent, { force: true, dryRun: options.dryRun });
|
|
797
|
+
}
|
|
646
798
|
|
|
647
799
|
// src/generators/actions.ts
|
|
648
800
|
import * as path5 from "path";
|
|
@@ -682,12 +834,15 @@ export async function get${pascalName}(id: ${idType}) {
|
|
|
682
834
|
.from(${camelPlural})
|
|
683
835
|
.where(eq(${camelPlural}.id, id))
|
|
684
836
|
.limit(1);
|
|
837
|
+
|
|
685
838
|
return result[0] ?? null;
|
|
686
839
|
}
|
|
687
840
|
|
|
688
841
|
export async function create${pascalName}(data: Omit<New${pascalName}, "id" | "createdAt" | "updatedAt">) {
|
|
689
842
|
const result = await db.insert(${camelPlural}).values(data).returning();
|
|
843
|
+
|
|
690
844
|
revalidatePath("/${kebabPlural}");
|
|
845
|
+
|
|
691
846
|
return result[0];
|
|
692
847
|
}
|
|
693
848
|
|
|
@@ -700,12 +855,15 @@ export async function update${pascalName}(
|
|
|
700
855
|
.set({ ...data, updatedAt: new Date() })
|
|
701
856
|
.where(eq(${camelPlural}.id, id))
|
|
702
857
|
.returning();
|
|
858
|
+
|
|
703
859
|
revalidatePath("/${kebabPlural}");
|
|
860
|
+
|
|
704
861
|
return result[0];
|
|
705
862
|
}
|
|
706
863
|
|
|
707
864
|
export async function delete${pascalName}(id: ${idType}) {
|
|
708
865
|
await db.delete(${camelPlural}).where(eq(${camelPlural}.id, id));
|
|
866
|
+
|
|
709
867
|
revalidatePath("/${kebabPlural}");
|
|
710
868
|
}
|
|
711
869
|
`;
|
|
@@ -821,9 +979,11 @@ import { create${pascalName} } from "../actions";
|
|
|
821
979
|
export default function New${pascalName}Page() {
|
|
822
980
|
async function handleCreate(formData: FormData) {
|
|
823
981
|
"use server";
|
|
982
|
+
|
|
824
983
|
await create${pascalName}({
|
|
825
984
|
${fields.map((f) => ` ${f.name}: ${formDataValue(f)},`).join("\n")}
|
|
826
985
|
});
|
|
986
|
+
|
|
827
987
|
redirect("/${kebabPlural}");
|
|
828
988
|
}
|
|
829
989
|
|
|
@@ -856,9 +1016,11 @@ ${fields.map((f) => generateFormField(f, camelName)).join("\n\n")}
|
|
|
856
1016
|
}
|
|
857
1017
|
function generateShowPage(pascalName, _pascalPlural, camelName, kebabPlural, fields, options = {}) {
|
|
858
1018
|
const idHandling = options.uuid ? `const ${camelName} = await get${pascalName}(id);` : `const numericId = Number(id);
|
|
1019
|
+
|
|
859
1020
|
if (isNaN(numericId)) {
|
|
860
1021
|
notFound();
|
|
861
1022
|
}
|
|
1023
|
+
|
|
862
1024
|
const ${camelName} = await get${pascalName}(numericId);`;
|
|
863
1025
|
return `import { notFound } from "next/navigation";
|
|
864
1026
|
import Link from "next/link";
|
|
@@ -902,11 +1064,11 @@ ${fields.map(
|
|
|
902
1064
|
<dt className="text-sm text-gray-500">${toPascalCase(f.name)}</dt>
|
|
903
1065
|
<dd className="mt-1 text-gray-900">{${camelName}.${f.name}}</dd>
|
|
904
1066
|
</div>`
|
|
905
|
-
).join("\n")}
|
|
1067
|
+
).join("\n")}${options.noTimestamps ? "" : `
|
|
906
1068
|
<div className="py-3">
|
|
907
1069
|
<dt className="text-sm text-gray-500">Created At</dt>
|
|
908
1070
|
<dd className="mt-1 text-gray-900">{${camelName}.createdAt.toLocaleString()}</dd>
|
|
909
|
-
</div
|
|
1071
|
+
</div>`}
|
|
910
1072
|
</dl>
|
|
911
1073
|
</div>
|
|
912
1074
|
);
|
|
@@ -915,9 +1077,11 @@ ${fields.map(
|
|
|
915
1077
|
}
|
|
916
1078
|
function generateEditPage(pascalName, camelName, kebabPlural, fields, options = {}) {
|
|
917
1079
|
const idHandling = options.uuid ? `const ${camelName} = await get${pascalName}(id);` : `const numericId = Number(id);
|
|
1080
|
+
|
|
918
1081
|
if (isNaN(numericId)) {
|
|
919
1082
|
notFound();
|
|
920
1083
|
}
|
|
1084
|
+
|
|
921
1085
|
const ${camelName} = await get${pascalName}(numericId);`;
|
|
922
1086
|
const updateId = options.uuid ? "id" : "numericId";
|
|
923
1087
|
return `import { notFound, redirect } from "next/navigation";
|
|
@@ -938,9 +1102,11 @@ export default async function Edit${pascalName}Page({
|
|
|
938
1102
|
|
|
939
1103
|
async function handleUpdate(formData: FormData) {
|
|
940
1104
|
"use server";
|
|
1105
|
+
|
|
941
1106
|
await update${pascalName}(${updateId}, {
|
|
942
1107
|
${fields.map((f) => ` ${f.name}: ${formDataValue(f)},`).join("\n")}
|
|
943
1108
|
});
|
|
1109
|
+
|
|
944
1110
|
redirect("/${kebabPlural}");
|
|
945
1111
|
}
|
|
946
1112
|
|
|
@@ -971,17 +1137,20 @@ ${fields.map((f) => generateFormField(f, camelName, true)).join("\n\n")}
|
|
|
971
1137
|
}
|
|
972
1138
|
`;
|
|
973
1139
|
}
|
|
974
|
-
function
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1140
|
+
function createFieldContext(field, camelName, withDefault) {
|
|
1141
|
+
return {
|
|
1142
|
+
field,
|
|
1143
|
+
label: toPascalCase(field.name),
|
|
1144
|
+
optionalLabel: field.nullable ? ` <span className="text-gray-400">(optional)</span>` : "",
|
|
1145
|
+
required: field.nullable ? "" : " required",
|
|
1146
|
+
defaultValue: withDefault ? ` defaultValue={${camelName}.${field.name}}` : ""
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
function generateTextareaField(ctx) {
|
|
1150
|
+
const { field, label, optionalLabel, required, defaultValue } = ctx;
|
|
1151
|
+
const rows = field.type === "json" ? 6 : 4;
|
|
1152
|
+
const placeholder = field.type === "json" ? ` placeholder="{}"` : "";
|
|
1153
|
+
return ` <div>
|
|
985
1154
|
<label htmlFor="${field.name}" className="block text-sm font-medium text-gray-700">
|
|
986
1155
|
${label}${optionalLabel}
|
|
987
1156
|
</label>
|
|
@@ -989,13 +1158,14 @@ function generateFormField(field, camelName, withDefault = false) {
|
|
|
989
1158
|
id="${field.name}"
|
|
990
1159
|
name="${field.name}"
|
|
991
1160
|
rows={${rows}}
|
|
992
|
-
className="
|
|
1161
|
+
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 resize-none"${defaultValue}${placeholder}${required}
|
|
993
1162
|
/>
|
|
994
1163
|
</div>`;
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1164
|
+
}
|
|
1165
|
+
function generateCheckboxField(ctx, camelName, withDefault) {
|
|
1166
|
+
const { field, label } = ctx;
|
|
1167
|
+
const defaultChecked = withDefault ? ` defaultChecked={${camelName}.${field.name}}` : "";
|
|
1168
|
+
return ` <div className="flex items-center gap-2">
|
|
999
1169
|
<input
|
|
1000
1170
|
type="checkbox"
|
|
1001
1171
|
id="${field.name}"
|
|
@@ -1006,38 +1176,27 @@ function generateFormField(field, camelName, withDefault = false) {
|
|
|
1006
1176
|
${label}
|
|
1007
1177
|
</label>
|
|
1008
1178
|
</div>`;
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
${
|
|
1014
|
-
|
|
1015
|
-
<input
|
|
1016
|
-
type="number"
|
|
1017
|
-
id="${field.name}"
|
|
1018
|
-
name="${field.name}"
|
|
1019
|
-
className="${inputClasses}"${defaultValue}${required}
|
|
1020
|
-
/>
|
|
1021
|
-
</div>`;
|
|
1022
|
-
}
|
|
1023
|
-
if (field.type === "float" || field.type === "decimal") {
|
|
1024
|
-
const step = field.type === "decimal" ? "0.01" : "any";
|
|
1025
|
-
return ` <div>
|
|
1179
|
+
}
|
|
1180
|
+
function generateNumberField(ctx, step) {
|
|
1181
|
+
const { field, label, optionalLabel, required, defaultValue } = ctx;
|
|
1182
|
+
const stepAttr = step ? `
|
|
1183
|
+
step="${step}"` : "";
|
|
1184
|
+
return ` <div>
|
|
1026
1185
|
<label htmlFor="${field.name}" className="block text-sm font-medium text-gray-700">
|
|
1027
1186
|
${label}${optionalLabel}
|
|
1028
1187
|
</label>
|
|
1029
1188
|
<input
|
|
1030
|
-
type="number"
|
|
1031
|
-
step="${step}"
|
|
1189
|
+
type="number"${stepAttr}
|
|
1032
1190
|
id="${field.name}"
|
|
1033
1191
|
name="${field.name}"
|
|
1034
|
-
className="
|
|
1192
|
+
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}
|
|
1035
1193
|
/>
|
|
1036
1194
|
</div>`;
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1195
|
+
}
|
|
1196
|
+
function generateDateField(ctx, camelName, withDefault) {
|
|
1197
|
+
const { field, label, optionalLabel, required } = ctx;
|
|
1198
|
+
const dateDefault = withDefault ? ` defaultValue={${camelName}.${field.name}?.toISOString().split("T")[0]}` : "";
|
|
1199
|
+
return ` <div>
|
|
1041
1200
|
<label htmlFor="${field.name}" className="block text-sm font-medium text-gray-700">
|
|
1042
1201
|
${label}${optionalLabel}
|
|
1043
1202
|
</label>
|
|
@@ -1045,13 +1204,14 @@ function generateFormField(field, camelName, withDefault = false) {
|
|
|
1045
1204
|
type="date"
|
|
1046
1205
|
id="${field.name}"
|
|
1047
1206
|
name="${field.name}"
|
|
1048
|
-
className="
|
|
1207
|
+
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}
|
|
1049
1208
|
/>
|
|
1050
1209
|
</div>`;
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1210
|
+
}
|
|
1211
|
+
function generateDatetimeField(ctx, camelName, withDefault) {
|
|
1212
|
+
const { field, label, optionalLabel, required } = ctx;
|
|
1213
|
+
const dateDefault = withDefault ? ` defaultValue={${camelName}.${field.name}?.toISOString().slice(0, 16)}` : "";
|
|
1214
|
+
return ` <div>
|
|
1055
1215
|
<label htmlFor="${field.name}" className="block text-sm font-medium text-gray-700">
|
|
1056
1216
|
${label}${optionalLabel}
|
|
1057
1217
|
</label>
|
|
@@ -1059,25 +1219,28 @@ function generateFormField(field, camelName, withDefault = false) {
|
|
|
1059
1219
|
type="datetime-local"
|
|
1060
1220
|
id="${field.name}"
|
|
1061
1221
|
name="${field.name}"
|
|
1062
|
-
className="
|
|
1222
|
+
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}
|
|
1063
1223
|
/>
|
|
1064
1224
|
</div>`;
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1225
|
+
}
|
|
1226
|
+
function generateSelectField(ctx) {
|
|
1227
|
+
const { field, label, optionalLabel, required, defaultValue } = ctx;
|
|
1228
|
+
const options = field.enumValues.map((v) => ` <option value="${escapeString(v)}">${toPascalCase(v)}</option>`).join("\n");
|
|
1229
|
+
return ` <div>
|
|
1069
1230
|
<label htmlFor="${field.name}" className="block text-sm font-medium text-gray-700">
|
|
1070
1231
|
${label}${optionalLabel}
|
|
1071
1232
|
</label>
|
|
1072
1233
|
<select
|
|
1073
1234
|
id="${field.name}"
|
|
1074
1235
|
name="${field.name}"
|
|
1075
|
-
className="
|
|
1236
|
+
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}
|
|
1076
1237
|
>
|
|
1077
1238
|
${options}
|
|
1078
1239
|
</select>
|
|
1079
1240
|
</div>`;
|
|
1080
|
-
|
|
1241
|
+
}
|
|
1242
|
+
function generateTextField(ctx) {
|
|
1243
|
+
const { field, label, optionalLabel, required, defaultValue } = ctx;
|
|
1081
1244
|
return ` <div>
|
|
1082
1245
|
<label htmlFor="${field.name}" className="block text-sm font-medium text-gray-700">
|
|
1083
1246
|
${label}${optionalLabel}
|
|
@@ -1086,14 +1249,46 @@ ${options}
|
|
|
1086
1249
|
type="text"
|
|
1087
1250
|
id="${field.name}"
|
|
1088
1251
|
name="${field.name}"
|
|
1089
|
-
className="
|
|
1252
|
+
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}
|
|
1090
1253
|
/>
|
|
1091
1254
|
</div>`;
|
|
1092
1255
|
}
|
|
1256
|
+
function generateFormField(field, camelName, withDefault = false) {
|
|
1257
|
+
const ctx = createFieldContext(field, camelName, withDefault);
|
|
1258
|
+
switch (field.type) {
|
|
1259
|
+
case "text":
|
|
1260
|
+
case "json":
|
|
1261
|
+
return generateTextareaField(ctx);
|
|
1262
|
+
case "boolean":
|
|
1263
|
+
case "bool":
|
|
1264
|
+
return generateCheckboxField(ctx, camelName, withDefault);
|
|
1265
|
+
case "integer":
|
|
1266
|
+
case "int":
|
|
1267
|
+
case "bigint":
|
|
1268
|
+
return generateNumberField(ctx);
|
|
1269
|
+
case "float":
|
|
1270
|
+
return generateNumberField(ctx, "any");
|
|
1271
|
+
case "decimal":
|
|
1272
|
+
return generateNumberField(ctx, "0.01");
|
|
1273
|
+
case "date":
|
|
1274
|
+
return generateDateField(ctx, camelName, withDefault);
|
|
1275
|
+
case "datetime":
|
|
1276
|
+
case "timestamp":
|
|
1277
|
+
return generateDatetimeField(ctx, camelName, withDefault);
|
|
1278
|
+
default:
|
|
1279
|
+
if (field.isEnum && field.enumValues) {
|
|
1280
|
+
return generateSelectField(ctx);
|
|
1281
|
+
}
|
|
1282
|
+
return generateTextField(ctx);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1093
1285
|
function formDataValue(field) {
|
|
1094
1286
|
const getValue = `formData.get("${field.name}")`;
|
|
1095
1287
|
const asString = `${getValue} as string`;
|
|
1096
1288
|
if (field.nullable) {
|
|
1289
|
+
if (field.type === "boolean" || field.type === "bool") {
|
|
1290
|
+
return `${getValue} === "on" ? true : null`;
|
|
1291
|
+
}
|
|
1097
1292
|
if (field.type === "integer" || field.type === "int" || field.type === "bigint") {
|
|
1098
1293
|
return `${getValue} ? parseInt(${asString}) : null`;
|
|
1099
1294
|
}
|
|
@@ -1120,6 +1315,9 @@ function formDataValue(field) {
|
|
|
1120
1315
|
if (field.type === "float") {
|
|
1121
1316
|
return `parseFloat(${asString})`;
|
|
1122
1317
|
}
|
|
1318
|
+
if (field.type === "decimal") {
|
|
1319
|
+
return asString;
|
|
1320
|
+
}
|
|
1123
1321
|
if (field.type === "datetime" || field.type === "timestamp" || field.type === "date") {
|
|
1124
1322
|
return `new Date(${asString})`;
|
|
1125
1323
|
}
|
|
@@ -1318,47 +1516,44 @@ export async function DELETE(request: Request, { params }: Params) {
|
|
|
1318
1516
|
|
|
1319
1517
|
// src/generators/destroy.ts
|
|
1320
1518
|
import * as path8 from "path";
|
|
1321
|
-
|
|
1519
|
+
import { confirm, isCancel } from "@clack/prompts";
|
|
1520
|
+
async function destroy(name, type, buildPath, options = {}) {
|
|
1322
1521
|
validateModelName(name);
|
|
1323
1522
|
const ctx = createModelContext(name);
|
|
1324
|
-
const config = detectProjectConfig();
|
|
1325
1523
|
const prefix = options.dryRun ? "[dry-run] " : "";
|
|
1326
1524
|
log.info(`
|
|
1327
|
-
${prefix}Destroying
|
|
1525
|
+
${prefix}Destroying ${type} ${ctx.pascalName}...
|
|
1328
1526
|
`);
|
|
1329
|
-
const basePath =
|
|
1527
|
+
const basePath = buildPath(ctx);
|
|
1528
|
+
if (!options.dryRun && !options.force && fileExists(basePath)) {
|
|
1529
|
+
const confirmed = await confirm({
|
|
1530
|
+
message: `Delete ${basePath}?`
|
|
1531
|
+
});
|
|
1532
|
+
if (isCancel(confirmed) || !confirmed) {
|
|
1533
|
+
log.info("Aborted.");
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1330
1537
|
deleteDirectory(basePath, options);
|
|
1331
|
-
|
|
1332
|
-
Note: Schema in ${config.dbPath}/schema.ts was not modified.`);
|
|
1333
|
-
log.info(` Remove the table definition manually if needed.`);
|
|
1538
|
+
removeFromSchema(ctx.tableName, options);
|
|
1334
1539
|
}
|
|
1335
|
-
function
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
const
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
const basePath = path8.join(getAppPath(), ctx.kebabPlural);
|
|
1344
|
-
deleteDirectory(basePath, options);
|
|
1345
|
-
log.info(`
|
|
1346
|
-
Note: Schema in ${config.dbPath}/schema.ts was not modified.`);
|
|
1347
|
-
log.info(` Remove the table definition manually if needed.`);
|
|
1540
|
+
function removeFromSchema(tableName, options) {
|
|
1541
|
+
if (!modelExistsInSchema(tableName)) {
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
const schemaPath = path8.join(getDbPath(), "schema.ts");
|
|
1545
|
+
const content = readFile(schemaPath);
|
|
1546
|
+
const cleaned = removeModelFromSchemaContent(content, tableName);
|
|
1547
|
+
writeFile(schemaPath, cleaned, { force: true, dryRun: options.dryRun });
|
|
1348
1548
|
}
|
|
1349
|
-
function
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
const basePath = path8.join(getAppPath(), "api", ctx.kebabPlural);
|
|
1358
|
-
deleteDirectory(basePath, options);
|
|
1359
|
-
log.info(`
|
|
1360
|
-
Note: Schema in ${config.dbPath}/schema.ts was not modified.`);
|
|
1361
|
-
log.info(` Remove the table definition manually if needed.`);
|
|
1549
|
+
async function destroyScaffold(name, options = {}) {
|
|
1550
|
+
return destroy(name, "scaffold", (ctx) => path8.join(getAppPath(), ctx.kebabPlural), options);
|
|
1551
|
+
}
|
|
1552
|
+
async function destroyResource(name, options = {}) {
|
|
1553
|
+
return destroy(name, "resource", (ctx) => path8.join(getAppPath(), ctx.kebabPlural), options);
|
|
1554
|
+
}
|
|
1555
|
+
async function destroyApi(name, options = {}) {
|
|
1556
|
+
return destroy(name, "API", (ctx) => path8.join(getAppPath(), "api", ctx.kebabPlural), options);
|
|
1362
1557
|
}
|
|
1363
1558
|
|
|
1364
1559
|
// src/index.ts
|
|
@@ -1460,17 +1655,17 @@ program.command("destroy <type> <name>").alias("d").description(
|
|
|
1460
1655
|
Examples:
|
|
1461
1656
|
brizzle destroy scaffold post
|
|
1462
1657
|
brizzle d api product --dry-run`
|
|
1463
|
-
).option("-n, --dry-run", "Preview changes without deleting files").action((type, name, opts) => {
|
|
1658
|
+
).option("-f, --force", "Skip confirmation prompt").option("-n, --dry-run", "Preview changes without deleting files").action(async (type, name, opts) => {
|
|
1464
1659
|
try {
|
|
1465
1660
|
switch (type) {
|
|
1466
1661
|
case "scaffold":
|
|
1467
|
-
destroyScaffold(name, opts);
|
|
1662
|
+
await destroyScaffold(name, opts);
|
|
1468
1663
|
break;
|
|
1469
1664
|
case "resource":
|
|
1470
|
-
destroyResource(name, opts);
|
|
1665
|
+
await destroyResource(name, opts);
|
|
1471
1666
|
break;
|
|
1472
1667
|
case "api":
|
|
1473
|
-
destroyApi(name, opts);
|
|
1668
|
+
await destroyApi(name, opts);
|
|
1474
1669
|
break;
|
|
1475
1670
|
default:
|
|
1476
1671
|
throw new Error(`Unknown type "${type}". Use: scaffold, resource, or api`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brizzle",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Rails-like generators for Next.js + Drizzle ORM projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,10 +50,12 @@
|
|
|
50
50
|
"homepage": "https://github.com/mantaskaveckas/brizzle#readme",
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@clack/prompts": "^0.10.0",
|
|
53
|
-
"commander": "^14.0.2"
|
|
53
|
+
"commander": "^14.0.2",
|
|
54
|
+
"pluralize": "^8.0.0"
|
|
54
55
|
},
|
|
55
56
|
"devDependencies": {
|
|
56
57
|
"@types/node": "^20",
|
|
58
|
+
"@types/pluralize": "^0.0.33",
|
|
57
59
|
"tsup": "^8",
|
|
58
60
|
"typescript": "^5",
|
|
59
61
|
"vitest": "^4"
|