@drzl/cli 4.15.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-FZNDWYBB.js → chunk-I4KMDCQR.js} +46 -1
- package/dist/chunk-I4KMDCQR.js.map +1 -0
- package/dist/cli.cjs +424 -76
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +86 -58
- package/dist/cli.js.map +1 -1
- package/dist/config.cjs +45 -0
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +42 -2
- package/dist/config.d.ts +42 -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 +8 -8
- package/dist/chunk-FZNDWYBB.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);
|
|
@@ -950,6 +1265,22 @@ var GeneratorSchema = import_zod.z.object({
|
|
|
950
1265
|
* fact about the table rather than the row. This checks the half that needs no database.
|
|
951
1266
|
*/
|
|
952
1267
|
duplicateFinder: import_zod.z.boolean().optional(),
|
|
1268
|
+
/**
|
|
1269
|
+
* Emit `NestedInsert<Table>` and `NestedSelect<Table>` beside the flat schemas: the table plus
|
|
1270
|
+
* one key per relation, so `{ ...user, posts: [...] }` can be validated whole.
|
|
1271
|
+
*
|
|
1272
|
+
* Nothing in the Drizzle validator ecosystem describes that payload, and `db.insert` drops the
|
|
1273
|
+
* relation key silently rather than refusing it, so the children are never written and nothing
|
|
1274
|
+
* says so.
|
|
1275
|
+
*/
|
|
1276
|
+
nestedSchemas: import_zod.z.boolean().optional(),
|
|
1277
|
+
/**
|
|
1278
|
+
* How many levels of children a nested schema describes. Defaults to 1, capped at 3.
|
|
1279
|
+
*
|
|
1280
|
+
* Nesting is expanded inline rather than by reference, so this multiplies the emitted size, and
|
|
1281
|
+
* it is also what terminates a cycle: `users -> posts -> users` stops here.
|
|
1282
|
+
*/
|
|
1283
|
+
nestedDepth: import_zod.z.number().int().optional(),
|
|
953
1284
|
naming: NamingSchema.optional(),
|
|
954
1285
|
outputHeader: import_zod.z.object({
|
|
955
1286
|
enabled: import_zod.z.boolean().default(true).optional(),
|
|
@@ -972,6 +1303,35 @@ var GeneratorSchema = import_zod.z.object({
|
|
|
972
1303
|
target: import_zod.z.enum(["draft-2020-12", "openapi-3.1", "openapi-3.0"]).optional(),
|
|
973
1304
|
/** Also emit `components.ts` for the `json-schema` generator, ready for an OpenAPI document. */
|
|
974
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(),
|
|
975
1335
|
// service generator specific options
|
|
976
1336
|
path: import_zod.z.string().optional(),
|
|
977
1337
|
dataAccess: import_zod.z.enum(["stub", "drizzle"]).default("stub").optional(),
|
|
@@ -1275,30 +1635,6 @@ function trpcOptions(g, cfg, servicesDir) {
|
|
|
1275
1635
|
};
|
|
1276
1636
|
}
|
|
1277
1637
|
|
|
1278
|
-
// src/validation-options.ts
|
|
1279
|
-
function validationOptions(g, cfg, outDir, caps = {}) {
|
|
1280
|
-
return {
|
|
1281
|
-
outDir,
|
|
1282
|
-
outputHeader: g.outputHeader,
|
|
1283
|
-
format: g.format,
|
|
1284
|
-
schemaSuffix: g.schemaSuffix,
|
|
1285
|
-
fileSuffix: g.fileSuffix,
|
|
1286
|
-
importExtension: g.importExtension,
|
|
1287
|
-
affix: g.affix,
|
|
1288
|
-
coerceDates: g.coerceDates,
|
|
1289
|
-
applyDefaults: g.applyDefaults,
|
|
1290
|
-
duplicateFinder: g.duplicateFinder,
|
|
1291
|
-
// Only where the generator can act on them, so an unsupported option is absent rather than
|
|
1292
|
-
// present and ignored.
|
|
1293
|
-
...caps.schemaTypes ? {
|
|
1294
|
-
// Needed by both: the reference is resolved relative to the emitted file.
|
|
1295
|
-
schemaPath: cfg.schema,
|
|
1296
|
-
typedJson: g.typedJson,
|
|
1297
|
-
typedColumns: g.typedColumns
|
|
1298
|
-
} : {}
|
|
1299
|
-
};
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
1638
|
// src/drift.ts
|
|
1303
1639
|
var import_node_fs = require("fs");
|
|
1304
1640
|
var import_node_path = __toESM(require("path"), 1);
|
|
@@ -1701,12 +2037,7 @@ program.command("generate").description("Run configured generators (drzl.config.
|
|
|
1701
2037
|
);
|
|
1702
2038
|
const gen = new JsonSchemaGenerator2(analysis);
|
|
1703
2039
|
const target = g.path ?? "src/validators/json-schema";
|
|
1704
|
-
const files = await gen.generate(
|
|
1705
|
-
// JSON Schema is data, so nothing here references a type from the schema module.
|
|
1706
|
-
...validationOptions(g, cfg, target, { schemaTypes: false }),
|
|
1707
|
-
target: g.target,
|
|
1708
|
-
components: g.components
|
|
1709
|
-
});
|
|
2040
|
+
const files = await gen.generate(jsonSchemaOptions(g, cfg, target));
|
|
1710
2041
|
progress.stop();
|
|
1711
2042
|
(0, import_ora.default)().succeed(import_chalk2.default.green(`Generated (json-schema): ${files.length} files`));
|
|
1712
2043
|
files.forEach((f) => console.log(" -", import_chalk2.default.cyan(f)));
|
|
@@ -1811,10 +2142,7 @@ program.command("generate:trpc").argument("<schema>", "path to drizzle schema (T
|
|
|
1811
2142
|
// cannot become the branch that forgets it.
|
|
1812
2143
|
servicesDir: opts.servicesDir
|
|
1813
2144
|
});
|
|
1814
|
-
console.log(
|
|
1815
|
-
import_chalk2.default.green(`Generated:`),
|
|
1816
|
-
files.map((f) => import_chalk2.default.cyan(f)).join(", ")
|
|
1817
|
-
);
|
|
2145
|
+
console.log(import_chalk2.default.green(`Generated:`), files.map((f) => import_chalk2.default.cyan(f)).join(", "));
|
|
1818
2146
|
maybeShowSponsorMessage({ reason: "generate:trpc" });
|
|
1819
2147
|
} catch (e) {
|
|
1820
2148
|
reportGeneratorFailure("trpc", e);
|
|
@@ -2002,15 +2330,9 @@ program.command("watch").description("Watch schema and regenerate on changes").o
|
|
|
2002
2330
|
);
|
|
2003
2331
|
const gen = new ZodGenerator(analysis);
|
|
2004
2332
|
const target = g.path ?? "src/validators/zod";
|
|
2005
|
-
const files = await gen.generate(
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
format: g.format,
|
|
2009
|
-
schemaSuffix: g.schemaSuffix,
|
|
2010
|
-
fileSuffix: g.fileSuffix,
|
|
2011
|
-
importExtension: g.importExtension,
|
|
2012
|
-
affix: g.affix
|
|
2013
|
-
});
|
|
2333
|
+
const files = await gen.generate(
|
|
2334
|
+
validationOptions(g, cfg, target, { schemaTypes: true })
|
|
2335
|
+
);
|
|
2014
2336
|
opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
|
|
2015
2337
|
import_chalk2.default.green(`Generated (zod): ${files.length} files`),
|
|
2016
2338
|
files.map((f) => import_chalk2.default.cyan(f)).join(", ")
|
|
@@ -2028,15 +2350,9 @@ program.command("watch").description("Watch schema and regenerate on changes").o
|
|
|
2028
2350
|
);
|
|
2029
2351
|
const gen = new ValibotGenerator(analysis);
|
|
2030
2352
|
const target = g.path ?? "src/validators/valibot";
|
|
2031
|
-
const files = await gen.generate(
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
format: g.format,
|
|
2035
|
-
schemaSuffix: g.schemaSuffix,
|
|
2036
|
-
fileSuffix: g.fileSuffix,
|
|
2037
|
-
importExtension: g.importExtension,
|
|
2038
|
-
affix: g.affix
|
|
2039
|
-
});
|
|
2353
|
+
const files = await gen.generate(
|
|
2354
|
+
validationOptions(g, cfg, target, { schemaTypes: true })
|
|
2355
|
+
);
|
|
2040
2356
|
opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
|
|
2041
2357
|
import_chalk2.default.green(`Generated (valibot): ${files.length} files`),
|
|
2042
2358
|
files.map((f) => import_chalk2.default.cyan(f)).join(", ")
|
|
@@ -2054,15 +2370,9 @@ program.command("watch").description("Watch schema and regenerate on changes").o
|
|
|
2054
2370
|
);
|
|
2055
2371
|
const gen = new ArkTypeGenerator(analysis);
|
|
2056
2372
|
const target = g.path ?? "src/validators/arktype";
|
|
2057
|
-
const files = await gen.generate(
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
format: g.format,
|
|
2061
|
-
schemaSuffix: g.schemaSuffix,
|
|
2062
|
-
fileSuffix: g.fileSuffix,
|
|
2063
|
-
importExtension: g.importExtension,
|
|
2064
|
-
affix: g.affix
|
|
2065
|
-
});
|
|
2373
|
+
const files = await gen.generate(
|
|
2374
|
+
validationOptions(g, cfg, target, { schemaTypes: false })
|
|
2375
|
+
);
|
|
2066
2376
|
opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
|
|
2067
2377
|
import_chalk2.default.green(`Generated (arktype): ${files.length} files`),
|
|
2068
2378
|
files.map((f) => import_chalk2.default.cyan(f)).join(", ")
|
|
@@ -2072,6 +2382,44 @@ program.command("watch").description("Watch schema and regenerate on changes").o
|
|
|
2072
2382
|
reportGeneratorFailure(g.kind, e);
|
|
2073
2383
|
return;
|
|
2074
2384
|
}
|
|
2385
|
+
} else if (g.kind === "typebox") {
|
|
2386
|
+
try {
|
|
2387
|
+
const { TypeBoxGenerator } = await loadGenerator(
|
|
2388
|
+
"@drzl/generator-typebox",
|
|
2389
|
+
() => import("@drzl/generator-typebox")
|
|
2390
|
+
);
|
|
2391
|
+
const gen = new TypeBoxGenerator(analysis);
|
|
2392
|
+
const target = g.path ?? "src/validators/typebox";
|
|
2393
|
+
const files = await gen.generate(
|
|
2394
|
+
validationOptions(g, cfg, target, { schemaTypes: true })
|
|
2395
|
+
);
|
|
2396
|
+
opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
|
|
2397
|
+
import_chalk2.default.green(`Generated (typebox): ${files.length} files`),
|
|
2398
|
+
files.map((f) => import_chalk2.default.cyan(f)).join(", ")
|
|
2399
|
+
);
|
|
2400
|
+
newFiles.push(...files);
|
|
2401
|
+
} catch (e) {
|
|
2402
|
+
reportGeneratorFailure(g.kind, e);
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
} else if (g.kind === "json-schema") {
|
|
2406
|
+
try {
|
|
2407
|
+
const { JsonSchemaGenerator: JsonSchemaGenerator2 } = await loadGenerator(
|
|
2408
|
+
"@drzl/generator-json-schema",
|
|
2409
|
+
() => Promise.resolve().then(() => (init_dist2(), dist_exports2))
|
|
2410
|
+
);
|
|
2411
|
+
const gen = new JsonSchemaGenerator2(analysis);
|
|
2412
|
+
const target = g.path ?? "src/validators/json-schema";
|
|
2413
|
+
const files = await gen.generate(jsonSchemaOptions(g, cfg, target));
|
|
2414
|
+
opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
|
|
2415
|
+
import_chalk2.default.green(`Generated (json-schema): ${files.length} files`),
|
|
2416
|
+
files.map((f) => import_chalk2.default.cyan(f)).join(", ")
|
|
2417
|
+
);
|
|
2418
|
+
newFiles.push(...files);
|
|
2419
|
+
} catch (e) {
|
|
2420
|
+
reportGeneratorFailure(g.kind, e);
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2075
2423
|
}
|
|
2076
2424
|
}
|
|
2077
2425
|
const added = newFiles.filter((f) => !lastFiles.includes(f));
|