brizzle 0.2.6 → 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 +130 -46
  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
  }
@@ -163,25 +164,10 @@ function escapeString(str) {
163
164
  return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
164
165
  }
165
166
  function pluralize(str) {
166
- if (str.endsWith("y") && !/[aeiou]y$/.test(str)) {
167
- return str.slice(0, -1) + "ies";
168
- }
169
- if (str.endsWith("s") || str.endsWith("x") || str.endsWith("ch") || str.endsWith("sh")) {
170
- return str + "es";
171
- }
172
- return str + "s";
167
+ return pluralizeLib.plural(str);
173
168
  }
174
169
  function singularize(str) {
175
- if (str.endsWith("ies")) {
176
- return str.slice(0, -3) + "y";
177
- }
178
- if (str.endsWith("es") && (str.endsWith("xes") || str.endsWith("ches") || str.endsWith("shes") || str.endsWith("sses"))) {
179
- return str.slice(0, -2);
180
- }
181
- if (str.endsWith("s") && !str.endsWith("ss")) {
182
- return str.slice(0, -1);
183
- }
184
- return str;
170
+ return pluralizeLib.singular(str);
185
171
  }
186
172
  function createModelContext(name) {
187
173
  const singularName = singularize(name);
@@ -330,6 +316,25 @@ function validateFieldDefinition(fieldDef) {
330
316
  `Enum field "${name}" requires values. Example: ${name}:enum:draft,published,archived`
331
317
  );
332
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
+ }
333
338
  }
334
339
  }
335
340
 
@@ -542,7 +547,7 @@ function getRequiredImports(fields, dialect, options = {}) {
542
547
  const baseType = drizzleTypeDef.split("(")[0];
543
548
  types.add(baseType);
544
549
  }
545
- if (dialect !== "mysql") {
550
+ if (dialect === "sqlite" && hasEnums) {
546
551
  types.add("text");
547
552
  }
548
553
  return Array.from(types);
@@ -596,6 +601,9 @@ function fileExists(filePath) {
596
601
  function readFile(filePath) {
597
602
  return fs2.readFileSync(filePath, "utf-8");
598
603
  }
604
+ function escapeRegExp(str) {
605
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
606
+ }
599
607
  function modelExistsInSchema(tableName) {
600
608
  const schemaPath = path3.join(getDbPath(), "schema.ts");
601
609
  if (!fs2.existsSync(schemaPath)) {
@@ -603,10 +611,46 @@ function modelExistsInSchema(tableName) {
603
611
  }
604
612
  const content = fs2.readFileSync(schemaPath, "utf-8");
605
613
  const pattern = new RegExp(
606
- `(?:sqliteTable|pgTable|mysqlTable)\\s*\\(\\s*["']${tableName}["']`
614
+ `(?:sqliteTable|pgTable|mysqlTable)\\s*\\(\\s*["']${escapeRegExp(tableName)}["']`
607
615
  );
608
616
  return pattern.test(content);
609
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
+ }
610
654
 
611
655
  // src/generators/model.ts
612
656
  function generateModel(name, fieldArgs, options = {}) {
@@ -620,15 +664,13 @@ function generateModel(name, fieldArgs, options = {}) {
620
664
  );
621
665
  }
622
666
  const schemaPath = path4.join(getDbPath(), "schema.ts");
623
- if (fileExists(schemaPath) && !modelExistsInSchema(ctx.tableName)) {
624
- appendToSchema(schemaPath, ctx.camelPlural, ctx.tableName, fields, dialect, options);
625
- } else if (!fileExists(schemaPath)) {
667
+ if (!fileExists(schemaPath)) {
626
668
  const schemaContent = generateSchemaContent(ctx.camelPlural, ctx.tableName, fields, dialect, options);
627
669
  writeFile(schemaPath, schemaContent, options);
670
+ } else if (modelExistsInSchema(ctx.tableName)) {
671
+ replaceInSchema(schemaPath, ctx.camelPlural, ctx.tableName, fields, dialect, options);
628
672
  } else {
629
- throw new Error(
630
- `Cannot regenerate model "${ctx.pascalName}" - manual removal from schema required.`
631
- );
673
+ appendToSchema(schemaPath, ctx.camelPlural, ctx.tableName, fields, dialect, options);
632
674
  }
633
675
  }
634
676
  function generateSchemaContent(modelName, tableName, fields, dialect, options = {}) {
@@ -660,7 +702,7 @@ function generateTableDefinition(modelName, tableName, fields, dialect, options
660
702
  const tableFunction = getTableFunction(dialect);
661
703
  const idColumn = getIdColumn(dialect, options.uuid);
662
704
  const timestampColumns = getTimestampColumns(dialect, options.noTimestamps);
663
- const fieldDefinitions = generateFieldDefinitions(fields, dialect);
705
+ const fieldDefinitions = generateFieldDefinitions(fields, dialect, options);
664
706
  const lines = [` ${idColumn},`];
665
707
  if (fieldDefinitions) {
666
708
  lines.push(fieldDefinitions);
@@ -672,7 +714,7 @@ function generateTableDefinition(modelName, tableName, fields, dialect, options
672
714
  ${lines.join("\n")}
673
715
  });`;
674
716
  }
675
- function generateFieldDefinitions(fields, dialect) {
717
+ function generateFieldDefinitions(fields, dialect, options = {}) {
676
718
  return fields.map((field) => {
677
719
  const columnName = toSnakeCase(field.name);
678
720
  const modifiers = getFieldModifiers(field);
@@ -681,8 +723,8 @@ function generateFieldDefinitions(fields, dialect) {
681
723
  }
682
724
  const drizzleTypeDef = drizzleType(field, dialect);
683
725
  if (field.isReference && field.referenceTo) {
684
- const intType = dialect === "mysql" ? "int" : "integer";
685
- 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},`;
686
728
  }
687
729
  if (dialect === "mysql" && drizzleTypeDef === "varchar") {
688
730
  const length = field.type === "uuid" ? 36 : 255;
@@ -706,6 +748,24 @@ function getFieldModifiers(field) {
706
748
  }
707
749
  return modifiers.join("");
708
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
+ }
709
769
  function generateEnumField(field, columnName, dialect) {
710
770
  const values = field.enumValues;
711
771
  const modifiers = getFieldModifiers(field);
@@ -727,6 +787,14 @@ function appendToSchema(schemaPath, modelName, tableName, fields, dialect, optio
727
787
  const newContent = existingContent + enumDefinitions + "\n" + tableDefinition + "\n";
728
788
  writeFile(schemaPath, newContent, { force: true, dryRun: options.dryRun });
729
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
+ }
730
798
 
731
799
  // src/generators/actions.ts
732
800
  import * as path5 from "path";
@@ -996,11 +1064,11 @@ ${fields.map(
996
1064
  <dt className="text-sm text-gray-500">${toPascalCase(f.name)}</dt>
997
1065
  <dd className="mt-1 text-gray-900">{${camelName}.${f.name}}</dd>
998
1066
  </div>`
999
- ).join("\n")}
1067
+ ).join("\n")}${options.noTimestamps ? "" : `
1000
1068
  <div className="py-3">
1001
1069
  <dt className="text-sm text-gray-500">Created At</dt>
1002
1070
  <dd className="mt-1 text-gray-900">{${camelName}.createdAt.toLocaleString()}</dd>
1003
- </div>
1071
+ </div>`}
1004
1072
  </dl>
1005
1073
  </div>
1006
1074
  );
@@ -1448,28 +1516,44 @@ export async function DELETE(request: Request, { params }: Params) {
1448
1516
 
1449
1517
  // src/generators/destroy.ts
1450
1518
  import * as path8 from "path";
1451
- function destroy(name, type, buildPath, options = {}) {
1519
+ import { confirm, isCancel } from "@clack/prompts";
1520
+ async function destroy(name, type, buildPath, options = {}) {
1452
1521
  validateModelName(name);
1453
1522
  const ctx = createModelContext(name);
1454
- const config = detectProjectConfig();
1455
1523
  const prefix = options.dryRun ? "[dry-run] " : "";
1456
1524
  log.info(`
1457
1525
  ${prefix}Destroying ${type} ${ctx.pascalName}...
1458
1526
  `);
1459
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
+ }
1460
1537
  deleteDirectory(basePath, options);
1461
- log.info(`
1462
- Note: Schema in ${config.dbPath}/schema.ts was not modified.`);
1463
- log.info(` Remove the table definition manually if needed.`);
1538
+ removeFromSchema(ctx.tableName, options);
1539
+ }
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 });
1464
1548
  }
1465
- function destroyScaffold(name, options = {}) {
1466
- destroy(name, "scaffold", (ctx) => path8.join(getAppPath(), ctx.kebabPlural), options);
1549
+ async function destroyScaffold(name, options = {}) {
1550
+ return destroy(name, "scaffold", (ctx) => path8.join(getAppPath(), ctx.kebabPlural), options);
1467
1551
  }
1468
- function destroyResource(name, options = {}) {
1469
- destroy(name, "resource", (ctx) => path8.join(getAppPath(), ctx.kebabPlural), options);
1552
+ async function destroyResource(name, options = {}) {
1553
+ return destroy(name, "resource", (ctx) => path8.join(getAppPath(), ctx.kebabPlural), options);
1470
1554
  }
1471
- function destroyApi(name, options = {}) {
1472
- destroy(name, "API", (ctx) => path8.join(getAppPath(), "api", ctx.kebabPlural), options);
1555
+ async function destroyApi(name, options = {}) {
1556
+ return destroy(name, "API", (ctx) => path8.join(getAppPath(), "api", ctx.kebabPlural), options);
1473
1557
  }
1474
1558
 
1475
1559
  // src/index.ts
@@ -1571,17 +1655,17 @@ program.command("destroy <type> <name>").alias("d").description(
1571
1655
  Examples:
1572
1656
  brizzle destroy scaffold post
1573
1657
  brizzle d api product --dry-run`
1574
- ).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) => {
1575
1659
  try {
1576
1660
  switch (type) {
1577
1661
  case "scaffold":
1578
- destroyScaffold(name, opts);
1662
+ await destroyScaffold(name, opts);
1579
1663
  break;
1580
1664
  case "resource":
1581
- destroyResource(name, opts);
1665
+ await destroyResource(name, opts);
1582
1666
  break;
1583
1667
  case "api":
1584
- destroyApi(name, opts);
1668
+ await destroyApi(name, opts);
1585
1669
  break;
1586
1670
  default:
1587
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.6",
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"