@drzl/cli 4.16.0 → 4.17.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 CHANGED
@@ -547,9 +547,11 @@ ${body}
547
547
  // ../generator-json-schema/dist/index.js
548
548
  var dist_exports2 = {};
549
549
  __export(dist_exports2, {
550
+ DRAFT: () => DRAFT,
550
551
  JsonSchemaGenerator: () => JsonSchemaGenerator,
551
552
  componentsDocument: () => componentsDocument,
552
553
  default: () => index_default2,
554
+ openApiDocument: () => openApiDocument,
553
555
  tableSchemas: () => tableSchemas
554
556
  });
555
557
  function baseSchema(c, mode, target, checks, sets, lengths) {
@@ -561,7 +563,7 @@ function baseSchema(c, mode, target, checks, sets, lengths) {
561
563
  case "custom":
562
564
  return {};
563
565
  case "buffer":
564
- return { type: "string", contentEncoding: "base64" };
566
+ return base64(target);
565
567
  case "tuple":
566
568
  return target === "openapi-3.0" ? { type: "array", items: { type: "number" }, minItems: s.length, maxItems: s.length } : {
567
569
  type: "array",
@@ -596,19 +598,22 @@ function baseSchema(c, mode, target, checks, sets, lengths) {
596
598
  if (c.enumValues && c.enumValues.length) return { enum: [...c.enumValues] };
597
599
  const mine = c.arrayDimensions ? [] : checks.filter((k) => k.column === c.name);
598
600
  const eq = mine.find((k) => k.operator === "=");
599
- if (eq) return { const: eq.kind === "string" ? eq.value : Number(eq.value) };
601
+ if (eq) {
602
+ const only = eq.kind === "string" ? eq.value : Number(eq.value);
603
+ return target === "openapi-3.0" ? { enum: [only] } : { const: only };
604
+ }
600
605
  switch (c.tsType) {
601
606
  case "string": {
602
607
  const out = { type: "string" };
603
608
  if (c.format === "uuid") out.format = UUID_FORMAT;
604
- else if (c.format && import_validation_core3.COLUMN_FORMATS[c.format]) out.pattern = import_validation_core3.COLUMN_FORMATS[c.format];
609
+ else if (c.format && import_validation_core4.COLUMN_FORMATS[c.format]) out.pattern = import_validation_core4.COLUMN_FORMATS[c.format];
605
610
  if (c.maxLength !== void 0) out.maxLength = c.maxLength;
606
611
  applyByteCap(out, c);
607
612
  applyLengths(out, c, lengths);
608
613
  return out;
609
614
  }
610
615
  case "number": {
611
- const out = { type: (0, import_validation_core3.isIntegerColumn)(c) ? "integer" : "number" };
616
+ const out = { type: (0, import_validation_core4.isIntegerColumn)(c) ? "integer" : "number" };
612
617
  if (!c.arrayDimensions) applyNumericBounds(out, c, checks, target);
613
618
  return out;
614
619
  }
@@ -619,7 +624,7 @@ function baseSchema(c, mode, target, checks, sets, lengths) {
619
624
  case "Date":
620
625
  return { type: "string", format: "date-time" };
621
626
  case "Uint8Array":
622
- return { type: "string", contentEncoding: "base64" };
627
+ return base64(target);
623
628
  default:
624
629
  return {};
625
630
  }
@@ -745,7 +750,7 @@ function tableSchema(table, cols, mode, target, applyDefaults, parsed) {
745
750
  };
746
751
  }
747
752
  function collect(table) {
748
- const parsed = (table.checks ?? []).map((k) => (0, import_validation_core3.parseCheck)(k.expression, k.name));
753
+ const parsed = (table.checks ?? []).map((k) => (0, import_validation_core4.parseCheck)(k.expression, k.name));
749
754
  return {
750
755
  checks: parsed.flatMap((p) => p.ok ? p.checks : []),
751
756
  sets: parsed.flatMap((p) => p.ok ? p.sets ?? [] : []),
@@ -757,11 +762,11 @@ function collect(table) {
757
762
  function tableSchemas(table, opts = {}) {
758
763
  const target = opts.target ?? "draft-2020-12";
759
764
  const parsed = collect(table);
760
- const build = (cols, mode) => tableSchema(table, cols, mode, target, !!opts.applyDefaults, parsed);
765
+ const build2 = (cols, mode) => tableSchema(table, cols, mode, target, !!opts.applyDefaults, parsed);
761
766
  return {
762
- insert: build((0, import_validation_core3.insertColumns)(table), "insert"),
763
- update: build((0, import_validation_core3.updateColumns)(table), "update"),
764
- select: build((0, import_validation_core3.selectColumns)(table), "select")
767
+ insert: build2((0, import_validation_core4.insertColumns)(table), "insert"),
768
+ update: build2((0, import_validation_core4.updateColumns)(table), "update"),
769
+ select: build2((0, import_validation_core4.selectColumns)(table), "select")
765
770
  };
766
771
  }
767
772
  function componentsDocument(tables, opts = {}) {
@@ -776,6 +781,217 @@ function componentsDocument(tables, opts = {}) {
776
781
  }
777
782
  return { schemas };
778
783
  }
784
+ function keyColumns2(table) {
785
+ const names = table.primaryKey?.columns ?? [];
786
+ if (!names.length) return null;
787
+ const cols = names.map((n) => table.columns.find((c) => c.name === n));
788
+ if (cols.some((c) => !c)) return null;
789
+ return cols;
790
+ }
791
+ function foreignKeysOf(table) {
792
+ if (table.foreignKeys?.length) return table.foreignKeys;
793
+ return table.columns.filter((c) => c.references).map((c) => ({
794
+ columns: [c.name],
795
+ foreignTable: c.references.table,
796
+ foreignColumns: [c.references.column]
797
+ }));
798
+ }
799
+ function build(tables, opts) {
800
+ const target = opts.target ?? "draft-2020-12";
801
+ const schemaTarget = target === "openapi-3.0" ? "openapi-3.0" : "openapi-3.1";
802
+ const failure = String(opts.validationStatus ?? 400);
803
+ const paths = {};
804
+ const schemas = {};
805
+ const tags = [];
806
+ const operationIds = /* @__PURE__ */ new Map();
807
+ const owner = /* @__PURE__ */ new Map();
808
+ const claim = (path6, by, label) => {
809
+ const taken = owner.get(path6);
810
+ if (taken !== void 0 && taken.by !== by) {
811
+ throw new Error(
812
+ `@drzl/generator-json-schema: the OpenAPI path "${path6}" 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.`
813
+ );
814
+ }
815
+ owner.set(path6, { by, label });
816
+ };
817
+ const operation = (id, table, rest) => {
818
+ const clash = operationIds.get(id);
819
+ if (clash !== void 0) {
820
+ throw new Error(
821
+ `@drzl/generator-json-schema: the operationId "${id}" would be emitted for both "${clash}" and "${table.name}". An operationId is the method name a client generator derives, and the specification requires it to be unique across the document.`
822
+ );
823
+ }
824
+ operationIds.set(id, table.name);
825
+ return { operationId: id, tags: [table.name], ...rest };
826
+ };
827
+ const built = tables.map((table) => ({
828
+ table,
829
+ key: keyColumns2(table),
830
+ segment: resourceSegment(table),
831
+ schemas: tableSchemas(table, { target: schemaTarget, applyDefaults: opts.applyDefaults })
832
+ }));
833
+ for (const { table, key, segment, schemas: built3 } of built) {
834
+ for (const mode of modesFor(table, key)) {
835
+ const { $schema: _dialect, $id: _id, ...rest } = built3[mode];
836
+ schemas[componentName(table, mode)] = rest;
837
+ }
838
+ const notes = [];
839
+ if (!key) notes.push("It has no primary key, so no path addresses a single row.");
840
+ if (table.readOnly) {
841
+ notes.push("It refuses every write, so only reads are described.");
842
+ }
843
+ tags.push({ name: table.name, description: [`Table "${table.name}".`, ...notes].join(" ") });
844
+ const T = pascal(table.tsName);
845
+ const select = ref(componentName(table, "select"));
846
+ const validationFailed = {
847
+ description: "The request does not match the schema for this operation.",
848
+ ...jsonBody(ref(ERROR_SCHEMA))
849
+ };
850
+ const collidable = [
851
+ ...table.primaryKey ? [`primary key (${table.primaryKey.columns.join(", ")})`] : [],
852
+ ...table.unique.map((u) => `${u.name ? `${u.name} ` : ""}(${u.columns.join(", ")})`)
853
+ ];
854
+ const conflict = (constraints) => ({
855
+ description: `The row collides with an existing one on ${constraints.join("; ")}.`,
856
+ ...jsonBody(ref(ERROR_SCHEMA))
857
+ });
858
+ const collection = `/${segment}`;
859
+ claim(collection, table.tsName, table.name);
860
+ const item = {
861
+ get: operation(`list${T}`, table, {
862
+ summary: `List every ${table.name} row.`,
863
+ // No pagination parameters. Whether the server implements a limit, an offset or a cursor is
864
+ // not something a Drizzle schema states, and a declared parameter nothing honours is worse
865
+ // than an undeclared one.
866
+ responses: {
867
+ "200": {
868
+ description: `Every ${table.name} row.`,
869
+ ...jsonBody({ type: "array", items: select })
870
+ }
871
+ }
872
+ })
873
+ };
874
+ if (!table.readOnly) {
875
+ item.post = operation(`create${T}`, table, {
876
+ summary: `Create a ${table.name} row.`,
877
+ requestBody: { required: true, ...jsonBody(ref(componentName(table, "insert"))) },
878
+ responses: {
879
+ "201": { description: `The ${table.name} row that was created.`, ...jsonBody(select) },
880
+ [failure]: validationFailed,
881
+ ...collidable.length ? { "409": conflict(collidable) } : {}
882
+ }
883
+ });
884
+ }
885
+ paths[collection] = item;
886
+ if (!key) continue;
887
+ const itemPath = `${collection}/${key.map((c) => `{${c.name}}`).join("/")}`;
888
+ claim(itemPath, table.tsName, table.name);
889
+ const parameters = key.map((c) => ({
890
+ name: c.name,
891
+ in: "path",
892
+ required: true,
893
+ description: `${c.name}, from the primary key of ${table.name}.`,
894
+ // The column's own schema rather than a string, so an integer key is declared as one and a
895
+ // uuid key carries its format. This is the whole point of reading the real key.
896
+ schema: built3.select.properties[c.name] ?? {}
897
+ }));
898
+ const missing = {
899
+ description: `No ${table.name} row has that ${key.map((c) => c.name).join(" and ")}.`,
900
+ ...jsonBody(ref(ERROR_SCHEMA))
901
+ };
902
+ const byId = {
903
+ parameters,
904
+ get: operation(`get${T}`, table, {
905
+ summary: `Read one ${table.name} row.`,
906
+ responses: {
907
+ "200": { description: `The requested ${table.name} row.`, ...jsonBody(select) },
908
+ [failure]: validationFailed,
909
+ "404": missing
910
+ }
911
+ })
912
+ };
913
+ if (!table.readOnly) {
914
+ byId.patch = operation(`update${T}`, table, {
915
+ summary: `Patch one ${table.name} row.`,
916
+ requestBody: { required: true, ...jsonBody(ref(componentName(table, "update"))) },
917
+ responses: {
918
+ "200": { description: `The ${table.name} row after the patch.`, ...jsonBody(select) },
919
+ [failure]: validationFailed,
920
+ "404": missing,
921
+ // The primary key is not in the update schema, so a patch cannot collide on it. Only a
922
+ // unique constraint over other columns can.
923
+ ...table.unique.length ? {
924
+ "409": conflict(
925
+ table.unique.map((u) => `${u.name ? `${u.name} ` : ""}(${u.columns.join(", ")})`)
926
+ )
927
+ } : {}
928
+ }
929
+ });
930
+ byId.delete = operation(`delete${T}`, table, {
931
+ summary: `Delete one ${table.name} row.`,
932
+ responses: {
933
+ // No body. Handing back the deleted row is the alternative and it is not a true statement
934
+ // on every dialect DRZL supports: RETURNING is Postgres and SQLite, and MySQL has no such
935
+ // clause, so an implementation there has nothing to send.
936
+ "204": { description: `The ${table.name} row was deleted. No content is returned.` },
937
+ [failure]: validationFailed,
938
+ "404": missing
939
+ }
940
+ });
941
+ }
942
+ paths[itemPath] = byId;
943
+ if (!opts.includeRelations) continue;
944
+ for (const child of built) {
945
+ if (child.table === table) continue;
946
+ const matching = foreignKeysOf(child.table).filter(
947
+ (fk) => fk.foreignTable === table.name && fk.foreignColumns.length === key.length && fk.foreignColumns.every((c, i) => c === key[i].name)
948
+ );
949
+ if (matching.length !== 1) continue;
950
+ const subPath = `${itemPath}/${child.segment}`;
951
+ claim(
952
+ subPath,
953
+ `${table.tsName} -> ${child.table.tsName}`,
954
+ `${table.name} -> ${child.table.name}`
955
+ );
956
+ paths[subPath] = {
957
+ parameters,
958
+ get: operation(`list${T}${pascal(child.table.tsName)}`, child.table, {
959
+ summary: `List the ${child.table.name} rows belonging to one ${table.name} row.`,
960
+ responses: {
961
+ "200": {
962
+ description: `The ${child.table.name} rows whose ${matching[0].columns.join(", ")} names this ${table.name} row.`,
963
+ ...jsonBody({ type: "array", items: ref(componentName(child.table, "select")) })
964
+ },
965
+ [failure]: validationFailed,
966
+ "404": missing
967
+ }
968
+ })
969
+ };
970
+ }
971
+ }
972
+ return { paths, schemas, tags };
973
+ }
974
+ function openApiDocument(tables, opts = {}) {
975
+ const target = opts.target ?? "draft-2020-12";
976
+ const { paths, schemas, tags } = build(tables, opts);
977
+ if (ERROR_SCHEMA in schemas) {
978
+ throw new Error(
979
+ `@drzl/generator-json-schema: a table produced the component schema name "${ERROR_SCHEMA}", which the document already uses for its error responses.`
980
+ );
981
+ }
982
+ return {
983
+ openapi: target === "openapi-3.0" ? "3.0.3" : "3.1.1",
984
+ info: {
985
+ title: opts.info?.title ?? "API",
986
+ version: opts.info?.version ?? "0.0.0",
987
+ 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."
988
+ },
989
+ ...opts.servers?.length ? { servers: opts.servers } : {},
990
+ paths,
991
+ components: { schemas: { ...schemas, [ERROR_SCHEMA]: errorSchema() } },
992
+ tags
993
+ };
994
+ }
779
995
  function renderTableModule(table, affix, target, applyDefaults) {
780
996
  const T = table.tsName;
781
997
  const schemas = tableSchemas(table, { target, applyDefaults });
@@ -784,6 +1000,12 @@ function renderTableModule(table, affix, target, applyDefaults) {
784
1000
  export type ${(0, import_validation_core3.typeName)(mode, T, affix)} = typeof ${(0, import_validation_core3.schemaName)(mode, T, affix)};`;
785
1001
  return [decl("insert"), decl("update"), decl("select")].join("\n\n") + "\n";
786
1002
  }
1003
+ function resolveDocument(opt) {
1004
+ if (!opt) return null;
1005
+ const o = opt === true ? {} : opt;
1006
+ if (o.enabled === false) return null;
1007
+ return { ...o, format: o.format ?? "ts" };
1008
+ }
787
1009
  function buildHeader2(h) {
788
1010
  if (h?.enabled === false) return "";
789
1011
  const text = h?.text ?? "// Generated by DRZL. Do not edit by hand.";
@@ -791,14 +1013,38 @@ function buildHeader2(h) {
791
1013
 
792
1014
  `;
793
1015
  }
794
- var import_validation_core3, DEFAULT_FILE_SUFFIX, DRAFT, UUID_FORMAT, JsonSchemaGenerator, index_default2;
1016
+ var import_validation_core3, import_validation_core4, DRAFT, UUID_FORMAT, base64, ERROR_SCHEMA, componentName, ref, pascal, modesFor, resourceSegment, jsonBody, errorSchema, DEFAULT_FILE_SUFFIX, JsonSchemaGenerator, index_default2;
795
1017
  var init_dist2 = __esm({
796
1018
  "../generator-json-schema/dist/index.js"() {
797
1019
  "use strict";
798
1020
  import_validation_core3 = require("@drzl/validation-core");
799
- DEFAULT_FILE_SUFFIX = ".schema.ts";
1021
+ import_validation_core4 = require("@drzl/validation-core");
800
1022
  DRAFT = "https://json-schema.org/draft/2020-12/schema";
801
1023
  UUID_FORMAT = "uuid";
1024
+ base64 = (target) => target === "openapi-3.0" ? { type: "string", format: "byte" } : { type: "string", contentEncoding: "base64" };
1025
+ ERROR_SCHEMA = "Error";
1026
+ componentName = (table, mode) => `${table.tsName}${mode[0].toUpperCase()}${mode.slice(1)}`;
1027
+ ref = (name) => ({ $ref: `#/components/schemas/${name}` });
1028
+ pascal = (s) => s.charAt(0).toUpperCase() + s.slice(1);
1029
+ modesFor = (table, key) => [
1030
+ ...table.readOnly ? [] : ["insert"],
1031
+ ...table.readOnly || !key ? [] : ["update"],
1032
+ "select"
1033
+ ];
1034
+ resourceSegment = (table) => encodeURIComponent(table.name);
1035
+ jsonBody = (schema) => ({ content: { "application/json": { schema } } });
1036
+ errorSchema = () => ({
1037
+ title: "error",
1038
+ description: "What an operation returns when it does not return the row.",
1039
+ type: "object",
1040
+ properties: {
1041
+ message: { type: "string" },
1042
+ code: { type: "string" }
1043
+ },
1044
+ required: ["message"],
1045
+ additionalProperties: true
1046
+ });
1047
+ DEFAULT_FILE_SUFFIX = ".schema.ts";
802
1048
  JsonSchemaGenerator = class {
803
1049
  constructor(analysis) {
804
1050
  this.analysis = analysis;
@@ -813,6 +1059,7 @@ var init_dist2 = __esm({
813
1059
  const affix = (0, import_validation_core3.resolveAffix)(opts);
814
1060
  const fileSuffix = opts.fileSuffix ?? DEFAULT_FILE_SUFFIX;
815
1061
  const target = opts.target ?? "draft-2020-12";
1062
+ const document = resolveDocument(opts.document);
816
1063
  for (const table of this.analysis.tables) {
817
1064
  const filePath = path6.join(out, (0, import_validation_core3.moduleFileName)(table.tsName, fileSuffix));
818
1065
  const code = renderTableModule(table, affix, target, !!opts.applyDefaults);
@@ -839,10 +1086,38 @@ var init_dist2 = __esm({
839
1086
  );
840
1087
  files.push(componentsPath);
841
1088
  }
1089
+ if (document) {
1090
+ const built = openApiDocument(this.analysis.tables, {
1091
+ target,
1092
+ applyDefaults: !!opts.applyDefaults,
1093
+ includeRelations: !!opts.includeRelations,
1094
+ info: document.info,
1095
+ servers: document.servers,
1096
+ validationStatus: document.validationStatus
1097
+ });
1098
+ const body = JSON.stringify(built, null, 2);
1099
+ if (document.format !== "json") {
1100
+ const tsPath = path6.join(out, "openapi.ts");
1101
+ const code = `export const openapi = ${body} as const;
1102
+ `;
1103
+ await fs3.writeFile(
1104
+ tsPath,
1105
+ await (0, import_validation_core3.formatCode)(buildHeader2(opts.outputHeader) + code, tsPath, opts.format),
1106
+ "utf8"
1107
+ );
1108
+ files.push(tsPath);
1109
+ }
1110
+ if (document.format !== "ts") {
1111
+ const jsonPath = path6.join(out, "openapi.json");
1112
+ await fs3.writeFile(jsonPath, body + "\n", "utf8");
1113
+ files.push(jsonPath);
1114
+ }
1115
+ }
1116
+ const ext = opts.importExtension === "none" ? "" : ".js";
842
1117
  const indexPath = path6.join(out, "index.ts");
843
- const index = this.analysis.tables.map((t) => `export * from '${(0, import_validation_core3.moduleSpecifier)(t.tsName, fileSuffix, opts.importExtension)}';`).concat(
844
- opts.components ? [`export * from './components${opts.importExtension === "none" ? "" : ".js"}';`] : []
845
- ).join("\n") + "\n";
1118
+ const index = this.analysis.tables.map(
1119
+ (t) => `export * from '${(0, import_validation_core3.moduleSpecifier)(t.tsName, fileSuffix, opts.importExtension)}';`
1120
+ ).concat(opts.components ? [`export * from './components${ext}';`] : []).concat(document && document.format !== "json" ? [`export * from './openapi${ext}';`] : []).join("\n") + "\n";
846
1121
  const indexFormatted = await (0, import_validation_core3.formatCode)(
847
1122
  buildHeader2(opts.outputHeader) + index,
848
1123
  indexPath,
@@ -875,6 +1150,46 @@ var import_commander = require("commander");
875
1150
  var path5 = __toESM(require("path"), 1);
876
1151
  var import_ora = __toESM(require("ora"), 1);
877
1152
 
1153
+ // src/validation-options.ts
1154
+ function validationOptions(g, cfg, outDir, caps = {}) {
1155
+ return {
1156
+ outDir,
1157
+ outputHeader: g.outputHeader,
1158
+ format: g.format,
1159
+ schemaSuffix: g.schemaSuffix,
1160
+ fileSuffix: g.fileSuffix,
1161
+ importExtension: g.importExtension,
1162
+ affix: g.affix,
1163
+ coerceDates: g.coerceDates,
1164
+ applyDefaults: g.applyDefaults,
1165
+ duplicateFinder: g.duplicateFinder,
1166
+ nestedSchemas: g.nestedSchemas,
1167
+ nestedDepth: g.nestedDepth,
1168
+ // Only where the generator can act on them, so an unsupported option is absent rather than
1169
+ // present and ignored.
1170
+ ...caps.schemaTypes ? {
1171
+ // Needed by both: the reference is resolved relative to the emitted file.
1172
+ schemaPath: cfg.schema,
1173
+ typedJson: g.typedJson,
1174
+ typedColumns: g.typedColumns
1175
+ } : {}
1176
+ };
1177
+ }
1178
+
1179
+ // src/json-schema-options.ts
1180
+ function jsonSchemaOptions(g, cfg, outDir) {
1181
+ return {
1182
+ // JSON Schema is data, so nothing it emits references a type from the schema module.
1183
+ ...validationOptions(g, cfg, outDir, { schemaTypes: false }),
1184
+ target: g.target,
1185
+ components: g.components,
1186
+ document: g.document,
1187
+ // Read only while emitting a document, where it adds `/users/{id}/posts`. The per-table
1188
+ // schemas are flat whatever it says.
1189
+ includeRelations: g.includeRelations
1190
+ };
1191
+ }
1192
+
878
1193
  // src/config.ts
879
1194
  var import_validation_core = require("@drzl/validation-core");
880
1195
  var fs = __toESM(require("fs"), 1);
@@ -988,6 +1303,35 @@ var GeneratorSchema = import_zod.z.object({
988
1303
  target: import_zod.z.enum(["draft-2020-12", "openapi-3.1", "openapi-3.0"]).optional(),
989
1304
  /** Also emit `components.ts` for the `json-schema` generator, ready for an OpenAPI document. */
990
1305
  components: import_zod.z.boolean().optional(),
1306
+ /**
1307
+ * Also emit the whole OpenAPI document for the `json-schema` generator: paths, verbs, request and
1308
+ * response bodies per table, with `components.schemas` embedded so the file stands alone.
1309
+ *
1310
+ * `true` is the short form. The object form carries the three things a Drizzle schema genuinely
1311
+ * cannot say: what the API is called, where it is served, and which status code that particular
1312
+ * server answers a request that fails its schema with.
1313
+ */
1314
+ document: import_zod.z.union([
1315
+ import_zod.z.boolean(),
1316
+ import_zod.z.object({
1317
+ enabled: import_zod.z.boolean().optional(),
1318
+ /** `ts` (default) writes a module, `json` the file OpenAPI tooling reads directly. */
1319
+ format: import_zod.z.enum(["ts", "json", "both"]).optional(),
1320
+ info: import_zod.z.object({
1321
+ title: import_zod.z.string().optional(),
1322
+ version: import_zod.z.string().optional(),
1323
+ description: import_zod.z.string().optional()
1324
+ }).strict().optional(),
1325
+ /**
1326
+ * Omitted by default, which the specification reads as a single server at `/`: the
1327
+ * document describes whatever is serving it. A placeholder host would be a fabrication
1328
+ * that tooling then follows.
1329
+ */
1330
+ servers: import_zod.z.array(import_zod.z.object({ url: import_zod.z.string(), description: import_zod.z.string().optional() }).strict()).optional(),
1331
+ /** 400 by default. 422 is the other defensible reading; exactly one is emitted. */
1332
+ validationStatus: import_zod.z.union([import_zod.z.literal(400), import_zod.z.literal(422)]).optional()
1333
+ }).strict()
1334
+ ]).optional(),
991
1335
  // service generator specific options
992
1336
  path: import_zod.z.string().optional(),
993
1337
  dataAccess: import_zod.z.enum(["stub", "drizzle"]).default("stub").optional(),
@@ -1291,32 +1635,6 @@ function trpcOptions(g, cfg, servicesDir) {
1291
1635
  };
1292
1636
  }
1293
1637
 
1294
- // src/validation-options.ts
1295
- function validationOptions(g, cfg, outDir, caps = {}) {
1296
- return {
1297
- outDir,
1298
- outputHeader: g.outputHeader,
1299
- format: g.format,
1300
- schemaSuffix: g.schemaSuffix,
1301
- fileSuffix: g.fileSuffix,
1302
- importExtension: g.importExtension,
1303
- affix: g.affix,
1304
- coerceDates: g.coerceDates,
1305
- applyDefaults: g.applyDefaults,
1306
- duplicateFinder: g.duplicateFinder,
1307
- nestedSchemas: g.nestedSchemas,
1308
- nestedDepth: g.nestedDepth,
1309
- // Only where the generator can act on them, so an unsupported option is absent rather than
1310
- // present and ignored.
1311
- ...caps.schemaTypes ? {
1312
- // Needed by both: the reference is resolved relative to the emitted file.
1313
- schemaPath: cfg.schema,
1314
- typedJson: g.typedJson,
1315
- typedColumns: g.typedColumns
1316
- } : {}
1317
- };
1318
- }
1319
-
1320
1638
  // src/drift.ts
1321
1639
  var import_node_fs = require("fs");
1322
1640
  var import_node_path = __toESM(require("path"), 1);
@@ -1719,12 +2037,7 @@ program.command("generate").description("Run configured generators (drzl.config.
1719
2037
  );
1720
2038
  const gen = new JsonSchemaGenerator2(analysis);
1721
2039
  const target = g.path ?? "src/validators/json-schema";
1722
- const files = await gen.generate({
1723
- // JSON Schema is data, so nothing here references a type from the schema module.
1724
- ...validationOptions(g, cfg, target, { schemaTypes: false }),
1725
- target: g.target,
1726
- components: g.components
1727
- });
2040
+ const files = await gen.generate(jsonSchemaOptions(g, cfg, target));
1728
2041
  progress.stop();
1729
2042
  (0, import_ora.default)().succeed(import_chalk2.default.green(`Generated (json-schema): ${files.length} files`));
1730
2043
  files.forEach((f) => console.log(" -", import_chalk2.default.cyan(f)));
@@ -2097,12 +2410,7 @@ program.command("watch").description("Watch schema and regenerate on changes").o
2097
2410
  );
2098
2411
  const gen = new JsonSchemaGenerator2(analysis);
2099
2412
  const target = g.path ?? "src/validators/json-schema";
2100
- const files = await gen.generate({
2101
- // JSON Schema is data, so nothing here references a type from the schema module.
2102
- ...validationOptions(g, cfg, target, { schemaTypes: false }),
2103
- target: g.target,
2104
- components: g.components
2105
- });
2413
+ const files = await gen.generate(jsonSchemaOptions(g, cfg, target));
2106
2414
  opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
2107
2415
  import_chalk2.default.green(`Generated (json-schema): ${files.length} files`),
2108
2416
  files.map((f) => import_chalk2.default.cyan(f)).join(", ")