brizzle 0.2.6 → 0.2.8

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 +153 -47
  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
 
@@ -501,6 +506,24 @@ function getTimestampColumns(dialect, noTimestamps = false) {
501
506
  .$defaultFn(() => new Date())`;
502
507
  }
503
508
  }
509
+ function extractImportsFromSchema(content) {
510
+ const importMatch = content.match(/import\s*\{([^}]+)\}\s*from\s*["']drizzle-orm\/[^"']+["']/);
511
+ if (!importMatch) {
512
+ return [];
513
+ }
514
+ return importMatch[1].split(",").map((s) => s.trim()).filter((s) => s.length > 0);
515
+ }
516
+ function updateSchemaImports(content, newImports, dialect) {
517
+ const existingImports = extractImportsFromSchema(content);
518
+ const mergedImports = Array.from(/* @__PURE__ */ new Set([...existingImports, ...newImports]));
519
+ const drizzleImport = getDrizzleImport(dialect);
520
+ const newImportLine = `import { ${mergedImports.join(", ")} } from "${drizzleImport}";`;
521
+ const importRegex = /import\s*\{[^}]+\}\s*from\s*["']drizzle-orm\/[^"']+["'];?/;
522
+ if (importRegex.test(content)) {
523
+ return content.replace(importRegex, newImportLine);
524
+ }
525
+ return newImportLine + "\n" + content;
526
+ }
504
527
  function getRequiredImports(fields, dialect, options = {}) {
505
528
  const types = /* @__PURE__ */ new Set();
506
529
  types.add(getTableFunction(dialect));
@@ -542,7 +565,7 @@ function getRequiredImports(fields, dialect, options = {}) {
542
565
  const baseType = drizzleTypeDef.split("(")[0];
543
566
  types.add(baseType);
544
567
  }
545
- if (dialect !== "mysql") {
568
+ if (dialect === "sqlite" && hasEnums) {
546
569
  types.add("text");
547
570
  }
548
571
  return Array.from(types);
@@ -596,6 +619,9 @@ function fileExists(filePath) {
596
619
  function readFile(filePath) {
597
620
  return fs2.readFileSync(filePath, "utf-8");
598
621
  }
622
+ function escapeRegExp(str) {
623
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
624
+ }
599
625
  function modelExistsInSchema(tableName) {
600
626
  const schemaPath = path3.join(getDbPath(), "schema.ts");
601
627
  if (!fs2.existsSync(schemaPath)) {
@@ -603,10 +629,46 @@ function modelExistsInSchema(tableName) {
603
629
  }
604
630
  const content = fs2.readFileSync(schemaPath, "utf-8");
605
631
  const pattern = new RegExp(
606
- `(?:sqliteTable|pgTable|mysqlTable)\\s*\\(\\s*["']${tableName}["']`
632
+ `(?:sqliteTable|pgTable|mysqlTable)\\s*\\(\\s*["']${escapeRegExp(tableName)}["']`
607
633
  );
608
634
  return pattern.test(content);
609
635
  }
636
+ function removeModelFromSchemaContent(content, tableName) {
637
+ const lines = content.split("\n");
638
+ const tablePattern = new RegExp(
639
+ `^export\\s+const\\s+\\w+\\s*=\\s*(?:sqliteTable|pgTable|mysqlTable)\\s*\\(\\s*["']${escapeRegExp(tableName)}["']`
640
+ );
641
+ let startIdx = -1;
642
+ let endIdx = -1;
643
+ let braceCount = 0;
644
+ let foundOpenBrace = false;
645
+ for (let i = 0; i < lines.length; i++) {
646
+ if (startIdx === -1) {
647
+ if (tablePattern.test(lines[i])) {
648
+ startIdx = i;
649
+ } else {
650
+ continue;
651
+ }
652
+ }
653
+ for (const char of lines[i]) {
654
+ if (char === "{") {
655
+ braceCount++;
656
+ foundOpenBrace = true;
657
+ } else if (char === "}") {
658
+ braceCount--;
659
+ }
660
+ }
661
+ if (foundOpenBrace && braceCount === 0) {
662
+ endIdx = i;
663
+ break;
664
+ }
665
+ }
666
+ if (startIdx === -1 || endIdx === -1) {
667
+ return content;
668
+ }
669
+ lines.splice(startIdx, endIdx - startIdx + 1);
670
+ return lines.join("\n").replace(/\n{3,}/g, "\n\n");
671
+ }
610
672
 
611
673
  // src/generators/model.ts
612
674
  function generateModel(name, fieldArgs, options = {}) {
@@ -620,15 +682,13 @@ function generateModel(name, fieldArgs, options = {}) {
620
682
  );
621
683
  }
622
684
  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)) {
685
+ if (!fileExists(schemaPath)) {
626
686
  const schemaContent = generateSchemaContent(ctx.camelPlural, ctx.tableName, fields, dialect, options);
627
687
  writeFile(schemaPath, schemaContent, options);
688
+ } else if (modelExistsInSchema(ctx.tableName)) {
689
+ replaceInSchema(schemaPath, ctx.camelPlural, ctx.tableName, fields, dialect, options);
628
690
  } else {
629
- throw new Error(
630
- `Cannot regenerate model "${ctx.pascalName}" - manual removal from schema required.`
631
- );
691
+ appendToSchema(schemaPath, ctx.camelPlural, ctx.tableName, fields, dialect, options);
632
692
  }
633
693
  }
634
694
  function generateSchemaContent(modelName, tableName, fields, dialect, options = {}) {
@@ -660,7 +720,7 @@ function generateTableDefinition(modelName, tableName, fields, dialect, options
660
720
  const tableFunction = getTableFunction(dialect);
661
721
  const idColumn = getIdColumn(dialect, options.uuid);
662
722
  const timestampColumns = getTimestampColumns(dialect, options.noTimestamps);
663
- const fieldDefinitions = generateFieldDefinitions(fields, dialect);
723
+ const fieldDefinitions = generateFieldDefinitions(fields, dialect, options);
664
724
  const lines = [` ${idColumn},`];
665
725
  if (fieldDefinitions) {
666
726
  lines.push(fieldDefinitions);
@@ -672,7 +732,7 @@ function generateTableDefinition(modelName, tableName, fields, dialect, options
672
732
  ${lines.join("\n")}
673
733
  });`;
674
734
  }
675
- function generateFieldDefinitions(fields, dialect) {
735
+ function generateFieldDefinitions(fields, dialect, options = {}) {
676
736
  return fields.map((field) => {
677
737
  const columnName = toSnakeCase(field.name);
678
738
  const modifiers = getFieldModifiers(field);
@@ -681,8 +741,8 @@ function generateFieldDefinitions(fields, dialect) {
681
741
  }
682
742
  const drizzleTypeDef = drizzleType(field, dialect);
683
743
  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},`;
744
+ const refTarget = `${toCamelCase(pluralize(field.referenceTo))}.id`;
745
+ return ` ${field.name}: ${getReferenceType(dialect, columnName, options.uuid)}.references(() => ${refTarget})${modifiers},`;
686
746
  }
687
747
  if (dialect === "mysql" && drizzleTypeDef === "varchar") {
688
748
  const length = field.type === "uuid" ? 36 : 255;
@@ -706,6 +766,24 @@ function getFieldModifiers(field) {
706
766
  }
707
767
  return modifiers.join("");
708
768
  }
769
+ function getReferenceType(dialect, columnName, useUuid = false) {
770
+ if (useUuid) {
771
+ switch (dialect) {
772
+ case "postgresql":
773
+ return `uuid("${columnName}")`;
774
+ case "mysql":
775
+ return `varchar("${columnName}", { length: 36 })`;
776
+ default:
777
+ return `text("${columnName}")`;
778
+ }
779
+ }
780
+ switch (dialect) {
781
+ case "mysql":
782
+ return `int("${columnName}")`;
783
+ default:
784
+ return `integer("${columnName}")`;
785
+ }
786
+ }
709
787
  function generateEnumField(field, columnName, dialect) {
710
788
  const values = field.enumValues;
711
789
  const modifiers = getFieldModifiers(field);
@@ -722,9 +800,21 @@ function generateEnumField(field, columnName, dialect) {
722
800
  }
723
801
  function appendToSchema(schemaPath, modelName, tableName, fields, dialect, options) {
724
802
  const existingContent = readFile(schemaPath);
803
+ const newImports = getRequiredImports(fields, dialect, options);
804
+ const updatedContent = updateSchemaImports(existingContent, newImports, dialect);
725
805
  const enumDefinitions = generateEnumDefinitions(fields, dialect);
726
806
  const tableDefinition = generateTableDefinition(modelName, tableName, fields, dialect, options);
727
- const newContent = existingContent + enumDefinitions + "\n" + tableDefinition + "\n";
807
+ const newContent = updatedContent + enumDefinitions + "\n" + tableDefinition + "\n";
808
+ writeFile(schemaPath, newContent, { force: true, dryRun: options.dryRun });
809
+ }
810
+ function replaceInSchema(schemaPath, modelName, tableName, fields, dialect, options) {
811
+ const existingContent = readFile(schemaPath);
812
+ const cleanedContent = removeModelFromSchemaContent(existingContent, tableName);
813
+ const newImports = getRequiredImports(fields, dialect, options);
814
+ const updatedContent = updateSchemaImports(cleanedContent, newImports, dialect);
815
+ const enumDefinitions = generateEnumDefinitions(fields, dialect);
816
+ const tableDefinition = generateTableDefinition(modelName, tableName, fields, dialect, options);
817
+ const newContent = updatedContent.trimEnd() + "\n" + enumDefinitions + "\n" + tableDefinition + "\n";
728
818
  writeFile(schemaPath, newContent, { force: true, dryRun: options.dryRun });
729
819
  }
730
820
 
@@ -996,11 +1086,11 @@ ${fields.map(
996
1086
  <dt className="text-sm text-gray-500">${toPascalCase(f.name)}</dt>
997
1087
  <dd className="mt-1 text-gray-900">{${camelName}.${f.name}}</dd>
998
1088
  </div>`
999
- ).join("\n")}
1089
+ ).join("\n")}${options.noTimestamps ? "" : `
1000
1090
  <div className="py-3">
1001
1091
  <dt className="text-sm text-gray-500">Created At</dt>
1002
1092
  <dd className="mt-1 text-gray-900">{${camelName}.createdAt.toLocaleString()}</dd>
1003
- </div>
1093
+ </div>`}
1004
1094
  </dl>
1005
1095
  </div>
1006
1096
  );
@@ -1448,28 +1538,44 @@ export async function DELETE(request: Request, { params }: Params) {
1448
1538
 
1449
1539
  // src/generators/destroy.ts
1450
1540
  import * as path8 from "path";
1451
- function destroy(name, type, buildPath, options = {}) {
1541
+ import { confirm, isCancel } from "@clack/prompts";
1542
+ async function destroy(name, type, buildPath, options = {}) {
1452
1543
  validateModelName(name);
1453
1544
  const ctx = createModelContext(name);
1454
- const config = detectProjectConfig();
1455
1545
  const prefix = options.dryRun ? "[dry-run] " : "";
1456
1546
  log.info(`
1457
1547
  ${prefix}Destroying ${type} ${ctx.pascalName}...
1458
1548
  `);
1459
1549
  const basePath = buildPath(ctx);
1550
+ if (!options.dryRun && !options.force && fileExists(basePath)) {
1551
+ const confirmed = await confirm({
1552
+ message: `Delete ${basePath}?`
1553
+ });
1554
+ if (isCancel(confirmed) || !confirmed) {
1555
+ log.info("Aborted.");
1556
+ return;
1557
+ }
1558
+ }
1460
1559
  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.`);
1560
+ removeFromSchema(ctx.tableName, options);
1561
+ }
1562
+ function removeFromSchema(tableName, options) {
1563
+ if (!modelExistsInSchema(tableName)) {
1564
+ return;
1565
+ }
1566
+ const schemaPath = path8.join(getDbPath(), "schema.ts");
1567
+ const content = readFile(schemaPath);
1568
+ const cleaned = removeModelFromSchemaContent(content, tableName);
1569
+ writeFile(schemaPath, cleaned, { force: true, dryRun: options.dryRun });
1464
1570
  }
1465
- function destroyScaffold(name, options = {}) {
1466
- destroy(name, "scaffold", (ctx) => path8.join(getAppPath(), ctx.kebabPlural), options);
1571
+ async function destroyScaffold(name, options = {}) {
1572
+ return destroy(name, "scaffold", (ctx) => path8.join(getAppPath(), ctx.kebabPlural), options);
1467
1573
  }
1468
- function destroyResource(name, options = {}) {
1469
- destroy(name, "resource", (ctx) => path8.join(getAppPath(), ctx.kebabPlural), options);
1574
+ async function destroyResource(name, options = {}) {
1575
+ return destroy(name, "resource", (ctx) => path8.join(getAppPath(), ctx.kebabPlural), options);
1470
1576
  }
1471
- function destroyApi(name, options = {}) {
1472
- destroy(name, "API", (ctx) => path8.join(getAppPath(), "api", ctx.kebabPlural), options);
1577
+ async function destroyApi(name, options = {}) {
1578
+ return destroy(name, "API", (ctx) => path8.join(getAppPath(), "api", ctx.kebabPlural), options);
1473
1579
  }
1474
1580
 
1475
1581
  // src/index.ts
@@ -1571,17 +1677,17 @@ program.command("destroy <type> <name>").alias("d").description(
1571
1677
  Examples:
1572
1678
  brizzle destroy scaffold post
1573
1679
  brizzle d api product --dry-run`
1574
- ).option("-n, --dry-run", "Preview changes without deleting files").action((type, name, opts) => {
1680
+ ).option("-f, --force", "Skip confirmation prompt").option("-n, --dry-run", "Preview changes without deleting files").action(async (type, name, opts) => {
1575
1681
  try {
1576
1682
  switch (type) {
1577
1683
  case "scaffold":
1578
- destroyScaffold(name, opts);
1684
+ await destroyScaffold(name, opts);
1579
1685
  break;
1580
1686
  case "resource":
1581
- destroyResource(name, opts);
1687
+ await destroyResource(name, opts);
1582
1688
  break;
1583
1689
  case "api":
1584
- destroyApi(name, opts);
1690
+ await destroyApi(name, opts);
1585
1691
  break;
1586
1692
  default:
1587
1693
  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.8",
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"