From ec7e53dbd5cc42302f4ab749595d74835a540902 Mon Sep 17 00:00:00 2001 From: Wojciech Date: Sun, 27 Sep 2020 11:56:32 +0200 Subject: [PATCH] feat: add flat module --- .cspell.json | 2 +- .../src/nested/value-trigger.component.tsx | 4 +- src/backend/adapters/record/base-record.ts | 20 +-- .../decorators/property/property-decorator.ts | 2 +- .../utils/decorate-properties.spec.ts | 18 ++- .../resource/utils/decorate-properties.ts | 16 ++- .../utils/populator/populate-property.spec.ts | 43 +++++-- .../utils/populator/populate-property.ts | 29 ++++- src/frontend/bundle-entry.jsx | 4 +- .../docs/on-property-change.doc.md | 1 - .../hooks/use-record/update-record.ts | 26 +--- src/utils/flat/constants.ts | 6 + src/utils/flat/filter-params.ts | 21 +++ src/utils/flat/flat-module.ts | 42 ++++++ src/utils/flat/flat.types.ts | 22 ++++ src/utils/flat/get.spec.ts | 56 ++++++++ src/utils/flat/get.ts | 32 +++++ src/utils/flat/index.ts | 2 + src/utils/flat/property-key-regex.ts | 7 + src/utils/flat/set.spec.ts | 121 ++++++++++++++++++ src/utils/flat/set.ts | 83 ++++++++++++ src/utils/index.ts | 1 + 22 files changed, 481 insertions(+), 77 deletions(-) create mode 100644 src/utils/flat/constants.ts create mode 100644 src/utils/flat/filter-params.ts create mode 100644 src/utils/flat/flat-module.ts create mode 100644 src/utils/flat/flat.types.ts create mode 100644 src/utils/flat/get.spec.ts create mode 100644 src/utils/flat/get.ts create mode 100644 src/utils/flat/index.ts create mode 100644 src/utils/flat/property-key-regex.ts create mode 100644 src/utils/flat/set.spec.ts create mode 100644 src/utils/flat/set.ts diff --git a/.cspell.json b/.cspell.json index 43633f152..e77a36fe2 100644 --- a/.cspell.json +++ b/.cspell.json @@ -7,7 +7,7 @@ "codecov", "bulma", "unmount", "testid", "woff", "iife", "sourcemap", "Roboto", "camelize", "datepicker", "camelcase", "fullwidth", "wysiwig", "Helvetica", "Neue", "Arial", "nowrap", "textfield", "scrollable", "flexbox", "treal", "xxxl", - "adminbro", "Checkmark", "overridable", "Postgres", "Hana" + "adminbro", "Checkmark", "overridable", "Postgres", "Hana", "Wojtek", "Krysiak" ], "ignorePaths": [ "src/frontend/assets/**/*" diff --git a/example-app/src/nested/value-trigger.component.tsx b/example-app/src/nested/value-trigger.component.tsx index 95055a751..f2419220b 100644 --- a/example-app/src/nested/value-trigger.component.tsx +++ b/example-app/src/nested/value-trigger.component.tsx @@ -1,12 +1,12 @@ import React from 'react' -import { EditPropertyProps, unflatten } from 'admin-bro' +import { EditPropertyProps, flat } from 'admin-bro' import { Button, Box } from '@admin-bro/design-system' const ValueTrigger: React.FC = (props) => { const { onChange, record } = props // eslint-disable-next-line @typescript-eslint/no-unused-vars - const data = unflatten(record.params) + const data = flat.unflatten(record.params) const handleClick = (): void => { onChange({ diff --git a/src/backend/adapters/record/base-record.ts b/src/backend/adapters/record/base-record.ts index 029f7dd9f..a27b4b26b 100644 --- a/src/backend/adapters/record/base-record.ts +++ b/src/backend/adapters/record/base-record.ts @@ -1,5 +1,5 @@ -import * as flat from 'flat' import _ from 'lodash' +import { flat } from '../../../utils/flat' import { ParamsType } from './params.type' import BaseResource from '../resource/base-resource' import ValidationError, { RecordError, PropertyErrors } from '../../utils/errors/validation-error' @@ -51,30 +51,18 @@ class BaseRecord { * @return {any} value for given field */ param(path: string): any { - if (this.params && this.params[path]) { - return this.params[path] - } - const subParams = this.namespaceParams(path) - if (subParams) { - const unflattenSubParams = flat.unflatten(subParams) as Record - return path.split('.').reduce((m, v) => m[v], unflattenSubParams) - } - return undefined + return flat.get(this.params, path) } /** * Returns object containing all params keys starting with prefix + * * @param {string} prefix * * @return {object | undefined} */ namespaceParams(prefix: string): Record | void { - const regex = new RegExp(`^${prefix}`) - const keys = Object.keys(this.params).filter(key => key.match(regex)) - if (keys.length) { - return keys.reduce((memo, key) => ({ ...memo, [key]: this.params[key] }), {}) - } - return undefined + return flat.filterParams(this.params, prefix) } /** diff --git a/src/backend/decorators/property/property-decorator.ts b/src/backend/decorators/property/property-decorator.ts index bb7d1ddbb..3e5fdcff2 100644 --- a/src/backend/decorators/property/property-decorator.ts +++ b/src/backend/decorators/property/property-decorator.ts @@ -115,7 +115,7 @@ class PropertyDecorator { * @returns {PropertyType} */ type(): PropertyType { - if (this.options.reference) { + if (typeof this.options.reference === 'string') { return 'reference' } return overrideFromOptions('type', this.property, this.options) as PropertyType diff --git a/src/backend/decorators/resource/utils/decorate-properties.spec.ts b/src/backend/decorators/resource/utils/decorate-properties.spec.ts index ae16288ba..cbae8184a 100644 --- a/src/backend/decorators/resource/utils/decorate-properties.spec.ts +++ b/src/backend/decorators/resource/utils/decorate-properties.spec.ts @@ -28,12 +28,14 @@ describe('decorateProperties', () => { context('One property with options', () => { let decoratedProperties: DecoratedProperties - const newType = 'boolean' + const isSortable = true + const newIsSortable = false + const type = 'boolean' beforeEach(() => { - property = new BaseProperty({ path, type: 'string' }) + property = new BaseProperty({ path, type, isSortable }) resource.properties.returns([property]) - decorator.options = { properties: { [path]: { type: newType } } } + decorator.options = { properties: { [path]: { isSortable: newIsSortable } } } decoratedProperties = decorateProperties(resource, admin, decorator) }) @@ -43,10 +45,16 @@ describe('decorateProperties', () => { expect(decoratedProperties[path]).not.to.be.undefined }) - it('decorates it that the type is updated', () => { + it('decorates it that the isSortable is updated', () => { const decorated = decoratedProperties[path] - expect(decorated.type()).to.eq(newType) + expect(decorated.isSortable()).to.eq(newIsSortable) + }) + + it('leaves all other fields like type unchanged', () => { + const decorated = decoratedProperties[path] + + expect(decorated.type()).to.eq(type) }) }) diff --git a/src/backend/decorators/resource/utils/decorate-properties.ts b/src/backend/decorators/resource/utils/decorate-properties.ts index 4f25b10b9..d80649a0a 100644 --- a/src/backend/decorators/resource/utils/decorate-properties.ts +++ b/src/backend/decorators/resource/utils/decorate-properties.ts @@ -33,13 +33,15 @@ export function decorateProperties( // decorate all properties user gave in options but they don't exist in the resource if (options.properties) { Object.keys(options.properties).forEach((key) => { - const property = new BaseProperty({ path: key, isSortable: false }) - properties[key] = new PropertyDecorator({ - property, - admin, - options: options.properties && options.properties[key], - resource: decorator, - }) + if (!properties[key]) { // checking if property hasn't been decorated yet + const property = new BaseProperty({ path: key, isSortable: false }) + properties[key] = new PropertyDecorator({ + property, + admin, + options: options.properties && options.properties[key], + resource: decorator, + }) + } }) } return properties diff --git a/src/backend/utils/populator/populate-property.spec.ts b/src/backend/utils/populator/populate-property.spec.ts index f749195e2..7ba43b2ed 100644 --- a/src/backend/utils/populator/populate-property.spec.ts +++ b/src/backend/utils/populator/populate-property.spec.ts @@ -2,40 +2,41 @@ import { expect } from 'chai' import sinon, { SinonStubbedInstance } from 'sinon' import { BaseProperty, BaseRecord, BaseResource } from '../../adapters' -import { ResourceDecorator } from '../../decorators' -import { populateProperty, PopulatorNarrowedProperty } from './populate-property' +import { PropertyDecorator, ResourceDecorator } from '../../decorators' +import { populateProperty } from './populate-property' describe('populateProperty', () => { const userId = '1234' const path = 'userId' - let property: PopulatorNarrowedProperty let resourceDecorator: SinonStubbedInstance let referenceResource: SinonStubbedInstance let record: SinonStubbedInstance let userRecord: SinonStubbedInstance + let property: SinonStubbedInstance & PropertyDecorator + let populatedResponse: Array | null beforeEach(() => { resourceDecorator = sinon.createStubInstance(ResourceDecorator) referenceResource = sinon.createStubInstance(BaseResource) record = sinon.createStubInstance(BaseRecord) userRecord = sinon.createStubInstance(BaseRecord) + property = sinon.createStubInstance(PropertyDecorator) as typeof property + property.resource.returns(resourceDecorator as unknown as ResourceDecorator) + property.reference.returns(referenceResource as unknown as BaseResource) + property.property = { reference: 'someRawReference' } as unknown as BaseProperty + property.path = path + }) - property = { - resource: () => resourceDecorator as unknown as ResourceDecorator, - reference: () => referenceResource as unknown as BaseResource, - property: { reference: 'someRawReference' } as unknown as BaseProperty, - path, - } + afterEach(() => { + sinon.restore() }) it('returns empty array when no records are given', async () => { expect(await populateProperty([], property)).to.deep.eq([]) }) - context('2 same records having with reference key', () => { - let populatedResponse: Array | null - + context('2 same records with reference key', () => { beforeEach(async () => { record.param.returns(userId) userRecord.id.returns(userId) @@ -58,4 +59,22 @@ describe('populateProperty', () => { expect(populatedRecord?.populate).to.have.been.calledWith(path, userRecord) }) }) + + context('record with array property being also a reference', () => { + const [userId1, userId2] = ['user1', 'user2'] + + beforeEach(async () => { + record.param.returns([userId1, userId2]) + // resourceDecorator + userRecord.id.returns(userId) + property.isArray.returns(true) + referenceResource.findMany.resolves([userRecord]) + + populatedResponse = await populateProperty([record, record], property) + }) + + it('properly finds references in arrays', () => { + expect(referenceResource.findMany).to.have.been.calledOnceWith([userId1, userId2]) + }) + }) }) diff --git a/src/backend/utils/populator/populate-property.ts b/src/backend/utils/populator/populate-property.ts index 950ae4f1d..82531dc8e 100644 --- a/src/backend/utils/populator/populate-property.ts +++ b/src/backend/utils/populator/populate-property.ts @@ -1,11 +1,7 @@ +import { DELIMITER } from '../../../utils/flat/constants' import { BaseRecord } from '../../adapters' import PropertyDecorator from '../../decorators/property/property-decorator' -export type PopulatorNarrowedProperty = Pick< - InstanceType, - 'resource' | 'reference' | 'property' | 'path' -> - /** * It populates one property in given records * @@ -14,7 +10,7 @@ export type PopulatorNarrowedProperty = Pick< */ export const populateProperty = async ( records: Array | null, - property: PopulatorNarrowedProperty, + property: PropertyDecorator, ): Promise | null> => { const decoratedResource = property.resource() @@ -45,6 +41,13 @@ export const populateProperty = async ( if (!foreignKeyValue) { return memo } + // array properties returns arrays so we have to take the all into consideration + if (property.isArray()) { + return foreignKeyValue.reduce((arrayMemo, valueInArray) => ({ + ...arrayMemo, + [valueInArray]: null, + }), memo) + } return { ...memo, [foreignKeyValue]: null, @@ -76,8 +79,20 @@ export const populateProperty = async ( return records.map((record) => { // we set record.populated['userId'] = externalIdsMap[record.param('userId)] + // but this can also be an array - we have to check it const foreignKeyValue = record.param(property.path) - record.populate(property.path, externalIdsMap[foreignKeyValue]) + + if (Array.isArray(foreignKeyValue)) { + foreignKeyValue.forEach((foreignKeyValueItem, index) => { + record.populate( + [property.path, index].join(DELIMITER), + externalIdsMap[foreignKeyValueItem], + ) + }) + } else { + record.populate(property.path, externalIdsMap[foreignKeyValue]) + } + return record }) } diff --git a/src/frontend/bundle-entry.jsx b/src/frontend/bundle-entry.jsx index e171aba6a..969be5ded 100644 --- a/src/frontend/bundle-entry.jsx +++ b/src/frontend/bundle-entry.jsx @@ -4,7 +4,6 @@ import { BrowserRouter } from 'react-router-dom' import { ThemeProvider } from 'styled-components' import { initReactI18next } from 'react-i18next' import i18n from 'i18next' -import flat from 'flat' import App from './components/application' import BasePropertyComponent from './components/property-type' @@ -14,6 +13,7 @@ import * as AppComponents from './components/app' import * as Hooks from './hooks' import ApiClient from './utils/api-client' import withNotice from './hoc/with-notice' +import { flat } from '../utils/flat' const env = { NODE_ENV: process.env.NODE_ENV || 'development', @@ -58,6 +58,8 @@ export default { env, ...AppComponents, ...Hooks, + flat, + // TODO: remove this from the next release flatten: flat.flatten, unflatten: flat.unflatten, } diff --git a/src/frontend/components/property-type/docs/on-property-change.doc.md b/src/frontend/components/property-type/docs/on-property-change.doc.md index 1c2cacf7e..3976993f7 100644 --- a/src/frontend/components/property-type/docs/on-property-change.doc.md +++ b/src/frontend/components/property-type/docs/on-property-change.doc.md @@ -13,7 +13,6 @@ property: `name`. ```javascript import React from 'react' - import { unflatten } from 'admin-bro' import { Button, Box } from '@admin-bro/design-system' const ValueTrigger = (props) => { diff --git a/src/frontend/hooks/use-record/update-record.ts b/src/frontend/hooks/use-record/update-record.ts index a09f3a1d2..c404a7299 100644 --- a/src/frontend/hooks/use-record/update-record.ts +++ b/src/frontend/hooks/use-record/update-record.ts @@ -1,4 +1,4 @@ -import flat from 'flat' +import { flat } from '../../../utils/flat' import { RecordJSON } from '../../interfaces' /** @@ -34,35 +34,13 @@ const updateRecord = ( ) => (previousRecord: RecordJSON): RecordJSON => { let populatedModified = false const populatedCopy = { ...previousRecord.populated } - const paramsCopy = { ...previousRecord.params } + const paramsCopy = flat.set(previousRecord.params, property, value) - // clear previous value - Object.keys(paramsCopy) - .filter(key => key === property || key.startsWith(`${property}.`)) - .forEach(k => delete paramsCopy[k]) if (property in populatedCopy) { delete populatedCopy[property] populatedModified = true } - // set new value - if (typeof value !== 'undefined') { - if (typeof value === 'object' && !(value instanceof File) && value !== null) { - const flattened = flat.flatten(value) as any - if (Object.keys(flattened).length) { - Object.keys(flattened).forEach((key) => { - paramsCopy[`${property}.${key}`] = flattened[key] - }) - } else if (Array.isArray(value)) { - paramsCopy[property] = [] - } else { - paramsCopy[property] = {} - } - } else { - paramsCopy[property] = value - } - } - if (selectedRecord) { populatedCopy[property] = selectedRecord populatedModified = true diff --git a/src/utils/flat/constants.ts b/src/utils/flat/constants.ts new file mode 100644 index 000000000..891e52f36 --- /dev/null +++ b/src/utils/flat/constants.ts @@ -0,0 +1,6 @@ +/** + * + * @memberof module:flat + * @new + */ +export const DELIMITER = '.' diff --git a/src/utils/flat/filter-params.ts b/src/utils/flat/filter-params.ts new file mode 100644 index 000000000..666f330a0 --- /dev/null +++ b/src/utils/flat/filter-params.ts @@ -0,0 +1,21 @@ +import { propertyKeyRegex } from './property-key-regex' +import { FlattenParams } from '.' + +/** + * + * @memberof module:flat + * @param {FlattenParams} params + * @param {string} property + * @new + */ +export const filterParams = (params: FlattenParams, property: string): FlattenParams => { + const regex = propertyKeyRegex(property) + + // filter all keys which starts with property + return Object.keys(params) + .filter(key => key.match(regex)) + .reduce((memo, key) => ({ + ...memo, + [key]: (params[key] as string), + }), {} as FlattenParams) +} diff --git a/src/utils/flat/flat-module.ts b/src/utils/flat/flat-module.ts new file mode 100644 index 000000000..4b0d69f70 --- /dev/null +++ b/src/utils/flat/flat-module.ts @@ -0,0 +1,42 @@ +import { flatten, unflatten } from 'flat' + +import { DELIMITER } from './constants' +import { filterParams } from './filter-params' +import { set } from './set' +import { get } from './get' + +/** + * + * @memberof module:flat + */ +export type FlatModuleType = { + flatten: typeof flatten; + unflatten: typeof unflatten; + set: typeof set; + get: typeof get; + filterParams: typeof filterParams; + DELIMITER: typeof DELIMITER; +} + +/** + * All the data in records are stored in flatten version. + * + * Helpers gathered in this module will help you manage them + * + * @module flat + * @new + */ +export const flat: FlatModuleType = { + /** + * Raw `flatten` function exported from original `flat` package. + */ + flatten, + /** + * Raw `unflatten` function exported from original `flat` package. + */ + unflatten, + set, + get, + filterParams, + DELIMITER, +} diff --git a/src/utils/flat/flat.types.ts b/src/utils/flat/flat.types.ts new file mode 100644 index 000000000..42c9957c3 --- /dev/null +++ b/src/utils/flat/flat.types.ts @@ -0,0 +1,22 @@ + +/** + * Available fo flatten value + * + * @memberof module:flat + */ +export type FlattenValue = 'string' | + 'boolean' | + 'number' | + null | + [] | + {} | + File + +/** + * Type of flatten params + * + * @memberof module:flat + */ +export type FlattenParams = { + [key: string]: FlattenValue; +} diff --git a/src/utils/flat/get.spec.ts b/src/utils/flat/get.spec.ts new file mode 100644 index 000000000..9246c1758 --- /dev/null +++ b/src/utils/flat/get.spec.ts @@ -0,0 +1,56 @@ +import { expect } from 'chai' +import { FlattenParams } from '../flat' +import { get } from './get' + +describe('module:flat.get', () => { + let params: FlattenParams + + beforeEach(() => { + params = { + name: 'Wojtek', + surname: 'Krysiak', + age: 36, + 'interest.OfMe.0': 'javascript', + 'interest.OfMe.1': 'typescript', + 'interest.OfMe.2': 'brainTumor', + interests: 'Generally everything', + 'meta.position': 'CTO', + 'meta.workingHours': '9:00-17:00', + 'meta.duties': 'everything', + 'meta.fun': '8/10', + nulled: null, + emptyArray: [], + emptyObject: {}, + } + }) + + it('returns regular string', () => { + expect(get(params, 'name')).to.eq(params.name) + }) + + it('returns undefined for non existing property', () => { + expect(get(params, 'nameNotExisting')).to.be.undefined + }) + + it('returns nested array', () => { + expect(get(params, 'interest.OfMe')).to.deep.equal([ + 'javascript', + 'typescript', + 'brainTumor', + ]) + }) + + it('returns object with nested array', () => { + expect(get(params, 'interest')).to.deep.equal({ + OfMe: [ + 'javascript', + 'typescript', + 'brainTumor', + ], + }) + }) + + it('returns undefined when not exact property is given', () => { + expect(get(params, 'interest.Of')).to.be.undefined + }) +}) diff --git a/src/utils/flat/get.ts b/src/utils/flat/get.ts new file mode 100644 index 000000000..83e963033 --- /dev/null +++ b/src/utils/flat/get.ts @@ -0,0 +1,32 @@ +import { unflatten } from 'flat' +import { DELIMITER } from './constants' +import { filterParams } from './filter-params' +import { FlattenParams } from '../flat' +import { propertyKeyRegex } from './property-key-regex' + +const TEMP_HOLDING_KEY = 'TEMP_HOLDING_KEY' + +/** + * + * @memberof module:flat + * @param {FlattenParams} params + * @param {string} property + */ +export const get = (params: FlattenParams = {}, property: string): any => { + if (params[property]) { + return params[property] + } + + const regex = propertyKeyRegex(property) + const filteredParams = filterParams(params, property) + + const nestedProperties = Object.keys(filteredParams).reduce((memo, key) => ({ + ...memo, + [key.replace(regex, `${TEMP_HOLDING_KEY}${DELIMITER}`)]: filteredParams[key], + }), {} as FlattenParams) + + if (Object.keys(nestedProperties).length) { + return (unflatten(nestedProperties) as {})[TEMP_HOLDING_KEY] + } + return undefined +} diff --git a/src/utils/flat/index.ts b/src/utils/flat/index.ts new file mode 100644 index 000000000..db9fb5dc2 --- /dev/null +++ b/src/utils/flat/index.ts @@ -0,0 +1,2 @@ +export * from './flat-module' +export * from './flat.types' diff --git a/src/utils/flat/property-key-regex.ts b/src/utils/flat/property-key-regex.ts new file mode 100644 index 000000000..f96c0d052 --- /dev/null +++ b/src/utils/flat/property-key-regex.ts @@ -0,0 +1,7 @@ +import { DELIMITER } from './constants' +// this is the regex used to find all existing properties starting with a key + +export const propertyKeyRegex = (property: string): RegExp => { + const escapedDelimiter = `\\${DELIMITER}` + return new RegExp(`^${property.replace(DELIMITER, escapedDelimiter)}($|${escapedDelimiter})`) +} diff --git a/src/utils/flat/set.spec.ts b/src/utils/flat/set.spec.ts new file mode 100644 index 000000000..fae230354 --- /dev/null +++ b/src/utils/flat/set.spec.ts @@ -0,0 +1,121 @@ +import { expect } from 'chai' + +import { FlattenParams } from './flat.types' +import { set } from './set' + +describe('module:flat.set', () => { + let params: FlattenParams + let newParams: FlattenParams + + beforeEach(() => { + params = { + name: 'Wojtek', + surname: 'Krysiak', + age: 36, + 'interest.OfMe.0': 'javascript', + 'interest.OfMe.1': 'typescript', + 'interest.OfMe.2': 'brainTumor', + interests: 'Generally everything', + 'meta.position': 'CTO', + 'meta.workingHours': '9:00-17:00', + 'meta.duties': 'everything', + 'meta.fun': '8/10', + } + }) + + it('sets regular property when it is default type', () => { + const age = 37 + + expect(set(params, 'age', age)).to.have.property('age', 37) + }) + + context('passing basic types', () => { + const newPropertyName = 'newProperty' + + it('does not change the record when regular file is set', function () { + const file = new File([], 'amazing.me') + + newParams = set(params, newPropertyName, file) + + expect(newParams[newPropertyName]).to.equal(file) + }) + + it('sets null', () => { + expect(set(params, newPropertyName, null)).to.have.property(newPropertyName, null) + }) + + it('sets empty object', () => { + expect(set(params, newPropertyName, {})).to.deep.include({ [newPropertyName]: {} }) + }) + + it('sets empty array', () => { + expect(set(params, newPropertyName, [])).to.deep.include({ [newPropertyName]: [] }) + }) + }) + + context('passing array', () => { + const interest = ['js', 'ts'] + + beforeEach(() => { + newParams = set(params, 'interest.OfMe', interest) + }) + + it('replaces sets values for all new arrays items', () => { + expect(newParams).to.include({ + 'interest.OfMe.0': 'js', + 'interest.OfMe.1': 'ts', + }) + }) + + it('removes old values', () => { + expect(newParams).not.to.have.property('interest.OfMe.2') + }) + + it('leaves other values which name starts the same', () => { + expect(newParams).to.have.property('interests', params.interests) + }) + }) + + context('value is undefined', () => { + const property = 'meta' + + beforeEach(() => { + newParams = set(params, property) + }) + + it('removes all existing properties', () => { + expect(newParams).not.to.have.keys( + 'meta.position', + 'meta.workingHours', + 'meta.duties', + 'meta.fun', + ) + }) + + it('does not set any new key', () => { + expect(Object.keys(newParams).length).to.eq(Object.keys(params).length - 4) + }) + }) + + context('mixed type was inside and should be updated', () => { + const meta = { + position: 'adminBroCEO', + workingHours: '6:00-21:00', + } + + beforeEach(() => { + newParams = set(params, 'meta', meta) + }) + + it('clears the previous value for nested string', () => { + expect(newParams).not.to.have.keys('meta.duties', 'meta.fun') + }) + + it('sets the new value for nested string', () => { + expect(newParams).to.include({ + 'meta.position': meta.position, + 'meta.workingHours': meta.workingHours, + }) + }) + }) +}) diff --git a/src/utils/flat/set.ts b/src/utils/flat/set.ts new file mode 100644 index 000000000..4516ff5e9 --- /dev/null +++ b/src/utils/flat/set.ts @@ -0,0 +1,83 @@ +import { flatten } from 'flat' +import { DELIMITER } from './constants' +import { FlattenParams } from '../flat' +import { propertyKeyRegex } from './property-key-regex' + +/** + * + * @memberof module:flat + * @param {FlattenParams} params + * @param {string} property + * @param {any} [value] if not give function will only try to remove old keys + */ +export const set = (params: FlattenParams = {}, property: string, value?: any): FlattenParams => { + const regex = propertyKeyRegex(property) + + // remove all existing keys + const paramsCopy = Object.keys(params) + .filter(key => !key.match(regex)) + .reduce((memo, key) => ({ ...memo, [key]: params[key] }), {} as FlattenParams) + + if (typeof value !== 'undefined') { + if (typeof value === 'object' && !(value instanceof File) && value !== null) { + const flattened = flatten(value) as any + + if (Object.keys(flattened).length) { + Object.keys(flattened).forEach((key) => { + paramsCopy[`${property}${DELIMITER}${key}`] = flattened[key] + }) + } else if (Array.isArray(value)) { + paramsCopy[property] = [] + } else { + paramsCopy[property] = {} + } + } else { + paramsCopy[property] = value + } + } + return paramsCopy +} + + +// const filteredParams = Object.entries(params) +// .filter(([key]) => !key.match(regex)) +// .reduce((memo, [key, value]) => ({ +// ...memo, +// [key]: value, +// }), {}) + +// return flatten({ +// ...filteredParams, +// [propertyPath]: array, +// }) + + +// const populatedCopy = { ...previousRecord.populated } +// const paramsCopy = { ...previousRecord.params } + +// // clear previous value +// Object.keys(paramsCopy) +// .filter(key => key === property || key.startsWith(`${property}.`)) +// .forEach(k => delete paramsCopy[k]) +// if (property in populatedCopy) { +// delete populatedCopy[property] +// populatedModified = true +// } + +// // set new value +// if (typeof value !== 'undefined') { +// if (typeof value === 'object' && !(value instanceof File) && value !== null) { +// const flattened = flat.flatten(value) as any +// if (Object.keys(flattened).length) { +// Object.keys(flattened).forEach((key) => { +// paramsCopy[`${property}.${key}`] = flattened[key] +// }) +// } else if (Array.isArray(value)) { +// paramsCopy[property] = [] +// } else { +// paramsCopy[property] = {} +// } +// } else { +// paramsCopy[property] = value +// } +// } diff --git a/src/utils/index.ts b/src/utils/index.ts index a2eef6fb5..45f0589bc 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1 +1,2 @@ export * from './translate-functions.factory' +export * from './flat'