Skip to content

Commit ab6140c

Browse files
Merge pull request #647 from SoftwareBrothers/#645/fix-array-items-for-nested-properties
fix: array manipulaton in deeply nested properties
2 parents cb60200 + 0d045e3 commit ab6140c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+719
-186
lines changed

example-app/src/employees/employee.entity.js

+39
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,33 @@
11
const mongoose = require('mongoose')
22

3+
const Skill = new mongoose.Schema({
4+
name: String,
5+
level: {
6+
type: String,
7+
enum: ['junior', 'middle', 'senior'],
8+
},
9+
Profession: {
10+
type: mongoose.SchemaTypes.ObjectId,
11+
ref: 'Profession',
12+
},
13+
})
14+
15+
const OtherSchema = new mongoose.Schema({
16+
name: String,
17+
arrayed: {
18+
type: [Skill],
19+
},
20+
})
21+
22+
const Skills = new mongoose.Schema({
23+
softShills: [{
24+
type: Skill,
25+
}],
26+
hardSkills: [{
27+
type: Skill,
28+
}],
29+
})
30+
331
const EmployeeSchema = new mongoose.Schema({
432
name: {
533
type: String,
@@ -16,10 +44,21 @@ const EmployeeSchema = new mongoose.Schema({
1644
type: mongoose.SchemaTypes.ObjectId,
1745
ref: 'Company',
1846
},
47+
interests: {
48+
type: [String],
49+
},
1950
professions: [{
2051
type: mongoose.SchemaTypes.ObjectId,
2152
ref: 'Profession',
2253
}],
54+
55+
Skills: {
56+
type: Skills,
57+
},
58+
59+
otherField: {
60+
type: [OtherSchema],
61+
},
2362
})
2463

2564
const Employee = mongoose.model('Employee', EmployeeSchema)

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

+18-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { flat } from '../../../utils/flat'
1+
import { flat, GetOptions } from '../../../utils/flat'
22
import { ParamsType } from './params.type'
33
import BaseResource from '../resource/base-resource'
44
import ValidationError, { RecordError, PropertyErrors } from '../../utils/errors/validation-error'
@@ -69,11 +69,12 @@ class BaseRecord {
6969
*
7070
* @param {string} [propertyPath] path for the property. If not set function returns an entire
7171
* unflatten object
72+
* @param {GetOptions} [options]
7273
* @return {any} unflatten data under given path
7374
* @new in version 3.3
7475
*/
75-
get(propertyPath?: string): any {
76-
return flat.get(this.params, propertyPath)
76+
get(propertyPath?: string, options?: GetOptions): any {
77+
return flat.get(this.params, propertyPath, options)
7778
}
7879

7980
/**
@@ -96,11 +97,25 @@ class BaseRecord {
9697
* @param {string} prefix
9798
*
9899
* @return {object | undefined}
100+
* @deprecated in favour of {@link selectParams}
99101
*/
100102
namespaceParams(prefix: string): Record<string, any> | void {
101103
return flat.selectParams(this.params, prefix)
102104
}
103105

106+
/**
107+
* Returns object containing all params keys starting with prefix
108+
*
109+
* @param {string} prefix
110+
* @param {GetOptions} [options]
111+
*
112+
* @return {object | undefined}
113+
* @new in version 3.3
114+
*/
115+
selectParams(prefix: string, options?: GetOptions): Record<string, any> | void {
116+
return flat.selectParams(this.params, prefix, options)
117+
}
118+
104119
/**
105120
* Updates given Record in the data store. Practically it invokes
106121
* {@link BaseResource.update} method.

src/backend/controllers/api-controller.spec.js

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ describe('ApiController', function () {
3838
resourceActions: () => [this.action],
3939
recordActions: () => [this.action],
4040
recordsDecorator: records => records,
41+
getFlattenProperties: this.sinon.stub().returns([property]),
4142
id: this.resourceName,
4243
}),
4344
find: this.sinon.stub().returns([]),

src/backend/decorators/property/property-decorator.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ describe('PropertyDecorator', () => {
193193
'isArray',
194194
'custom',
195195
'resourceId',
196-
'path',
196+
'propertyPath',
197197
'isRequired',
198198
'isVirtual',
199199
'props',

src/backend/decorators/property/property-decorator.ts

+11-11
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import PropertyOptions from './property-options.interface'
33
import BaseResource from '../../adapters/resource/base-resource'
44
import BaseProperty, { PropertyType } from '../../adapters/property/base-property'
55
import ResourceDecorator from '../resource/resource-decorator'
6-
import { PropertyPlace, PropertyJSON } from '../../../frontend/interfaces'
6+
import { PropertyPlace, BasePropertyJSON } from '../../../frontend/interfaces'
77
import { overrideFromOptions } from './utils'
88

99
/**
@@ -23,7 +23,7 @@ class PropertyDecorator {
2323
* This path serves as a key in {@link PropertyOptions} to identify which
2424
* property has to be updated
2525
*/
26-
public path: string
26+
public propertyPath: string
2727

2828
/**
2929
* Indicates if given property has been created in AdminBro and hasn't been returned by the
@@ -63,7 +63,7 @@ class PropertyDecorator {
6363
this.property = property
6464
this._admin = admin
6565
this._resource = resource
66-
this.path = path || property.name()
66+
this.propertyPath = path || property.name()
6767
this.isVirtual = !!isVirtual
6868
this.virtualSubProperties = []
6969

@@ -123,7 +123,7 @@ class PropertyDecorator {
123123
* @return {string}
124124
*/
125125
label(): string {
126-
return this._admin.translateProperty(this.path, this._resource.id())
126+
return this._admin.translateProperty(this.propertyPath, this._resource.id())
127127
}
128128

129129
/**
@@ -153,7 +153,7 @@ class PropertyDecorator {
153153
return values.map(val => ({
154154
value: val,
155155
label: this._admin.translateProperty(
156-
`${this.path}.${val}`,
156+
`${this.propertyPath}.${val}`,
157157
this._resource.id(),
158158
{ defaultValue: val },
159159
),
@@ -246,7 +246,7 @@ class PropertyDecorator {
246246
*
247247
* @return {PropertyJSON}
248248
*/
249-
toJSON(where?: PropertyPlace): PropertyJSON {
249+
toJSON(where?: PropertyPlace): BasePropertyJSON {
250250
return {
251251
isTitle: this.isTitle(),
252252
isId: this.isId(),
@@ -256,7 +256,7 @@ class PropertyDecorator {
256256
isRequired: this.isRequired(),
257257
availableValues: this.availableValues(),
258258
name: this.name(),
259-
path: this.path,
259+
propertyPath: this.propertyPath,
260260
isDisabled: this.isDisabled(),
261261
label: this.label(),
262262
type: this.type(),
@@ -280,7 +280,7 @@ class PropertyDecorator {
280280
*/
281281
subProperties(): Array<PropertyDecorator> {
282282
const dbSubProperties = this.property.subProperties().map((subProperty) => {
283-
const path = `${this.path}.${subProperty.name()}`
283+
const path = `${this.propertyPath}.${subProperty.name()}`
284284
const decorated = new PropertyDecorator({
285285
property: subProperty,
286286
admin: this._admin,
@@ -301,14 +301,14 @@ class PropertyDecorator {
301301
* Returns PropertyOptions passed by the user for a subProperty. Furthermore
302302
* it changes property name to the nested property key.
303303
*
304-
* @param {BaseProperty} subProperty
304+
* @param {String} propertyPath
305305
* @return {PropertyOptions}
306306
* @private
307307
*/
308-
private getOptionsForSubProperty(path: string): PropertyOptions {
308+
private getOptionsForSubProperty(propertyPath: string): PropertyOptions {
309309
const propertyOptions = (this._resource.options || {}).properties || {}
310310
return {
311-
...propertyOptions[path],
311+
...propertyOptions[propertyPath],
312312
}
313313
}
314314
}

src/backend/decorators/resource/resource-decorator.spec.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ describe('ResourceDecorator', function () {
159159
const decoratedProperty = decorator.getPropertyByKey(path) as PropertyDecorator
160160

161161
expect(decoratedProperty).to.be.an.instanceof(PropertyDecorator)
162-
expect(decoratedProperty.path).to.eq(path)
162+
expect(decoratedProperty.propertyPath).to.eq(path)
163163
})
164164

165165
it('returns nested property under 2 level nested mixed', function () {
@@ -171,7 +171,7 @@ describe('ResourceDecorator', function () {
171171
const decoratedProperty = decorator.getPropertyByKey(path) as PropertyDecorator
172172

173173
expect(decoratedProperty).to.be.an.instanceof(PropertyDecorator)
174-
expect(decoratedProperty.path).to.eq(path)
174+
expect(decoratedProperty.propertyPath).to.eq(path)
175175
})
176176

177177
it('returns property when it is an array', function () {
@@ -182,7 +182,7 @@ describe('ResourceDecorator', function () {
182182
const decoratedProperty = decorator.getPropertyByKey(path) as PropertyDecorator
183183

184184
expect(decoratedProperty).to.be.an.instanceof(PropertyDecorator)
185-
expect(decoratedProperty.path).to.eq(arrayProperty.path())
185+
expect(decoratedProperty.propertyPath).to.eq(arrayProperty.path())
186186
})
187187

188188
it('returns property when it is an nested array', function () {
@@ -196,7 +196,7 @@ describe('ResourceDecorator', function () {
196196
const decoratedProperty = decorator.getPropertyByKey(path) as PropertyDecorator
197197

198198
expect(decoratedProperty).to.be.an.instanceof(PropertyDecorator)
199-
expect(decoratedProperty.path).to.eq([arrayProperty.path(), nested1Property.path()].join('.'))
199+
expect(decoratedProperty.propertyPath).to.eq([arrayProperty.path(), nested1Property.path()].join('.'))
200200
})
201201
})
202202

src/backend/decorators/resource/resource-decorator.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import AdminBro from '../../../admin-bro'
77

88
import { ResourceOptions } from './resource-options.interface'
99
import { CurrentAdmin } from '../../../current-admin.interface'
10-
import { ResourceJSON, PropertyJSON, PropertyPlace } from '../../../frontend/interfaces'
10+
import { ResourceJSON, PropertyPlace } from '../../../frontend/interfaces'
1111
import {
1212
decorateActions,
1313
decorateProperties,
@@ -86,7 +86,7 @@ class ResourceDecorator {
8686
this.options.properties = this.options.properties || {}
8787

8888
/**
89-
* List of all decorated properties
89+
* List of all decorated root properties
9090
* @type {Array<PropertyDecorator>}
9191
*/
9292
this.properties = decorateProperties(resource, admin, this)
@@ -179,14 +179,17 @@ class ResourceDecorator {
179179
return properties
180180
}
181181

182-
getFlattenProperties(): Record<string, PropertyJSON> {
182+
/**
183+
* Returns all the properties with corresponding subProperties in one object.
184+
*/
185+
getFlattenProperties(): Record<string, PropertyDecorator> {
183186
return Object.keys(this.properties).reduce((memo, propertyName) => {
184187
const property = this.properties[propertyName]
185188

186189
const subProperties = flatSubProperties(property)
187190
return {
188191
...memo,
189-
[propertyName]: property.toJSON(),
192+
[propertyName]: property,
190193
...subProperties,
191194
}
192195
}, {})
@@ -294,6 +297,12 @@ class ResourceDecorator {
294297
* @return {ResourceJSON}
295298
*/
296299
toJSON(currentAdmin?: CurrentAdmin): ResourceJSON {
300+
const flattenProperties = this.getFlattenProperties()
301+
const flattenPropertiesJSON = Object.keys(flattenProperties).reduce((memo, key) => ({
302+
...memo,
303+
[key]: flattenProperties[key].toJSON(),
304+
}), {})
305+
297306
return {
298307
id: this.id(),
299308
name: this.getResourceName(),
@@ -302,7 +311,7 @@ class ResourceDecorator {
302311
titleProperty: this.titleProperty().toJSON(),
303312
resourceActions: this.resourceActions(currentAdmin).map(ra => ra.toJSON(currentAdmin)),
304313
actions: Object.values(this.actions).map(action => action.toJSON(currentAdmin)),
305-
properties: this.getFlattenProperties(),
314+
properties: flattenPropertiesJSON,
306315
listProperties: this.getProperties({
307316
where: 'list', max: DEFAULT_MAX_COLUMNS_IN_LIST,
308317
}).map(property => property.toJSON('list')),

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ const organizeNestedProperties = (
7474
// reverse because we start by by finding from the longest path
7575
// and removes itself. (skips arrays)
7676
// changes 'root.nested.0.nested1' to [root.nested', 'root']
77-
const parts = pathToParts(property.path, { skipArrayIndexes: true }).reverse().splice(1)
77+
const parts = pathToParts(property.propertyPath, { skipArrayIndexes: true }).reverse().splice(1)
7878
if (parts.length) {
7979
const mixedPropertyPath = parts.find(part => (
8080
properties[part] && properties[part].type() === 'mixed'

src/backend/decorators/resource/utils/find-sub-property.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ export const findSubProperty = (
1616
): PropertyDecorator | null => {
1717
const subProperties = rootProperty.subProperties()
1818
const foundPath = pathParts.find(path => (
19-
subProperties.find(supProperty => supProperty.path === path)))
19+
subProperties.find(supProperty => supProperty.propertyPath === path)))
2020
if (foundPath) {
21-
const subProperty = subProperties.find(supProperty => supProperty.path === foundPath)
21+
const subProperty = subProperties.find(supProperty => supProperty.propertyPath === foundPath)
2222
if (subProperty && foundPath !== pathParts[pathParts.length - 1]) {
2323
// if foundPath is not the last (full) path - checkout recursively all subProperties
2424
return findSubProperty(pathParts, subProperty)

src/backend/decorators/resource/utils/flat-sub-properties.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import PropertyDecorator from '../../property/property-decorator'
2-
import { PropertyJSON } from '../../../../frontend/interfaces'
32

43
/**
54
* Bu default all subProperties are nested as an array in root Property. This is easy for
@@ -15,10 +14,10 @@ import { PropertyJSON } from '../../../../frontend/interfaces'
1514
*/
1615
export const flatSubProperties = (
1716
rootProperty: PropertyDecorator,
18-
): Record<string, PropertyJSON> => (
17+
): Record<string, PropertyDecorator> => (
1918
rootProperty.subProperties().reduce((subMemo, subProperty) => ({
2019
...subMemo,
21-
[subProperty.path]: subProperty.toJSON(),
20+
[subProperty.propertyPath]: subProperty,
2221
...flatSubProperties(subProperty),
2322
}), {})
2423
)

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe('populateProperty', () => {
2525
property.resource.returns(resourceDecorator as unknown as ResourceDecorator)
2626
property.reference.returns(referenceResource as unknown as BaseResource)
2727
property.property = { reference: 'someRawReference' } as unknown as BaseProperty
28-
property.path = path
28+
property.propertyPath = path
2929
})
3030

3131
afterEach(() => {
@@ -39,6 +39,7 @@ describe('populateProperty', () => {
3939
context('2 same records with reference key', () => {
4040
beforeEach(async () => {
4141
record.get.returns(userId)
42+
record.selectParams.returns({ [path]: userId })
4243
userRecord.id.returns(userId)
4344
referenceResource.findMany.resolves([userRecord])
4445

0 commit comments

Comments
 (0)