Skip to content

Commit

Permalink
feat: add flat module
Browse files Browse the repository at this point in the history
  • Loading branch information
wojtek-krysiak committed Sep 27, 2020
1 parent ffc92a6 commit ec7e53d
Show file tree
Hide file tree
Showing 22 changed files with 481 additions and 77 deletions.
2 changes: 1 addition & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/**/*"
Expand Down
4 changes: 2 additions & 2 deletions example-app/src/nested/value-trigger.component.tsx
Original file line number Diff line number Diff line change
@@ -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<EditPropertyProps> = (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({
Expand Down
20 changes: 4 additions & 16 deletions src/backend/adapters/record/base-record.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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<string, any>
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<string, any> | 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)
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/backend/decorators/property/property-decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 13 additions & 5 deletions src/backend/decorators/resource/utils/decorate-properties.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand All @@ -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)
})
})

Expand Down
16 changes: 9 additions & 7 deletions src/backend/decorators/resource/utils/decorate-properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 31 additions & 12 deletions src/backend/utils/populator/populate-property.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ResourceDecorator>
let referenceResource: SinonStubbedInstance<BaseResource>
let record: SinonStubbedInstance<BaseRecord>
let userRecord: SinonStubbedInstance<BaseRecord>
let property: SinonStubbedInstance<PropertyDecorator> & PropertyDecorator

let populatedResponse: Array<BaseRecord> | 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<BaseRecord> | null

context('2 same records with reference key', () => {
beforeEach(async () => {
record.param.returns(userId)
userRecord.id.returns(userId)
Expand All @@ -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])
})
})
})
29 changes: 22 additions & 7 deletions src/backend/utils/populator/populate-property.ts
Original file line number Diff line number Diff line change
@@ -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<typeof PropertyDecorator>,
'resource' | 'reference' | 'property' | 'path'
>

/**
* It populates one property in given records
*
Expand All @@ -14,7 +10,7 @@ export type PopulatorNarrowedProperty = Pick<
*/
export const populateProperty = async (
records: Array<BaseRecord> | null,
property: PopulatorNarrowedProperty,
property: PropertyDecorator,
): Promise<Array<BaseRecord> | null> => {
const decoratedResource = property.resource()

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
})
}
4 changes: 3 additions & 1 deletion src/frontend/bundle-entry.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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',
Expand Down Expand Up @@ -58,6 +58,8 @@ export default {
env,
...AppComponents,
...Hooks,
flat,
// TODO: remove this from the next release
flatten: flat.flatten,
unflatten: flat.unflatten,
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
26 changes: 2 additions & 24 deletions src/frontend/hooks/use-record/update-record.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import flat from 'flat'
import { flat } from '../../../utils/flat'
import { RecordJSON } from '../../interfaces'

/**
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/utils/flat/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
*
* @memberof module:flat
* @new
*/
export const DELIMITER = '.'
21 changes: 21 additions & 0 deletions src/utils/flat/filter-params.ts
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit ec7e53d

Please sign in to comment.