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.
Files changed (2) hide show
  1. package/dist/index.js +319 -124
  2. 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
- if (str.endsWith("y") && !/[aeiou]y$/.test(str)) {
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
- if (str.endsWith("ies")) {
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 !== "mysql") {
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) && !modelExistsInSchema(ctx.tableName)) {
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
- throw new Error(
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 intType = dialect === "mysql" ? "int" : "integer";
601
- return ` ${field.name}: ${intType}("${columnName}").references(() => ${toCamelCase(pluralize(field.referenceTo))}.id)${modifiers},`;
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 generateFormField(field, camelName, withDefault = false) {
975
- const label = toPascalCase(field.name);
976
- const defaultValue = withDefault ? ` defaultValue={${camelName}.${field.name}}` : "";
977
- 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";
978
- 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";
979
- const optionalLabel = field.nullable ? ` <span className="text-gray-400">(optional)</span>` : "";
980
- const required = field.nullable ? "" : " required";
981
- if (field.type === "text" || field.type === "json") {
982
- const rows = field.type === "json" ? 6 : 4;
983
- const placeholder = field.type === "json" ? ` placeholder="{}"` : "";
984
- return ` <div>
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="${inputClasses} resize-none"${defaultValue}${placeholder}${required}
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
- if (field.type === "boolean" || field.type === "bool") {
997
- const defaultChecked = withDefault ? ` defaultChecked={${camelName}.${field.name}}` : "";
998
- return ` <div className="flex items-center gap-2">
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
- if (field.type === "integer" || field.type === "int" || field.type === "bigint") {
1011
- return ` <div>
1012
- <label htmlFor="${field.name}" className="block text-sm font-medium text-gray-700">
1013
- ${label}${optionalLabel}
1014
- </label>
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="${inputClasses}"${defaultValue}${required}
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
- if (field.type === "date") {
1039
- const dateDefault = withDefault ? ` defaultValue={${camelName}.${field.name}?.toISOString().split("T")[0]}` : "";
1040
- return ` <div>
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="${inputClasses}"${dateDefault}${required}
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
- if (field.type === "datetime" || field.type === "timestamp") {
1053
- const dateDefault = withDefault ? ` defaultValue={${camelName}.${field.name}?.toISOString().slice(0, 16)}` : "";
1054
- return ` <div>
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="${inputClasses}"${dateDefault}${required}
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
- if (field.isEnum && field.enumValues) {
1067
- const options = field.enumValues.map((v) => ` <option value="${v}">${toPascalCase(v)}</option>`).join("\n");
1068
- return ` <div>
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="${selectClasses}"${defaultValue}${required}
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="${inputClasses}"${defaultValue}${required}
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
- function destroyScaffold(name, options = {}) {
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 scaffold ${ctx.pascalName}...
1525
+ ${prefix}Destroying ${type} ${ctx.pascalName}...
1328
1526
  `);
1329
- const basePath = path8.join(getAppPath(), ctx.kebabPlural);
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
- log.info(`
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 destroyResource(name, options = {}) {
1336
- validateModelName(name);
1337
- const ctx = createModelContext(name);
1338
- const config = detectProjectConfig();
1339
- const prefix = options.dryRun ? "[dry-run] " : "";
1340
- log.info(`
1341
- ${prefix}Destroying resource ${ctx.pascalName}...
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 destroyApi(name, options = {}) {
1350
- validateModelName(name);
1351
- const ctx = createModelContext(name);
1352
- const config = detectProjectConfig();
1353
- const prefix = options.dryRun ? "[dry-run] " : "";
1354
- log.info(`
1355
- ${prefix}Destroying API ${ctx.pascalName}...
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.5",
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"