@drzl/cli 4.37.0 → 4.38.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-EQ4OSVMJ.js → chunk-VG6SD2GP.js} +7 -1
- package/dist/chunk-VG6SD2GP.js.map +1 -0
- package/dist/cli.cjs +137 -0
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +134 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.cjs +7 -0
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +16 -1
- package/dist/config.d.ts +16 -1
- package/dist/config.js +3 -1
- package/dist/drzl.config.schema.json +1 -0
- package/package.json +3 -2
- package/dist/chunk-EQ4OSVMJ.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -30,13 +30,14 @@ import {
|
|
|
30
30
|
nestjsOutDir,
|
|
31
31
|
nextOutDir,
|
|
32
32
|
openApiFetchOutDir,
|
|
33
|
+
pothosOutDir,
|
|
33
34
|
seedOutDir,
|
|
34
35
|
tableAliases,
|
|
35
36
|
tableFilterWarnings,
|
|
36
37
|
tanstackStartOutDir,
|
|
37
38
|
trpcOutDir,
|
|
38
39
|
tsRestOutDir
|
|
39
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-VG6SD2GP.js";
|
|
40
41
|
|
|
41
42
|
// src/cli.ts
|
|
42
43
|
import { qualifiedTableName as qualifiedTableName2, SchemaAnalyzer as SchemaAnalyzer2 } from "@drzl/analyzer";
|
|
@@ -531,6 +532,17 @@ function fastCheckOptions(g, cfg) {
|
|
|
531
532
|
};
|
|
532
533
|
}
|
|
533
534
|
|
|
535
|
+
// src/pothos-options.ts
|
|
536
|
+
function pothosOptions(g, cfg) {
|
|
537
|
+
return {
|
|
538
|
+
outputDir: pothosOutDir(g, cfg),
|
|
539
|
+
naming: g.naming,
|
|
540
|
+
outputHeader: g.outputHeader,
|
|
541
|
+
format: g.format,
|
|
542
|
+
importExtension: g.importExtension
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
534
546
|
// src/h3-options.ts
|
|
535
547
|
var VALIDATOR_DEFAULT_DIRS6 = {
|
|
536
548
|
zod: "src/validators/zod",
|
|
@@ -893,6 +905,14 @@ var GENERATORS = [
|
|
|
893
905
|
outputDir: (g, cfg) => fastCheckOutDir(g, cfg),
|
|
894
906
|
options: (g, cfg) => fastCheckOptions(g, cfg)
|
|
895
907
|
},
|
|
908
|
+
{
|
|
909
|
+
kind: "pothos",
|
|
910
|
+
specifier: "@drzl/generator-pothos",
|
|
911
|
+
load: () => import("@drzl/generator-pothos"),
|
|
912
|
+
construct: (m, analysis) => new m.PothosGenerator(analysis),
|
|
913
|
+
outputDir: (g, cfg) => pothosOutDir(g, cfg),
|
|
914
|
+
options: (g, cfg) => pothosOptions(g, cfg)
|
|
915
|
+
},
|
|
896
916
|
{
|
|
897
917
|
kind: "service",
|
|
898
918
|
specifier: "@drzl/generator-service",
|
|
@@ -1136,6 +1156,101 @@ import { parseCheck as parseCheck2 } from "@drzl/validation-core";
|
|
|
1136
1156
|
|
|
1137
1157
|
// src/doctor.ts
|
|
1138
1158
|
import { lengthMeasure, parseCheck } from "@drzl/validation-core";
|
|
1159
|
+
|
|
1160
|
+
// src/auth-tables.ts
|
|
1161
|
+
var SIGNATURES = [
|
|
1162
|
+
{
|
|
1163
|
+
model: "account",
|
|
1164
|
+
// `providerId` beside `accountId` is the distinctive pair: it is a link to an external identity
|
|
1165
|
+
// provider, which an application table has no reason to model this way.
|
|
1166
|
+
required: ["accountId", "providerId", "userId"],
|
|
1167
|
+
supporting: ["accessToken", "refreshToken", "idToken", "password", "scope"],
|
|
1168
|
+
conventionalName: "account",
|
|
1169
|
+
secrets: ["accessToken", "refreshToken", "idToken", "password"]
|
|
1170
|
+
},
|
|
1171
|
+
{
|
|
1172
|
+
model: "session",
|
|
1173
|
+
required: ["token", "expiresAt", "userId"],
|
|
1174
|
+
supporting: ["ipAddress", "userAgent"],
|
|
1175
|
+
conventionalName: "session",
|
|
1176
|
+
// A session token is a bearer credential: whoever reads it is that user until it expires.
|
|
1177
|
+
secrets: ["token"]
|
|
1178
|
+
},
|
|
1179
|
+
{
|
|
1180
|
+
model: "verification",
|
|
1181
|
+
// `identifier` and `value` are deliberately generic names, which is why `expiresAt` is required
|
|
1182
|
+
// too: the three together are a short-lived token store and little else.
|
|
1183
|
+
required: ["identifier", "value", "expiresAt"],
|
|
1184
|
+
supporting: [],
|
|
1185
|
+
conventionalName: "verification",
|
|
1186
|
+
secrets: ["value"]
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
model: "user",
|
|
1190
|
+
required: ["email", "emailVerified"],
|
|
1191
|
+
supporting: ["name", "image"],
|
|
1192
|
+
conventionalName: "user",
|
|
1193
|
+
secrets: []
|
|
1194
|
+
}
|
|
1195
|
+
];
|
|
1196
|
+
function columnKeys(table) {
|
|
1197
|
+
const out = /* @__PURE__ */ new Map();
|
|
1198
|
+
for (const c of table.columns) out.set(normalise(c.name), c);
|
|
1199
|
+
return out;
|
|
1200
|
+
}
|
|
1201
|
+
function normalise(name) {
|
|
1202
|
+
return name.replace(/[_-]/g, "").toLowerCase();
|
|
1203
|
+
}
|
|
1204
|
+
function has(keys, name) {
|
|
1205
|
+
return keys.has(normalise(name));
|
|
1206
|
+
}
|
|
1207
|
+
function actualName(table, wanted) {
|
|
1208
|
+
return table.columns.find((c) => normalise(c.name) === normalise(wanted))?.name;
|
|
1209
|
+
}
|
|
1210
|
+
function matchOne(table, sig) {
|
|
1211
|
+
const keys = columnKeys(table);
|
|
1212
|
+
const missing = sig.required.filter((r) => !has(keys, r));
|
|
1213
|
+
if (missing.length) return void 0;
|
|
1214
|
+
const supporting = sig.supporting.filter((s) => has(keys, s));
|
|
1215
|
+
const nameMatches = normalise(table.name) === normalise(sig.conventionalName) || normalise(table.name) === `${normalise(sig.conventionalName)}s`;
|
|
1216
|
+
const confidence = supporting.length > 0 || nameMatches ? "strong" : "likely";
|
|
1217
|
+
const matched = [...sig.required, ...supporting].map((c) => actualName(table, c)).filter((c) => Boolean(c));
|
|
1218
|
+
const secrets = sig.secrets.map((c) => actualName(table, c)).filter((c) => Boolean(c));
|
|
1219
|
+
return { table: table.name, model: sig.model, confidence, matched, secrets };
|
|
1220
|
+
}
|
|
1221
|
+
function detectAuthTables(analysis) {
|
|
1222
|
+
const found = [];
|
|
1223
|
+
for (const table of analysis.tables) {
|
|
1224
|
+
for (const sig of SIGNATURES) {
|
|
1225
|
+
const m = matchOne(table, sig);
|
|
1226
|
+
if (m) {
|
|
1227
|
+
found.push(m);
|
|
1228
|
+
break;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
const hasNonUser = found.some((f) => f.model !== "user");
|
|
1233
|
+
return hasNonUser ? found : found.filter((f) => f.model !== "user");
|
|
1234
|
+
}
|
|
1235
|
+
function authTablesWithSecrets(matches) {
|
|
1236
|
+
return matches.filter((m) => m.secrets.length > 0);
|
|
1237
|
+
}
|
|
1238
|
+
function excludeSuggestion(matches) {
|
|
1239
|
+
const names = [...new Set(matches.map((m) => m.table))].sort();
|
|
1240
|
+
return `exclude: [${names.map((n) => `'${n}'`).join(", ")}]`;
|
|
1241
|
+
}
|
|
1242
|
+
function authTableWarnings(analysis) {
|
|
1243
|
+
const risky = authTablesWithSecrets(detectAuthTables(analysis));
|
|
1244
|
+
if (!risky.length) return [];
|
|
1245
|
+
const lines = risky.map(
|
|
1246
|
+
(m) => `"${m.table}" looks like an authentication library's ${m.model} table and holds ${m.secrets.map((c) => `"${c}"`).join(", ")}. Generating for it publishes ${m.secrets.length === 1 ? "that column" : "those columns"}.`
|
|
1247
|
+
);
|
|
1248
|
+
return [
|
|
1249
|
+
`drzl generate: ${lines.join(" ")} Keep ${risky.length === 1 ? "it" : "them"} out with ${excludeSuggestion(risky)}, or leave it if the route is deliberate.`
|
|
1250
|
+
];
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// src/doctor.ts
|
|
1139
1254
|
import { Chalk as Chalk2 } from "chalk";
|
|
1140
1255
|
var PLAIN = new Chalk2({ level: 0 });
|
|
1141
1256
|
var HANDLED_CODES = /* @__PURE__ */ new Set(["DRZL_ANL_UNKNOWN_COLUMN"]);
|
|
@@ -1308,6 +1423,18 @@ function buildDoctorReport(analysis, schemaPath) {
|
|
|
1308
1423
|
hint: i.hint
|
|
1309
1424
|
});
|
|
1310
1425
|
}
|
|
1426
|
+
for (const match of detectAuthTables(analysis)) {
|
|
1427
|
+
const secrets = match.secrets.length ? ` It holds ${match.secrets.map((c) => `\`${c}\``).join(", ")}, which a generated read route would return to whoever calls it.` : "";
|
|
1428
|
+
findings.push({
|
|
1429
|
+
kind: "auth-table",
|
|
1430
|
+
// A warning rather than an error: this is a real leak and it is also a guess about someone
|
|
1431
|
+
// else's schema, and a doctor that failed the build on a guess would be switched off.
|
|
1432
|
+
level: "warn",
|
|
1433
|
+
table: match.table,
|
|
1434
|
+
message: `"${match.table}" matches the shape of an authentication library's ${match.model} table (${match.matched.join(", ")}).${secrets}`,
|
|
1435
|
+
hint: `Keep it out of every generator with ${excludeSuggestion([match])}.`
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1311
1438
|
for (const i of analysis.issues) {
|
|
1312
1439
|
if (i.code !== "DRZL_ANL_UNKNOWN_COLUMN") continue;
|
|
1313
1440
|
findings.push({
|
|
@@ -1346,6 +1473,11 @@ var SECTIONS = [
|
|
|
1346
1473
|
title: "Columns DRZL cannot type",
|
|
1347
1474
|
why: "These get a validator that accepts any value."
|
|
1348
1475
|
},
|
|
1476
|
+
{
|
|
1477
|
+
kinds: ["auth-table"],
|
|
1478
|
+
title: "Tables that look like an authentication library's",
|
|
1479
|
+
why: "Every generator loops over every table it finds, so these get routes too."
|
|
1480
|
+
},
|
|
1349
1481
|
{
|
|
1350
1482
|
kinds: ["check-declined", "check-unknown-column", "check-not-scalar", "check-uncountable"],
|
|
1351
1483
|
title: "CHECK constraints DRZL does not enforce",
|
|
@@ -3925,6 +4057,7 @@ withOutputFlags(
|
|
|
3925
4057
|
analysis.tables = filterTables(narrowed.tables, cfg);
|
|
3926
4058
|
for (const w of [...narrowed.warnings, ...filterWarnings]) warn(w);
|
|
3927
4059
|
for (const w of wideColumnWarning(analysis.issues)) warn(w);
|
|
4060
|
+
for (const w of authTableWarnings(analysis)) warn(w);
|
|
3928
4061
|
const empty = nothingToGenerate({
|
|
3929
4062
|
schema: source.schema,
|
|
3930
4063
|
analyzed: narrowed.tables,
|