@orpc/json-schema 2.0.0-beta.47 → 2.0.0-beta.48

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.
@@ -0,0 +1,357 @@
1
+ import { get, isDeepEqual, omit, toArray } from "@orpc/shared";
2
+ import { JSON_SCHEMA_LOGIC_KEYWORDS } from "./constants.mjs";
3
+ import { decodeJsonPointerSegment, encodeJsonPointerSegment, hoistRecursiveRefToDef, mapJsonSchemaRefs, resolveJsonSchemaRootLocalRef } from "./ref-utils.mjs";
4
+ import { ensureJsonSchemaObject, isJsonArraySchema, isUnconstrainedSchema } from "./utils.mjs";
5
+
6
+ function promoteJsonSchemaDefs(schema, mergedDefs, basePath) {
7
+ const { $defs, ...rootBody } = schema;
8
+ const renameMap = new Map();
9
+
10
+ const mapRef = (ref) => {
11
+ if (ref === "#") {
12
+ return basePath;
13
+ }
14
+ if (ref.startsWith("#/$defs/")) {
15
+ const [encodedName, ...segments] = ref.slice("#/$defs/".length).split("/");
16
+ const newName = renameMap.get(decodeJsonPointerSegment(encodedName));
17
+ return newName === undefined ? ref : [
18
+ "#/$defs",
19
+ encodeJsonPointerSegment(newName),
20
+ ...segments
21
+ ].join("/");
22
+ }
23
+ if (ref.startsWith("#/") && get(rootBody, ref.slice(2).split("/").map(decodeJsonPointerSegment)) !== undefined) {
24
+ return `${basePath}/${ref.slice(2)}`;
25
+ }
26
+ return ref;
27
+ };
28
+ const defs = Object.entries($defs ?? {}).filter(([, def]) => def !== undefined);
29
+ if (defs.length > 0) {
30
+ const promoted = new Map();
31
+ const reserved = new Set([...mergedDefs.keys(), ...defs.map(([name]) => name)]);
32
+
33
+ for (let changed = true; changed;) {
34
+ changed = false;
35
+ for (const [name, def] of defs) {
36
+ const body = mapJsonSchemaRefs(def, mapRef);
37
+ promoted.set(name, body);
38
+ if (renameMap.has(name) || !mergedDefs.has(name) || isDeepEqual(mergedDefs.get(name), body)) {
39
+ continue;
40
+ }
41
+ let counter = 2;
42
+ while (reserved.has(`${name}${counter}`)) {
43
+ counter++;
44
+ }
45
+ reserved.add(`${name}${counter}`);
46
+ renameMap.set(name, `${name}${counter}`);
47
+ changed = true;
48
+ }
49
+ }
50
+ for (const [name, body] of promoted) {
51
+ mergedDefs.set(renameMap.get(name) ?? name, body);
52
+ }
53
+ }
54
+ return mapJsonSchemaRefs(rootBody, mapRef);
55
+ }
56
+
57
+ export function combineJsonSchemasWithComposition(keyword, schemas) {
58
+ if (schemas.length <= 1) {
59
+ return schemas[0] ?? true;
60
+ }
61
+ const mergedDefs = new Map();
62
+ const compositionBranches = [];
63
+ for (let i = 0; i < schemas.length; i++) {
64
+ const schema = schemas[i];
65
+ compositionBranches.push(typeof schema === "boolean" ? schema : promoteJsonSchemaDefs(schema, mergedDefs, `#/${keyword}/${i}`));
66
+ }
67
+ const result = { [keyword]: compositionBranches };
68
+ if (mergedDefs.size > 0) {
69
+ result.$defs = Object.fromEntries(mergedDefs);
70
+ }
71
+ return result;
72
+ }
73
+
74
+ export function combineJsonObjectSchemaEntries(entries, additionalProperties) {
75
+ const properties = new Map();
76
+ const required = [];
77
+ const mergedDefs = new Map();
78
+ for (const [name, propertySchema, optional] of entries) {
79
+ if (!optional) {
80
+ required.push(name);
81
+ }
82
+ properties.set(name, typeof propertySchema === "boolean" ? propertySchema : promoteJsonSchemaDefs(propertySchema, mergedDefs, `#/properties/${encodeJsonPointerSegment(name)}`));
83
+ }
84
+ const schema = {
85
+ type: "object",
86
+ properties: Object.fromEntries(properties)
87
+ };
88
+ if (required.length > 0) {
89
+ schema.required = required;
90
+ }
91
+ if (additionalProperties !== undefined) {
92
+ schema.additionalProperties = typeof additionalProperties === "boolean" ? additionalProperties : promoteJsonSchemaDefs(additionalProperties, mergedDefs, "#/additionalProperties");
93
+ }
94
+ if (mergedDefs.size > 0) {
95
+ schema.$defs = Object.fromEntries(mergedDefs);
96
+ }
97
+ return schema;
98
+ }
99
+
100
+ export function extractJsonObjectSchemaEntries(schema) {
101
+ schema = hoistRecursiveRefToDef(schema);
102
+ if (typeof schema !== "object") {
103
+ return undefined;
104
+ }
105
+ const result = extractJsonObjectSchemaEntriesInternal(omit(schema, ["$defs"]), schema.$defs, new Set());
106
+ if (!result.objectLike) {
107
+ return undefined;
108
+ }
109
+ return result.entries.map(([n, s, ...r]) => [
110
+ n,
111
+ withRootDefs(s, schema.$defs),
112
+ ...r
113
+ ]);
114
+ }
115
+
116
+ export function extractJsonObjectSchemaAdditionalProperties(schema) {
117
+ schema = hoistRecursiveRefToDef(schema);
118
+ if (typeof schema !== "object") {
119
+ return undefined;
120
+ }
121
+ const resolved = resolveJsonSchemaRootLocalRef(schema);
122
+ if (typeof resolved !== "object" || resolved.additionalProperties === undefined) {
123
+ return undefined;
124
+ }
125
+ return withRootDefs(resolved.additionalProperties, schema.$defs);
126
+ }
127
+ const JSON_SCHEMA_OBJECT_KEYWORDS = [
128
+ "properties",
129
+ "patternProperties",
130
+ "additionalProperties",
131
+ "required",
132
+ "propertyNames",
133
+ "minProperties",
134
+ "maxProperties"
135
+ ];
136
+
137
+ export function isJsonObjectLikeSchema(schema) {
138
+ return isJsonObjectLikeSchemaInternal(schema, typeof schema === "object" ? schema.$defs : undefined, new Set());
139
+ }
140
+ function isJsonObjectLikeSchemaInternal(schema, $defs, followedRefs) {
141
+ if (typeof schema === "object" && typeof schema.$ref === "string") {
142
+
143
+ if (followedRefs.has(schema.$ref)) {
144
+ return false;
145
+ }
146
+ followedRefs = new Set(followedRefs).add(schema.$ref);
147
+ }
148
+ const resolved = resolveJsonSchemaRootLocalRef(schema, $defs);
149
+ if (typeof resolved === "boolean") {
150
+ return true;
151
+ }
152
+ if (resolved.type !== undefined) {
153
+ return resolved.type === "object";
154
+ }
155
+ if (JSON_SCHEMA_OBJECT_KEYWORDS.some((keyword) => keyword in resolved)) {
156
+ return true;
157
+ }
158
+ for (const keyword of [
159
+ "allOf",
160
+ "anyOf",
161
+ "oneOf"
162
+ ]) {
163
+ const members = resolved[keyword];
164
+ if (Array.isArray(members) && members.length > 0) {
165
+ return members.every((member) => isJsonObjectLikeSchemaInternal(member, $defs, followedRefs));
166
+ }
167
+ }
168
+ return isUnconstrainedSchema(resolved);
169
+ }
170
+ function extractJsonObjectSchemaEntriesInternal(schema, $defs, resolvingRefs) {
171
+ if (typeof schema !== "object") {
172
+ return {
173
+ entries: [],
174
+ objectLike: false
175
+ };
176
+ }
177
+ if (typeof schema.$ref === "string") {
178
+ if (resolvingRefs.has(schema.$ref)) {
179
+ return {
180
+ entries: [],
181
+ objectLike: true
182
+ };
183
+ }
184
+ const resolved = resolveJsonSchemaRootLocalRef(schema, $defs);
185
+ if (resolved !== schema) {
186
+ return extractJsonObjectSchemaEntriesInternal(resolved, $defs, new Set(resolvingRefs).add(schema.$ref));
187
+ }
188
+ }
189
+ const sources = [];
190
+ if (schema.properties) {
191
+ sources.push({
192
+ entries: Object.entries(schema.properties).map(([name, propertySchema]) => {
193
+ return [
194
+ name,
195
+ propertySchema,
196
+ !schema.required?.includes(name)
197
+ ];
198
+ }),
199
+ source: "direct"
200
+ });
201
+ }
202
+ let objectLike = schema.type === "object" || schema.properties !== undefined || schema.required !== undefined || schema.additionalProperties !== undefined;
203
+ for (const keyword of [
204
+ "anyOf",
205
+ "oneOf",
206
+ "allOf"
207
+ ]) {
208
+ const branches = schema[keyword];
209
+ if (branches === undefined) {
210
+ continue;
211
+ }
212
+ const branchResults = branches.map((branch) => extractJsonObjectSchemaEntriesInternal(branch, $defs, resolvingRefs));
213
+ if (branchResults.some((result) => !result.objectLike)) {
214
+ return {
215
+ entries: [],
216
+ objectLike: false
217
+ };
218
+ }
219
+ const entriesByName = new Map();
220
+ for (const result of branchResults) {
221
+ for (const entry of result.entries) {
222
+ const entries = entriesByName.get(entry[0]);
223
+ if (entries) {
224
+ entries.push(entry);
225
+ } else {
226
+ entriesByName.set(entry[0], [entry]);
227
+ }
228
+ }
229
+ }
230
+ objectLike = true;
231
+ sources.push({
232
+ entries: Array.from(entriesByName.entries()).map(([name, entries]) => {
233
+ const schemas = deduplicateJsonSchemas(entries.map((entry) => entry[1]));
234
+ const required = keyword === "allOf" ? entries.some((entry) => !entry[2]) : branchResults.every((result) => {
235
+ const entry = result.entries.find((item) => item[0] === name);
236
+ return entry !== undefined && !entry[2];
237
+ });
238
+ return [
239
+ name,
240
+ schemas.length === 1 ? schemas[0] : keyword === "allOf" ? { allOf: schemas } : { anyOf: schemas },
241
+ !required
242
+ ];
243
+ }),
244
+ source: keyword
245
+ });
246
+ }
247
+ const sourceEntries = new Map();
248
+ for (const { entries, source } of sources) {
249
+ for (const entry of entries) {
250
+ const existing = sourceEntries.get(entry[0]);
251
+ if (existing) {
252
+ existing[source] = entry;
253
+ } else {
254
+ sourceEntries.set(entry[0], { [source]: entry });
255
+ }
256
+ }
257
+ }
258
+ return {
259
+ entries: Array.from(sourceEntries.entries()).map(([name, entries]) => {
260
+ const schemas = deduplicateJsonSchemas([
261
+ entries.direct?.[1],
262
+ entries.allOf?.[1],
263
+ entries.anyOf?.[1],
264
+ entries.oneOf?.[1]
265
+ ].filter((schema) => schema !== undefined));
266
+ return [
267
+ name,
268
+ schemas.length === 1 ? schemas[0] : { allOf: schemas },
269
+ !schema.required?.includes(name) && [
270
+ entries.allOf,
271
+ entries.anyOf,
272
+ entries.oneOf
273
+ ].every((entry) => entry === undefined || entry[2])
274
+ ];
275
+ }),
276
+ objectLike
277
+ };
278
+ }
279
+
280
+ export function flattenJsonUnionSchema(schema) {
281
+ return deduplicateJsonSchemas(flattenJsonUnionSchemaInternal(schema, new Set()));
282
+ }
283
+ function flattenJsonUnionSchemaInternal(schema, resolvingRefs) {
284
+ if (typeof schema !== "object") {
285
+ return [schema];
286
+ }
287
+ if (typeof schema.$ref === "string") {
288
+ if (resolvingRefs.has(schema.$ref)) {
289
+ return [];
290
+ }
291
+ const resolved = resolveJsonSchemaRootLocalRef(schema);
292
+ if (resolved !== schema) {
293
+ const result = flattenJsonUnionSchemaInternal(resolved, new Set(resolvingRefs).add(schema.$ref));
294
+ if (result.length > 1) {
295
+ return result;
296
+ }
297
+ }
298
+ }
299
+ const { anyOf: _anyOf, oneOf: _oneOf, ...rest } = schema;
300
+ const entries = Object.entries(rest).filter(([, val]) => val !== undefined);
301
+ for (const keyword of ["anyOf", "oneOf"]) {
302
+ if (schema[keyword]) {
303
+ return schema[keyword].flatMap((s) => {
304
+ s = ensureJsonSchemaObject(s);
305
+ const mergedSchema = {
306
+ ...s,
307
+ ...Object.fromEntries(entries.filter(([key]) => s[key] === undefined)),
308
+ $defs: schema.$defs
309
+ };
310
+ const conflicts = entries.filter(([key]) => s[key] !== undefined);
311
+ if (conflicts.length) {
312
+ mergedSchema.allOf = [...toArray(mergedSchema.allOf), Object.fromEntries(conflicts)];
313
+ }
314
+ return flattenJsonUnionSchemaInternal(mergedSchema, resolvingRefs);
315
+ });
316
+ }
317
+ }
318
+ return [schema];
319
+ }
320
+
321
+ export function matchArrayableJsonSchema(schema) {
322
+ const schemas = flattenJsonUnionSchema(schema);
323
+ if (schemas.length !== 2) {
324
+ return undefined;
325
+ }
326
+ const arraySchema = schemas.find(isJsonArraySchema);
327
+ if (arraySchema === undefined) {
328
+ return undefined;
329
+ }
330
+ const items1 = arraySchema.items ?? true;
331
+ const items2 = schemas.find((s) => s !== arraySchema);
332
+ const logicItem1 = Object.fromEntries(Object.entries(ensureJsonSchemaObject(items1)).filter(([key]) => JSON_SCHEMA_LOGIC_KEYWORDS.has(key)));
333
+ const logicItem2 = Object.fromEntries(Object.entries(ensureJsonSchemaObject(items2)).filter(([key]) => JSON_SCHEMA_LOGIC_KEYWORDS.has(key)));
334
+ if (!isDeepEqual(logicItem1, logicItem2)) {
335
+ return undefined;
336
+ }
337
+ return [items2, arraySchema];
338
+ }
339
+ export function deduplicateJsonSchemas(schemas) {
340
+ const result = [];
341
+ for (const schema of schemas) {
342
+ if (result.some((i) => isDeepEqual(i, schema))) {
343
+ continue;
344
+ }
345
+ result.push(schema);
346
+ }
347
+ return result;
348
+ }
349
+ function withRootDefs(schema, $defs) {
350
+ if (typeof schema === "boolean" || !$defs) {
351
+ return schema;
352
+ }
353
+ return {
354
+ ...schema,
355
+ $defs
356
+ };
357
+ }
@@ -0,0 +1,4 @@
1
+ import type { JsonSchemaKeywords } from "./types.mjs";
2
+ export declare const JSON_SCHEMA_LOGIC_KEYWORDS: Set<JsonSchemaKeywords | string & {}>;
3
+ export declare const JSON_SCHEMA_PRIMITIVE_TYPES: Set<string>;
4
+ export declare const JSON_SCHEMA_RECORD_KEYWORDS: Set<string>;
@@ -0,0 +1,61 @@
1
+ export const JSON_SCHEMA_LOGIC_KEYWORDS = /* @__PURE__ */ new Set([
2
+ "$dynamicRef",
3
+ "$ref",
4
+ "additionalItems",
5
+ "additionalProperties",
6
+ "allOf",
7
+ "anyOf",
8
+ "const",
9
+ "contains",
10
+ "contentEncoding",
11
+ "contentMediaType",
12
+ "contentSchema",
13
+ "dependencies",
14
+ "dependentRequired",
15
+ "dependentSchemas",
16
+ "else",
17
+ "enum",
18
+ "exclusiveMaximum",
19
+ "exclusiveMinimum",
20
+ "format",
21
+ "if",
22
+ "items",
23
+ "maxContains",
24
+ "maximum",
25
+ "maxItems",
26
+ "maxLength",
27
+ "maxProperties",
28
+ "minContains",
29
+ "minimum",
30
+ "minItems",
31
+ "minLength",
32
+ "minProperties",
33
+ "multipleOf",
34
+ "not",
35
+ "oneOf",
36
+ "pattern",
37
+ "patternProperties",
38
+ "prefixItems",
39
+ "properties",
40
+ "propertyNames",
41
+ "required",
42
+ "then",
43
+ "type",
44
+ "unevaluatedItems",
45
+ "unevaluatedProperties",
46
+ "uniqueItems"
47
+ ]);
48
+ export const JSON_SCHEMA_PRIMITIVE_TYPES = /* @__PURE__ */ new Set([
49
+ "string",
50
+ "number",
51
+ "integer",
52
+ "boolean",
53
+ "null"
54
+ ]);
55
+ export const JSON_SCHEMA_RECORD_KEYWORDS = /* @__PURE__ */ new Set([
56
+ "properties",
57
+ "patternProperties",
58
+ "dependentSchemas",
59
+ "dependencies",
60
+ "$defs"
61
+ ]);
@@ -0,0 +1,29 @@
1
+ import type { AnySchema } from "@orpc/contract";
2
+ import type { JsonSchema } from "./types.mjs";
3
+ /**
4
+ * The conversion direction: `input` targets the schema's input type, `output` targets its output type.
5
+ *
6
+ * @see {@link https://orpc.dev/docs/integrations/standard-schema | Standard Schema Integration}
7
+ */
8
+ export type JsonSchemaConverterDirection = "input" | "output";
9
+ /**
10
+ * Interface for converting validation schemas into JSON Schema representations,
11
+ * used by tools such as the OpenAPI Generator and Smart Coercion plugins.
12
+ *
13
+ * @see {@link https://orpc.dev/docs/integrations/standard-schema | Standard Schema Integration}
14
+ */
15
+ export interface JsonSchemaConverter {
16
+ /**
17
+ * Determines whether this converter can handle the given schema.
18
+ */
19
+ condition(schema: AnySchema | undefined, direction: JsonSchemaConverterDirection): boolean;
20
+ /**
21
+ * Converts an ORPC schema to a JSON Schema representation.
22
+ */
23
+ convert(schema: AnySchema | undefined, direction: JsonSchemaConverterDirection): [jsonSchema: JsonSchema, optional: boolean];
24
+ }
25
+ export declare class DelegatingJsonSchemaConverter implements Pick<JsonSchemaConverter, "convert"> {
26
+ private readonly converters;
27
+ constructor(converters?: JsonSchemaConverter[]);
28
+ convert(schema: AnySchema | undefined, direction: JsonSchemaConverterDirection): [jsonSchema: JsonSchema, optional: boolean];
29
+ }
@@ -0,0 +1,14 @@
1
+ export class DelegatingJsonSchemaConverter {
2
+ converters;
3
+ constructor(converters = []) {
4
+ this.converters = converters;
5
+ }
6
+ convert(schema, direction) {
7
+ for (const converter of this.converters) {
8
+ if (converter.condition(schema, direction)) {
9
+ return converter.convert(schema, direction);
10
+ }
11
+ }
12
+ return [{}, true];
13
+ }
14
+ }