@orpc/json-schema 2.0.0-beta.20 → 2.0.0-beta.21
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/index.mjs +107 -76
- package/package.json +5 -5
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { get, isPlainObject,
|
|
1
|
+
import { get, isPlainObject, tryOrUndefined, toArray, isDeepEqual, omit, isTypescriptObject } from '@orpc/shared';
|
|
2
2
|
export { Format as JsonSchemaFormat, TypeName as JsonSchemaType } from 'json-schema-typed/draft-2020-12';
|
|
3
3
|
import { ORPCError, cloneORPCError } from '@orpc/client';
|
|
4
4
|
import { getProcedureContractOrThrow } from '@orpc/contract';
|
|
@@ -169,7 +169,12 @@ var JsonSchemaXNativeType = /* @__PURE__ */ ((JsonSchemaXNativeType2) => {
|
|
|
169
169
|
return JsonSchemaXNativeType2;
|
|
170
170
|
})(JsonSchemaXNativeType || {});
|
|
171
171
|
|
|
172
|
-
const
|
|
172
|
+
const UNSATISFIED = 0;
|
|
173
|
+
const LOOSELY_SATISFIED = 1;
|
|
174
|
+
const SATISFIED = 2;
|
|
175
|
+
function minSatisfaction(a, b) {
|
|
176
|
+
return a < b ? a : b;
|
|
177
|
+
}
|
|
173
178
|
class JsonSchemaCoercer {
|
|
174
179
|
coerce([schema, optional], value) {
|
|
175
180
|
if (optional && value === void 0) {
|
|
@@ -178,25 +183,29 @@ class JsonSchemaCoercer {
|
|
|
178
183
|
const [, coerced] = this.coerceInternal(schema, schema, value);
|
|
179
184
|
return coerced;
|
|
180
185
|
}
|
|
181
|
-
coerceInternal(rootSchema, schema, value) {
|
|
186
|
+
coerceInternal(rootSchema, schema, value, appliedRefs) {
|
|
182
187
|
if (typeof schema === "boolean") {
|
|
183
|
-
return [schema, value];
|
|
188
|
+
return [schema ? SATISFIED : UNSATISFIED, value];
|
|
184
189
|
}
|
|
185
190
|
if (Array.isArray(schema.type)) {
|
|
186
191
|
return this.coerceInternal(
|
|
187
192
|
rootSchema,
|
|
188
193
|
{ anyOf: schema.type.map((type) => ({ ...schema, type })) },
|
|
189
|
-
value
|
|
194
|
+
value,
|
|
195
|
+
appliedRefs
|
|
190
196
|
);
|
|
191
197
|
}
|
|
192
198
|
let coerced = value;
|
|
193
|
-
let satisfied =
|
|
199
|
+
let satisfied = SATISFIED;
|
|
194
200
|
if (typeof schema.$ref === "string") {
|
|
195
201
|
const resolved = schema.$ref.startsWith("#/") ? get(rootSchema, schema.$ref.slice("#/".length).split("/").map(decodeJsonPointerSegment)) : schema.$ref === "#" ? rootSchema : void 0;
|
|
196
|
-
if (resolved !== void 0) {
|
|
197
|
-
|
|
202
|
+
if (resolved !== void 0 && !appliedRefs?.has(resolved)) {
|
|
203
|
+
appliedRefs ??= /* @__PURE__ */ new Set();
|
|
204
|
+
appliedRefs.add(resolved);
|
|
205
|
+
const [subSatisfied, subCoerced] = this.coerceInternal(rootSchema, resolved, coerced, appliedRefs);
|
|
206
|
+
appliedRefs.delete(resolved);
|
|
198
207
|
coerced = subCoerced;
|
|
199
|
-
satisfied = subSatisfied;
|
|
208
|
+
satisfied = minSatisfaction(satisfied, subSatisfied);
|
|
200
209
|
}
|
|
201
210
|
}
|
|
202
211
|
const enumValues = schema.const !== void 0 ? [schema.const] : schema.enum;
|
|
@@ -210,24 +219,24 @@ class JsonSchemaCoercer {
|
|
|
210
219
|
if (enumValues.includes(booleanValue)) {
|
|
211
220
|
coerced = booleanValue;
|
|
212
221
|
} else {
|
|
213
|
-
satisfied =
|
|
222
|
+
satisfied = UNSATISFIED;
|
|
214
223
|
}
|
|
215
224
|
}
|
|
216
225
|
} else {
|
|
217
|
-
satisfied =
|
|
226
|
+
satisfied = UNSATISFIED;
|
|
218
227
|
}
|
|
219
228
|
}
|
|
220
229
|
if (schema.type) {
|
|
221
230
|
switch (schema.type) {
|
|
222
231
|
case "null": {
|
|
223
232
|
if (coerced !== null) {
|
|
224
|
-
satisfied =
|
|
233
|
+
satisfied = UNSATISFIED;
|
|
225
234
|
}
|
|
226
235
|
break;
|
|
227
236
|
}
|
|
228
237
|
case "string": {
|
|
229
238
|
if (typeof coerced !== "string") {
|
|
230
|
-
satisfied =
|
|
239
|
+
satisfied = UNSATISFIED;
|
|
231
240
|
}
|
|
232
241
|
break;
|
|
233
242
|
}
|
|
@@ -236,7 +245,7 @@ class JsonSchemaCoercer {
|
|
|
236
245
|
coerced = stringToNumber(coerced);
|
|
237
246
|
}
|
|
238
247
|
if (typeof coerced !== "number") {
|
|
239
|
-
satisfied =
|
|
248
|
+
satisfied = UNSATISFIED;
|
|
240
249
|
}
|
|
241
250
|
break;
|
|
242
251
|
}
|
|
@@ -245,7 +254,7 @@ class JsonSchemaCoercer {
|
|
|
245
254
|
coerced = stringToInteger(coerced);
|
|
246
255
|
}
|
|
247
256
|
if (typeof coerced !== "number" || !Number.isInteger(coerced)) {
|
|
248
|
-
satisfied =
|
|
257
|
+
satisfied = UNSATISFIED;
|
|
249
258
|
}
|
|
250
259
|
break;
|
|
251
260
|
}
|
|
@@ -254,7 +263,7 @@ class JsonSchemaCoercer {
|
|
|
254
263
|
coerced = stringToBoolean(coerced);
|
|
255
264
|
}
|
|
256
265
|
if (typeof coerced !== "boolean") {
|
|
257
|
-
satisfied =
|
|
266
|
+
satisfied = UNSATISFIED;
|
|
258
267
|
}
|
|
259
268
|
break;
|
|
260
269
|
}
|
|
@@ -266,26 +275,24 @@ class JsonSchemaCoercer {
|
|
|
266
275
|
const coercedItems = coerced.map((item, i) => {
|
|
267
276
|
const subSchema = prefixItemSchemas[i] ?? itemSchema;
|
|
268
277
|
if (subSchema === void 0) {
|
|
269
|
-
satisfied =
|
|
278
|
+
satisfied = minSatisfaction(satisfied, LOOSELY_SATISFIED);
|
|
270
279
|
return item;
|
|
271
280
|
}
|
|
272
281
|
const [subSatisfied, subCoerced] = this.coerceInternal(rootSchema, subSchema, item);
|
|
273
|
-
|
|
274
|
-
satisfied = false;
|
|
275
|
-
}
|
|
282
|
+
satisfied = minSatisfaction(satisfied, subSatisfied);
|
|
276
283
|
if (subCoerced !== item) {
|
|
277
284
|
shouldUseCoercedItems = true;
|
|
278
285
|
}
|
|
279
286
|
return subCoerced;
|
|
280
287
|
});
|
|
281
288
|
if (coercedItems.length < prefixItemSchemas.length) {
|
|
282
|
-
satisfied =
|
|
289
|
+
satisfied = UNSATISFIED;
|
|
283
290
|
}
|
|
284
291
|
if (shouldUseCoercedItems) {
|
|
285
292
|
coerced = coercedItems;
|
|
286
293
|
}
|
|
287
294
|
} else {
|
|
288
|
-
satisfied =
|
|
295
|
+
satisfied = UNSATISFIED;
|
|
289
296
|
}
|
|
290
297
|
break;
|
|
291
298
|
}
|
|
@@ -295,35 +302,36 @@ class JsonSchemaCoercer {
|
|
|
295
302
|
}
|
|
296
303
|
if (isPlainObject(coerced)) {
|
|
297
304
|
let shouldUseCoercedItems = false;
|
|
298
|
-
const coercedItems = {};
|
|
299
|
-
const patternProperties = Object.entries(schema.patternProperties ?? {}).
|
|
305
|
+
const coercedItems = { ...coerced };
|
|
306
|
+
const patternProperties = Object.entries(schema.patternProperties ?? {}).flatMap(([key, value2]) => {
|
|
307
|
+
const pattern = tryOrUndefined(() => new RegExp(key));
|
|
308
|
+
return pattern ? [[pattern, value2]] : [];
|
|
309
|
+
});
|
|
310
|
+
const propertySchemas = schema.properties;
|
|
300
311
|
for (const key in coerced) {
|
|
301
312
|
const value2 = coerced[key];
|
|
302
|
-
const subSchema =
|
|
303
|
-
if (value2
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
if (subCoerced !== value2) {
|
|
315
|
-
shouldUseCoercedItems = true;
|
|
313
|
+
const subSchema = (propertySchemas !== void 0 && Object.hasOwn(propertySchemas, key) ? propertySchemas[key] : void 0) ?? patternProperties.find(([pattern]) => pattern.test(key))?.[1] ?? schema.additionalProperties;
|
|
314
|
+
if (value2 !== void 0 || schema.required?.includes(key)) {
|
|
315
|
+
if (subSchema === void 0) {
|
|
316
|
+
satisfied = minSatisfaction(satisfied, LOOSELY_SATISFIED);
|
|
317
|
+
} else {
|
|
318
|
+
const [subSatisfied, subCoerced] = this.coerceInternal(rootSchema, subSchema, value2);
|
|
319
|
+
coercedItems[key] = subCoerced;
|
|
320
|
+
satisfied = minSatisfaction(satisfied, subSatisfied);
|
|
321
|
+
if (subCoerced !== value2) {
|
|
322
|
+
shouldUseCoercedItems = true;
|
|
323
|
+
}
|
|
316
324
|
}
|
|
317
325
|
}
|
|
318
326
|
}
|
|
319
327
|
if (schema.required?.some((key) => !Object.hasOwn(coercedItems, key))) {
|
|
320
|
-
satisfied =
|
|
328
|
+
satisfied = UNSATISFIED;
|
|
321
329
|
}
|
|
322
330
|
if (shouldUseCoercedItems) {
|
|
323
331
|
coerced = coercedItems;
|
|
324
332
|
}
|
|
325
333
|
} else {
|
|
326
|
-
satisfied =
|
|
334
|
+
satisfied = UNSATISFIED;
|
|
327
335
|
}
|
|
328
336
|
break;
|
|
329
337
|
}
|
|
@@ -336,7 +344,7 @@ class JsonSchemaCoercer {
|
|
|
336
344
|
coerced = stringToDate(coerced);
|
|
337
345
|
}
|
|
338
346
|
if (!(coerced instanceof Date)) {
|
|
339
|
-
satisfied =
|
|
347
|
+
satisfied = UNSATISFIED;
|
|
340
348
|
}
|
|
341
349
|
break;
|
|
342
350
|
}
|
|
@@ -350,7 +358,7 @@ class JsonSchemaCoercer {
|
|
|
350
358
|
break;
|
|
351
359
|
}
|
|
352
360
|
if (typeof coerced !== "bigint") {
|
|
353
|
-
satisfied =
|
|
361
|
+
satisfied = UNSATISFIED;
|
|
354
362
|
}
|
|
355
363
|
break;
|
|
356
364
|
}
|
|
@@ -359,7 +367,7 @@ class JsonSchemaCoercer {
|
|
|
359
367
|
coerced = stringToRegExp(coerced);
|
|
360
368
|
}
|
|
361
369
|
if (!(coerced instanceof RegExp)) {
|
|
362
|
-
satisfied =
|
|
370
|
+
satisfied = UNSATISFIED;
|
|
363
371
|
}
|
|
364
372
|
break;
|
|
365
373
|
}
|
|
@@ -368,7 +376,7 @@ class JsonSchemaCoercer {
|
|
|
368
376
|
coerced = stringToURL(coerced);
|
|
369
377
|
}
|
|
370
378
|
if (!(coerced instanceof URL)) {
|
|
371
|
-
satisfied =
|
|
379
|
+
satisfied = UNSATISFIED;
|
|
372
380
|
}
|
|
373
381
|
break;
|
|
374
382
|
}
|
|
@@ -377,7 +385,7 @@ class JsonSchemaCoercer {
|
|
|
377
385
|
coerced = arrayToSet(coerced);
|
|
378
386
|
}
|
|
379
387
|
if (!(coerced instanceof Set)) {
|
|
380
|
-
satisfied =
|
|
388
|
+
satisfied = UNSATISFIED;
|
|
381
389
|
}
|
|
382
390
|
break;
|
|
383
391
|
}
|
|
@@ -386,7 +394,7 @@ class JsonSchemaCoercer {
|
|
|
386
394
|
coerced = arrayToMap(coerced);
|
|
387
395
|
}
|
|
388
396
|
if (!(coerced instanceof Map)) {
|
|
389
|
-
satisfied =
|
|
397
|
+
satisfied = UNSATISFIED;
|
|
390
398
|
}
|
|
391
399
|
break;
|
|
392
400
|
}
|
|
@@ -394,54 +402,78 @@ class JsonSchemaCoercer {
|
|
|
394
402
|
}
|
|
395
403
|
if (schema.allOf) {
|
|
396
404
|
for (const subSchema of schema.allOf) {
|
|
397
|
-
const [subSatisfied, subCoerced] = this.coerceInternal(rootSchema, subSchema, coerced);
|
|
405
|
+
const [subSatisfied, subCoerced] = this.coerceInternal(rootSchema, subSchema, coerced, appliedRefs);
|
|
398
406
|
coerced = subCoerced;
|
|
399
|
-
|
|
400
|
-
satisfied = false;
|
|
401
|
-
}
|
|
407
|
+
satisfied = minSatisfaction(satisfied, subSatisfied);
|
|
402
408
|
}
|
|
403
409
|
}
|
|
404
410
|
for (const key of ["anyOf", "oneOf"]) {
|
|
405
411
|
if (schema[key]) {
|
|
406
|
-
let
|
|
412
|
+
let best;
|
|
407
413
|
for (const subSchema of schema[key]) {
|
|
408
|
-
const [subSatisfied, subCoerced] = this.coerceInternal(rootSchema, subSchema, coerced);
|
|
409
|
-
if (subSatisfied) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
414
|
+
const [subSatisfied, subCoerced] = this.coerceInternal(rootSchema, subSchema, coerced, appliedRefs);
|
|
415
|
+
if (subSatisfied === UNSATISFIED) {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (subCoerced === coerced) {
|
|
419
|
+
best = { satisfied: subSatisfied, value: subCoerced };
|
|
420
|
+
break;
|
|
416
421
|
}
|
|
422
|
+
if (!best || subSatisfied > best.satisfied) {
|
|
423
|
+
best = { satisfied: subSatisfied, value: subCoerced };
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (best) {
|
|
427
|
+
coerced = best.value;
|
|
428
|
+
satisfied = minSatisfaction(satisfied, best.satisfied);
|
|
429
|
+
} else {
|
|
430
|
+
satisfied = UNSATISFIED;
|
|
417
431
|
}
|
|
418
|
-
coerced = bestOptions ? bestOptions.coerced : coerced;
|
|
419
|
-
satisfied = bestOptions ? bestOptions.satisfied : false;
|
|
420
432
|
}
|
|
421
433
|
}
|
|
422
434
|
if (typeof schema.not !== "undefined") {
|
|
423
|
-
const [notSatisfied] = this.coerceInternal(rootSchema, schema.not, coerced);
|
|
424
|
-
if (notSatisfied) {
|
|
425
|
-
satisfied =
|
|
435
|
+
const [notSatisfied] = this.coerceInternal(rootSchema, schema.not, coerced, appliedRefs);
|
|
436
|
+
if (notSatisfied !== UNSATISFIED) {
|
|
437
|
+
satisfied = UNSATISFIED;
|
|
426
438
|
}
|
|
427
439
|
}
|
|
428
440
|
return [satisfied, coerced];
|
|
429
441
|
}
|
|
430
442
|
}
|
|
443
|
+
const NUMBER_PATTERN = /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/;
|
|
431
444
|
function stringToNumber(value) {
|
|
432
|
-
|
|
433
|
-
|
|
445
|
+
if (!NUMBER_PATTERN.test(value)) {
|
|
446
|
+
return value;
|
|
447
|
+
}
|
|
448
|
+
const num = Number(value);
|
|
449
|
+
if (num < Number.MIN_SAFE_INTEGER || num > Number.MAX_SAFE_INTEGER) {
|
|
434
450
|
return value;
|
|
435
451
|
}
|
|
436
452
|
return num;
|
|
437
453
|
}
|
|
454
|
+
const INTEGER_PATTERN = /^-?(?:0|[1-9]\d*)$/;
|
|
438
455
|
function stringToInteger(value) {
|
|
439
|
-
|
|
440
|
-
|
|
456
|
+
if (!INTEGER_PATTERN.test(value)) {
|
|
457
|
+
return value;
|
|
458
|
+
}
|
|
459
|
+
const num = Number(value);
|
|
460
|
+
if (!Number.isSafeInteger(num)) {
|
|
441
461
|
return value;
|
|
442
462
|
}
|
|
443
463
|
return num;
|
|
444
464
|
}
|
|
465
|
+
function stringToBigInt(value) {
|
|
466
|
+
if (!INTEGER_PATTERN.test(value)) {
|
|
467
|
+
return value;
|
|
468
|
+
}
|
|
469
|
+
return BigInt(value);
|
|
470
|
+
}
|
|
471
|
+
function numberToBigInt(value) {
|
|
472
|
+
if (!Number.isInteger(value)) {
|
|
473
|
+
return value;
|
|
474
|
+
}
|
|
475
|
+
return BigInt(value);
|
|
476
|
+
}
|
|
445
477
|
function stringToBoolean(value) {
|
|
446
478
|
const lower = value.toLowerCase();
|
|
447
479
|
if (lower === "false" || lower === "off") {
|
|
@@ -452,21 +484,20 @@ function stringToBoolean(value) {
|
|
|
452
484
|
}
|
|
453
485
|
return value;
|
|
454
486
|
}
|
|
455
|
-
|
|
456
|
-
return tryOrUndefined(() => BigInt(value)) ?? value;
|
|
457
|
-
}
|
|
458
|
-
function numberToBigInt(value) {
|
|
459
|
-
return tryOrUndefined(() => BigInt(value)) ?? value;
|
|
460
|
-
}
|
|
487
|
+
const DATE_TIME_PATTERN = /^[+-]?\d{4,6}-\d{1,2}-\d{1,2}(?:[T ].*)?$/;
|
|
461
488
|
function stringToDate(value) {
|
|
489
|
+
if (!DATE_TIME_PATTERN.test(value)) {
|
|
490
|
+
return value;
|
|
491
|
+
}
|
|
462
492
|
const date = new Date(value);
|
|
463
|
-
if (Number.isNaN(date.getTime())
|
|
493
|
+
if (Number.isNaN(date.getTime())) {
|
|
464
494
|
return value;
|
|
465
495
|
}
|
|
466
496
|
return date;
|
|
467
497
|
}
|
|
498
|
+
const REGEXP_PATTERN = /^\/([\s\S]*)\/([a-z]*)$/;
|
|
468
499
|
function stringToRegExp(value) {
|
|
469
|
-
const match = value.match(
|
|
500
|
+
const match = value.match(REGEXP_PATTERN);
|
|
470
501
|
if (match) {
|
|
471
502
|
const [, pattern, flags] = match;
|
|
472
503
|
return tryOrUndefined(() => new RegExp(pattern, flags)) ?? value;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orpc/json-schema",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.0.0-beta.
|
|
4
|
+
"version": "2.0.0-beta.21",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://orpc.dev",
|
|
7
7
|
"repository": {
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@standard-schema/spec": "^1.1.0",
|
|
29
29
|
"json-schema-typed": "^8.0.2",
|
|
30
|
-
"@orpc/client": "2.0.0-beta.
|
|
31
|
-
"@orpc/contract": "2.0.0-beta.
|
|
32
|
-
"@orpc/
|
|
33
|
-
"@orpc/
|
|
30
|
+
"@orpc/client": "2.0.0-beta.21",
|
|
31
|
+
"@orpc/contract": "2.0.0-beta.21",
|
|
32
|
+
"@orpc/server": "2.0.0-beta.21",
|
|
33
|
+
"@orpc/shared": "2.0.0-beta.21"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"zod": "^4.4.3"
|