Added parseDataSet to entity configuration

This commit is contained in:
Yossi Kolesnicov 2018-08-06 15:26:46 +03:00
Родитель dd4f567d3c
Коммит e7f7f5a34a
9 изменённых файлов: 152 добавлений и 39 удалений

Просмотреть файл

@ -2,5 +2,6 @@ export interface DataSet<T>{
count:number, count:number,
items:Array<T>, items:Array<T>,
next?:string, next?:string,
previous?:string previous?:string,
meta?:object
} }

Просмотреть файл

@ -4,11 +4,11 @@ import {EntityConfigBase} from "./entity-config.base";
import {ModelBase} from "../models/model.base"; import {ModelBase} from "../models/model.base";
import {EntityId} from "../models/entity-id.type"; import {EntityId} from "../models/entity-id.type";
export interface DataEntityConstructor<TEntity extends ModelBase, TRawData = any, TId extends EntityId = string> extends DataEntityType<TEntity, TRawData, TId>{ export interface DataEntityConstructor<TEntity extends ModelBase, TRawData = any, TId extends EntityId = string, TDataSet = any> extends DataEntityType<TEntity, TRawData, TId, TDataSet>{
new(data?:any, rawData?:TRawData): TEntity new(data?:any, rawData?:TRawData): TEntity
} }
export interface DataEntityType<TEntity extends ModelBase = any, TRawData = any, TId extends EntityId = string>{ export interface DataEntityType<TEntity extends ModelBase = any, TRawData = any, TId extends EntityId = string, TDataSet = any>{
new(data?:EntityModelConfigBase, rawData?:TRawData):TEntity, new(data?:EntityModelConfigBase, rawData?:TRawData):TEntity,
singularName?:string, singularName?:string,
pluralName?:string, pluralName?:string,

Просмотреть файл

@ -5,10 +5,10 @@ import {DataQuery} from "../dataset/data-query";
import {HttpOptions, RequestMethod} from "../services/http.service"; import {HttpOptions, RequestMethod} from "../services/http.service";
import {ModelBase} from "../models/model.base"; import {ModelBase} from "../models/model.base";
import {ApiCallBackendConfigInterface} from "../models/api-call-backend-config.interface"; import {ApiCallBackendConfigInterface} from "../models/api-call-backend-config.interface";
import {EntityModelBase} from "../models/entity-model.base";
import {EntityId} from "../models/entity-id.type"; import {EntityId} from "../models/entity-id.type";
import {DataSet} from "../dataset/dataset";
export class ModelEntity<TEntity extends ModelBase = any, TRawData = any, TId extends EntityId = string> extends EntityConfigBase<TEntity, TRawData, TId> implements EntityConfig<TEntity, TRawData, TId> { export class ModelEntity<TEntity extends ModelBase = any, TRawData = any, TId extends EntityId = string, TDataSet = any> extends EntityConfigBase<TEntity, TRawData, TId> implements EntityConfig<TEntity, TRawData, TId> {
endpoint:EntityConfigFunctionOrValue; endpoint:EntityConfigFunctionOrValue;
loadAll?:boolean = false; loadAll?:boolean = false;
cache?:boolean | ModelEntityCacheConfig<TEntity>; cache?:boolean | ModelEntityCacheConfig<TEntity>;
@ -17,6 +17,7 @@ export class ModelEntity<TEntity extends ModelBase = any, TRawData = any, TId ex
allItemsEndpoint?:string; allItemsEndpoint?:string;
allItemsEndpointTrailingSlash?:boolean; allItemsEndpointTrailingSlash?:boolean;
parseDataQuery?:(dataQuery:DataQuery) => { [index:string]:any }; parseDataQuery?:(dataQuery:DataQuery) => { [index:string]:any };
parseDataSet?:(dataSet:TDataSet) => DataSet<TRawData>;
parseItemQuery?:(itemId:EntityId, entity?:IEntityConfigBase<TEntity, TRawData, TId>, config?:ParisConfig, params?:{ [index:string]:any }) => string; parseItemQuery?:(itemId:EntityId, entity?:IEntityConfigBase<TEntity, TRawData, TId>, config?:ParisConfig, params?:{ [index:string]:any }) => string;
parseSaveQuery?:(item:TEntity, entity?:IEntityConfigBase, config?:ParisConfig) => string; parseSaveQuery?:(item:TEntity, entity?:IEntityConfigBase, config?:ParisConfig) => string;
parseRemoveQuery?:(items:Array<TEntity>, entity?:IEntityConfigBase, config?:ParisConfig) => string; parseRemoveQuery?:(items:Array<TEntity>, entity?:IEntityConfigBase, config?:ParisConfig) => string;
@ -35,11 +36,12 @@ export class ModelEntity<TEntity extends ModelBase = any, TRawData = any, TId ex
export interface EntityConfig< export interface EntityConfig<
TEntity extends ModelBase, TEntity extends ModelBase,
TRawData = any, TRawData = any,
TId extends EntityId = string> TId extends EntityId = string,
extends IEntityConfigBase<TEntity, TRawData, TId>, EntityBackendConfig<TEntity, TRawData, TId> TDataSet = any>
extends IEntityConfigBase<TEntity, TRawData, TId>, EntityBackendConfig<TEntity, TRawData, TId, TDataSet>
{ } { }
export interface EntityBackendConfig<TEntity extends ModelBase, TRawData = any, TId extends EntityId = string> extends ApiCallBackendConfigInterface{ export interface EntityBackendConfig<TEntity extends ModelBase, TRawData = any, TId extends EntityId = string, TDataSet = any> 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. * 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. * 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<TEntity extends ModelBase, TRawData = any,
*/ */
parseDataQuery?:(dataQuery:DataQuery) => { [index:string]:any }, parseDataQuery?:(dataQuery:DataQuery) => { [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 <caption>Parsing a DataSet from a raw object returned by the backend</caption>
* ```typescript
* parseDataSet: (rawDataSet:TodoRawDataSet) => ({ items: rawDataSet.todoItems, next: rawDataSet.$nextPage, count: rawDataSet.total })
* ```
* @param dataSet
*/
parseDataSet?:(dataSet:TDataSet) => DataSet<TRawData>;
/** /**
* 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}. * 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. * `parseItemQuery` allows to specify a different URL. This is useful if your API doesn't follow the REST standard.

Просмотреть файл

@ -2,8 +2,8 @@ import {EntityConfig, ModelEntity} from "./entity.config";
import {DataEntityType} from "./data-entity.base"; import {DataEntityType} from "./data-entity.base";
import {entitiesService} from "../services/entities.service"; import {entitiesService} from "../services/entities.service";
export function Entity(config:EntityConfig<any, any, any>){ export function Entity(config:EntityConfig<any, any, any, any>){
return (target:DataEntityType<any, any, any>) => { return (target:DataEntityType<any, any, any, any>) => {
let entity:ModelEntity = new ModelEntity(config, target.prototype.constructor); let entity:ModelEntity = new ModelEntity(config, target.prototype.constructor);
target.entityConfig = entity; target.entityConfig = entity;
target.singularName = config.singularName; target.singularName = config.singularName;

Просмотреть файл

@ -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<number>{
@EntityField()
name:string;
}
interface TodoListRawDataSet {
lists: Array<any>,
$nextPage: string,
total: number,
lastUpdate: number
}

Просмотреть файл

@ -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<SimpleEntity>;
before(() => {
console.log("REFLECT", Reflect);
dataSet = parseDataSet<SimpleEntity, RawDataSet>(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<SimpleEntity>{
return {
items: rawDataSet.results,
next: rawDataSet.$next,
count: rawDataSet.total
}
}
interface SimpleEntity{
id:number,
name:string
}
interface RawDataSet {
results: Array<SimpleEntity>,
$next: string,
total: number
}

Просмотреть файл

@ -8,46 +8,53 @@ import {DataSet} from "../dataset/dataset";
import {ReadonlyRepository} from "./readonly-repository"; import {ReadonlyRepository} from "./readonly-repository";
import {map} from "rxjs/operators"; import {map} from "rxjs/operators";
export function rawDataToDataSet<T extends ModelBase, R = any>( const DEFAULT_ALL_ITEMS_PROPERTY = 'items';
rawDataSet:any,
entityConstructor:DataEntityConstructor<T>, export function rawDataToDataSet<TEntity extends ModelBase, TRawData = any, TDataSet extends any = any>(
rawDataSet:TDataSet,
entityConstructor:DataEntityConstructor<TEntity>,
allItemsProperty:string, allItemsProperty:string,
paris:Paris, paris:Paris,
dataOptions:DataOptions = defaultDataOptions, dataOptions:DataOptions = defaultDataOptions,
query?:DataQuery):Observable<DataSet<T>>{ query?:DataQuery):Observable<DataSet<TEntity>>{
let rawItems: Array<R> = rawDataSet instanceof Array ? rawDataSet : rawDataSet[allItemsProperty]; let dataSet:DataSet<TRawData> = parseDataSet(rawDataSet, allItemsProperty, entityConstructor.entityConfig.parseDataSet);
if (!rawItems || !rawItems.length) if (!dataSet.items || !dataSet.items.length)
return of({ count: 0, items: [] }); return of({ count: 0, items: [] });
return modelArray<T, R>(rawItems, entityConstructor, paris, dataOptions, query).pipe( return modelArray<TEntity, TRawData>(dataSet.items, entityConstructor, paris, dataOptions, query).pipe(
map((items:Array<T>) => { map((items:Array<TEntity>) => {
return Object.freeze({ return Object.freeze(Object.assign(dataSet, {
count: rawDataSet.count, items: items,
items: items, }));
next: rawDataSet.next, })
previous: rawDataSet.previous );
}); }
})
);
}
export function modelArray<T extends ModelBase, R = any>( export function parseDataSet<TRawData = any, TDataSet extends any = any>(rawDataSet:TDataSet, allItemsProperty:string = DEFAULT_ALL_ITEMS_PROPERTY, parseDataSet?:(rawDataSet:TDataSet) => DataSet<TRawData>):DataSet<TRawData>{
data:Array<any>, return rawDataSet instanceof Array
entityConstructor:DataEntityConstructor<T>, ? { count: 0, items: rawDataSet }
: parseDataSet
? parseDataSet(rawDataSet) || { count: 0, items: [] }
: { count: rawDataSet.count, items: rawDataSet[allItemsProperty] };
}
export function modelArray<TEntity extends ModelBase, TRawData = any>(
rawData:Array<TRawData>,
entityConstructor:DataEntityConstructor<TEntity>,
paris:Paris, paris:Paris,
dataOptions:DataOptions = defaultDataOptions, dataOptions:DataOptions = defaultDataOptions,
query?:DataQuery):Observable<Array<T>>{ query?:DataQuery):Observable<Array<TEntity>>{
if (!data.length) if (!rawData.length)
return of([]); return of([]);
else { else {
const itemCreators: Array<Observable<T>> = data.map((itemData: R) => const itemCreators: Array<Observable<TEntity>> = rawData.map((itemData: TRawData) =>
modelItem<T, R>(entityConstructor, itemData, paris, dataOptions, query)); modelItem<TEntity, TRawData>(entityConstructor, itemData, paris, dataOptions, query));
return combineLatest.apply(this, itemCreators); return combineLatest.apply(this, itemCreators);
} }
} }
export function modelItem<T extends ModelBase, R = any>(entityConstructor:DataEntityConstructor<T>, data:R, paris:Paris, dataOptions: DataOptions = defaultDataOptions, query?:DataQuery):Observable<T>{ export function modelItem<TEntity extends ModelBase, TRawData = any>(entityConstructor:DataEntityConstructor<TEntity>, rawData:TRawData, paris:Paris, dataOptions: DataOptions = defaultDataOptions, query?:DataQuery):Observable<TEntity>{
return ReadonlyRepository.getModelData(data, entityConstructor.entityConfig || entityConstructor.valueObjectConfig, paris.config, paris, dataOptions, query); return ReadonlyRepository.getModelData(rawData, entityConstructor.entityConfig || entityConstructor.valueObjectConfig, paris.config, paris, dataOptions, query);
} }

Просмотреть файл

@ -12,7 +12,7 @@
"build": "gulp build", "build": "gulp build",
"dev": "rollup -c -w", "dev": "rollup -c -w",
"prepublishOnly": "npm run build", "prepublishOnly": "npm run build",
"test": "mocha -r ts-node/register lib/**/*.spec.ts", "test": "mocha --opts ./test/mocha.opts",
"docs": "typedoc --options typedocconfig.ts" "docs": "typedoc --options typedocconfig.ts"
}, },
"author": "Yossi Kolesnicov", "author": "Yossi Kolesnicov",
@ -50,7 +50,7 @@
"gulp-typescript": "^3.1.3", "gulp-typescript": "^3.1.3",
"husky": "^1.0.0-rc.13", "husky": "^1.0.0-rc.13",
"intl": "^1.2.5", "intl": "^1.2.5",
"lodash.get": "^4.4.2", "lodash-es": "4.17.10",
"merge2": "^1.0.2", "merge2": "^1.0.2",
"mocha": "^5.2.0", "mocha": "^5.2.0",
"reflect-metadata": "^0.1.12", "reflect-metadata": "^0.1.12",

2
test/mocha.opts Normal file
Просмотреть файл

@ -0,0 +1,2 @@
--ui mocha-typescript
lib/**/*.spec.ts