Skip to content

Commit 3dd1747

Browse files
feat: handling onverriding params in BaseRecord
* `field: null` hasn't been overriden when flat.set('field.0') * add `flat.merge()` * getPropertyByKey got new param: `skipArrayIndexes` * both `BaseRecord.save` and `upate` store flatten params * fix error with saving arrays of refrences when nulls are given
1 parent 9af1968 commit 3dd1747

File tree

17 files changed

+199
-42
lines changed

17 files changed

+199
-42
lines changed

example-app/cypress/integration/employees/create-employee.spec.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ context('resources/Employee/actions/new', () => {
5757
expect(record.params.name).to.eq(data.name)
5858
expect(record.params.email).to.eq(data.email)
5959
expect(record.params.company).not.to.undefined
60-
expect(record.params.professions).to.have.lengthOf(2)
60+
expect(record.params['professions.0']).not.to.undefined
61+
expect(record.params['professions.1']).not.to.undefined
6162
})
6263
})
6364
})

example-app/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"admin-bro": "3.3.0-beta.13",
2222
"@admin-bro/design-system": "1.7.0-beta.17",
2323
"@admin-bro/express": "3.0.1",
24-
"@admin-bro/mongoose": "1.0.0",
24+
"@admin-bro/mongoose": "1.0.2",
2525
"@admin-bro/sequelize": "1.1.1",
2626
"argon2": "^0.26.2",
2727
"connect-mongo": "^3.2.0",

example-app/yarn.lock

+4-4
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@
3333
resolved "https://registry.yarnpkg.com/@admin-bro/express/-/express-3.0.1.tgz#21e6693cce257da6f25e75be28b4e2b7ef387531"
3434
integrity sha512-CNGZxdMBFhhr9WGDKmdEpuxYumryO9VNZN+HK93o+Xjqx7lDfFk7smUDFRxmfoHsmaI4QRpErmvswNwpGGzevA==
3535

36-
"@admin-bro/[email protected].0":
37-
version "1.0.0"
38-
resolved "https://registry.yarnpkg.com/@admin-bro/mongoose/-/mongoose-1.0.0.tgz#4cdfbba15d0bc67d081235887f8c92ef210498e0"
39-
integrity sha512-U85x7Rc8ApadNRFQHQLZkGcDP+f6WFhYJheCMISDzaMHQ+/hSp8eFWQd/2Y/RBvfDwHuzZLqBQkFVazSXUWGCQ==
36+
"@admin-bro/[email protected].2":
37+
version "1.0.2"
38+
resolved "https://registry.yarnpkg.com/@admin-bro/mongoose/-/mongoose-1.0.2.tgz#e9a380240ad37f3777439df873972e1f5351d85b"
39+
integrity sha512-vOnTElNrbdlml+FTqMCxKgu+u1m8c86RwuGhBrsIbwo5wr6ER0s+SV2fzCo5aKsxa4Jy2aAMR3FmYy01PxVaKA==
4040
dependencies:
4141
escape-regexp "0.0.1"
4242
flat "^4.1.0"

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
},
7676
"homepage": "https://github.com/SoftwareBrothers/admin-bro#readme",
7777
"dependencies": {
78-
"@admin-bro/design-system": "^1.7.0-beta.17",
78+
"@admin-bro/design-system": "^1.7.0-beta.18",
7979
"@babel/core": "^7.10.2",
8080
"@babel/parser": "^7.10.2",
8181
"@babel/plugin-transform-runtime": "^7.10.1",
@@ -112,7 +112,7 @@
112112
"styled-components": "^5.1.0"
113113
},
114114
"devDependencies": {
115-
"@admin-bro/design-system": "^1.7.0-beta.17",
115+
"@admin-bro/design-system": "^1.7.0-beta.18",
116116
"@babel/cli": "^7.4.4",
117117
"@commitlint/cli": "^8.3.5",
118118
"@commitlint/config-conventional": "^8.3.4",

src/backend/adapters/record/base-record.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ class BaseRecord {
7373
* @return {any} unflatten data under given path
7474
* @new in version 3.3
7575
*/
76-
get(propertyPath: string | undefined): any {
76+
get(propertyPath?: string): any {
7777
return flat.get(this.params, propertyPath)
7878
}
7979

@@ -114,7 +114,8 @@ class BaseRecord {
114114
async update(params): Promise<BaseRecord> {
115115
try {
116116
this.storeParams(params)
117-
this.params = await this.resource.update(this.id(), params)
117+
const returnedParams = await this.resource.update(this.id(), params)
118+
this.storeParams(returnedParams)
118119
} catch (e) {
119120
if (e instanceof ValidationError) {
120121
this.errors = e.propertyErrors
@@ -139,11 +140,13 @@ class BaseRecord {
139140
*/
140141
async save(): Promise<BaseRecord> {
141142
try {
143+
let returnedParams
142144
if (this.id()) {
143-
this.params = await this.resource.update(this.id(), this.params)
145+
returnedParams = await this.resource.update(this.id(), this.params)
144146
} else {
145-
this.params = await this.resource.create(this.params)
147+
returnedParams = await this.resource.create(this.params)
146148
}
149+
this.storeParams(returnedParams)
147150
} catch (e) {
148151
if (e instanceof ValidationError) {
149152
this.errors = e.propertyErrors
@@ -246,7 +249,7 @@ class BaseRecord {
246249
* @param {object} [payloadData]
247250
*/
248251
storeParams(payloadData?: object): void {
249-
this.params = _.merge(this.params, payloadData ? flat.flatten(payloadData) : {})
252+
this.params = flat.merge(this.params, payloadData)
250253
}
251254
}
252255

src/backend/decorators/resource/utils/decorate-properties.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,9 @@ const organizeNestedProperties = (
7070
const rootPropertyKeys = Object.keys(properties).filter((key) => {
7171
const property = properties[key]
7272
// reverse because we start by by finding from the longest path
73-
// and removes itself.
74-
// changes 'root.nested.nested1' to [root.nested', 'root']
75-
const parts = pathToParts(property.path).reverse().splice(1)
73+
// and removes itself. (skips arrays)
74+
// changes 'root.nested.0.nested1' to [root.nested', 'root']
75+
const parts = pathToParts(property.path, { skipArrayIndexes: true }).reverse().splice(1)
7676
if (parts.length) {
7777
const mixedPropertyPath = parts.find(part => (
7878
properties[part] && properties[part].type() === 'mixed'

src/backend/decorators/resource/utils/get-property-by-key.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export const getPropertyByKey = (
88
propertyPath: string,
99
properties: DecoratedProperties,
1010
): PropertyDecorator | null => {
11-
const parts = pathToParts(propertyPath)
11+
const parts = pathToParts(propertyPath, { skipArrayIndexes: true })
1212
const fullPath = parts[parts.length - 1]
1313
const property = properties[fullPath]
1414

src/backend/utils/populator/populate-property.spec.ts

+18-3
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,27 @@ describe('populateProperty', () => {
6969
userRecord.id.returns(userId)
7070
property.isArray.returns(true)
7171
referenceResource.findMany.resolves([userRecord])
72+
})
7273

73-
populatedResponse = await populateProperty([record, record], property)
74+
context('filled array ', () => {
75+
beforeEach(async () => {
76+
record.get.returns([userId1, userId2])
77+
populatedResponse = await populateProperty([record, record], property)
78+
})
79+
it('properly finds references in arrays', async () => {
80+
expect(referenceResource.findMany).to.have.been.calledOnceWith([userId1, userId2])
81+
})
7482
})
7583

76-
it('properly finds references in arrays', () => {
77-
expect(referenceResource.findMany).to.have.been.calledOnceWith([userId1, userId2])
84+
context('array value set to null', () => {
85+
beforeEach(async () => {
86+
record.get.returns(undefined)
87+
populatedResponse = await populateProperty([record, record], property)
88+
})
89+
90+
it('dees not look for any record', () => {
91+
expect(referenceResource.findMany).not.to.have.been.called
92+
})
7893
})
7994
})
8095

src/backend/utils/populator/populate-property.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ export const populateProperty = async (
4343
const externalIdsMap = records.reduce((memo, baseRecord) => {
4444
const foreignKeyValue = baseRecord.get(property.path)
4545
// array properties returns arrays so we have to take the all into consideration
46-
if (property.isArray()) {
47-
return (foreignKeyValue as Array<string | number>).reduce((arrayMemo, valueInArray) => ({
46+
if (Array.isArray(foreignKeyValue) && property.isArray()) {
47+
return foreignKeyValue.reduce((arrayMemo, valueInArray) => ({
4848
...arrayMemo,
4949
...(isValueSearchable(valueInArray) ? { [valueInArray]: valueInArray } : {}),
5050
}), memo)

src/frontend/components/app/action-header/actions-to-button-group.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ export const actionsToButtonGroup = (
2020
label: action.label,
2121
variant: action.variant,
2222
source: action,
23-
href,
23+
href: href || undefined,
2424
// when href is not defined - handle click should also be not defined
2525
// This prevents from "cursor: pointer;"
26-
onClick: href ? handleClick : null,
26+
onClick: href ? handleClick : undefined,
2727
'data-testid': buildActionTestId(action),
2828
buttons: [],
2929
}

src/frontend/components/app/records-table/record-in-list.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ export const RecordInList: React.FC<RecordInListProps> = (props) => {
7878

7979
const buttons = [{
8080
icon: 'OverflowMenuHorizontal',
81-
variant: 'light',
82-
label: null,
81+
variant: 'light' as const,
82+
label: undefined,
8383
'data-testid': 'actions-dropdown',
8484
buttons: actionsToButtonGroup({
8585
actions: recordActions,

src/utils/flat/flat-module.ts

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { selectParams } from './select-params'
55
import { filterOutParams } from './filter-out-params'
66
import { set } from './set'
77
import { get } from './get'
8+
import { merge } from './merge'
89
import { pathToParts } from './path-to-parts'
910

1011
/**
@@ -20,6 +21,7 @@ export type FlatModuleType = {
2021
filterOutParams: typeof filterOutParams;
2122
DELIMITER: typeof DELIMITER;
2223
pathToParts: typeof pathToParts;
24+
merge: typeof merge;
2325
}
2426

2527
/**
@@ -58,4 +60,5 @@ export const flat: FlatModuleType = {
5860
filterOutParams,
5961
DELIMITER,
6062
pathToParts,
63+
merge,
6164
}

src/utils/flat/merge.spec.ts

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { expect } from 'chai'
2+
import { merge } from './merge'
3+
4+
describe('merge', () => {
5+
it('removes nulled arrays when nested items were found', () => {
6+
const object1 = { status: 'draft', postImage: null, blogImageSizes: null }
7+
const object2 = { 'blogImageSizes.0': 4130, 'blogImageMimeTypes.0': 'image/jpeg' }
8+
9+
expect(merge(object1, object2)).to.deep.equal({
10+
status: 'draft',
11+
postImage: null,
12+
'blogImageSizes.0': 4130,
13+
'blogImageMimeTypes.0': 'image/jpeg',
14+
})
15+
})
16+
17+
context('object with nested fields are given in the first argument', () => {
18+
const object1 = { status: {
19+
type: 'draft',
20+
updated: 'yesterday',
21+
tags: ['super'],
22+
} }
23+
24+
it('flattens everything and changes just nested property when it was given nested', () => {
25+
const object2 = { 'status.type': 'newDraft' }
26+
27+
expect(merge(object1, object2)).to.deep.equal({
28+
'status.type': object2['status.type'],
29+
'status.updated': 'yesterday',
30+
'status.tags.0': 'super',
31+
})
32+
})
33+
34+
it('changes entire record when 2 objects are given', () => {
35+
const object2 = { status: {
36+
type: 'newType',
37+
updated: 'today',
38+
} }
39+
40+
expect(merge(object1, object2)).to.deep.equal({
41+
'status.type': object2.status.type,
42+
'status.updated': 'today',
43+
})
44+
})
45+
})
46+
47+
describe('multiple parameters', () => {
48+
const object1 = { status: { type: 'draft' } }
49+
50+
it('returns flatten object when one other argument is given', () => {
51+
expect(merge(object1)).to.deep.equal({
52+
'status.type': 'draft',
53+
})
54+
})
55+
56+
it('merges more then 2 arguments', () => {
57+
const object2 = {
58+
'status.type': 'status2',
59+
'status.age': '1 day',
60+
}
61+
const object3 = {
62+
'status.type': 'status3',
63+
names: [
64+
'Wojtek',
65+
],
66+
}
67+
expect(merge(object1, object2, object3)).to.deep.equal({
68+
'status.type': 'status2',
69+
'status.age': '1 day',
70+
'names.0': 'Wojtek',
71+
})
72+
})
73+
})
74+
})

src/utils/flat/merge.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { flatten } from 'flat'
2+
import { FlattenParams } from './flat.types'
3+
import { set } from './set'
4+
5+
/**
6+
* Merges params together and returns flatten result
7+
*
8+
* @param {any} params
9+
* @param {Array<any>} ...mergeParams
10+
* @memberof flat
11+
*/
12+
export const merge = (params: any = {}, ...mergeParams: Array<any>): FlattenParams => {
13+
const flattenParams = flatten(params)
14+
15+
// reverse because we merge from right
16+
return mergeParams.reverse().reduce((globalMemo, mergeParam) => (
17+
Object.keys(mergeParam)
18+
.reduce((memo, key) => (
19+
set(memo, key, mergeParam[key])
20+
), globalMemo)
21+
), flattenParams as Record<string, any>)
22+
}

src/utils/flat/path-to-parts.ts

+26-9
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,46 @@
11
import { PathParts } from './path-parts.type'
22

3+
/**
4+
* @memberof module:flat
5+
* @alias PathToPartsOptions
6+
*/
7+
export type PathToPartsOptions = {
8+
/**
9+
* Indicates if array indexes should be skipped from the outcome.
10+
*/
11+
skipArrayIndexes?: boolean;
12+
}
13+
314
/**
415
* Changes path with flatten notation, with dots (.) inside, to array of all possible
516
* keys which can have a property.
617
*
718
* - changes: `nested.nested2.normalInner`
819
* - to `["nested", "nested.nested2", "nested.nested2.normalInner"]`
920
*
10-
* Also it takes care of the arrays, which are separated by numbers (indexes).
21+
* When skipArrayIndexes is set to true it also it takes care of the arrays, which are
22+
* separated by numbers (indexes). Then it:
1123
* - changes: `nested.0.normalInner.1`
1224
* - to: `nested.normalInner`
1325
*
1426
* Everything because when we look for a property of a given path it can be inside a
1527
* mixed property. So first, we have to find top level mixed property, and then,
1628
* step by step, find inside each of them.
1729
*
18-
* @private
19-
*
20-
* @param {string} propertyPath
21-
*
30+
* @param {string} propertyPath
31+
* @param {PathToPartsOptions} options
2232
* @return {PathParts}
33+
*
34+
* @memberof module:flat
35+
* @alias pathToParts
2336
*/
24-
export const pathToParts = (propertyPath: string): PathParts => (
25-
// eslint-disable-next-line no-restricted-globals
26-
propertyPath.split('.').filter(part => isNaN(+part)).reduce((memo, part) => {
37+
export const pathToParts = (propertyPath: string, options: PathToPartsOptions = {}): PathParts => {
38+
let allParts = propertyPath.split('.')
39+
if (options.skipArrayIndexes) {
40+
// eslint-disable-next-line no-restricted-globals
41+
allParts = allParts.filter(part => isNaN(+part))
42+
}
43+
return allParts.reduce((memo, part) => {
2744
if (memo.length) {
2845
return [
2946
...memo,
@@ -32,4 +49,4 @@ export const pathToParts = (propertyPath: string): PathParts => (
3249
}
3350
return [part]
3451
}, [] as Array<string>)
35-
)
52+
}

0 commit comments

Comments
 (0)