@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.
@@ -1,794 +0,0 @@
1
- // ../generator-json-schema/dist/index.js
2
- import { fileWriter } from "@drzl/validation-core";
3
- import {
4
- formatCode,
5
- moduleFileName,
6
- moduleSpecifier,
7
- resolveAffix,
8
- schemaName,
9
- typeName
10
- } from "@drzl/validation-core";
11
- import { qualifiedForeignTable, qualifiedTableName } from "@drzl/analyzer";
12
- import {
13
- applyWirePolicy,
14
- canonicalMembers,
15
- comparisonWire,
16
- COLUMN_FORMATS,
17
- insertColumns,
18
- isIntegerColumn,
19
- parseCheck,
20
- selectColumns,
21
- updateColumns
22
- } from "@drzl/validation-core";
23
- var identity = (values) => JSON.stringify([...values]);
24
- function enumKey(name) {
25
- const safe = name.replace(/[^A-Za-z0-9.\-_]/g, "_").replace(/^_+|_+$/g, "");
26
- return safe.length ? safe : void 0;
27
- }
28
- function declaredEnumColumns(columns) {
29
- return columns.filter((c) => !c.shape && c.enumValues && c.enumValues.length);
30
- }
31
- function planSharedEnums(columns, enums, ref2, reserved = /* @__PURE__ */ new Set()) {
32
- if (!enums?.length) return void 0;
33
- const uses = /* @__PURE__ */ new Map();
34
- for (const c of declaredEnumColumns(columns)) {
35
- const id = identity(c.enumValues);
36
- uses.set(id, (uses.get(id) ?? 0) + 1);
37
- }
38
- const keyed = /* @__PURE__ */ new Map();
39
- const taken = new Set(reserved);
40
- for (const e of enums) {
41
- const id = identity(e.values);
42
- if ((uses.get(id) ?? 0) < 2) continue;
43
- if (keyed.has(id)) continue;
44
- const key = enumKey(e.name);
45
- if (!key || taken.has(key)) continue;
46
- taken.add(key);
47
- keyed.set(id, { key, values: [...e.values] });
48
- }
49
- if (!keyed.size) return void 0;
50
- const used = /* @__PURE__ */ new Set();
51
- return {
52
- resolve(values) {
53
- const hit = keyed.get(identity(values));
54
- if (!hit) return void 0;
55
- used.add(hit.key);
56
- return ref2(hit.key);
57
- },
58
- definitions() {
59
- const out = {};
60
- for (const { key, values } of keyed.values()) {
61
- if (used.has(key)) out[key] = { enum: [...values] };
62
- }
63
- return out;
64
- }
65
- };
66
- }
67
- var allColumns = (tables) => tables.flatMap((t) => t.columns);
68
- var DRAFT = "https://json-schema.org/draft/2020-12/schema";
69
- var UUID_FORMAT = "uuid";
70
- var base64 = (target) => target === "openapi-3.0" ? { type: "string", format: "byte" } : { type: "string", contentEncoding: "base64" };
71
- function canonicalSetPattern(values, integerOnly) {
72
- const branches = canonicalMembers(values).map((member) => {
73
- if (member === "0") return integerOnly ? "[+-]?0+" : "[+-]?(?:0+(?:\\.0*)?|0*\\.0+)";
74
- const sign = member.startsWith("-") ? "-" : "\\+?";
75
- const body = member.startsWith("-") ? member.slice(1) : member;
76
- const [int = "", frac = ""] = body.split(".");
77
- if (integerOnly) return `${sign}0*${int}`;
78
- if (!frac) return `${sign}0*${int}(?:\\.0*)?`;
79
- return int === "0" ? `${sign}0*\\.${frac}0*` : `${sign}0*${int}\\.${frac}0*`;
80
- });
81
- return `^(?:${branches.join("|")})$`;
82
- }
83
- function baseSchema(c, mode, target, checks, sets, lengths, enumRef) {
84
- const s = c.shape;
85
- if (s) {
86
- switch (s.kind) {
87
- case "json":
88
- return {};
89
- case "custom":
90
- return {};
91
- case "buffer": {
92
- const bin = base64(target);
93
- applyBinaryLengths(bin, c, lengths);
94
- return bin;
95
- }
96
- case "tuple":
97
- return target === "openapi-3.0" ? { type: "array", items: { type: "number" }, minItems: s.length, maxItems: s.length } : {
98
- type: "array",
99
- prefixItems: Array.from({ length: s.length }, () => ({ type: "number" })),
100
- minItems: s.length,
101
- maxItems: s.length
102
- };
103
- case "numberObject":
104
- return {
105
- type: "object",
106
- properties: Object.fromEntries(s.fields.map((f) => [f, { type: "number" }])),
107
- required: [...s.fields]
108
- };
109
- case "numberVector":
110
- return {
111
- type: "array",
112
- items: { type: "number" },
113
- ...s.length ? { minItems: s.length, maxItems: s.length } : {}
114
- };
115
- case "bitstring":
116
- return {
117
- type: "string",
118
- pattern: "^[01]*$",
119
- ...s.length ? s.exact ? { minLength: s.length, maxLength: s.length } : { maxLength: s.length } : {}
120
- };
121
- case "byteString":
122
- return { type: "string", ...s.length ? { maxLength: s.length } : {} };
123
- }
124
- }
125
- const set = sets.find((x) => x.column === c.name);
126
- if (set) {
127
- if (comparisonWire(c) === "numeric-string") {
128
- return { type: "string", pattern: canonicalSetPattern(set.values, c.dbType === "BIGINT") };
129
- }
130
- return {
131
- enum: set.values.map(
132
- (v) => set.kind === "string" || c.tsType === "bigint" ? v : Number(v)
133
- )
134
- };
135
- }
136
- if (c.enumValues && c.enumValues.length) {
137
- const ref2 = enumRef?.(c.enumValues);
138
- return ref2 ? { $ref: ref2 } : { enum: [...c.enumValues] };
139
- }
140
- const mine = c.arrayDimensions ? [] : checks.filter((k) => k.column === c.name);
141
- const eq = mine.find((k) => k.operator === "=");
142
- if (eq) {
143
- if (comparisonWire(c) === "numeric-string") {
144
- return { type: "string", pattern: canonicalSetPattern([eq.value], c.dbType === "BIGINT") };
145
- }
146
- const only = eq.kind === "string" || c.tsType === "bigint" ? eq.value : Number(eq.value);
147
- return target === "openapi-3.0" ? { enum: [only] } : { const: only };
148
- }
149
- switch (c.tsType) {
150
- case "string": {
151
- const out = { type: "string" };
152
- if (c.format === "uuid") out.format = UUID_FORMAT;
153
- else if (c.format && COLUMN_FORMATS[c.format]) out.pattern = COLUMN_FORMATS[c.format];
154
- if (c.maxLength !== void 0) out.maxLength = c.maxLength;
155
- applyByteCap(out, c, lengths);
156
- applyLengths(out, c, lengths);
157
- return out;
158
- }
159
- case "number": {
160
- const out = { type: isIntegerColumn(c) ? "integer" : "number" };
161
- if (!c.arrayDimensions) applyNumericBounds(out, c, checks, target);
162
- return out;
163
- }
164
- case "bigint":
165
- return {
166
- type: "string",
167
- pattern: typeof c.min === "string" && !c.min.startsWith("-") ? "^\\d+$" : "^-?\\d+$"
168
- };
169
- case "boolean":
170
- return { type: "boolean" };
171
- case "Date":
172
- return { type: "string", format: "date-time" };
173
- case "Uint8Array":
174
- return base64(target);
175
- default:
176
- return {};
177
- }
178
- }
179
- function applyByteCap(out, c, lengths) {
180
- const budget = byteBudget(c, lengths);
181
- if (budget === void 0) return;
182
- out.maxLength = Math.min(Number(out.maxLength ?? Infinity), budget);
183
- out.description = BYTE_BUDGET_NOTE(budget);
184
- }
185
- function ceilingOf(k) {
186
- if (k.operator === "<=" || k.operator === "=") return Number(k.value);
187
- if (k.operator === "<") return Number(k.value) - 1;
188
- return void 0;
189
- }
190
- function byteBudget(c, lengths) {
191
- const bounds = [
192
- ...c.maxBytes ? [c.maxBytes] : [],
193
- ...lengths.filter((k) => k.column === c.name && k.unit === "bytes").map(ceilingOf).filter((n) => n !== void 0)
194
- ];
195
- return bounds.length ? Math.min(...bounds) : void 0;
196
- }
197
- function applyLengths(out, c, lengths) {
198
- for (const k of lengths.filter((x) => x.column === c.name && x.unit !== "bytes")) {
199
- const n = Number(k.value);
200
- if (k.operator === ">=") out.minLength = Math.max(Number(out.minLength ?? 0), n);
201
- else if (k.operator === ">") out.minLength = Math.max(Number(out.minLength ?? 0), n + 1);
202
- else if (k.operator === "<=") out.maxLength = Math.min(Number(out.maxLength ?? Infinity), n);
203
- else if (k.operator === "<") out.maxLength = Math.min(Number(out.maxLength ?? Infinity), n - 1);
204
- else if (k.operator === "=") {
205
- out.minLength = n;
206
- out.maxLength = n;
207
- }
208
- }
209
- }
210
- function applyBinaryLengths(out, c, lengths) {
211
- const budget = byteBudget(c, lengths);
212
- if (budget === void 0) return;
213
- out.maxLength = 4 * Math.ceil(budget / 3);
214
- 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.`;
215
- }
216
- var 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.`;
217
- function applyNumericBounds(out, c, checks, target) {
218
- let min = c.min !== void 0 ? { value: Number(c.min), exclusive: false } : void 0;
219
- let max = c.max !== void 0 ? { value: Number(c.max), exclusive: false } : void 0;
220
- for (const k of checks.filter((x) => x.column === c.name && x.kind === "number")) {
221
- if (k.operator === ">=") min = { value: Number(k.value), exclusive: false };
222
- else if (k.operator === ">") min = { value: Number(k.value), exclusive: true };
223
- else if (k.operator === "<=") max = { value: Number(k.value), exclusive: false };
224
- else if (k.operator === "<") max = { value: Number(k.value), exclusive: true };
225
- }
226
- const old = target === "openapi-3.0";
227
- if (min) {
228
- if (min.exclusive && !old) out.exclusiveMinimum = min.value;
229
- else {
230
- out.minimum = min.value;
231
- if (min.exclusive) out.exclusiveMinimum = true;
232
- }
233
- }
234
- if (max) {
235
- if (max.exclusive && !old) out.exclusiveMaximum = max.value;
236
- else {
237
- out.maximum = max.value;
238
- if (max.exclusive) out.exclusiveMaximum = true;
239
- }
240
- }
241
- }
242
- function cardinalityBounds(c, cardinalities) {
243
- if (!c.arrayDimensions) return {};
244
- const out = {};
245
- for (const k of cardinalities.filter((x) => x.column === c.name)) {
246
- const n = Number(k.value);
247
- if (k.operator === ">=") out.minItems = n;
248
- else if (k.operator === ">") out.minItems = n + 1;
249
- else if (k.operator === "<=") out.maxItems = n;
250
- else if (k.operator === "<") out.maxItems = n - 1;
251
- else if (k.operator === "=") {
252
- out.minItems = n;
253
- out.maxItems = n;
254
- }
255
- }
256
- return out;
257
- }
258
- function makeNullable(s, target) {
259
- if (target === "openapi-3.0") return { ...s, nullable: true };
260
- if ("$ref" in s) return { anyOf: [s, { type: "null" }] };
261
- if (s.type === void 0) {
262
- if (Array.isArray(s.enum)) return { ...s, enum: [...s.enum, null] };
263
- if ("const" in s) {
264
- const { const: k, ...rest } = s;
265
- return { ...rest, enum: [k, null] };
266
- }
267
- return s;
268
- }
269
- return { ...s, type: [s.type, "null"] };
270
- }
271
- function columnSchema(c, mode, target, checks, sets, lengths, cardinalities, applyDefault, enumRef) {
272
- const wantsDefault = mode === "insert" && applyDefault && c.defaultValue !== void 0;
273
- const refBlockedBy30 = target === "openapi-3.0" && !c.arrayDimensions && (c.nullable || wantsDefault);
274
- let s = baseSchema(c, mode, target, checks, sets, lengths, refBlockedBy30 ? void 0 : enumRef);
275
- const dims = c.arrayDimensions ?? 0;
276
- for (let i = 0; i < dims; i++) {
277
- s = { type: "array", items: s, ...i === dims - 1 ? cardinalityBounds(c, cardinalities) : {} };
278
- }
279
- if (c.nullable) s = makeNullable(s, target);
280
- if (mode === "insert" && applyDefault && c.defaultValue !== void 0) {
281
- s = { ...s, default: c.defaultValue };
282
- }
283
- return s;
284
- }
285
- function rowDescription(rows, cols) {
286
- const present = new Set(cols.map((c) => c.name));
287
- const applicable = rows.filter((r) => present.has(r.left) && present.has(r.right));
288
- if (!applicable.length) return void 0;
289
- const list = applicable.map((r) => `${r.name ? `${r.name}: ` : ""}${r.left} ${r.operator} ${r.right}`).join("; ");
290
- return `Row constraints not expressible in JSON Schema: ${list}`;
291
- }
292
- function tableSchema(table, cols, mode, target, applyDefaults, parsed, enums, localDefs) {
293
- const properties = {};
294
- const required = [];
295
- for (const c of cols) {
296
- properties[c.name] = columnSchema(
297
- c,
298
- mode,
299
- target,
300
- parsed.checks,
301
- parsed.sets,
302
- parsed.lengths,
303
- parsed.cardinalities,
304
- applyDefaults,
305
- enums?.resolve
306
- );
307
- const suppliedOnInsert = c.hasDefault || applyDefaults && c.defaultValue !== void 0 || c.isGenerated;
308
- const optional = mode === "update" || mode === "insert" && suppliedOnInsert;
309
- if (!optional) required.push(c.name);
310
- }
311
- const desc = rowDescription(parsed.rows, cols);
312
- const defs = localDefs ? enums?.definitions() ?? {} : {};
313
- return {
314
- ...target === "draft-2020-12" ? { $schema: DRAFT } : {},
315
- $id: `${table.tsName}.${mode}`,
316
- title: `${mode} ${table.tsName}`,
317
- ...desc ? { description: desc } : {},
318
- type: "object",
319
- properties,
320
- ...required.length ? { required } : {},
321
- additionalProperties: false,
322
- ...Object.keys(defs).length ? { $defs: defs } : {}
323
- };
324
- }
325
- function collect(table) {
326
- const parsed = (table.checks ?? []).map((k) => parseCheck(k.expression, k.name));
327
- const { checks, sets } = applyWirePolicy(
328
- table.columns,
329
- parsed.flatMap((p) => p.ok ? p.checks : []),
330
- parsed.flatMap((p) => p.ok ? p.sets ?? [] : [])
331
- );
332
- return {
333
- checks,
334
- sets,
335
- rows: parsed.flatMap((p) => p.ok ? p.rows ?? [] : []),
336
- lengths: parsed.flatMap((p) => p.ok ? p.lengths ?? [] : []),
337
- cardinalities: parsed.flatMap((p) => p.ok ? p.cardinalities ?? [] : [])
338
- };
339
- }
340
- function tableSchemas(table, opts = {}) {
341
- const target = opts.target ?? "draft-2020-12";
342
- const parsed = collect(table);
343
- const localDefs = target === "draft-2020-12";
344
- const build2 = (cols, mode) => {
345
- const plan = localDefs ? planSharedEnums(cols, opts.enums, (key) => `#/$defs/${key}`) : void 0;
346
- return tableSchema(table, cols, mode, target, !!opts.applyDefaults, parsed, plan, localDefs);
347
- };
348
- return {
349
- insert: build2(insertColumns(table), "insert"),
350
- update: build2(updateColumns(table), "update"),
351
- select: build2(selectColumns(table), "select")
352
- };
353
- }
354
- function tableSchemasWith(table, target, applyDefaults, plan, modes = MODES) {
355
- const parsed = collect(table);
356
- const columns = {
357
- insert: () => insertColumns(table),
358
- update: () => updateColumns(table),
359
- select: () => selectColumns(table)
360
- };
361
- const out = {};
362
- for (const mode of modes) {
363
- out[mode] = tableSchema(
364
- table,
365
- columns[mode](),
366
- mode,
367
- target,
368
- applyDefaults,
369
- parsed,
370
- plan,
371
- false
372
- );
373
- }
374
- return out;
375
- }
376
- function componentsDocument(tables, opts = {}) {
377
- const target = opts.target ?? "draft-2020-12";
378
- const schemas = {};
379
- for (const table of tables) {
380
- const built = tableSchemasWith(table, target, !!opts.applyDefaults, void 0);
381
- for (const mode of MODES) {
382
- const { $schema: _dialect, $id: _id, ...rest } = built[mode];
383
- schemas[componentSchemaName(table, mode)] = rest;
384
- }
385
- }
386
- return { schemas };
387
- }
388
- var MODES = ["insert", "update", "select"];
389
- var componentSchemaName = (table, mode) => `${table.tsName}${mode[0].toUpperCase()}${mode.slice(1)}`;
390
- function documentSchemas(tables, opts) {
391
- const plan = planSharedEnums(
392
- tables.flatMap((t) => t.columns),
393
- opts.enums,
394
- (key) => `#/components/schemas/${key}`,
395
- opts.reserved
396
- );
397
- const built = /* @__PURE__ */ new Map();
398
- for (const table of tables) {
399
- const all = tableSchemasWith(table, opts.target, opts.applyDefaults, plan, opts.modes(table));
400
- built.set(table, all);
401
- }
402
- return { built, definitions: () => plan?.definitions() ?? {} };
403
- }
404
- var ERROR_SCHEMA = "Error";
405
- var componentName = componentSchemaName;
406
- var ref = (name) => ({ $ref: `#/components/schemas/${name}` });
407
- var pascal = (s) => s.charAt(0).toUpperCase() + s.slice(1);
408
- function keyColumns(table) {
409
- const names = table.primaryKey?.columns ?? [];
410
- if (!names.length) return null;
411
- const cols = names.map((n) => table.columns.find((c) => c.name === n));
412
- if (cols.some((c) => !c)) return null;
413
- return cols;
414
- }
415
- var modesFor = (table, key) => [
416
- ...table.readOnly ? [] : ["insert"],
417
- ...table.readOnly || !key ? [] : ["update"],
418
- "select"
419
- ];
420
- var resourceSegment = (table) => table.schema ? `${encodeURIComponent(table.schema)}/${encodeURIComponent(table.name)}` : encodeURIComponent(table.name);
421
- function foreignKeysOf(table) {
422
- if (table.foreignKeys?.length) return table.foreignKeys;
423
- return table.columns.filter((c) => c.references).map((c) => ({
424
- columns: [c.name],
425
- foreignTable: c.references.table,
426
- ...c.references.schema ? { foreignSchema: c.references.schema } : {},
427
- foreignColumns: [c.references.column]
428
- }));
429
- }
430
- var jsonBody = (schema) => ({ content: { "application/json": { schema } } });
431
- function build(tables, opts) {
432
- const target = opts.target ?? "draft-2020-12";
433
- const schemaTarget = target === "openapi-3.0" ? "openapi-3.0" : "openapi-3.1";
434
- const failure = String(opts.validationStatus ?? 400);
435
- const paths = {};
436
- const schemas = {};
437
- const tags = [];
438
- const operationIds = /* @__PURE__ */ new Map();
439
- const owner = /* @__PURE__ */ new Map();
440
- const claim = (path, by, label) => {
441
- const taken = owner.get(path);
442
- if (taken !== void 0 && taken.by !== by) {
443
- throw new Error(
444
- `@drzl/generator-json-schema: the OpenAPI path "${path}" 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.`
445
- );
446
- }
447
- owner.set(path, { by, label });
448
- };
449
- const operation = (id, table, rest) => {
450
- const clash = operationIds.get(id);
451
- if (clash !== void 0) {
452
- throw new Error(
453
- `@drzl/generator-json-schema: the operationId "${id}" would be emitted for both "${clash}" and "${qualifiedTableName(table)}". An operationId is the method name a client generator derives, and the specification requires it to be unique across the document.`
454
- );
455
- }
456
- operationIds.set(id, qualifiedTableName(table));
457
- return { operationId: id, tags: [qualifiedTableName(table)], ...rest };
458
- };
459
- const keys = new Map(tables.map((t) => [t, keyColumns(t)]));
460
- const carried = new Map(tables.map((t) => [t, modesFor(t, keys.get(t))]));
461
- const reserved = /* @__PURE__ */ new Set([
462
- ERROR_SCHEMA,
463
- ...tables.flatMap((t) => carried.get(t).map((m) => componentName(t, m)))
464
- ]);
465
- const shared = documentSchemas(tables, {
466
- target: schemaTarget,
467
- applyDefaults: !!opts.applyDefaults,
468
- reserved,
469
- modes: (t) => carried.get(t),
470
- ...opts.enums ? { enums: opts.enums } : {}
471
- });
472
- const built = tables.map((table) => ({
473
- table,
474
- key: keys.get(table),
475
- segment: resourceSegment(table),
476
- schemas: shared.built.get(table)
477
- }));
478
- for (const { table, key, segment, schemas: built3 } of built) {
479
- for (const mode of carried.get(table)) {
480
- const { $schema: _dialect, $id: _id, ...rest } = built3[mode];
481
- schemas[componentName(table, mode)] = rest;
482
- }
483
- const notes = [];
484
- if (!key) notes.push("It has no primary key, so no path addresses a single row.");
485
- if (table.readOnly) {
486
- notes.push("It refuses every write, so only reads are described.");
487
- }
488
- tags.push({
489
- name: qualifiedTableName(table),
490
- description: [`Table "${qualifiedTableName(table)}".`, ...notes].join(" ")
491
- });
492
- const T = pascal(table.tsName);
493
- const select = ref(componentName(table, "select"));
494
- const validationFailed = {
495
- description: "The request does not match the schema for this operation.",
496
- ...jsonBody(ref(ERROR_SCHEMA))
497
- };
498
- const collidable = [
499
- ...table.primaryKey ? [`primary key (${table.primaryKey.columns.join(", ")})`] : [],
500
- ...table.unique.map((u) => `${u.name ? `${u.name} ` : ""}(${u.columns.join(", ")})`)
501
- ];
502
- const conflict = (constraints) => ({
503
- description: `The row collides with an existing one on ${constraints.join("; ")}.`,
504
- ...jsonBody(ref(ERROR_SCHEMA))
505
- });
506
- const collection = `/${segment}`;
507
- claim(collection, table.tsName, qualifiedTableName(table));
508
- const item = {
509
- get: operation(`list${T}`, table, {
510
- summary: `List every ${table.name} row.`,
511
- // No pagination parameters. Whether the server implements a limit, an offset or a cursor is
512
- // not something a Drizzle schema states, and a declared parameter nothing honours is worse
513
- // than an undeclared one.
514
- responses: {
515
- "200": {
516
- description: `Every ${table.name} row.`,
517
- ...jsonBody({ type: "array", items: select })
518
- }
519
- }
520
- })
521
- };
522
- if (!table.readOnly) {
523
- item.post = operation(`create${T}`, table, {
524
- summary: `Create a ${table.name} row.`,
525
- requestBody: { required: true, ...jsonBody(ref(componentName(table, "insert"))) },
526
- responses: {
527
- "201": { description: `The ${table.name} row that was created.`, ...jsonBody(select) },
528
- [failure]: validationFailed,
529
- ...collidable.length ? { "409": conflict(collidable) } : {}
530
- }
531
- });
532
- }
533
- paths[collection] = item;
534
- if (!key) continue;
535
- const itemPath = `${collection}/${key.map((c) => `{${c.name}}`).join("/")}`;
536
- claim(itemPath, table.tsName, qualifiedTableName(table));
537
- const parameters = key.map((c) => ({
538
- name: c.name,
539
- in: "path",
540
- required: true,
541
- description: `${c.name}, from the primary key of ${table.name}.`,
542
- // The column's own schema rather than a string, so an integer key is declared as one and a
543
- // uuid key carries its format. This is the whole point of reading the real key.
544
- schema: built3.select.properties[c.name] ?? {}
545
- }));
546
- const missing = {
547
- description: `No ${table.name} row has that ${key.map((c) => c.name).join(" and ")}.`,
548
- ...jsonBody(ref(ERROR_SCHEMA))
549
- };
550
- const byId = {
551
- parameters,
552
- get: operation(`get${T}`, table, {
553
- summary: `Read one ${table.name} row.`,
554
- responses: {
555
- "200": { description: `The requested ${table.name} row.`, ...jsonBody(select) },
556
- [failure]: validationFailed,
557
- "404": missing
558
- }
559
- })
560
- };
561
- if (!table.readOnly) {
562
- byId.patch = operation(`update${T}`, table, {
563
- summary: `Patch one ${table.name} row.`,
564
- requestBody: { required: true, ...jsonBody(ref(componentName(table, "update"))) },
565
- responses: {
566
- "200": { description: `The ${table.name} row after the patch.`, ...jsonBody(select) },
567
- [failure]: validationFailed,
568
- "404": missing,
569
- // The primary key is not in the update schema, so a patch cannot collide on it. Only a
570
- // unique constraint over other columns can.
571
- ...table.unique.length ? {
572
- "409": conflict(
573
- table.unique.map((u) => `${u.name ? `${u.name} ` : ""}(${u.columns.join(", ")})`)
574
- )
575
- } : {}
576
- }
577
- });
578
- byId.delete = operation(`delete${T}`, table, {
579
- summary: `Delete one ${table.name} row.`,
580
- responses: {
581
- // No body. Handing back the deleted row is the alternative and it is not a true statement
582
- // on every dialect DRZL supports: RETURNING is Postgres and SQLite, and MySQL has no such
583
- // clause, so an implementation there has nothing to send.
584
- "204": { description: `The ${table.name} row was deleted. No content is returned.` },
585
- [failure]: validationFailed,
586
- "404": missing
587
- }
588
- });
589
- }
590
- paths[itemPath] = byId;
591
- if (!opts.includeRelations) continue;
592
- for (const child of built) {
593
- if (child.table === table) continue;
594
- const matching = foreignKeysOf(child.table).filter(
595
- (fk) => (
596
- // Qualified on both sides. On the bare name a key pointing at `reporting.users` also
597
- // answered for `public.users`, so the child was hung under a parent in another schema.
598
- qualifiedForeignTable(fk) === qualifiedTableName(table) && fk.foreignColumns.length === key.length && fk.foreignColumns.every((c, i) => c === key[i].name)
599
- )
600
- );
601
- if (matching.length !== 1) continue;
602
- const subPath = `${itemPath}/${child.segment}`;
603
- claim(
604
- subPath,
605
- `${table.tsName} -> ${child.table.tsName}`,
606
- `${qualifiedTableName(table)} -> ${qualifiedTableName(child.table)}`
607
- );
608
- paths[subPath] = {
609
- parameters,
610
- get: operation(`list${T}${pascal(child.table.tsName)}`, child.table, {
611
- summary: `List the ${child.table.name} rows belonging to one ${table.name} row.`,
612
- responses: {
613
- "200": {
614
- description: `The ${child.table.name} rows whose ${matching[0].columns.join(", ")} names this ${table.name} row.`,
615
- ...jsonBody({ type: "array", items: ref(componentName(child.table, "select")) })
616
- },
617
- [failure]: validationFailed,
618
- "404": missing
619
- }
620
- })
621
- };
622
- }
623
- }
624
- return { paths, schemas: { ...schemas, ...shared.definitions() }, tags };
625
- }
626
- var errorSchema = () => ({
627
- title: "error",
628
- description: "What an operation returns when it does not return the row.",
629
- type: "object",
630
- properties: {
631
- message: { type: "string" },
632
- code: { type: "string" }
633
- },
634
- required: ["message"],
635
- additionalProperties: true
636
- });
637
- function openApiDocument(tables, opts = {}) {
638
- const target = opts.target ?? "draft-2020-12";
639
- const { paths, schemas, tags } = build(tables, opts);
640
- if (ERROR_SCHEMA in schemas) {
641
- throw new Error(
642
- `@drzl/generator-json-schema: a table produced the component schema name "${ERROR_SCHEMA}", which the document already uses for its error responses.`
643
- );
644
- }
645
- return {
646
- openapi: target === "openapi-3.0" ? "3.0.3" : "3.1.1",
647
- info: {
648
- title: opts.info?.title ?? "API",
649
- version: opts.info?.version ?? "0.0.0",
650
- 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."
651
- },
652
- ...opts.servers?.length ? { servers: opts.servers } : {},
653
- paths,
654
- components: { schemas: { ...schemas, [ERROR_SCHEMA]: errorSchema() } },
655
- tags
656
- };
657
- }
658
- var DEFAULT_FILE_SUFFIX = ".schema.ts";
659
- function renderTableModule(table, affix, target, applyDefaults, enums) {
660
- const T = table.tsName;
661
- const schemas = tableSchemas(table, { target, applyDefaults, ...enums ? { enums } : {} });
662
- const decl = (mode) => `export const ${schemaName(mode, T, affix)} = ${JSON.stringify(schemas[mode], null, 2)} as const;
663
-
664
- export type ${typeName(mode, T, affix)} = typeof ${schemaName(mode, T, affix)};`;
665
- return [decl("insert"), decl("update"), decl("select")].join("\n\n") + "\n";
666
- }
667
- function resolveDocument(opt) {
668
- if (!opt) return null;
669
- const o = opt === true ? {} : opt;
670
- if (o.enabled === false) return null;
671
- return { ...o, format: o.format ?? "ts" };
672
- }
673
- var JsonSchemaGenerator = class {
674
- constructor(analysis) {
675
- this.analysis = analysis;
676
- this.library = "json-schema";
677
- }
678
- async generate(opts) {
679
- const fs = fileWriter(opts.fileSink);
680
- const path = await import("path");
681
- const out = path.resolve(process.cwd(), opts.outDir);
682
- const files = [];
683
- await fs.mkdir(out, { recursive: true });
684
- const affix = resolveAffix(opts);
685
- const fileSuffix = opts.fileSuffix ?? DEFAULT_FILE_SUFFIX;
686
- const target = opts.target ?? "draft-2020-12";
687
- const document = resolveDocument(opts.document);
688
- for (const table of this.analysis.tables) {
689
- const filePath = path.join(out, moduleFileName(table.tsName, fileSuffix));
690
- const code = renderTableModule(
691
- table,
692
- affix,
693
- target,
694
- !!opts.applyDefaults,
695
- opts.sharedEnums ? this.analysis.enums : void 0
696
- );
697
- const formatted = await formatCode(
698
- buildHeader(opts.outputHeader) + code,
699
- filePath,
700
- opts.format
701
- );
702
- await fs.writeFile(filePath, formatted, "utf8");
703
- files.push(filePath);
704
- }
705
- if (opts.components) {
706
- const doc = componentsDocument(this.analysis.tables, {
707
- target,
708
- applyDefaults: !!opts.applyDefaults
709
- });
710
- const componentsPath = path.join(out, "components.ts");
711
- const code = `export const components = ${JSON.stringify(doc, null, 2)} as const;
712
- `;
713
- await fs.writeFile(
714
- componentsPath,
715
- await formatCode(buildHeader(opts.outputHeader) + code, componentsPath, opts.format),
716
- "utf8"
717
- );
718
- files.push(componentsPath);
719
- }
720
- if (document) {
721
- const built = openApiDocument(this.analysis.tables, {
722
- target,
723
- applyDefaults: !!opts.applyDefaults,
724
- includeRelations: !!opts.includeRelations,
725
- enums: this.analysis.enums,
726
- info: document.info,
727
- servers: document.servers,
728
- validationStatus: document.validationStatus
729
- });
730
- const body = JSON.stringify(built, null, 2);
731
- if (document.format !== "json") {
732
- const tsPath = path.join(out, "openapi.ts");
733
- const code = `export const openapi = ${body} as const;
734
- `;
735
- await fs.writeFile(
736
- tsPath,
737
- await formatCode(buildHeader(opts.outputHeader) + code, tsPath, opts.format),
738
- "utf8"
739
- );
740
- files.push(tsPath);
741
- }
742
- if (document.format !== "ts") {
743
- const jsonPath = path.join(out, "openapi.json");
744
- await fs.writeFile(jsonPath, body + "\n", "utf8");
745
- files.push(jsonPath);
746
- }
747
- }
748
- const ext = opts.importExtension === "none" ? "" : ".js";
749
- const indexPath = path.join(out, "index.ts");
750
- const index = this.analysis.tables.map(
751
- (t) => `export * from '${moduleSpecifier(t.tsName, fileSuffix, opts.importExtension)}';`
752
- ).concat(opts.components ? [`export * from './components${ext}';`] : []).concat(document && document.format !== "json" ? [`export * from './openapi${ext}';`] : []).join("\n") + "\n";
753
- const indexFormatted = await formatCode(
754
- buildHeader(opts.outputHeader) + index,
755
- indexPath,
756
- opts.format
757
- );
758
- await fs.writeFile(indexPath, indexFormatted, "utf8");
759
- files.push(indexPath);
760
- return files;
761
- }
762
- renderTable(table, opts) {
763
- return renderTableModule(
764
- table,
765
- resolveAffix(opts),
766
- opts?.target ?? "draft-2020-12",
767
- !!opts?.applyDefaults,
768
- opts?.sharedEnums ? this.analysis.enums : void 0
769
- );
770
- }
771
- };
772
- var index_default = JsonSchemaGenerator;
773
- function buildHeader(h) {
774
- if (h?.enabled === false) return "";
775
- const text = h?.text ?? "// Generated by DRZL. Do not edit by hand.";
776
- return `${text}
777
-
778
- `;
779
- }
780
-
781
- export {
782
- enumKey,
783
- planSharedEnums,
784
- allColumns,
785
- DRAFT,
786
- tableSchemas,
787
- componentsDocument,
788
- componentSchemaName,
789
- documentSchemas,
790
- openApiDocument,
791
- JsonSchemaGenerator,
792
- index_default
793
- };
794
- //# sourceMappingURL=chunk-KKPDOZOD.js.map