@drzl/cli 4.23.0 → 4.24.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/cli.cjs +66 -4333
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +21 -15
- package/dist/cli.js.map +1 -1
- package/dist/config.d.cts +2 -2
- package/dist/config.d.ts +2 -2
- package/package.json +11 -13
- package/dist/chunk-KKPDOZOD.js +0 -794
- package/dist/chunk-KKPDOZOD.js.map +0 -1
- package/dist/dist-KQMPKOFK.js +0 -350
- package/dist/dist-KQMPKOFK.js.map +0 -1
- package/dist/dist-KX62ETKK.js +0 -634
- package/dist/dist-KX62ETKK.js.map +0 -1
- package/dist/dist-P24ILO5N.js +0 -431
- package/dist/dist-P24ILO5N.js.map +0 -1
- package/dist/dist-QYH7DRFY.js +0 -27
- package/dist/dist-QYH7DRFY.js.map +0 -1
- package/dist/dist-SGI2I53L.js +0 -434
- package/dist/dist-SGI2I53L.js.map +0 -1
- package/dist/dist-T5376MW7.js +0 -489
- package/dist/dist-T5376MW7.js.map +0 -1
- package/dist/dist-UVP6B4XJ.js +0 -646
- package/dist/dist-UVP6B4XJ.js.map +0 -1
- package/dist/dist-ZIHNXQ7U.js +0 -522
- package/dist/dist-ZIHNXQ7U.js.map +0 -1
package/dist/cli.cjs
CHANGED
|
@@ -7,4298 +7,25 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
7
7
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
8
|
var __getProtoOf = Object.getPrototypeOf;
|
|
9
9
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
-
var
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
16
|
-
};
|
|
17
|
-
var __copyProps = (to, from, except, desc) => {
|
|
18
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
19
|
-
for (let key of __getOwnPropNames(from))
|
|
20
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
21
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
22
|
-
}
|
|
23
|
-
return to;
|
|
24
|
-
};
|
|
25
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
26
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
27
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
28
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
29
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
30
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
31
|
-
mod
|
|
32
|
-
));
|
|
33
|
-
|
|
34
|
-
// ../generator-trpc/dist/index.js
|
|
35
|
-
var dist_exports = {};
|
|
36
|
-
__export(dist_exports, {
|
|
37
|
-
BASE_MODULE: () => BASE_MODULE,
|
|
38
|
-
TRPCGenerator: () => TRPCGenerator,
|
|
39
|
-
TRPC_MAJOR: () => TRPC_MAJOR,
|
|
40
|
-
default: () => index_default
|
|
41
|
-
});
|
|
42
|
-
function isWide(column) {
|
|
43
|
-
if (column.enumValues && column.enumValues.length) return false;
|
|
44
|
-
if (column.shape?.kind === "tuple" || column.shape?.kind === "numberObject") return false;
|
|
45
|
-
return !["number", "string", "boolean", "Date"].includes(column.tsType);
|
|
46
|
-
}
|
|
47
|
-
function mapExpr(column, lib, mode) {
|
|
48
|
-
const d = LIBS[lib];
|
|
49
|
-
let base = (() => {
|
|
50
|
-
if (column.enumValues && column.enumValues.length) return d.enum(column.enumValues);
|
|
51
|
-
if (column.shape?.kind === "tuple" && d.tuple) return d.tuple(column.shape.length);
|
|
52
|
-
if (column.shape?.kind === "numberObject" && d.numberObject) {
|
|
53
|
-
return d.numberObject(column.shape.fields);
|
|
54
|
-
}
|
|
55
|
-
switch (column.tsType) {
|
|
56
|
-
case "number":
|
|
57
|
-
return d.number;
|
|
58
|
-
case "string":
|
|
59
|
-
return d.string;
|
|
60
|
-
case "boolean":
|
|
61
|
-
return d.boolean;
|
|
62
|
-
case "Date":
|
|
63
|
-
return d.date;
|
|
64
|
-
default:
|
|
65
|
-
return d.unknown;
|
|
66
|
-
}
|
|
67
|
-
})();
|
|
68
|
-
if (column.nullable) base = d.nullable(base);
|
|
69
|
-
if (mode !== "select") {
|
|
70
|
-
const optional = mode === "update" || column.nullable || column.hasDefault;
|
|
71
|
-
if (optional) base = d.optional(base);
|
|
72
|
-
}
|
|
73
|
-
return base;
|
|
74
|
-
}
|
|
75
|
-
function field(column, lib, mode) {
|
|
76
|
-
const d = LIBS[lib];
|
|
77
|
-
const expr = mapExpr(column, lib, mode);
|
|
78
|
-
return `${objectKey(column.name)}: ${d.fieldIsString ? JSON.stringify(expr) : expr}`;
|
|
79
|
-
}
|
|
80
|
-
function objectKey(name) {
|
|
81
|
-
return isIdent(name) ? name : JSON.stringify(name);
|
|
82
|
-
}
|
|
83
|
-
function renderSchema(table, lib, mode) {
|
|
84
|
-
const d = LIBS[lib];
|
|
85
|
-
const cols = table.columns.filter((c) => mode === "select" ? true : !c.isGenerated);
|
|
86
|
-
const body = cols.map((c) => ` ${field(c, lib, mode)},`).join("\n");
|
|
87
|
-
const schema = d.object(body);
|
|
88
|
-
return mode === "update" && d.partialUpdate ? d.partialUpdate(schema) : schema;
|
|
89
|
-
}
|
|
90
|
-
function toCase(s, c) {
|
|
91
|
-
if (!c) return s;
|
|
92
|
-
const parts = s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]/g, " ").split(/\s+/);
|
|
93
|
-
if (c === "camel") {
|
|
94
|
-
return parts.map(
|
|
95
|
-
(p, i) => i === 0 ? p.toLowerCase() : p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()
|
|
96
|
-
).join("");
|
|
97
|
-
}
|
|
98
|
-
if (c === "kebab") return parts.map((p) => p.toLowerCase()).join("-");
|
|
99
|
-
if (c === "snake") return parts.map((p) => p.toLowerCase()).join("_");
|
|
100
|
-
return s;
|
|
101
|
-
}
|
|
102
|
-
function keyColumns(table) {
|
|
103
|
-
const names = table.primaryKey?.columns ?? [];
|
|
104
|
-
if (!names.length) return null;
|
|
105
|
-
const cols = names.map((n) => table.columns.find((c) => c.name === n));
|
|
106
|
-
if (cols.some((c) => !c)) return null;
|
|
107
|
-
return cols;
|
|
108
|
-
}
|
|
109
|
-
function renderBase(opts) {
|
|
110
|
-
const injection = opts.databaseInjection?.enabled === true;
|
|
111
|
-
const dbType = opts.databaseInjection?.databaseType ?? "unknown";
|
|
112
|
-
const typeImport = opts.databaseInjection?.databaseTypeImport ? `import type { ${opts.databaseInjection.databaseTypeImport.name} } from '${opts.databaseInjection.databaseTypeImport.from}';
|
|
113
|
-
` : "";
|
|
114
|
-
const trpcImport = injection ? `import { initTRPC, TRPCError } from '@trpc/server';` : `import { initTRPC } from '@trpc/server';`;
|
|
115
|
-
const context = injection ? `/**
|
|
116
|
-
* What your \`createContext\` hands every procedure.
|
|
117
|
-
*
|
|
118
|
-
* \`db\` is optional here and required by \`dbProcedure\` below. That split is what lets an adapter
|
|
119
|
-
* build a context without a handle, for a health check or a public route, while every generated
|
|
120
|
-
* procedure still sees one that is present.
|
|
121
|
-
*/
|
|
122
|
-
export interface Context {
|
|
123
|
-
db?: ${dbType};
|
|
124
|
-
}` : `/**
|
|
125
|
-
* What your \`createContext\` hands every procedure. Nothing generated reads it, so it is left
|
|
126
|
-
* open; narrow it to the shape your own context really has.
|
|
127
|
-
*/
|
|
128
|
-
export type Context = Record<string, unknown>;`;
|
|
129
|
-
const middleware = injection ? `
|
|
130
|
-
/**
|
|
131
|
-
* The builder every generated procedure is built from: it refuses to run without a database
|
|
132
|
-
* handle, and narrows \`ctx.db\` from optional to present for everything downstream.
|
|
133
|
-
*/
|
|
134
|
-
export const dbProcedure = t.procedure.use(async ({ ctx, next }) => {
|
|
135
|
-
if (!ctx.db) {
|
|
136
|
-
throw new TRPCError({
|
|
137
|
-
code: 'INTERNAL_SERVER_ERROR',
|
|
138
|
-
message: 'No database handle on the tRPC context. Provide one from createContext.',
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
return next({ ctx: { db: ctx.db } });
|
|
142
|
-
});
|
|
143
|
-
` : "";
|
|
144
|
-
return `// Generated by @drzl/generator-trpc
|
|
145
|
-
// The shared tRPC base. Every generated router imports from here.
|
|
146
|
-
${trpcImport}
|
|
147
|
-
${typeImport}
|
|
148
|
-
${context}
|
|
149
|
-
|
|
150
|
-
const t = initTRPC.context<Context>().create();
|
|
151
|
-
|
|
152
|
-
export const router = t.router;
|
|
153
|
-
export const mergeRouters = t.mergeRouters;
|
|
154
|
-
export const middleware = t.middleware;
|
|
155
|
-
/** Needed to call this router in-process, from a test or from SSR. */
|
|
156
|
-
export const createCallerFactory = t.createCallerFactory;
|
|
157
|
-
export const publicProcedure = t.procedure;
|
|
158
|
-
${middleware}`;
|
|
159
|
-
}
|
|
160
|
-
function renderRouter(table, opts, ctx) {
|
|
161
|
-
const lib = opts.validation?.library ?? "zod";
|
|
162
|
-
const d = LIBS[lib];
|
|
163
|
-
const service = opts.template === "service";
|
|
164
|
-
const injection = opts.databaseInjection?.enabled === true;
|
|
165
|
-
const builder = injection ? "dbProcedure" : "publicProcedure";
|
|
166
|
-
const insertName = `Insert${table.tsName}Schema`;
|
|
167
|
-
const updateName = `Update${table.tsName}Schema`;
|
|
168
|
-
const selectName = `Select${table.tsName}Schema`;
|
|
169
|
-
const writable = !table.readOnly;
|
|
170
|
-
const key = keyColumns(table);
|
|
171
|
-
const Service = `${cap(singularize(table.tsName))}Service`;
|
|
172
|
-
const serviceKeyable = !!key && key.every(serviceKeyExpressible);
|
|
173
|
-
const keyArg = key ? key.map((c) => `input.${c.name}`).join(", ") : "";
|
|
174
|
-
const dbArg = injection ? "ctx.db, " : "";
|
|
175
|
-
const wiredParams = injection ? "{ ctx, input }" : "{ input }";
|
|
176
|
-
const procedures = [];
|
|
177
|
-
const notImplemented = (what) => `throw new Error('Not implemented: ${what} ${table.tsName}.');`;
|
|
178
|
-
procedures.push({
|
|
179
|
-
name: "list",
|
|
180
|
-
kind: "query",
|
|
181
|
-
output: d.arrayOf(selectName),
|
|
182
|
-
params: service && injection ? "{ ctx }" : "",
|
|
183
|
-
body: service ? [`return await ${Service}.getAll(${injection ? "ctx.db" : ""});`] : ["return [];"]
|
|
184
|
-
});
|
|
185
|
-
const keyInput = key ? d.objectInline(key.map((c) => field(c, lib, "select")).join(", ")) : void 0;
|
|
186
|
-
if (key && keyInput) {
|
|
187
|
-
const wired = service && serviceKeyable;
|
|
188
|
-
procedures.push({
|
|
189
|
-
name: "byId",
|
|
190
|
-
kind: "query",
|
|
191
|
-
input: keyInput,
|
|
192
|
-
output: d.nullableOf(selectName),
|
|
193
|
-
params: wired ? wiredParams : "{ input: _input }",
|
|
194
|
-
body: wired ? [`return await ${Service}.getById(${dbArg}${keyArg});`] : service ? [serviceKeyNote(table), notImplemented("byId")] : ["return null;"]
|
|
195
|
-
});
|
|
196
|
-
if (writable) {
|
|
197
|
-
const updateInput = d.objectInline(
|
|
198
|
-
[...key.map((c) => field(c, lib, "select")), `data: ${updateName}`].join(", ")
|
|
199
|
-
);
|
|
200
|
-
procedures.push({
|
|
201
|
-
name: "update",
|
|
202
|
-
kind: "mutation",
|
|
203
|
-
input: updateInput,
|
|
204
|
-
output: selectName,
|
|
205
|
-
params: wired ? wiredParams : "{ input: _input }",
|
|
206
|
-
body: wired ? [`return await ${Service}.update(${dbArg}${keyArg}, input.data);`] : service ? [serviceKeyNote(table), notImplemented("update")] : [notImplemented("update")]
|
|
207
|
-
});
|
|
208
|
-
procedures.push({
|
|
209
|
-
name: "delete",
|
|
210
|
-
kind: "mutation",
|
|
211
|
-
input: keyInput,
|
|
212
|
-
output: d.booleanSchema,
|
|
213
|
-
params: wired ? wiredParams : "{ input: _input }",
|
|
214
|
-
body: wired ? [`return await ${Service}.delete(${dbArg}${keyArg});`] : service ? [serviceKeyNote(table), notImplemented("delete")] : ["return true;"]
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
if (writable) {
|
|
219
|
-
procedures.push({
|
|
220
|
-
name: "create",
|
|
221
|
-
kind: "mutation",
|
|
222
|
-
input: insertName,
|
|
223
|
-
output: selectName,
|
|
224
|
-
params: service ? wiredParams : "{ input: _input }",
|
|
225
|
-
body: service ? [`return await ${Service}.create(${dbArg}input);`] : [notImplemented("create")]
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
if (opts.includeRelations) {
|
|
229
|
-
const taken = new Set(procedures.map((p) => p.name));
|
|
230
|
-
procedures.push(...relationProcedures(table, lib, selectName, taken, service));
|
|
231
|
-
}
|
|
232
|
-
const order = ["list", "byId", "create", "update", "delete"];
|
|
233
|
-
const rank = (n) => order.indexOf(n) === -1 ? order.length : order.indexOf(n);
|
|
234
|
-
procedures.sort((a, b) => rank(a.name) - rank(b.name));
|
|
235
|
-
const routerName = routerExportName(table, opts.naming);
|
|
236
|
-
const entries = procedures.map((p) => {
|
|
237
|
-
const rawKey = toCase(p.name, opts.naming?.procedureCase);
|
|
238
|
-
const propKey = isIdent(rawKey) ? rawKey : JSON.stringify(rawKey);
|
|
239
|
-
return [
|
|
240
|
-
` ${propKey}: ${builder}`,
|
|
241
|
-
...p.input ? [` .input(${p.input})`] : [],
|
|
242
|
-
` .output(${p.output})`,
|
|
243
|
-
` .${p.kind}(async (${p.params}) => {`,
|
|
244
|
-
...p.body.map((line) => ` ${line}`),
|
|
245
|
-
` }),`
|
|
246
|
-
].join("\n");
|
|
247
|
-
}).join("\n");
|
|
248
|
-
const body = `export const ${routerName} = router({
|
|
249
|
-
${entries}
|
|
250
|
-
});
|
|
251
|
-
`;
|
|
252
|
-
const useShared = !!opts.validation?.useShared && !!opts.validation?.importPath;
|
|
253
|
-
const declared = [];
|
|
254
|
-
if (!useShared) {
|
|
255
|
-
if (writable) {
|
|
256
|
-
declared.push(`export const ${insertName} = ${renderSchema(table, lib, "insert")};`);
|
|
257
|
-
declared.push(`export const ${updateName} = ${renderSchema(table, lib, "update")};`);
|
|
258
|
-
}
|
|
259
|
-
declared.push(`export const ${selectName} = ${renderSchema(table, lib, "select")};`);
|
|
260
|
-
}
|
|
261
|
-
const decided = [...declared, body].join("\n\n");
|
|
262
|
-
const imports = [];
|
|
263
|
-
if (useShared) {
|
|
264
|
-
const sharedAffix = (0, import_validation_core3.resolveAffix)({
|
|
265
|
-
affix: opts.validation?.affix,
|
|
266
|
-
schemaSuffix: opts.validation?.schemaSuffix
|
|
267
|
-
});
|
|
268
|
-
const wanted = [
|
|
269
|
-
["insert", insertName],
|
|
270
|
-
["update", updateName],
|
|
271
|
-
["select", selectName]
|
|
272
|
-
].filter(([, local]) => decided.includes(local));
|
|
273
|
-
if (wanted.length) {
|
|
274
|
-
const spec = (0, import_validation_core3.resolveConfiguredImport)(
|
|
275
|
-
opts.validation.importPath,
|
|
276
|
-
ctx.out,
|
|
277
|
-
process.cwd(),
|
|
278
|
-
opts.importExtension
|
|
279
|
-
);
|
|
280
|
-
const names = wanted.map(([mode, local]) => {
|
|
281
|
-
const exported = (0, import_validation_core3.schemaName)(mode, table.tsName, sharedAffix);
|
|
282
|
-
return exported === local ? local : `${exported} as ${local}`;
|
|
283
|
-
}).join(", ");
|
|
284
|
-
imports.push(`import { ${names} } from '${spec}';`);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
imports.push(
|
|
288
|
-
`import { ${[builder, "router"].sort().join(", ")} } from '${(0, import_validation_core3.importSpecifier)(
|
|
289
|
-
`./${BASE_MODULE}.ts`,
|
|
290
|
-
opts.importExtension
|
|
291
|
-
)}';`
|
|
292
|
-
);
|
|
293
|
-
if (service) {
|
|
294
|
-
imports.push(`import { ${Service} } from '${serviceImportSpecifier(table, ctx, opts)}';`);
|
|
295
|
-
}
|
|
296
|
-
if (LIB_USAGE[lib].test(decided)) imports.unshift(LIB_IMPORTS[lib]);
|
|
297
|
-
const wide = table.columns.filter(isWide).map((c) => c.name);
|
|
298
|
-
const wideNote = wide.length ? `// No validated type for ${wide.length === 1 ? "this column" : "these columns"}: ${wide.join(", ")}.
|
|
299
|
-
// DRZL could not derive one from the schema, so the router accepts any value there.
|
|
300
|
-
` : "";
|
|
301
|
-
return `// Generated by @drzl/generator-trpc
|
|
302
|
-
// Router for table: ${table.name}
|
|
303
|
-
${wideNote}${imports.join("\n")}
|
|
304
|
-
|
|
305
|
-
${decided}`;
|
|
306
|
-
}
|
|
307
|
-
function relationProcedures(table, lib, selectSchemaName, taken, service) {
|
|
308
|
-
const d = LIBS[lib];
|
|
309
|
-
const out = [];
|
|
310
|
-
for (const fk of table.foreignKeys ?? []) {
|
|
311
|
-
if (fk.columns.length !== 1) continue;
|
|
312
|
-
const colName = fk.columns[0];
|
|
313
|
-
const column = table.columns.find((c) => c.name === colName);
|
|
314
|
-
if (!column) continue;
|
|
315
|
-
const name = `listBy${cap(colName)}`;
|
|
316
|
-
if (taken.has(name)) continue;
|
|
317
|
-
taken.add(name);
|
|
318
|
-
out.push({
|
|
319
|
-
name,
|
|
320
|
-
kind: "query",
|
|
321
|
-
input: d.objectInline(field(column, lib, "select")),
|
|
322
|
-
output: d.arrayOf(selectSchemaName),
|
|
323
|
-
params: "{ input: _input }",
|
|
324
|
-
body: [
|
|
325
|
-
`// Rows of ${table.name} whose ${JSON.stringify(colName)} matches _input.${colName}.`,
|
|
326
|
-
// In `service` mode every other procedure really does reach the database, so a lookup
|
|
327
|
-
// quietly answering with an empty array would read as "no matching rows". There is no
|
|
328
|
-
// generated service method for it, so it says so instead. In `standard` mode everything
|
|
329
|
-
// is a stub and `[]` is consistent with `list`.
|
|
330
|
-
service ? `throw new Error('Not implemented: ${name} ${table.tsName}.');` : "return [];"
|
|
331
|
-
]
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
return out;
|
|
335
|
-
}
|
|
336
|
-
function renderBarrel(routers, ctx, path9, opts) {
|
|
337
|
-
const baseSpec = (0, import_validation_core3.importSpecifier)(`./${BASE_MODULE}.ts`, opts.importExtension);
|
|
338
|
-
const reExports = `export { createCallerFactory, publicProcedure, router } from '${baseSpec}';
|
|
339
|
-
` + (opts.databaseInjection?.enabled === true ? `export { dbProcedure } from '${baseSpec}';
|
|
340
|
-
` : "") + `export type { Context } from '${baseSpec}';
|
|
341
|
-
`;
|
|
342
|
-
if (!routers.length) {
|
|
343
|
-
return `// Generated by @drzl/generator-trpc
|
|
344
|
-
// No tables detected in analysis. Add tables to your schema and regenerate.
|
|
345
|
-
import { router } from '${baseSpec}';
|
|
346
|
-
|
|
347
|
-
export const appRouter = router({});
|
|
348
|
-
|
|
349
|
-
/** The type a tRPC client is parameterised by: \`createTRPCClient<AppRouter>()\`. */
|
|
350
|
-
export type AppRouter = typeof appRouter;
|
|
351
|
-
|
|
352
|
-
${reExports}`;
|
|
353
|
-
}
|
|
354
|
-
const entries = routers.map(({ filePath, exportName, table }) => ({
|
|
355
|
-
rel: (0, import_validation_core3.importSpecifier)(
|
|
356
|
-
"./" + path9.relative(ctx.out, filePath).replace(/\\/g, "/"),
|
|
357
|
-
opts.importExtension
|
|
358
|
-
),
|
|
359
|
-
exportName,
|
|
360
|
-
// The namespace a client reaches this table's procedures through: `trpc.userProfiles.list`.
|
|
361
|
-
// `tsName` verbatim, because it is already a valid identifier and it is the name the user
|
|
362
|
-
// wrote in their schema. The oRPC barrel lowercases this key, turning `userProfiles` into
|
|
363
|
-
// `userprofiles`: harmless in an object literal nobody reads, and not harmless when the key
|
|
364
|
-
// is the public API of a typed client.
|
|
365
|
-
key: table.tsName
|
|
366
|
-
}));
|
|
367
|
-
const importLines = entries.map(({ rel, exportName }) => `import { ${exportName} } from '${rel}';`).join("\n");
|
|
368
|
-
const bodyLines = entries.map(({ key, exportName }) => ` ${isIdent(key) ? key : JSON.stringify(key)}: ${exportName},`).join("\n");
|
|
369
|
-
return `// Generated by @drzl/generator-trpc
|
|
370
|
-
import { router } from '${baseSpec}';
|
|
371
|
-
${importLines}
|
|
372
|
-
|
|
373
|
-
export const appRouter = router({
|
|
374
|
-
${bodyLines}
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
/** The type a tRPC client is parameterised by: \`createTRPCClient<AppRouter>()\`. */
|
|
378
|
-
export type AppRouter = typeof appRouter;
|
|
379
|
-
|
|
380
|
-
${reExports}`;
|
|
381
|
-
}
|
|
382
|
-
function serviceKeyExpressible(c) {
|
|
383
|
-
if (c.enumValues && c.enumValues.length) return true;
|
|
384
|
-
return ["number", "string", "boolean", "Date"].includes(c.tsType);
|
|
385
|
-
}
|
|
386
|
-
function serviceKeyNote(table) {
|
|
387
|
-
const cols = table.primaryKey?.columns ?? [];
|
|
388
|
-
const untyped = cols.filter((n) => {
|
|
389
|
-
const c = table.columns.find((x) => x.name === n);
|
|
390
|
-
return !c || !serviceKeyExpressible(c);
|
|
391
|
-
});
|
|
392
|
-
const what = untyped.length === 1 ? `its column ${untyped[0]}` : `its columns ${untyped.join(", ")}`;
|
|
393
|
-
return `// ${table.name} is keyed on (${cols.join(", ")}) and DRZL cannot type ${what}: the input
|
|
394
|
-
// schema carries unknown there, which the service's typed key parameter does not accept.
|
|
395
|
-
// Wire this to your own lookup.`;
|
|
396
|
-
}
|
|
397
|
-
function routerExportName(table, naming) {
|
|
398
|
-
const base = `${table.tsName}${naming?.routerSuffix ?? "Router"}`;
|
|
399
|
-
const c = naming?.procedureCase;
|
|
400
|
-
return toCase(base, c === "kebab" ? "camel" : c);
|
|
401
|
-
}
|
|
402
|
-
function serviceImportSpecifier(table, ctx, opts) {
|
|
403
|
-
const rel = relativePosix(ctx.out, ctx.services);
|
|
404
|
-
const dir = !rel ? "." : rel.startsWith(".") ? rel : `./${rel}`;
|
|
405
|
-
return (0, import_validation_core3.importSpecifier)(`${dir}/${singularize(table.tsName)}Service.ts`, opts.importExtension);
|
|
406
|
-
}
|
|
407
|
-
function relativePosix(from, to) {
|
|
408
|
-
const norm = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
409
|
-
const a = norm(from).split("/");
|
|
410
|
-
const b = norm(to).split("/");
|
|
411
|
-
let i = 0;
|
|
412
|
-
while (i < a.length && i < b.length && a[i] === b[i]) i++;
|
|
413
|
-
return [...Array.from({ length: a.length - i }, () => ".."), ...b.slice(i)].join("/");
|
|
414
|
-
}
|
|
415
|
-
function buildHeader(h) {
|
|
416
|
-
if (h && h.enabled === false) return "";
|
|
417
|
-
const text = h?.text?.trim();
|
|
418
|
-
const lines = text ? text.split(/\r?\n/).map((l) => `// ${l}`) : [
|
|
419
|
-
"// Generated by DRZL (@drzl/*)",
|
|
420
|
-
"// Generated output is granted to you under your project's license.",
|
|
421
|
-
"// You may use, copy, modify, and distribute without attribution."
|
|
422
|
-
];
|
|
423
|
-
return lines.join("\n") + "\n\n";
|
|
424
|
-
}
|
|
425
|
-
var import_validation_core2, import_validation_core3, TRPC_MAJOR, q, LIB_IMPORTS, LIB_USAGE, LIBS, cap, singularize, isIdent, BASE_MODULE, TRPCGenerator, index_default;
|
|
426
|
-
var init_dist = __esm({
|
|
427
|
-
"../generator-trpc/dist/index.js"() {
|
|
428
|
-
"use strict";
|
|
429
|
-
import_validation_core2 = require("@drzl/validation-core");
|
|
430
|
-
import_validation_core3 = require("@drzl/validation-core");
|
|
431
|
-
TRPC_MAJOR = 11;
|
|
432
|
-
q = (v) => JSON.stringify(v);
|
|
433
|
-
LIB_IMPORTS = {
|
|
434
|
-
zod: "import { z } from 'zod';",
|
|
435
|
-
valibot: "import * as v from 'valibot';",
|
|
436
|
-
arktype: "import { type } from 'arktype';"
|
|
437
|
-
};
|
|
438
|
-
LIB_USAGE = {
|
|
439
|
-
zod: /\bz\./,
|
|
440
|
-
valibot: /\bv\./,
|
|
441
|
-
arktype: /\btype\(/
|
|
442
|
-
};
|
|
443
|
-
LIBS = {
|
|
444
|
-
zod: {
|
|
445
|
-
number: "z.number()",
|
|
446
|
-
string: "z.string()",
|
|
447
|
-
boolean: "z.boolean()",
|
|
448
|
-
date: "z.date()",
|
|
449
|
-
unknown: "z.unknown()",
|
|
450
|
-
tuple: (n) => `z.tuple([${Array.from({ length: n }, () => "z.number()").join(", ")}])`,
|
|
451
|
-
numberObject: (fields) => `z.object({ ${fields.map((f) => `${f}: z.number()`).join(", ")} })`,
|
|
452
|
-
enum: (vals) => `z.enum([${vals.map(q).join(", ")}] as const)`,
|
|
453
|
-
nullable: (b) => `${b}.nullable()`,
|
|
454
|
-
optional: (b) => `${b}.optional()`,
|
|
455
|
-
object: (body) => `z.object({
|
|
456
|
-
${body}
|
|
457
|
-
})`,
|
|
458
|
-
objectInline: (body) => `z.object({ ${body} })`,
|
|
459
|
-
partialUpdate: (s) => `${s}.partial()`,
|
|
460
|
-
arrayOf: (s) => `z.array(${s})`,
|
|
461
|
-
nullableOf: (s) => `${s}.nullable()`,
|
|
462
|
-
booleanSchema: "z.boolean()"
|
|
463
|
-
},
|
|
464
|
-
valibot: {
|
|
465
|
-
number: "v.number()",
|
|
466
|
-
string: "v.string()",
|
|
467
|
-
boolean: "v.boolean()",
|
|
468
|
-
date: "v.date()",
|
|
469
|
-
unknown: "v.unknown()",
|
|
470
|
-
tuple: (n) => `v.tuple([${Array.from({ length: n }, () => "v.number()").join(", ")}])`,
|
|
471
|
-
numberObject: (fields) => `v.object({ ${fields.map((f) => `${f}: v.number()`).join(", ")} })`,
|
|
472
|
-
enum: (vals) => `v.picklist([${vals.map(q).join(", ")}] as const)`,
|
|
473
|
-
nullable: (b) => `v.nullable(${b})`,
|
|
474
|
-
optional: (b) => `v.optional(${b})`,
|
|
475
|
-
object: (body) => `v.object({
|
|
476
|
-
${body}
|
|
477
|
-
})`,
|
|
478
|
-
objectInline: (body) => `v.object({ ${body} })`,
|
|
479
|
-
arrayOf: (s) => `v.array(${s})`,
|
|
480
|
-
nullableOf: (s) => `v.nullable(${s})`,
|
|
481
|
-
booleanSchema: "v.boolean()"
|
|
482
|
-
},
|
|
483
|
-
arktype: {
|
|
484
|
-
number: "number",
|
|
485
|
-
string: "string",
|
|
486
|
-
boolean: "boolean",
|
|
487
|
-
date: "Date",
|
|
488
|
-
unknown: "unknown",
|
|
489
|
-
// The surrounding encode adds the quotes, so the union is built with the inner quoting
|
|
490
|
-
// ArkType expects. Emitting `'${...}'` here produces `''admin' | 'user''`, which does not parse.
|
|
491
|
-
enum: (vals) => vals.map((x) => `'${x.replace(/'/g, "\\'")}'`).join(" | "),
|
|
492
|
-
nullable: (b) => `(${b} | null)`,
|
|
493
|
-
optional: (b) => `${b}?`,
|
|
494
|
-
object: (body) => `type({
|
|
495
|
-
${body}
|
|
496
|
-
})`,
|
|
497
|
-
objectInline: (body) => `type({ ${body} })`,
|
|
498
|
-
fieldIsString: true,
|
|
499
|
-
arrayOf: (s) => `${s}.array()`,
|
|
500
|
-
nullableOf: (s) => `${s}.or('null')`,
|
|
501
|
-
booleanSchema: `type('boolean')`
|
|
502
|
-
}
|
|
503
|
-
};
|
|
504
|
-
cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
505
|
-
singularize = (s) => s.endsWith("ies") ? s.slice(0, -3) + "y" : s.endsWith("s") ? s.slice(0, -1) : s;
|
|
506
|
-
isIdent = (s) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s);
|
|
507
|
-
BASE_MODULE = "trpc";
|
|
508
|
-
TRPCGenerator = class {
|
|
509
|
-
constructor(analysis) {
|
|
510
|
-
this.analysis = analysis;
|
|
511
|
-
}
|
|
512
|
-
async generate(opts) {
|
|
513
|
-
const fs6 = (0, import_validation_core2.fileWriter)(opts.fileSink);
|
|
514
|
-
const path9 = await import("path");
|
|
515
|
-
const out = path9.resolve(process.cwd(), opts.outputDir);
|
|
516
|
-
const ctx = {
|
|
517
|
-
out,
|
|
518
|
-
services: path9.resolve(process.cwd(), opts.servicesDir ?? "src/services")
|
|
519
|
-
};
|
|
520
|
-
await fs6.mkdir(out, { recursive: true });
|
|
521
|
-
const files = [];
|
|
522
|
-
const write = async (filePath, content) => {
|
|
523
|
-
const formatted = await (0, import_validation_core3.formatCode)(
|
|
524
|
-
buildHeader(opts.outputHeader) + content,
|
|
525
|
-
filePath,
|
|
526
|
-
opts.format
|
|
527
|
-
);
|
|
528
|
-
await fs6.writeFile(filePath, formatted, "utf8");
|
|
529
|
-
files.push(filePath);
|
|
530
|
-
};
|
|
531
|
-
const basePath = path9.join(out, `${BASE_MODULE}.ts`);
|
|
532
|
-
await write(basePath, renderBase(opts));
|
|
533
|
-
const routers = [];
|
|
534
|
-
const total = this.analysis.tables.length;
|
|
535
|
-
let index = 0;
|
|
536
|
-
for (const table of this.analysis.tables) {
|
|
537
|
-
const base = `${table.tsName}${opts.naming?.routerSuffix ?? ""}`;
|
|
538
|
-
const filePath = path9.join(out, `${toCase(base, opts.naming?.procedureCase)}.ts`);
|
|
539
|
-
if (filePath === basePath) {
|
|
540
|
-
throw new Error(
|
|
541
|
-
`@drzl/generator-trpc: the router for table "${table.name}" would be written to ${filePath}, which is the shared tRPC base module this generator also writes. Set naming.routerSuffix to move it out of the way.`
|
|
542
|
-
);
|
|
543
|
-
}
|
|
544
|
-
await write(filePath, renderRouter(table, opts, ctx));
|
|
545
|
-
routers.push({ table, filePath, exportName: routerExportName(table, opts.naming) });
|
|
546
|
-
index++;
|
|
547
|
-
opts.onProgress?.({ index, total, table: table.name, filePath });
|
|
548
|
-
}
|
|
549
|
-
await write(path9.join(out, "index.ts"), renderBarrel(routers, ctx, path9, opts));
|
|
550
|
-
return { files };
|
|
551
|
-
}
|
|
552
|
-
};
|
|
553
|
-
index_default = TRPCGenerator;
|
|
554
|
-
}
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
// ../generator-hono/dist/index.js
|
|
558
|
-
var dist_exports2 = {};
|
|
559
|
-
__export(dist_exports2, {
|
|
560
|
-
APP_MODULE: () => APP_MODULE,
|
|
561
|
-
HonoGenerator: () => HonoGenerator,
|
|
562
|
-
default: () => index_default2
|
|
563
|
-
});
|
|
564
|
-
function keyColumns2(table) {
|
|
565
|
-
const names = table.primaryKey?.columns ?? [];
|
|
566
|
-
if (!names.length) return null;
|
|
567
|
-
const cols = names.map((n) => table.columns.find((c) => c.name === n));
|
|
568
|
-
if (cols.some((c) => !c)) return null;
|
|
569
|
-
return cols;
|
|
570
|
-
}
|
|
571
|
-
function isWide2(column) {
|
|
572
|
-
if (column.enumValues && column.enumValues.length) return false;
|
|
573
|
-
if (column.shape?.kind === "tuple" || column.shape?.kind === "numberObject") return false;
|
|
574
|
-
return !["number", "string", "boolean", "Date"].includes(column.tsType);
|
|
575
|
-
}
|
|
576
|
-
function mapExpr2(column, lib, mode) {
|
|
577
|
-
const d = LIBS2[lib];
|
|
578
|
-
let base = (() => {
|
|
579
|
-
if (column.enumValues && column.enumValues.length) return d.enum(column.enumValues);
|
|
580
|
-
switch (column.tsType) {
|
|
581
|
-
case "number":
|
|
582
|
-
return d.number;
|
|
583
|
-
case "string":
|
|
584
|
-
return d.string;
|
|
585
|
-
case "boolean":
|
|
586
|
-
return d.boolean;
|
|
587
|
-
case "Date":
|
|
588
|
-
return d.date;
|
|
589
|
-
default:
|
|
590
|
-
return d.unknown;
|
|
591
|
-
}
|
|
592
|
-
})();
|
|
593
|
-
if (column.nullable) base = d.nullable(base);
|
|
594
|
-
if (mode !== "select") {
|
|
595
|
-
const optional = mode === "update" || column.nullable || column.hasDefault;
|
|
596
|
-
if (optional) base = d.optional(base);
|
|
597
|
-
}
|
|
598
|
-
return base;
|
|
599
|
-
}
|
|
600
|
-
function objectKey2(name) {
|
|
601
|
-
return isIdent2(name) ? name : JSON.stringify(name);
|
|
602
|
-
}
|
|
603
|
-
function field2(column, lib, mode) {
|
|
604
|
-
const d = LIBS2[lib];
|
|
605
|
-
const expr = mapExpr2(column, lib, mode);
|
|
606
|
-
return `${objectKey2(column.name)}: ${d.fieldIsString ? JSON.stringify(expr) : expr}`;
|
|
607
|
-
}
|
|
608
|
-
function paramField(column, lib) {
|
|
609
|
-
const d = LIBS2[lib];
|
|
610
|
-
const expr = (() => {
|
|
611
|
-
if (column.enumValues && column.enumValues.length) return d.enum(column.enumValues);
|
|
612
|
-
return d.coerce(column.tsType) ?? d.string;
|
|
613
|
-
})();
|
|
614
|
-
return `${objectKey2(column.name)}: ${d.fieldIsString ? JSON.stringify(expr) : expr}`;
|
|
615
|
-
}
|
|
616
|
-
function renderSchema2(table, lib, mode) {
|
|
617
|
-
const d = LIBS2[lib];
|
|
618
|
-
const cols = table.columns.filter((c) => mode === "select" ? true : !c.isGenerated);
|
|
619
|
-
const body = cols.map((c) => ` ${field2(c, lib, mode)},`).join("\n");
|
|
620
|
-
const schema = d.object(body);
|
|
621
|
-
return mode === "update" && d.partialUpdate ? d.partialUpdate(schema) : schema;
|
|
622
|
-
}
|
|
623
|
-
function toCase2(s, c) {
|
|
624
|
-
if (!c) return s;
|
|
625
|
-
const parts = s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]/g, " ").split(/\s+/);
|
|
626
|
-
if (c === "camel") {
|
|
627
|
-
return parts.map(
|
|
628
|
-
(p, i) => i === 0 ? p.toLowerCase() : p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()
|
|
629
|
-
).join("");
|
|
630
|
-
}
|
|
631
|
-
if (c === "kebab") return parts.map((p) => p.toLowerCase()).join("-");
|
|
632
|
-
if (c === "snake") return parts.map((p) => p.toLowerCase()).join("_");
|
|
633
|
-
return s;
|
|
634
|
-
}
|
|
635
|
-
function routesExportName(table, naming) {
|
|
636
|
-
const base = `${table.tsName}${naming?.routerSuffix ?? "Routes"}`;
|
|
637
|
-
const c = naming?.procedureCase;
|
|
638
|
-
return toCase2(base, c === "kebab" ? "camel" : c);
|
|
639
|
-
}
|
|
640
|
-
function mountPath(table, naming) {
|
|
641
|
-
return `/${toCase2(table.tsName, naming?.procedureCase)}`;
|
|
642
|
-
}
|
|
643
|
-
function buildHeader2(h) {
|
|
644
|
-
if (h && h.enabled === false) return "";
|
|
645
|
-
const text = h?.text?.trim();
|
|
646
|
-
const lines = text ? text.split(/\r?\n/).map((l) => `// ${l}`) : [
|
|
647
|
-
"// Generated by DRZL (@drzl/*)",
|
|
648
|
-
"// Generated output is granted to you under your project's license.",
|
|
649
|
-
"// You may use, copy, modify, and distribute without attribution."
|
|
650
|
-
];
|
|
651
|
-
return lines.join("\n") + "\n\n";
|
|
652
|
-
}
|
|
653
|
-
function renderRoutes(table, opts, ctx) {
|
|
654
|
-
const lib = opts.validation?.library ?? "zod";
|
|
655
|
-
const d = LIBS2[lib];
|
|
656
|
-
const validator = VALIDATORS[opts.validator ?? "standard"];
|
|
657
|
-
const insertName = `Insert${table.tsName}Schema`;
|
|
658
|
-
const updateName = `Update${table.tsName}Schema`;
|
|
659
|
-
const selectName = `Select${table.tsName}Schema`;
|
|
660
|
-
const paramsName = `${cap2(table.tsName)}ParamsSchema`;
|
|
661
|
-
const rowType = `Select${table.tsName}Row`;
|
|
662
|
-
const writable = !table.readOnly;
|
|
663
|
-
const key = keyColumns2(table);
|
|
664
|
-
const routes = [];
|
|
665
|
-
const notImplemented = (what) => `throw new Error('Not implemented: ${what} ${table.tsName}.');`;
|
|
666
|
-
routes.push({
|
|
667
|
-
name: "list",
|
|
668
|
-
method: "get",
|
|
669
|
-
path: "/",
|
|
670
|
-
middleware: [],
|
|
671
|
-
// The stub states its own contract. tRPC has `.output()` and Hono has nothing like it: what a
|
|
672
|
-
// Hono client infers is the *handler's return type*, so the only place an output schema can be
|
|
673
|
-
// honoured is the value handed to `c.json`. Annotating the local is what puts the select shape
|
|
674
|
-
// into `hc<AppType>()` rather than `never[]`.
|
|
675
|
-
body: [`const rows: ${rowType}[] = [];`, "return c.json(rows);"]
|
|
676
|
-
});
|
|
677
|
-
if (key) {
|
|
678
|
-
const keyPath = "/" + key.map((c) => `:${c.name}`).join("/");
|
|
679
|
-
routes.push({
|
|
680
|
-
name: "byId",
|
|
681
|
-
method: "get",
|
|
682
|
-
path: keyPath,
|
|
683
|
-
middleware: [`${validator.fn}('param', ${paramsName})`],
|
|
684
|
-
body: [VALID_PARAM_HINT, `const row: ${rowType} | null = null;`, "return c.json(row);"]
|
|
685
|
-
});
|
|
686
|
-
if (writable) {
|
|
687
|
-
routes.push({
|
|
688
|
-
name: "update",
|
|
689
|
-
method: "patch",
|
|
690
|
-
path: keyPath,
|
|
691
|
-
middleware: [
|
|
692
|
-
`${validator.fn}('param', ${paramsName})`,
|
|
693
|
-
`${validator.fn}('json', ${updateName})`
|
|
694
|
-
],
|
|
695
|
-
body: [notImplemented("update")]
|
|
696
|
-
});
|
|
697
|
-
routes.push({
|
|
698
|
-
name: "delete",
|
|
699
|
-
method: "delete",
|
|
700
|
-
path: keyPath,
|
|
701
|
-
middleware: [`${validator.fn}('param', ${paramsName})`],
|
|
702
|
-
body: [VALID_PARAM_HINT, "return c.json(true);"]
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
if (writable) {
|
|
707
|
-
routes.push({
|
|
708
|
-
name: "create",
|
|
709
|
-
method: "post",
|
|
710
|
-
path: "/",
|
|
711
|
-
middleware: [`${validator.fn}('json', ${insertName})`],
|
|
712
|
-
body: [notImplemented("create")]
|
|
713
|
-
});
|
|
714
|
-
}
|
|
715
|
-
if (opts.includeRelations) {
|
|
716
|
-
routes.push(...relationRoutes(table, lib, rowType, validator.fn, opts));
|
|
717
|
-
}
|
|
718
|
-
const order = ["list", "byId", "create", "update", "delete"];
|
|
719
|
-
const rank = (n) => order.indexOf(n) === -1 ? order.length : order.indexOf(n);
|
|
720
|
-
routes.sort((a, b) => rank(a.name) - rank(b.name));
|
|
721
|
-
const exportName = routesExportName(table, opts.naming);
|
|
722
|
-
const chain = routes.map((r) => {
|
|
723
|
-
const args = [lit(r.path), ...r.middleware].join(", ");
|
|
724
|
-
const ctxParam = r.body.some((line) => /\bc\./.test(line)) ? "c" : "_c";
|
|
725
|
-
return [
|
|
726
|
-
` .${r.method}(${args}, async (${ctxParam}) => {`,
|
|
727
|
-
...r.body.map((line) => ` ${line}`),
|
|
728
|
-
` })`
|
|
729
|
-
].join("\n");
|
|
730
|
-
}).join("\n");
|
|
731
|
-
const body = `export const ${exportName} = new Hono()
|
|
732
|
-
${chain};
|
|
733
|
-
`;
|
|
734
|
-
const useShared = !!opts.validation?.useShared && !!opts.validation?.importPath;
|
|
735
|
-
const declared = [];
|
|
736
|
-
if (!useShared) {
|
|
737
|
-
if (writable) {
|
|
738
|
-
declared.push(`export const ${insertName} = ${renderSchema2(table, lib, "insert")};`);
|
|
739
|
-
declared.push(`export const ${updateName} = ${renderSchema2(table, lib, "update")};`);
|
|
740
|
-
}
|
|
741
|
-
declared.push(`export const ${selectName} = ${renderSchema2(table, lib, "select")};`);
|
|
742
|
-
}
|
|
743
|
-
if (key) {
|
|
744
|
-
declared.push(
|
|
745
|
-
`export const ${paramsName} = ${d.objectInline(
|
|
746
|
-
key.map((c) => paramField(c, lib)).join(", ")
|
|
747
|
-
)};`
|
|
748
|
-
);
|
|
749
|
-
}
|
|
750
|
-
declared.push(`export type ${rowType} = ${d.infer(selectName)};`);
|
|
751
|
-
const decided = [...declared, body].join("\n\n");
|
|
752
|
-
const imports = [];
|
|
753
|
-
if (useShared) {
|
|
754
|
-
const sharedAffix = (0, import_validation_core5.resolveAffix)({
|
|
755
|
-
affix: opts.validation?.affix,
|
|
756
|
-
schemaSuffix: opts.validation?.schemaSuffix
|
|
757
|
-
});
|
|
758
|
-
const wanted = [
|
|
759
|
-
["insert", insertName],
|
|
760
|
-
["update", updateName],
|
|
761
|
-
["select", selectName]
|
|
762
|
-
].filter(([, local]) => decided.includes(local));
|
|
763
|
-
if (wanted.length) {
|
|
764
|
-
const spec = (0, import_validation_core5.resolveConfiguredImport)(
|
|
765
|
-
opts.validation.importPath,
|
|
766
|
-
ctx.out,
|
|
767
|
-
process.cwd(),
|
|
768
|
-
opts.importExtension
|
|
769
|
-
);
|
|
770
|
-
const names = wanted.map(([mode, local]) => {
|
|
771
|
-
const exported = (0, import_validation_core5.schemaName)(mode, table.tsName, sharedAffix);
|
|
772
|
-
return exported === local ? local : `${exported} as ${local}`;
|
|
773
|
-
}).join(", ");
|
|
774
|
-
imports.push(`import { ${names} } from '${spec}';`);
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
imports.push(`import { Hono } from 'hono';`);
|
|
778
|
-
if (decided.includes(`${validator.fn}(`)) {
|
|
779
|
-
imports.push(`import { ${validator.fn} } from '${validator.from}';`);
|
|
780
|
-
}
|
|
781
|
-
if (LIB_USAGE2[lib].test(decided)) imports.unshift(LIB_IMPORTS2[lib]);
|
|
782
|
-
const wide = table.columns.filter(isWide2).map((c) => c.name);
|
|
783
|
-
const wideNote = wide.length ? `// No validated type for ${wide.length === 1 ? "this column" : "these columns"}: ${wide.join(", ")}.
|
|
784
|
-
// DRZL could not derive one from the schema, so these routes accept any value there.
|
|
785
|
-
` : "";
|
|
786
|
-
return `// Generated by @drzl/generator-hono
|
|
787
|
-
// Routes for table: ${table.name}
|
|
788
|
-
${wideNote}${imports.join("\n")}
|
|
789
|
-
|
|
790
|
-
${decided}`;
|
|
791
|
-
}
|
|
792
|
-
function relationRoutes(table, lib, rowType, validatorFn, opts) {
|
|
793
|
-
const d = LIBS2[lib];
|
|
794
|
-
const out = [];
|
|
795
|
-
const taken = /* @__PURE__ */ new Set();
|
|
796
|
-
for (const fk of table.foreignKeys ?? []) {
|
|
797
|
-
if (fk.columns.length !== 1) continue;
|
|
798
|
-
const colName = fk.columns[0];
|
|
799
|
-
const column = table.columns.find((c) => c.name === colName);
|
|
800
|
-
if (!column) continue;
|
|
801
|
-
const segment = toCase2(`by-${colName}`, opts.naming?.procedureCase ?? "kebab");
|
|
802
|
-
if (taken.has(segment)) continue;
|
|
803
|
-
taken.add(segment);
|
|
804
|
-
const inline = d.objectInline(paramField(column, lib));
|
|
805
|
-
out.push({
|
|
806
|
-
name: `listBy${cap2(colName)}`,
|
|
807
|
-
method: "get",
|
|
808
|
-
path: `/${segment}/:${colName}`,
|
|
809
|
-
middleware: [`${validatorFn}('param', ${inline})`],
|
|
810
|
-
body: [VALID_PARAM_HINT, `const rows: ${rowType}[] = [];`, "return c.json(rows);"]
|
|
811
|
-
});
|
|
812
|
-
}
|
|
813
|
-
return out;
|
|
814
|
-
}
|
|
815
|
-
function renderBarrel2(modules, ctx, path9, opts) {
|
|
816
|
-
if (!modules.length) {
|
|
817
|
-
return `// Generated by @drzl/generator-hono
|
|
818
|
-
// No tables detected in analysis. Add tables to your schema and regenerate.
|
|
819
|
-
import { Hono } from 'hono';
|
|
820
|
-
|
|
821
|
-
export const app = new Hono();
|
|
822
|
-
|
|
823
|
-
/** The type a Hono client is parameterised by: \`hc<AppType>('/')\`. */
|
|
824
|
-
export type AppType = typeof app;
|
|
825
|
-
`;
|
|
826
|
-
}
|
|
827
|
-
const entries = modules.map(({ filePath, exportName, table }) => ({
|
|
828
|
-
rel: (0, import_validation_core5.importSpecifier)(
|
|
829
|
-
"./" + path9.relative(ctx.out, filePath).replace(/\\/g, "/"),
|
|
830
|
-
opts.importExtension
|
|
831
|
-
),
|
|
832
|
-
exportName,
|
|
833
|
-
mount: mountPath(table, opts.naming)
|
|
834
|
-
}));
|
|
835
|
-
const imports = entries.map((e) => `import { ${e.exportName} } from '${e.rel}';`).join("\n");
|
|
836
|
-
const chain = entries.map((e) => ` .route(${lit(e.mount)}, ${e.exportName})`).join("\n");
|
|
837
|
-
const reExports = entries.map((e) => `export * from '${e.rel}';`).join("\n");
|
|
838
|
-
return `// Generated by @drzl/generator-hono
|
|
839
|
-
import { Hono } from 'hono';
|
|
840
|
-
${imports}
|
|
841
|
-
|
|
842
|
-
export const app = new Hono()
|
|
843
|
-
${chain};
|
|
844
|
-
|
|
845
|
-
/** The type a Hono client is parameterised by: \`hc<AppType>('/')\`. */
|
|
846
|
-
export type AppType = typeof app;
|
|
847
|
-
|
|
848
|
-
${reExports}
|
|
849
|
-
`;
|
|
850
|
-
}
|
|
851
|
-
var import_validation_core4, import_validation_core5, APP_MODULE, q2, lit, NUMERIC_SEGMENT, LIB_IMPORTS2, LIB_USAGE2, LIBS2, cap2, isIdent2, HonoGenerator, index_default2, VALIDATORS, VALID_PARAM_HINT;
|
|
852
|
-
var init_dist2 = __esm({
|
|
853
|
-
"../generator-hono/dist/index.js"() {
|
|
854
|
-
"use strict";
|
|
855
|
-
import_validation_core4 = require("@drzl/validation-core");
|
|
856
|
-
import_validation_core5 = require("@drzl/validation-core");
|
|
857
|
-
APP_MODULE = "index";
|
|
858
|
-
q2 = (v) => JSON.stringify(v);
|
|
859
|
-
lit = (v) => /['\\]/.test(v) ? JSON.stringify(v) : `'${v}'`;
|
|
860
|
-
NUMERIC_SEGMENT = String.raw`/^-?\d+(\.\d+)?$/`;
|
|
861
|
-
LIB_IMPORTS2 = {
|
|
862
|
-
zod: "import { z } from 'zod';",
|
|
863
|
-
valibot: "import * as v from 'valibot';",
|
|
864
|
-
arktype: "import { type } from 'arktype';"
|
|
865
|
-
};
|
|
866
|
-
LIB_USAGE2 = {
|
|
867
|
-
zod: /\bz\./,
|
|
868
|
-
valibot: /\bv\./,
|
|
869
|
-
arktype: /\btype\(|\.infer\b/
|
|
870
|
-
};
|
|
871
|
-
LIBS2 = {
|
|
872
|
-
zod: {
|
|
873
|
-
number: "z.number()",
|
|
874
|
-
string: "z.string()",
|
|
875
|
-
boolean: "z.boolean()",
|
|
876
|
-
date: "z.date()",
|
|
877
|
-
unknown: "z.unknown()",
|
|
878
|
-
enum: (vals) => `z.enum([${vals.map(q2).join(", ")}] as const)`,
|
|
879
|
-
nullable: (b) => `${b}.nullable()`,
|
|
880
|
-
optional: (b) => `${b}.optional()`,
|
|
881
|
-
object: (body) => `z.object({
|
|
882
|
-
${body}
|
|
883
|
-
})`,
|
|
884
|
-
objectInline: (body) => `z.object({ ${body} })`,
|
|
885
|
-
partialUpdate: (s) => `${s}.partial()`,
|
|
886
|
-
// Not `z.coerce.number()`, and not `z.coerce.date()`: both accept far more than a path
|
|
887
|
-
// segment addressing a row should. See the measured grid on `LibDialect.coerce`.
|
|
888
|
-
coerce: (t) => t === "number" ? `z.string().regex(${NUMERIC_SEGMENT}).transform(Number)` : t === "Date" ? "z.iso.datetime().transform((s) => new Date(s))" : null,
|
|
889
|
-
infer: (s) => `z.output<typeof ${s}>`
|
|
890
|
-
},
|
|
891
|
-
valibot: {
|
|
892
|
-
number: "v.number()",
|
|
893
|
-
string: "v.string()",
|
|
894
|
-
boolean: "v.boolean()",
|
|
895
|
-
date: "v.date()",
|
|
896
|
-
unknown: "v.unknown()",
|
|
897
|
-
enum: (vals) => `v.picklist([${vals.map(q2).join(", ")}] as const)`,
|
|
898
|
-
nullable: (b) => `v.nullable(${b})`,
|
|
899
|
-
optional: (b) => `v.optional(${b})`,
|
|
900
|
-
object: (body) => `v.object({
|
|
901
|
-
${body}
|
|
902
|
-
})`,
|
|
903
|
-
objectInline: (body) => `v.object({ ${body} })`,
|
|
904
|
-
// A valibot pipe step sees the previous step's *output*, so the check has to happen while the
|
|
905
|
-
// value is still the string: by the time a `v.transform(Number)` has run there is no string
|
|
906
|
-
// left to look at. See the measured grid on `LibDialect.coerce`.
|
|
907
|
-
coerce: (t) => t === "number" ? `v.pipe(v.string(), v.regex(${NUMERIC_SEGMENT}), v.transform(Number))` : t === "Date" ? "v.pipe(v.string(), v.isoTimestamp(), v.transform((s) => new Date(s)))" : null,
|
|
908
|
-
infer: (s) => `v.InferOutput<typeof ${s}>`
|
|
909
|
-
},
|
|
910
|
-
arktype: {
|
|
911
|
-
number: "number",
|
|
912
|
-
string: "string",
|
|
913
|
-
boolean: "boolean",
|
|
914
|
-
date: "Date",
|
|
915
|
-
unknown: "unknown",
|
|
916
|
-
// The surrounding encode adds the quotes, so the union is built with the inner quoting
|
|
917
|
-
// ArkType expects.
|
|
918
|
-
enum: (vals) => vals.map((x) => `'${x.replace(/'/g, "\\'")}'`).join(" | "),
|
|
919
|
-
nullable: (b) => `(${b} | null)`,
|
|
920
|
-
optional: (b) => `${b}?`,
|
|
921
|
-
object: (body) => `type({
|
|
922
|
-
${body}
|
|
923
|
-
})`,
|
|
924
|
-
objectInline: (body) => `type({ ${body} })`,
|
|
925
|
-
fieldIsString: true,
|
|
926
|
-
// ArkType ships these as keywords, and they are *morphs*: the declared output type is
|
|
927
|
-
// `number`, not `string`. Returned bare, because `fieldIsString` quotes every expression this
|
|
928
|
-
// dialect produces and a keyword returned pre-quoted arrives as `"'string.numeric.parse'"`,
|
|
929
|
-
// which ArkType reads as a string *literal* type matching nothing but that sentence.
|
|
930
|
-
//
|
|
931
|
-
// `string.date.parse` and not `string.date.iso.parse` was the first draft, and it accepts
|
|
932
|
-
// `"1"` as the year 2001, which is the same over-permissiveness the coercing spellings have.
|
|
933
|
-
coerce: (t) => t === "number" ? "string.numeric.parse" : t === "Date" ? "string.date.iso.parse" : null,
|
|
934
|
-
infer: (s) => `typeof ${s}.infer`
|
|
935
|
-
}
|
|
936
|
-
};
|
|
937
|
-
cap2 = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
938
|
-
isIdent2 = (s) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s);
|
|
939
|
-
HonoGenerator = class {
|
|
940
|
-
constructor(analysis) {
|
|
941
|
-
this.analysis = analysis;
|
|
942
|
-
}
|
|
943
|
-
async generate(opts) {
|
|
944
|
-
const fs6 = (0, import_validation_core4.fileWriter)(opts.fileSink);
|
|
945
|
-
const path9 = await import("path");
|
|
946
|
-
const out = path9.resolve(process.cwd(), opts.outputDir);
|
|
947
|
-
const ctx = { out };
|
|
948
|
-
await fs6.mkdir(out, { recursive: true });
|
|
949
|
-
const files = [];
|
|
950
|
-
const write = async (filePath, content) => {
|
|
951
|
-
const formatted = await (0, import_validation_core5.formatCode)(
|
|
952
|
-
buildHeader2(opts.outputHeader) + content,
|
|
953
|
-
filePath,
|
|
954
|
-
opts.format
|
|
955
|
-
);
|
|
956
|
-
await fs6.writeFile(filePath, formatted, "utf8");
|
|
957
|
-
files.push(filePath);
|
|
958
|
-
};
|
|
959
|
-
const barrelPath = path9.join(out, `${APP_MODULE}.ts`);
|
|
960
|
-
const modules = [];
|
|
961
|
-
const total = this.analysis.tables.length;
|
|
962
|
-
let index = 0;
|
|
963
|
-
for (const table of this.analysis.tables) {
|
|
964
|
-
const base = `${table.tsName}${opts.naming?.routerSuffix ?? ""}`;
|
|
965
|
-
const filePath = path9.join(out, `${toCase2(base, opts.naming?.procedureCase)}.ts`);
|
|
966
|
-
if (filePath === barrelPath) {
|
|
967
|
-
throw new Error(
|
|
968
|
-
`@drzl/generator-hono: the routes for table "${table.name}" would be written to ${filePath}, which is the barrel this generator also writes. Set naming.routerSuffix to move it out of the way.`
|
|
969
|
-
);
|
|
970
|
-
}
|
|
971
|
-
await write(filePath, renderRoutes(table, opts, ctx));
|
|
972
|
-
modules.push({ table, filePath, exportName: routesExportName(table, opts.naming) });
|
|
973
|
-
index++;
|
|
974
|
-
opts.onProgress?.({ index, total, table: table.name, filePath });
|
|
975
|
-
}
|
|
976
|
-
await write(barrelPath, renderBarrel2(modules, ctx, path9, opts));
|
|
977
|
-
return { files };
|
|
978
|
-
}
|
|
979
|
-
};
|
|
980
|
-
index_default2 = HonoGenerator;
|
|
981
|
-
VALIDATORS = {
|
|
982
|
-
standard: { fn: "sValidator", from: "@hono/standard-validator" },
|
|
983
|
-
zod: { fn: "zValidator", from: "@hono/zod-validator" }
|
|
984
|
-
};
|
|
985
|
-
VALID_PARAM_HINT = "// The validated path parameters are at c.req.valid('param').";
|
|
986
|
-
}
|
|
987
|
-
});
|
|
988
|
-
|
|
989
|
-
// ../generator-express/dist/index.js
|
|
990
|
-
var dist_exports3 = {};
|
|
991
|
-
__export(dist_exports3, {
|
|
992
|
-
APP_MODULE: () => APP_MODULE2,
|
|
993
|
-
ExpressGenerator: () => ExpressGenerator,
|
|
994
|
-
VALIDATION_MODULE: () => VALIDATION_MODULE,
|
|
995
|
-
default: () => index_default3
|
|
996
|
-
});
|
|
997
|
-
function keyColumns3(table) {
|
|
998
|
-
const names = table.primaryKey?.columns ?? [];
|
|
999
|
-
if (!names.length) return null;
|
|
1000
|
-
const cols = names.map((n) => table.columns.find((c) => c.name === n));
|
|
1001
|
-
if (cols.some((c) => !c)) return null;
|
|
1002
|
-
return cols;
|
|
1003
|
-
}
|
|
1004
|
-
function isWide3(column) {
|
|
1005
|
-
if (column.enumValues && column.enumValues.length) return false;
|
|
1006
|
-
if (column.shape?.kind === "tuple" || column.shape?.kind === "numberObject") return false;
|
|
1007
|
-
return !["number", "string", "boolean", "Date"].includes(column.tsType);
|
|
1008
|
-
}
|
|
1009
|
-
function mapExpr3(column, lib, mode) {
|
|
1010
|
-
const d = LIBS3[lib];
|
|
1011
|
-
let base = (() => {
|
|
1012
|
-
if (column.enumValues && column.enumValues.length) return d.enum(column.enumValues);
|
|
1013
|
-
switch (column.tsType) {
|
|
1014
|
-
case "number":
|
|
1015
|
-
return d.number;
|
|
1016
|
-
case "string":
|
|
1017
|
-
return d.string;
|
|
1018
|
-
case "boolean":
|
|
1019
|
-
return d.boolean;
|
|
1020
|
-
case "Date":
|
|
1021
|
-
return d.date;
|
|
1022
|
-
default:
|
|
1023
|
-
return d.unknown;
|
|
1024
|
-
}
|
|
1025
|
-
})();
|
|
1026
|
-
if (column.nullable) base = d.nullable(base);
|
|
1027
|
-
if (mode !== "select") {
|
|
1028
|
-
const optional = mode === "update" || column.nullable || column.hasDefault;
|
|
1029
|
-
if (optional) base = d.optional(base);
|
|
1030
|
-
}
|
|
1031
|
-
return base;
|
|
1032
|
-
}
|
|
1033
|
-
function objectKey3(name) {
|
|
1034
|
-
return isIdent3(name) ? name : JSON.stringify(name);
|
|
1035
|
-
}
|
|
1036
|
-
function field3(column, lib, mode) {
|
|
1037
|
-
const d = LIBS3[lib];
|
|
1038
|
-
const expr = mapExpr3(column, lib, mode);
|
|
1039
|
-
return `${objectKey3(column.name)}: ${d.fieldIsString ? JSON.stringify(expr) : expr}`;
|
|
1040
|
-
}
|
|
1041
|
-
function paramField2(column, lib) {
|
|
1042
|
-
const d = LIBS3[lib];
|
|
1043
|
-
const expr = (() => {
|
|
1044
|
-
if (column.enumValues && column.enumValues.length) return d.enum(column.enumValues);
|
|
1045
|
-
return d.coerce(column.tsType) ?? d.string;
|
|
1046
|
-
})();
|
|
1047
|
-
return `${objectKey3(column.name)}: ${d.fieldIsString ? JSON.stringify(expr) : expr}`;
|
|
1048
|
-
}
|
|
1049
|
-
function renderSchema3(table, lib, mode) {
|
|
1050
|
-
const d = LIBS3[lib];
|
|
1051
|
-
const cols = table.columns.filter((c) => mode === "select" ? true : !c.isGenerated);
|
|
1052
|
-
const body = cols.map((c) => ` ${field3(c, lib, mode)},`).join("\n");
|
|
1053
|
-
const schema = d.object(body);
|
|
1054
|
-
return mode === "update" && d.partialUpdate ? d.partialUpdate(schema) : schema;
|
|
1055
|
-
}
|
|
1056
|
-
function toCase3(s, c) {
|
|
1057
|
-
if (!c) return s;
|
|
1058
|
-
const parts = s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]/g, " ").split(/\s+/);
|
|
1059
|
-
if (c === "camel") {
|
|
1060
|
-
return parts.map(
|
|
1061
|
-
(p, i) => i === 0 ? p.toLowerCase() : p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()
|
|
1062
|
-
).join("");
|
|
1063
|
-
}
|
|
1064
|
-
if (c === "kebab") return parts.map((p) => p.toLowerCase()).join("-");
|
|
1065
|
-
if (c === "snake") return parts.map((p) => p.toLowerCase()).join("_");
|
|
1066
|
-
return s;
|
|
1067
|
-
}
|
|
1068
|
-
function routesExportName2(table, naming) {
|
|
1069
|
-
const base = `${table.tsName}${naming?.routerSuffix ?? "Routes"}`;
|
|
1070
|
-
const c = naming?.procedureCase;
|
|
1071
|
-
return toCase3(base, c === "kebab" ? "camel" : c);
|
|
1072
|
-
}
|
|
1073
|
-
function mountPath2(table, naming) {
|
|
1074
|
-
return `/${toCase3(table.tsName, naming?.procedureCase)}`;
|
|
1075
|
-
}
|
|
1076
|
-
function buildHeader3(h) {
|
|
1077
|
-
if (h && h.enabled === false) return "";
|
|
1078
|
-
const text = h?.text?.trim();
|
|
1079
|
-
const lines = text ? text.split(/\r?\n/).map((l) => `// ${l}`) : [
|
|
1080
|
-
"// Generated by DRZL (@drzl/*)",
|
|
1081
|
-
"// Generated output is granted to you under your project's license.",
|
|
1082
|
-
"// You may use, copy, modify, and distribute without attribution."
|
|
1083
|
-
];
|
|
1084
|
-
return lines.join("\n") + "\n\n";
|
|
1085
|
-
}
|
|
1086
|
-
function renderValidationModule() {
|
|
1087
|
-
return `// Generated by @drzl/generator-express
|
|
1088
|
-
// A validation middleware over Standard Schema v1, so the same routes accept zod, valibot or
|
|
1089
|
-
// arktype schemas. On failure it answers 400 with { error, slot, issues: [{ message, path }] };
|
|
1090
|
-
// on success it replaces req[slot] with the parsed output and calls next().
|
|
1091
|
-
import type { RequestHandler } from 'express';
|
|
1092
|
-
|
|
1093
|
-
interface StandardIssue {
|
|
1094
|
-
readonly message: string;
|
|
1095
|
-
readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }> | undefined;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
type StandardResult<Output> =
|
|
1099
|
-
| { readonly value: Output; readonly issues?: undefined }
|
|
1100
|
-
| { readonly issues: ReadonlyArray<StandardIssue> };
|
|
1101
|
-
|
|
1102
|
-
export interface StandardSchema<Output = unknown> {
|
|
1103
|
-
readonly '~standard': {
|
|
1104
|
-
readonly version: 1;
|
|
1105
|
-
readonly vendor: string;
|
|
1106
|
-
readonly validate: (
|
|
1107
|
-
value: unknown
|
|
1108
|
-
) => StandardResult<Output> | Promise<StandardResult<Output>>;
|
|
1109
|
-
};
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
export function ${VALIDATE_FN}(slot: 'params' | 'body', schema: StandardSchema): RequestHandler {
|
|
1113
|
-
return async (req, res, next) => {
|
|
1114
|
-
const result = await schema['~standard'].validate(req[slot]);
|
|
1115
|
-
if (result.issues) {
|
|
1116
|
-
res.status(400).json({
|
|
1117
|
-
error: 'Validation failed',
|
|
1118
|
-
slot,
|
|
1119
|
-
issues: result.issues.map((issue) => ({
|
|
1120
|
-
message: issue.message,
|
|
1121
|
-
path: (issue.path ?? []).map((p) => (typeof p === 'object' && p !== null ? p.key : p)),
|
|
1122
|
-
})),
|
|
1123
|
-
});
|
|
1124
|
-
return;
|
|
1125
|
-
}
|
|
1126
|
-
req[slot] = result.value as never;
|
|
1127
|
-
next();
|
|
1128
|
-
};
|
|
1129
|
-
}
|
|
1130
|
-
`;
|
|
1131
|
-
}
|
|
1132
|
-
function renderRoutes2(table, opts, ctx) {
|
|
1133
|
-
const lib = opts.validation?.library ?? "zod";
|
|
1134
|
-
const d = LIBS3[lib];
|
|
1135
|
-
const insertName = `Insert${table.tsName}Schema`;
|
|
1136
|
-
const updateName = `Update${table.tsName}Schema`;
|
|
1137
|
-
const selectName = `Select${table.tsName}Schema`;
|
|
1138
|
-
const paramsName = `${cap3(table.tsName)}ParamsSchema`;
|
|
1139
|
-
const rowType = `Select${table.tsName}Row`;
|
|
1140
|
-
const writable = !table.readOnly;
|
|
1141
|
-
const key = keyColumns3(table);
|
|
1142
|
-
const routes = [];
|
|
1143
|
-
const notImplemented = (what) => `throw new Error('Not implemented: ${what} ${table.tsName}.');`;
|
|
1144
|
-
routes.push({
|
|
1145
|
-
name: "list",
|
|
1146
|
-
method: "get",
|
|
1147
|
-
path: "/",
|
|
1148
|
-
middleware: [],
|
|
1149
|
-
resType: `${rowType}[]`,
|
|
1150
|
-
// The stub states its own contract twice over: the annotated local is what a reader sees,
|
|
1151
|
-
// and the annotated res parameter is what Express's own types hold the handler to. Nothing
|
|
1152
|
-
// infers a client from either, which the docs say plainly; the types are for the person
|
|
1153
|
-
// filling the stub in.
|
|
1154
|
-
body: [`const rows: ${rowType}[] = [];`, "res.json(rows);"]
|
|
1155
|
-
});
|
|
1156
|
-
if (key) {
|
|
1157
|
-
const keyPath = "/" + key.map((c) => `:${c.name}`).join("/");
|
|
1158
|
-
routes.push({
|
|
1159
|
-
name: "byId",
|
|
1160
|
-
method: "get",
|
|
1161
|
-
path: keyPath,
|
|
1162
|
-
middleware: [`${VALIDATE_FN}('params', ${paramsName})`],
|
|
1163
|
-
resType: `${rowType} | null`,
|
|
1164
|
-
body: [VALID_PARAM_HINT2, `const row: ${rowType} | null = null;`, "res.json(row);"]
|
|
1165
|
-
});
|
|
1166
|
-
if (writable) {
|
|
1167
|
-
routes.push({
|
|
1168
|
-
name: "update",
|
|
1169
|
-
method: "patch",
|
|
1170
|
-
path: keyPath,
|
|
1171
|
-
middleware: [
|
|
1172
|
-
`${VALIDATE_FN}('params', ${paramsName})`,
|
|
1173
|
-
"json()",
|
|
1174
|
-
`${VALIDATE_FN}('body', ${updateName})`
|
|
1175
|
-
],
|
|
1176
|
-
resType: rowType,
|
|
1177
|
-
body: [notImplemented("update")]
|
|
1178
|
-
});
|
|
1179
|
-
routes.push({
|
|
1180
|
-
name: "delete",
|
|
1181
|
-
method: "delete",
|
|
1182
|
-
path: keyPath,
|
|
1183
|
-
middleware: [`${VALIDATE_FN}('params', ${paramsName})`],
|
|
1184
|
-
resType: "boolean",
|
|
1185
|
-
body: [VALID_PARAM_HINT2, "res.json(true);"]
|
|
1186
|
-
});
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
if (writable) {
|
|
1190
|
-
routes.push({
|
|
1191
|
-
name: "create",
|
|
1192
|
-
method: "post",
|
|
1193
|
-
path: "/",
|
|
1194
|
-
middleware: ["json()", `${VALIDATE_FN}('body', ${insertName})`],
|
|
1195
|
-
resType: rowType,
|
|
1196
|
-
body: [notImplemented("create")]
|
|
1197
|
-
});
|
|
1198
|
-
}
|
|
1199
|
-
if (opts.includeRelations) {
|
|
1200
|
-
routes.push(...relationRoutes2(table, lib, rowType, opts));
|
|
1201
|
-
}
|
|
1202
|
-
const order = ["list", "byId", "create", "update", "delete"];
|
|
1203
|
-
const rank = (n) => order.indexOf(n) === -1 ? order.length : order.indexOf(n);
|
|
1204
|
-
routes.sort((a, b) => rank(a.name) - rank(b.name));
|
|
1205
|
-
const exportName = routesExportName2(table, opts.naming);
|
|
1206
|
-
const statements = routes.map((r) => {
|
|
1207
|
-
const args = [lit2(r.path), ...r.middleware].join(", ");
|
|
1208
|
-
const reads = (what) => r.body.some((line) => !line.startsWith("//") && what.test(line));
|
|
1209
|
-
const reqParam = reads(/\breq\./) ? "req" : "_req";
|
|
1210
|
-
const resParam = reads(/\bres\./) ? "res" : "_res";
|
|
1211
|
-
return [
|
|
1212
|
-
`${exportName}.${r.method}(${args}, async (${reqParam}, ${resParam}: Response<${r.resType}>) => {`,
|
|
1213
|
-
...r.body.map((line) => ` ${line}`),
|
|
1214
|
-
`});`
|
|
1215
|
-
].join("\n");
|
|
1216
|
-
}).join("\n\n");
|
|
1217
|
-
const body = `export const ${exportName} = Router();
|
|
1218
|
-
|
|
1219
|
-
${statements}
|
|
1220
|
-
`;
|
|
1221
|
-
const useShared = !!opts.validation?.useShared && !!opts.validation?.importPath;
|
|
1222
|
-
const declared = [];
|
|
1223
|
-
if (!useShared) {
|
|
1224
|
-
if (writable) {
|
|
1225
|
-
declared.push(`export const ${insertName} = ${renderSchema3(table, lib, "insert")};`);
|
|
1226
|
-
declared.push(`export const ${updateName} = ${renderSchema3(table, lib, "update")};`);
|
|
1227
|
-
}
|
|
1228
|
-
declared.push(`export const ${selectName} = ${renderSchema3(table, lib, "select")};`);
|
|
1229
|
-
}
|
|
1230
|
-
if (key) {
|
|
1231
|
-
declared.push(
|
|
1232
|
-
`export const ${paramsName} = ${d.objectInline(
|
|
1233
|
-
key.map((c) => paramField2(c, lib)).join(", ")
|
|
1234
|
-
)};`
|
|
1235
|
-
);
|
|
1236
|
-
}
|
|
1237
|
-
declared.push(`export type ${rowType} = ${d.infer(selectName)};`);
|
|
1238
|
-
const decided = [...declared, body].join("\n\n");
|
|
1239
|
-
const imports = [];
|
|
1240
|
-
if (useShared) {
|
|
1241
|
-
const sharedAffix = (0, import_validation_core7.resolveAffix)({
|
|
1242
|
-
affix: opts.validation?.affix,
|
|
1243
|
-
schemaSuffix: opts.validation?.schemaSuffix
|
|
1244
|
-
});
|
|
1245
|
-
const wanted = [
|
|
1246
|
-
["insert", insertName],
|
|
1247
|
-
["update", updateName],
|
|
1248
|
-
["select", selectName]
|
|
1249
|
-
].filter(([, local]) => decided.includes(local));
|
|
1250
|
-
if (wanted.length) {
|
|
1251
|
-
const spec = (0, import_validation_core7.resolveConfiguredImport)(
|
|
1252
|
-
opts.validation.importPath,
|
|
1253
|
-
ctx.out,
|
|
1254
|
-
process.cwd(),
|
|
1255
|
-
opts.importExtension
|
|
1256
|
-
);
|
|
1257
|
-
const names = wanted.map(([mode, local]) => {
|
|
1258
|
-
const exported = (0, import_validation_core7.schemaName)(mode, table.tsName, sharedAffix);
|
|
1259
|
-
return exported === local ? local : `${exported} as ${local}`;
|
|
1260
|
-
}).join(", ");
|
|
1261
|
-
imports.push(`import { ${names} } from '${spec}';`);
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
const expressNames = ["Router"];
|
|
1265
|
-
if (decided.includes("json()")) expressNames.push("json");
|
|
1266
|
-
expressNames.push("type Response");
|
|
1267
|
-
imports.push(`import { ${expressNames.join(", ")} } from 'express';`);
|
|
1268
|
-
if (decided.includes(`${VALIDATE_FN}('`)) {
|
|
1269
|
-
const spec = (0, import_validation_core7.importSpecifier)(`./${VALIDATION_MODULE}.ts`, opts.importExtension);
|
|
1270
|
-
imports.push(`import { ${VALIDATE_FN} } from '${spec}';`);
|
|
1271
|
-
}
|
|
1272
|
-
if (LIB_USAGE3[lib].test(decided)) imports.unshift(LIB_IMPORTS3[lib]);
|
|
1273
|
-
const wide = table.columns.filter(isWide3).map((c) => c.name);
|
|
1274
|
-
const wideNote = wide.length ? `// No validated type for ${wide.length === 1 ? "this column" : "these columns"}: ${wide.join(", ")}.
|
|
1275
|
-
// DRZL could not derive one from the schema, so these routes accept any value there.
|
|
1276
|
-
` : "";
|
|
1277
|
-
return `// Generated by @drzl/generator-express
|
|
1278
|
-
// Routes for table: ${table.name}
|
|
1279
|
-
${wideNote}${imports.join("\n")}
|
|
1280
|
-
|
|
1281
|
-
${decided}`;
|
|
1282
|
-
}
|
|
1283
|
-
function relationRoutes2(table, lib, rowType, opts) {
|
|
1284
|
-
const d = LIBS3[lib];
|
|
1285
|
-
const out = [];
|
|
1286
|
-
const taken = /* @__PURE__ */ new Set();
|
|
1287
|
-
for (const fk of table.foreignKeys ?? []) {
|
|
1288
|
-
if (fk.columns.length !== 1) continue;
|
|
1289
|
-
const colName = fk.columns[0];
|
|
1290
|
-
const column = table.columns.find((c) => c.name === colName);
|
|
1291
|
-
if (!column) continue;
|
|
1292
|
-
const segment = toCase3(`by-${colName}`, opts.naming?.procedureCase ?? "kebab");
|
|
1293
|
-
if (taken.has(segment)) continue;
|
|
1294
|
-
taken.add(segment);
|
|
1295
|
-
const inline = d.objectInline(paramField2(column, lib));
|
|
1296
|
-
out.push({
|
|
1297
|
-
name: `listBy${cap3(colName)}`,
|
|
1298
|
-
method: "get",
|
|
1299
|
-
path: `/${segment}/:${colName}`,
|
|
1300
|
-
middleware: [`${VALIDATE_FN}('params', ${inline})`],
|
|
1301
|
-
resType: `${rowType}[]`,
|
|
1302
|
-
body: [VALID_PARAM_HINT2, `const rows: ${rowType}[] = [];`, "res.json(rows);"]
|
|
1303
|
-
});
|
|
1304
|
-
}
|
|
1305
|
-
return out;
|
|
1306
|
-
}
|
|
1307
|
-
function renderBarrel3(modules, ctx, path9, opts) {
|
|
1308
|
-
if (!modules.length) {
|
|
1309
|
-
return `// Generated by @drzl/generator-express
|
|
1310
|
-
// No tables detected in analysis. Add tables to your schema and regenerate.
|
|
1311
|
-
import express from 'express';
|
|
1312
|
-
|
|
1313
|
-
export const app = express();
|
|
1314
|
-
`;
|
|
1315
|
-
}
|
|
1316
|
-
const entries = modules.map(({ filePath, exportName, table }) => ({
|
|
1317
|
-
rel: (0, import_validation_core7.importSpecifier)(
|
|
1318
|
-
"./" + path9.relative(ctx.out, filePath).replace(/\\/g, "/"),
|
|
1319
|
-
opts.importExtension
|
|
1320
|
-
),
|
|
1321
|
-
exportName,
|
|
1322
|
-
mount: mountPath2(table, opts.naming)
|
|
1323
|
-
}));
|
|
1324
|
-
const imports = entries.map((e) => `import { ${e.exportName} } from '${e.rel}';`).join("\n");
|
|
1325
|
-
const mounts = entries.map((e) => `app.use(${lit2(e.mount)}, ${e.exportName});`).join("\n");
|
|
1326
|
-
const reExports = entries.map((e) => `export * from '${e.rel}';`).join("\n");
|
|
1327
|
-
return `// Generated by @drzl/generator-express
|
|
1328
|
-
import express from 'express';
|
|
1329
|
-
${imports}
|
|
1330
|
-
|
|
1331
|
-
export const app = express();
|
|
1332
|
-
|
|
1333
|
-
${mounts}
|
|
1334
|
-
|
|
1335
|
-
${reExports}
|
|
1336
|
-
`;
|
|
1337
|
-
}
|
|
1338
|
-
var import_validation_core6, import_validation_core7, APP_MODULE2, VALIDATION_MODULE, q3, lit2, NUMERIC_SEGMENT2, LIB_IMPORTS3, LIB_USAGE3, LIBS3, cap3, isIdent3, ExpressGenerator, index_default3, VALIDATE_FN, VALID_PARAM_HINT2;
|
|
1339
|
-
var init_dist3 = __esm({
|
|
1340
|
-
"../generator-express/dist/index.js"() {
|
|
1341
|
-
"use strict";
|
|
1342
|
-
import_validation_core6 = require("@drzl/validation-core");
|
|
1343
|
-
import_validation_core7 = require("@drzl/validation-core");
|
|
1344
|
-
APP_MODULE2 = "index";
|
|
1345
|
-
VALIDATION_MODULE = "validation";
|
|
1346
|
-
q3 = (v) => JSON.stringify(v);
|
|
1347
|
-
lit2 = (v) => /['\\]/.test(v) ? JSON.stringify(v) : `'${v}'`;
|
|
1348
|
-
NUMERIC_SEGMENT2 = String.raw`/^-?\d+(\.\d+)?$/`;
|
|
1349
|
-
LIB_IMPORTS3 = {
|
|
1350
|
-
zod: "import { z } from 'zod';",
|
|
1351
|
-
valibot: "import * as v from 'valibot';",
|
|
1352
|
-
arktype: "import { type } from 'arktype';"
|
|
1353
|
-
};
|
|
1354
|
-
LIB_USAGE3 = {
|
|
1355
|
-
zod: /\bz\./,
|
|
1356
|
-
valibot: /\bv\./,
|
|
1357
|
-
arktype: /\btype\(|\.infer\b/
|
|
1358
|
-
};
|
|
1359
|
-
LIBS3 = {
|
|
1360
|
-
zod: {
|
|
1361
|
-
number: "z.number()",
|
|
1362
|
-
string: "z.string()",
|
|
1363
|
-
boolean: "z.boolean()",
|
|
1364
|
-
date: "z.date()",
|
|
1365
|
-
unknown: "z.unknown()",
|
|
1366
|
-
enum: (vals) => `z.enum([${vals.map(q3).join(", ")}] as const)`,
|
|
1367
|
-
nullable: (b) => `${b}.nullable()`,
|
|
1368
|
-
optional: (b) => `${b}.optional()`,
|
|
1369
|
-
object: (body) => `z.object({
|
|
1370
|
-
${body}
|
|
1371
|
-
})`,
|
|
1372
|
-
objectInline: (body) => `z.object({ ${body} })`,
|
|
1373
|
-
partialUpdate: (s) => `${s}.partial()`,
|
|
1374
|
-
// Not `z.coerce.number()`, and not `z.coerce.date()`: both accept far more than a path
|
|
1375
|
-
// segment addressing a row should. See the grid referenced on `LibDialect.coerce`.
|
|
1376
|
-
coerce: (t) => t === "number" ? `z.string().regex(${NUMERIC_SEGMENT2}).transform(Number)` : t === "Date" ? "z.iso.datetime().transform((s) => new Date(s))" : null,
|
|
1377
|
-
infer: (s) => `z.output<typeof ${s}>`
|
|
1378
|
-
},
|
|
1379
|
-
valibot: {
|
|
1380
|
-
number: "v.number()",
|
|
1381
|
-
string: "v.string()",
|
|
1382
|
-
boolean: "v.boolean()",
|
|
1383
|
-
date: "v.date()",
|
|
1384
|
-
unknown: "v.unknown()",
|
|
1385
|
-
enum: (vals) => `v.picklist([${vals.map(q3).join(", ")}] as const)`,
|
|
1386
|
-
nullable: (b) => `v.nullable(${b})`,
|
|
1387
|
-
optional: (b) => `v.optional(${b})`,
|
|
1388
|
-
object: (body) => `v.object({
|
|
1389
|
-
${body}
|
|
1390
|
-
})`,
|
|
1391
|
-
objectInline: (body) => `v.object({ ${body} })`,
|
|
1392
|
-
// A valibot pipe step sees the previous step's *output*, so the check has to happen while the
|
|
1393
|
-
// value is still the string: after a `v.transform(Number)` there is no string left to look at.
|
|
1394
|
-
coerce: (t) => t === "number" ? `v.pipe(v.string(), v.regex(${NUMERIC_SEGMENT2}), v.transform(Number))` : t === "Date" ? "v.pipe(v.string(), v.isoTimestamp(), v.transform((s) => new Date(s)))" : null,
|
|
1395
|
-
infer: (s) => `v.InferOutput<typeof ${s}>`
|
|
1396
|
-
},
|
|
1397
|
-
arktype: {
|
|
1398
|
-
number: "number",
|
|
1399
|
-
string: "string",
|
|
1400
|
-
boolean: "boolean",
|
|
1401
|
-
date: "Date",
|
|
1402
|
-
unknown: "unknown",
|
|
1403
|
-
// The surrounding encode adds the quotes, so the union is built with the inner quoting
|
|
1404
|
-
// ArkType expects.
|
|
1405
|
-
enum: (vals) => vals.map((x) => `'${x.replace(/'/g, "\\'")}'`).join(" | "),
|
|
1406
|
-
nullable: (b) => `(${b} | null)`,
|
|
1407
|
-
optional: (b) => `${b}?`,
|
|
1408
|
-
object: (body) => `type({
|
|
1409
|
-
${body}
|
|
1410
|
-
})`,
|
|
1411
|
-
objectInline: (body) => `type({ ${body} })`,
|
|
1412
|
-
fieldIsString: true,
|
|
1413
|
-
// ArkType ships these as keywords, and they are morphs: the declared output type is `number`,
|
|
1414
|
-
// not `string`. Returned bare, because `fieldIsString` quotes every expression this dialect
|
|
1415
|
-
// produces, and a keyword returned pre-quoted arrives as a string *literal* type matching
|
|
1416
|
-
// nothing but that sentence. `string.date.iso.parse` and not `string.date.parse`, which
|
|
1417
|
-
// accepts `"1"` as the year 2001.
|
|
1418
|
-
coerce: (t) => t === "number" ? "string.numeric.parse" : t === "Date" ? "string.date.iso.parse" : null,
|
|
1419
|
-
infer: (s) => `typeof ${s}.infer`
|
|
1420
|
-
}
|
|
1421
|
-
};
|
|
1422
|
-
cap3 = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
1423
|
-
isIdent3 = (s) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s);
|
|
1424
|
-
ExpressGenerator = class {
|
|
1425
|
-
constructor(analysis) {
|
|
1426
|
-
this.analysis = analysis;
|
|
1427
|
-
}
|
|
1428
|
-
async generate(opts) {
|
|
1429
|
-
const fs6 = (0, import_validation_core6.fileWriter)(opts.fileSink);
|
|
1430
|
-
const path9 = await import("path");
|
|
1431
|
-
const out = path9.resolve(process.cwd(), opts.outputDir);
|
|
1432
|
-
const ctx = { out };
|
|
1433
|
-
await fs6.mkdir(out, { recursive: true });
|
|
1434
|
-
const files = [];
|
|
1435
|
-
const write = async (filePath, content) => {
|
|
1436
|
-
const formatted = await (0, import_validation_core7.formatCode)(
|
|
1437
|
-
buildHeader3(opts.outputHeader) + content,
|
|
1438
|
-
filePath,
|
|
1439
|
-
opts.format
|
|
1440
|
-
);
|
|
1441
|
-
await fs6.writeFile(filePath, formatted, "utf8");
|
|
1442
|
-
files.push(filePath);
|
|
1443
|
-
};
|
|
1444
|
-
const barrelPath = path9.join(out, `${APP_MODULE2}.ts`);
|
|
1445
|
-
const validationPath = path9.join(out, `${VALIDATION_MODULE}.ts`);
|
|
1446
|
-
const modules = [];
|
|
1447
|
-
const total = this.analysis.tables.length;
|
|
1448
|
-
let index = 0;
|
|
1449
|
-
let anyValidates = false;
|
|
1450
|
-
for (const table of this.analysis.tables) {
|
|
1451
|
-
const base = `${table.tsName}${opts.naming?.routerSuffix ?? ""}`;
|
|
1452
|
-
const filePath = path9.join(out, `${toCase3(base, opts.naming?.procedureCase)}.ts`);
|
|
1453
|
-
if (filePath === barrelPath || filePath === validationPath) {
|
|
1454
|
-
const which = filePath === barrelPath ? "the barrel" : "the validation middleware module";
|
|
1455
|
-
throw new Error(
|
|
1456
|
-
`@drzl/generator-express: the routes for table "${table.name}" would be written to ${filePath}, which is ${which} this generator also writes. Set naming.routerSuffix to move it out of the way.`
|
|
1457
|
-
);
|
|
1458
|
-
}
|
|
1459
|
-
const source = renderRoutes2(table, opts, ctx);
|
|
1460
|
-
if (source.includes(`${VALIDATE_FN}('`)) anyValidates = true;
|
|
1461
|
-
await write(filePath, source);
|
|
1462
|
-
modules.push({ table, filePath, exportName: routesExportName2(table, opts.naming) });
|
|
1463
|
-
index++;
|
|
1464
|
-
opts.onProgress?.({ index, total, table: table.name, filePath });
|
|
1465
|
-
}
|
|
1466
|
-
if (anyValidates) {
|
|
1467
|
-
await write(validationPath, renderValidationModule());
|
|
1468
|
-
}
|
|
1469
|
-
await write(barrelPath, renderBarrel3(modules, ctx, path9, opts));
|
|
1470
|
-
return { files };
|
|
1471
|
-
}
|
|
1472
|
-
};
|
|
1473
|
-
index_default3 = ExpressGenerator;
|
|
1474
|
-
VALIDATE_FN = "validate";
|
|
1475
|
-
VALID_PARAM_HINT2 = "// validate() has already replaced req.params with the parsed values.";
|
|
1476
|
-
}
|
|
1477
|
-
});
|
|
1478
|
-
|
|
1479
|
-
// ../generator-json-schema/dist/index.js
|
|
1480
|
-
var dist_exports4 = {};
|
|
1481
|
-
__export(dist_exports4, {
|
|
1482
|
-
DRAFT: () => DRAFT,
|
|
1483
|
-
JsonSchemaGenerator: () => JsonSchemaGenerator,
|
|
1484
|
-
allColumns: () => allColumns,
|
|
1485
|
-
componentSchemaName: () => componentSchemaName,
|
|
1486
|
-
componentsDocument: () => componentsDocument,
|
|
1487
|
-
default: () => index_default4,
|
|
1488
|
-
documentSchemas: () => documentSchemas,
|
|
1489
|
-
enumKey: () => enumKey,
|
|
1490
|
-
openApiDocument: () => openApiDocument,
|
|
1491
|
-
planSharedEnums: () => planSharedEnums,
|
|
1492
|
-
tableSchemas: () => tableSchemas
|
|
1493
|
-
});
|
|
1494
|
-
function enumKey(name) {
|
|
1495
|
-
const safe = name.replace(/[^A-Za-z0-9.\-_]/g, "_").replace(/^_+|_+$/g, "");
|
|
1496
|
-
return safe.length ? safe : void 0;
|
|
1497
|
-
}
|
|
1498
|
-
function declaredEnumColumns(columns) {
|
|
1499
|
-
return columns.filter((c) => !c.shape && c.enumValues && c.enumValues.length);
|
|
1500
|
-
}
|
|
1501
|
-
function planSharedEnums(columns, enums, ref2, reserved = /* @__PURE__ */ new Set()) {
|
|
1502
|
-
if (!enums?.length) return void 0;
|
|
1503
|
-
const uses = /* @__PURE__ */ new Map();
|
|
1504
|
-
for (const c of declaredEnumColumns(columns)) {
|
|
1505
|
-
const id = identity(c.enumValues);
|
|
1506
|
-
uses.set(id, (uses.get(id) ?? 0) + 1);
|
|
1507
|
-
}
|
|
1508
|
-
const keyed = /* @__PURE__ */ new Map();
|
|
1509
|
-
const taken = new Set(reserved);
|
|
1510
|
-
for (const e of enums) {
|
|
1511
|
-
const id = identity(e.values);
|
|
1512
|
-
if ((uses.get(id) ?? 0) < 2) continue;
|
|
1513
|
-
if (keyed.has(id)) continue;
|
|
1514
|
-
const key = enumKey(e.name);
|
|
1515
|
-
if (!key || taken.has(key)) continue;
|
|
1516
|
-
taken.add(key);
|
|
1517
|
-
keyed.set(id, { key, values: [...e.values] });
|
|
1518
|
-
}
|
|
1519
|
-
if (!keyed.size) return void 0;
|
|
1520
|
-
const used = /* @__PURE__ */ new Set();
|
|
1521
|
-
return {
|
|
1522
|
-
resolve(values) {
|
|
1523
|
-
const hit = keyed.get(identity(values));
|
|
1524
|
-
if (!hit) return void 0;
|
|
1525
|
-
used.add(hit.key);
|
|
1526
|
-
return ref2(hit.key);
|
|
1527
|
-
},
|
|
1528
|
-
definitions() {
|
|
1529
|
-
const out = {};
|
|
1530
|
-
for (const { key, values } of keyed.values()) {
|
|
1531
|
-
if (used.has(key)) out[key] = { enum: [...values] };
|
|
1532
|
-
}
|
|
1533
|
-
return out;
|
|
1534
|
-
}
|
|
1535
|
-
};
|
|
1536
|
-
}
|
|
1537
|
-
function canonicalSetPattern(values, integerOnly) {
|
|
1538
|
-
const branches = (0, import_validation_core10.canonicalMembers)(values).map((member) => {
|
|
1539
|
-
if (member === "0") return integerOnly ? "[+-]?0+" : "[+-]?(?:0+(?:\\.0*)?|0*\\.0+)";
|
|
1540
|
-
const sign = member.startsWith("-") ? "-" : "\\+?";
|
|
1541
|
-
const body = member.startsWith("-") ? member.slice(1) : member;
|
|
1542
|
-
const [int = "", frac = ""] = body.split(".");
|
|
1543
|
-
if (integerOnly) return `${sign}0*${int}`;
|
|
1544
|
-
if (!frac) return `${sign}0*${int}(?:\\.0*)?`;
|
|
1545
|
-
return int === "0" ? `${sign}0*\\.${frac}0*` : `${sign}0*${int}\\.${frac}0*`;
|
|
1546
|
-
});
|
|
1547
|
-
return `^(?:${branches.join("|")})$`;
|
|
1548
|
-
}
|
|
1549
|
-
function baseSchema(c, mode, target, checks, sets, lengths, enumRef) {
|
|
1550
|
-
const s = c.shape;
|
|
1551
|
-
if (s) {
|
|
1552
|
-
switch (s.kind) {
|
|
1553
|
-
case "json":
|
|
1554
|
-
return {};
|
|
1555
|
-
case "custom":
|
|
1556
|
-
return {};
|
|
1557
|
-
case "buffer": {
|
|
1558
|
-
const bin = base64(target);
|
|
1559
|
-
applyBinaryLengths(bin, c, lengths);
|
|
1560
|
-
return bin;
|
|
1561
|
-
}
|
|
1562
|
-
case "tuple":
|
|
1563
|
-
return target === "openapi-3.0" ? { type: "array", items: { type: "number" }, minItems: s.length, maxItems: s.length } : {
|
|
1564
|
-
type: "array",
|
|
1565
|
-
prefixItems: Array.from({ length: s.length }, () => ({ type: "number" })),
|
|
1566
|
-
minItems: s.length,
|
|
1567
|
-
maxItems: s.length
|
|
1568
|
-
};
|
|
1569
|
-
case "numberObject":
|
|
1570
|
-
return {
|
|
1571
|
-
type: "object",
|
|
1572
|
-
properties: Object.fromEntries(s.fields.map((f) => [f, { type: "number" }])),
|
|
1573
|
-
required: [...s.fields]
|
|
1574
|
-
};
|
|
1575
|
-
case "numberVector":
|
|
1576
|
-
return {
|
|
1577
|
-
type: "array",
|
|
1578
|
-
items: { type: "number" },
|
|
1579
|
-
...s.length ? { minItems: s.length, maxItems: s.length } : {}
|
|
1580
|
-
};
|
|
1581
|
-
case "bitstring":
|
|
1582
|
-
return {
|
|
1583
|
-
type: "string",
|
|
1584
|
-
pattern: "^[01]*$",
|
|
1585
|
-
...s.length ? s.exact ? { minLength: s.length, maxLength: s.length } : { maxLength: s.length } : {}
|
|
1586
|
-
};
|
|
1587
|
-
case "byteString":
|
|
1588
|
-
return { type: "string", ...s.length ? { maxLength: s.length } : {} };
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
|
-
const set = sets.find((x) => x.column === c.name);
|
|
1592
|
-
if (set) {
|
|
1593
|
-
if ((0, import_validation_core10.comparisonWire)(c) === "numeric-string") {
|
|
1594
|
-
return { type: "string", pattern: canonicalSetPattern(set.values, c.dbType === "BIGINT") };
|
|
1595
|
-
}
|
|
1596
|
-
return {
|
|
1597
|
-
enum: set.values.map(
|
|
1598
|
-
(v) => set.kind === "string" || c.tsType === "bigint" ? v : Number(v)
|
|
1599
|
-
)
|
|
1600
|
-
};
|
|
1601
|
-
}
|
|
1602
|
-
if (c.enumValues && c.enumValues.length) {
|
|
1603
|
-
const ref2 = enumRef?.(c.enumValues);
|
|
1604
|
-
return ref2 ? { $ref: ref2 } : { enum: [...c.enumValues] };
|
|
1605
|
-
}
|
|
1606
|
-
const mine = c.arrayDimensions ? [] : checks.filter((k) => k.column === c.name);
|
|
1607
|
-
const eq = mine.find((k) => k.operator === "=");
|
|
1608
|
-
if (eq) {
|
|
1609
|
-
if ((0, import_validation_core10.comparisonWire)(c) === "numeric-string") {
|
|
1610
|
-
return { type: "string", pattern: canonicalSetPattern([eq.value], c.dbType === "BIGINT") };
|
|
1611
|
-
}
|
|
1612
|
-
const only = eq.kind === "string" || c.tsType === "bigint" ? eq.value : Number(eq.value);
|
|
1613
|
-
return target === "openapi-3.0" ? { enum: [only] } : { const: only };
|
|
1614
|
-
}
|
|
1615
|
-
switch (c.tsType) {
|
|
1616
|
-
case "string": {
|
|
1617
|
-
const out = { type: "string" };
|
|
1618
|
-
if (c.format === "uuid") out.format = UUID_FORMAT;
|
|
1619
|
-
else if (c.format && import_validation_core10.COLUMN_FORMATS[c.format]) out.pattern = import_validation_core10.COLUMN_FORMATS[c.format];
|
|
1620
|
-
if (c.maxLength !== void 0) out.maxLength = c.maxLength;
|
|
1621
|
-
applyByteCap(out, c, lengths);
|
|
1622
|
-
applyLengths(out, c, lengths);
|
|
1623
|
-
return out;
|
|
1624
|
-
}
|
|
1625
|
-
case "number": {
|
|
1626
|
-
const out = { type: (0, import_validation_core10.isIntegerColumn)(c) ? "integer" : "number" };
|
|
1627
|
-
if (!c.arrayDimensions) applyNumericBounds(out, c, checks, target);
|
|
1628
|
-
return out;
|
|
1629
|
-
}
|
|
1630
|
-
case "bigint":
|
|
1631
|
-
return {
|
|
1632
|
-
type: "string",
|
|
1633
|
-
pattern: typeof c.min === "string" && !c.min.startsWith("-") ? "^\\d+$" : "^-?\\d+$"
|
|
1634
|
-
};
|
|
1635
|
-
case "boolean":
|
|
1636
|
-
return { type: "boolean" };
|
|
1637
|
-
case "Date":
|
|
1638
|
-
return { type: "string", format: "date-time" };
|
|
1639
|
-
case "Uint8Array":
|
|
1640
|
-
return base64(target);
|
|
1641
|
-
default:
|
|
1642
|
-
return {};
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
function applyByteCap(out, c, lengths) {
|
|
1646
|
-
const budget = byteBudget(c, lengths);
|
|
1647
|
-
if (budget === void 0) return;
|
|
1648
|
-
out.maxLength = Math.min(Number(out.maxLength ?? Infinity), budget);
|
|
1649
|
-
out.description = BYTE_BUDGET_NOTE(budget);
|
|
1650
|
-
}
|
|
1651
|
-
function ceilingOf(k) {
|
|
1652
|
-
if (k.operator === "<=" || k.operator === "=") return Number(k.value);
|
|
1653
|
-
if (k.operator === "<") return Number(k.value) - 1;
|
|
1654
|
-
return void 0;
|
|
1655
|
-
}
|
|
1656
|
-
function byteBudget(c, lengths) {
|
|
1657
|
-
const bounds = [
|
|
1658
|
-
...c.maxBytes ? [c.maxBytes] : [],
|
|
1659
|
-
...lengths.filter((k) => k.column === c.name && k.unit === "bytes").map(ceilingOf).filter((n) => n !== void 0)
|
|
1660
|
-
];
|
|
1661
|
-
return bounds.length ? Math.min(...bounds) : void 0;
|
|
1662
|
-
}
|
|
1663
|
-
function applyLengths(out, c, lengths) {
|
|
1664
|
-
for (const k of lengths.filter((x) => x.column === c.name && x.unit !== "bytes")) {
|
|
1665
|
-
const n = Number(k.value);
|
|
1666
|
-
if (k.operator === ">=") out.minLength = Math.max(Number(out.minLength ?? 0), n);
|
|
1667
|
-
else if (k.operator === ">") out.minLength = Math.max(Number(out.minLength ?? 0), n + 1);
|
|
1668
|
-
else if (k.operator === "<=") out.maxLength = Math.min(Number(out.maxLength ?? Infinity), n);
|
|
1669
|
-
else if (k.operator === "<") out.maxLength = Math.min(Number(out.maxLength ?? Infinity), n - 1);
|
|
1670
|
-
else if (k.operator === "=") {
|
|
1671
|
-
out.minLength = n;
|
|
1672
|
-
out.maxLength = n;
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
function applyBinaryLengths(out, c, lengths) {
|
|
1677
|
-
const budget = byteBudget(c, lengths);
|
|
1678
|
-
if (budget === void 0) return;
|
|
1679
|
-
out.maxLength = 4 * Math.ceil(budget / 3);
|
|
1680
|
-
out.description = `At most ${budget} bytes, which JSON Schema has no keyword for. The value travels as base64, and maxLength counts the characters of that encoding: it refuses nothing the column accepts, and a value one or two bytes over the limit encodes to the same number of characters as one inside it.`;
|
|
1681
|
-
}
|
|
1682
|
-
function applyNumericBounds(out, c, checks, target) {
|
|
1683
|
-
let min = c.min !== void 0 ? { value: Number(c.min), exclusive: false } : void 0;
|
|
1684
|
-
let max = c.max !== void 0 ? { value: Number(c.max), exclusive: false } : void 0;
|
|
1685
|
-
for (const k of checks.filter((x) => x.column === c.name && x.kind === "number")) {
|
|
1686
|
-
if (k.operator === ">=") min = { value: Number(k.value), exclusive: false };
|
|
1687
|
-
else if (k.operator === ">") min = { value: Number(k.value), exclusive: true };
|
|
1688
|
-
else if (k.operator === "<=") max = { value: Number(k.value), exclusive: false };
|
|
1689
|
-
else if (k.operator === "<") max = { value: Number(k.value), exclusive: true };
|
|
1690
|
-
}
|
|
1691
|
-
const old = target === "openapi-3.0";
|
|
1692
|
-
if (min) {
|
|
1693
|
-
if (min.exclusive && !old) out.exclusiveMinimum = min.value;
|
|
1694
|
-
else {
|
|
1695
|
-
out.minimum = min.value;
|
|
1696
|
-
if (min.exclusive) out.exclusiveMinimum = true;
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
if (max) {
|
|
1700
|
-
if (max.exclusive && !old) out.exclusiveMaximum = max.value;
|
|
1701
|
-
else {
|
|
1702
|
-
out.maximum = max.value;
|
|
1703
|
-
if (max.exclusive) out.exclusiveMaximum = true;
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
function cardinalityBounds(c, cardinalities) {
|
|
1708
|
-
if (!c.arrayDimensions) return {};
|
|
1709
|
-
const out = {};
|
|
1710
|
-
for (const k of cardinalities.filter((x) => x.column === c.name)) {
|
|
1711
|
-
const n = Number(k.value);
|
|
1712
|
-
if (k.operator === ">=") out.minItems = n;
|
|
1713
|
-
else if (k.operator === ">") out.minItems = n + 1;
|
|
1714
|
-
else if (k.operator === "<=") out.maxItems = n;
|
|
1715
|
-
else if (k.operator === "<") out.maxItems = n - 1;
|
|
1716
|
-
else if (k.operator === "=") {
|
|
1717
|
-
out.minItems = n;
|
|
1718
|
-
out.maxItems = n;
|
|
1719
|
-
}
|
|
1720
|
-
}
|
|
1721
|
-
return out;
|
|
1722
|
-
}
|
|
1723
|
-
function makeNullable(s, target) {
|
|
1724
|
-
if (target === "openapi-3.0") return { ...s, nullable: true };
|
|
1725
|
-
if ("$ref" in s) return { anyOf: [s, { type: "null" }] };
|
|
1726
|
-
if (s.type === void 0) {
|
|
1727
|
-
if (Array.isArray(s.enum)) return { ...s, enum: [...s.enum, null] };
|
|
1728
|
-
if ("const" in s) {
|
|
1729
|
-
const { const: k, ...rest } = s;
|
|
1730
|
-
return { ...rest, enum: [k, null] };
|
|
1731
|
-
}
|
|
1732
|
-
return s;
|
|
1733
|
-
}
|
|
1734
|
-
return { ...s, type: [s.type, "null"] };
|
|
1735
|
-
}
|
|
1736
|
-
function columnSchema(c, mode, target, checks, sets, lengths, cardinalities, applyDefault, enumRef) {
|
|
1737
|
-
const wantsDefault = mode === "insert" && applyDefault && c.defaultValue !== void 0;
|
|
1738
|
-
const refBlockedBy30 = target === "openapi-3.0" && !c.arrayDimensions && (c.nullable || wantsDefault);
|
|
1739
|
-
let s = baseSchema(c, mode, target, checks, sets, lengths, refBlockedBy30 ? void 0 : enumRef);
|
|
1740
|
-
const dims = c.arrayDimensions ?? 0;
|
|
1741
|
-
for (let i = 0; i < dims; i++) {
|
|
1742
|
-
s = { type: "array", items: s, ...i === dims - 1 ? cardinalityBounds(c, cardinalities) : {} };
|
|
1743
|
-
}
|
|
1744
|
-
if (c.nullable) s = makeNullable(s, target);
|
|
1745
|
-
if (mode === "insert" && applyDefault && c.defaultValue !== void 0) {
|
|
1746
|
-
s = { ...s, default: c.defaultValue };
|
|
1747
|
-
}
|
|
1748
|
-
return s;
|
|
1749
|
-
}
|
|
1750
|
-
function rowDescription(rows, cols) {
|
|
1751
|
-
const present = new Set(cols.map((c) => c.name));
|
|
1752
|
-
const applicable = rows.filter((r) => present.has(r.left) && present.has(r.right));
|
|
1753
|
-
if (!applicable.length) return void 0;
|
|
1754
|
-
const list = applicable.map((r) => `${r.name ? `${r.name}: ` : ""}${r.left} ${r.operator} ${r.right}`).join("; ");
|
|
1755
|
-
return `Row constraints not expressible in JSON Schema: ${list}`;
|
|
1756
|
-
}
|
|
1757
|
-
function tableSchema(table, cols, mode, target, applyDefaults, parsed, enums, localDefs) {
|
|
1758
|
-
const properties = {};
|
|
1759
|
-
const required = [];
|
|
1760
|
-
for (const c of cols) {
|
|
1761
|
-
properties[c.name] = columnSchema(
|
|
1762
|
-
c,
|
|
1763
|
-
mode,
|
|
1764
|
-
target,
|
|
1765
|
-
parsed.checks,
|
|
1766
|
-
parsed.sets,
|
|
1767
|
-
parsed.lengths,
|
|
1768
|
-
parsed.cardinalities,
|
|
1769
|
-
applyDefaults,
|
|
1770
|
-
enums?.resolve
|
|
1771
|
-
);
|
|
1772
|
-
const suppliedOnInsert = c.hasDefault || applyDefaults && c.defaultValue !== void 0 || c.isGenerated;
|
|
1773
|
-
const optional = mode === "update" || mode === "insert" && suppliedOnInsert;
|
|
1774
|
-
if (!optional) required.push(c.name);
|
|
1775
|
-
}
|
|
1776
|
-
const desc = rowDescription(parsed.rows, cols);
|
|
1777
|
-
const defs = localDefs ? enums?.definitions() ?? {} : {};
|
|
1778
|
-
return {
|
|
1779
|
-
...target === "draft-2020-12" ? { $schema: DRAFT } : {},
|
|
1780
|
-
$id: `${table.tsName}.${mode}`,
|
|
1781
|
-
title: `${mode} ${table.tsName}`,
|
|
1782
|
-
...desc ? { description: desc } : {},
|
|
1783
|
-
type: "object",
|
|
1784
|
-
properties,
|
|
1785
|
-
...required.length ? { required } : {},
|
|
1786
|
-
additionalProperties: false,
|
|
1787
|
-
...Object.keys(defs).length ? { $defs: defs } : {}
|
|
1788
|
-
};
|
|
1789
|
-
}
|
|
1790
|
-
function collect(table) {
|
|
1791
|
-
const parsed = (table.checks ?? []).map((k) => (0, import_validation_core10.parseCheck)(k.expression, k.name));
|
|
1792
|
-
const { checks, sets } = (0, import_validation_core10.applyWirePolicy)(
|
|
1793
|
-
table.columns,
|
|
1794
|
-
parsed.flatMap((p) => p.ok ? p.checks : []),
|
|
1795
|
-
parsed.flatMap((p) => p.ok ? p.sets ?? [] : [])
|
|
1796
|
-
);
|
|
1797
|
-
return {
|
|
1798
|
-
checks,
|
|
1799
|
-
sets,
|
|
1800
|
-
rows: parsed.flatMap((p) => p.ok ? p.rows ?? [] : []),
|
|
1801
|
-
lengths: parsed.flatMap((p) => p.ok ? p.lengths ?? [] : []),
|
|
1802
|
-
cardinalities: parsed.flatMap((p) => p.ok ? p.cardinalities ?? [] : [])
|
|
1803
|
-
};
|
|
1804
|
-
}
|
|
1805
|
-
function tableSchemas(table, opts = {}) {
|
|
1806
|
-
const target = opts.target ?? "draft-2020-12";
|
|
1807
|
-
const parsed = collect(table);
|
|
1808
|
-
const localDefs = target === "draft-2020-12";
|
|
1809
|
-
const build2 = (cols, mode) => {
|
|
1810
|
-
const plan = localDefs ? planSharedEnums(cols, opts.enums, (key) => `#/$defs/${key}`) : void 0;
|
|
1811
|
-
return tableSchema(table, cols, mode, target, !!opts.applyDefaults, parsed, plan, localDefs);
|
|
1812
|
-
};
|
|
1813
|
-
return {
|
|
1814
|
-
insert: build2((0, import_validation_core10.insertColumns)(table), "insert"),
|
|
1815
|
-
update: build2((0, import_validation_core10.updateColumns)(table), "update"),
|
|
1816
|
-
select: build2((0, import_validation_core10.selectColumns)(table), "select")
|
|
1817
|
-
};
|
|
1818
|
-
}
|
|
1819
|
-
function tableSchemasWith(table, target, applyDefaults, plan, modes = MODES) {
|
|
1820
|
-
const parsed = collect(table);
|
|
1821
|
-
const columns = {
|
|
1822
|
-
insert: () => (0, import_validation_core10.insertColumns)(table),
|
|
1823
|
-
update: () => (0, import_validation_core10.updateColumns)(table),
|
|
1824
|
-
select: () => (0, import_validation_core10.selectColumns)(table)
|
|
1825
|
-
};
|
|
1826
|
-
const out = {};
|
|
1827
|
-
for (const mode of modes) {
|
|
1828
|
-
out[mode] = tableSchema(
|
|
1829
|
-
table,
|
|
1830
|
-
columns[mode](),
|
|
1831
|
-
mode,
|
|
1832
|
-
target,
|
|
1833
|
-
applyDefaults,
|
|
1834
|
-
parsed,
|
|
1835
|
-
plan,
|
|
1836
|
-
false
|
|
1837
|
-
);
|
|
1838
|
-
}
|
|
1839
|
-
return out;
|
|
1840
|
-
}
|
|
1841
|
-
function componentsDocument(tables, opts = {}) {
|
|
1842
|
-
const target = opts.target ?? "draft-2020-12";
|
|
1843
|
-
const schemas = {};
|
|
1844
|
-
for (const table of tables) {
|
|
1845
|
-
const built = tableSchemasWith(table, target, !!opts.applyDefaults, void 0);
|
|
1846
|
-
for (const mode of MODES) {
|
|
1847
|
-
const { $schema: _dialect, $id: _id, ...rest } = built[mode];
|
|
1848
|
-
schemas[componentSchemaName(table, mode)] = rest;
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
return { schemas };
|
|
1852
|
-
}
|
|
1853
|
-
function documentSchemas(tables, opts) {
|
|
1854
|
-
const plan = planSharedEnums(
|
|
1855
|
-
tables.flatMap((t) => t.columns),
|
|
1856
|
-
opts.enums,
|
|
1857
|
-
(key) => `#/components/schemas/${key}`,
|
|
1858
|
-
opts.reserved
|
|
1859
|
-
);
|
|
1860
|
-
const built = /* @__PURE__ */ new Map();
|
|
1861
|
-
for (const table of tables) {
|
|
1862
|
-
const all = tableSchemasWith(table, opts.target, opts.applyDefaults, plan, opts.modes(table));
|
|
1863
|
-
built.set(table, all);
|
|
1864
|
-
}
|
|
1865
|
-
return { built, definitions: () => plan?.definitions() ?? {} };
|
|
1866
|
-
}
|
|
1867
|
-
function keyColumns4(table) {
|
|
1868
|
-
const names = table.primaryKey?.columns ?? [];
|
|
1869
|
-
if (!names.length) return null;
|
|
1870
|
-
const cols = names.map((n) => table.columns.find((c) => c.name === n));
|
|
1871
|
-
if (cols.some((c) => !c)) return null;
|
|
1872
|
-
return cols;
|
|
1873
|
-
}
|
|
1874
|
-
function foreignKeysOf(table) {
|
|
1875
|
-
if (table.foreignKeys?.length) return table.foreignKeys;
|
|
1876
|
-
return table.columns.filter((c) => c.references).map((c) => ({
|
|
1877
|
-
columns: [c.name],
|
|
1878
|
-
foreignTable: c.references.table,
|
|
1879
|
-
...c.references.schema ? { foreignSchema: c.references.schema } : {},
|
|
1880
|
-
foreignColumns: [c.references.column]
|
|
1881
|
-
}));
|
|
1882
|
-
}
|
|
1883
|
-
function build(tables, opts) {
|
|
1884
|
-
const target = opts.target ?? "draft-2020-12";
|
|
1885
|
-
const schemaTarget = target === "openapi-3.0" ? "openapi-3.0" : "openapi-3.1";
|
|
1886
|
-
const failure = String(opts.validationStatus ?? 400);
|
|
1887
|
-
const paths = {};
|
|
1888
|
-
const schemas = {};
|
|
1889
|
-
const tags = [];
|
|
1890
|
-
const operationIds = /* @__PURE__ */ new Map();
|
|
1891
|
-
const owner = /* @__PURE__ */ new Map();
|
|
1892
|
-
const claim = (path9, by, label) => {
|
|
1893
|
-
const taken = owner.get(path9);
|
|
1894
|
-
if (taken !== void 0 && taken.by !== by) {
|
|
1895
|
-
throw new Error(
|
|
1896
|
-
`@drzl/generator-json-schema: the OpenAPI path "${path9}" is claimed twice: by table "${taken.label}" (exported as ${taken.by}) and by table "${label}" (exported as ${by}). A path names one resource, so one of the two has to be left out of this generator with the config's "exclude" list.`
|
|
1897
|
-
);
|
|
1898
|
-
}
|
|
1899
|
-
owner.set(path9, { by, label });
|
|
1900
|
-
};
|
|
1901
|
-
const operation = (id, table, rest) => {
|
|
1902
|
-
const clash = operationIds.get(id);
|
|
1903
|
-
if (clash !== void 0) {
|
|
1904
|
-
throw new Error(
|
|
1905
|
-
`@drzl/generator-json-schema: the operationId "${id}" would be emitted for both "${clash}" and "${(0, import_analyzer.qualifiedTableName)(table)}". An operationId is the method name a client generator derives, and the specification requires it to be unique across the document.`
|
|
1906
|
-
);
|
|
1907
|
-
}
|
|
1908
|
-
operationIds.set(id, (0, import_analyzer.qualifiedTableName)(table));
|
|
1909
|
-
return { operationId: id, tags: [(0, import_analyzer.qualifiedTableName)(table)], ...rest };
|
|
1910
|
-
};
|
|
1911
|
-
const keys = new Map(tables.map((t) => [t, keyColumns4(t)]));
|
|
1912
|
-
const carried = new Map(tables.map((t) => [t, modesFor(t, keys.get(t))]));
|
|
1913
|
-
const reserved = /* @__PURE__ */ new Set([
|
|
1914
|
-
ERROR_SCHEMA,
|
|
1915
|
-
...tables.flatMap((t) => carried.get(t).map((m) => componentName(t, m)))
|
|
1916
|
-
]);
|
|
1917
|
-
const shared = documentSchemas(tables, {
|
|
1918
|
-
target: schemaTarget,
|
|
1919
|
-
applyDefaults: !!opts.applyDefaults,
|
|
1920
|
-
reserved,
|
|
1921
|
-
modes: (t) => carried.get(t),
|
|
1922
|
-
...opts.enums ? { enums: opts.enums } : {}
|
|
1923
|
-
});
|
|
1924
|
-
const built = tables.map((table) => ({
|
|
1925
|
-
table,
|
|
1926
|
-
key: keys.get(table),
|
|
1927
|
-
segment: resourceSegment(table),
|
|
1928
|
-
schemas: shared.built.get(table)
|
|
1929
|
-
}));
|
|
1930
|
-
for (const { table, key, segment, schemas: built3 } of built) {
|
|
1931
|
-
for (const mode of carried.get(table)) {
|
|
1932
|
-
const { $schema: _dialect, $id: _id, ...rest } = built3[mode];
|
|
1933
|
-
schemas[componentName(table, mode)] = rest;
|
|
1934
|
-
}
|
|
1935
|
-
const notes = [];
|
|
1936
|
-
if (!key) notes.push("It has no primary key, so no path addresses a single row.");
|
|
1937
|
-
if (table.readOnly) {
|
|
1938
|
-
notes.push("It refuses every write, so only reads are described.");
|
|
1939
|
-
}
|
|
1940
|
-
tags.push({
|
|
1941
|
-
name: (0, import_analyzer.qualifiedTableName)(table),
|
|
1942
|
-
description: [`Table "${(0, import_analyzer.qualifiedTableName)(table)}".`, ...notes].join(" ")
|
|
1943
|
-
});
|
|
1944
|
-
const T = pascal(table.tsName);
|
|
1945
|
-
const select = ref(componentName(table, "select"));
|
|
1946
|
-
const validationFailed = {
|
|
1947
|
-
description: "The request does not match the schema for this operation.",
|
|
1948
|
-
...jsonBody(ref(ERROR_SCHEMA))
|
|
1949
|
-
};
|
|
1950
|
-
const collidable = [
|
|
1951
|
-
...table.primaryKey ? [`primary key (${table.primaryKey.columns.join(", ")})`] : [],
|
|
1952
|
-
...table.unique.map((u) => `${u.name ? `${u.name} ` : ""}(${u.columns.join(", ")})`)
|
|
1953
|
-
];
|
|
1954
|
-
const conflict = (constraints) => ({
|
|
1955
|
-
description: `The row collides with an existing one on ${constraints.join("; ")}.`,
|
|
1956
|
-
...jsonBody(ref(ERROR_SCHEMA))
|
|
1957
|
-
});
|
|
1958
|
-
const collection = `/${segment}`;
|
|
1959
|
-
claim(collection, table.tsName, (0, import_analyzer.qualifiedTableName)(table));
|
|
1960
|
-
const item = {
|
|
1961
|
-
get: operation(`list${T}`, table, {
|
|
1962
|
-
summary: `List every ${table.name} row.`,
|
|
1963
|
-
// No pagination parameters. Whether the server implements a limit, an offset or a cursor is
|
|
1964
|
-
// not something a Drizzle schema states, and a declared parameter nothing honours is worse
|
|
1965
|
-
// than an undeclared one.
|
|
1966
|
-
responses: {
|
|
1967
|
-
"200": {
|
|
1968
|
-
description: `Every ${table.name} row.`,
|
|
1969
|
-
...jsonBody({ type: "array", items: select })
|
|
1970
|
-
}
|
|
1971
|
-
}
|
|
1972
|
-
})
|
|
1973
|
-
};
|
|
1974
|
-
if (!table.readOnly) {
|
|
1975
|
-
item.post = operation(`create${T}`, table, {
|
|
1976
|
-
summary: `Create a ${table.name} row.`,
|
|
1977
|
-
requestBody: { required: true, ...jsonBody(ref(componentName(table, "insert"))) },
|
|
1978
|
-
responses: {
|
|
1979
|
-
"201": { description: `The ${table.name} row that was created.`, ...jsonBody(select) },
|
|
1980
|
-
[failure]: validationFailed,
|
|
1981
|
-
...collidable.length ? { "409": conflict(collidable) } : {}
|
|
1982
|
-
}
|
|
1983
|
-
});
|
|
1984
|
-
}
|
|
1985
|
-
paths[collection] = item;
|
|
1986
|
-
if (!key) continue;
|
|
1987
|
-
const itemPath = `${collection}/${key.map((c) => `{${c.name}}`).join("/")}`;
|
|
1988
|
-
claim(itemPath, table.tsName, (0, import_analyzer.qualifiedTableName)(table));
|
|
1989
|
-
const parameters = key.map((c) => ({
|
|
1990
|
-
name: c.name,
|
|
1991
|
-
in: "path",
|
|
1992
|
-
required: true,
|
|
1993
|
-
description: `${c.name}, from the primary key of ${table.name}.`,
|
|
1994
|
-
// The column's own schema rather than a string, so an integer key is declared as one and a
|
|
1995
|
-
// uuid key carries its format. This is the whole point of reading the real key.
|
|
1996
|
-
schema: built3.select.properties[c.name] ?? {}
|
|
1997
|
-
}));
|
|
1998
|
-
const missing = {
|
|
1999
|
-
description: `No ${table.name} row has that ${key.map((c) => c.name).join(" and ")}.`,
|
|
2000
|
-
...jsonBody(ref(ERROR_SCHEMA))
|
|
2001
|
-
};
|
|
2002
|
-
const byId = {
|
|
2003
|
-
parameters,
|
|
2004
|
-
get: operation(`get${T}`, table, {
|
|
2005
|
-
summary: `Read one ${table.name} row.`,
|
|
2006
|
-
responses: {
|
|
2007
|
-
"200": { description: `The requested ${table.name} row.`, ...jsonBody(select) },
|
|
2008
|
-
[failure]: validationFailed,
|
|
2009
|
-
"404": missing
|
|
2010
|
-
}
|
|
2011
|
-
})
|
|
2012
|
-
};
|
|
2013
|
-
if (!table.readOnly) {
|
|
2014
|
-
byId.patch = operation(`update${T}`, table, {
|
|
2015
|
-
summary: `Patch one ${table.name} row.`,
|
|
2016
|
-
requestBody: { required: true, ...jsonBody(ref(componentName(table, "update"))) },
|
|
2017
|
-
responses: {
|
|
2018
|
-
"200": { description: `The ${table.name} row after the patch.`, ...jsonBody(select) },
|
|
2019
|
-
[failure]: validationFailed,
|
|
2020
|
-
"404": missing,
|
|
2021
|
-
// The primary key is not in the update schema, so a patch cannot collide on it. Only a
|
|
2022
|
-
// unique constraint over other columns can.
|
|
2023
|
-
...table.unique.length ? {
|
|
2024
|
-
"409": conflict(
|
|
2025
|
-
table.unique.map((u) => `${u.name ? `${u.name} ` : ""}(${u.columns.join(", ")})`)
|
|
2026
|
-
)
|
|
2027
|
-
} : {}
|
|
2028
|
-
}
|
|
2029
|
-
});
|
|
2030
|
-
byId.delete = operation(`delete${T}`, table, {
|
|
2031
|
-
summary: `Delete one ${table.name} row.`,
|
|
2032
|
-
responses: {
|
|
2033
|
-
// No body. Handing back the deleted row is the alternative and it is not a true statement
|
|
2034
|
-
// on every dialect DRZL supports: RETURNING is Postgres and SQLite, and MySQL has no such
|
|
2035
|
-
// clause, so an implementation there has nothing to send.
|
|
2036
|
-
"204": { description: `The ${table.name} row was deleted. No content is returned.` },
|
|
2037
|
-
[failure]: validationFailed,
|
|
2038
|
-
"404": missing
|
|
2039
|
-
}
|
|
2040
|
-
});
|
|
2041
|
-
}
|
|
2042
|
-
paths[itemPath] = byId;
|
|
2043
|
-
if (!opts.includeRelations) continue;
|
|
2044
|
-
for (const child of built) {
|
|
2045
|
-
if (child.table === table) continue;
|
|
2046
|
-
const matching = foreignKeysOf(child.table).filter(
|
|
2047
|
-
(fk) => (
|
|
2048
|
-
// Qualified on both sides. On the bare name a key pointing at `reporting.users` also
|
|
2049
|
-
// answered for `public.users`, so the child was hung under a parent in another schema.
|
|
2050
|
-
(0, import_analyzer.qualifiedForeignTable)(fk) === (0, import_analyzer.qualifiedTableName)(table) && fk.foreignColumns.length === key.length && fk.foreignColumns.every((c, i) => c === key[i].name)
|
|
2051
|
-
)
|
|
2052
|
-
);
|
|
2053
|
-
if (matching.length !== 1) continue;
|
|
2054
|
-
const subPath = `${itemPath}/${child.segment}`;
|
|
2055
|
-
claim(
|
|
2056
|
-
subPath,
|
|
2057
|
-
`${table.tsName} -> ${child.table.tsName}`,
|
|
2058
|
-
`${(0, import_analyzer.qualifiedTableName)(table)} -> ${(0, import_analyzer.qualifiedTableName)(child.table)}`
|
|
2059
|
-
);
|
|
2060
|
-
paths[subPath] = {
|
|
2061
|
-
parameters,
|
|
2062
|
-
get: operation(`list${T}${pascal(child.table.tsName)}`, child.table, {
|
|
2063
|
-
summary: `List the ${child.table.name} rows belonging to one ${table.name} row.`,
|
|
2064
|
-
responses: {
|
|
2065
|
-
"200": {
|
|
2066
|
-
description: `The ${child.table.name} rows whose ${matching[0].columns.join(", ")} names this ${table.name} row.`,
|
|
2067
|
-
...jsonBody({ type: "array", items: ref(componentName(child.table, "select")) })
|
|
2068
|
-
},
|
|
2069
|
-
[failure]: validationFailed,
|
|
2070
|
-
"404": missing
|
|
2071
|
-
}
|
|
2072
|
-
})
|
|
2073
|
-
};
|
|
2074
|
-
}
|
|
2075
|
-
}
|
|
2076
|
-
return { paths, schemas: { ...schemas, ...shared.definitions() }, tags };
|
|
2077
|
-
}
|
|
2078
|
-
function openApiDocument(tables, opts = {}) {
|
|
2079
|
-
const target = opts.target ?? "draft-2020-12";
|
|
2080
|
-
const { paths, schemas, tags } = build(tables, opts);
|
|
2081
|
-
if (ERROR_SCHEMA in schemas) {
|
|
2082
|
-
throw new Error(
|
|
2083
|
-
`@drzl/generator-json-schema: a table produced the component schema name "${ERROR_SCHEMA}", which the document already uses for its error responses.`
|
|
2084
|
-
);
|
|
2085
|
-
}
|
|
2086
|
-
return {
|
|
2087
|
-
openapi: target === "openapi-3.0" ? "3.0.3" : "3.1.1",
|
|
2088
|
-
info: {
|
|
2089
|
-
title: opts.info?.title ?? "API",
|
|
2090
|
-
version: opts.info?.version ?? "0.0.0",
|
|
2091
|
-
description: opts.info?.description ?? "Generated by DRZL from a Drizzle schema. Paths, request bodies and response bodies are derived from the schema alone; nothing here has been checked against a running server."
|
|
2092
|
-
},
|
|
2093
|
-
...opts.servers?.length ? { servers: opts.servers } : {},
|
|
2094
|
-
paths,
|
|
2095
|
-
components: { schemas: { ...schemas, [ERROR_SCHEMA]: errorSchema() } },
|
|
2096
|
-
tags
|
|
2097
|
-
};
|
|
2098
|
-
}
|
|
2099
|
-
function renderTableModule(table, affix, target, applyDefaults, enums) {
|
|
2100
|
-
const T = table.tsName;
|
|
2101
|
-
const schemas = tableSchemas(table, { target, applyDefaults, ...enums ? { enums } : {} });
|
|
2102
|
-
const decl = (mode) => `export const ${(0, import_validation_core9.schemaName)(mode, T, affix)} = ${JSON.stringify(schemas[mode], null, 2)} as const;
|
|
2103
|
-
|
|
2104
|
-
export type ${(0, import_validation_core9.typeName)(mode, T, affix)} = typeof ${(0, import_validation_core9.schemaName)(mode, T, affix)};`;
|
|
2105
|
-
return [decl("insert"), decl("update"), decl("select")].join("\n\n") + "\n";
|
|
2106
|
-
}
|
|
2107
|
-
function resolveDocument(opt) {
|
|
2108
|
-
if (!opt) return null;
|
|
2109
|
-
const o = opt === true ? {} : opt;
|
|
2110
|
-
if (o.enabled === false) return null;
|
|
2111
|
-
return { ...o, format: o.format ?? "ts" };
|
|
2112
|
-
}
|
|
2113
|
-
function buildHeader4(h) {
|
|
2114
|
-
if (h?.enabled === false) return "";
|
|
2115
|
-
const text = h?.text ?? "// Generated by DRZL. Do not edit by hand.";
|
|
2116
|
-
return `${text}
|
|
2117
|
-
|
|
2118
|
-
`;
|
|
2119
|
-
}
|
|
2120
|
-
var import_validation_core8, import_validation_core9, import_analyzer, import_validation_core10, identity, allColumns, DRAFT, UUID_FORMAT, base64, BYTE_BUDGET_NOTE, MODES, componentSchemaName, ERROR_SCHEMA, componentName, ref, pascal, modesFor, resourceSegment, jsonBody, errorSchema, DEFAULT_FILE_SUFFIX, JsonSchemaGenerator, index_default4;
|
|
2121
|
-
var init_dist4 = __esm({
|
|
2122
|
-
"../generator-json-schema/dist/index.js"() {
|
|
2123
|
-
"use strict";
|
|
2124
|
-
import_validation_core8 = require("@drzl/validation-core");
|
|
2125
|
-
import_validation_core9 = require("@drzl/validation-core");
|
|
2126
|
-
import_analyzer = require("@drzl/analyzer");
|
|
2127
|
-
import_validation_core10 = require("@drzl/validation-core");
|
|
2128
|
-
identity = (values) => JSON.stringify([...values]);
|
|
2129
|
-
allColumns = (tables) => tables.flatMap((t) => t.columns);
|
|
2130
|
-
DRAFT = "https://json-schema.org/draft/2020-12/schema";
|
|
2131
|
-
UUID_FORMAT = "uuid";
|
|
2132
|
-
base64 = (target) => target === "openapi-3.0" ? { type: "string", format: "byte" } : { type: "string", contentEncoding: "base64" };
|
|
2133
|
-
BYTE_BUDGET_NOTE = (n) => `At most ${n} bytes of UTF-8, which JSON Schema has no keyword for. maxLength counts characters: it refuses nothing the column accepts, and a string of multi-byte characters can satisfy it and still be too long for the column.`;
|
|
2134
|
-
MODES = ["insert", "update", "select"];
|
|
2135
|
-
componentSchemaName = (table, mode) => `${table.tsName}${mode[0].toUpperCase()}${mode.slice(1)}`;
|
|
2136
|
-
ERROR_SCHEMA = "Error";
|
|
2137
|
-
componentName = componentSchemaName;
|
|
2138
|
-
ref = (name) => ({ $ref: `#/components/schemas/${name}` });
|
|
2139
|
-
pascal = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
2140
|
-
modesFor = (table, key) => [
|
|
2141
|
-
...table.readOnly ? [] : ["insert"],
|
|
2142
|
-
...table.readOnly || !key ? [] : ["update"],
|
|
2143
|
-
"select"
|
|
2144
|
-
];
|
|
2145
|
-
resourceSegment = (table) => table.schema ? `${encodeURIComponent(table.schema)}/${encodeURIComponent(table.name)}` : encodeURIComponent(table.name);
|
|
2146
|
-
jsonBody = (schema) => ({ content: { "application/json": { schema } } });
|
|
2147
|
-
errorSchema = () => ({
|
|
2148
|
-
title: "error",
|
|
2149
|
-
description: "What an operation returns when it does not return the row.",
|
|
2150
|
-
type: "object",
|
|
2151
|
-
properties: {
|
|
2152
|
-
message: { type: "string" },
|
|
2153
|
-
code: { type: "string" }
|
|
2154
|
-
},
|
|
2155
|
-
required: ["message"],
|
|
2156
|
-
additionalProperties: true
|
|
2157
|
-
});
|
|
2158
|
-
DEFAULT_FILE_SUFFIX = ".schema.ts";
|
|
2159
|
-
JsonSchemaGenerator = class {
|
|
2160
|
-
constructor(analysis) {
|
|
2161
|
-
this.analysis = analysis;
|
|
2162
|
-
this.library = "json-schema";
|
|
2163
|
-
}
|
|
2164
|
-
async generate(opts) {
|
|
2165
|
-
const fs6 = (0, import_validation_core8.fileWriter)(opts.fileSink);
|
|
2166
|
-
const path9 = await import("path");
|
|
2167
|
-
const out = path9.resolve(process.cwd(), opts.outDir);
|
|
2168
|
-
const files = [];
|
|
2169
|
-
await fs6.mkdir(out, { recursive: true });
|
|
2170
|
-
const affix = (0, import_validation_core9.resolveAffix)(opts);
|
|
2171
|
-
const fileSuffix = opts.fileSuffix ?? DEFAULT_FILE_SUFFIX;
|
|
2172
|
-
const target = opts.target ?? "draft-2020-12";
|
|
2173
|
-
const document = resolveDocument(opts.document);
|
|
2174
|
-
for (const table of this.analysis.tables) {
|
|
2175
|
-
const filePath = path9.join(out, (0, import_validation_core9.moduleFileName)(table.tsName, fileSuffix));
|
|
2176
|
-
const code = renderTableModule(
|
|
2177
|
-
table,
|
|
2178
|
-
affix,
|
|
2179
|
-
target,
|
|
2180
|
-
!!opts.applyDefaults,
|
|
2181
|
-
opts.sharedEnums ? this.analysis.enums : void 0
|
|
2182
|
-
);
|
|
2183
|
-
const formatted = await (0, import_validation_core9.formatCode)(
|
|
2184
|
-
buildHeader4(opts.outputHeader) + code,
|
|
2185
|
-
filePath,
|
|
2186
|
-
opts.format
|
|
2187
|
-
);
|
|
2188
|
-
await fs6.writeFile(filePath, formatted, "utf8");
|
|
2189
|
-
files.push(filePath);
|
|
2190
|
-
}
|
|
2191
|
-
if (opts.components) {
|
|
2192
|
-
const doc = componentsDocument(this.analysis.tables, {
|
|
2193
|
-
target,
|
|
2194
|
-
applyDefaults: !!opts.applyDefaults
|
|
2195
|
-
});
|
|
2196
|
-
const componentsPath = path9.join(out, "components.ts");
|
|
2197
|
-
const code = `export const components = ${JSON.stringify(doc, null, 2)} as const;
|
|
2198
|
-
`;
|
|
2199
|
-
await fs6.writeFile(
|
|
2200
|
-
componentsPath,
|
|
2201
|
-
await (0, import_validation_core9.formatCode)(buildHeader4(opts.outputHeader) + code, componentsPath, opts.format),
|
|
2202
|
-
"utf8"
|
|
2203
|
-
);
|
|
2204
|
-
files.push(componentsPath);
|
|
2205
|
-
}
|
|
2206
|
-
if (document) {
|
|
2207
|
-
const built = openApiDocument(this.analysis.tables, {
|
|
2208
|
-
target,
|
|
2209
|
-
applyDefaults: !!opts.applyDefaults,
|
|
2210
|
-
includeRelations: !!opts.includeRelations,
|
|
2211
|
-
enums: this.analysis.enums,
|
|
2212
|
-
info: document.info,
|
|
2213
|
-
servers: document.servers,
|
|
2214
|
-
validationStatus: document.validationStatus
|
|
2215
|
-
});
|
|
2216
|
-
const body = JSON.stringify(built, null, 2);
|
|
2217
|
-
if (document.format !== "json") {
|
|
2218
|
-
const tsPath = path9.join(out, "openapi.ts");
|
|
2219
|
-
const code = `export const openapi = ${body} as const;
|
|
2220
|
-
`;
|
|
2221
|
-
await fs6.writeFile(
|
|
2222
|
-
tsPath,
|
|
2223
|
-
await (0, import_validation_core9.formatCode)(buildHeader4(opts.outputHeader) + code, tsPath, opts.format),
|
|
2224
|
-
"utf8"
|
|
2225
|
-
);
|
|
2226
|
-
files.push(tsPath);
|
|
2227
|
-
}
|
|
2228
|
-
if (document.format !== "ts") {
|
|
2229
|
-
const jsonPath = path9.join(out, "openapi.json");
|
|
2230
|
-
await fs6.writeFile(jsonPath, body + "\n", "utf8");
|
|
2231
|
-
files.push(jsonPath);
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
const ext = opts.importExtension === "none" ? "" : ".js";
|
|
2235
|
-
const indexPath = path9.join(out, "index.ts");
|
|
2236
|
-
const index = this.analysis.tables.map(
|
|
2237
|
-
(t) => `export * from '${(0, import_validation_core9.moduleSpecifier)(t.tsName, fileSuffix, opts.importExtension)}';`
|
|
2238
|
-
).concat(opts.components ? [`export * from './components${ext}';`] : []).concat(document && document.format !== "json" ? [`export * from './openapi${ext}';`] : []).join("\n") + "\n";
|
|
2239
|
-
const indexFormatted = await (0, import_validation_core9.formatCode)(
|
|
2240
|
-
buildHeader4(opts.outputHeader) + index,
|
|
2241
|
-
indexPath,
|
|
2242
|
-
opts.format
|
|
2243
|
-
);
|
|
2244
|
-
await fs6.writeFile(indexPath, indexFormatted, "utf8");
|
|
2245
|
-
files.push(indexPath);
|
|
2246
|
-
return files;
|
|
2247
|
-
}
|
|
2248
|
-
renderTable(table, opts) {
|
|
2249
|
-
return renderTableModule(
|
|
2250
|
-
table,
|
|
2251
|
-
(0, import_validation_core9.resolveAffix)(opts),
|
|
2252
|
-
opts?.target ?? "draft-2020-12",
|
|
2253
|
-
!!opts?.applyDefaults,
|
|
2254
|
-
opts?.sharedEnums ? this.analysis.enums : void 0
|
|
2255
|
-
);
|
|
2256
|
-
}
|
|
2257
|
-
};
|
|
2258
|
-
index_default4 = JsonSchemaGenerator;
|
|
2259
|
-
}
|
|
2260
|
-
});
|
|
2261
|
-
|
|
2262
|
-
// ../generator-fastify/dist/index.js
|
|
2263
|
-
var dist_exports5 = {};
|
|
2264
|
-
__export(dist_exports5, {
|
|
2265
|
-
APP_MODULE: () => APP_MODULE3,
|
|
2266
|
-
FastifyGenerator: () => FastifyGenerator,
|
|
2267
|
-
default: () => index_default5
|
|
2268
|
-
});
|
|
2269
|
-
function keyColumns5(table) {
|
|
2270
|
-
const names = table.primaryKey?.columns ?? [];
|
|
2271
|
-
if (!names.length) return null;
|
|
2272
|
-
const cols = names.map((n) => table.columns.find((c) => c.name === n));
|
|
2273
|
-
if (cols.some((c) => !c)) return null;
|
|
2274
|
-
return cols;
|
|
2275
|
-
}
|
|
2276
|
-
function segmentSchema(column) {
|
|
2277
|
-
if (column.enumValues && column.enumValues.length) return { enum: [...column.enumValues] };
|
|
2278
|
-
switch (column.tsType) {
|
|
2279
|
-
case "number":
|
|
2280
|
-
return { type: "string", pattern: NUMERIC_SEGMENT_PATTERN };
|
|
2281
|
-
case "bigint":
|
|
2282
|
-
return { type: "string", pattern: BIGINT_SEGMENT_PATTERN };
|
|
2283
|
-
case "Date":
|
|
2284
|
-
return { type: "string", format: "date-time" };
|
|
2285
|
-
default:
|
|
2286
|
-
return { type: "string" };
|
|
2287
|
-
}
|
|
2288
|
-
}
|
|
2289
|
-
function paramsSchema(cols) {
|
|
2290
|
-
return {
|
|
2291
|
-
type: "object",
|
|
2292
|
-
properties: Object.fromEntries(cols.map((c) => [c.name, segmentSchema(c)])),
|
|
2293
|
-
required: cols.map((c) => c.name),
|
|
2294
|
-
additionalProperties: false
|
|
2295
|
-
};
|
|
2296
|
-
}
|
|
2297
|
-
function adaptForFastify(schema) {
|
|
2298
|
-
const { $schema: _dialect, $id: _id, ...rest } = walk(schema);
|
|
2299
|
-
return rest;
|
|
2300
|
-
}
|
|
2301
|
-
function walk(value) {
|
|
2302
|
-
if (Array.isArray(value)) return value.map(walk);
|
|
2303
|
-
if (value && typeof value === "object") {
|
|
2304
|
-
const out = {};
|
|
2305
|
-
for (const [k, v] of Object.entries(value)) {
|
|
2306
|
-
if (k === "prefixItems") continue;
|
|
2307
|
-
out[k] = walk(v);
|
|
2308
|
-
}
|
|
2309
|
-
const prefix = value.prefixItems;
|
|
2310
|
-
if (Array.isArray(prefix) && prefix.length) out.items = walk(prefix[0]);
|
|
2311
|
-
return out;
|
|
2312
|
-
}
|
|
2313
|
-
return value;
|
|
2314
|
-
}
|
|
2315
|
-
function rowFieldType(column) {
|
|
2316
|
-
if (column.enumValues && column.enumValues.length) {
|
|
2317
|
-
return column.enumValues.map((v) => `'${v.replace(/'/g, "\\'")}'`).join(" | ");
|
|
2318
|
-
}
|
|
2319
|
-
const s = column.shape;
|
|
2320
|
-
if (s) {
|
|
2321
|
-
switch (s.kind) {
|
|
2322
|
-
case "tuple":
|
|
2323
|
-
return `[${Array.from({ length: s.length }, () => "number").join(", ")}]`;
|
|
2324
|
-
case "numberObject":
|
|
2325
|
-
return `{ ${s.fields.map((f) => `${f}: number`).join("; ")} }`;
|
|
2326
|
-
case "numberVector":
|
|
2327
|
-
return "number[]";
|
|
2328
|
-
default:
|
|
2329
|
-
return "unknown";
|
|
2330
|
-
}
|
|
2331
|
-
}
|
|
2332
|
-
switch (column.tsType) {
|
|
2333
|
-
case "number":
|
|
2334
|
-
return "number";
|
|
2335
|
-
case "string":
|
|
2336
|
-
return "string";
|
|
2337
|
-
case "boolean":
|
|
2338
|
-
return "boolean";
|
|
2339
|
-
case "Date":
|
|
2340
|
-
return "Date";
|
|
2341
|
-
case "bigint":
|
|
2342
|
-
return "bigint";
|
|
2343
|
-
default:
|
|
2344
|
-
return "unknown";
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
function objectKey4(name) {
|
|
2348
|
-
return isIdent4(name) ? name : JSON.stringify(name);
|
|
2349
|
-
}
|
|
2350
|
-
function toCase4(s, c) {
|
|
2351
|
-
if (!c) return s;
|
|
2352
|
-
const parts = s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]/g, " ").split(/\s+/);
|
|
2353
|
-
if (c === "camel") {
|
|
2354
|
-
return parts.map(
|
|
2355
|
-
(p, i) => i === 0 ? p.toLowerCase() : p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()
|
|
2356
|
-
).join("");
|
|
2357
|
-
}
|
|
2358
|
-
if (c === "kebab") return parts.map((p) => p.toLowerCase()).join("-");
|
|
2359
|
-
if (c === "snake") return parts.map((p) => p.toLowerCase()).join("_");
|
|
2360
|
-
return s;
|
|
2361
|
-
}
|
|
2362
|
-
function routesExportName3(table, naming) {
|
|
2363
|
-
const base = `${table.tsName}${naming?.routerSuffix ?? "Routes"}`;
|
|
2364
|
-
const c = naming?.procedureCase;
|
|
2365
|
-
return toCase4(base, c === "kebab" ? "camel" : c);
|
|
2366
|
-
}
|
|
2367
|
-
function mountPath3(table, naming) {
|
|
2368
|
-
return `/${toCase4(table.tsName, naming?.procedureCase)}`;
|
|
2369
|
-
}
|
|
2370
|
-
function buildHeader5(h) {
|
|
2371
|
-
if (h && h.enabled === false) return "";
|
|
2372
|
-
const text = h?.text?.trim();
|
|
2373
|
-
const lines = text ? text.split(/\r?\n/).map((l) => `// ${l}`) : [
|
|
2374
|
-
"// Generated by DRZL (@drzl/*)",
|
|
2375
|
-
"// Generated output is granted to you under your project's license.",
|
|
2376
|
-
"// You may use, copy, modify, and distribute without attribution."
|
|
2377
|
-
];
|
|
2378
|
-
return lines.join("\n") + "\n\n";
|
|
2379
|
-
}
|
|
2380
|
-
function renderRoutes3(table, opts) {
|
|
2381
|
-
const insertName = `Insert${table.tsName}Schema`;
|
|
2382
|
-
const updateName = `Update${table.tsName}Schema`;
|
|
2383
|
-
const selectName = `Select${table.tsName}Schema`;
|
|
2384
|
-
const paramsName = `${cap4(table.tsName)}ParamsSchema`;
|
|
2385
|
-
const rowType = `Select${table.tsName}Row`;
|
|
2386
|
-
const writable = !table.readOnly;
|
|
2387
|
-
const key = keyColumns5(table);
|
|
2388
|
-
const routes = [];
|
|
2389
|
-
const notImplemented = (what) => `throw new Error('Not implemented: ${what} ${table.tsName}.');`;
|
|
2390
|
-
const paramHint = `// Fastify has already validated req.params against ${paramsName}; numeric key segments stay strings here.`;
|
|
2391
|
-
routes.push({
|
|
2392
|
-
name: "list",
|
|
2393
|
-
method: "get",
|
|
2394
|
-
path: "/",
|
|
2395
|
-
schema: [`response: { 200: { type: 'array', items: ${selectName} } }`],
|
|
2396
|
-
replyType: `${rowType}[]`,
|
|
2397
|
-
// The stub states its contract twice over: the annotated local is what a reader sees, and the
|
|
2398
|
-
// Reply generic is what Fastify's own types hold the handler to. The response schema is what
|
|
2399
|
-
// the serializer runs; nothing infers a client from any of them, which the docs say plainly.
|
|
2400
|
-
body: [`const rows: ${rowType}[] = [];`, "return rows;"]
|
|
2401
|
-
});
|
|
2402
|
-
if (key) {
|
|
2403
|
-
const keyPath = "/" + key.map((c) => `:${c.name}`).join("/");
|
|
2404
|
-
routes.push({
|
|
2405
|
-
name: "byId",
|
|
2406
|
-
method: "get",
|
|
2407
|
-
path: keyPath,
|
|
2408
|
-
schema: [
|
|
2409
|
-
`params: ${paramsName}`,
|
|
2410
|
-
`response: { 200: ${selectName}, 404: ${NOT_FOUND_SCHEMA} }`
|
|
2411
|
-
],
|
|
2412
|
-
replyType: `${rowType} | { message: string }`,
|
|
2413
|
-
body: [
|
|
2414
|
-
paramHint,
|
|
2415
|
-
`const row: ${rowType} | null = null;`,
|
|
2416
|
-
"if (row !== null) return row;",
|
|
2417
|
-
`return reply.code(404).send({ message: ${lit3(`${table.tsName} row not found`)} });`
|
|
2418
|
-
]
|
|
2419
|
-
});
|
|
2420
|
-
if (writable) {
|
|
2421
|
-
routes.push({
|
|
2422
|
-
name: "update",
|
|
2423
|
-
method: "patch",
|
|
2424
|
-
path: keyPath,
|
|
2425
|
-
schema: [
|
|
2426
|
-
`params: ${paramsName}`,
|
|
2427
|
-
`body: ${updateName}`,
|
|
2428
|
-
`response: { 200: ${selectName} }`
|
|
2429
|
-
],
|
|
2430
|
-
replyType: rowType,
|
|
2431
|
-
body: [notImplemented("update")]
|
|
2432
|
-
});
|
|
2433
|
-
routes.push({
|
|
2434
|
-
name: "delete",
|
|
2435
|
-
method: "delete",
|
|
2436
|
-
path: keyPath,
|
|
2437
|
-
schema: [`params: ${paramsName}`, `response: { 200: { type: 'boolean' } }`],
|
|
2438
|
-
replyType: "boolean",
|
|
2439
|
-
body: [paramHint, "return true;"]
|
|
2440
|
-
});
|
|
2441
|
-
}
|
|
2442
|
-
}
|
|
2443
|
-
if (writable) {
|
|
2444
|
-
routes.push({
|
|
2445
|
-
name: "create",
|
|
2446
|
-
method: "post",
|
|
2447
|
-
path: "/",
|
|
2448
|
-
schema: [`body: ${insertName}`, `response: { 200: ${selectName} }`],
|
|
2449
|
-
replyType: rowType,
|
|
2450
|
-
body: [notImplemented("create")]
|
|
2451
|
-
});
|
|
2452
|
-
}
|
|
2453
|
-
if (opts.includeRelations) {
|
|
2454
|
-
routes.push(...relationRoutes3(table, rowType, selectName, opts));
|
|
2455
|
-
}
|
|
2456
|
-
const order = ["list", "byId", "create", "update", "delete"];
|
|
2457
|
-
const rank = (n) => order.indexOf(n) === -1 ? order.length : order.indexOf(n);
|
|
2458
|
-
routes.sort((a, b) => rank(a.name) - rank(b.name));
|
|
2459
|
-
const exportName = routesExportName3(table, opts.naming);
|
|
2460
|
-
const statements = routes.map((r) => {
|
|
2461
|
-
const reads = (what) => r.body.some((line) => !line.startsWith("//") && what.test(line));
|
|
2462
|
-
const params = reads(/\breply\./) ? reads(/\breq\./) ? "(req, reply)" : "(_req, reply)" : reads(/\breq\./) ? "(req)" : "()";
|
|
2463
|
-
return [
|
|
2464
|
-
` app.${r.method}<{ Reply: ${r.replyType} }>(${lit3(r.path)}, {`,
|
|
2465
|
-
` schema: { ${r.schema.join(", ")} },`,
|
|
2466
|
-
` }, async ${params} => {`,
|
|
2467
|
-
...r.body.map((line) => ` ${line}`),
|
|
2468
|
-
` });`
|
|
2469
|
-
].join("\n");
|
|
2470
|
-
}).join("\n\n");
|
|
2471
|
-
const plugin = `export const ${exportName}: FastifyPluginAsync = async (app) => {
|
|
2472
|
-
${statements}
|
|
2473
|
-
};
|
|
2474
|
-
`;
|
|
2475
|
-
const schemas = tableSchemas(table);
|
|
2476
|
-
const declared = [];
|
|
2477
|
-
const emit = (name, schema) => `export const ${name} = ${JSON.stringify(adaptForFastify(schema), null, 2)} as const;`;
|
|
2478
|
-
if (writable) {
|
|
2479
|
-
declared.push(emit(insertName, schemas.insert));
|
|
2480
|
-
declared.push(emit(updateName, schemas.update));
|
|
2481
|
-
}
|
|
2482
|
-
declared.push(emit(selectName, schemas.select));
|
|
2483
|
-
if (key) {
|
|
2484
|
-
declared.push(emit(paramsName, paramsSchema(key)));
|
|
2485
|
-
}
|
|
2486
|
-
const rowFields = (0, import_validation_core12.selectColumns)(table).map((c) => ` ${objectKey4(c.name)}: ${rowFieldType(c)}${c.nullable ? " | null" : ""};`).join("\n");
|
|
2487
|
-
declared.push(`export interface ${rowType} {
|
|
2488
|
-
${rowFields}
|
|
2489
|
-
}`);
|
|
2490
|
-
const wide = (0, import_validation_core12.selectColumns)(table).filter((c) => rowFieldType(c) === "unknown").map((c) => c.name);
|
|
2491
|
-
const wideNote = wide.length ? `// No precise type for ${wide.length === 1 ? "this column" : "these columns"}: ${wide.join(", ")}.
|
|
2492
|
-
// DRZL could not derive one from the schema, so these routes carry it as unknown and its
|
|
2493
|
-
// schema constrains only what the builder could state.
|
|
2494
|
-
` : "";
|
|
2495
|
-
return `// Generated by @drzl/generator-fastify
|
|
2496
|
-
// Routes for table: ${table.name}
|
|
2497
|
-
${wideNote}import type { FastifyPluginAsync } from 'fastify';
|
|
2498
|
-
|
|
2499
|
-
${declared.join("\n\n")}
|
|
2500
|
-
|
|
2501
|
-
${plugin}`;
|
|
2502
|
-
}
|
|
2503
|
-
function relationRoutes3(table, rowType, selectName, opts) {
|
|
2504
|
-
const out = [];
|
|
2505
|
-
const taken = /* @__PURE__ */ new Set();
|
|
2506
|
-
for (const fk of table.foreignKeys ?? []) {
|
|
2507
|
-
if (fk.columns.length !== 1) continue;
|
|
2508
|
-
const colName = fk.columns[0];
|
|
2509
|
-
const column = table.columns.find((c) => c.name === colName);
|
|
2510
|
-
if (!column) continue;
|
|
2511
|
-
const segment = toCase4(`by-${colName}`, opts.naming?.procedureCase ?? "kebab");
|
|
2512
|
-
if (taken.has(segment)) continue;
|
|
2513
|
-
taken.add(segment);
|
|
2514
|
-
const params = JSON.stringify(paramsSchema([column]));
|
|
2515
|
-
out.push({
|
|
2516
|
-
name: `listBy${cap4(colName)}`,
|
|
2517
|
-
method: "get",
|
|
2518
|
-
path: `/${segment}/:${colName}`,
|
|
2519
|
-
schema: [`params: ${params}`, `response: { 200: { type: 'array', items: ${selectName} } }`],
|
|
2520
|
-
replyType: `${rowType}[]`,
|
|
2521
|
-
body: [`const rows: ${rowType}[] = [];`, "return rows;"]
|
|
2522
|
-
});
|
|
2523
|
-
}
|
|
2524
|
-
return out;
|
|
2525
|
-
}
|
|
2526
|
-
function renderBarrel4(modules, ctx, path9, opts) {
|
|
2527
|
-
if (!modules.length) {
|
|
2528
|
-
return `// Generated by @drzl/generator-fastify
|
|
2529
|
-
// No tables detected in analysis. Add tables to your schema and regenerate.
|
|
2530
|
-
import type { FastifyPluginAsync } from 'fastify';
|
|
2531
|
-
|
|
2532
|
-
export const routes: FastifyPluginAsync = async () => {};
|
|
2533
|
-
`;
|
|
2534
|
-
}
|
|
2535
|
-
const entries = modules.map(({ filePath, exportName, table }) => ({
|
|
2536
|
-
rel: (0, import_validation_core12.importSpecifier)(
|
|
2537
|
-
"./" + path9.relative(ctx.out, filePath).replace(/\\/g, "/"),
|
|
2538
|
-
opts.importExtension
|
|
2539
|
-
),
|
|
2540
|
-
exportName,
|
|
2541
|
-
mount: mountPath3(table, opts.naming)
|
|
2542
|
-
}));
|
|
2543
|
-
const imports = entries.map((e) => `import { ${e.exportName} } from '${e.rel}';`).join("\n");
|
|
2544
|
-
const registrations = entries.map((e) => ` app.register(${e.exportName}, { prefix: ${lit3(e.mount)} });`).join("\n");
|
|
2545
|
-
const reExports = entries.map((e) => `export * from '${e.rel}';`).join("\n");
|
|
2546
|
-
return `// Generated by @drzl/generator-fastify
|
|
2547
|
-
import type { FastifyPluginAsync } from 'fastify';
|
|
2548
|
-
${imports}
|
|
2549
|
-
|
|
2550
|
-
export const routes: FastifyPluginAsync = async (app) => {
|
|
2551
|
-
${registrations}
|
|
2552
|
-
};
|
|
2553
|
-
|
|
2554
|
-
${reExports}
|
|
2555
|
-
`;
|
|
2556
|
-
}
|
|
2557
|
-
var import_validation_core11, import_validation_core12, APP_MODULE3, lit3, NUMERIC_SEGMENT_PATTERN, BIGINT_SEGMENT_PATTERN, NOT_FOUND_SCHEMA, cap4, isIdent4, FastifyGenerator, index_default5;
|
|
2558
|
-
var init_dist5 = __esm({
|
|
2559
|
-
"../generator-fastify/dist/index.js"() {
|
|
2560
|
-
"use strict";
|
|
2561
|
-
import_validation_core11 = require("@drzl/validation-core");
|
|
2562
|
-
init_dist4();
|
|
2563
|
-
import_validation_core12 = require("@drzl/validation-core");
|
|
2564
|
-
APP_MODULE3 = "index";
|
|
2565
|
-
lit3 = (v) => /['\\]/.test(v) ? JSON.stringify(v) : `'${v}'`;
|
|
2566
|
-
NUMERIC_SEGMENT_PATTERN = "^-?\\d+(\\.\\d+)?$";
|
|
2567
|
-
BIGINT_SEGMENT_PATTERN = "^-?\\d+$";
|
|
2568
|
-
NOT_FOUND_SCHEMA = "{ type: 'object', properties: { message: { type: 'string' } }, required: ['message'], additionalProperties: false }";
|
|
2569
|
-
cap4 = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
2570
|
-
isIdent4 = (s) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s);
|
|
2571
|
-
FastifyGenerator = class {
|
|
2572
|
-
constructor(analysis) {
|
|
2573
|
-
this.analysis = analysis;
|
|
2574
|
-
}
|
|
2575
|
-
async generate(opts) {
|
|
2576
|
-
const fs6 = (0, import_validation_core11.fileWriter)(opts.fileSink);
|
|
2577
|
-
const path9 = await import("path");
|
|
2578
|
-
const out = path9.resolve(process.cwd(), opts.outputDir);
|
|
2579
|
-
const ctx = { out };
|
|
2580
|
-
await fs6.mkdir(out, { recursive: true });
|
|
2581
|
-
const files = [];
|
|
2582
|
-
const write = async (filePath, content) => {
|
|
2583
|
-
const formatted = await (0, import_validation_core12.formatCode)(
|
|
2584
|
-
buildHeader5(opts.outputHeader) + content,
|
|
2585
|
-
filePath,
|
|
2586
|
-
opts.format
|
|
2587
|
-
);
|
|
2588
|
-
await fs6.writeFile(filePath, formatted, "utf8");
|
|
2589
|
-
files.push(filePath);
|
|
2590
|
-
};
|
|
2591
|
-
const barrelPath = path9.join(out, `${APP_MODULE3}.ts`);
|
|
2592
|
-
const modules = [];
|
|
2593
|
-
const total = this.analysis.tables.length;
|
|
2594
|
-
let index = 0;
|
|
2595
|
-
for (const table of this.analysis.tables) {
|
|
2596
|
-
const base = `${table.tsName}${opts.naming?.routerSuffix ?? ""}`;
|
|
2597
|
-
const filePath = path9.join(out, `${toCase4(base, opts.naming?.procedureCase)}.ts`);
|
|
2598
|
-
if (filePath === barrelPath) {
|
|
2599
|
-
throw new Error(
|
|
2600
|
-
`@drzl/generator-fastify: the routes for table "${table.name}" would be written to ${filePath}, which is the barrel this generator also writes. Set naming.routerSuffix to move it out of the way.`
|
|
2601
|
-
);
|
|
2602
|
-
}
|
|
2603
|
-
await write(filePath, renderRoutes3(table, opts));
|
|
2604
|
-
modules.push({ table, filePath, exportName: routesExportName3(table, opts.naming) });
|
|
2605
|
-
index++;
|
|
2606
|
-
opts.onProgress?.({ index, total, table: table.name, filePath });
|
|
2607
|
-
}
|
|
2608
|
-
await write(barrelPath, renderBarrel4(modules, ctx, path9, opts));
|
|
2609
|
-
return { files };
|
|
2610
|
-
}
|
|
2611
|
-
};
|
|
2612
|
-
index_default5 = FastifyGenerator;
|
|
2613
|
-
}
|
|
2614
|
-
});
|
|
2615
|
-
|
|
2616
|
-
// ../generator-nestjs/dist/index.js
|
|
2617
|
-
var dist_exports6 = {};
|
|
2618
|
-
__export(dist_exports6, {
|
|
2619
|
-
APP_MODULE: () => APP_MODULE4,
|
|
2620
|
-
NestJSGenerator: () => NestJSGenerator,
|
|
2621
|
-
VALIDATION_MODULE: () => VALIDATION_MODULE2,
|
|
2622
|
-
default: () => index_default6
|
|
2623
|
-
});
|
|
2624
|
-
function keyColumns6(table) {
|
|
2625
|
-
const names = table.primaryKey?.columns ?? [];
|
|
2626
|
-
if (!names.length) return null;
|
|
2627
|
-
const cols = names.map((n) => table.columns.find((c) => c.name === n));
|
|
2628
|
-
if (cols.some((c) => !c)) return null;
|
|
2629
|
-
return cols;
|
|
2630
|
-
}
|
|
2631
|
-
function isWide4(column) {
|
|
2632
|
-
if (column.enumValues && column.enumValues.length) return false;
|
|
2633
|
-
if (column.shape?.kind === "tuple" || column.shape?.kind === "numberObject") return false;
|
|
2634
|
-
return !["number", "string", "boolean", "Date", "bigint"].includes(column.tsType);
|
|
2635
|
-
}
|
|
2636
|
-
function baseExpr(column, d, mode) {
|
|
2637
|
-
if (column.enumValues && column.enumValues.length) return d.enum(column.enumValues);
|
|
2638
|
-
switch (column.tsType) {
|
|
2639
|
-
case "number":
|
|
2640
|
-
return d.number;
|
|
2641
|
-
case "string":
|
|
2642
|
-
return d.string;
|
|
2643
|
-
case "boolean":
|
|
2644
|
-
return d.boolean;
|
|
2645
|
-
case "Date":
|
|
2646
|
-
return mode === "select" ? d.date : d.dateInput;
|
|
2647
|
-
case "bigint":
|
|
2648
|
-
return d.bigint;
|
|
2649
|
-
default:
|
|
2650
|
-
return d.unknown;
|
|
2651
|
-
}
|
|
2652
|
-
}
|
|
2653
|
-
function mapExpr4(column, lib, mode) {
|
|
2654
|
-
const d = LIBS4[lib];
|
|
2655
|
-
let expr = baseExpr(column, d, mode);
|
|
2656
|
-
if (column.nullable) expr = d.nullable(expr);
|
|
2657
|
-
if (mode === "update" || mode === "insert" && column.hasDefault) expr = d.optional(expr);
|
|
2658
|
-
return expr;
|
|
2659
|
-
}
|
|
2660
|
-
function objectKey5(name) {
|
|
2661
|
-
return isIdent5(name) ? name : JSON.stringify(name);
|
|
2662
|
-
}
|
|
2663
|
-
function field4(column, lib, mode) {
|
|
2664
|
-
const d = LIBS4[lib];
|
|
2665
|
-
const expr = mapExpr4(column, lib, mode);
|
|
2666
|
-
return `${objectKey5(column.name)}: ${d.fieldIsString ? JSON.stringify(expr) : expr}`;
|
|
2667
|
-
}
|
|
2668
|
-
function paramField3(column, lib) {
|
|
2669
|
-
const d = LIBS4[lib];
|
|
2670
|
-
const expr = (() => {
|
|
2671
|
-
if (column.enumValues && column.enumValues.length) return d.enum(column.enumValues);
|
|
2672
|
-
return d.coerce(column.tsType) ?? d.string;
|
|
2673
|
-
})();
|
|
2674
|
-
return `${objectKey5(column.name)}: ${d.fieldIsString ? JSON.stringify(expr) : expr}`;
|
|
2675
|
-
}
|
|
2676
|
-
function renderSchema4(cols, lib, mode) {
|
|
2677
|
-
const d = LIBS4[lib];
|
|
2678
|
-
const body = cols.map((c) => ` ${field4(c, lib, mode)},`).join("\n");
|
|
2679
|
-
return d.object(body);
|
|
2680
|
-
}
|
|
2681
|
-
function fieldType(column) {
|
|
2682
|
-
if (column.enumValues && column.enumValues.length) {
|
|
2683
|
-
return column.enumValues.map((x) => `'${x.replace(/'/g, "\\'")}'`).join(" | ");
|
|
2684
|
-
}
|
|
2685
|
-
switch (column.tsType) {
|
|
2686
|
-
case "number":
|
|
2687
|
-
return "number";
|
|
2688
|
-
case "string":
|
|
2689
|
-
return "string";
|
|
2690
|
-
case "boolean":
|
|
2691
|
-
return "boolean";
|
|
2692
|
-
case "Date":
|
|
2693
|
-
return "Date";
|
|
2694
|
-
case "bigint":
|
|
2695
|
-
return "string";
|
|
2696
|
-
default:
|
|
2697
|
-
return "unknown";
|
|
2698
|
-
}
|
|
2699
|
-
}
|
|
2700
|
-
function paramFieldType(column) {
|
|
2701
|
-
if (column.enumValues && column.enumValues.length) return fieldType(column);
|
|
2702
|
-
switch (column.tsType) {
|
|
2703
|
-
case "number":
|
|
2704
|
-
return "number";
|
|
2705
|
-
case "Date":
|
|
2706
|
-
return "Date";
|
|
2707
|
-
default:
|
|
2708
|
-
return "string";
|
|
2709
|
-
}
|
|
2710
|
-
}
|
|
2711
|
-
function classField(column, mode) {
|
|
2712
|
-
const key = isIdent5(column.name) ? column.name : `'${column.name.replace(/'/g, "\\'")}'`;
|
|
2713
|
-
if (mode === "params") return ` ${key}!: ${paramFieldType(column)};`;
|
|
2714
|
-
const optional = mode === "update" || mode === "insert" && column.hasDefault;
|
|
2715
|
-
const type = `${fieldType(column)}${column.nullable ? " | null" : ""}`;
|
|
2716
|
-
return optional ? ` ${key}?: ${type};` : ` ${key}!: ${type};`;
|
|
2717
|
-
}
|
|
2718
|
-
function toCase5(s, c) {
|
|
2719
|
-
if (!c) return s;
|
|
2720
|
-
const parts = s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]/g, " ").split(/\s+/);
|
|
2721
|
-
if (c === "camel") {
|
|
2722
|
-
return parts.map(
|
|
2723
|
-
(p, i) => i === 0 ? p.toLowerCase() : p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()
|
|
2724
|
-
).join("");
|
|
2725
|
-
}
|
|
2726
|
-
if (c === "kebab") return parts.map((p) => p.toLowerCase()).join("-");
|
|
2727
|
-
if (c === "snake") return parts.map((p) => p.toLowerCase()).join("_");
|
|
2728
|
-
return s;
|
|
2729
|
-
}
|
|
2730
|
-
function buildHeader6(h) {
|
|
2731
|
-
if (h && h.enabled === false) return "";
|
|
2732
|
-
const text = h?.text?.trim();
|
|
2733
|
-
const lines = text ? text.split(/\r?\n/).map((l) => `// ${l}`) : [
|
|
2734
|
-
"// Generated by DRZL (@drzl/*)",
|
|
2735
|
-
"// Generated output is granted to you under your project's license.",
|
|
2736
|
-
"// You may use, copy, modify, and distribute without attribution."
|
|
2737
|
-
];
|
|
2738
|
-
return lines.join("\n") + "\n\n";
|
|
2739
|
-
}
|
|
2740
|
-
function renderValidationModule2() {
|
|
2741
|
-
return `// Generated by @drzl/generator-nestjs
|
|
2742
|
-
// A validation pipe over Standard Schema v1. Bind it globally
|
|
2743
|
-
// (app.useGlobalPipes(new SchemaValidationPipe())) or per handler with @UsePipes. A parameter
|
|
2744
|
-
// whose class carries a static Standard Schema (every DTO in this directory) is validated and
|
|
2745
|
-
// replaced by the parsed output; anything else, primitives and foreign DTOs alike, passes
|
|
2746
|
-
// through untouched. On failure it throws BadRequestException with
|
|
2747
|
-
// { error, slot, issues: [{ message, path }] }, which Nest answers as a 400.
|
|
2748
|
-
//
|
|
2749
|
-
// These DTOs do not use Nest's class-validator ValidationPipe and assume nothing about its
|
|
2750
|
-
// options. If your app also binds one globally, note one measured interaction: with
|
|
2751
|
-
// whitelist: true it strips every property of these classes before this pipe runs, because
|
|
2752
|
-
// they carry no class-validator metadata.
|
|
2753
|
-
import { BadRequestException, type ArgumentMetadata, type PipeTransform } from '@nestjs/common';
|
|
2754
|
-
|
|
2755
|
-
interface StandardIssue {
|
|
2756
|
-
readonly message: string;
|
|
2757
|
-
readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }> | undefined;
|
|
2758
|
-
}
|
|
2759
|
-
|
|
2760
|
-
type StandardResult<Output> =
|
|
2761
|
-
| { readonly value: Output; readonly issues?: undefined }
|
|
2762
|
-
| { readonly issues: ReadonlyArray<StandardIssue> };
|
|
2763
|
-
|
|
2764
|
-
export interface StandardSchema<Output = unknown> {
|
|
2765
|
-
readonly '~standard': {
|
|
2766
|
-
readonly version: 1;
|
|
2767
|
-
readonly vendor: string;
|
|
2768
|
-
readonly validate: (
|
|
2769
|
-
value: unknown
|
|
2770
|
-
) => StandardResult<Output> | Promise<StandardResult<Output>>;
|
|
2771
|
-
};
|
|
2772
|
-
}
|
|
2773
|
-
|
|
2774
|
-
interface CarriesSchema {
|
|
2775
|
-
readonly schema: StandardSchema;
|
|
2776
|
-
}
|
|
2777
|
-
|
|
2778
|
-
function carriesSchema(metatype: unknown): metatype is CarriesSchema {
|
|
2779
|
-
return (
|
|
2780
|
-
typeof metatype === 'function' &&
|
|
2781
|
-
typeof (metatype as { schema?: { '~standard'?: { validate?: unknown } } }).schema?.[
|
|
2782
|
-
'~standard'
|
|
2783
|
-
]?.validate === 'function'
|
|
2784
|
-
);
|
|
2785
|
-
}
|
|
2786
|
-
|
|
2787
|
-
export class SchemaValidationPipe implements PipeTransform {
|
|
2788
|
-
async transform(value: unknown, metadata: ArgumentMetadata): Promise<unknown> {
|
|
2789
|
-
const { metatype } = metadata;
|
|
2790
|
-
if (!carriesSchema(metatype)) return value;
|
|
2791
|
-
const result = await metatype.schema['~standard'].validate(value);
|
|
2792
|
-
if (result.issues) {
|
|
2793
|
-
throw new BadRequestException({
|
|
2794
|
-
error: 'Validation failed',
|
|
2795
|
-
slot: metadata.type,
|
|
2796
|
-
issues: result.issues.map((issue) => ({
|
|
2797
|
-
message: issue.message,
|
|
2798
|
-
path: (issue.path ?? []).map((p) => (typeof p === 'object' && p !== null ? p.key : p)),
|
|
2799
|
-
})),
|
|
2800
|
-
});
|
|
2801
|
-
}
|
|
2802
|
-
return result.value;
|
|
2803
|
-
}
|
|
2804
|
-
}
|
|
2805
|
-
`;
|
|
2806
|
-
}
|
|
2807
|
-
function renderTable(table, opts) {
|
|
2808
|
-
const lib = opts.validation?.library ?? "zod";
|
|
2809
|
-
const d = LIBS4[lib];
|
|
2810
|
-
const insertName = `Insert${table.tsName}Schema`;
|
|
2811
|
-
const updateName = `Update${table.tsName}Schema`;
|
|
2812
|
-
const selectName = `Select${table.tsName}Schema`;
|
|
2813
|
-
const paramsName = `${cap5(table.tsName)}ParamsSchema`;
|
|
2814
|
-
const createDto = `Create${cap5(table.tsName)}Dto`;
|
|
2815
|
-
const updateDto = `Update${cap5(table.tsName)}Dto`;
|
|
2816
|
-
const paramsDto = `${cap5(table.tsName)}ParamsDto`;
|
|
2817
|
-
const entity = `${cap5(table.tsName)}Entity`;
|
|
2818
|
-
const writable = !table.readOnly;
|
|
2819
|
-
const key = keyColumns6(table);
|
|
2820
|
-
const declared = [];
|
|
2821
|
-
const insertCols = (0, import_validation_core14.insertColumns)(table);
|
|
2822
|
-
const updateCols = (0, import_validation_core14.updateColumns)(table);
|
|
2823
|
-
const selectCols = (0, import_validation_core14.selectColumns)(table);
|
|
2824
|
-
if (writable) {
|
|
2825
|
-
declared.push(`export const ${insertName} = ${renderSchema4(insertCols, lib, "insert")};`);
|
|
2826
|
-
declared.push(`export const ${updateName} = ${renderSchema4(updateCols, lib, "update")};`);
|
|
2827
|
-
}
|
|
2828
|
-
declared.push(`export const ${selectName} = ${renderSchema4(selectCols, lib, "select")};`);
|
|
2829
|
-
if (key) {
|
|
2830
|
-
declared.push(
|
|
2831
|
-
`export const ${paramsName} = ${d.objectInline(key.map((c) => paramField3(c, lib)).join(", "))};`
|
|
2832
|
-
);
|
|
2833
|
-
}
|
|
2834
|
-
const dtoClass = (name, doc, cols, mode, schema) => {
|
|
2835
|
-
const fields = cols.map((c) => classField(c, mode)).join("\n");
|
|
2836
|
-
return `/**
|
|
2837
|
-
* ${doc}
|
|
2838
|
-
*/
|
|
2839
|
-
export class ${name} {
|
|
2840
|
-
${fields}
|
|
2841
|
-
|
|
2842
|
-
static readonly schema: StandardSchema<${name}> = ${schema};
|
|
2843
|
-
}`;
|
|
2844
|
-
};
|
|
2845
|
-
if (writable) {
|
|
2846
|
-
declared.push(
|
|
2847
|
-
dtoClass(
|
|
2848
|
-
createDto,
|
|
2849
|
-
`The insert shape of ${table.name}. Fields state what the pipe hands your controller; a nullable column with no default is required, null spelled out.`,
|
|
2850
|
-
insertCols,
|
|
2851
|
-
"insert",
|
|
2852
|
-
insertName
|
|
2853
|
-
)
|
|
2854
|
-
);
|
|
2855
|
-
declared.push(
|
|
2856
|
-
dtoClass(
|
|
2857
|
-
updateDto,
|
|
2858
|
-
`The update shape of ${table.name}: every field optional, primary key excluded.`,
|
|
2859
|
-
updateCols,
|
|
2860
|
-
"update",
|
|
2861
|
-
updateName
|
|
2862
|
-
)
|
|
2863
|
-
);
|
|
2864
|
-
}
|
|
2865
|
-
if (key) {
|
|
2866
|
-
declared.push(
|
|
2867
|
-
dtoClass(
|
|
2868
|
-
paramsDto,
|
|
2869
|
-
`The path parameters addressing one ${table.name} row, parsed strictly from their string segments.`,
|
|
2870
|
-
key,
|
|
2871
|
-
"params",
|
|
2872
|
-
paramsName
|
|
2873
|
-
)
|
|
2874
|
-
);
|
|
2875
|
-
}
|
|
2876
|
-
declared.push(
|
|
2877
|
-
dtoClass(
|
|
2878
|
-
entity,
|
|
2879
|
-
`One ${table.name} row, at its select shape.`,
|
|
2880
|
-
selectCols,
|
|
2881
|
-
"select",
|
|
2882
|
-
selectName
|
|
2883
|
-
)
|
|
2884
|
-
);
|
|
2885
|
-
const decided = declared.join("\n\n");
|
|
2886
|
-
const imports = [];
|
|
2887
|
-
imports.push(LIB_IMPORTS4[lib]);
|
|
2888
|
-
const spec = (0, import_validation_core14.importSpecifier)(`./${VALIDATION_MODULE2}.ts`, opts.importExtension);
|
|
2889
|
-
imports.push(`import type { StandardSchema } from '${spec}';`);
|
|
2890
|
-
const wide = table.columns.filter(isWide4).map((c) => c.name);
|
|
2891
|
-
const wideNote = wide.length ? `// No validated type for ${wide.length === 1 ? "this column" : "these columns"}: ${wide.join(", ")}.
|
|
2892
|
-
// DRZL could not derive one from the schema, so these DTOs accept any value there.
|
|
2893
|
-
` : "";
|
|
2894
|
-
return `// Generated by @drzl/generator-nestjs
|
|
2895
|
-
// DTOs for table: ${table.name}
|
|
2896
|
-
${wideNote}${imports.join("\n")}
|
|
2897
|
-
|
|
2898
|
-
${decided}
|
|
2899
|
-
`;
|
|
2900
|
-
}
|
|
2901
|
-
function renderBarrel5(modules, ctx, path9, opts) {
|
|
2902
|
-
const validationSpec = (0, import_validation_core14.importSpecifier)(`./${VALIDATION_MODULE2}.ts`, opts.importExtension);
|
|
2903
|
-
if (!modules.length) {
|
|
2904
|
-
return `// Generated by @drzl/generator-nestjs
|
|
2905
|
-
// No tables detected in analysis. Add tables to your schema and regenerate.
|
|
2906
|
-
export * from '${validationSpec}';
|
|
2907
|
-
`;
|
|
2908
|
-
}
|
|
2909
|
-
const reExports = modules.map(({ filePath }) => {
|
|
2910
|
-
const rel = (0, import_validation_core14.importSpecifier)(
|
|
2911
|
-
"./" + path9.relative(ctx.out, filePath).replace(/\\/g, "/"),
|
|
2912
|
-
opts.importExtension
|
|
2913
|
-
);
|
|
2914
|
-
return `export * from '${rel}';`;
|
|
2915
|
-
}).join("\n");
|
|
2916
|
-
return `// Generated by @drzl/generator-nestjs
|
|
2917
|
-
${reExports}
|
|
2918
|
-
export * from '${validationSpec}';
|
|
2919
|
-
`;
|
|
2920
|
-
}
|
|
2921
|
-
var import_validation_core13, import_validation_core14, APP_MODULE4, VALIDATION_MODULE2, q4, NUMERIC_SEGMENT3, BIGINT_DIGITS, LIB_IMPORTS4, LIBS4, cap5, isIdent5, NestJSGenerator, index_default6;
|
|
2922
|
-
var init_dist6 = __esm({
|
|
2923
|
-
"../generator-nestjs/dist/index.js"() {
|
|
2924
|
-
"use strict";
|
|
2925
|
-
import_validation_core13 = require("@drzl/validation-core");
|
|
2926
|
-
import_validation_core14 = require("@drzl/validation-core");
|
|
2927
|
-
APP_MODULE4 = "index";
|
|
2928
|
-
VALIDATION_MODULE2 = "validation";
|
|
2929
|
-
q4 = (v) => JSON.stringify(v);
|
|
2930
|
-
NUMERIC_SEGMENT3 = String.raw`/^-?\d+(\.\d+)?$/`;
|
|
2931
|
-
BIGINT_DIGITS = String.raw`/^-?\d+$/`;
|
|
2932
|
-
LIB_IMPORTS4 = {
|
|
2933
|
-
zod: "import { z } from 'zod';",
|
|
2934
|
-
valibot: "import * as v from 'valibot';",
|
|
2935
|
-
arktype: "import { type } from 'arktype';"
|
|
2936
|
-
};
|
|
2937
|
-
LIBS4 = {
|
|
2938
|
-
zod: {
|
|
2939
|
-
number: "z.number()",
|
|
2940
|
-
string: "z.string()",
|
|
2941
|
-
boolean: "z.boolean()",
|
|
2942
|
-
date: "z.date()",
|
|
2943
|
-
dateInput: "z.iso.datetime().transform((s) => new Date(s))",
|
|
2944
|
-
bigint: `z.string().regex(${BIGINT_DIGITS})`,
|
|
2945
|
-
unknown: "z.unknown()",
|
|
2946
|
-
enum: (vals) => `z.enum([${vals.map(q4).join(", ")}] as const)`,
|
|
2947
|
-
nullable: (b) => `${b}.nullable()`,
|
|
2948
|
-
optional: (b) => `${b}.optional()`,
|
|
2949
|
-
object: (body) => `z.object({
|
|
2950
|
-
${body}
|
|
2951
|
-
})`,
|
|
2952
|
-
objectInline: (body) => `z.object({ ${body} })`,
|
|
2953
|
-
coerce: (t) => t === "number" ? `z.string().regex(${NUMERIC_SEGMENT3}).transform(Number)` : t === "Date" ? "z.iso.datetime().transform((s) => new Date(s))" : t === "bigint" ? `z.string().regex(${BIGINT_DIGITS})` : null
|
|
2954
|
-
},
|
|
2955
|
-
valibot: {
|
|
2956
|
-
number: "v.number()",
|
|
2957
|
-
string: "v.string()",
|
|
2958
|
-
boolean: "v.boolean()",
|
|
2959
|
-
date: "v.date()",
|
|
2960
|
-
dateInput: "v.pipe(v.string(), v.isoTimestamp(), v.transform((s) => new Date(s)))",
|
|
2961
|
-
bigint: `v.pipe(v.string(), v.regex(${BIGINT_DIGITS}))`,
|
|
2962
|
-
unknown: "v.unknown()",
|
|
2963
|
-
enum: (vals) => `v.picklist([${vals.map(q4).join(", ")}] as const)`,
|
|
2964
|
-
nullable: (b) => `v.nullable(${b})`,
|
|
2965
|
-
optional: (b) => `v.optional(${b})`,
|
|
2966
|
-
object: (body) => `v.object({
|
|
2967
|
-
${body}
|
|
2968
|
-
})`,
|
|
2969
|
-
objectInline: (body) => `v.object({ ${body} })`,
|
|
2970
|
-
// A valibot pipe step sees the previous step's *output*, so the check has to happen while
|
|
2971
|
-
// the value is still the string: after a `v.transform(Number)` there is no string left.
|
|
2972
|
-
coerce: (t) => t === "number" ? `v.pipe(v.string(), v.regex(${NUMERIC_SEGMENT3}), v.transform(Number))` : t === "Date" ? "v.pipe(v.string(), v.isoTimestamp(), v.transform((s) => new Date(s)))" : t === "bigint" ? `v.pipe(v.string(), v.regex(${BIGINT_DIGITS}))` : null
|
|
2973
|
-
},
|
|
2974
|
-
arktype: {
|
|
2975
|
-
number: "number",
|
|
2976
|
-
string: "string",
|
|
2977
|
-
boolean: "boolean",
|
|
2978
|
-
date: "Date",
|
|
2979
|
-
dateInput: "string.date.iso.parse",
|
|
2980
|
-
bigint: BIGINT_DIGITS,
|
|
2981
|
-
unknown: "unknown",
|
|
2982
|
-
// The surrounding encode adds the quotes, so the union is built with the inner quoting
|
|
2983
|
-
// ArkType expects.
|
|
2984
|
-
enum: (vals) => vals.map((x) => `'${x.replace(/'/g, "\\'")}'`).join(" | "),
|
|
2985
|
-
nullable: (b) => `(${b} | null)`,
|
|
2986
|
-
optional: (b) => `${b}?`,
|
|
2987
|
-
// `.onUndeclaredKey('delete')` because ArkType's default keeps undeclared keys (measured on
|
|
2988
|
-
// 2.2.3), where zod and valibot strip them. A DTO's whole point is that the controller
|
|
2989
|
-
// receives the declared shape, so the three libraries are aligned on the strict side.
|
|
2990
|
-
object: (body) => `type({
|
|
2991
|
-
${body}
|
|
2992
|
-
}).onUndeclaredKey('delete')`,
|
|
2993
|
-
objectInline: (body) => `type({ ${body} }).onUndeclaredKey('delete')`,
|
|
2994
|
-
fieldIsString: true,
|
|
2995
|
-
// ArkType ships these as keywords, and they are morphs: the declared output type is the
|
|
2996
|
-
// parsed one. `string.date.iso.parse` and not `string.date.parse`, which accepts `"1"` as
|
|
2997
|
-
// the year 2001.
|
|
2998
|
-
coerce: (t) => t === "number" ? "string.numeric.parse" : t === "Date" ? "string.date.iso.parse" : t === "bigint" ? BIGINT_DIGITS : null
|
|
2999
|
-
}
|
|
3000
|
-
};
|
|
3001
|
-
cap5 = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
3002
|
-
isIdent5 = (s) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s);
|
|
3003
|
-
NestJSGenerator = class {
|
|
3004
|
-
constructor(analysis) {
|
|
3005
|
-
this.analysis = analysis;
|
|
3006
|
-
}
|
|
3007
|
-
async generate(opts) {
|
|
3008
|
-
const fs6 = (0, import_validation_core13.fileWriter)(opts.fileSink);
|
|
3009
|
-
const path9 = await import("path");
|
|
3010
|
-
const out = path9.resolve(process.cwd(), opts.outputDir);
|
|
3011
|
-
const ctx = { out };
|
|
3012
|
-
await fs6.mkdir(out, { recursive: true });
|
|
3013
|
-
const files = [];
|
|
3014
|
-
const write = async (filePath, content) => {
|
|
3015
|
-
const formatted = await (0, import_validation_core14.formatCode)(
|
|
3016
|
-
buildHeader6(opts.outputHeader) + content,
|
|
3017
|
-
filePath,
|
|
3018
|
-
opts.format
|
|
3019
|
-
);
|
|
3020
|
-
await fs6.writeFile(filePath, formatted, "utf8");
|
|
3021
|
-
files.push(filePath);
|
|
3022
|
-
};
|
|
3023
|
-
const barrelPath = path9.join(out, `${APP_MODULE4}.ts`);
|
|
3024
|
-
const validationPath = path9.join(out, `${VALIDATION_MODULE2}.ts`);
|
|
3025
|
-
const modules = [];
|
|
3026
|
-
const total = this.analysis.tables.length;
|
|
3027
|
-
let index = 0;
|
|
3028
|
-
for (const table of this.analysis.tables) {
|
|
3029
|
-
const base = `${table.tsName}${opts.naming?.routerSuffix ?? ""}`;
|
|
3030
|
-
const filePath = path9.join(out, `${toCase5(base, opts.naming?.procedureCase)}.ts`);
|
|
3031
|
-
if (filePath === barrelPath || filePath === validationPath) {
|
|
3032
|
-
const which = filePath === barrelPath ? "the barrel" : "the validation pipe module";
|
|
3033
|
-
throw new Error(
|
|
3034
|
-
`@drzl/generator-nestjs: the DTOs for table "${table.name}" would be written to ${filePath}, which is ${which} this generator also writes. Set naming.routerSuffix to move it out of the way.`
|
|
3035
|
-
);
|
|
3036
|
-
}
|
|
3037
|
-
await write(filePath, renderTable(table, opts));
|
|
3038
|
-
modules.push({ filePath });
|
|
3039
|
-
index++;
|
|
3040
|
-
opts.onProgress?.({ index, total, table: table.name, filePath });
|
|
3041
|
-
}
|
|
3042
|
-
await write(validationPath, renderValidationModule2());
|
|
3043
|
-
await write(barrelPath, renderBarrel5(modules, ctx, path9, opts));
|
|
3044
|
-
return { files };
|
|
3045
|
-
}
|
|
3046
|
-
};
|
|
3047
|
-
index_default6 = NestJSGenerator;
|
|
3048
|
-
}
|
|
3049
|
-
});
|
|
3050
|
-
|
|
3051
|
-
// ../generator-graphql/dist/index.js
|
|
3052
|
-
var dist_exports7 = {};
|
|
3053
|
-
__export(dist_exports7, {
|
|
3054
|
-
APP_MODULE: () => APP_MODULE5,
|
|
3055
|
-
GraphQLGenerator: () => GraphQLGenerator,
|
|
3056
|
-
SCALARS_MODULE: () => SCALARS_MODULE,
|
|
3057
|
-
default: () => index_default7
|
|
3058
|
-
});
|
|
3059
|
-
function gqlIdent(s) {
|
|
3060
|
-
const cleaned = s.replace(/[^_0-9A-Za-z]+/g, "_");
|
|
3061
|
-
return /^[0-9]/.test(cleaned) ? `_${cleaned}` : cleaned;
|
|
3062
|
-
}
|
|
3063
|
-
function tpl(s) {
|
|
3064
|
-
return s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
3065
|
-
}
|
|
3066
|
-
function keyColumns7(table) {
|
|
3067
|
-
const names = table.primaryKey?.columns ?? [];
|
|
3068
|
-
if (!names.length) return null;
|
|
3069
|
-
const cols = names.map((n) => table.columns.find((c) => c.name === n));
|
|
3070
|
-
if (cols.some((c) => !c)) return null;
|
|
3071
|
-
return cols;
|
|
3072
|
-
}
|
|
3073
|
-
function fitsInt32(c) {
|
|
3074
|
-
if (c.min === void 0 || c.max === void 0) return false;
|
|
3075
|
-
try {
|
|
3076
|
-
return BigInt(c.min) >= INT32_MIN && BigInt(c.max) <= INT32_MAX;
|
|
3077
|
-
} catch {
|
|
3078
|
-
return false;
|
|
3079
|
-
}
|
|
3080
|
-
}
|
|
3081
|
-
function planEnum(typeName3, values) {
|
|
3082
|
-
const members = values.map((value) => {
|
|
3083
|
-
const verbatimOk = GRAPHQL_NAME.test(value) && !RESERVED_ENUM_VALUES.has(value) && !value.startsWith("__");
|
|
3084
|
-
if (verbatimOk) return { name: value, value, renamed: false };
|
|
3085
|
-
let name = gqlIdent(value.toUpperCase());
|
|
3086
|
-
name = name.replace(/^_+/, "_");
|
|
3087
|
-
if (!GRAPHQL_NAME.test(name) || RESERVED_ENUM_VALUES.has(name) || name.startsWith("__")) {
|
|
3088
|
-
return { name: "", value, renamed: true };
|
|
3089
|
-
}
|
|
3090
|
-
return { name, value, renamed: true };
|
|
3091
|
-
});
|
|
3092
|
-
if (members.some((m) => !m.name)) return null;
|
|
3093
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3094
|
-
for (const m of members) {
|
|
3095
|
-
if (seen.has(m.name)) return null;
|
|
3096
|
-
seen.add(m.name);
|
|
3097
|
-
}
|
|
3098
|
-
return { typeName: typeName3, members };
|
|
3099
|
-
}
|
|
3100
|
-
function mapped(sdl, rowTs, inputTs = rowTs, scalars = []) {
|
|
3101
|
-
return { sdl, rowTs, inputTs, scalars };
|
|
3102
|
-
}
|
|
3103
|
-
function planColumn(table, c) {
|
|
3104
|
-
const field5 = GRAPHQL_NAME.test(c.name) ? c.name : gqlIdent(c.name);
|
|
3105
|
-
const base = (() => {
|
|
3106
|
-
if (c.enumValues && c.enumValues.length) {
|
|
3107
|
-
const typeName3 = `${cap6(gqlIdent(table.tsName))}${cap6(gqlIdent(c.name))}Enum`;
|
|
3108
|
-
const plan = planEnum(typeName3, c.enumValues);
|
|
3109
|
-
if (!plan) {
|
|
3110
|
-
return {
|
|
3111
|
-
...mapped("String", "string"),
|
|
3112
|
-
note: `Column "${c.name}": the enum values ${c.enumValues.join(", ")} cannot all be spelled as distinct GraphQL enum members, so the column is exposed as String carrying the database values verbatim.`
|
|
3113
|
-
};
|
|
3114
|
-
}
|
|
3115
|
-
const union = c.enumValues.map(q5).join(" | ");
|
|
3116
|
-
return { ...mapped(plan.typeName, union), enumPlan: plan };
|
|
3117
|
-
}
|
|
3118
|
-
switch (c.shape?.kind) {
|
|
3119
|
-
case "tuple":
|
|
3120
|
-
return mapped("[Float!]", "number[]");
|
|
3121
|
-
case "numberObject":
|
|
3122
|
-
return mapped("JSON", "unknown", "unknown", ["JSON"]);
|
|
3123
|
-
case "buffer":
|
|
3124
|
-
return {
|
|
3125
|
-
...mapped("JSON", "unknown", "unknown", ["JSON"]),
|
|
3126
|
-
note: `Column "${c.name}" is binary; GraphQL has no binary type, so it rides the JSON scalar and your resolver picks an encoding.`
|
|
3127
|
-
};
|
|
3128
|
-
case "json":
|
|
3129
|
-
return mapped("JSON", "unknown", "unknown", ["JSON"]);
|
|
3130
|
-
case "bitstring":
|
|
3131
|
-
case "byteString":
|
|
3132
|
-
return mapped("String", "string");
|
|
3133
|
-
default:
|
|
3134
|
-
break;
|
|
3135
|
-
}
|
|
3136
|
-
if (c.dbType === "VECTOR") return mapped("[Float!]", "number[]");
|
|
3137
|
-
switch (c.tsType) {
|
|
3138
|
-
case "number":
|
|
3139
|
-
return mapped((0, import_validation_core16.isIntegerColumn)(c) && fitsInt32(c) ? "Int" : "Float", "number");
|
|
3140
|
-
case "string":
|
|
3141
|
-
return mapped(c.format === "uuid" ? "ID" : "String", "string");
|
|
3142
|
-
case "boolean":
|
|
3143
|
-
return mapped("Boolean", "boolean");
|
|
3144
|
-
case "Date":
|
|
3145
|
-
return mapped("DateTime", "Date", "Date", ["DateTime"]);
|
|
3146
|
-
case "bigint":
|
|
3147
|
-
return mapped("BigInt", "string | bigint", "string", ["BigInt"]);
|
|
3148
|
-
default:
|
|
3149
|
-
return {
|
|
3150
|
-
...mapped("JSON", "unknown", "unknown", ["JSON"]),
|
|
3151
|
-
note: `No GraphQL type for column "${c.name}": DRZL could not derive one from the schema, so it is exposed through the JSON scalar and accepts any value there.`
|
|
3152
|
-
};
|
|
3153
|
-
}
|
|
3154
|
-
})();
|
|
3155
|
-
let { sdl, rowTs, inputTs } = base;
|
|
3156
|
-
const dims = c.arrayDimensions ?? 0;
|
|
3157
|
-
for (let i = 0; i < dims; i++) {
|
|
3158
|
-
sdl = `[${sdl}]`;
|
|
3159
|
-
rowTs = `(${rowTs} | null)[]`;
|
|
3160
|
-
inputTs = `(${inputTs} | null)[]`;
|
|
3161
|
-
}
|
|
3162
|
-
return {
|
|
3163
|
-
column: c,
|
|
3164
|
-
field: field5,
|
|
3165
|
-
renamed: field5 !== c.name,
|
|
3166
|
-
sdl,
|
|
3167
|
-
rowTs,
|
|
3168
|
-
inputTs,
|
|
3169
|
-
scalars: base.scalars,
|
|
3170
|
-
enumPlan: base.enumPlan,
|
|
3171
|
-
note: base.note
|
|
3172
|
-
};
|
|
3173
|
-
}
|
|
3174
|
-
function planTable(table) {
|
|
3175
|
-
const typeName3 = cap6(gqlIdent(table.tsName));
|
|
3176
|
-
const fieldBase = gqlIdent(table.tsName);
|
|
3177
|
-
const select = (0, import_validation_core16.selectColumns)(table).map((c) => planColumn(table, c));
|
|
3178
|
-
const writable = !table.readOnly;
|
|
3179
|
-
const insert = writable ? (0, import_validation_core16.insertColumns)(table).map((c) => planColumn(table, c)) : [];
|
|
3180
|
-
const update = writable ? (0, import_validation_core16.updateColumns)(table).map((c) => planColumn(table, c)) : [];
|
|
3181
|
-
const keyCols = keyColumns7(table);
|
|
3182
|
-
const key = keyCols ? keyCols.map((c) => planColumn(table, c)) : null;
|
|
3183
|
-
const scalars = /* @__PURE__ */ new Set();
|
|
3184
|
-
const enums = [];
|
|
3185
|
-
const notes = [];
|
|
3186
|
-
for (const p of select) {
|
|
3187
|
-
for (const s of p.scalars) scalars.add(s);
|
|
3188
|
-
if (p.enumPlan) enums.push(p.enumPlan);
|
|
3189
|
-
if (p.note) notes.push(p.note);
|
|
3190
|
-
}
|
|
3191
|
-
const createInput = `Create${typeName3}Input`;
|
|
3192
|
-
const updateInput = `Update${typeName3}Input`;
|
|
3193
|
-
const hasCreate = writable && insert.length > 0;
|
|
3194
|
-
const hasUpdate = writable && key !== null && update.length > 0;
|
|
3195
|
-
const keyArgsSdl = key ? key.map((p) => `${p.field}: ${p.sdl}!`).join(", ") : "";
|
|
3196
|
-
const keyArgsTs = key ? key.map((p) => `${p.field}: ${p.inputTs}`).join("; ") : "";
|
|
3197
|
-
const ops = [];
|
|
3198
|
-
ops.push({
|
|
3199
|
-
parent: "Query",
|
|
3200
|
-
name: fieldBase,
|
|
3201
|
-
argsSdl: "",
|
|
3202
|
-
resultSdl: `[${typeName3}!]!`,
|
|
3203
|
-
argsTs: "Record<string, never>",
|
|
3204
|
-
resultTs: `${typeName3}[]`
|
|
3205
|
-
});
|
|
3206
|
-
if (key) {
|
|
3207
|
-
ops.push({
|
|
3208
|
-
parent: "Query",
|
|
3209
|
-
name: `${fieldBase}ById`,
|
|
3210
|
-
argsSdl: keyArgsSdl,
|
|
3211
|
-
resultSdl: typeName3,
|
|
3212
|
-
argsTs: `{ ${keyArgsTs} }`,
|
|
3213
|
-
resultTs: `${typeName3} | null`
|
|
3214
|
-
});
|
|
3215
|
-
}
|
|
3216
|
-
if (hasCreate) {
|
|
3217
|
-
ops.push({
|
|
3218
|
-
parent: "Mutation",
|
|
3219
|
-
name: `create${typeName3}`,
|
|
3220
|
-
argsSdl: `input: ${createInput}!`,
|
|
3221
|
-
resultSdl: `${typeName3}!`,
|
|
3222
|
-
argsTs: `{ input: ${createInput} }`,
|
|
3223
|
-
resultTs: typeName3
|
|
3224
|
-
});
|
|
3225
|
-
}
|
|
3226
|
-
if (hasUpdate) {
|
|
3227
|
-
ops.push({
|
|
3228
|
-
parent: "Mutation",
|
|
3229
|
-
name: `update${typeName3}`,
|
|
3230
|
-
argsSdl: `${keyArgsSdl}, input: ${updateInput}!`,
|
|
3231
|
-
resultSdl: `${typeName3}!`,
|
|
3232
|
-
argsTs: `{ ${keyArgsTs}; input: ${updateInput} }`,
|
|
3233
|
-
resultTs: typeName3
|
|
3234
|
-
});
|
|
3235
|
-
}
|
|
3236
|
-
if (writable && key) {
|
|
3237
|
-
ops.push({
|
|
3238
|
-
parent: "Mutation",
|
|
3239
|
-
name: `delete${typeName3}`,
|
|
3240
|
-
argsSdl: keyArgsSdl,
|
|
3241
|
-
resultSdl: "Boolean!",
|
|
3242
|
-
argsTs: `{ ${keyArgsTs} }`,
|
|
3243
|
-
resultTs: "boolean"
|
|
3244
|
-
});
|
|
3245
|
-
}
|
|
3246
|
-
const renamedSelect = select.filter((p) => p.renamed);
|
|
3247
|
-
for (const p of renamedSelect) {
|
|
3248
|
-
notes.push(
|
|
3249
|
-
`Column "${p.column.name}" is not a valid GraphQL field name, so it is exposed as "${p.field}": output is mapped back by the emitted field resolver, and on inputs the value arrives under "${p.field}" for your resolver to write back.`
|
|
3250
|
-
);
|
|
3251
|
-
}
|
|
3252
|
-
return {
|
|
3253
|
-
table,
|
|
3254
|
-
typeName: typeName3,
|
|
3255
|
-
createInput,
|
|
3256
|
-
updateInput,
|
|
3257
|
-
select,
|
|
3258
|
-
insert,
|
|
3259
|
-
update,
|
|
3260
|
-
key,
|
|
3261
|
-
writable,
|
|
3262
|
-
hasCreate,
|
|
3263
|
-
hasUpdate,
|
|
3264
|
-
ops,
|
|
3265
|
-
scalars,
|
|
3266
|
-
enums,
|
|
3267
|
-
notes,
|
|
3268
|
-
renamedSelect
|
|
3269
|
-
};
|
|
3270
|
-
}
|
|
3271
|
-
function renderEnumSdl(plan) {
|
|
3272
|
-
const lines = [`enum ${plan.typeName} {`];
|
|
3273
|
-
for (const m of plan.members) {
|
|
3274
|
-
if (m.renamed) lines.push(` ${JSON.stringify(`Database value: ${m.value}`)}`);
|
|
3275
|
-
lines.push(` ${m.name}`);
|
|
3276
|
-
}
|
|
3277
|
-
lines.push("}");
|
|
3278
|
-
return lines.join("\n");
|
|
3279
|
-
}
|
|
3280
|
-
function renderTypeSdl(plan) {
|
|
3281
|
-
const parts = [];
|
|
3282
|
-
for (const e of plan.enums) parts.push(renderEnumSdl(e));
|
|
3283
|
-
parts.push(
|
|
3284
|
-
[
|
|
3285
|
-
`type ${plan.typeName} {`,
|
|
3286
|
-
...plan.select.map((p) => ` ${p.field}: ${p.sdl}${p.column.nullable ? "" : "!"}`),
|
|
3287
|
-
"}"
|
|
3288
|
-
].join("\n")
|
|
3289
|
-
);
|
|
3290
|
-
if (plan.hasCreate) {
|
|
3291
|
-
parts.push(
|
|
3292
|
-
[
|
|
3293
|
-
`input ${plan.createInput} {`,
|
|
3294
|
-
...plan.insert.map((p) => {
|
|
3295
|
-
const required = !p.column.nullable && !p.column.hasDefault;
|
|
3296
|
-
return ` ${p.field}: ${p.sdl}${required ? "!" : ""}`;
|
|
3297
|
-
}),
|
|
3298
|
-
"}"
|
|
3299
|
-
].join("\n")
|
|
3300
|
-
);
|
|
3301
|
-
}
|
|
3302
|
-
if (plan.hasUpdate) {
|
|
3303
|
-
parts.push(
|
|
3304
|
-
[
|
|
3305
|
-
`input ${plan.updateInput} {`,
|
|
3306
|
-
...plan.update.map((p) => ` ${p.field}: ${p.sdl}`),
|
|
3307
|
-
"}"
|
|
3308
|
-
].join("\n")
|
|
3309
|
-
);
|
|
3310
|
-
}
|
|
3311
|
-
return parts.join("\n\n");
|
|
3312
|
-
}
|
|
3313
|
-
function rowField(p) {
|
|
3314
|
-
const key = isIdent6(p.column.name) ? p.column.name : q5(p.column.name);
|
|
3315
|
-
return ` ${key}: ${p.rowTs}${p.column.nullable ? " | null" : ""};`;
|
|
3316
|
-
}
|
|
3317
|
-
function inputField(p, mode) {
|
|
3318
|
-
const required = mode === "insert" && !p.column.nullable && !p.column.hasDefault;
|
|
3319
|
-
return required ? ` ${p.field}: ${p.inputTs};` : ` ${p.field}?: ${p.inputTs} | null;`;
|
|
3320
|
-
}
|
|
3321
|
-
function renderResolvers(plan) {
|
|
3322
|
-
const lines = ["{"];
|
|
3323
|
-
const byParent = (parent) => plan.ops.filter((o) => o.parent === parent);
|
|
3324
|
-
const renderOps = (parent) => {
|
|
3325
|
-
lines.push(` ${parent}: {`);
|
|
3326
|
-
for (const op of byParent(parent)) {
|
|
3327
|
-
lines.push(
|
|
3328
|
-
` ${op.name}: (_parent: unknown, _args: ${op.argsTs}): ${op.resultTs} => {`,
|
|
3329
|
-
` ${stubBody(parent, op.name)}`,
|
|
3330
|
-
` },`
|
|
3331
|
-
);
|
|
3332
|
-
}
|
|
3333
|
-
lines.push(" },");
|
|
3334
|
-
};
|
|
3335
|
-
renderOps("Query");
|
|
3336
|
-
if (byParent("Mutation").length) renderOps("Mutation");
|
|
3337
|
-
for (const e of plan.enums) {
|
|
3338
|
-
const mapped2 = e.members.filter((m) => m.renamed);
|
|
3339
|
-
if (!mapped2.length) continue;
|
|
3340
|
-
lines.push(
|
|
3341
|
-
` ${e.typeName}: { ${mapped2.map((m) => `${m.name}: ${q5(m.value)}`).join(", ")} },`
|
|
3342
|
-
);
|
|
3343
|
-
}
|
|
3344
|
-
if (plan.renamedSelect.length) {
|
|
3345
|
-
lines.push(` ${plan.typeName}: {`);
|
|
3346
|
-
for (const p of plan.renamedSelect) {
|
|
3347
|
-
const type = `${p.rowTs}${p.column.nullable ? " | null" : ""}`;
|
|
3348
|
-
lines.push(` ${p.field}: (parent: ${plan.typeName}): ${type} => parent[${q5(p.column.name)}],`);
|
|
3349
|
-
}
|
|
3350
|
-
lines.push(" },");
|
|
3351
|
-
}
|
|
3352
|
-
lines.push("}");
|
|
3353
|
-
return lines.join("\n");
|
|
3354
|
-
}
|
|
3355
|
-
function renderTable2(plan) {
|
|
3356
|
-
const t = plan.table;
|
|
3357
|
-
const declared = [];
|
|
3358
|
-
declared.push(
|
|
3359
|
-
`/** One ${t.name} row, as your resolvers return it: database values, database spellings. */`,
|
|
3360
|
-
`export interface ${plan.typeName} {`,
|
|
3361
|
-
...plan.select.map(rowField),
|
|
3362
|
-
"}"
|
|
3363
|
-
);
|
|
3364
|
-
if (plan.hasCreate) {
|
|
3365
|
-
declared.push(
|
|
3366
|
-
"",
|
|
3367
|
-
`/** The create${plan.typeName} input, as GraphQL hands it to your resolver. */`,
|
|
3368
|
-
`export interface ${plan.createInput} {`,
|
|
3369
|
-
...plan.insert.map((p) => inputField(p, "insert")),
|
|
3370
|
-
"}"
|
|
3371
|
-
);
|
|
3372
|
-
}
|
|
3373
|
-
if (plan.hasUpdate) {
|
|
3374
|
-
declared.push(
|
|
3375
|
-
"",
|
|
3376
|
-
`/** The update${plan.typeName} patch: every field optional, primary key excluded. */`,
|
|
3377
|
-
`export interface ${plan.updateInput} {`,
|
|
3378
|
-
...plan.update.map((p) => inputField(p, "update")),
|
|
3379
|
-
"}"
|
|
3380
|
-
);
|
|
3381
|
-
}
|
|
3382
|
-
const tsName = gqlIdent(t.tsName);
|
|
3383
|
-
const notes = plan.notes.length ? plan.notes.map((n) => `// ${n.replace(/\n/g, " ")}`).join("\n") + "\n" : "";
|
|
3384
|
-
return `// Generated by @drzl/generator-graphql
|
|
3385
|
-
// GraphQL SDL and resolver stubs for table: ${t.name}
|
|
3386
|
-
${notes}${declared.join("\n")}
|
|
3387
|
-
|
|
3388
|
-
/** The SDL for this table's types. The Query and Mutation fields live in the barrel. */
|
|
3389
|
-
export const ${tsName}TypeDefs = \`${tpl(renderTypeSdl(plan))}\`;
|
|
3390
|
-
|
|
3391
|
-
/** Stubs that throw until replaced, plus the enum value maps and field resolvers the schema needs. */
|
|
3392
|
-
export const ${tsName}Resolvers = ${renderResolvers(plan)};
|
|
3393
|
-
`;
|
|
3394
|
-
}
|
|
3395
|
-
function renderScalarsModule() {
|
|
3396
|
-
return `// Generated by @drzl/generator-graphql
|
|
3397
|
-
// Dependency-free scalar configs for the resolvers map. Each hook is named twice because
|
|
3398
|
-
// graphql 17 renamed serialize/parseValue/parseLiteral to coerceOutputValue/coerceInputValue/
|
|
3399
|
-
// coerceInputLiteral, and the schema builder assigns whichever names the running graphql reads.
|
|
3400
|
-
|
|
3401
|
-
/** The literal AST shape the literal hooks read, structurally, so nothing is imported. */
|
|
3402
|
-
export interface LiteralNode {
|
|
3403
|
-
kind: string;
|
|
3404
|
-
value?: unknown;
|
|
3405
|
-
values?: LiteralNode[];
|
|
3406
|
-
fields?: { name: { value: string }; value: LiteralNode }[];
|
|
3407
|
-
name?: { value: string };
|
|
3408
|
-
}
|
|
3409
|
-
|
|
3410
|
-
// Strict ISO 8601 datetime with seconds and an offset: new Date('1') is the year 2001.
|
|
3411
|
-
const ISO_DATETIME = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})$/;
|
|
3412
|
-
|
|
3413
|
-
const toIso = (value: unknown): string => {
|
|
3414
|
-
if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString();
|
|
3415
|
-
throw new Error('DateTime.serialize: expected a Date');
|
|
3416
|
-
};
|
|
3417
|
-
const fromIso = (value: unknown): Date => {
|
|
3418
|
-
if (typeof value !== 'string' || !ISO_DATETIME.test(value)) {
|
|
3419
|
-
throw new Error('DateTime: expected a strict ISO 8601 datetime string');
|
|
3420
|
-
}
|
|
3421
|
-
const parsed = new Date(value);
|
|
3422
|
-
if (Number.isNaN(parsed.getTime())) throw new Error('DateTime: unreadable datetime string');
|
|
3423
|
-
return parsed;
|
|
3424
|
-
};
|
|
3425
|
-
const fromIsoLiteral = (ast: LiteralNode): Date => {
|
|
3426
|
-
if (ast.kind !== 'StringValue') throw new Error('DateTime: expected a string literal');
|
|
3427
|
-
return fromIso(ast.value);
|
|
3428
|
-
};
|
|
3429
|
-
|
|
3430
|
-
/** ISO 8601 datetime string in, real Date to the resolver, toISOString() out. */
|
|
3431
|
-
export const DateTimeScalar = {
|
|
3432
|
-
name: 'DateTime',
|
|
3433
|
-
description: 'A strict ISO 8601 datetime string, e.g. 2026-01-02T03:04:05.000Z.',
|
|
3434
|
-
serialize: toIso,
|
|
3435
|
-
parseValue: fromIso,
|
|
3436
|
-
parseLiteral: fromIsoLiteral,
|
|
3437
|
-
coerceOutputValue: toIso,
|
|
3438
|
-
coerceInputValue: fromIso,
|
|
3439
|
-
coerceInputLiteral: fromIsoLiteral,
|
|
3440
|
-
};
|
|
3441
|
-
|
|
3442
|
-
const DIGITS = /^-?\\d+$/;
|
|
3443
|
-
|
|
3444
|
-
const toDigits = (value: unknown): string => {
|
|
3445
|
-
if (typeof value === 'bigint') return value.toString();
|
|
3446
|
-
if (typeof value === 'string' && DIGITS.test(value)) return value;
|
|
3447
|
-
throw new Error('BigInt.serialize: expected a bigint or a decimal digit string');
|
|
3448
|
-
};
|
|
3449
|
-
// Variables take the digit string only: a JSON number was already rounded by JSON.parse.
|
|
3450
|
-
const fromDigits = (value: unknown): string => {
|
|
3451
|
-
if (typeof value !== 'string' || !DIGITS.test(value)) {
|
|
3452
|
-
throw new Error('BigInt: expected a string of decimal digits');
|
|
3453
|
-
}
|
|
3454
|
-
return value;
|
|
3455
|
-
};
|
|
3456
|
-
// An inline integer literal is lossless: the AST carries its raw digits as a string.
|
|
3457
|
-
const fromDigitsLiteral = (ast: LiteralNode): string => {
|
|
3458
|
-
if (ast.kind === 'IntValue') return String(ast.value);
|
|
3459
|
-
if (ast.kind === 'StringValue' && typeof ast.value === 'string' && DIGITS.test(ast.value)) {
|
|
3460
|
-
return ast.value;
|
|
3461
|
-
}
|
|
3462
|
-
throw new Error('BigInt: expected an integer literal or a string of decimal digits');
|
|
3463
|
-
};
|
|
3464
|
-
|
|
3465
|
-
/** A 64-bit-safe integer crossing the wire as its decimal digits. */
|
|
3466
|
-
export const BigIntScalar = {
|
|
3467
|
-
name: 'BigInt',
|
|
3468
|
-
description: 'An arbitrary-precision integer as a string of decimal digits.',
|
|
3469
|
-
serialize: toDigits,
|
|
3470
|
-
parseValue: fromDigits,
|
|
3471
|
-
parseLiteral: fromDigitsLiteral,
|
|
3472
|
-
coerceOutputValue: toDigits,
|
|
3473
|
-
coerceInputValue: fromDigits,
|
|
3474
|
-
coerceInputLiteral: fromDigitsLiteral,
|
|
3475
|
-
};
|
|
3476
|
-
|
|
3477
|
-
const identity = (value: unknown): unknown => value;
|
|
3478
|
-
const fromJSONLiteral = (ast: LiteralNode, variables?: Record<string, unknown> | null): unknown => {
|
|
3479
|
-
switch (ast.kind) {
|
|
3480
|
-
case 'StringValue':
|
|
3481
|
-
case 'BooleanValue':
|
|
3482
|
-
case 'EnumValue':
|
|
3483
|
-
return ast.value;
|
|
3484
|
-
case 'IntValue':
|
|
3485
|
-
case 'FloatValue':
|
|
3486
|
-
return Number(ast.value);
|
|
3487
|
-
case 'NullValue':
|
|
3488
|
-
return null;
|
|
3489
|
-
case 'ListValue':
|
|
3490
|
-
return (ast.values ?? []).map((v) => fromJSONLiteral(v, variables));
|
|
3491
|
-
case 'ObjectValue': {
|
|
3492
|
-
const out: Record<string, unknown> = {};
|
|
3493
|
-
for (const f of ast.fields ?? []) out[f.name.value] = fromJSONLiteral(f.value, variables);
|
|
3494
|
-
return out;
|
|
3495
|
-
}
|
|
3496
|
-
case 'Variable':
|
|
3497
|
-
return variables ? variables[ast.name?.value ?? ''] : undefined;
|
|
3498
|
-
default:
|
|
3499
|
-
throw new Error('JSON: unsupported literal kind ' + ast.kind);
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
3500
15
|
}
|
|
16
|
+
return to;
|
|
3501
17
|
};
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
coerceOutputValue: identity,
|
|
3511
|
-
coerceInputValue: identity,
|
|
3512
|
-
coerceInputLiteral: fromJSONLiteral,
|
|
3513
|
-
};
|
|
3514
|
-
`;
|
|
3515
|
-
}
|
|
3516
|
-
function renderBarrel6(plans, modules, usedScalars, scalarsSpec) {
|
|
3517
|
-
if (!plans.length) {
|
|
3518
|
-
return `// Generated by @drzl/generator-graphql
|
|
3519
|
-
// No tables detected in analysis. Add tables to your schema and regenerate.
|
|
3520
|
-
// A GraphQL schema needs a Query type with at least one field, so no typeDefs are composed.
|
|
3521
|
-
export * from '${scalarsSpec}';
|
|
3522
|
-
|
|
3523
|
-
export const typeDefs = '';
|
|
3524
|
-
|
|
3525
|
-
export const resolvers = {};
|
|
3526
|
-
`;
|
|
3527
|
-
}
|
|
3528
|
-
const imports = [];
|
|
3529
|
-
if (usedScalars.length) {
|
|
3530
|
-
const names = usedScalars.map((s) => SCALAR_EXPORTS[s]).sort();
|
|
3531
|
-
imports.push(`import { ${names.join(", ")} } from '${scalarsSpec}';`);
|
|
3532
|
-
}
|
|
3533
|
-
for (const plan of plans) {
|
|
3534
|
-
const tsName = gqlIdent(plan.table.tsName);
|
|
3535
|
-
imports.push(
|
|
3536
|
-
`import { ${tsName}Resolvers, ${tsName}TypeDefs } from '${modules.get(plan)}';`
|
|
3537
|
-
);
|
|
3538
|
-
}
|
|
3539
|
-
const reExports = [
|
|
3540
|
-
`export * from '${scalarsSpec}';`,
|
|
3541
|
-
...plans.map((p) => `export * from '${modules.get(p)}';`)
|
|
3542
|
-
];
|
|
3543
|
-
const queryLines = plans.flatMap(
|
|
3544
|
-
(p) => p.ops.filter((o) => o.parent === "Query").map((o) => ` ${o.name}${o.argsSdl ? `(${o.argsSdl})` : ""}: ${o.resultSdl}`)
|
|
3545
|
-
);
|
|
3546
|
-
const mutationLines = plans.flatMap(
|
|
3547
|
-
(p) => p.ops.filter((o) => o.parent === "Mutation").map((o) => ` ${o.name}(${o.argsSdl}): ${o.resultSdl}`)
|
|
3548
|
-
);
|
|
3549
|
-
const typeDefParts = [
|
|
3550
|
-
...usedScalars.map((s) => `'scalar ${s}'`),
|
|
3551
|
-
...plans.map((p) => `${gqlIdent(p.table.tsName)}TypeDefs`),
|
|
3552
|
-
`\`${tpl(["type Query {", ...queryLines, "}"].join("\n"))}\``
|
|
3553
|
-
];
|
|
3554
|
-
if (mutationLines.length) {
|
|
3555
|
-
typeDefParts.push(`\`${tpl(["type Mutation {", ...mutationLines, "}"].join("\n"))}\``);
|
|
3556
|
-
}
|
|
3557
|
-
const resolverLines = ["export const resolvers = {"];
|
|
3558
|
-
for (const s of usedScalars) resolverLines.push(` ${s}: ${SCALAR_EXPORTS[s]},`);
|
|
3559
|
-
for (const p of plans) {
|
|
3560
|
-
const tsName = gqlIdent(p.table.tsName);
|
|
3561
|
-
for (const e of p.enums) {
|
|
3562
|
-
if (e.members.some((m) => m.renamed)) {
|
|
3563
|
-
resolverLines.push(` ${e.typeName}: ${tsName}Resolvers.${e.typeName},`);
|
|
3564
|
-
}
|
|
3565
|
-
}
|
|
3566
|
-
if (p.renamedSelect.length) {
|
|
3567
|
-
resolverLines.push(` ${p.typeName}: ${tsName}Resolvers.${p.typeName},`);
|
|
3568
|
-
}
|
|
3569
|
-
}
|
|
3570
|
-
resolverLines.push(" Query: {");
|
|
3571
|
-
for (const p of plans) resolverLines.push(` ...${gqlIdent(p.table.tsName)}Resolvers.Query,`);
|
|
3572
|
-
resolverLines.push(" },");
|
|
3573
|
-
const mutating = plans.filter((p) => p.ops.some((o) => o.parent === "Mutation"));
|
|
3574
|
-
if (mutating.length) {
|
|
3575
|
-
resolverLines.push(" Mutation: {");
|
|
3576
|
-
for (const p of mutating) {
|
|
3577
|
-
resolverLines.push(` ...${gqlIdent(p.table.tsName)}Resolvers.Mutation,`);
|
|
3578
|
-
}
|
|
3579
|
-
resolverLines.push(" },");
|
|
3580
|
-
}
|
|
3581
|
-
resolverLines.push("};");
|
|
3582
|
-
return `// Generated by @drzl/generator-graphql
|
|
3583
|
-
// The whole schema in one pair: hand typeDefs and resolvers to makeExecutableSchema,
|
|
3584
|
-
// createSchema (graphql-yoga) or new ApolloServer(...). Plain buildSchema(typeDefs) accepts
|
|
3585
|
-
// the SDL too, but takes no resolvers, so scalar and enum behaviour will not attach there.
|
|
3586
|
-
${imports.join("\n")}
|
|
3587
|
-
|
|
3588
|
-
${reExports.join("\n")}
|
|
3589
|
-
|
|
3590
|
-
/** The whole schema's SDL. */
|
|
3591
|
-
export const typeDefs = [
|
|
3592
|
-
${typeDefParts.map((p) => ` ${p},`).join("\n")}
|
|
3593
|
-
].join('\\n\\n');
|
|
3594
|
-
|
|
3595
|
-
/** Everything merged. Override per field: { ...resolvers, Query: { ...resolvers.Query, users: yours } } */
|
|
3596
|
-
${resolverLines.join("\n")}
|
|
3597
|
-
`;
|
|
3598
|
-
}
|
|
3599
|
-
function buildHeader7(h) {
|
|
3600
|
-
if (h && h.enabled === false) return "";
|
|
3601
|
-
const text = h?.text?.trim();
|
|
3602
|
-
const lines = text ? text.split(/\r?\n/).map((l) => `// ${l}`) : [
|
|
3603
|
-
"// Generated by DRZL (@drzl/*)",
|
|
3604
|
-
"// Generated output is granted to you under your project's license.",
|
|
3605
|
-
"// You may use, copy, modify, and distribute without attribution."
|
|
3606
|
-
];
|
|
3607
|
-
return lines.join("\n") + "\n\n";
|
|
3608
|
-
}
|
|
3609
|
-
function toCase6(s, c) {
|
|
3610
|
-
if (!c) return s;
|
|
3611
|
-
const parts = s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]/g, " ").split(/\s+/);
|
|
3612
|
-
if (c === "camel") {
|
|
3613
|
-
return parts.map(
|
|
3614
|
-
(p, i) => i === 0 ? p.toLowerCase() : p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()
|
|
3615
|
-
).join("");
|
|
3616
|
-
}
|
|
3617
|
-
if (c === "kebab") return parts.map((p) => p.toLowerCase()).join("-");
|
|
3618
|
-
if (c === "snake") return parts.map((p) => p.toLowerCase()).join("_");
|
|
3619
|
-
return s;
|
|
3620
|
-
}
|
|
3621
|
-
var import_validation_core15, import_validation_core16, APP_MODULE5, SCALARS_MODULE, GRAPHQL_NAME, RESERVED_ENUM_VALUES, INT32_MIN, INT32_MAX, cap6, isIdent6, q5, stubBody, SCALAR_EXPORTS, GraphQLGenerator, index_default7;
|
|
3622
|
-
var init_dist7 = __esm({
|
|
3623
|
-
"../generator-graphql/dist/index.js"() {
|
|
3624
|
-
"use strict";
|
|
3625
|
-
import_validation_core15 = require("@drzl/validation-core");
|
|
3626
|
-
import_validation_core16 = require("@drzl/validation-core");
|
|
3627
|
-
APP_MODULE5 = "index";
|
|
3628
|
-
SCALARS_MODULE = "scalars";
|
|
3629
|
-
GRAPHQL_NAME = /^[_A-Za-z][_0-9A-Za-z]*$/;
|
|
3630
|
-
RESERVED_ENUM_VALUES = /* @__PURE__ */ new Set(["true", "false", "null"]);
|
|
3631
|
-
INT32_MIN = -2147483648n;
|
|
3632
|
-
INT32_MAX = 2147483647n;
|
|
3633
|
-
cap6 = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
3634
|
-
isIdent6 = (s) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s);
|
|
3635
|
-
q5 = (v) => `'${v.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
3636
|
-
stubBody = (parent, name) => `throw new Error('Not implemented: ${parent}.${name}. Replace this stub with your data layer.');`;
|
|
3637
|
-
SCALAR_EXPORTS = {
|
|
3638
|
-
DateTime: "DateTimeScalar",
|
|
3639
|
-
BigInt: "BigIntScalar",
|
|
3640
|
-
JSON: "JSONScalar"
|
|
3641
|
-
};
|
|
3642
|
-
GraphQLGenerator = class {
|
|
3643
|
-
constructor(analysis) {
|
|
3644
|
-
this.analysis = analysis;
|
|
3645
|
-
}
|
|
3646
|
-
async generate(opts) {
|
|
3647
|
-
const fs6 = (0, import_validation_core15.fileWriter)(opts.fileSink);
|
|
3648
|
-
const path9 = await import("path");
|
|
3649
|
-
const out = path9.resolve(process.cwd(), opts.outputDir);
|
|
3650
|
-
await fs6.mkdir(out, { recursive: true });
|
|
3651
|
-
const files = [];
|
|
3652
|
-
const write = async (filePath, content) => {
|
|
3653
|
-
const formatted = await (0, import_validation_core16.formatCode)(
|
|
3654
|
-
buildHeader7(opts.outputHeader) + content,
|
|
3655
|
-
filePath,
|
|
3656
|
-
opts.format
|
|
3657
|
-
);
|
|
3658
|
-
await fs6.writeFile(filePath, formatted, "utf8");
|
|
3659
|
-
files.push(filePath);
|
|
3660
|
-
};
|
|
3661
|
-
const barrelPath = path9.join(out, `${APP_MODULE5}.ts`);
|
|
3662
|
-
const scalarsPath = path9.join(out, `${SCALARS_MODULE}.ts`);
|
|
3663
|
-
const plans = this.analysis.tables.map(planTable);
|
|
3664
|
-
const modules = /* @__PURE__ */ new Map();
|
|
3665
|
-
const total = plans.length;
|
|
3666
|
-
let index = 0;
|
|
3667
|
-
for (const plan of plans) {
|
|
3668
|
-
const base = `${plan.table.tsName}${opts.naming?.routerSuffix ?? ""}`;
|
|
3669
|
-
const filePath = path9.join(out, `${toCase6(base, opts.naming?.procedureCase)}.ts`);
|
|
3670
|
-
if (filePath === barrelPath || filePath === scalarsPath) {
|
|
3671
|
-
const which = filePath === barrelPath ? "the barrel" : "the scalars module";
|
|
3672
|
-
throw new Error(
|
|
3673
|
-
`@drzl/generator-graphql: the module for table "${plan.table.name}" would be written to ${filePath}, which is ${which} this generator also writes. Set naming.routerSuffix to move it out of the way.`
|
|
3674
|
-
);
|
|
3675
|
-
}
|
|
3676
|
-
await write(filePath, renderTable2(plan));
|
|
3677
|
-
modules.set(
|
|
3678
|
-
plan,
|
|
3679
|
-
(0, import_validation_core16.importSpecifier)("./" + path9.relative(out, filePath).replace(/\\/g, "/"), opts.importExtension)
|
|
3680
|
-
);
|
|
3681
|
-
index++;
|
|
3682
|
-
opts.onProgress?.({ index, total, table: plan.table.name, filePath });
|
|
3683
|
-
}
|
|
3684
|
-
const usedScalars = ["DateTime", "BigInt", "JSON"].filter(
|
|
3685
|
-
(s) => plans.some((p) => p.scalars.has(s))
|
|
3686
|
-
);
|
|
3687
|
-
const scalarsSpec = (0, import_validation_core16.importSpecifier)(`./${SCALARS_MODULE}.ts`, opts.importExtension);
|
|
3688
|
-
await write(scalarsPath, renderScalarsModule());
|
|
3689
|
-
await write(barrelPath, renderBarrel6(plans, modules, usedScalars, scalarsSpec));
|
|
3690
|
-
return { files };
|
|
3691
|
-
}
|
|
3692
|
-
};
|
|
3693
|
-
index_default7 = GraphQLGenerator;
|
|
3694
|
-
}
|
|
3695
|
-
});
|
|
3696
|
-
|
|
3697
|
-
// ../generator-effect/dist/index.js
|
|
3698
|
-
var dist_exports8 = {};
|
|
3699
|
-
__export(dist_exports8, {
|
|
3700
|
-
EffectGenerator: () => EffectGenerator,
|
|
3701
|
-
default: () => index_default8
|
|
3702
|
-
});
|
|
3703
|
-
function filter(expr, description) {
|
|
3704
|
-
return `${NS}.filter((v) => ${expr}, { description: ${JSON.stringify(description)} })`;
|
|
3705
|
-
}
|
|
3706
|
-
function piped(base, steps) {
|
|
3707
|
-
return steps.length ? `${base}.pipe(${steps.join(", ")})` : base;
|
|
3708
|
-
}
|
|
3709
|
-
function isUnknownExpr(expr) {
|
|
3710
|
-
return expr === UNKNOWN_EXPR;
|
|
3711
|
-
}
|
|
3712
|
-
function withNarrowedType(expr, ref2) {
|
|
3713
|
-
return `${expr} as unknown as ${NS}.Schema<${ref2}>`;
|
|
3714
|
-
}
|
|
3715
|
-
function numericBounds(c, checks) {
|
|
3716
|
-
let lo = c.min !== void 0 ? { fn: "greaterThanOrEqualTo", value: c.min } : void 0;
|
|
3717
|
-
let hi = c.max !== void 0 ? { fn: "lessThanOrEqualTo", value: c.max } : void 0;
|
|
3718
|
-
for (const k of checks.filter((x) => x.column === c.name && x.kind === "number")) {
|
|
3719
|
-
if (k.operator === ">=") lo = { fn: "greaterThanOrEqualTo", value: k.value };
|
|
3720
|
-
else if (k.operator === ">") lo = { fn: "greaterThan", value: k.value };
|
|
3721
|
-
else if (k.operator === "<=") hi = { fn: "lessThanOrEqualTo", value: k.value };
|
|
3722
|
-
else if (k.operator === "<") hi = { fn: "lessThan", value: k.value };
|
|
3723
|
-
}
|
|
3724
|
-
return [lo, hi].filter(Boolean).map((x) => `${NS}.${x.fn}(${x.value})`);
|
|
3725
|
-
}
|
|
3726
|
-
function foldedIntoBounds(c, checks) {
|
|
3727
|
-
if (c.arrayDimensions || c.shape) return /* @__PURE__ */ new Set();
|
|
3728
|
-
if (c.tsType !== "number" && c.tsType !== "bigint") return /* @__PURE__ */ new Set();
|
|
3729
|
-
return new Set(
|
|
3730
|
-
checks.filter(
|
|
3731
|
-
(k) => k.column === c.name && k.kind === "number" && k.operator !== "=" && k.operator !== "<>"
|
|
3732
|
-
)
|
|
3733
|
-
);
|
|
3734
|
-
}
|
|
3735
|
-
function nonFiniteBranches(c) {
|
|
3736
|
-
const { nan, infinity } = (0, import_validation_core18.nonFiniteAccepted)(c);
|
|
3737
|
-
return [
|
|
3738
|
-
...nan ? [`${NS}.Number.pipe(${filter("Number.isNaN(v)", "NaN, which this column stores")})`] : [],
|
|
3739
|
-
...infinity ? [`${NS}.Literal(Infinity, -Infinity)`] : []
|
|
3740
|
-
];
|
|
3741
|
-
}
|
|
3742
|
-
function withNonFinite(c, base) {
|
|
3743
|
-
const branches = nonFiniteBranches(c);
|
|
3744
|
-
return branches.length ? `${NS}.Union(${base}, ${branches.join(", ")})` : base;
|
|
3745
|
-
}
|
|
3746
|
-
function dateExpr(mode, coerceDates) {
|
|
3747
|
-
const plain = `${NS}.ValidDateFromSelf`;
|
|
3748
|
-
if (coerceDates === "none") return plain;
|
|
3749
|
-
const fromString = piped(`${NS}.String`, [
|
|
3750
|
-
`${NS}.pattern(new RegExp(${JSON.stringify(import_validation_core18.COERCIBLE_DATE_STRING)}))`,
|
|
3751
|
-
filter((0, import_validation_core18.parsesToADate)("new Date(v)"), "a date the runtime can parse")
|
|
3752
|
-
]);
|
|
3753
|
-
const fromNumber = piped(`${NS}.Number`, [
|
|
3754
|
-
filter((0, import_validation_core18.parsesToADate)("new Date(v)"), "a date the runtime can parse")
|
|
3755
|
-
]);
|
|
3756
|
-
const union = `${NS}.Union(${plain}, ${fromString}, ${fromNumber})`;
|
|
3757
|
-
if (coerceDates === "all") return union;
|
|
3758
|
-
return mode === "select" ? plain : union;
|
|
3759
|
-
}
|
|
3760
|
-
function capSteps(c, mode) {
|
|
3761
|
-
const steps = [];
|
|
3762
|
-
if (c.shape?.kind === "byteString") {
|
|
3763
|
-
const n = c.shape.length;
|
|
3764
|
-
if (!n) return steps;
|
|
3765
|
-
return mode === "select" ? [filter(`${import_validation_core18.CODEPOINT_LENGTH} <= ${n}`, `at most ${n} characters`)] : [filter(`new TextEncoder().encode(v).length <= ${n}`, `at most ${n} bytes`)];
|
|
3766
|
-
}
|
|
3767
|
-
if (c.maxLength) {
|
|
3768
|
-
steps.push(
|
|
3769
|
-
filter(`${import_validation_core18.CODEPOINT_LENGTH} <= ${c.maxLength}`, `at most ${c.maxLength} characters`)
|
|
3770
|
-
);
|
|
3771
|
-
}
|
|
3772
|
-
if (c.maxBytes) {
|
|
3773
|
-
steps.push(
|
|
3774
|
-
filter(`new TextEncoder().encode(v).length <= ${c.maxBytes}`, `at most ${c.maxBytes} bytes`)
|
|
3775
|
-
);
|
|
3776
|
-
}
|
|
3777
|
-
return steps;
|
|
3778
|
-
}
|
|
3779
|
-
function lengthSteps(c, lengths) {
|
|
3780
|
-
return lengths.filter((k) => k.column === c.name).flatMap((k) => {
|
|
3781
|
-
const measure = (0, import_validation_core18.lengthMeasure)(c, k);
|
|
3782
|
-
if (!measure) return [];
|
|
3783
|
-
return [
|
|
3784
|
-
filter(
|
|
3785
|
-
`${(0, import_validation_core18.measureExpression)(measure, "v")} ${OPS[k.operator]} ${k.value}`,
|
|
3786
|
-
(0, import_validation_core18.lengthCheckLabel)(k)
|
|
3787
|
-
)
|
|
3788
|
-
];
|
|
3789
|
-
});
|
|
3790
|
-
}
|
|
3791
|
-
function cardinalitySteps(c, cardinalities) {
|
|
3792
|
-
if (!c.arrayDimensions) return [];
|
|
3793
|
-
return cardinalities.filter((k) => k.column === c.name).map(
|
|
3794
|
-
(k) => filter(
|
|
3795
|
-
`v.length ${OPS[k.operator]} ${k.value}`,
|
|
3796
|
-
`${k.name ? `${k.name}: ` : ""}cardinality(${c.name}) ${k.operator} ${k.value}`
|
|
3797
|
-
)
|
|
3798
|
-
);
|
|
3799
|
-
}
|
|
3800
|
-
function checkSteps(c, checks) {
|
|
3801
|
-
if (c.arrayDimensions || c.shape) return [];
|
|
3802
|
-
const folded2 = foldedIntoBounds(c, checks);
|
|
3803
|
-
const numericWire = (0, import_validation_core18.comparisonWire)(c) === "numeric-string";
|
|
3804
|
-
return checks.filter((k) => k.column === c.name && !folded2.has(k)).map((k) => {
|
|
3805
|
-
const label = `${k.name ? `${k.name}: ` : ""}${c.name} ${k.operator} ${k.value}`;
|
|
3806
|
-
if (numericWire) {
|
|
3807
|
-
if (k.operator === "=" || k.operator === "<>") {
|
|
3808
|
-
const canon = JSON.stringify((0, import_validation_core18.canonicalNumericText)(k.value));
|
|
3809
|
-
const op = k.operator === "=" ? "===" : "!==";
|
|
3810
|
-
return filter(`${import_validation_core18.NUMERIC_CANON_NAME}(v) ${op} ${canon}`, label);
|
|
3811
|
-
}
|
|
3812
|
-
return filter(`Number(v) ${OPS[k.operator]} ${k.value}`, label);
|
|
3813
|
-
}
|
|
3814
|
-
const literal = k.kind === "string" ? JSON.stringify(k.value) : (0, import_validation_core18.wireNumberLiteral)(c, k.value);
|
|
3815
|
-
return filter(`v ${OPS[k.operator]} ${literal}`, label);
|
|
3816
|
-
});
|
|
3817
|
-
}
|
|
3818
|
-
function hasNoRuntimeType(c) {
|
|
3819
|
-
return c.tsType === "any" || c.shape?.kind === "custom" || c.shape?.kind === "json";
|
|
3820
|
-
}
|
|
3821
|
-
function shapeExpr(c, mode, replaced = false) {
|
|
3822
|
-
const s = c.shape;
|
|
3823
|
-
if (!s) return void 0;
|
|
3824
|
-
switch (s.kind) {
|
|
3825
|
-
case "json":
|
|
3826
|
-
return replaced ? UNKNOWN_EXPR : JSON_CONST;
|
|
3827
|
-
case "custom":
|
|
3828
|
-
return UNKNOWN_EXPR;
|
|
3829
|
-
case "buffer":
|
|
3830
|
-
return `${NS}.Uint8ArrayFromSelf`;
|
|
3831
|
-
case "tuple":
|
|
3832
|
-
return `${NS}.Tuple(${Array.from({ length: s.length }, () => `${NS}.Number`).join(", ")})`;
|
|
3833
|
-
case "numberObject":
|
|
3834
|
-
return `${NS}.Struct({ ${s.fields.map((f) => `${f}: ${NS}.Number`).join(", ")} })`;
|
|
3835
|
-
case "numberVector":
|
|
3836
|
-
return piped(
|
|
3837
|
-
`${NS}.Array(${NS}.Number)`,
|
|
3838
|
-
s.length ? [filter(`v.length === ${s.length}`, `exactly ${s.length} elements`)] : []
|
|
3839
|
-
);
|
|
3840
|
-
case "bitstring":
|
|
3841
|
-
return piped(`${NS}.String`, [
|
|
3842
|
-
`${NS}.pattern(/^[01]*$/)`,
|
|
3843
|
-
...s.length ? [
|
|
3844
|
-
s.exact ? filter(`v.length === ${s.length}`, `exactly ${s.length} binary digits`) : filter(`v.length <= ${s.length}`, `at most ${s.length} binary digits`)
|
|
3845
|
-
] : []
|
|
3846
|
-
]);
|
|
3847
|
-
case "byteString":
|
|
3848
|
-
return piped(`${NS}.String`, capSteps(c, mode));
|
|
3849
|
-
}
|
|
3850
|
-
}
|
|
3851
|
-
function exprForColumn(c, mode, coerceDates, checks, sets, lengths, replaced) {
|
|
3852
|
-
const shaped = shapeExpr(c, mode, replaced);
|
|
3853
|
-
if (shaped) return piped(shaped, lengthSteps(c, lengths));
|
|
3854
|
-
const set = sets.find((x) => x.column === c.name);
|
|
3855
|
-
if (set) {
|
|
3856
|
-
if ((0, import_validation_core18.comparisonWire)(c) === "numeric-string") {
|
|
3857
|
-
const members = (0, import_validation_core18.canonicalMembers)(set.values);
|
|
3858
|
-
const test = members.map((m) => `canon === ${JSON.stringify(m)}`).join(" || ");
|
|
3859
|
-
return piped(`${NS}.String`, [
|
|
3860
|
-
filter(`((canon) => ${test})(${import_validation_core18.NUMERIC_CANON_NAME}(v))`, (0, import_validation_core18.describeSet)(set))
|
|
3861
|
-
]);
|
|
3862
|
-
}
|
|
3863
|
-
const values = set.values.map(
|
|
3864
|
-
(v) => set.kind === "string" ? JSON.stringify(v) : (0, import_validation_core18.wireNumberLiteral)(c, v)
|
|
3865
|
-
);
|
|
3866
|
-
return `${NS}.Literal(${values.join(", ")})`;
|
|
3867
|
-
}
|
|
3868
|
-
if (c.arrayDimensions) checks = [];
|
|
3869
|
-
if (c.enumValues && c.enumValues.length) {
|
|
3870
|
-
return `${NS}.Literal(${c.enumValues.map((v) => JSON.stringify(v)).join(", ")})`;
|
|
3871
|
-
}
|
|
3872
|
-
const eq = checks.find((k) => k.column === c.name && k.operator === "=");
|
|
3873
|
-
if (eq && !c.shape && (0, import_validation_core18.comparisonWire)(c) !== "numeric-string") {
|
|
3874
|
-
return `${NS}.Literal(${eq.kind === "string" ? JSON.stringify(eq.value) : (0, import_validation_core18.wireNumberLiteral)(c, eq.value)})`;
|
|
3875
|
-
}
|
|
3876
|
-
const rest = [...checkSteps(c, checks), ...lengthSteps(c, lengths)];
|
|
3877
|
-
switch (c.tsType) {
|
|
3878
|
-
case "string": {
|
|
3879
|
-
const base = c.format === "uuid" ? `${NS}.UUID` : `${NS}.String`;
|
|
3880
|
-
const pattern = c.format && c.format !== "uuid" && import_validation_core18.COLUMN_FORMATS[c.format] ? [`${NS}.pattern(new RegExp(${JSON.stringify(import_validation_core18.COLUMN_FORMATS[c.format])}))`] : [];
|
|
3881
|
-
return piped(base, [...pattern, ...capSteps(c, mode), ...rest]);
|
|
3882
|
-
}
|
|
3883
|
-
case "number": {
|
|
3884
|
-
const base = (0, import_validation_core18.isIntegerColumn)(c) ? `${NS}.Int` : `${NS}.Finite`;
|
|
3885
|
-
return withNonFinite(c, piped(base, [...numericBounds(c, checks), ...rest]));
|
|
3886
|
-
}
|
|
3887
|
-
case "bigint":
|
|
3888
|
-
return piped(`${NS}.BigIntFromSelf`, [
|
|
3889
|
-
...c.min !== void 0 ? [`${NS}.greaterThanOrEqualToBigInt(${c.min}n)`] : [],
|
|
3890
|
-
...c.max !== void 0 ? [`${NS}.lessThanOrEqualToBigInt(${c.max}n)`] : [],
|
|
3891
|
-
...rest
|
|
3892
|
-
]);
|
|
3893
|
-
case "boolean":
|
|
3894
|
-
return `${NS}.Boolean`;
|
|
3895
|
-
case "Date":
|
|
3896
|
-
return dateExpr(mode, coerceDates);
|
|
3897
|
-
case "Uint8Array":
|
|
3898
|
-
return `${NS}.Uint8ArrayFromSelf`;
|
|
3899
|
-
case "any":
|
|
3900
|
-
return UNKNOWN_EXPR;
|
|
3901
|
-
default:
|
|
3902
|
-
return UNKNOWN_EXPR;
|
|
3903
|
-
}
|
|
3904
|
-
}
|
|
3905
|
-
function renderField(c, mode, coerceDates, checks, sets, lengths, cardinalities, applyDefault, narrowRef, brand) {
|
|
3906
|
-
let expr = exprForColumn(
|
|
3907
|
-
c,
|
|
3908
|
-
mode,
|
|
3909
|
-
coerceDates,
|
|
3910
|
-
checks,
|
|
3911
|
-
sets,
|
|
3912
|
-
lengths,
|
|
3913
|
-
!!narrowRef && hasNoRuntimeType(c)
|
|
3914
|
-
);
|
|
3915
|
-
const dims = c.arrayDimensions ?? 0;
|
|
3916
|
-
for (let i = 0; i < dims; i++) {
|
|
3917
|
-
expr = `${NS}.Array(${expr})`;
|
|
3918
|
-
if (i === dims - 1) expr = piped(expr, cardinalitySteps(c, cardinalities));
|
|
3919
|
-
}
|
|
3920
|
-
if (brand) expr = piped(expr, [`${NS}.brand(${JSON.stringify(brand)})`]);
|
|
3921
|
-
if (c.nullable && !isUnknownExpr(expr)) expr = `${NS}.NullOr(${expr})`;
|
|
3922
|
-
if (narrowRef && !brand) expr = withNarrowedType(expr, narrowRef);
|
|
3923
|
-
if (mode === "select") return expr;
|
|
3924
|
-
const wantsDefault = mode === "insert" && applyDefault && c.defaultValue !== void 0;
|
|
3925
|
-
if (wantsDefault) {
|
|
3926
|
-
return `${NS}.optionalWith(${expr}, { default: () => ${JSON.stringify(c.defaultValue)} })`;
|
|
3927
|
-
}
|
|
3928
|
-
if (mode === "update" || c.nullable || c.hasDefault) return `${NS}.optional(${expr})`;
|
|
3929
|
-
return expr;
|
|
3930
|
-
}
|
|
3931
|
-
function wantsRef(c, allColumns2) {
|
|
3932
|
-
return allColumns2 || hasNoRuntimeType(c);
|
|
3933
|
-
}
|
|
3934
|
-
function renderObjectShape(cols, mode, coerceDates, checks, sets, lengths, cardinalities, typedJson, applyDefaults, brands) {
|
|
3935
|
-
return cols.map((c) => {
|
|
3936
|
-
const ref2 = typedJson && wantsRef(c, !!typedJson.allColumns) ? `(typeof ${typedJson.table}.$infer${typedJson.mode === "insert" ? "Insert" : "Select"})[${JSON.stringify(c.name)}]` : void 0;
|
|
3937
|
-
const field5 = renderField(
|
|
3938
|
-
c,
|
|
3939
|
-
mode,
|
|
3940
|
-
coerceDates,
|
|
3941
|
-
checks,
|
|
3942
|
-
sets,
|
|
3943
|
-
lengths,
|
|
3944
|
-
cardinalities,
|
|
3945
|
-
applyDefaults,
|
|
3946
|
-
ref2,
|
|
3947
|
-
brands?.plan.brandOf(brands.tsName, c.name)
|
|
3948
|
-
);
|
|
3949
|
-
return ` ${JSON.stringify(c.name)}: ${field5},`;
|
|
3950
|
-
}).join("\n");
|
|
3951
|
-
}
|
|
3952
|
-
function rowSteps(rows, cols) {
|
|
3953
|
-
const present = new Set(cols.map((c) => c.name));
|
|
3954
|
-
return rows.filter((r) => present.has(r.left) && present.has(r.right)).map((r) => {
|
|
3955
|
-
const l = `o[${JSON.stringify(r.left)}]`;
|
|
3956
|
-
const rt = `o[${JSON.stringify(r.right)}]`;
|
|
3957
|
-
const msg = `${r.name ? `${r.name}: ` : ""}${r.left} ${r.operator} ${r.right}`;
|
|
3958
|
-
return `${NS}.filter((o) => ${l} == null || ${rt} == null || ${l} ${OPS[r.operator]} ${rt}, { description: ${JSON.stringify(msg)} })`;
|
|
3959
|
-
});
|
|
3960
|
-
}
|
|
3961
|
-
function indentBlock(code, by = " ") {
|
|
3962
|
-
return code.split("\n").map((line) => line ? by + line : line).join("\n");
|
|
3963
|
-
}
|
|
3964
|
-
function parsedChecksFor(table) {
|
|
3965
|
-
const parsed = (table.checks ?? []).map((k) => (0, import_validation_core18.parseCheck)(k.expression, k.name));
|
|
3966
|
-
const { checks, sets } = (0, import_validation_core18.applyWirePolicy)(
|
|
3967
|
-
table.columns,
|
|
3968
|
-
parsed.flatMap((p) => p.ok ? p.checks : []),
|
|
3969
|
-
parsed.flatMap((p) => p.ok ? p.sets ?? [] : [])
|
|
3970
|
-
);
|
|
3971
|
-
return {
|
|
3972
|
-
checks,
|
|
3973
|
-
sets,
|
|
3974
|
-
rows: parsed.flatMap((p) => p.ok ? p.rows ?? [] : []),
|
|
3975
|
-
lengths: parsed.flatMap((p) => p.ok ? p.lengths ?? [] : []),
|
|
3976
|
-
cardinalities: parsed.flatMap((p) => p.ok ? p.cardinalities ?? [] : [])
|
|
3977
|
-
};
|
|
3978
|
-
}
|
|
3979
|
-
function nestedNodeCols(node, mode) {
|
|
3980
|
-
const all = mode === "insert" ? (0, import_validation_core18.insertColumns)(node.table) : (0, import_validation_core18.selectColumns)(node.table);
|
|
3981
|
-
return (0, import_validation_core18.nestedNodeColumns)(all, node);
|
|
3982
|
-
}
|
|
3983
|
-
function nestedNodes(node, into = []) {
|
|
3984
|
-
into.push(node);
|
|
3985
|
-
for (const arm of node.arms) nestedNodes(arm.child, into);
|
|
3986
|
-
return into;
|
|
3987
|
-
}
|
|
3988
|
-
function renderNestedObject(node, mode, coerceDates, typedJson, applyDefaults, brands) {
|
|
3989
|
-
const cols = nestedNodeCols(node, mode);
|
|
3990
|
-
const { checks, sets, rows, lengths, cardinalities } = parsedChecksFor(node.table);
|
|
3991
|
-
const tj = typedJson ? { table: node.table.tsName, mode, allColumns: typedJson.allColumns } : void 0;
|
|
3992
|
-
const fields = renderObjectShape(
|
|
3993
|
-
cols,
|
|
3994
|
-
mode,
|
|
3995
|
-
coerceDates,
|
|
3996
|
-
checks,
|
|
3997
|
-
sets,
|
|
3998
|
-
lengths,
|
|
3999
|
-
cardinalities,
|
|
4000
|
-
tj,
|
|
4001
|
-
applyDefaults,
|
|
4002
|
-
brands ? { plan: brands, tsName: node.table.tsName } : void 0
|
|
4003
|
-
);
|
|
4004
|
-
const arms = node.arms.map((arm) => {
|
|
4005
|
-
const notes = (0, import_validation_core18.nestedArmNotes)(arm).map((n) => ` // ${n}
|
|
4006
|
-
`).join("");
|
|
4007
|
-
const child = renderNestedObject(
|
|
4008
|
-
arm.child,
|
|
4009
|
-
mode,
|
|
4010
|
-
coerceDates,
|
|
4011
|
-
typedJson,
|
|
4012
|
-
applyDefaults,
|
|
4013
|
-
brands
|
|
4014
|
-
);
|
|
4015
|
-
const inner = arm.single ? `${NS}.NullOr(
|
|
4016
|
-
${indentBlock(indentBlock(child))}
|
|
4017
|
-
)` : `${NS}.Array(
|
|
4018
|
-
${indentBlock(indentBlock(child))}
|
|
4019
|
-
)`;
|
|
4020
|
-
return `${notes} ${JSON.stringify(arm.key)}: ${NS}.optional(${inner}),`;
|
|
4021
|
-
});
|
|
4022
|
-
const body = [fields, ...arms].filter(Boolean).join("\n");
|
|
4023
|
-
return piped(`${NS}.Struct({
|
|
4024
|
-
${body}
|
|
4025
|
-
})`, rowSteps(rows, cols));
|
|
4026
|
-
}
|
|
4027
|
-
function renderNestedSchemas(table, affix, coerceDates, typedJson, applyDefaults, plans, brands) {
|
|
4028
|
-
const out = [];
|
|
4029
|
-
for (const mode of ["insert", "select"]) {
|
|
4030
|
-
const plan = plans[mode];
|
|
4031
|
-
if (!plan) continue;
|
|
4032
|
-
const name = (0, import_validation_core18.nestedSchemaName)(mode, table.tsName, affix);
|
|
4033
|
-
const tname = (0, import_validation_core18.nestedTypeName)(mode, table.tsName, affix);
|
|
4034
|
-
const expr = renderNestedObject(plan, mode, coerceDates, typedJson, applyDefaults, brands);
|
|
4035
|
-
out.push(
|
|
4036
|
-
`export const ${name} = ${expr};
|
|
4037
|
-
|
|
4038
|
-
export type ${tname} = ${NS}.Schema.Type<typeof ${name}>;
|
|
4039
|
-
|
|
4040
|
-
export const ${STANDARD_PREFIX}${name} = ${NS}.standardSchemaV1(${name});`
|
|
4041
|
-
);
|
|
4042
|
-
}
|
|
4043
|
-
return out.length ? `
|
|
4044
|
-
${out.join("\n\n")}
|
|
4045
|
-
` : "";
|
|
4046
|
-
}
|
|
4047
|
-
function nestedPlansFor(table, analysis, depth) {
|
|
4048
|
-
const out = {};
|
|
4049
|
-
for (const mode of ["insert", "select"]) {
|
|
4050
|
-
if (mode === "insert" && table.readOnly) continue;
|
|
4051
|
-
const plan = (0, import_validation_core18.buildNestedPlan)(table, analysis.tables, analysis.relations ?? [], mode, depth);
|
|
4052
|
-
if (plan) out[mode] = plan;
|
|
4053
|
-
}
|
|
4054
|
-
return out;
|
|
4055
|
-
}
|
|
4056
|
-
function renderTableSchemas(table, affix, coerceDates, typedJson, applyDefaults = false, wantsDuplicateFinder = false, nested = {}, brands) {
|
|
4057
|
-
const T = table.tsName;
|
|
4058
|
-
const insertCols = (0, import_validation_core18.insertColumns)(table);
|
|
4059
|
-
const updateCols = (0, import_validation_core18.updateColumns)(table);
|
|
4060
|
-
const selectCols = (0, import_validation_core18.selectColumns)(table);
|
|
4061
|
-
const { checks, sets, rows, lengths, cardinalities } = parsedChecksFor(table);
|
|
4062
|
-
const tj = typedJson ? { table: T, allColumns: typedJson.allColumns } : void 0;
|
|
4063
|
-
const modes = [
|
|
4064
|
-
["insert", insertCols],
|
|
4065
|
-
["update", updateCols],
|
|
4066
|
-
["select", selectCols]
|
|
4067
|
-
];
|
|
4068
|
-
const blocks = modes.map(([mode, cols]) => {
|
|
4069
|
-
const name = (0, import_validation_core18.schemaName)(mode, T, affix);
|
|
4070
|
-
const tname = (0, import_validation_core18.typeName)(mode, T, affix);
|
|
4071
|
-
const body = renderObjectShape(
|
|
4072
|
-
cols,
|
|
4073
|
-
mode,
|
|
4074
|
-
coerceDates,
|
|
4075
|
-
checks,
|
|
4076
|
-
sets,
|
|
4077
|
-
lengths,
|
|
4078
|
-
cardinalities,
|
|
4079
|
-
// The update schema references the insert-side inferred types: both describe a value going
|
|
4080
|
-
// in, and `$inferSelect` would name the post-default type for a column a write may omit.
|
|
4081
|
-
tj ? { ...tj, mode: mode === "select" ? "select" : "insert" } : void 0,
|
|
4082
|
-
applyDefaults,
|
|
4083
|
-
brands ? { plan: brands, tsName: T } : void 0
|
|
4084
|
-
);
|
|
4085
|
-
const expr = piped(`${NS}.Struct({
|
|
4086
|
-
${body}
|
|
4087
|
-
})`, rowSteps(rows, cols));
|
|
4088
|
-
return `export const ${name} = ${expr};
|
|
4089
|
-
|
|
4090
|
-
export type ${tname} = ${NS}.Schema.Type<typeof ${name}>;
|
|
4091
|
-
|
|
4092
|
-
export const ${STANDARD_PREFIX}${name} = ${NS}.standardSchemaV1(${name});`;
|
|
4093
|
-
});
|
|
4094
|
-
const nestedByTable = ["insert", "select"].flatMap((m) => {
|
|
4095
|
-
const plan = nested[m];
|
|
4096
|
-
return plan ? nestedNodes(plan).map((n) => [n.table.tsName, nestedNodeCols(n, m)]) : [];
|
|
4097
|
-
});
|
|
4098
|
-
const nestedCols = nestedByTable.flatMap(([, cs]) => cs);
|
|
4099
|
-
const referenced = /* @__PURE__ */ new Set();
|
|
4100
|
-
if (typedJson) {
|
|
4101
|
-
const all = !!typedJson.allColumns;
|
|
4102
|
-
if ([...insertCols, ...updateCols, ...selectCols].some((c) => wantsRef(c, all))) {
|
|
4103
|
-
referenced.add(T);
|
|
4104
|
-
}
|
|
4105
|
-
for (const [name, cs] of nestedByTable) {
|
|
4106
|
-
if (cs.some((c) => wantsRef(c, all))) referenced.add(name);
|
|
4107
|
-
}
|
|
4108
|
-
}
|
|
4109
|
-
const schemaImport = referenced.size ? `import type { ${[...referenced].join(", ")} } from '${typedJson.schemaSpecifier}';
|
|
4110
|
-
` : "";
|
|
4111
|
-
const needsJson = !typedJson && [...insertCols, ...updateCols, ...selectCols, ...nestedCols].some(
|
|
4112
|
-
(c) => c.shape?.kind === "json"
|
|
4113
|
-
);
|
|
4114
|
-
const finder = wantsDuplicateFinder ? (0, import_validation_core18.renderDuplicateFinder)(table, `findDuplicate${T}`, (0, import_validation_core18.typeName)("insert", T, affix)) : void 0;
|
|
4115
|
-
const duplicates = finder ? `
|
|
4116
|
-
${finder}
|
|
4117
|
-
` : "";
|
|
4118
|
-
const nestedCode = renderNestedSchemas(
|
|
4119
|
-
table,
|
|
4120
|
-
affix,
|
|
4121
|
-
coerceDates,
|
|
4122
|
-
typedJson,
|
|
4123
|
-
applyDefaults,
|
|
4124
|
-
nested,
|
|
4125
|
-
brands
|
|
4126
|
-
);
|
|
4127
|
-
const selectName = (0, import_validation_core18.schemaName)("select", T, affix);
|
|
4128
|
-
const brandAliases = (brands?.aliasesFor(T) ?? []).map(
|
|
4129
|
-
(a) => `/** The nominal type of ${T}.${a.column}. */
|
|
4130
|
-
export type ${a.alias} = ${NS}.Schema.Type<typeof ${selectName}>[${JSON.stringify(a.column)}];`
|
|
4131
|
-
).join("\n\n");
|
|
4132
|
-
const brandCode = brandAliases ? `
|
|
4133
|
-
${brandAliases}
|
|
4134
|
-
` : "";
|
|
4135
|
-
const involved = [
|
|
4136
|
-
table,
|
|
4137
|
-
...["insert", "select"].flatMap((m) => {
|
|
4138
|
-
const plan = nested[m];
|
|
4139
|
-
return plan ? nestedNodes(plan).map((n) => n.table) : [];
|
|
4140
|
-
})
|
|
4141
|
-
];
|
|
4142
|
-
const canonPreamble = involved.some((t) => {
|
|
4143
|
-
const own = parsedChecksFor(t);
|
|
4144
|
-
return (0, import_validation_core18.needsNumericCanon)(t.columns, own.checks, own.sets);
|
|
4145
|
-
}) ? `
|
|
4146
|
-
${import_validation_core18.NUMERIC_CANON_SOURCE}` : "";
|
|
4147
|
-
return `import * as ${NS} from 'effect/Schema';
|
|
4148
|
-
${schemaImport}${needsJson ? `
|
|
4149
|
-
${JSON_PREAMBLE}` : ""}${canonPreamble}
|
|
4150
|
-
${blocks.join("\n\n")}
|
|
4151
|
-
${brandCode}${nestedCode}${duplicates}`;
|
|
4152
|
-
}
|
|
4153
|
-
function buildHeader8(h) {
|
|
4154
|
-
if (h && h.enabled === false) return "";
|
|
4155
|
-
const text = h?.text?.trim();
|
|
4156
|
-
const lines = text ? text.split(/\r?\n/).map((l) => `// ${l}`) : [
|
|
4157
|
-
"// Generated by DRZL (@drzl/*)",
|
|
4158
|
-
"// Generated output is granted to you under your project's license.",
|
|
4159
|
-
"// You may use, copy, modify, and distribute without attribution."
|
|
4160
|
-
];
|
|
4161
|
-
return lines.join("\n") + "\n\n";
|
|
4162
|
-
}
|
|
4163
|
-
var import_validation_core17, import_validation_core18, DEFAULT_FILE_SUFFIX2, STANDARD_PREFIX, NS, OPS, JSON_CONST, JSON_PREAMBLE, UNKNOWN_EXPR, EffectGenerator, index_default8;
|
|
4164
|
-
var init_dist8 = __esm({
|
|
4165
|
-
"../generator-effect/dist/index.js"() {
|
|
4166
|
-
"use strict";
|
|
4167
|
-
import_validation_core17 = require("@drzl/validation-core");
|
|
4168
|
-
import_validation_core18 = require("@drzl/validation-core");
|
|
4169
|
-
DEFAULT_FILE_SUFFIX2 = ".effect.ts";
|
|
4170
|
-
STANDARD_PREFIX = "Standard";
|
|
4171
|
-
NS = "Schema";
|
|
4172
|
-
OPS = {
|
|
4173
|
-
">=": ">=",
|
|
4174
|
-
">": ">",
|
|
4175
|
-
"<=": "<=",
|
|
4176
|
-
"<": "<",
|
|
4177
|
-
"=": "===",
|
|
4178
|
-
"<>": "!=="
|
|
4179
|
-
};
|
|
4180
|
-
JSON_CONST = "DrzlJsonValue";
|
|
4181
|
-
JSON_PREAMBLE = `type ${JSON_CONST}Type =
|
|
4182
|
-
| string
|
|
4183
|
-
| number
|
|
4184
|
-
| boolean
|
|
4185
|
-
| null
|
|
4186
|
-
| readonly ${JSON_CONST}Type[]
|
|
4187
|
-
| { readonly [key: string]: ${JSON_CONST}Type };
|
|
4188
|
-
|
|
4189
|
-
const ${JSON_CONST}: ${NS}.Schema<${JSON_CONST}Type, unknown> = ${NS}.suspend(() =>
|
|
4190
|
-
${NS}.Union(
|
|
4191
|
-
${NS}.String,
|
|
4192
|
-
${NS}.Finite,
|
|
4193
|
-
${NS}.Boolean,
|
|
4194
|
-
${NS}.Null,
|
|
4195
|
-
${NS}.Array(${JSON_CONST}),
|
|
4196
|
-
// The plain-object test comes before the record, not after it. \`Schema.Record\` rebuilds its
|
|
4197
|
-
// output, so a check placed after it inspects that new object and reports every input as
|
|
4198
|
-
// plain. A Date sailed through: it has no own enumerable keys, so the record accepted it and
|
|
4199
|
-
// rebuilt it as \`{}\`.
|
|
4200
|
-
${NS}.Unknown.pipe(
|
|
4201
|
-
${NS}.filter(
|
|
4202
|
-
(o) => {
|
|
4203
|
-
if (typeof o !== 'object' || o === null || Array.isArray(o)) return false;
|
|
4204
|
-
const p = Object.getPrototypeOf(o);
|
|
4205
|
-
return p === Object.prototype || p === null;
|
|
4206
|
-
},
|
|
4207
|
-
{ description: 'a plain object' }
|
|
4208
|
-
),
|
|
4209
|
-
${NS}.compose(${NS}.Record({ key: ${NS}.String, value: ${JSON_CONST} }), { strict: false })
|
|
4210
|
-
)
|
|
4211
|
-
)
|
|
4212
|
-
);
|
|
4213
|
-
`;
|
|
4214
|
-
UNKNOWN_EXPR = `${NS}.Unknown`;
|
|
4215
|
-
EffectGenerator = class {
|
|
4216
|
-
constructor(analysis) {
|
|
4217
|
-
this.analysis = analysis;
|
|
4218
|
-
this.library = "effect";
|
|
4219
|
-
}
|
|
4220
|
-
async generate(opts) {
|
|
4221
|
-
const fs6 = (0, import_validation_core17.fileWriter)(opts.fileSink);
|
|
4222
|
-
const path9 = await import("path");
|
|
4223
|
-
const out = path9.resolve(process.cwd(), opts.outDir);
|
|
4224
|
-
const files = [];
|
|
4225
|
-
await fs6.mkdir(out, { recursive: true });
|
|
4226
|
-
const affix = (0, import_validation_core18.resolveAffix)(opts);
|
|
4227
|
-
const coerceDates = opts.coerceDates ?? "input";
|
|
4228
|
-
const fileSuffix = opts.fileSuffix ?? DEFAULT_FILE_SUFFIX2;
|
|
4229
|
-
const wantsTypes = opts.typedJson || opts.typedColumns;
|
|
4230
|
-
const typedJson = wantsTypes && opts.schemaPath ? {
|
|
4231
|
-
schemaSpecifier: (0, import_validation_core18.resolveConfiguredImport)(
|
|
4232
|
-
opts.schemaPath,
|
|
4233
|
-
out,
|
|
4234
|
-
process.cwd(),
|
|
4235
|
-
opts.importExtension
|
|
4236
|
-
),
|
|
4237
|
-
allColumns: !!opts.typedColumns
|
|
4238
|
-
} : void 0;
|
|
4239
|
-
if (wantsTypes && !opts.schemaPath) {
|
|
4240
|
-
console.warn(
|
|
4241
|
-
"[drzl] typedJson was requested but the schema path is unknown, so json columns keep their wide type."
|
|
4242
|
-
);
|
|
4243
|
-
}
|
|
4244
|
-
const nestedDepth = opts.nestedSchemas ? (0, import_validation_core18.resolveNestedDepth)(opts.nestedDepth, (m) => console.warn(m)) : 0;
|
|
4245
|
-
const brands = (0, import_validation_core18.buildBrandPlan)(this.analysis.tables, opts.branded);
|
|
4246
|
-
for (const note of brands?.notes ?? []) console.warn(`[drzl] ${note}`);
|
|
4247
|
-
for (const table of this.analysis.tables) {
|
|
4248
|
-
const filePath = path9.join(out, (0, import_validation_core18.moduleFileName)(table.tsName, fileSuffix));
|
|
4249
|
-
const code = renderTableSchemas(
|
|
4250
|
-
table,
|
|
4251
|
-
affix,
|
|
4252
|
-
coerceDates,
|
|
4253
|
-
typedJson,
|
|
4254
|
-
!!opts.applyDefaults,
|
|
4255
|
-
!!opts?.duplicateFinder,
|
|
4256
|
-
opts.nestedSchemas ? nestedPlansFor(table, this.analysis, nestedDepth) : {},
|
|
4257
|
-
brands
|
|
4258
|
-
);
|
|
4259
|
-
const formatted = await (0, import_validation_core18.formatCode)(
|
|
4260
|
-
buildHeader8(opts.outputHeader) + code,
|
|
4261
|
-
filePath,
|
|
4262
|
-
opts.format
|
|
4263
|
-
);
|
|
4264
|
-
await fs6.writeFile(filePath, formatted, "utf8");
|
|
4265
|
-
files.push(filePath);
|
|
4266
|
-
}
|
|
4267
|
-
const indexPath = path9.join(out, "index.ts");
|
|
4268
|
-
const indexFormatted = await (0, import_validation_core18.formatCode)(
|
|
4269
|
-
buildHeader8(opts.outputHeader) + this.defaultIndex(this.analysis, opts),
|
|
4270
|
-
indexPath,
|
|
4271
|
-
opts.format
|
|
4272
|
-
);
|
|
4273
|
-
await fs6.writeFile(indexPath, indexFormatted, "utf8");
|
|
4274
|
-
files.push(indexPath);
|
|
4275
|
-
return files;
|
|
4276
|
-
}
|
|
4277
|
-
renderTable(table, opts) {
|
|
4278
|
-
return renderTableSchemas(
|
|
4279
|
-
table,
|
|
4280
|
-
(0, import_validation_core18.resolveAffix)(opts),
|
|
4281
|
-
opts?.coerceDates ?? "input",
|
|
4282
|
-
void 0,
|
|
4283
|
-
!!opts?.applyDefaults,
|
|
4284
|
-
!!opts?.duplicateFinder,
|
|
4285
|
-
{},
|
|
4286
|
-
(0, import_validation_core18.buildBrandPlan)(this.analysis.tables, opts?.branded)
|
|
4287
|
-
);
|
|
4288
|
-
}
|
|
4289
|
-
defaultIndex(analysis, opts) {
|
|
4290
|
-
const fileSuffix = opts.fileSuffix ?? DEFAULT_FILE_SUFFIX2;
|
|
4291
|
-
return analysis.tables.map(
|
|
4292
|
-
(t) => `export * from '${(0, import_validation_core18.moduleSpecifier)(t.tsName, fileSuffix, opts.importExtension)}';`
|
|
4293
|
-
).join("\n") + "\n";
|
|
4294
|
-
}
|
|
4295
|
-
};
|
|
4296
|
-
index_default8 = EffectGenerator;
|
|
4297
|
-
}
|
|
4298
|
-
});
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
19
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
20
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
21
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
22
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
23
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
24
|
+
mod
|
|
25
|
+
));
|
|
4299
26
|
|
|
4300
27
|
// src/cli.ts
|
|
4301
|
-
var
|
|
28
|
+
var import_analyzer3 = require("@drzl/analyzer");
|
|
4302
29
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
4303
30
|
var import_commander = require("commander");
|
|
4304
31
|
var path8 = __toESM(require("path"), 1);
|
|
@@ -5677,14 +1404,20 @@ var GENERATORS = [
|
|
|
5677
1404
|
},
|
|
5678
1405
|
{
|
|
5679
1406
|
kind: "trpc",
|
|
5680
|
-
//
|
|
5681
|
-
// cannot publish through npm's trusted-publisher
|
|
5682
|
-
// by hand, and naming it as a hard dependency
|
|
5683
|
-
// `npm i @drzl/cli` for everyone until it
|
|
5684
|
-
//
|
|
5685
|
-
//
|
|
1407
|
+
// This one and seven others were `optionalDependencies` until every one of them had been
|
|
1408
|
+
// published: a package that has never existed cannot publish through npm's trusted-publisher
|
|
1409
|
+
// OIDC flow, so its first version goes out by hand, and naming it as a hard dependency in the
|
|
1410
|
+
// same release breaks `npm i @drzl/cli` for everyone until it does exist. An optional
|
|
1411
|
+
// dependency is skipped by the installer instead, which made that release safe.
|
|
1412
|
+
//
|
|
1413
|
+
// The side effect was invisible and lasted longer than the reason: tsup externalises
|
|
1414
|
+
// `dependencies` and `peerDependencies` and bundles everything else, so those eight travelled
|
|
1415
|
+
// inside `dist` while the other six were resolved from `node_modules`. All fourteen are on the
|
|
1416
|
+
// registry now and all fourteen are `dependencies`, which is what makes every one of them a
|
|
1417
|
+
// package that can genuinely be absent, and `loadGenerator` tell absence apart from failure
|
|
1418
|
+
// for every kind rather than for six of them.
|
|
5686
1419
|
specifier: "@drzl/generator-trpc",
|
|
5687
|
-
load: () =>
|
|
1420
|
+
load: () => import("@drzl/generator-trpc"),
|
|
5688
1421
|
construct: (m, analysis) => new m.TRPCGenerator(analysis),
|
|
5689
1422
|
outputDir: (g, cfg) => trpcOutDir(g, cfg),
|
|
5690
1423
|
options: (g, cfg, ctx) => trpcOptions(g, cfg, ctx.servicesDir)
|
|
@@ -5692,7 +1425,7 @@ var GENERATORS = [
|
|
|
5692
1425
|
{
|
|
5693
1426
|
kind: "hono",
|
|
5694
1427
|
specifier: "@drzl/generator-hono",
|
|
5695
|
-
load: () =>
|
|
1428
|
+
load: () => import("@drzl/generator-hono"),
|
|
5696
1429
|
construct: (m, analysis) => new m.HonoGenerator(analysis),
|
|
5697
1430
|
outputDir: (g, cfg) => honoOutDir(g, cfg),
|
|
5698
1431
|
options: (g, cfg) => honoOptions(g, cfg)
|
|
@@ -5700,7 +1433,7 @@ var GENERATORS = [
|
|
|
5700
1433
|
{
|
|
5701
1434
|
kind: "express",
|
|
5702
1435
|
specifier: "@drzl/generator-express",
|
|
5703
|
-
load: () =>
|
|
1436
|
+
load: () => import("@drzl/generator-express"),
|
|
5704
1437
|
construct: (m, analysis) => new m.ExpressGenerator(analysis),
|
|
5705
1438
|
outputDir: (g, cfg) => expressOutDir(g, cfg),
|
|
5706
1439
|
options: (g, cfg) => expressOptions(g, cfg)
|
|
@@ -5708,7 +1441,7 @@ var GENERATORS = [
|
|
|
5708
1441
|
{
|
|
5709
1442
|
kind: "fastify",
|
|
5710
1443
|
specifier: "@drzl/generator-fastify",
|
|
5711
|
-
load: () =>
|
|
1444
|
+
load: () => import("@drzl/generator-fastify"),
|
|
5712
1445
|
construct: (m, analysis) => new m.FastifyGenerator(analysis),
|
|
5713
1446
|
outputDir: (g, cfg) => fastifyOutDir(g, cfg),
|
|
5714
1447
|
options: (g, cfg) => fastifyOptions(g, cfg)
|
|
@@ -5716,7 +1449,7 @@ var GENERATORS = [
|
|
|
5716
1449
|
{
|
|
5717
1450
|
kind: "nestjs",
|
|
5718
1451
|
specifier: "@drzl/generator-nestjs",
|
|
5719
|
-
load: () =>
|
|
1452
|
+
load: () => import("@drzl/generator-nestjs"),
|
|
5720
1453
|
construct: (m, analysis) => new m.NestJSGenerator(analysis),
|
|
5721
1454
|
outputDir: (g, cfg) => nestjsOutDir(g, cfg),
|
|
5722
1455
|
options: (g, cfg) => nestjsOptions(g, cfg)
|
|
@@ -5724,7 +1457,7 @@ var GENERATORS = [
|
|
|
5724
1457
|
{
|
|
5725
1458
|
kind: "graphql",
|
|
5726
1459
|
specifier: "@drzl/generator-graphql",
|
|
5727
|
-
load: () =>
|
|
1460
|
+
load: () => import("@drzl/generator-graphql"),
|
|
5728
1461
|
construct: (m, analysis) => new m.GraphQLGenerator(analysis),
|
|
5729
1462
|
outputDir: (g, cfg) => graphqlOutDir(g, cfg),
|
|
5730
1463
|
options: (g, cfg) => graphqlOptions(g, cfg)
|
|
@@ -5778,7 +1511,7 @@ var GENERATORS = [
|
|
|
5778
1511
|
{
|
|
5779
1512
|
kind: "effect",
|
|
5780
1513
|
specifier: "@drzl/generator-effect",
|
|
5781
|
-
load: () =>
|
|
1514
|
+
load: () => import("@drzl/generator-effect"),
|
|
5782
1515
|
construct: (m, analysis) => new m.EffectGenerator(analysis),
|
|
5783
1516
|
outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS.effect,
|
|
5784
1517
|
options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: true })
|
|
@@ -5786,7 +1519,7 @@ var GENERATORS = [
|
|
|
5786
1519
|
{
|
|
5787
1520
|
kind: "json-schema",
|
|
5788
1521
|
specifier: "@drzl/generator-json-schema",
|
|
5789
|
-
load: () =>
|
|
1522
|
+
load: () => import("@drzl/generator-json-schema"),
|
|
5790
1523
|
construct: (m, analysis) => new m.JsonSchemaGenerator(analysis),
|
|
5791
1524
|
outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS["json-schema"],
|
|
5792
1525
|
options: (g, cfg, ctx) => jsonSchemaOptions(g, cfg, ctx.outDir)
|
|
@@ -5968,10 +1701,10 @@ function nothingToGenerate(opts) {
|
|
|
5968
1701
|
}
|
|
5969
1702
|
|
|
5970
1703
|
// src/column-filter.ts
|
|
5971
|
-
var
|
|
1704
|
+
var import_validation_core3 = require("@drzl/validation-core");
|
|
5972
1705
|
|
|
5973
1706
|
// src/doctor.ts
|
|
5974
|
-
var
|
|
1707
|
+
var import_validation_core2 = require("@drzl/validation-core");
|
|
5975
1708
|
var import_chalk2 = require("chalk");
|
|
5976
1709
|
var PLAIN = new import_chalk2.Chalk({ level: 0 });
|
|
5977
1710
|
var HANDLED_CODES = /* @__PURE__ */ new Set(["DRZL_ANL_UNKNOWN_COLUMN"]);
|
|
@@ -6036,7 +1769,7 @@ function checkFindings(table) {
|
|
|
6036
1769
|
const label = k.name ? `"${k.name}"` : "an unnamed constraint";
|
|
6037
1770
|
const raw = k.expression ?? "";
|
|
6038
1771
|
const expr = raw.trim() ? raw : "(empty)";
|
|
6039
|
-
const parsed = (0,
|
|
1772
|
+
const parsed = (0, import_validation_core2.parseCheck)(raw, k.name);
|
|
6040
1773
|
if (!parsed.ok) {
|
|
6041
1774
|
out.push({
|
|
6042
1775
|
kind: "check-declined",
|
|
@@ -6061,7 +1794,7 @@ function checkFindings(table) {
|
|
|
6061
1794
|
}
|
|
6062
1795
|
for (const l of parsed.lengths ?? []) {
|
|
6063
1796
|
const col = byName.get(l.column);
|
|
6064
|
-
if (!col || (0,
|
|
1797
|
+
if (!col || (0, import_validation_core2.lengthMeasure)(col, l)) continue;
|
|
6065
1798
|
out.push({
|
|
6066
1799
|
kind: "check-uncountable",
|
|
6067
1800
|
level: "warn",
|
|
@@ -6270,7 +2003,7 @@ function renderDoctorReport(report, style = PLAIN) {
|
|
|
6270
2003
|
|
|
6271
2004
|
// src/column-filter.ts
|
|
6272
2005
|
function checkedColumns(expression, name) {
|
|
6273
|
-
const parsed = (0,
|
|
2006
|
+
const parsed = (0, import_validation_core3.parseCheck)(expression, name);
|
|
6274
2007
|
if (!parsed.ok) return [];
|
|
6275
2008
|
return [...new Set(namedColumns(parsed).map((n) => n.column))];
|
|
6276
2009
|
}
|
|
@@ -6362,8 +2095,8 @@ function filterColumns(tables, spec) {
|
|
|
6362
2095
|
}
|
|
6363
2096
|
|
|
6364
2097
|
// src/explain.ts
|
|
6365
|
-
var
|
|
6366
|
-
var
|
|
2098
|
+
var import_analyzer = require("@drzl/analyzer");
|
|
2099
|
+
var import_validation_core4 = require("@drzl/validation-core");
|
|
6367
2100
|
var import_chalk3 = require("chalk");
|
|
6368
2101
|
var PLAIN2 = new import_chalk3.Chalk({ level: 0 });
|
|
6369
2102
|
function namesOf(table) {
|
|
@@ -6555,8 +2288,8 @@ function gapsFor(table, constraints, issues) {
|
|
|
6555
2288
|
}
|
|
6556
2289
|
function explainTable(analysis, match, options = {}) {
|
|
6557
2290
|
const table = match.table;
|
|
6558
|
-
const qualified = (0,
|
|
6559
|
-
const constraints = (0,
|
|
2291
|
+
const qualified = (0, import_analyzer.qualifiedTableName)(table);
|
|
2292
|
+
const constraints = (0, import_validation_core4.tableConstraints)(table).constraints;
|
|
6560
2293
|
const capped = new Set(
|
|
6561
2294
|
constraints.filter((c) => c.kind === "maxLength" || c.kind === "maxBytes").flatMap((c) => c.columns)
|
|
6562
2295
|
);
|
|
@@ -6588,11 +2321,11 @@ function explainTable(analysis, match, options = {}) {
|
|
|
6588
2321
|
})
|
|
6589
2322
|
}));
|
|
6590
2323
|
const relations = analysis.relations.filter((r) => r.from === qualified || r.to === qualified || r.via === qualified).map((r) => ({ ...r, outgoing: r.from === qualified }));
|
|
6591
|
-
const
|
|
2324
|
+
const keyColumns = table.columns.filter((c) => primaryKeyColumns.has(c.name));
|
|
6592
2325
|
const primaryKey = table.primaryKey?.columns.length ? {
|
|
6593
2326
|
...table.primaryKey.name ? { name: table.primaryKey.name } : {},
|
|
6594
2327
|
columns: [...table.primaryKey.columns],
|
|
6595
|
-
generated:
|
|
2328
|
+
generated: keyColumns.length > 0 && keyColumns.every((c) => c.isGenerated || c.hasDefault)
|
|
6596
2329
|
} : null;
|
|
6597
2330
|
const removed = options.keptColumns ? table.columns.map((c) => c.name).filter((name) => !options.keptColumns.includes(name)) : [];
|
|
6598
2331
|
return {
|
|
@@ -6619,7 +2352,7 @@ function explainTable(analysis, match, options = {}) {
|
|
|
6619
2352
|
foreignKeys: (table.foreignKeys ?? []).map((fk) => ({
|
|
6620
2353
|
...fk.name ? { name: fk.name } : {},
|
|
6621
2354
|
columns: [...fk.columns],
|
|
6622
|
-
references: { table: (0,
|
|
2355
|
+
references: { table: (0, import_analyzer.qualifiedForeignTable)(fk), columns: [...fk.foreignColumns] },
|
|
6623
2356
|
...fk.onDelete ? { onDelete: fk.onDelete } : {},
|
|
6624
2357
|
...fk.onUpdate ? { onUpdate: fk.onUpdate } : {}
|
|
6625
2358
|
})),
|
|
@@ -6633,10 +2366,10 @@ function summarize(analysis) {
|
|
|
6633
2366
|
name: table.name,
|
|
6634
2367
|
tsName: table.tsName,
|
|
6635
2368
|
...table.schema ? { schema: table.schema } : {},
|
|
6636
|
-
qualified: (0,
|
|
2369
|
+
qualified: (0, import_analyzer.qualifiedTableName)(table),
|
|
6637
2370
|
columns: table.columns.length,
|
|
6638
2371
|
checks: table.checks?.length ?? 0,
|
|
6639
|
-
gaps: gapsFor(table, (0,
|
|
2372
|
+
gaps: gapsFor(table, (0, import_validation_core4.tableConstraints)(table).constraints, analysis.issues).length
|
|
6640
2373
|
}));
|
|
6641
2374
|
}
|
|
6642
2375
|
var WIDTH = 80;
|
|
@@ -6685,14 +2418,14 @@ function renderExplanation(explanation, context, style = PLAIN2) {
|
|
|
6685
2418
|
const out = [];
|
|
6686
2419
|
const plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
|
|
6687
2420
|
out.push(style.bold(explanation.qualified) + style.dim(` ${context.schema}`));
|
|
6688
|
-
const
|
|
2421
|
+
const identity = [
|
|
6689
2422
|
context.dialect,
|
|
6690
2423
|
`table "${explanation.name}"`,
|
|
6691
2424
|
`export "${explanation.tsName}"`,
|
|
6692
2425
|
plural(explanation.columns.length, "column")
|
|
6693
2426
|
];
|
|
6694
|
-
if (explanation.readOnly)
|
|
6695
|
-
out.push(style.dim(" " +
|
|
2427
|
+
if (explanation.readOnly) identity.push("read-only, so no insert or update schema is emitted");
|
|
2428
|
+
out.push(style.dim(" " + identity.join(", ")));
|
|
6696
2429
|
if (!explanation.matchedExactly) {
|
|
6697
2430
|
out.push(style.dim(` matched on ${MATCH_LABELS[explanation.matchedOn]}, ignoring case`));
|
|
6698
2431
|
}
|
|
@@ -7075,7 +2808,7 @@ var import_node_fs = require("fs");
|
|
|
7075
2808
|
var import_node_path = __toESM(require("path"), 1);
|
|
7076
2809
|
async function snapshotDir(dir) {
|
|
7077
2810
|
const out = /* @__PURE__ */ new Map();
|
|
7078
|
-
async function
|
|
2811
|
+
async function walk(current) {
|
|
7079
2812
|
let entries;
|
|
7080
2813
|
try {
|
|
7081
2814
|
entries = await import_node_fs.promises.readdir(current, { withFileTypes: true });
|
|
@@ -7084,11 +2817,11 @@ async function snapshotDir(dir) {
|
|
|
7084
2817
|
}
|
|
7085
2818
|
for (const e of entries) {
|
|
7086
2819
|
const full = import_node_path.default.join(current, e.name);
|
|
7087
|
-
if (e.isDirectory()) await
|
|
2820
|
+
if (e.isDirectory()) await walk(full);
|
|
7088
2821
|
else out.set(import_node_path.default.relative(dir, full), await import_node_fs.promises.readFile(full, "utf8"));
|
|
7089
2822
|
}
|
|
7090
2823
|
}
|
|
7091
|
-
await
|
|
2824
|
+
await walk(dir);
|
|
7092
2825
|
return out;
|
|
7093
2826
|
}
|
|
7094
2827
|
async function snapshotAll(dirs) {
|
|
@@ -7450,7 +3183,7 @@ function createRebuildScheduler(options) {
|
|
|
7450
3183
|
}
|
|
7451
3184
|
|
|
7452
3185
|
// src/init.ts
|
|
7453
|
-
var
|
|
3186
|
+
var import_analyzer2 = require("@drzl/analyzer");
|
|
7454
3187
|
var fs5 = __toESM(require("fs"), 1);
|
|
7455
3188
|
var path5 = __toESM(require("path"), 1);
|
|
7456
3189
|
var INIT_GENERATOR_CHOICES = [
|
|
@@ -7489,7 +3222,7 @@ function schemaCandidates() {
|
|
|
7489
3222
|
async function classifySchemaCandidate(target) {
|
|
7490
3223
|
let analysis;
|
|
7491
3224
|
try {
|
|
7492
|
-
analysis = await new
|
|
3225
|
+
analysis = await new import_analyzer2.SchemaAnalyzer(target).analyze({
|
|
7493
3226
|
includeRelations: false,
|
|
7494
3227
|
validateConstraints: false
|
|
7495
3228
|
});
|
|
@@ -7692,7 +3425,7 @@ function normalizeGenerators(kinds) {
|
|
|
7692
3425
|
for (const k of kinds) {
|
|
7693
3426
|
if (!known.has(k)) {
|
|
7694
3427
|
throw new Error(
|
|
7695
|
-
`drzl init: "${k}" is not a generator init can scaffold. Choose from ${[...known].join(", ")}. Every other kind is
|
|
3428
|
+
`drzl init: "${k}" is not a generator init can scaffold. Choose from ${[...known].join(", ")}. Every other kind is installed and works; add it to drzl.config by hand, following the entry for it in the docs.`
|
|
7696
3429
|
);
|
|
7697
3430
|
}
|
|
7698
3431
|
}
|
|
@@ -7952,7 +3685,7 @@ withOutputFlags(
|
|
|
7952
3685
|
).action(async (schema, opts) => {
|
|
7953
3686
|
const out = outputFor(opts);
|
|
7954
3687
|
try {
|
|
7955
|
-
const analyzer = new
|
|
3688
|
+
const analyzer = new import_analyzer3.SchemaAnalyzer(schema);
|
|
7956
3689
|
const spinner = out.spinner("Analyzing schema...");
|
|
7957
3690
|
const start = Date.now();
|
|
7958
3691
|
const res = await analyzer.analyze({
|
|
@@ -8003,7 +3736,7 @@ withOutputFlags(
|
|
|
8003
3736
|
process.exit(EXIT_FAILED);
|
|
8004
3737
|
return;
|
|
8005
3738
|
}
|
|
8006
|
-
const analyzer = new
|
|
3739
|
+
const analyzer = new import_analyzer3.SchemaAnalyzer(target);
|
|
8007
3740
|
const analysis = await analyzer.analyze({
|
|
8008
3741
|
includeRelations: true,
|
|
8009
3742
|
validateConstraints: true
|
|
@@ -8080,7 +3813,7 @@ withOutputFlags(
|
|
|
8080
3813
|
}
|
|
8081
3814
|
if (source.note) out.note(out.errStyle.gray(source.note));
|
|
8082
3815
|
const spinner = out.spinner("Reading the schema...");
|
|
8083
|
-
const analysis = await new
|
|
3816
|
+
const analysis = await new import_analyzer3.SchemaAnalyzer(source.schema).analyze({
|
|
8084
3817
|
// Both on, for the reason `doctor` turns both on: this command's job is to say everything
|
|
8085
3818
|
// that is known, and a relation that appears only under a flag is one a reader would be
|
|
8086
3819
|
// told is absent.
|
|
@@ -8111,7 +3844,7 @@ withOutputFlags(
|
|
|
8111
3844
|
let keptTables;
|
|
8112
3845
|
let keptColumns;
|
|
8113
3846
|
if (cfg) {
|
|
8114
|
-
keptTables = filterTables(analysis.tables, cfg).map((t) => (0,
|
|
3847
|
+
keptTables = filterTables(analysis.tables, cfg).map((t) => (0, import_analyzer3.qualifiedTableName)(t));
|
|
8115
3848
|
try {
|
|
8116
3849
|
const narrowed = filterColumns(
|
|
8117
3850
|
[match.table],
|
|
@@ -8206,7 +3939,7 @@ withOutputFlags(
|
|
|
8206
3939
|
)
|
|
8207
3940
|
);
|
|
8208
3941
|
}
|
|
8209
|
-
const analyzer = new
|
|
3942
|
+
const analyzer = new import_analyzer3.SchemaAnalyzer(source.schema);
|
|
8210
3943
|
const spinner = out.spinner("Analyzing...");
|
|
8211
3944
|
const t0 = Date.now();
|
|
8212
3945
|
const analysis = await analyzer.analyze({
|
|
@@ -8421,7 +4154,7 @@ withOutputFlags(
|
|
|
8421
4154
|
const out = outputFor(opts);
|
|
8422
4155
|
out.warn(deprecationNotice("generate:orpc", "orpc", schema, cmd));
|
|
8423
4156
|
try {
|
|
8424
|
-
const analyzer = new
|
|
4157
|
+
const analyzer = new import_analyzer3.SchemaAnalyzer(schema);
|
|
8425
4158
|
const analysis = await analyzer.analyze({
|
|
8426
4159
|
includeRelations: !!opts.includeRelations,
|
|
8427
4160
|
validateConstraints: true
|
|
@@ -8464,7 +4197,7 @@ withOutputFlags(
|
|
|
8464
4197
|
const out = outputFor(opts);
|
|
8465
4198
|
out.warn(deprecationNotice("generate:trpc", "trpc", schema, cmd));
|
|
8466
4199
|
try {
|
|
8467
|
-
const analyzer = new
|
|
4200
|
+
const analyzer = new import_analyzer3.SchemaAnalyzer(schema);
|
|
8468
4201
|
const analysis = await analyzer.analyze({
|
|
8469
4202
|
includeRelations: !!opts.includeRelations,
|
|
8470
4203
|
validateConstraints: true
|
|
@@ -8645,7 +4378,7 @@ program.command("watch").description("Watch schema and regenerate on changes").o
|
|
|
8645
4378
|
});
|
|
8646
4379
|
}
|
|
8647
4380
|
for (const w of source.warnings) out.warn(w);
|
|
8648
|
-
const analyzer = new
|
|
4381
|
+
const analyzer = new import_analyzer3.SchemaAnalyzer(source.schema);
|
|
8649
4382
|
const analysis = await analyzer.analyze({
|
|
8650
4383
|
includeRelations: cfg.analyzer.includeRelations,
|
|
8651
4384
|
validateConstraints: cfg.analyzer.validateConstraints,
|