зеркало из https://github.com/microsoft/paris.git
Added parseDataSet to entity configuration
This commit is contained in:
Родитель
dd4f567d3c
Коммит
e7f7f5a34a
|
@ -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",
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
--ui mocha-typescript
|
||||||
|
lib/**/*.spec.ts
|
Загрузка…
Ссылка в новой задаче