@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/{chunk-K4J4XIFO.js → chunk-I4KMDCQR.js} +30 -1
- package/dist/chunk-I4KMDCQR.js.map +1 -0
- package/dist/cli.cjs +361 -53
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +38 -34
- package/dist/cli.js.map +1 -1
- package/dist/config.cjs +29 -0
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +38 -2
- package/dist/config.d.ts +38 -2
- package/dist/config.js +1 -1
- package/dist/dist-TRJLPIWT.js +603 -0
- package/dist/dist-TRJLPIWT.js.map +1 -0
- package/package.json +7 -7
- package/dist/chunk-K4J4XIFO.js.map +0 -1
- package/dist/dist-UE55LTXV.js +0 -327
- package/dist/dist-UE55LTXV.js.map +0 -1
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
|
|
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)
|
|
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 &&
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
765
|
+
const build2 = (cols, mode) => tableSchema(table, cols, mode, target, !!opts.applyDefaults, parsed);
|
|
761
766
|
return {
|
|
762
|
-
insert:
|
|
763
|
-
update:
|
|
764
|
-
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,
|
|
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
|
-
|
|
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(
|
|
844
|
-
|
|
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(", ")
|