brizzle 0.2.4 → 0.2.6

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 (3) hide show
  1. package/README.md +4 -3
  2. package/dist/index.js +194 -83
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,9 +6,10 @@
6
6
 
7
7
  <p align="center">
8
8
  <a href="https://github.com/mantaskaveckas/brizzle/actions/workflows/ci.yml"><img src="https://github.com/mantaskaveckas/brizzle/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
9
- [![npm version](https://img.shields.io/npm/v/brizzle)](https://www.npmjs.com/package/brizzle)
10
- [![npm downloads](https://img.shields.io/npm/dm/brizzle)](https://www.npmjs.com/package/brizzle)
11
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
+ <a href="https://www.npmjs.com/package/brizzle"><img src="https://img.shields.io/npm/v/brizzle" alt="npm version"></a>
10
+ <a href="https://www.npmjs.com/package/brizzle"><img src="https://img.shields.io/npm/dm/brizzle" alt="npm downloads"></a>
11
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
12
+ </p>
12
13
 
13
14
  Rails-like generators for Next.js + Drizzle ORM projects. Generate models, server actions, CRUD pages, and API routes with a single command.
14
15
 
package/dist/index.js CHANGED
@@ -159,6 +159,9 @@ function toSnakeCase(str) {
159
159
  function toKebabCase(str) {
160
160
  return toSnakeCase(str).replace(/_/g, "-");
161
161
  }
162
+ function escapeString(str) {
163
+ return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
164
+ }
162
165
  function pluralize(str) {
163
166
  if (str.endsWith("y") && !/[aeiou]y$/.test(str)) {
164
167
  return str.slice(0, -1) + "ies";
@@ -200,6 +203,82 @@ function createModelContext(name) {
200
203
  }
201
204
 
202
205
  // src/lib/validation.ts
206
+ var SQL_RESERVED_WORDS = [
207
+ // SQL keywords
208
+ "select",
209
+ "from",
210
+ "where",
211
+ "insert",
212
+ "update",
213
+ "delete",
214
+ "drop",
215
+ "create",
216
+ "alter",
217
+ "index",
218
+ "table",
219
+ "column",
220
+ "database",
221
+ "schema",
222
+ "and",
223
+ "or",
224
+ "not",
225
+ "null",
226
+ "true",
227
+ "false",
228
+ "order",
229
+ "by",
230
+ "group",
231
+ "having",
232
+ "limit",
233
+ "offset",
234
+ "join",
235
+ "left",
236
+ "right",
237
+ "inner",
238
+ "outer",
239
+ "on",
240
+ "as",
241
+ "in",
242
+ "between",
243
+ "like",
244
+ "is",
245
+ "case",
246
+ "when",
247
+ "then",
248
+ "else",
249
+ "end",
250
+ "exists",
251
+ "distinct",
252
+ "all",
253
+ "any",
254
+ "union",
255
+ "intersect",
256
+ "except",
257
+ "primary",
258
+ "foreign",
259
+ "key",
260
+ "references",
261
+ "unique",
262
+ "default",
263
+ "check",
264
+ "constraint",
265
+ // Common type names that might conflict
266
+ "int",
267
+ "integer",
268
+ "float",
269
+ "double",
270
+ "decimal",
271
+ "numeric",
272
+ "boolean",
273
+ "bool",
274
+ "text",
275
+ "varchar",
276
+ "char",
277
+ "date",
278
+ "time",
279
+ "timestamp",
280
+ "datetime"
281
+ ];
203
282
  function validateModelName(name) {
204
283
  if (!name) {
205
284
  throw new Error("Model name is required");
@@ -232,6 +311,11 @@ function validateFieldDefinition(fieldDef) {
232
311
  `Invalid field name "${name}". Must be camelCase (start with lowercase letter).`
233
312
  );
234
313
  }
314
+ if (SQL_RESERVED_WORDS.includes(name.toLowerCase())) {
315
+ throw new Error(
316
+ `Field name "${name}" is a SQL reserved word. Consider renaming to "${name}Value" or "${name}Field".`
317
+ );
318
+ }
235
319
  if (type && !type.startsWith("references") && type !== "enum" && type !== "unique") {
236
320
  if (!VALID_FIELD_TYPES.includes(type)) {
237
321
  throw new Error(
@@ -567,7 +651,7 @@ function generateEnumDefinitions(fields, dialect) {
567
651
  }
568
652
  return enumFields.map((field) => {
569
653
  const enumName = `${field.name}Enum`;
570
- const values = field.enumValues.map((v) => `"${v}"`).join(", ");
654
+ const values = field.enumValues.map((v) => `"${escapeString(v)}"`).join(", ");
571
655
  return `
572
656
  export const ${enumName} = pgEnum("${toSnakeCase(field.name)}", [${values}]);`;
573
657
  }).join("\n");
@@ -629,10 +713,10 @@ function generateEnumField(field, columnName, dialect) {
629
713
  case "postgresql":
630
714
  return ` ${field.name}: ${field.name}Enum("${columnName}")${modifiers},`;
631
715
  case "mysql":
632
- const mysqlValues = values.map((v) => `"${v}"`).join(", ");
716
+ const mysqlValues = values.map((v) => `"${escapeString(v)}"`).join(", ");
633
717
  return ` ${field.name}: mysqlEnum("${columnName}", [${mysqlValues}])${modifiers},`;
634
718
  default:
635
- const sqliteValues = values.map((v) => `"${v}"`).join(", ");
719
+ const sqliteValues = values.map((v) => `"${escapeString(v)}"`).join(", ");
636
720
  return ` ${field.name}: text("${columnName}", { enum: [${sqliteValues}] })${modifiers},`;
637
721
  }
638
722
  }
@@ -682,12 +766,15 @@ export async function get${pascalName}(id: ${idType}) {
682
766
  .from(${camelPlural})
683
767
  .where(eq(${camelPlural}.id, id))
684
768
  .limit(1);
769
+
685
770
  return result[0] ?? null;
686
771
  }
687
772
 
688
773
  export async function create${pascalName}(data: Omit<New${pascalName}, "id" | "createdAt" | "updatedAt">) {
689
774
  const result = await db.insert(${camelPlural}).values(data).returning();
775
+
690
776
  revalidatePath("/${kebabPlural}");
777
+
691
778
  return result[0];
692
779
  }
693
780
 
@@ -700,12 +787,15 @@ export async function update${pascalName}(
700
787
  .set({ ...data, updatedAt: new Date() })
701
788
  .where(eq(${camelPlural}.id, id))
702
789
  .returning();
790
+
703
791
  revalidatePath("/${kebabPlural}");
792
+
704
793
  return result[0];
705
794
  }
706
795
 
707
796
  export async function delete${pascalName}(id: ${idType}) {
708
797
  await db.delete(${camelPlural}).where(eq(${camelPlural}.id, id));
798
+
709
799
  revalidatePath("/${kebabPlural}");
710
800
  }
711
801
  `;
@@ -821,9 +911,11 @@ import { create${pascalName} } from "../actions";
821
911
  export default function New${pascalName}Page() {
822
912
  async function handleCreate(formData: FormData) {
823
913
  "use server";
914
+
824
915
  await create${pascalName}({
825
916
  ${fields.map((f) => ` ${f.name}: ${formDataValue(f)},`).join("\n")}
826
917
  });
918
+
827
919
  redirect("/${kebabPlural}");
828
920
  }
829
921
 
@@ -856,9 +948,11 @@ ${fields.map((f) => generateFormField(f, camelName)).join("\n\n")}
856
948
  }
857
949
  function generateShowPage(pascalName, _pascalPlural, camelName, kebabPlural, fields, options = {}) {
858
950
  const idHandling = options.uuid ? `const ${camelName} = await get${pascalName}(id);` : `const numericId = Number(id);
951
+
859
952
  if (isNaN(numericId)) {
860
953
  notFound();
861
954
  }
955
+
862
956
  const ${camelName} = await get${pascalName}(numericId);`;
863
957
  return `import { notFound } from "next/navigation";
864
958
  import Link from "next/link";
@@ -915,9 +1009,11 @@ ${fields.map(
915
1009
  }
916
1010
  function generateEditPage(pascalName, camelName, kebabPlural, fields, options = {}) {
917
1011
  const idHandling = options.uuid ? `const ${camelName} = await get${pascalName}(id);` : `const numericId = Number(id);
1012
+
918
1013
  if (isNaN(numericId)) {
919
1014
  notFound();
920
1015
  }
1016
+
921
1017
  const ${camelName} = await get${pascalName}(numericId);`;
922
1018
  const updateId = options.uuid ? "id" : "numericId";
923
1019
  return `import { notFound, redirect } from "next/navigation";
@@ -938,9 +1034,11 @@ export default async function Edit${pascalName}Page({
938
1034
 
939
1035
  async function handleUpdate(formData: FormData) {
940
1036
  "use server";
1037
+
941
1038
  await update${pascalName}(${updateId}, {
942
1039
  ${fields.map((f) => ` ${f.name}: ${formDataValue(f)},`).join("\n")}
943
1040
  });
1041
+
944
1042
  redirect("/${kebabPlural}");
945
1043
  }
946
1044
 
@@ -971,17 +1069,20 @@ ${fields.map((f) => generateFormField(f, camelName, true)).join("\n\n")}
971
1069
  }
972
1070
  `;
973
1071
  }
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>
1072
+ function createFieldContext(field, camelName, withDefault) {
1073
+ return {
1074
+ field,
1075
+ label: toPascalCase(field.name),
1076
+ optionalLabel: field.nullable ? ` <span className="text-gray-400">(optional)</span>` : "",
1077
+ required: field.nullable ? "" : " required",
1078
+ defaultValue: withDefault ? ` defaultValue={${camelName}.${field.name}}` : ""
1079
+ };
1080
+ }
1081
+ function generateTextareaField(ctx) {
1082
+ const { field, label, optionalLabel, required, defaultValue } = ctx;
1083
+ const rows = field.type === "json" ? 6 : 4;
1084
+ const placeholder = field.type === "json" ? ` placeholder="{}"` : "";
1085
+ return ` <div>
985
1086
  <label htmlFor="${field.name}" className="block text-sm font-medium text-gray-700">
986
1087
  ${label}${optionalLabel}
987
1088
  </label>
@@ -989,13 +1090,14 @@ function generateFormField(field, camelName, withDefault = false) {
989
1090
  id="${field.name}"
990
1091
  name="${field.name}"
991
1092
  rows={${rows}}
992
- className="${inputClasses} resize-none"${defaultValue}${placeholder}${required}
1093
+ 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
1094
  />
994
1095
  </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">
1096
+ }
1097
+ function generateCheckboxField(ctx, camelName, withDefault) {
1098
+ const { field, label } = ctx;
1099
+ const defaultChecked = withDefault ? ` defaultChecked={${camelName}.${field.name}}` : "";
1100
+ return ` <div className="flex items-center gap-2">
999
1101
  <input
1000
1102
  type="checkbox"
1001
1103
  id="${field.name}"
@@ -1006,38 +1108,27 @@ function generateFormField(field, camelName, withDefault = false) {
1006
1108
  ${label}
1007
1109
  </label>
1008
1110
  </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>
1111
+ }
1112
+ function generateNumberField(ctx, step) {
1113
+ const { field, label, optionalLabel, required, defaultValue } = ctx;
1114
+ const stepAttr = step ? `
1115
+ step="${step}"` : "";
1116
+ return ` <div>
1026
1117
  <label htmlFor="${field.name}" className="block text-sm font-medium text-gray-700">
1027
1118
  ${label}${optionalLabel}
1028
1119
  </label>
1029
1120
  <input
1030
- type="number"
1031
- step="${step}"
1121
+ type="number"${stepAttr}
1032
1122
  id="${field.name}"
1033
1123
  name="${field.name}"
1034
- className="${inputClasses}"${defaultValue}${required}
1124
+ 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
1125
  />
1036
1126
  </div>`;
1037
- }
1038
- if (field.type === "date") {
1039
- const dateDefault = withDefault ? ` defaultValue={${camelName}.${field.name}?.toISOString().split("T")[0]}` : "";
1040
- return ` <div>
1127
+ }
1128
+ function generateDateField(ctx, camelName, withDefault) {
1129
+ const { field, label, optionalLabel, required } = ctx;
1130
+ const dateDefault = withDefault ? ` defaultValue={${camelName}.${field.name}?.toISOString().split("T")[0]}` : "";
1131
+ return ` <div>
1041
1132
  <label htmlFor="${field.name}" className="block text-sm font-medium text-gray-700">
1042
1133
  ${label}${optionalLabel}
1043
1134
  </label>
@@ -1045,13 +1136,14 @@ function generateFormField(field, camelName, withDefault = false) {
1045
1136
  type="date"
1046
1137
  id="${field.name}"
1047
1138
  name="${field.name}"
1048
- className="${inputClasses}"${dateDefault}${required}
1139
+ 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
1140
  />
1050
1141
  </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>
1142
+ }
1143
+ function generateDatetimeField(ctx, camelName, withDefault) {
1144
+ const { field, label, optionalLabel, required } = ctx;
1145
+ const dateDefault = withDefault ? ` defaultValue={${camelName}.${field.name}?.toISOString().slice(0, 16)}` : "";
1146
+ return ` <div>
1055
1147
  <label htmlFor="${field.name}" className="block text-sm font-medium text-gray-700">
1056
1148
  ${label}${optionalLabel}
1057
1149
  </label>
@@ -1059,25 +1151,28 @@ function generateFormField(field, camelName, withDefault = false) {
1059
1151
  type="datetime-local"
1060
1152
  id="${field.name}"
1061
1153
  name="${field.name}"
1062
- className="${inputClasses}"${dateDefault}${required}
1154
+ 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
1155
  />
1064
1156
  </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>
1157
+ }
1158
+ function generateSelectField(ctx) {
1159
+ const { field, label, optionalLabel, required, defaultValue } = ctx;
1160
+ const options = field.enumValues.map((v) => ` <option value="${escapeString(v)}">${toPascalCase(v)}</option>`).join("\n");
1161
+ return ` <div>
1069
1162
  <label htmlFor="${field.name}" className="block text-sm font-medium text-gray-700">
1070
1163
  ${label}${optionalLabel}
1071
1164
  </label>
1072
1165
  <select
1073
1166
  id="${field.name}"
1074
1167
  name="${field.name}"
1075
- className="${selectClasses}"${defaultValue}${required}
1168
+ 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
1169
  >
1077
1170
  ${options}
1078
1171
  </select>
1079
1172
  </div>`;
1080
- }
1173
+ }
1174
+ function generateTextField(ctx) {
1175
+ const { field, label, optionalLabel, required, defaultValue } = ctx;
1081
1176
  return ` <div>
1082
1177
  <label htmlFor="${field.name}" className="block text-sm font-medium text-gray-700">
1083
1178
  ${label}${optionalLabel}
@@ -1086,14 +1181,46 @@ ${options}
1086
1181
  type="text"
1087
1182
  id="${field.name}"
1088
1183
  name="${field.name}"
1089
- className="${inputClasses}"${defaultValue}${required}
1184
+ 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
1185
  />
1091
1186
  </div>`;
1092
1187
  }
1188
+ function generateFormField(field, camelName, withDefault = false) {
1189
+ const ctx = createFieldContext(field, camelName, withDefault);
1190
+ switch (field.type) {
1191
+ case "text":
1192
+ case "json":
1193
+ return generateTextareaField(ctx);
1194
+ case "boolean":
1195
+ case "bool":
1196
+ return generateCheckboxField(ctx, camelName, withDefault);
1197
+ case "integer":
1198
+ case "int":
1199
+ case "bigint":
1200
+ return generateNumberField(ctx);
1201
+ case "float":
1202
+ return generateNumberField(ctx, "any");
1203
+ case "decimal":
1204
+ return generateNumberField(ctx, "0.01");
1205
+ case "date":
1206
+ return generateDateField(ctx, camelName, withDefault);
1207
+ case "datetime":
1208
+ case "timestamp":
1209
+ return generateDatetimeField(ctx, camelName, withDefault);
1210
+ default:
1211
+ if (field.isEnum && field.enumValues) {
1212
+ return generateSelectField(ctx);
1213
+ }
1214
+ return generateTextField(ctx);
1215
+ }
1216
+ }
1093
1217
  function formDataValue(field) {
1094
1218
  const getValue = `formData.get("${field.name}")`;
1095
1219
  const asString = `${getValue} as string`;
1096
1220
  if (field.nullable) {
1221
+ if (field.type === "boolean" || field.type === "bool") {
1222
+ return `${getValue} === "on" ? true : null`;
1223
+ }
1097
1224
  if (field.type === "integer" || field.type === "int" || field.type === "bigint") {
1098
1225
  return `${getValue} ? parseInt(${asString}) : null`;
1099
1226
  }
@@ -1120,6 +1247,9 @@ function formDataValue(field) {
1120
1247
  if (field.type === "float") {
1121
1248
  return `parseFloat(${asString})`;
1122
1249
  }
1250
+ if (field.type === "decimal") {
1251
+ return asString;
1252
+ }
1123
1253
  if (field.type === "datetime" || field.type === "timestamp" || field.type === "date") {
1124
1254
  return `new Date(${asString})`;
1125
1255
  }
@@ -1318,47 +1448,28 @@ export async function DELETE(request: Request, { params }: Params) {
1318
1448
 
1319
1449
  // src/generators/destroy.ts
1320
1450
  import * as path8 from "path";
1321
- function destroyScaffold(name, options = {}) {
1451
+ function destroy(name, type, buildPath, options = {}) {
1322
1452
  validateModelName(name);
1323
1453
  const ctx = createModelContext(name);
1324
1454
  const config = detectProjectConfig();
1325
1455
  const prefix = options.dryRun ? "[dry-run] " : "";
1326
1456
  log.info(`
1327
- ${prefix}Destroying scaffold ${ctx.pascalName}...
1457
+ ${prefix}Destroying ${type} ${ctx.pascalName}...
1328
1458
  `);
1329
- const basePath = path8.join(getAppPath(), ctx.kebabPlural);
1459
+ const basePath = buildPath(ctx);
1330
1460
  deleteDirectory(basePath, options);
1331
1461
  log.info(`
1332
1462
  Note: Schema in ${config.dbPath}/schema.ts was not modified.`);
1333
1463
  log.info(` Remove the table definition manually if needed.`);
1334
1464
  }
1465
+ function destroyScaffold(name, options = {}) {
1466
+ destroy(name, "scaffold", (ctx) => path8.join(getAppPath(), ctx.kebabPlural), options);
1467
+ }
1335
1468
  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.`);
1469
+ destroy(name, "resource", (ctx) => path8.join(getAppPath(), ctx.kebabPlural), options);
1348
1470
  }
1349
1471
  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.`);
1472
+ destroy(name, "API", (ctx) => path8.join(getAppPath(), "api", ctx.kebabPlural), options);
1362
1473
  }
1363
1474
 
1364
1475
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brizzle",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Rails-like generators for Next.js + Drizzle ORM projects",
5
5
  "type": "module",
6
6
  "bin": {