Skip to content

Commit

Permalink
feat: define reference in PropertyOptions
Browse files Browse the repository at this point in the history
By omiting the ORM we can now define relationship between resources

fixes
- Referenced field is displayed as raw id instead of a link #583
- Custom Reference #416
  • Loading branch information
wojtek-krysiak committed Sep 26, 2020
1 parent 7cc68da commit 052661d
Show file tree
Hide file tree
Showing 19 changed files with 201 additions and 47 deletions.
1 change: 1 addition & 0 deletions app/package.json
Expand Up @@ -7,6 +7,7 @@
"build": "tsc",
"start": "yarn build && node build/src/index.js",
"dev": "concurrently \"wait-on build/src/index.js && nodemon node build/src/index.js\" \"yarn build --watch\"",
"clean": "rm -rf build && mkdir build",
"cypress:open": "cypress open",
"cypress:run": "cypress run"
},
Expand Down
9 changes: 9 additions & 0 deletions app/src/admin/parents.ts
@@ -0,0 +1,9 @@
export const ContentParent = {
name: 'Content',
icon: 'Blog',
}

export const ProductsParent = {
name: 'Store Management',
icon: 'InventoryManagement',
}
3 changes: 2 additions & 1 deletion app/src/admin/resources/blog-post/blog-post-resource.ts
@@ -1,5 +1,6 @@
import { ResourceOptions } from 'admin-bro'
import { ContentParent } from '../../parents'

export const BlogPostResource: ResourceOptions = {

parent: ContentParent,
}
6 changes: 6 additions & 0 deletions app/src/admin/resources/brand/brand-resource.ts
@@ -0,0 +1,6 @@
import { ResourceOptions } from 'admin-bro'
import { ProductsParent } from '../../parents'

export const BrandResource: ResourceOptions = {
parent: ProductsParent,
}
1 change: 1 addition & 0 deletions app/src/admin/resources/brand/index.ts
@@ -0,0 +1 @@
export { BrandResource as options } from './brand-resource'
1 change: 1 addition & 0 deletions app/src/admin/resources/product/index.ts
@@ -0,0 +1 @@
export { ProductResource as options } from './product-resource'
12 changes: 12 additions & 0 deletions app/src/admin/resources/product/product-resource.ts
@@ -0,0 +1,12 @@
import { ResourceOptions } from 'admin-bro'
import { ProductsParent } from '../../parents'

export const ProductResource: ResourceOptions = {
parent: ProductsParent,
properties: {
brandId: {
reference: 'Brands',
position: 10,
},
},
}
2 changes: 1 addition & 1 deletion app/src/databases/models.type.ts
@@ -1 +1 @@
export type AvailableModels = 'User' | 'BlogPost'
export type AvailableModels = 'User' | 'BlogPost' | 'Brand' | 'Product'
22 changes: 22 additions & 0 deletions app/src/databases/sequelize/brand-model.ts
@@ -0,0 +1,22 @@
import { DataTypes, Model, UUIDV4 } from 'sequelize'
import { sequelize } from './connect'

export interface BrandInterface extends Model {
id: string;
name: string;
}

export const BrandModel = sequelize.define<BrandInterface>('Brands', {
// Model attributes are defined here
id: {
primaryKey: true,
type: DataTypes.UUID,
defaultValue: UUIDV4,
},
name: {
allowNull: false,
type: DataTypes.STRING,
},
}, {
// Other model options go here
})
4 changes: 4 additions & 0 deletions app/src/databases/sequelize/models.ts
@@ -1,8 +1,12 @@
import { AvailableModels } from '../models.type'
import { UserModel } from './user-model'
import { BlogPostModel } from './blog-post-model'
import { BrandModel } from './brand-model'
import { ProductModel } from './product-model'

export const models: Record<AvailableModels, any> = {
User: UserModel,
BlogPost: BlogPostModel,
Brand: BrandModel,
Product: ProductModel,
}
29 changes: 29 additions & 0 deletions app/src/databases/sequelize/product-model.ts
@@ -0,0 +1,29 @@
import { DataTypes, Model, UUIDV4 } from 'sequelize'
import { sequelize } from './connect'

export interface ProductInterface extends Model {
id: string;
name: string;
description?: string;
}

export const ProductModel = sequelize.define<ProductInterface>('Products', {
// Model attributes are defined here
id: {
primaryKey: true,
type: DataTypes.UUID,
defaultValue: UUIDV4,
},
name: {
allowNull: false,
type: DataTypes.STRING,
},
description: {
type: DataTypes.TEXT,
},
brandId: {
type: DataTypes.STRING,
},
}, {
// Other model options go here
})
5 changes: 1 addition & 4 deletions app/src/databases/sequelize/user-model.ts
@@ -1,4 +1,5 @@
import { DataTypes, Model, UUIDV4 } from 'sequelize'
import { BlogPostModel } from './blog-post-model'
import { sequelize } from './connect'

export interface UserInterface extends Model {
Expand Down Expand Up @@ -33,7 +34,3 @@ export const UserModel = sequelize.define<UserInterface>('Users', {
}, {
// Other model options go here
})

UserModel.hasMany(UserModel, {
foreignKey: 'userId',
})
15 changes: 8 additions & 7 deletions app/src/index.ts
Expand Up @@ -7,6 +7,8 @@ import AdminBro from 'admin-bro'
import AdminBroSequelize from '@admin-bro/sequelize'
import * as UserAdmin from './admin/resources/user'
import * as BlogPostAdmin from './admin/resources/blog-post'
import * as BrandAdmin from './admin/resources/brand'
import * as ProductAdmin from './admin/resources/product'

import { connect, models, sessionStore, authenticate, createAdmin } from './databases/sequelize'
import { listen } from './plugins/express'
Expand All @@ -19,13 +21,12 @@ const run = async (): Promise<void> => {

const admin = new AdminBro({
...options,
resources: [{
resource: models.User,
...UserAdmin,
}, {
resource: models.BlogPost,
...BlogPostAdmin,
}],
resources: [
{ resource: models.User, ...UserAdmin },
{ resource: models.BlogPost, ...BlogPostAdmin },
{ resource: models.Brand, ...BrandAdmin },
{ resource: models.Product, ...ProductAdmin },
],
})

await createAdmin()
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -7,7 +7,7 @@
"scripts": {
"test": "mocha ./spec/index.js",
"types": "tsc",
"clean": "rm -rf lib && mkdir lib",
"clean": "rm -rf lib && mkdir lib && rm -fr types && mkdir types",
"build": "babel src --out-dir lib --copy-files --extensions '.ts,.js,.jsx,.tsx'",
"lint": "eslint './spec/**/*.js' './spec/**/*.ts' './src/**/*.js' './src/**/*.jsx' './src/**/*.ts' './src/**/*.tsx' --ignore-pattern='*bundle.*.js' --ignore-pattern='*.d.ts'",
"cover": "NODE_ENV=test nyc --reporter=lcov --reporter=text-lcov npm test",
Expand Down
2 changes: 0 additions & 2 deletions src/backend/adapters/record/params.type.ts
@@ -1,5 +1,3 @@
import { number } from 'prop-types'

/**
* @alias ParamsTypeValue
* @memberof BaseRecord
Expand Down
1 change: 0 additions & 1 deletion src/backend/adapters/resource/base-resource.spec.ts
Expand Up @@ -5,7 +5,6 @@ import chaiAsPromised from 'chai-as-promised'
import BaseResource from './base-resource'
import NotImplementedError from '../../utils/errors/not-implemented-error'
import Filter from '../../utils/filter/filter'
import BaseProperty from '../property/base-property'
import BaseRecord from '../record/base-record'
import AdminBro from '../../../admin-bro'
import ResourceDecorator from '../../decorators/resource/resource-decorator'
Expand Down
115 changes: 88 additions & 27 deletions src/backend/decorators/property/property-decorator.spec.ts
@@ -1,58 +1,119 @@
import { expect } from 'chai'
import sinon from 'sinon'
import sinon, { SinonStubbedInstance } from 'sinon'

import PropertyDecorator from './property-decorator'
import BaseProperty from '../../adapters/property/base-property'
import AdminBro from '../../../admin-bro'
import ResourceDecorator from '../resource/resource-decorator'
import { BaseResource } from '../../adapters'

describe('PropertyDecorator', function () {
describe('PropertyDecorator', () => {
const translatedProperty = 'translated property'
let stubbedAdmin: AdminBro
let stubbedAdmin: SinonStubbedInstance<AdminBro> & AdminBro
let property: BaseProperty
let args: { property: BaseProperty; admin: AdminBro; resource: ResourceDecorator }
let args: {
property: BaseProperty;
admin: typeof stubbedAdmin;
resource: ResourceDecorator;
}

beforeEach(function () {
beforeEach(() => {
property = new BaseProperty({ path: 'name', type: 'string' })
stubbedAdmin = sinon.createStubInstance(AdminBro)
stubbedAdmin.translateProperty = sinon.stub().returns(translatedProperty)
stubbedAdmin.translateProperty = sinon.stub().returns(translatedProperty) as any
args = { property, admin: stubbedAdmin, resource: { id: () => 'someId' } as ResourceDecorator }
})

describe('#isSortable', function () {
it('passes the execution to the base property', function () {
describe('#isSortable', () => {
it('passes the execution to the base property', () => {
sinon.stub(BaseProperty.prototype, 'isSortable').returns(false)
expect(new PropertyDecorator(args).isSortable()).to.equal(false)
})
})

describe('#isVisible', function () {
it('passes execution to BaseProperty.isVisible for list when no options are specified', function () {
describe('#isVisible', () => {
it('passes execution to BaseProperty.isVisible for list when no options are specified', () => {
expect(new PropertyDecorator(args).isVisible('list')).to.equal(property.isVisible())
})

it('passes execution to BaseProperty.isEditable for edit when no options are specified', function () {
it('passes execution to BaseProperty.isEditable for edit when no options are specified', () => {
sinon.stub(BaseProperty.prototype, 'isVisible').returns(false)
expect(new PropertyDecorator(args).isVisible('edit')).to.equal(property.isEditable())
})

it('sets new value when it is changed for all views by isVisible option', function () {
it('sets new value when it is changed for all views by isVisible option', () => {
const decorator = new PropertyDecorator({ ...args, options: { isVisible: false } })
expect(decorator.isVisible('list')).to.equal(false)
expect(decorator.isVisible('edit')).to.equal(false)
expect(decorator.isVisible('show')).to.equal(false)
})
})

describe('#label', function () {
it('returns translated label', function () {
describe('#label', () => {
it('returns translated label', () => {
sinon.stub(BaseProperty.prototype, 'name').returns('normalName')
expect(new PropertyDecorator(args).label()).to.equal(translatedProperty)
})
})

describe('#availableValues', function () {
it('map default value to { value, label } object and uses translations', function () {
describe('#reference', () => {
const rawReferenceValue = 'Article'
const optionsReferenceValue = 'BlogPost'
const ReferenceResource = 'OtherResource' as unknown as BaseResource

beforeEach(() => {
property = new BaseProperty({ path: 'externalId', type: 'reference' })
sinon.stub(property, 'reference').returns(rawReferenceValue)
args.admin.findResource.returns(ReferenceResource)
})

it('returns model from AdminBro for reference name in properties', () => {
new PropertyDecorator({ ...args, property }).reference()

expect(args.admin.findResource).to.have.been.calledWith(rawReferenceValue)
})

it('returns model from options when they are given', () => {
new PropertyDecorator({
...args,
property,
options: {
reference: optionsReferenceValue,
},
}).reference()

expect(args.admin.findResource).to.have.been.calledWith(optionsReferenceValue)
})
})

describe('#type', () => {
const propertyType = 'boolean'

beforeEach(() => {
property = new BaseProperty({ path: 'externalId', type: propertyType })
})

it('returns `reference` type if reference is set in options', () => {
const decorator = new PropertyDecorator({
...args,
property,
options: {
reference: 'SomeReference',
} })

expect(decorator.type()).to.equal('reference')
})

it('returns property reference when no options are given', () => {
const decorator = new PropertyDecorator({ ...args, property })

expect(decorator.type()).to.equal(propertyType)
})
})


describe('#availableValues', () => {
it('map default value to { value, label } object and uses translations', () => {
sinon.stub(BaseProperty.prototype, 'availableValues').returns(['val'])
expect(new PropertyDecorator(args).availableValues()).to.deep.equal([{
value: 'val',
Expand All @@ -61,31 +122,31 @@ describe('PropertyDecorator', function () {
})
})

describe('#position', function () {
it('returns -1 for title field', function () {
describe('#position', () => {
it('returns -1 for title field', () => {
sinon.stub(BaseProperty.prototype, 'isTitle').returns(true)
expect(new PropertyDecorator(args).position()).to.equal(-1)
})

it('returns 101 for second field', function () {
it('returns 101 for second field', () => {
sinon.stub(BaseProperty.prototype, 'isTitle').returns(false)
expect(new PropertyDecorator(args).position()).to.equal(101)
})

it('returns 0 for an id field', function () {
it('returns 0 for an id field', () => {
sinon.stub(BaseProperty.prototype, 'isTitle').returns(false)
sinon.stub(BaseProperty.prototype, 'isId').returns(true)
expect(new PropertyDecorator(args).position()).to.equal(0)
})
})

describe('#subProperties', function () {
describe('#subProperties', () => {
let propertyDecorator: PropertyDecorator
const propertyName = 'super'
const subPropertyName = 'nested'
const subPropertyLabel = 'nestedLabel'

beforeEach(function () {
beforeEach(() => {
property = new BaseProperty({ path: propertyName, type: 'string' })
sinon.stub(property, 'subProperties').returns([
new BaseProperty({ path: subPropertyName, type: 'string' }),
Expand All @@ -102,20 +163,20 @@ describe('PropertyDecorator', function () {
})
})

it('returns the array of decorated properties', function () {
it('returns the array of decorated properties', () => {
expect(propertyDecorator.subProperties()).to.have.lengthOf(1)
expect(propertyDecorator.subProperties()[0]).to.be.instanceOf(PropertyDecorator)
})

it('changes label of the nested property to what was given in PropertyOptions', function () {
it('changes label of the nested property to what was given in PropertyOptions', () => {
const subProperty = propertyDecorator.subProperties()[0]

expect(subProperty.label()).to.eq(translatedProperty)
})
})

describe('#toJSON', function () {
it('returns JSON representation of a property', function () {
describe('#toJSON', () => {
it('returns JSON representation of a property', () => {
expect(new PropertyDecorator(args).toJSON()).to.have.keys(
'isTitle',
'isId',
Expand All @@ -138,7 +199,7 @@ describe('PropertyDecorator', function () {
})
})

afterEach(function () {
afterEach(() => {
sinon.restore()
})
})

0 comments on commit 052661d

Please sign in to comment.