@drzl/cli 4.32.0 → 4.35.0
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/{chunk-BDIJ6WMG.js → chunk-S5JWTWWU.js} +15 -1
- package/dist/chunk-S5JWTWWU.js.map +1 -0
- package/dist/cli.cjs +257 -5
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +248 -6
- package/dist/cli.js.map +1 -1
- package/dist/config.cjs +16 -0
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +33 -3
- package/dist/config.d.ts +33 -3
- package/dist/config.js +5 -1
- package/dist/drzl.config.schema.json +7 -0
- package/package.json +3 -1
- package/dist/chunk-BDIJ6WMG.js.map +0 -1
package/dist/cli.cjs
CHANGED
|
@@ -515,6 +515,8 @@ var GeneratorKindSchema = import_zod.z.enum([
|
|
|
515
515
|
"next",
|
|
516
516
|
"tanstack-start",
|
|
517
517
|
"ts-rest",
|
|
518
|
+
"seed",
|
|
519
|
+
"fast-check",
|
|
518
520
|
"service",
|
|
519
521
|
"zod",
|
|
520
522
|
"valibot",
|
|
@@ -559,6 +561,8 @@ var GeneratorSchema = import_zod.z.object({
|
|
|
559
561
|
contractName: import_zod.z.string().optional(),
|
|
560
562
|
/** The identifier the assembled Elysia app is exported as. `elysia` only, defaults to `'app'`. */
|
|
561
563
|
appName: import_zod.z.string().optional(),
|
|
564
|
+
/** How many rows each generated seed function returns by default. `seed` only, defaults to 10. */
|
|
565
|
+
count: import_zod.z.number().int().positive().optional(),
|
|
562
566
|
/**
|
|
563
567
|
* Prefixed to every table's mount point. `elysia` only.
|
|
564
568
|
*
|
|
@@ -1028,6 +1032,12 @@ function tsRestOutDir(g, cfg) {
|
|
|
1028
1032
|
function elysiaOutDir(g, cfg) {
|
|
1029
1033
|
return g.path ?? cfg.outDir;
|
|
1030
1034
|
}
|
|
1035
|
+
function seedOutDir(g, cfg) {
|
|
1036
|
+
return g.path ?? cfg.outDir;
|
|
1037
|
+
}
|
|
1038
|
+
function fastCheckOutDir(g, cfg) {
|
|
1039
|
+
return g.path ?? cfg.outDir;
|
|
1040
|
+
}
|
|
1031
1041
|
function sharedSchemaNames(opts) {
|
|
1032
1042
|
const resolved = (0, import_validation_core.resolveAffix)(opts);
|
|
1033
1043
|
return import_validation_core.NAME_MODES.map((mode) => (0, import_validation_core.schemaName)(mode, import_validation_core.AFFIX_PROBE_TABLE, resolved));
|
|
@@ -1344,6 +1354,8 @@ function computeGeneratorOutputDirs(cfg, cwd = process.cwd()) {
|
|
|
1344
1354
|
if (g.kind === "effect-http") dirs.add(abs(effectHttpOutDir(g, cfg)));
|
|
1345
1355
|
if (g.kind === "ts-rest") dirs.add(abs(tsRestOutDir(g, cfg)));
|
|
1346
1356
|
if (g.kind === "elysia") dirs.add(abs(elysiaOutDir(g, cfg)));
|
|
1357
|
+
if (g.kind === "seed") dirs.add(abs(seedOutDir(g, cfg)));
|
|
1358
|
+
if (g.kind === "fast-check") dirs.add(abs(fastCheckOutDir(g, cfg)));
|
|
1347
1359
|
if (g.kind === "service") dirs.add(abs(g.path ?? "src/services"));
|
|
1348
1360
|
if (g.kind === "zod") dirs.add(abs(g.path ?? "src/validators/zod"));
|
|
1349
1361
|
if (g.kind === "valibot") dirs.add(abs(g.path ?? "src/validators/valibot"));
|
|
@@ -1627,6 +1639,29 @@ function elysiaOptions(g, cfg) {
|
|
|
1627
1639
|
};
|
|
1628
1640
|
}
|
|
1629
1641
|
|
|
1642
|
+
// src/seed-options.ts
|
|
1643
|
+
function seedOptions(g, cfg) {
|
|
1644
|
+
return {
|
|
1645
|
+
outputDir: seedOutDir(g, cfg),
|
|
1646
|
+
defaultCount: g.count,
|
|
1647
|
+
naming: g.naming,
|
|
1648
|
+
outputHeader: g.outputHeader,
|
|
1649
|
+
format: g.format,
|
|
1650
|
+
importExtension: g.importExtension
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// src/fast-check-options.ts
|
|
1655
|
+
function fastCheckOptions(g, cfg) {
|
|
1656
|
+
return {
|
|
1657
|
+
outputDir: fastCheckOutDir(g, cfg),
|
|
1658
|
+
naming: g.naming,
|
|
1659
|
+
outputHeader: g.outputHeader,
|
|
1660
|
+
format: g.format,
|
|
1661
|
+
importExtension: g.importExtension
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1630
1665
|
// src/h3-options.ts
|
|
1631
1666
|
var VALIDATOR_DEFAULT_DIRS4 = {
|
|
1632
1667
|
zod: "src/validators/zod",
|
|
@@ -1952,6 +1987,22 @@ var GENERATORS = [
|
|
|
1952
1987
|
outputDir: (g, cfg) => elysiaOutDir(g, cfg),
|
|
1953
1988
|
options: (g, cfg) => elysiaOptions(g, cfg)
|
|
1954
1989
|
},
|
|
1990
|
+
{
|
|
1991
|
+
kind: "seed",
|
|
1992
|
+
specifier: "@drzl/generator-seed",
|
|
1993
|
+
load: () => import("@drzl/generator-seed"),
|
|
1994
|
+
construct: (m, analysis) => new m.SeedGenerator(analysis),
|
|
1995
|
+
outputDir: (g, cfg) => seedOutDir(g, cfg),
|
|
1996
|
+
options: (g, cfg) => seedOptions(g, cfg)
|
|
1997
|
+
},
|
|
1998
|
+
{
|
|
1999
|
+
kind: "fast-check",
|
|
2000
|
+
specifier: "@drzl/generator-fast-check",
|
|
2001
|
+
load: () => import("@drzl/generator-fast-check"),
|
|
2002
|
+
construct: (m, analysis) => new m.FastCheckGenerator(analysis),
|
|
2003
|
+
outputDir: (g, cfg) => fastCheckOutDir(g, cfg),
|
|
2004
|
+
options: (g, cfg) => fastCheckOptions(g, cfg)
|
|
2005
|
+
},
|
|
1955
2006
|
{
|
|
1956
2007
|
kind: "service",
|
|
1957
2008
|
specifier: "@drzl/generator-service",
|
|
@@ -3293,6 +3344,180 @@ async function resolveSchemaSource(cfg, cwd = process.cwd()) {
|
|
|
3293
3344
|
};
|
|
3294
3345
|
}
|
|
3295
3346
|
|
|
3347
|
+
// src/constraint-drift.ts
|
|
3348
|
+
var import_validation_core5 = require("@drzl/validation-core");
|
|
3349
|
+
var import_chalk4 = require("chalk");
|
|
3350
|
+
var PLAIN3 = new import_chalk4.Chalk({ level: 0 });
|
|
3351
|
+
var PLAIN_TEXT_TYPES = /* @__PURE__ */ new Set([
|
|
3352
|
+
"text",
|
|
3353
|
+
"varchar",
|
|
3354
|
+
"character varying",
|
|
3355
|
+
"char",
|
|
3356
|
+
"character",
|
|
3357
|
+
"bpchar",
|
|
3358
|
+
"citext",
|
|
3359
|
+
"tinytext",
|
|
3360
|
+
"mediumtext",
|
|
3361
|
+
"longtext",
|
|
3362
|
+
"nvarchar",
|
|
3363
|
+
"nchar"
|
|
3364
|
+
]);
|
|
3365
|
+
function typeStem(sqlType) {
|
|
3366
|
+
return sqlType.replace(/\(.*$/, "").replace(/\[\]$/, "").trim().toLowerCase();
|
|
3367
|
+
}
|
|
3368
|
+
function isSchemaOnlyEnum(column) {
|
|
3369
|
+
if (!column.enumValues || column.enumValues.length === 0) return false;
|
|
3370
|
+
if (!column.sqlType) return false;
|
|
3371
|
+
return PLAIN_TEXT_TYPES.has(typeStem(column.sqlType));
|
|
3372
|
+
}
|
|
3373
|
+
function sqlLiterals(values) {
|
|
3374
|
+
return values.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ");
|
|
3375
|
+
}
|
|
3376
|
+
function derivedName(table, column) {
|
|
3377
|
+
return `${table}_${column}_check`;
|
|
3378
|
+
}
|
|
3379
|
+
function buildConstraintDriftReport(analysis, schemaPath) {
|
|
3380
|
+
const entries = [];
|
|
3381
|
+
for (const table of analysis.tables) {
|
|
3382
|
+
for (const c of (0, import_validation_core5.tableConstraints)(table).constraints) {
|
|
3383
|
+
if (c.enforced) {
|
|
3384
|
+
for (const part of c.unenforced ?? []) {
|
|
3385
|
+
entries.push({
|
|
3386
|
+
side: "database-only",
|
|
3387
|
+
table: table.name,
|
|
3388
|
+
columns: [...c.columns],
|
|
3389
|
+
rule: part.part,
|
|
3390
|
+
reason: part.reason,
|
|
3391
|
+
...c.name ? { constraint: c.name } : {}
|
|
3392
|
+
});
|
|
3393
|
+
}
|
|
3394
|
+
continue;
|
|
3395
|
+
}
|
|
3396
|
+
entries.push({
|
|
3397
|
+
side: "database-only",
|
|
3398
|
+
table: table.name,
|
|
3399
|
+
columns: [...c.columns],
|
|
3400
|
+
rule: c.rule,
|
|
3401
|
+
reason: reasonFor(c.kind, c.unenforced),
|
|
3402
|
+
...c.name ? { constraint: c.name } : {}
|
|
3403
|
+
});
|
|
3404
|
+
}
|
|
3405
|
+
for (const column of table.columns) {
|
|
3406
|
+
if (!isSchemaOnlyEnum(column)) continue;
|
|
3407
|
+
const values = column.enumValues;
|
|
3408
|
+
entries.push({
|
|
3409
|
+
side: "schema-only",
|
|
3410
|
+
table: table.name,
|
|
3411
|
+
columns: [column.name],
|
|
3412
|
+
rule: `${column.name} IN (${sqlLiterals(values)})`,
|
|
3413
|
+
reason: `the column is declared ${column.sqlType}, which holds any text; the set exists only in the generated schemas`,
|
|
3414
|
+
fix: `ALTER TABLE ${table.name} ADD CONSTRAINT ${derivedName(table.name, column.name)} CHECK (${column.name} IN (${sqlLiterals(values)}));`
|
|
3415
|
+
});
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
const databaseOnly = entries.filter((e) => e.side === "database-only").length;
|
|
3419
|
+
const schemaOnly = entries.filter((e) => e.side === "schema-only").length;
|
|
3420
|
+
return {
|
|
3421
|
+
schema: schemaPath,
|
|
3422
|
+
dialect: analysis.dialect,
|
|
3423
|
+
ok: entries.length === 0,
|
|
3424
|
+
counts: { tables: analysis.tables.length, databaseOnly, schemaOnly },
|
|
3425
|
+
entries
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
function reasonFor(kind, unenforced) {
|
|
3429
|
+
if (unenforced && unenforced.length) return unenforced.map((u) => u.reason).join("; ");
|
|
3430
|
+
switch (kind) {
|
|
3431
|
+
case "primaryKey":
|
|
3432
|
+
case "unique":
|
|
3433
|
+
return "whether a value is already taken is a fact about the table, not about one row";
|
|
3434
|
+
case "foreignKey":
|
|
3435
|
+
return "whether the referenced row exists is a question only the database can answer";
|
|
3436
|
+
default:
|
|
3437
|
+
return "no generated schema states it";
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
function wrap3(text, indent, first = indent, width = 96) {
|
|
3441
|
+
const words = text.split(/\s+/).filter(Boolean);
|
|
3442
|
+
const lines = [];
|
|
3443
|
+
let line = first;
|
|
3444
|
+
let started = false;
|
|
3445
|
+
for (const w of words) {
|
|
3446
|
+
if (started && line.length + 1 + w.length > width) {
|
|
3447
|
+
lines.push(line);
|
|
3448
|
+
line = indent + w;
|
|
3449
|
+
} else {
|
|
3450
|
+
line = started ? `${line} ${w}` : line + w;
|
|
3451
|
+
started = true;
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
if (started) lines.push(line);
|
|
3455
|
+
return lines.join("\n");
|
|
3456
|
+
}
|
|
3457
|
+
function renderConstraintDriftReport(report, style = PLAIN3) {
|
|
3458
|
+
const chalk = style;
|
|
3459
|
+
const out = [];
|
|
3460
|
+
const plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
|
|
3461
|
+
out.push(chalk.bold(`DRZL constraint drift ${report.schema}`));
|
|
3462
|
+
out.push(chalk.dim(`${report.dialect}, ${plural(report.counts.tables, "table")}`));
|
|
3463
|
+
out.push("");
|
|
3464
|
+
if (report.ok) {
|
|
3465
|
+
out.push(chalk.green("No drift."));
|
|
3466
|
+
out.push(chalk.dim(" Every constraint the database declares is one the generated schemas state."));
|
|
3467
|
+
out.push(chalk.dim(" Every set the generated schemas restrict is one the database enforces."));
|
|
3468
|
+
return out.join("\n");
|
|
3469
|
+
}
|
|
3470
|
+
const schemaOnly = report.entries.filter((e) => e.side === "schema-only");
|
|
3471
|
+
if (schemaOnly.length) {
|
|
3472
|
+
out.push(chalk.yellow("Your schemas enforce this and the database does not"));
|
|
3473
|
+
out.push(
|
|
3474
|
+
chalk.dim(
|
|
3475
|
+
wrap3(
|
|
3476
|
+
"Any other client writes past these. A migration, a psql session or an admin tool is not running your validators.",
|
|
3477
|
+
" "
|
|
3478
|
+
)
|
|
3479
|
+
)
|
|
3480
|
+
);
|
|
3481
|
+
out.push("");
|
|
3482
|
+
for (const e of schemaOnly) {
|
|
3483
|
+
out.push(` ${chalk.dim("-")} ${chalk.bold(e.table)}.${e.columns.join(", ")}`);
|
|
3484
|
+
out.push(wrap3(e.rule, " "));
|
|
3485
|
+
out.push(chalk.dim(wrap3(e.reason, " ")));
|
|
3486
|
+
if (e.fix) {
|
|
3487
|
+
out.push(chalk.dim(" Close it with:"));
|
|
3488
|
+
out.push(` ${e.fix}`);
|
|
3489
|
+
}
|
|
3490
|
+
out.push("");
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
const databaseOnly = report.entries.filter((e) => e.side === "database-only");
|
|
3494
|
+
if (databaseOnly.length) {
|
|
3495
|
+
out.push(chalk.cyan("The database enforces this and your schemas do not"));
|
|
3496
|
+
out.push(
|
|
3497
|
+
chalk.dim(
|
|
3498
|
+
wrap3(
|
|
3499
|
+
"Mostly not a defect: a key, a unique index and a foreign key are facts about the table rather than about one row, so no per-row validator can see them. Listed so nobody reads the generated schemas as the whole story.",
|
|
3500
|
+
" "
|
|
3501
|
+
)
|
|
3502
|
+
)
|
|
3503
|
+
);
|
|
3504
|
+
out.push("");
|
|
3505
|
+
for (const e of databaseOnly) {
|
|
3506
|
+
const name = e.constraint ? ` ${chalk.dim(`(${e.constraint})`)}` : "";
|
|
3507
|
+
out.push(` ${chalk.dim("-")} ${chalk.bold(e.table)}${name}`);
|
|
3508
|
+
out.push(wrap3(e.rule, " "));
|
|
3509
|
+
out.push(chalk.dim(wrap3(e.reason, " ")));
|
|
3510
|
+
out.push("");
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
out.push(
|
|
3514
|
+
chalk.dim(
|
|
3515
|
+
`${plural(report.counts.schemaOnly, "gap")} your schemas alone enforce, ${plural(report.counts.databaseOnly, "constraint")} only the database does.`
|
|
3516
|
+
)
|
|
3517
|
+
);
|
|
3518
|
+
return out.join("\n");
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3296
3521
|
// src/drift.ts
|
|
3297
3522
|
var import_node_fs = require("fs");
|
|
3298
3523
|
var import_node_path = __toESM(require("path"), 1);
|
|
@@ -4215,7 +4440,10 @@ withOutputFlags(
|
|
|
4215
4440
|
}
|
|
4216
4441
|
});
|
|
4217
4442
|
withOutputFlags(
|
|
4218
|
-
program.command("doctor").description("Report what DRZL cannot type or enforce in your schema, and why").argument("[schema]", "path to drizzle schema (TS); defaults to the schema in drzl.config").option("-c, --config <path>", "path to drzl.config, read when no schema argument is given").option("--strict", "exit 2 when anything is reported", false)
|
|
4443
|
+
program.command("doctor").description("Report what DRZL cannot type or enforce in your schema, and why").argument("[schema]", "path to drizzle schema (TS); defaults to the schema in drzl.config").option("-c, --config <path>", "path to drzl.config, read when no schema argument is given").option("--strict", "exit 2 when anything is reported", false).option(
|
|
4444
|
+
"--constraints",
|
|
4445
|
+
"report what each side enforces that the other does not, instead of the usual findings"
|
|
4446
|
+
)
|
|
4219
4447
|
).action(async (schema, opts) => {
|
|
4220
4448
|
const out = outputFor(opts);
|
|
4221
4449
|
{
|
|
@@ -4237,10 +4465,34 @@ withOutputFlags(
|
|
|
4237
4465
|
includeRelations: true,
|
|
4238
4466
|
validateConstraints: true
|
|
4239
4467
|
});
|
|
4240
|
-
const
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4468
|
+
const schemaLabel = Array.isArray(target) ? target.join(", ") : target;
|
|
4469
|
+
if (opts.constraints) {
|
|
4470
|
+
const preflight = buildDoctorReport(analysis, schemaLabel);
|
|
4471
|
+
const fatal = preflight.findings.filter((f) => f.level === "error");
|
|
4472
|
+
if (fatal.length) {
|
|
4473
|
+
if (opts.json)
|
|
4474
|
+
out.data(
|
|
4475
|
+
JSON.stringify(
|
|
4476
|
+
{ command: "doctor", exitCode: EXIT_FAILED, ...preflight },
|
|
4477
|
+
null,
|
|
4478
|
+
2
|
|
4479
|
+
)
|
|
4480
|
+
);
|
|
4481
|
+
else out.data(renderDoctorReport(preflight, out.outStyle));
|
|
4482
|
+
process.exit(EXIT_FAILED);
|
|
4483
|
+
return;
|
|
4484
|
+
}
|
|
4485
|
+
const drift = buildConstraintDriftReport(analysis, schemaLabel);
|
|
4486
|
+
const driftCode = opts.strict && drift.counts.schemaOnly ? EXIT_FINDINGS : EXIT_OK;
|
|
4487
|
+
if (opts.json)
|
|
4488
|
+
out.data(
|
|
4489
|
+
JSON.stringify({ command: "doctor", exitCode: driftCode, ...drift }, null, 2)
|
|
4490
|
+
);
|
|
4491
|
+
else out.data(renderConstraintDriftReport(drift, out.outStyle));
|
|
4492
|
+
process.exit(driftCode);
|
|
4493
|
+
return;
|
|
4494
|
+
}
|
|
4495
|
+
const report = buildDoctorReport(analysis, schemaLabel);
|
|
4244
4496
|
const unreadable = report.findings.some((f) => f.level === "error");
|
|
4245
4497
|
const code = unreadable ? EXIT_FAILED : opts.strict && report.findings.length ? EXIT_FINDINGS : EXIT_OK;
|
|
4246
4498
|
if (opts.json)
|