Skip to content

Commit bf7055e

Browse files
feat: move populator logic to the adminbro core
closes #587
1 parent 9517bc9 commit bf7055e

20 files changed

+630
-280
lines changed

app/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"license": "MIT",
66
"scripts": {
77
"build": "tsc",
8-
"start": "yarn build && node build/index.js",
8+
"start": "yarn build && node build/src/index.js",
99
"dev": "concurrently \"wait-on build/src/index.js && nodemon node build/src/index.js\" \"yarn build --watch\"",
1010
"cypress:open": "cypress open",
1111
"cypress:run": "cypress run"

package.json

+6-7
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@
8484
"@babel/preset-react": "^7.10.1",
8585
"@babel/preset-typescript": "^7.10.1",
8686
"@babel/register": "^7.10.1",
87-
"@rollup/plugin-typescript": "^6.0.0",
8887
"@types/react": "^16.9.16",
8988
"axios": "^0.19.2",
9089
"commander": "^5.1.0",
@@ -121,13 +120,13 @@
121120
"@types/chai-as-promised": "^7.1.2",
122121
"@types/flat": "^0.0.28",
123122
"@types/lodash": "^4.14.141",
124-
"@types/mocha": "^5.2.7",
123+
"@types/mocha": "^8.0.3",
125124
"@types/react-dom": "^16.9.8",
126125
"@types/react-redux": "^7.1.9",
127126
"@types/react-router": "^5.1.7",
128127
"@types/react-router-dom": "^5.1.5",
129-
"@types/sinon": "^7.5.0",
130-
"@types/sinon-chai": "^3.2.3",
128+
"@types/sinon": "^9.0.6",
129+
"@types/sinon-chai": "^3.2.5",
131130
"@types/styled-components": "^5.1.0",
132131
"@typescript-eslint/eslint-plugin": "^2.3.1",
133132
"@typescript-eslint/parser": "^2.3.1",
@@ -147,7 +146,7 @@
147146
"istanbul": "^0.4.5",
148147
"jsdom": "^15.1.0",
149148
"jsdom-global": "^3.0.2",
150-
"mocha": "^5.2.0",
149+
"mocha": "^8.1.3",
151150
"nyc": "^14.1.1",
152151
"react-testing-library": "^7.0.0",
153152
"recharts": "^1.7.1",
@@ -157,8 +156,8 @@
157156
"semantic-release": "^17.0.7",
158157
"semantic-release-jira-releases-sb": "^0.7.2",
159158
"semantic-release-slack-bot": "^1.6.2",
160-
"sinon": "^6.1.5",
161-
"sinon-chai": "^3.2.0",
159+
"sinon": "^9.0.3",
160+
"sinon-chai": "^3.5.0",
162161
"typescript": "^3.9.7"
163162
}
164163
}

spec/backend/helpers/resource-stub.ts

-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ export default (): BaseResource => ({
6262
databaseType: sinon.stub().returns(expectedResult.databaseType),
6363
count: sinon.stub(),
6464
find: sinon.stub(),
65-
populate: sinon.stub(),
6665
findOne: sinon.stub(),
6766
findMany: sinon.stub(),
6867
build: sinon.stub(),

src/backend/actions/list/list-action.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const ListAction: Action<ListActionResponse> = {
5757
offset: (page - 1) * perPage,
5858
sort,
5959
})
60-
const populatedRecords = await populator(records, listProperties)
60+
const populatedRecords = await populator(records)
6161

6262
// eslint-disable-next-line no-param-reassign
6363
context.records = populatedRecords

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,11 @@ class BaseRecord {
176176
/**
177177
* Populate record relations
178178
*
179-
* @param {string} propertyName name of the property which should be populated
179+
* @param {string} propertyPath name of the property which should be populated
180180
* @param {BaseRecord} record record to which property relates
181181
*/
182-
populate(propertyName: string, record: BaseRecord): void {
183-
this.populated[propertyName] = record
182+
populate(propertyPath: string, record: BaseRecord): void {
183+
this.populated[propertyPath] = record
184184
}
185185

186186
/**
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1+
import { number } from 'prop-types'
2+
3+
/**
4+
* @alias ParamsTypeValue
5+
* @memberof BaseRecord
6+
*/
7+
export type ParamsTypeValue = string | number | boolean | null | undefined | [] | {} | File
8+
19

210
/**
311
* @alias ParamsType
412
* @memberof BaseRecord
513
*/
614
export type ParamsType = Record<string, any>
15+
// TODO: change ^^^any to ParamsTypeValue

src/backend/adapters/resource/base-resource.spec.ts

-6
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,6 @@ describe('BaseResource', function () {
7171
})
7272
})
7373

74-
describe('#populate', function () {
75-
it('throws NotImplementedError', async function () {
76-
expect(resource.populate([], {} as BaseProperty)).to.be.rejectedWith(NotImplementedError)
77-
})
78-
})
79-
8074
describe('#findOne', function () {
8175
it('throws NotImplementedError', async function () {
8276
expect(resource.findOne('someId')).to.be.rejectedWith(NotImplementedError)

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

+2-20
Original file line numberDiff line numberDiff line change
@@ -153,24 +153,6 @@ class BaseResource {
153153
throw new NotImplementedError('BaseResource#find')
154154
}
155155

156-
157-
/**
158-
* Populates records with references for given property.
159-
*
160-
* Example: Let say resource `Article` has property `user_id` and it is a reference
161-
* to `User` resource. When you call this `User.populate([...articleRecords], userIdProperty)`
162-
* it should populate `articleRecords` with corresponding users.
163-
* So after that invoking `articleRecord.populated['user_id']` will return the user Record
164-
*
165-
* @param {Array<BaseRecord>} records all records which should be populated
166-
* @param {BaseProperty} property property which is a reference to `this` Resource
167-
*
168-
* @return {Promise<Array<BaseRecord>>} populated records
169-
*/
170-
async populate(records: Array<BaseRecord>, property: BaseProperty): Promise<Array<BaseRecord>> {
171-
throw new NotImplementedError('BaseResource#populate')
172-
}
173-
174156
/**
175157
* Finds one Record in the Resource by its id
176158
*
@@ -238,8 +220,8 @@ class BaseResource {
238220
/**
239221
* Delete given record by id
240222
*
241-
* @param {String|Number} id id of the Record
242-
* @throws {ValidationError} If there are validation errors it should be thrown
223+
* @param {String | Number} id id of the Record
224+
* @throws {ValidationError} If there are validation errors it should be thrown
243225
* @abstract
244226
*/
245227
async delete(id: string): Promise<void> {

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

+1
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ describe('PropertyDecorator', function () {
174174
'isArray',
175175
'custom',
176176
'resourceId',
177+
'path',
177178
'isRequired',
178179
)
179180
})

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

+8
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,13 @@ class PropertyDecorator {
101101
return this.overrideFromOptions(AvailablePropertyOptions.name)
102102
}
103103

104+
/**
105+
* Resource decorator of given property
106+
*/
107+
resource(): ResourceDecorator {
108+
return this._resource
109+
}
110+
104111
/**
105112
* Label of a property
106113
*
@@ -228,6 +235,7 @@ class PropertyDecorator {
228235
isRequired: this.isRequired(),
229236
availableValues: this.availableValues(),
230237
name: this.name(),
238+
path: this.path,
231239
isDisabled: this.isDisabled(),
232240
label: this.label(),
233241
type: this.type(),

src/backend/utils/populator/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './populator'
2+
export * from './populate-property'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { expect } from 'chai'
2+
import sinon, { SinonStubbedInstance } from 'sinon'
3+
import { BaseProperty, BaseRecord, BaseResource } from '../../adapters'
4+
5+
import { ResourceDecorator } from '../../decorators'
6+
import { populateProperty, PopulatorNarrowedProperty } from './populate-property'
7+
8+
describe('populateProperty', () => {
9+
const userId = '1234'
10+
const path = 'userId'
11+
let property: PopulatorNarrowedProperty
12+
let resourceDecorator: SinonStubbedInstance<ResourceDecorator>
13+
let referenceResource: SinonStubbedInstance<BaseResource>
14+
let record: SinonStubbedInstance<BaseRecord>
15+
let userRecord: SinonStubbedInstance<BaseRecord>
16+
17+
18+
beforeEach(() => {
19+
resourceDecorator = sinon.createStubInstance(ResourceDecorator)
20+
referenceResource = sinon.createStubInstance(BaseResource)
21+
record = sinon.createStubInstance(BaseRecord)
22+
userRecord = sinon.createStubInstance(BaseRecord)
23+
24+
property = {
25+
resource: () => resourceDecorator as unknown as ResourceDecorator,
26+
reference: () => referenceResource as unknown as BaseResource,
27+
property: { reference: 'someRawReference' } as unknown as BaseProperty,
28+
path,
29+
}
30+
})
31+
32+
it('returns empty array when no records are given', async () => {
33+
expect(await populateProperty([], property)).to.deep.eq([])
34+
})
35+
36+
context('2 same records having with reference key', () => {
37+
let populatedResponse: Array<BaseRecord> | null
38+
39+
beforeEach(async () => {
40+
record.param.returns(userId)
41+
userRecord.id.returns(userId)
42+
referenceResource.findMany.resolves([userRecord])
43+
44+
populatedResponse = await populateProperty([record, record], property)
45+
})
46+
47+
it('returns 2 records', async () => {
48+
expect(populatedResponse?.length).to.eq(2)
49+
})
50+
51+
it('calls findMany in with the list of userIds just once', () => {
52+
expect(referenceResource.findMany).to.have.been.calledOnceWith([userId])
53+
})
54+
55+
it('adds reference resource to record.populated', () => {
56+
const populatedRecord = populatedResponse && populatedResponse[0]
57+
58+
expect(populatedRecord?.populate).to.have.been.calledWith(path, userRecord)
59+
})
60+
})
61+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { BaseRecord } from '../../adapters'
2+
import PropertyDecorator from '../../decorators/property/property-decorator'
3+
4+
export type PopulatorNarrowedProperty = Pick<
5+
InstanceType<typeof PropertyDecorator>,
6+
'resource' | 'reference' | 'property' | 'path'
7+
>
8+
9+
/**
10+
* It populates one property in given records
11+
*
12+
* @param {Array<BaseRecord>} records array of records to populate
13+
* @param {PropertyDecorator} property Decorator for the reference property to populate
14+
*/
15+
export const populateProperty = async (
16+
records: Array<BaseRecord> | null,
17+
property: PopulatorNarrowedProperty,
18+
): Promise<Array<BaseRecord> | null> => {
19+
const decoratedResource = property.resource()
20+
21+
if (!records || !records.length) {
22+
return records
23+
}
24+
25+
const referencedResource = property.reference()
26+
27+
if (!referencedResource) {
28+
throw new Error([
29+
`There is no reference resource named: "${property.property.reference}"`,
30+
`for property: "${decoratedResource.id()}.properties.${property.path}"`,
31+
].join('\n'))
32+
}
33+
34+
// I will describe the process for following data:
35+
// - decoratedResource = 'Comment'
36+
// - referenceResource = 'User'
37+
// property.path = 'userId'
38+
39+
// first, we create externalIdsMap[1] = null where 1 is userId. This make keys unique and assign
40+
// nulls to each of them
41+
const externalIdsMap = records.reduce((memo, baseRecord) => {
42+
const foreignKeyValue = baseRecord.param(property.path)
43+
// when foreign key is not filled (like null) - don't add this because
44+
// BaseResource#findMany (which we will use) might break for nulls
45+
if (!foreignKeyValue) {
46+
return memo
47+
}
48+
return {
49+
...memo,
50+
[foreignKeyValue]: null,
51+
}
52+
}, {})
53+
54+
const uniqueExternalIds = Object.keys(externalIdsMap)
55+
56+
// when no record has `userId` filled = return input `records`
57+
if (!uniqueExternalIds.length) {
58+
return records
59+
}
60+
61+
// now find all referenced records: all users
62+
const referenceRecords = await referencedResource.findMany(uniqueExternalIds)
63+
64+
//
65+
if (!referenceRecords || !referenceRecords.length) {
66+
return records
67+
}
68+
69+
// now assign these users to `externalIdsMap` instead of the empty object we had. To speed up
70+
// assigning them to record#populated we will do in the next step
71+
referenceRecords.forEach((referenceRecord) => {
72+
// example: externalIds[1] = { ...userRecord } | null (if not found)
73+
const foreignKeyValue = referenceRecord.id()
74+
externalIdsMap[foreignKeyValue] = referenceRecord
75+
})
76+
77+
return records.map((record) => {
78+
// we set record.populated['userId'] = externalIdsMap[record.param('userId)]
79+
const foreignKeyValue = record.param(property.path)
80+
record.populate(property.path, externalIdsMap[foreignKeyValue])
81+
return record
82+
})
83+
}

src/backend/utils/populator/populator.spec.js

-37
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { expect } from 'chai'
2+
3+
import populator from './populator'
4+
5+
6+
describe('populator', () => {
7+
context('empty array given as params', () => {
8+
it('returns empty array when no records are given', async () => {
9+
const records = await populator([])
10+
expect(records).to.have.lengthOf(0)
11+
})
12+
})
13+
})

0 commit comments

Comments
 (0)