From e7f7f5a34a1b5e56bd065416ff5bca8d8af25fb0 Mon Sep 17 00:00:00 2001 From: Yossi Kolesnicov Date: Mon, 6 Aug 2018 15:26:46 +0300 Subject: [PATCH] Added parseDataSet to entity configuration --- lib/dataset/dataset.ts | 3 +- lib/entity/data-entity.base.ts | 4 +- lib/entity/entity.config.ts | 24 ++++++++--- lib/entity/entity.decorator.ts | 4 +- lib/mock/todo-list.entity.ts | 28 +++++++++++++ lib/repository/data-to-model.spec.ts | 61 ++++++++++++++++++++++++++++ lib/repository/data-to-model.ts | 61 ++++++++++++++++------------ package.json | 4 +- test/mocha.opts | 2 + 9 files changed, 152 insertions(+), 39 deletions(-) create mode 100644 lib/mock/todo-list.entity.ts create mode 100644 lib/repository/data-to-model.spec.ts create mode 100644 test/mocha.opts diff --git a/lib/dataset/dataset.ts b/lib/dataset/dataset.ts index 9a2642b..6c6afd7 100644 --- a/lib/dataset/dataset.ts +++ b/lib/dataset/dataset.ts @@ -2,5 +2,6 @@ export interface DataSet{ count:number, items:Array, next?:string, - previous?:string + previous?:string, + meta?:object } diff --git a/lib/entity/data-entity.base.ts b/lib/entity/data-entity.base.ts index cd249e4..26dfd64 100644 --- a/lib/entity/data-entity.base.ts +++ b/lib/entity/data-entity.base.ts @@ -4,11 +4,11 @@ import {EntityConfigBase} from "./entity-config.base"; import {ModelBase} from "../models/model.base"; import {EntityId} from "../models/entity-id.type"; -export interface DataEntityConstructor extends DataEntityType{ +export interface DataEntityConstructor extends DataEntityType{ new(data?:any, rawData?:TRawData): TEntity } -export interface DataEntityType{ +export interface DataEntityType{ new(data?:EntityModelConfigBase, rawData?:TRawData):TEntity, singularName?:string, pluralName?:string, diff --git a/lib/entity/entity.config.ts b/lib/entity/entity.config.ts index 96e8079..59995df 100644 --- a/lib/entity/entity.config.ts +++ b/lib/entity/entity.config.ts @@ -5,10 +5,10 @@ import {DataQuery} from "../dataset/data-query"; import {HttpOptions, RequestMethod} from "../services/http.service"; import {ModelBase} from "../models/model.base"; import {ApiCallBackendConfigInterface} from "../models/api-call-backend-config.interface"; -import {EntityModelBase} from "../models/entity-model.base"; import {EntityId} from "../models/entity-id.type"; +import {DataSet} from "../dataset/dataset"; -export class ModelEntity extends EntityConfigBase implements EntityConfig { +export class ModelEntity extends EntityConfigBase implements EntityConfig { endpoint:EntityConfigFunctionOrValue; loadAll?:boolean = false; cache?:boolean | ModelEntityCacheConfig; @@ -17,6 +17,7 @@ export class ModelEntity { [index:string]:any }; + parseDataSet?:(dataSet:TDataSet) => DataSet; parseItemQuery?:(itemId:EntityId, entity?:IEntityConfigBase, config?:ParisConfig, params?:{ [index:string]:any }) => string; parseSaveQuery?:(item:TEntity, entity?:IEntityConfigBase, config?:ParisConfig) => string; parseRemoveQuery?:(items:Array, entity?:IEntityConfigBase, config?:ParisConfig) => string; @@ -35,11 +36,12 @@ export class ModelEntity - extends IEntityConfigBase, EntityBackendConfig + TId extends EntityId = string, + TDataSet = any> + extends IEntityConfigBase, EntityBackendConfig { } -export interface EntityBackendConfig extends ApiCallBackendConfigInterface{ +export interface EntityBackendConfig extends ApiCallBackendConfigInterface{ /** * If true, all the Entity's items are fetched whenever any is needed, and then cached so subsequent requests are retrieved from cache rather than backend. * This makes sense to use for Entities whose values are few and not expected to change, such as enums. @@ -101,6 +103,18 @@ export interface EntityBackendConfig { [index:string]:any }, + /** + * For query results, Paris accepts either an array of items or an object. That object may contain properties such as 'count', 'next' and 'previous'. + * `parseDataSet`, if available, receives the object as it was returned from the API and parses it to a DataSet interface, so the original properties are available in the DataSet. + * + * @example Parsing a DataSet from a raw object returned by the backend + * ```typescript + * parseDataSet: (rawDataSet:TodoRawDataSet) => ({ items: rawDataSet.todoItems, next: rawDataSet.$nextPage, count: rawDataSet.total }) + * ``` + * @param dataSet + */ + parseDataSet?:(dataSet:TDataSet) => DataSet; + /** * When getting an Entity from backend (when calling repository.getItemById), Paris follows the REST standard and fetches it by GET from /{the Entity's endpoint}/{ID}. * `parseItemQuery` allows to specify a different URL. This is useful if your API doesn't follow the REST standard. diff --git a/lib/entity/entity.decorator.ts b/lib/entity/entity.decorator.ts index de5808c..a94c7ce 100644 --- a/lib/entity/entity.decorator.ts +++ b/lib/entity/entity.decorator.ts @@ -2,8 +2,8 @@ import {EntityConfig, ModelEntity} from "./entity.config"; import {DataEntityType} from "./data-entity.base"; import {entitiesService} from "../services/entities.service"; -export function Entity(config:EntityConfig){ - return (target:DataEntityType) => { +export function Entity(config:EntityConfig){ + return (target:DataEntityType) => { let entity:ModelEntity = new ModelEntity(config, target.prototype.constructor); target.entityConfig = entity; target.singularName = config.singularName; diff --git a/lib/mock/todo-list.entity.ts b/lib/mock/todo-list.entity.ts new file mode 100644 index 0000000..78e9e58 --- /dev/null +++ b/lib/mock/todo-list.entity.ts @@ -0,0 +1,28 @@ +import {EntityModelBase} from "../models/entity-model.base"; +import {Entity} from "../entity/entity.decorator"; +import {EntityField} from "../entity/entity-field.decorator"; + +@Entity({ + singularName: "Todo list", + pluralName: "Todo lists", + endpoint: "list", + parseDataSet: (rawDataSet:TodoListRawDataSet) => ({ + items: rawDataSet.lists, + next: rawDataSet.$nextPage, + count: rawDataSet.total, + meta: { + lastUpdate: new Date(rawDataSet.lastUpdate) + } + }) +}) +export class TodoList extends EntityModelBase{ + @EntityField() + name:string; +} + +interface TodoListRawDataSet { + lists: Array, + $nextPage: string, + total: number, + lastUpdate: number +} diff --git a/lib/repository/data-to-model.spec.ts b/lib/repository/data-to-model.spec.ts new file mode 100644 index 0000000..f24e636 --- /dev/null +++ b/lib/repository/data-to-model.spec.ts @@ -0,0 +1,61 @@ +import { suite, test, slow, timeout } from "mocha-typescript"; +import "reflect-metadata" +import * as chai from 'chai'; +import '../services/paris.init.spec'; +import {DataSet} from "../dataset/dataset"; +import {parseDataSet} from "./data-to-model"; + +const expect = chai.expect; + +const rawDataSet:RawDataSet = { + results: [ + { + id: 1, + name: "First" + }, + { + id: 2, + name: "Seconds" + } + ], + $next: '/api/todolist?page=2', + total: 123 +}; + +describe('Raw data -> model', () => { + describe('Create a DataSet', () => { + let dataSet:DataSet; + + before(() => { + console.log("REFLECT", Reflect); + dataSet = parseDataSet(rawDataSet, 'results', parseRawDataSet); + }); + + it('has items', () => { + expect(dataSet.items.length).to.equal(rawDataSet.results.length); + }); + + it('has a next property', () => { + expect(dataSet.next).to.equal(rawDataSet.$next); + }) + }); +}); + +function parseRawDataSet(rawDataSet:RawDataSet):DataSet{ + return { + items: rawDataSet.results, + next: rawDataSet.$next, + count: rawDataSet.total + } +} + +interface SimpleEntity{ + id:number, + name:string +} + +interface RawDataSet { + results: Array, + $next: string, + total: number +} diff --git a/lib/repository/data-to-model.ts b/lib/repository/data-to-model.ts index 46949ab..a2ecc73 100644 --- a/lib/repository/data-to-model.ts +++ b/lib/repository/data-to-model.ts @@ -8,46 +8,53 @@ import {DataSet} from "../dataset/dataset"; import {ReadonlyRepository} from "./readonly-repository"; import {map} from "rxjs/operators"; -export function rawDataToDataSet( - rawDataSet:any, - entityConstructor:DataEntityConstructor, +const DEFAULT_ALL_ITEMS_PROPERTY = 'items'; + +export function rawDataToDataSet( + rawDataSet:TDataSet, + entityConstructor:DataEntityConstructor, allItemsProperty:string, paris:Paris, dataOptions:DataOptions = defaultDataOptions, - query?:DataQuery):Observable>{ - let rawItems: Array = rawDataSet instanceof Array ? rawDataSet : rawDataSet[allItemsProperty]; + query?:DataQuery):Observable>{ + let dataSet:DataSet = parseDataSet(rawDataSet, allItemsProperty, entityConstructor.entityConfig.parseDataSet); - if (!rawItems || !rawItems.length) - return of({ count: 0, items: [] }); + if (!dataSet.items || !dataSet.items.length) + return of({ count: 0, items: [] }); - return modelArray(rawItems, entityConstructor, paris, dataOptions, query).pipe( - map((items:Array) => { - return Object.freeze({ - count: rawDataSet.count, - items: items, - next: rawDataSet.next, - previous: rawDataSet.previous - }); - }) - ); - } + return modelArray(dataSet.items, entityConstructor, paris, dataOptions, query).pipe( + map((items:Array) => { + return Object.freeze(Object.assign(dataSet, { + items: items, + })); + }) + ); +} -export function modelArray( - data:Array, - entityConstructor:DataEntityConstructor, +export function parseDataSet(rawDataSet:TDataSet, allItemsProperty:string = DEFAULT_ALL_ITEMS_PROPERTY, parseDataSet?:(rawDataSet:TDataSet) => DataSet):DataSet{ + return rawDataSet instanceof Array + ? { count: 0, items: rawDataSet } + : parseDataSet + ? parseDataSet(rawDataSet) || { count: 0, items: [] } + : { count: rawDataSet.count, items: rawDataSet[allItemsProperty] }; +} + +export function modelArray( + rawData:Array, + entityConstructor:DataEntityConstructor, paris:Paris, dataOptions:DataOptions = defaultDataOptions, - query?:DataQuery):Observable>{ - if (!data.length) + query?:DataQuery):Observable>{ + if (!rawData.length) return of([]); else { - const itemCreators: Array> = data.map((itemData: R) => - modelItem(entityConstructor, itemData, paris, dataOptions, query)); + const itemCreators: Array> = rawData.map((itemData: TRawData) => + modelItem(entityConstructor, itemData, paris, dataOptions, query)); return combineLatest.apply(this, itemCreators); } } -export function modelItem(entityConstructor:DataEntityConstructor, data:R, paris:Paris, dataOptions: DataOptions = defaultDataOptions, query?:DataQuery):Observable{ - return ReadonlyRepository.getModelData(data, entityConstructor.entityConfig || entityConstructor.valueObjectConfig, paris.config, paris, dataOptions, query); +export function modelItem(entityConstructor:DataEntityConstructor, rawData:TRawData, paris:Paris, dataOptions: DataOptions = defaultDataOptions, query?:DataQuery):Observable{ + return ReadonlyRepository.getModelData(rawData, entityConstructor.entityConfig || entityConstructor.valueObjectConfig, paris.config, paris, dataOptions, query); } diff --git a/package.json b/package.json index 94d1b00..4986f74 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "build": "gulp build", "dev": "rollup -c -w", "prepublishOnly": "npm run build", - "test": "mocha -r ts-node/register lib/**/*.spec.ts", + "test": "mocha --opts ./test/mocha.opts", "docs": "typedoc --options typedocconfig.ts" }, "author": "Yossi Kolesnicov", @@ -50,7 +50,7 @@ "gulp-typescript": "^3.1.3", "husky": "^1.0.0-rc.13", "intl": "^1.2.5", - "lodash.get": "^4.4.2", + "lodash-es": "4.17.10", "merge2": "^1.0.2", "mocha": "^5.2.0", "reflect-metadata": "^0.1.12", diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..76d3666 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,2 @@ +--ui mocha-typescript +lib/**/*.spec.ts