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