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.
- package/dist/index.js +153 -47
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
|
685
|
-
return ` ${field.name}: ${
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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.
|
|
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"
|