From 1efd3069fae624738af4728c6da604d464db751b Mon Sep 17 00:00:00 2001 From: Marek Bodinger Date: Fri, 22 Mar 2024 18:59:19 +0100 Subject: [PATCH 1/4] Extract omitExtraData functionality from Form.tsx (core) to utils --- packages/utils/src/createSchemaUtils.ts | 5 ++ packages/utils/src/schema/index.ts | 2 + packages/utils/src/schema/omitExtraData.ts | 65 ++++++++++++++++++++++ packages/utils/src/types.ts | 2 + 4 files changed, 74 insertions(+) create mode 100644 packages/utils/src/schema/omitExtraData.ts diff --git a/packages/utils/src/createSchemaUtils.ts b/packages/utils/src/createSchemaUtils.ts index 39e743d945..b8120cdc88 100644 --- a/packages/utils/src/createSchemaUtils.ts +++ b/packages/utils/src/createSchemaUtils.ts @@ -27,6 +27,7 @@ import { sanitizeDataForNewSchema, toIdSchema, toPathSchema, + omitExtraData, } from './schema'; /** The `SchemaUtils` class provides a wrapper around the publicly exported APIs in the `utils/schema` directory such @@ -275,6 +276,10 @@ class SchemaUtils { return toPathSchema(this.validator, schema, name, this.rootSchema, formData); } + + omitExtraData(schema: S, formData?: T): T | undefined { + return omitExtraData(this.validator, schema, this.rootSchema, formData); + } } /** Creates a `SchemaUtilsType` interface that is based around the given `validator` and `rootSchema` parameters. The diff --git a/packages/utils/src/schema/index.ts b/packages/utils/src/schema/index.ts index 1adbd67712..43f53b0433 100644 --- a/packages/utils/src/schema/index.ts +++ b/packages/utils/src/schema/index.ts @@ -11,6 +11,7 @@ import retrieveSchema from './retrieveSchema'; import sanitizeDataForNewSchema from './sanitizeDataForNewSchema'; import toIdSchema from './toIdSchema'; import toPathSchema from './toPathSchema'; +import omitExtraData from './omitExtraData'; export { getDefaultFormState, @@ -22,6 +23,7 @@ export { isMultiSelect, isSelect, mergeValidationData, + omitExtraData, retrieveSchema, sanitizeDataForNewSchema, toIdSchema, diff --git a/packages/utils/src/schema/omitExtraData.ts b/packages/utils/src/schema/omitExtraData.ts new file mode 100644 index 0000000000..b50ba5a44f --- /dev/null +++ b/packages/utils/src/schema/omitExtraData.ts @@ -0,0 +1,65 @@ +import { FormContextType, GenericObjectType, PathSchema, RJSFSchema, StrictRJSFSchema, ValidatorType } from '../types'; +import _pick from 'lodash/pick'; + +import _get from 'lodash/get'; +import _isEmpty from 'lodash/isEmpty'; +import toPathSchema from './toPathSchema'; +import { NAME_KEY, RJSF_ADDITONAL_PROPERTIES_FLAG } from '../constants'; + +export function getFieldNames(pathSchema: PathSchema, formData?: T): string[][] { + const getAllPaths = (_obj: GenericObjectType, acc: string[][] = [], paths: string[][] = [[]]) => { + Object.keys(_obj).forEach((key: string) => { + if (typeof _obj[key] === 'object') { + const newPaths = paths.map((path) => [...path, key]); + // If an object is marked with additionalProperties, all its keys are valid + if (_obj[key][RJSF_ADDITONAL_PROPERTIES_FLAG] && _obj[key][NAME_KEY] !== '') { + acc.push(_obj[key][NAME_KEY]); + } else { + getAllPaths(_obj[key], acc, newPaths); + } + } else if (key === NAME_KEY && _obj[key] !== '') { + paths.forEach((path) => { + const formValue = _get(formData, path); + // adds path to fieldNames if it points to a value + // or an empty object/array + if ( + typeof formValue !== 'object' || + _isEmpty(formValue) || + (Array.isArray(formValue) && formValue.every((val) => typeof val !== 'object')) + ) { + acc.push(path); + } + }); + } + }); + return acc; + }; + + return getAllPaths(pathSchema); +} + +export function getUsedFormData(formData: T | undefined, fields: string[][]): T | undefined { + // For the case of a single input form + if (fields.length === 0 && typeof formData !== 'object') { + return formData; + } + + // _pick has incorrect type definition, it works with string[][], because lodash/hasIn supports it + const data: GenericObjectType = _pick(formData, fields as unknown as string[]); + if (Array.isArray(formData)) { + return Object.keys(data).map((key: string) => data[key]) as unknown as T; + } + + return data as T; +} + +export default function omitExtraData< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any +>(validator: ValidatorType, schema: S, rootSchema: S = {} as S, formData?: T): T | undefined { + const pathSchema = toPathSchema(validator, schema, '', rootSchema, formData); + const fieldNames = getFieldNames(pathSchema, formData); + + return getUsedFormData(formData, fieldNames); +} diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index 4343be2353..0df891174e 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -1157,4 +1157,6 @@ export interface SchemaUtilsType; + + omitExtraData(schema: S, formData?: T, retrievedSchema?: S): T | undefined; } From 0d2e85afec63463f559716d2e696deef79624f97 Mon Sep 17 00:00:00 2001 From: Marek Bodinger Date: Fri, 22 Mar 2024 19:02:37 +0100 Subject: [PATCH 2/4] Create tests for omitExtraData: - getFieldNames tests are taken from Form.test.jsx and modified - getUsedFormData tests are taken from Form.test.jsx and modified, originally they contained schemas that weren't necessary - some extra tests for omitExtraData are created --- packages/utils/test/schema.test.ts | 2 + packages/utils/test/schema/index.ts | 2 + .../utils/test/schema/omitExtraDataTest.ts | 281 ++++++++++++++++++ 3 files changed, 285 insertions(+) create mode 100644 packages/utils/test/schema/omitExtraDataTest.ts diff --git a/packages/utils/test/schema.test.ts b/packages/utils/test/schema.test.ts index cb2b33f55a..704ca952ff 100644 --- a/packages/utils/test/schema.test.ts +++ b/packages/utils/test/schema.test.ts @@ -12,6 +12,7 @@ import { sanitizeDataForNewSchemaTest, toIdSchemaTest, toPathSchemaTest, + omitExtraDataTest, } from './schema'; const testValidator = getTestValidator({}); @@ -29,3 +30,4 @@ retrieveSchemaTest(testValidator); sanitizeDataForNewSchemaTest(testValidator); toIdSchemaTest(testValidator); toPathSchemaTest(testValidator); +omitExtraDataTest(testValidator); diff --git a/packages/utils/test/schema/index.ts b/packages/utils/test/schema/index.ts index e2997b6abd..bdab811f21 100644 --- a/packages/utils/test/schema/index.ts +++ b/packages/utils/test/schema/index.ts @@ -10,6 +10,7 @@ import retrieveSchemaTest from './retrieveSchemaTest'; import sanitizeDataForNewSchemaTest from './sanitizeDataForNewSchemaTest'; import toIdSchemaTest from './toIdSchemaTest'; import toPathSchemaTest from './toPathSchemaTest'; +import omitExtraDataTest from './omitExtraDataTest'; export * from './types'; @@ -26,4 +27,5 @@ export { sanitizeDataForNewSchemaTest, toIdSchemaTest, toPathSchemaTest, + omitExtraDataTest, }; diff --git a/packages/utils/test/schema/omitExtraDataTest.ts b/packages/utils/test/schema/omitExtraDataTest.ts new file mode 100644 index 0000000000..4cd283a4ac --- /dev/null +++ b/packages/utils/test/schema/omitExtraDataTest.ts @@ -0,0 +1,281 @@ +import { TestValidatorType } from './types'; +import { getFieldNames } from '../../src/schema/omitExtraData'; +import { omitExtraData, PathSchema, RJSFSchema } from '../../lib'; +import { getUsedFormData } from '../../lib/schema/omitExtraData'; + +export default function omitExtraDataTest(testValidator: TestValidatorType) { + describe('omitExtraData()', () => { + describe('getFieldNames()', () => { + it('should return an empty array for a single input form', () => { + const formData = 'foo'; + const pathSchema = { + $name: '', + }; + + expect(getFieldNames(pathSchema as PathSchema, formData)).toEqual([]); + }); + + it('should get field names from pathSchema', () => { + const formData = { + extra: { + foo: 'bar', + }, + level1: { + level2: 'test', + anotherThing: { + anotherThingNested: 'abc', + extra: 'asdf', + anotherThingNested2: 0, + }, + stringArray: ['scobochka'], + }, + level1a: 1.23, + }; + + const pathSchema = { + $name: '', + level1: { + $name: 'level1', + level2: { $name: 'level1.level2' }, + anotherThing: { + $name: 'level1.anotherThing', + anotherThingNested: { + $name: 'level1.anotherThing.anotherThingNested', + }, + anotherThingNested2: { + $name: 'level1.anotherThing.anotherThingNested2', + }, + }, + stringArray: { + $name: 'level1.stringArray', + }, + }, + level1a: { + $name: 'level1a', + }, + }; + + const fieldNames = getFieldNames(pathSchema as unknown as PathSchema, formData); + expect(fieldNames.sort()).toEqual( + [ + ['level1', 'anotherThing', 'anotherThingNested'], + ['level1', 'anotherThing', 'anotherThingNested2'], + ['level1', 'level2'], + ['level1', 'stringArray'], + ['level1a'], + ].sort() + ); + }); + + it('should get field marked as additionalProperties', () => { + const formData = { + extra: { + foo: 'bar', + }, + level1: { + level2: 'test', + extra: 'foo', + mixedMap: { + namedField: 'foo', + key1: 'val1', + }, + }, + level1a: 1.23, + }; + + const pathSchema = { + $name: '', + level1: { + $name: 'level1', + level2: { $name: 'level1.level2' }, + mixedMap: { + $name: 'level1.mixedMap', + __rjsf_additionalProperties: true, + namedField: { $name: 'level1.mixedMap.namedField' }, // this name should not be returned, as the root object paths should be returned for objects marked with additionalProperties + }, + }, + level1a: { + $name: 'level1a', + }, + }; + + const fieldNames = getFieldNames(pathSchema as unknown as PathSchema, formData); + expect(fieldNames.sort()).toEqual([['level1', 'level2'], 'level1.mixedMap', ['level1a']].sort()); + }); + + it('should get field names from pathSchema with array', () => { + const formData = { + address_list: [ + { + street_address: '21, Jump Street', + city: 'Babel', + state: 'Neverland', + }, + { + street_address: '1234 Schema Rd.', + city: 'New York', + state: 'Arizona', + }, + ], + }; + + const pathSchema = { + $name: '', + address_list: { + 0: { + $name: 'address_list.0', + city: { + $name: 'address_list.0.city', + }, + state: { + $name: 'address_list.0.state', + }, + street_address: { + $name: 'address_list.0.street_address', + }, + }, + 1: { + $name: 'address_list.1', + city: { + $name: 'address_list.1.city', + }, + state: { + $name: 'address_list.1.state', + }, + street_address: { + $name: 'address_list.1.street_address', + }, + }, + }, + }; + + const fieldNames = getFieldNames(pathSchema as unknown as PathSchema, formData); + expect(fieldNames.sort()).toEqual( + [ + ['address_list', '0', 'city'], + ['address_list', '0', 'state'], + ['address_list', '0', 'street_address'], + ['address_list', '1', 'city'], + ['address_list', '1', 'state'], + ['address_list', '1', 'street_address'], + ].sort() + ); + }); + }); + + describe('getUsedFormData()', () => { + it('should just return the single input form value', () => { + const formData = 'foo'; + + expect(getUsedFormData(formData, [])).toEqual('foo'); + }); + + it('should return the root level array', () => { + const formData: [] = []; + + expect(getUsedFormData(formData, [])).toEqual([]); + }); + + it('should call getUsedFormData with data from fields in event', () => { + const formData = { + foo: 'bar', + }; + + expect(getUsedFormData(formData, [['foo']])).toEqual({ foo: 'bar' }); + }); + + it('unused form values should be omitted', () => { + const formData = { + foo: 'bar', + baz: 'buzz', + list: [ + { title: 'title0', details: 'details0' }, + { title: 'title1', details: 'details1' }, + ], + }; + + expect(getUsedFormData(formData, [['foo'], ['list', '0', 'title'], ['list', '1', 'details']])).toEqual({ + foo: 'bar', + list: [{ title: 'title0' }, { details: 'details1' }], + }); + }); + }); + + it('should omit fields not defined in the schema', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + foo: { type: 'string' }, + }, + }; + const formData = { + foo: 'bar', + extraField: 'should be omitted', + }; + + expect(omitExtraData(testValidator, schema, schema, formData)).toEqual({ foo: 'bar' }); + }); + + it('should include nested object fields defined in the schema', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + nested: { + type: 'object', + properties: { + foo: { type: 'string' }, + }, + }, + }, + }; + const formData = { + nested: { + foo: 'bar', + extraField: 'should be omitted', + }, + }; + + expect(omitExtraData(testValidator, schema, schema, formData)).toEqual({ nested: { foo: 'bar' } }); + }); + + it('should handle arrays according to the schema', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + list: { + type: 'array', + items: { + type: 'object', + properties: { + foo: { type: 'string' }, + }, + }, + }, + }, + }; + const formData = { + list: [{ foo: 'bar', extraField: 'should be omitted' }, { foo: 'baz' }], + }; + + expect(omitExtraData(testValidator, schema, schema, formData)).toEqual({ + list: [{ foo: 'bar' }, { foo: 'baz' }], + }); + }); + + it('should not omit additional properties if the schema allows them', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + foo: { type: 'string' }, + }, + additionalProperties: true, + }; + const formData = { + foo: 'bar', + extraField: 'should not be omitted', + }; + + expect(omitExtraData(testValidator, schema, schema, formData)).toEqual(formData); + }); + }); +} From 2811a663055acf27e92d4ef1646dcf4634d32bb8 Mon Sep 17 00:00:00 2001 From: Marek Bodinger Date: Fri, 22 Mar 2024 19:09:01 +0100 Subject: [PATCH 3/4] Replace getFieldNames and getUsedFormData with schemaUtils.omitExtraData, remove moved tests, fix relevant tests --- packages/core/src/components/Form.tsx | 75 +----- packages/core/test/Form.test.jsx | 341 +------------------------- 2 files changed, 8 insertions(+), 408 deletions(-) diff --git a/packages/core/src/components/Form.tsx b/packages/core/src/components/Form.tsx index 31752c41e0..4586c716fb 100644 --- a/packages/core/src/components/Form.tsx +++ b/packages/core/src/components/Form.tsx @@ -6,21 +6,17 @@ import { ErrorSchema, ErrorTransformer, FormContextType, - GenericObjectType, getTemplate, getUiOptions, IdSchema, isObject, mergeObjects, - NAME_KEY, - PathSchema, StrictRJSFSchema, Registry, RegistryFieldsType, RegistryWidgetsType, RJSFSchema, RJSFValidationError, - RJSF_ADDITONAL_PROPERTIES_FLAG, SchemaUtilsType, shouldRender, SUBMIT_BTN_OPTIONS_KEY, @@ -34,9 +30,6 @@ import { ValidatorType, Experimental_DefaultFormStateBehavior, } from '@rjsf/utils'; -import _get from 'lodash/get'; -import _isEmpty from 'lodash/isEmpty'; -import _pick from 'lodash/pick'; import _toPath from 'lodash/toPath'; import getDefaultRegistry from '../getDefaultRegistry'; @@ -506,63 +499,6 @@ export default class Form< return null; } - /** Returns the `formData` with only the elements specified in the `fields` list - * - * @param formData - The data for the `Form` - * @param fields - The fields to keep while filtering - */ - getUsedFormData = (formData: T | undefined, fields: string[][]): T | undefined => { - // For the case of a single input form - if (fields.length === 0 && typeof formData !== 'object') { - return formData; - } - - // _pick has incorrect type definition, it works with string[][], because lodash/hasIn supports it - const data: GenericObjectType = _pick(formData, fields as unknown as string[]); - if (Array.isArray(formData)) { - return Object.keys(data).map((key: string) => data[key]) as unknown as T; - } - - return data as T; - }; - - /** Returns the list of field names from inspecting the `pathSchema` as well as using the `formData` - * - * @param pathSchema - The `PathSchema` object for the form - * @param [formData] - The form data to use while checking for empty objects/arrays - */ - getFieldNames = (pathSchema: PathSchema, formData?: T): string[][] => { - const getAllPaths = (_obj: GenericObjectType, acc: string[][] = [], paths: string[][] = [[]]) => { - Object.keys(_obj).forEach((key: string) => { - if (typeof _obj[key] === 'object') { - const newPaths = paths.map((path) => [...path, key]); - // If an object is marked with additionalProperties, all its keys are valid - if (_obj[key][RJSF_ADDITONAL_PROPERTIES_FLAG] && _obj[key][NAME_KEY] !== '') { - acc.push(_obj[key][NAME_KEY]); - } else { - getAllPaths(_obj[key], acc, newPaths); - } - } else if (key === NAME_KEY && _obj[key] !== '') { - paths.forEach((path) => { - const formValue = _get(formData, path); - // adds path to fieldNames if it points to a value - // or an empty object/array - if ( - typeof formValue !== 'object' || - _isEmpty(formValue) || - (Array.isArray(formValue) && formValue.every((val) => typeof val !== 'object')) - ) { - acc.push(path); - } - }); - } - }); - return acc; - }; - - return getAllPaths(pathSchema); - }; - /** Function to handle changes made to a field in the `Form`. This handler receives an entirely new copy of the * `formData` along with a new `ErrorSchema`. It will first update the `formData` with any missing default fields and * then, if `omitExtraData` and `liveOmit` are turned on, the `formData` will be filterer to remove any extra data not @@ -590,11 +526,8 @@ export default class Form< let _retrievedSchema: S | undefined; if (omitExtraData === true && liveOmit === true) { _retrievedSchema = schemaUtils.retrieveSchema(schema, formData); - const pathSchema = schemaUtils.toPathSchema(_retrievedSchema, '', formData); - - const fieldNames = this.getFieldNames(pathSchema, formData); + newFormData = schemaUtils.omitExtraData(_retrievedSchema, formData); - newFormData = this.getUsedFormData(formData, fieldNames); state = { formData: newFormData, }; @@ -702,11 +635,7 @@ export default class Form< if (omitExtraData === true) { const retrievedSchema = schemaUtils.retrieveSchema(schema, newFormData); - const pathSchema = schemaUtils.toPathSchema(retrievedSchema, '', newFormData); - - const fieldNames = this.getFieldNames(pathSchema, newFormData); - - newFormData = this.getUsedFormData(newFormData, fieldNames); + newFormData = schemaUtils.omitExtraData(retrievedSchema, newFormData); } if (noValidate || this.validateForm()) { diff --git a/packages/core/test/Form.test.jsx b/packages/core/test/Form.test.jsx index efd264a191..512743fbd5 100644 --- a/packages/core/test/Form.test.jsx +++ b/packages/core/test/Form.test.jsx @@ -3229,7 +3229,7 @@ describe('Form omitExtraData and liveOmit', () => { sandbox.restore(); }); - it('should call getUsedFormData when the omitExtraData prop is true and liveOmit is true', () => { + it('should call omitExtraData when the omitExtraData prop is true and liveOmit is true', () => { const schema = { type: 'object', properties: { @@ -3252,7 +3252,7 @@ describe('Form omitExtraData and liveOmit', () => { liveOmit, }); - sandbox.stub(comp, 'getUsedFormData').returns({ + sandbox.stub(comp.state.schemaUtils, 'omitExtraData').returns({ foo: '', }); @@ -3260,10 +3260,10 @@ describe('Form omitExtraData and liveOmit', () => { target: { value: 'new' }, }); - sinon.assert.calledOnce(comp.getUsedFormData); + sinon.assert.calledOnce(comp.state.schemaUtils.omitExtraData); }); - it('should not call getUsedFormData when the omitExtraData prop is true and liveOmit is unspecified', () => { + it('should not call omitExtraData when the omitExtraData prop is true and liveOmit is unspecified', () => { const schema = { type: 'object', properties: { @@ -3284,7 +3284,7 @@ describe('Form omitExtraData and liveOmit', () => { omitExtraData, }); - sandbox.stub(comp, 'getUsedFormData').returns({ + sandbox.stub(comp.state.schemaUtils, 'omitExtraData').returns({ foo: '', }); @@ -3292,336 +3292,7 @@ describe('Form omitExtraData and liveOmit', () => { target: { value: 'new' }, }); - sinon.assert.notCalled(comp.getUsedFormData); - }); - - describe('getUsedFormData', () => { - it('should call getUsedFormData when the omitExtraData prop is true', () => { - const schema = { - type: 'object', - properties: { - foo: { - type: 'string', - }, - }, - }; - const formData = { - foo: '', - }; - const onSubmit = sandbox.spy(); - const onError = sandbox.spy(); - const omitExtraData = true; - const { comp, node } = createFormComponent({ - schema, - formData, - onSubmit, - onError, - omitExtraData, - }); - - sandbox.stub(comp, 'getUsedFormData').returns({ - foo: '', - }); - - Simulate.submit(node); - - sinon.assert.calledOnce(comp.getUsedFormData); - }); - it('should just return the single input form value', () => { - const schema = { - title: 'A single-field form', - type: 'string', - }; - const formData = 'foo'; - const onSubmit = sandbox.spy(); - const { comp } = createFormComponent({ - schema, - formData, - onSubmit, - }); - - const result = comp.getUsedFormData(formData, []); - expect(result).eql('foo'); - }); - - it('should return the root level array', () => { - const schema = { - type: 'array', - items: { - type: 'string', - }, - }; - const formData = []; - const onSubmit = sandbox.spy(); - const { comp } = createFormComponent({ - schema, - formData, - onSubmit, - }); - - const result = comp.getUsedFormData(formData, []); - expect(result).eql([]); - }); - - it('should call getUsedFormData with data from fields in event', () => { - const schema = { - type: 'object', - properties: { - foo: { type: 'string' }, - }, - }; - const formData = { - foo: 'bar', - }; - const onSubmit = sandbox.spy(); - const { comp } = createFormComponent({ - schema, - formData, - onSubmit, - }); - - const result = comp.getUsedFormData(formData, ['foo']); - expect(result).eql({ foo: 'bar' }); - }); - - it('unused form values should be omitted', () => { - const schema = { - type: 'object', - properties: { - foo: { type: 'string' }, - baz: { type: 'string' }, - list: { - type: 'array', - items: { - type: 'object', - properties: { - title: { type: 'string' }, - details: { type: 'string' }, - }, - }, - }, - }, - }; - - const formData = { - foo: 'bar', - baz: 'buzz', - list: [ - { title: 'title0', details: 'details0' }, - { title: 'title1', details: 'details1' }, - ], - }; - const onSubmit = sandbox.spy(); - const { comp } = createFormComponent({ - schema, - formData, - onSubmit, - }); - - const result = comp.getUsedFormData(formData, ['foo', 'list.0.title', 'list.1.details']); - expect(result).eql({ - foo: 'bar', - list: [{ title: 'title0' }, { details: 'details1' }], - }); - }); - }); - - describe('getFieldNames()', () => { - it('should return an empty array for a single input form', () => { - const schema = { - type: 'string', - }; - - const formData = 'foo'; - - const onSubmit = sandbox.spy(); - const { comp } = createFormComponent({ - schema, - formData, - onSubmit, - }); - - const pathSchema = { - $name: '', - }; - - const fieldNames = comp.getFieldNames(pathSchema, formData); - expect(fieldNames).eql([]); - }); - - it('should get field names from pathSchema', () => { - const schema = {}; - - const formData = { - extra: { - foo: 'bar', - }, - level1: { - level2: 'test', - anotherThing: { - anotherThingNested: 'abc', - extra: 'asdf', - anotherThingNested2: 0, - }, - stringArray: ['scobochka'], - }, - level1a: 1.23, - }; - - const onSubmit = sandbox.spy(); - const { comp } = createFormComponent({ - schema, - formData, - onSubmit, - }); - - const pathSchema = { - $name: '', - level1: { - $name: 'level1', - level2: { $name: 'level1.level2' }, - anotherThing: { - $name: 'level1.anotherThing', - anotherThingNested: { - $name: 'level1.anotherThing.anotherThingNested', - }, - anotherThingNested2: { - $name: 'level1.anotherThing.anotherThingNested2', - }, - }, - stringArray: { - $name: 'level1.stringArray', - }, - }, - level1a: { - $name: 'level1a', - }, - }; - - const fieldNames = comp.getFieldNames(pathSchema, formData); - expect(fieldNames.sort()).eql( - [ - ['level1', 'anotherThing', 'anotherThingNested'], - ['level1', 'anotherThing', 'anotherThingNested2'], - ['level1', 'level2'], - ['level1', 'stringArray'], - ['level1a'], - ].sort() - ); - }); - - it('should get field marked as additionalProperties', () => { - const schema = {}; - - const formData = { - extra: { - foo: 'bar', - }, - level1: { - level2: 'test', - extra: 'foo', - mixedMap: { - namedField: 'foo', - key1: 'val1', - }, - }, - level1a: 1.23, - }; - - const onSubmit = sandbox.spy(); - const { comp } = createFormComponent({ - schema, - formData, - onSubmit, - }); - - const pathSchema = { - $name: '', - level1: { - $name: 'level1', - level2: { $name: 'level1.level2' }, - mixedMap: { - $name: 'level1.mixedMap', - __rjsf_additionalProperties: true, - namedField: { $name: 'level1.mixedMap.namedField' }, // this name should not be returned, as the root object paths should be returned for objects marked with additionalProperties - }, - }, - level1a: { - $name: 'level1a', - }, - }; - - const fieldNames = comp.getFieldNames(pathSchema, formData); - expect(fieldNames.sort()).eql([['level1', 'level2'], 'level1.mixedMap', ['level1a']].sort()); - }); - - it('should get field names from pathSchema with array', () => { - const schema = {}; - - const formData = { - address_list: [ - { - street_address: '21, Jump Street', - city: 'Babel', - state: 'Neverland', - }, - { - street_address: '1234 Schema Rd.', - city: 'New York', - state: 'Arizona', - }, - ], - }; - - const onSubmit = sandbox.spy(); - const { comp } = createFormComponent({ - schema, - formData, - onSubmit, - }); - - const pathSchema = { - $name: '', - address_list: { - 0: { - $name: 'address_list.0', - city: { - $name: 'address_list.0.city', - }, - state: { - $name: 'address_list.0.state', - }, - street_address: { - $name: 'address_list.0.street_address', - }, - }, - 1: { - $name: 'address_list.1', - city: { - $name: 'address_list.1.city', - }, - state: { - $name: 'address_list.1.state', - }, - street_address: { - $name: 'address_list.1.street_address', - }, - }, - }, - }; - - const fieldNames = comp.getFieldNames(pathSchema, formData); - expect(fieldNames.sort()).eql( - [ - ['address_list', '0', 'city'], - ['address_list', '0', 'state'], - ['address_list', '0', 'street_address'], - ['address_list', '1', 'city'], - ['address_list', '1', 'state'], - ['address_list', '1', 'street_address'], - ].sort() - ); - }); + sinon.assert.notCalled(comp.state.schemaUtils.omitExtraData); }); it('should not omit data on change with omitExtraData=false and liveOmit=false', () => { From 0bc1c115a57c5a04e50fedf062a306370548fee0 Mon Sep 17 00:00:00 2001 From: Marek Bodinger Date: Fri, 29 Mar 2024 12:02:37 +0100 Subject: [PATCH 4/4] Add documentation --- .../docs/api-reference/utility-functions.md | 61 +++++++++++++++++++ packages/utils/src/createSchemaUtils.ts | 9 +++ packages/utils/src/schema/omitExtraData.ts | 24 ++++++++ 3 files changed, 94 insertions(+) diff --git a/packages/docs/docs/api-reference/utility-functions.md b/packages/docs/docs/api-reference/utility-functions.md index fdee1aa0f6..81c75649a9 100644 --- a/packages/docs/docs/api-reference/utility-functions.md +++ b/packages/docs/docs/api-reference/utility-functions.md @@ -1080,6 +1080,67 @@ Generates an `PathSchema` object for the `schema`, recursively - PathSchema<T> - The `PathSchema` object for the `schema` +### omitExtraData() + +The function takes a schema and formData and returns a copy of the formData with any fields not defined in the schema removed. This is useful for ensuring that only data that is relevant to the schema is preserved. Objects with `additionalProperties` keyword set to `true` will not have their extra fields removed. + +```ts +const schema = { + type: 'object', + properties: { + child1: { + type: 'object', + properties: { + name: { type: 'string' }, + }, + }, + child2: { + type: 'object', + properties: { + name: { type: 'string' }, + }, + additionalProperties: true, + }, + }, +}; + +const formData = { + child1: { + name: 'John Doe', + extraField: 'This should be removed', + }, + child2: { + name: 'Jane Doe', + extraField: 'This should NOT be removed', + }, +}; + +const filteredFormData = omitExtraData(validator, schema, schema, formData); +console.log(filteredFormData); +/* +{ + child1: { + name: "John Doe", + }, + child2: { + name: "Jane Doe", + extraField: "This should NOT be removed", + }, +} +*/ +``` + +#### Parameters + +- validator: ValidatorType - An implementation of the `ValidatorType` interface that will be used when necessary +- schema: S - The schema to use for filtering the formData +- [rootSchema]: S | undefined - The root schema, used to primarily to look up `$ref`s +- [formData]: T | undefined - The formData to filter + +#### Returns + +- T: The new form data, with any fields not defined in the schema removed + ## Schema utils creation function ### createSchemaUtils() diff --git a/packages/utils/src/createSchemaUtils.ts b/packages/utils/src/createSchemaUtils.ts index b8120cdc88..c407bede92 100644 --- a/packages/utils/src/createSchemaUtils.ts +++ b/packages/utils/src/createSchemaUtils.ts @@ -277,6 +277,15 @@ class SchemaUtils(this.validator, schema, name, this.rootSchema, formData); } + /** + * The function takes a `schema` and `formData` and returns a copy of the formData with any fields not defined in the schema removed. + * This is useful for ensuring that only data that is relevant to the schema is preserved. Objects with `additionalProperties` + * keyword set to `true` will not have their extra fields removed. + * + * @param schema - The schema to use for filtering the `formData` + * @param [formData] - The formData to filter + * @returns The new form data, with any fields not defined in the schema removed + */ omitExtraData(schema: S, formData?: T): T | undefined { return omitExtraData(this.validator, schema, this.rootSchema, formData); } diff --git a/packages/utils/src/schema/omitExtraData.ts b/packages/utils/src/schema/omitExtraData.ts index b50ba5a44f..aab70a219b 100644 --- a/packages/utils/src/schema/omitExtraData.ts +++ b/packages/utils/src/schema/omitExtraData.ts @@ -6,6 +6,13 @@ import _isEmpty from 'lodash/isEmpty'; import toPathSchema from './toPathSchema'; import { NAME_KEY, RJSF_ADDITONAL_PROPERTIES_FLAG } from '../constants'; +/** + * A helper function that extracts all the existing paths from the given `pathSchema` and `formData`. + * + * @param pathSchema - The `PathSchema` from which to extract the field names + * @param [formData] - The formData to use to determine which fields are valid + * @returns A list of paths represented as string arrays + */ export function getFieldNames(pathSchema: PathSchema, formData?: T): string[][] { const getAllPaths = (_obj: GenericObjectType, acc: string[][] = [], paths: string[][] = [[]]) => { Object.keys(_obj).forEach((key: string) => { @@ -38,6 +45,12 @@ export function getFieldNames(pathSchema: PathSchema, formData?: T): return getAllPaths(pathSchema); } +/** + * A helper function to keep only the fields in the `formData` that are defined in the `fields` array. + * + * @param formData - The formData to extract the fields from + * @param fields - The fields to keep in the `formData` + */ export function getUsedFormData(formData: T | undefined, fields: string[][]): T | undefined { // For the case of a single input form if (fields.length === 0 && typeof formData !== 'object') { @@ -53,6 +66,17 @@ export function getUsedFormData(formData: T | undefined, fields: string return data as T; } +/** + * The function takes a `schema` and `formData` and returns a copy of the formData with any fields not defined in the schema removed. + * This is useful for ensuring that only data that is relevant to the schema is preserved. Objects with `additionalProperties` + * keyword set to `true` will not have their extra fields removed. + * + * @param validator - An implementation of the `ValidatorType` interface that will be used when necessary + * @param schema - The schema to use for filtering the formData + * @param [rootSchema] - The root schema, used to primarily to look up `$ref`s + * @param [formData] - The formData to filter + * @returns The new form data, with any fields not defined in the schema removed + */ export default function omitExtraData< T = any, S extends StrictRJSFSchema = RJSFSchema,