From 0ecbeb69740c5420c4594422254b4c3badb788ca Mon Sep 17 00:00:00 2001 From: Christian Wendt <54559756+cwendtxealth@users.noreply.github.com> Date: Mon, 24 Jul 2023 13:02:17 -0700 Subject: [PATCH] fix: Fixed 3778 By Resolving all refs in oneOf/AnyOf options (#3786) * fix: Fixed 3778 By Resolving all refs in oneOf/AnyOf options * remove console log * add test for sanitizeDataForNewSchema ref resolving * Update chanelog.md --- CHANGELOG.md | 8 +++ .../src/schema/getClosestMatchingOption.ts | 7 +-- packages/utils/src/schema/retrieveSchema.ts | 42 ++++++++++++-- .../utils/test/schema/retrieveSchemaTest.ts | 58 +++++++++++++++++++ .../schema/sanitizeDataForNewSchemaTest.ts | 31 ++++++++++ 5 files changed, 136 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cd86e0b87..cee338e53c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,14 @@ it according to semantic versioning. For example, if your PR adds a breaking cha should change the heading of the (upcoming) version to include a major version bump. --> +# 5.11.1 + +## @rjsf/utils + +- Created new `resolveAllReferences()` function to resolve all references within a schema's properties and array items. +- Updated `getClosestMatchingOption()` to use `resolveAllReferences()` for all oneOf/anyOf schemas +- Updated `resolveAnyOrOneOfSchemas()` to use `resolveAllReferences()` for all oneOf/anyOf schemas + # 5.11.0 ## @rjsf/core diff --git a/packages/utils/src/schema/getClosestMatchingOption.ts b/packages/utils/src/schema/getClosestMatchingOption.ts index 8ddda4474b..87ff1265ad 100644 --- a/packages/utils/src/schema/getClosestMatchingOption.ts +++ b/packages/utils/src/schema/getClosestMatchingOption.ts @@ -6,7 +6,7 @@ import reduce from 'lodash/reduce'; import times from 'lodash/times'; import getFirstMatchingOption from './getFirstMatchingOption'; -import retrieveSchema from './retrieveSchema'; +import retrieveSchema, { resolveAllReferences } from './retrieveSchema'; import { ONE_OF_KEY, REF_KEY, JUNK_OPTION_ID, ANY_OF_KEY } from '../constants'; import guessType from '../guessType'; import { FormContextType, RJSFSchema, StrictRJSFSchema, ValidatorType } from '../types'; @@ -145,10 +145,7 @@ export default function getClosestMatchingOption< ): number { // First resolve any refs in the options const resolvedOptions = options.map((option) => { - if (has(option, REF_KEY)) { - return retrieveSchema(validator, option, rootSchema, formData); - } - return option; + return resolveAllReferences(option, rootSchema); }); // Reduce the array of options down to a list of the indexes that are considered matching options const allValidIndexes = resolvedOptions.reduce((validList: number[], option, index: number) => { diff --git a/packages/utils/src/schema/retrieveSchema.ts b/packages/utils/src/schema/retrieveSchema.ts index 4e711fafe2..19f8a8dd0f 100644 --- a/packages/utils/src/schema/retrieveSchema.ts +++ b/packages/utils/src/schema/retrieveSchema.ts @@ -1,6 +1,7 @@ import get from 'lodash/get'; import set from 'lodash/set'; import times from 'lodash/times'; +import forEach from 'lodash/forEach'; import mergeAllOf, { Options } from 'json-schema-merge-allof'; import { @@ -12,6 +13,8 @@ import { IF_KEY, ONE_OF_KEY, REF_KEY, + PROPERTIES_KEY, + ITEMS_KEY, } from '../constants'; import findSchemaDefinition, { splitKeyElementFromObject } from '../findSchemaDefinition'; import getDiscriminatorFieldFromSchema from '../getDiscriminatorFieldFromSchema'; @@ -192,6 +195,39 @@ export function resolveReference(schema: S, rootSchema: S): S { + let resolvedSchema: S = schema; + // resolve top level ref + if (REF_KEY in resolvedSchema) { + const { $ref, ...localSchema } = resolvedSchema; + // Retrieve the referenced schema definition. + const refSchema = findSchemaDefinition($ref, rootSchema); + resolvedSchema = { ...refSchema, ...localSchema }; + } + + if (PROPERTIES_KEY in resolvedSchema) { + forEach(resolvedSchema[PROPERTIES_KEY], (value, key) => { + resolvedSchema[PROPERTIES_KEY]![key] = resolveAllReferences(value as S, rootSchema); + }); + } + + if ( + ITEMS_KEY in resolvedSchema && + !Array.isArray(resolvedSchema.items) && + typeof resolvedSchema.items !== 'boolean' + ) { + resolvedSchema.items = resolveAllReferences(resolvedSchema.items as S, rootSchema); + } + + return resolvedSchema; +} + /** Creates new 'properties' items for each key in the `formData` * * @param validator - An implementation of the `ValidatorType` interface that will be used when necessary @@ -333,11 +369,7 @@ export function resolveAnyOrOneOfSchemas< const formData = rawFormData === undefined && expandAllBranches ? ({} as T) : rawFormData; const discriminator = getDiscriminatorFieldFromSchema(schema); anyOrOneOf = anyOrOneOf.map((s) => { - if (REF_KEY in s) { - // For this ref situation, don't expand all branches and just pick the first/only schema result - return resolveReference(validator, s, rootSchema, false, formData)[0]; - } - return s; + return resolveAllReferences(s, rootSchema); }); // Call this to trigger the set of isValid() calls that the schema parser will need const option = getFirstMatchingOption(validator, formData, anyOrOneOf, rootSchema, discriminator); diff --git a/packages/utils/test/schema/retrieveSchemaTest.ts b/packages/utils/test/schema/retrieveSchemaTest.ts index b58f7507c1..5a63222b72 100644 --- a/packages/utils/test/schema/retrieveSchemaTest.ts +++ b/packages/utils/test/schema/retrieveSchemaTest.ts @@ -1307,6 +1307,64 @@ export default function retrieveSchemaTest(testValidator: TestValidatorType) { }, ]); }); + it('resolves oneOf with multiple $refs', () => { + const schema: RJSFSchema = { + oneOf: [ + { + type: 'object', + properties: { + field: { + $ref: '#/definitions/aObject', + }, + }, + }, + { + type: 'array', + items: { + $ref: '#/definitions/bObject', + }, + }, + ], + }; + const rootSchema: RJSFSchema = { + definitions: { + aObject: { + properties: { + a: { enum: ['typeA'] }, + b: { type: 'number' }, + }, + }, + bObject: { + properties: { + a: { enum: ['typeB'] }, + c: { type: 'boolean' }, + }, + }, + }, + }; + expect(resolveAnyOrOneOfSchemas(testValidator, schema, rootSchema, true)).toEqual([ + { + type: 'object', + properties: { + field: { + properties: { + a: { enum: ['typeA'] }, + b: { type: 'number' }, + }, + }, + }, + }, + { + type: 'array', + items: { + properties: { + a: { enum: ['typeB'] }, + c: { type: 'boolean' }, + }, + }, + }, + ]); + }); }); describe('resolveCondition()', () => { it('returns both conditions with expandAll', () => { diff --git a/packages/utils/test/schema/sanitizeDataForNewSchemaTest.ts b/packages/utils/test/schema/sanitizeDataForNewSchemaTest.ts index 76e58321ee..902d168716 100644 --- a/packages/utils/test/schema/sanitizeDataForNewSchemaTest.ts +++ b/packages/utils/test/schema/sanitizeDataForNewSchemaTest.ts @@ -258,6 +258,37 @@ export default function sanitizeDataForNewSchemaTest(testValidator: TestValidato }) ).toEqual({}); }); + it('returns empty formData after resolving schema refs', () => { + const rootSchema: RJSFSchema = { + definitions: { + string_def: { + type: 'string', + }, + }, + }; + const oldSchema: RJSFSchema = { + type: 'object', + properties: { + field: { + $ref: '#/definitions/string_def', + }, + oldField: { + type: 'string', + }, + }, + }; + const newSchema: RJSFSchema = { + type: 'object', + properties: { + field: { + $ref: '#/definitions/string_def', + }, + }, + }; + expect(sanitizeDataForNewSchema(testValidator, rootSchema, newSchema, oldSchema, { oldField: 'test' })).toEqual( + {} + ); + }); it('returns data when two arrays have same boolean items', () => { const oldSchema: RJSFSchema = { type: 'array',