diff --git a/CHANGELOG.md b/CHANGELOG.md index 33ffcd4e0c..422558cecc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,12 @@ should change the heading of the (upcoming) version to include a major version b - Extended `Registry` interface to include optional `experimental_componentUpdateStrategy` property - Added `shallowEquals()` utility function for shallow equality comparisons +# 6.0.0-beta.13 + +## rjsf/utils + +- Always make all references absolute in nested bundled schemas + # 6.0.0-beta.12 ## @rjsf/core diff --git a/packages/utils/src/constants.ts b/packages/utils/src/constants.ts index e379f0c372..4867358df5 100644 --- a/packages/utils/src/constants.ts +++ b/packages/utils/src/constants.ts @@ -46,4 +46,5 @@ export const UI_GLOBAL_OPTIONS_KEY = 'ui:globalOptions'; /** The JSON Schema version strings */ +export const JSON_SCHEMA_DRAFT_2019_09 = 'https://json-schema.org/draft/2019-09/schema'; export const JSON_SCHEMA_DRAFT_2020_12 = 'https://json-schema.org/draft/2020-12/schema'; diff --git a/packages/utils/src/createSchemaUtils.ts b/packages/utils/src/createSchemaUtils.ts index 28a3db9c2d..d3d1d21571 100644 --- a/packages/utils/src/createSchemaUtils.ts +++ b/packages/utils/src/createSchemaUtils.ts @@ -29,6 +29,9 @@ import { toIdSchema, toPathSchema, } from './schema'; +import { makeAllReferencesAbsolute } from './findSchemaDefinition'; +import { ID_KEY, JSON_SCHEMA_DRAFT_2020_12, SCHEMA_KEY } from './constants'; +import get from 'lodash/get'; /** The `SchemaUtils` class provides a wrapper around the publicly exported APIs in the `utils/schema` directory such * that one does not have to explicitly pass the `validator`, `rootSchema`, `experimental_defaultFormStateBehavior` or @@ -57,7 +60,11 @@ class SchemaUtils, ) { - this.rootSchema = rootSchema; + if (rootSchema[SCHEMA_KEY] === JSON_SCHEMA_DRAFT_2020_12) { + this.rootSchema = makeAllReferencesAbsolute(rootSchema, get(rootSchema, ID_KEY, '#')); + } else { + this.rootSchema = rootSchema; + } this.validator = validator; this.experimental_defaultFormStateBehavior = experimental_defaultFormStateBehavior; this.experimental_customMergeAllOf = experimental_customMergeAllOf; diff --git a/packages/utils/src/findSchemaDefinition.ts b/packages/utils/src/findSchemaDefinition.ts index 09abc7e215..4b06f8cac9 100644 --- a/packages/utils/src/findSchemaDefinition.ts +++ b/packages/utils/src/findSchemaDefinition.ts @@ -1,7 +1,14 @@ import jsonpointer from 'jsonpointer'; import omit from 'lodash/omit'; -import { ID_KEY, JSON_SCHEMA_DRAFT_2020_12, REF_KEY, SCHEMA_KEY } from './constants'; +import { + ALL_OF_KEY, + ID_KEY, + JSON_SCHEMA_DRAFT_2019_09, + JSON_SCHEMA_DRAFT_2020_12, + REF_KEY, + SCHEMA_KEY, +} from './constants'; import { GenericObjectType, RJSFSchema, StrictRJSFSchema } from './types'; import isObject from 'lodash/isObject'; import isEmpty from 'lodash/isEmpty'; @@ -20,7 +27,16 @@ function findEmbeddedSchemaRecursive(sc return schema; } for (const subSchema of Object.values(schema)) { - if (isObject(subSchema)) { + if (Array.isArray(subSchema)) { + for (const item of subSchema) { + if (isObject(item)) { + const result = findEmbeddedSchemaRecursive(item as S, ref); + if (result !== undefined) { + return result as S; + } + } + } + } else if (isObject(subSchema)) { const result = findEmbeddedSchemaRecursive(subSchema as S, ref); if (result !== undefined) { return result as S; @@ -30,6 +46,31 @@ function findEmbeddedSchemaRecursive(sc return undefined; } +/** Parses a JSONSchema and makes all references absolute with respect to + * the `baseURI` argument + * @param schema - The schema to be processed + * @param baseURI - The base URI to be used for resolving relative references + */ +export function makeAllReferencesAbsolute(schema: S, baseURI: string): S { + const currentURI = get(schema, ID_KEY, baseURI); + // Make all other references absolute + if (REF_KEY in schema) { + schema = { ...schema, [REF_KEY]: UriResolver.resolve(currentURI, schema[REF_KEY]!) }; + } + // Look for references in nested subschemas + for (const [key, subSchema] of Object.entries(schema)) { + if (Array.isArray(subSchema)) { + schema = { + ...schema, + [key]: subSchema.map((item) => (isObject(item) ? makeAllReferencesAbsolute(item as S, currentURI) : item)), + }; + } else if (isObject(subSchema)) { + schema = { ...schema, [key]: makeAllReferencesAbsolute(subSchema as S, currentURI) }; + } + } + return schema; +} + /** Splits out the value at the `key` in `object` from the `object`, returning an array that contains in the first * location, the `object` minus the `key: value` and in the second location the `value`. * @@ -103,7 +144,14 @@ export function findSchemaDefinitionRecursive(theRef, rootSchema, [...recurseList, ref], baseURI); if (Object.keys(remaining).length > 0) { - return { ...remaining, ...subSchema }; + if ( + rootSchema[SCHEMA_KEY] === JSON_SCHEMA_DRAFT_2019_09 || + rootSchema[SCHEMA_KEY] === JSON_SCHEMA_DRAFT_2020_12 + ) { + return { [ALL_OF_KEY]: [remaining, subSchema] } as S; + } else { + return { ...remaining, ...subSchema }; + } } return subSchema; } diff --git a/packages/utils/src/schema/retrieveSchema.ts b/packages/utils/src/schema/retrieveSchema.ts index cb6d58d9d7..807d87b63b 100644 --- a/packages/utils/src/schema/retrieveSchema.ts +++ b/packages/utils/src/schema/retrieveSchema.ts @@ -430,7 +430,7 @@ export function stubExistingAdditionalProperties< if (!isEmpty(matchingProperties)) { schema.properties[key] = retrieveSchema( validator, - { allOf: Object.values(matchingProperties) } as S, + { [ALL_OF_KEY]: Object.values(matchingProperties) } as S, rootSchema, get(formData, [key]) as T, experimental_customMergeAllOf, @@ -445,7 +445,7 @@ export function stubExistingAdditionalProperties< if (REF_KEY in schema.additionalProperties!) { additionalProperties = retrieveSchema( validator, - { $ref: get(schema.additionalProperties, [REF_KEY]) } as S, + { [REF_KEY]: get(schema.additionalProperties, [REF_KEY]) } as S, rootSchema, formData as T, experimental_customMergeAllOf, diff --git a/packages/utils/test/createSchemaUtils.test.ts b/packages/utils/test/createSchemaUtils.test.ts index 1beced47e4..8f8dae82d0 100644 --- a/packages/utils/test/createSchemaUtils.test.ts +++ b/packages/utils/test/createSchemaUtils.test.ts @@ -1,7 +1,12 @@ import { createSchemaUtils, Experimental_DefaultFormStateBehavior, + ID_KEY, + JSON_SCHEMA_DRAFT_2020_12, + PROPERTIES_KEY, + REF_KEY, RJSFSchema, + SCHEMA_KEY, SchemaUtilsType, ValidatorType, } from '../src'; @@ -23,6 +28,36 @@ describe('createSchemaUtils()', () => { expect(schemaUtils.getValidator()).toBe(testValidator); }); + describe('2020-12 schema', () => { + const rootSchema2020: RJSFSchema = { + [SCHEMA_KEY]: JSON_SCHEMA_DRAFT_2020_12, + [ID_KEY]: 'https://example.com/2020-12.json', + type: 'object', + $defs: { + example: { + type: 'integer', + }, + }, + [PROPERTIES_KEY]: { + ref: { + [REF_KEY]: '#/$defs/example', + }, + }, + }; + const schemaUtils2020: SchemaUtilsType = createSchemaUtils(testValidator, rootSchema2020, defaultFormStateBehavior); + + it('getRootSchema()', () => { + expect(schemaUtils2020.getRootSchema()).toEqual({ + ...rootSchema2020, + [PROPERTIES_KEY]: { + ref: { + [REF_KEY]: 'https://example.com/2020-12.json#/$defs/example', + }, + }, + }); + }); + }); + describe('doesSchemaUtilsDiffer()', () => { describe('constructed without defaultFormStateBehavior', () => { const schemaUtils: SchemaUtilsType = createSchemaUtils(testValidator, rootSchema); diff --git a/packages/utils/test/findSchemaDefinition.test.ts b/packages/utils/test/findSchemaDefinition.test.ts index 0cf130b9cb..1a31ff848b 100644 --- a/packages/utils/test/findSchemaDefinition.test.ts +++ b/packages/utils/test/findSchemaDefinition.test.ts @@ -1,5 +1,5 @@ -import { findSchemaDefinition, RJSFSchema } from '../src'; -import { findSchemaDefinitionRecursive } from '../src/findSchemaDefinition'; +import { findSchemaDefinition, ID_KEY, RJSFSchema } from '../src'; +import { findSchemaDefinitionRecursive, makeAllReferencesAbsolute } from '../src/findSchemaDefinition'; const schema: RJSFSchema = { type: 'object', @@ -46,34 +46,64 @@ const schema: RJSFSchema = { }, }; -const bundledSchema: RJSFSchema = { +const internalSchema: RJSFSchema = { $schema: 'https://json-schema.org/draft/2020-12/schema', + $id: 'https://example.com/bundled.ref.json', type: 'object', - $id: 'https://example.com/bundled.schema.json', $defs: { - bundledSchema: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - $id: 'https://example.com/bundled.ref.json', - type: 'object', - $defs: { - string: { - type: 'string', - }, - circularRef: { - $ref: '/bundled.schema.json/#/$defs/circularRef', + colors: { + type: 'string', + enum: ['red', 'green', 'blue'], + }, + string: { + type: 'string', + }, + circularRef: { + $ref: '/bundled.schema.json/#/$defs/circularRef', + }, + undefinedRef: { + $ref: '#/$defs/undefined', + }, + }, + properties: { + num: { + type: 'integer', + }, + string: { + $ref: '#/$defs/string', + }, + allOf: { + allOf: [ + { + $ref: '#/$defs/string', }, - undefinedRef: { - $ref: '#/$defs/undefined', + { + title: 'String', }, - }, - properties: { - num: { - type: 'integer', + ], + }, + }, +}; + +const bundledSchema: RJSFSchema = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + $id: 'https://example.com/bundled.schema.json', + $defs: { + bundledSchema: internalSchema, + bundledSchemaArray: { + anyOf: [ + { + $schema: 'https://json-schema.org/draft/2020-12/schema', + $id: 'https://example.com/bundled.ref.array.1.json', + type: 'object', }, - string: { - $ref: '#/$defs/string', + { + $schema: 'https://json-schema.org/draft/2020-12/schema', + $id: 'https://example.com/bundled.ref.array.2.json', + type: 'object', }, - }, + ], }, bundledAbsoluteRef: { $ref: 'https://example.com/bundled.ref.json', @@ -81,6 +111,9 @@ const bundledSchema: RJSFSchema = { bundledAbsoluteRefWithAnchor: { $ref: 'https://example.com/bundled.ref.json/#/properties/num', }, + bundledAbsoluteRefWithinArray: { + $ref: 'https://example.com/bundled.ref.array.1.json', + }, bundledRelativeRef: { $ref: '/bundled.ref.json', }, @@ -96,6 +129,9 @@ const bundledSchema: RJSFSchema = { undefinedRef: { $ref: '/undefined.ref.json', }, + undefinedRefWithAnchor: { + $ref: '/bundled.ref.json#/$defs/undefinedRef', + }, }, properties: { undefined: { @@ -150,38 +186,41 @@ describe('findSchemaDefinition()', () => { ); }); it('correctly resolves absolute bundled refs when JSON Schema Draft 2020-12', () => { - expect(findSchemaDefinition('#/$defs/bundledAbsoluteRef', bundledSchema)).toBe(bundledSchema.$defs!.bundledSchema); + expect(findSchemaDefinition('#/$defs/bundledAbsoluteRef', bundledSchema)).toStrictEqual(internalSchema); }); it('correctly resolves absolute bundled refs with anchors within a JSON Schema Draft 2020-12', () => { expect(findSchemaDefinition('#/$defs/bundledAbsoluteRefWithAnchor', bundledSchema)).toBe( - (bundledSchema.$defs!.bundledSchema as RJSFSchema).properties!.num, + internalSchema.properties!.num, + ); + }); + it('correctly resolves absolute bundled refs in arrays within a JSON Schema Draft 2020-12', () => { + expect(findSchemaDefinition('#/$defs/bundledAbsoluteRefWithinArray', bundledSchema)).toBe( + (bundledSchema.$defs!.bundledSchemaArray as RJSFSchema).anyOf![0], ); }); it('correctly resolves absolute bundled refs within a JSON Schema Draft 2020-12 without an `$id`', () => { const { $id: d, ...bundledSchemaWithoutId } = bundledSchema; - expect(findSchemaDefinition('#/$defs/bundledAbsoluteRef', bundledSchemaWithoutId)).toBe( - bundledSchema.$defs!.bundledSchema, - ); + expect(findSchemaDefinition('#/$defs/bundledAbsoluteRef', bundledSchemaWithoutId)).toStrictEqual(internalSchema); }); it('correctly resolves relative bundled refs within a JSON Schema Draft 2020-12', () => { - expect(findSchemaDefinition('#/$defs/bundledRelativeRef', bundledSchema)).toBe(bundledSchema.$defs!.bundledSchema); + expect(findSchemaDefinition('#/$defs/bundledRelativeRef', bundledSchema)).toStrictEqual(internalSchema); }); it('correctly resolves relative bundled refs with anchors within a JSON Schema Draft 2020-12', () => { expect(findSchemaDefinition('#/$defs/bundledRelativeRefWithAnchor', bundledSchema)).toBe( - (bundledSchema.$defs!.bundledSchema as RJSFSchema).$defs!.string, + internalSchema.$defs!.string, ); }); it('correctly resolves indirect bundled refs within a JSON Schema Draft 2020-12', () => { - expect(findSchemaDefinition('#/$defs/indirectRef', bundledSchema)).toBe(bundledSchema.$defs!.bundledSchema); + expect(findSchemaDefinition('#/$defs/indirectRef', bundledSchema)).toStrictEqual(internalSchema); }); it('correctly resolves refs with explicit base URI in a bundled JSON Schema', () => { - expect(findSchemaDefinition('bundled.ref.json', bundledSchema, 'https://example.com/undefined.ref.json')).toBe( - bundledSchema.$defs!.bundledSchema, - ); + expect( + findSchemaDefinition('bundled.ref.json', bundledSchema, 'https://example.com/undefined.ref.json'), + ).toStrictEqual(internalSchema); }); it('correctly resolves local refs with explicit base URI in a bundled JSON Schema', () => { expect(findSchemaDefinition('#/properties/num', bundledSchema, 'https://example.com/bundled.ref.json')).toBe( - (bundledSchema.$defs!.bundledSchema as RJSFSchema).properties!.num, + internalSchema.properties!.num, ); }); it('throws error when relative ref is undefined in a bundled JSON Schema', () => { @@ -189,6 +228,11 @@ describe('findSchemaDefinition()', () => { 'Could not find a definition for /undefined.ref.json', ); }); + it('throws error when relative ref with anchor is undefined in a bundled JSON Schema', () => { + expect(() => findSchemaDefinition('#/$defs/undefinedRefWithAnchor', bundledSchema)).toThrowError( + 'Could not find a definition for #/$defs/undefined', + ); + }); it('throws error when local ref is undefined in a bundled JSON Schema with explicit base URI', () => { expect(() => findSchemaDefinition('#/properties/undefined', bundledSchema, 'https://example.com/bundled.ref.json'), @@ -250,51 +294,55 @@ describe('findSchemaDefinitionRecursive()', () => { ).toThrowError('Could not find a definition for #/properties/num'); }); it('correctly resolves absolute bundled refs within a JSON Schema Draft 2020-12', () => { - expect(findSchemaDefinitionRecursive('#/$defs/bundledAbsoluteRef', bundledSchema)).toBe( - bundledSchema.$defs!.bundledSchema, - ); + expect(findSchemaDefinitionRecursive('#/$defs/bundledAbsoluteRef', bundledSchema)).toStrictEqual(internalSchema); }); it('correctly resolves absolute bundled refs with anchors within a JSON Schema Draft 2020-12', () => { expect(findSchemaDefinitionRecursive('#/$defs/bundledAbsoluteRefWithAnchor', bundledSchema)).toBe( - (bundledSchema.$defs!.bundledSchema as RJSFSchema).properties!.num, + internalSchema.properties!.num, + ); + }); + it('correctly resolves absolute bundled refs in arrays within a JSON Schema Draft 2020-12', () => { + expect(findSchemaDefinitionRecursive('#/$defs/bundledAbsoluteRefWithinArray', bundledSchema)).toBe( + (bundledSchema.$defs!.bundledSchemaArray as RJSFSchema).anyOf![0], ); }); it('correctly resolves absolute bundled refs within a JSON Schema Draft 2020-12 without an `$id`', () => { const { $id: d, ...bundledSchemaWithoutId } = bundledSchema; - expect(findSchemaDefinitionRecursive('#/$defs/bundledAbsoluteRef', bundledSchemaWithoutId)).toBe( - bundledSchema.$defs!.bundledSchema, + expect(findSchemaDefinitionRecursive('#/$defs/bundledAbsoluteRef', bundledSchemaWithoutId)).toStrictEqual( + internalSchema, ); }); it('correctly resolves relative bundled refs within a JSON Schema Draft 2020-12', () => { - expect(findSchemaDefinitionRecursive('#/$defs/bundledRelativeRef', bundledSchema)).toBe( - bundledSchema.$defs!.bundledSchema, - ); + expect(findSchemaDefinitionRecursive('#/$defs/bundledRelativeRef', bundledSchema)).toStrictEqual(internalSchema); }); it('correctly resolves relative bundled refs with anchors within a JSON Schema Draft 2020-12', () => { expect(findSchemaDefinitionRecursive('#/$defs/bundledRelativeRefWithAnchor', bundledSchema)).toBe( - (bundledSchema.$defs!.bundledSchema as RJSFSchema).$defs!.string, + internalSchema.$defs!.string, ); }); it('correctly resolves indirect bundled refs within a JSON Schema Draft 2020-12', () => { - expect(findSchemaDefinitionRecursive('#/$defs/indirectRef', bundledSchema)).toBe( - bundledSchema.$defs!.bundledSchema, - ); + expect(findSchemaDefinitionRecursive('#/$defs/indirectRef', bundledSchema)).toStrictEqual(internalSchema); }); it('correctly resolves relative refs with explicit base URI in a bundled JSON Schema', () => { expect( findSchemaDefinitionRecursive('bundled.ref.json', bundledSchema, [], 'https://example.com/undefined.ref.json'), - ).toBe(bundledSchema.$defs!.bundledSchema); + ).toStrictEqual(internalSchema); }); it('correctly resolves local refs with explicit base URI in a bundled JSON Schema', () => { expect( findSchemaDefinitionRecursive('#/properties/num', bundledSchema, [], 'https://example.com/bundled.ref.json'), - ).toBe((bundledSchema.$defs!.bundledSchema as RJSFSchema).properties!.num); + ).toBe(internalSchema.properties!.num); }); it('throws error when relative ref is undefined in a bundled JSON Schema', () => { expect(() => findSchemaDefinitionRecursive('#/$defs/undefinedRef', bundledSchema)).toThrowError( 'Could not find a definition for /undefined.ref.json', ); }); + it('throws error when relative ref with anchor is undefined in a bundled JSON Schema', () => { + expect(() => findSchemaDefinitionRecursive('#/$defs/undefinedRefWithAnchor', bundledSchema)).toThrowError( + 'Could not find a definition for #/$defs/undefined', + ); + }); it('throws error when local ref is undefined in a bundled JSON Schema with explicit base URI', () => { expect(() => findSchemaDefinitionRecursive( @@ -316,3 +364,30 @@ describe('findSchemaDefinitionRecursive()', () => { ); }); }); + +describe('makeAllReferencesAbsolute()', () => { + it('correctly makes all references absolute in a JSON Schema', () => { + expect(makeAllReferencesAbsolute(internalSchema, internalSchema[ID_KEY]!)).toStrictEqual({ + ...internalSchema, + $defs: { + ...internalSchema.$defs, + circularRef: { $ref: 'https://example.com/bundled.schema.json/#/$defs/circularRef' }, + undefinedRef: { $ref: 'https://example.com/bundled.ref.json#/$defs/undefined' }, + }, + properties: { + ...internalSchema.properties, + string: { $ref: 'https://example.com/bundled.ref.json#/$defs/string' }, + allOf: { + allOf: [ + { + $ref: 'https://example.com/bundled.ref.json#/$defs/string', + }, + { + title: 'String', + }, + ], + }, + }, + }); + }); +});