зеркало из https://github.com/microsoft/paris.git
Documentation for repositories.
This commit is contained in:
Родитель
350f495c85
Коммит
ed326be24e
|
@ -1,5 +1,19 @@
|
|||
/**
|
||||
* The modes for creating sub-models in a model.
|
||||
*/
|
||||
export enum DataAvailability{
|
||||
/**
|
||||
* All sub-models are created. Those that are not cached in Repositories will be fetched from backend.
|
||||
*/
|
||||
deep,
|
||||
|
||||
/**
|
||||
* Only sub-models of the requested model are fetched. Sub-models of sub-models won't be fetched from backend.
|
||||
*/
|
||||
flat,
|
||||
|
||||
/**
|
||||
* Only sub-models that are cached will be created. Data for sub-models won't be fetched from backend.
|
||||
*/
|
||||
available
|
||||
}
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
import {DataAvailability} from "./data-availability.enum";
|
||||
|
||||
/**
|
||||
* Configuration for fetching data for modeling
|
||||
*/
|
||||
export interface DataOptions{
|
||||
/**
|
||||
* If true, Paris will first look for cached data, before fetching data from backend.
|
||||
*/
|
||||
allowCache?:boolean,
|
||||
|
||||
/**
|
||||
* The {DataAvailability} mode for fetching data.
|
||||
*/
|
||||
availability?: DataAvailability
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import {ParisConfig} from "../config/paris-config";
|
|||
import {DataQuery} from "../dataset/data-query";
|
||||
|
||||
/**
|
||||
* Configuration for a model field decorator
|
||||
* Configuration for a model EntityField decorator
|
||||
*/
|
||||
export interface FieldConfig{
|
||||
/**
|
||||
|
|
|
@ -3,6 +3,12 @@ import {FieldConfig} from "./entity-field.config";
|
|||
import {entityFieldsService} from "../services/entity-fields.service";
|
||||
import {Field} from "./entity-field";
|
||||
|
||||
/**
|
||||
* All properties of models (Entity/ValueObject) that should be handled by Paris should be decorated with `EntityField`.
|
||||
* When Paris creates an instance of a model, it maps the raw data arrived from backend to class properties, through EntityFields.
|
||||
*
|
||||
* @param {FieldConfig} fieldConfig
|
||||
*/
|
||||
export function EntityField(fieldConfig?:FieldConfig):PropertyDecorator {
|
||||
return function (entityPrototype: DataEntityType, propertyKey: string | symbol) {
|
||||
let propertyConstructor:Function = (<any>Reflect).getMetadata("design:type", entityPrototype, propertyKey);
|
||||
|
|
|
@ -25,7 +25,12 @@ import {EntityErrorEvent, EntityErrorTypes} from "../events/entity-error.event";
|
|||
import {catchError, map, mergeMap, tap} from "rxjs/operators";
|
||||
import {IReadonlyRepository} from "./repository.interface";
|
||||
import {FIELD_DATA_SELF} from "../entity/entity-field.config";
|
||||
import {EntityId} from "../models/entity-id.type";
|
||||
|
||||
/**
|
||||
* A Repository is a service through which all of an Entity's data is fetched, cached and saved back to the backend
|
||||
* `ReadonlyRepository` is the base class for all Repositories, and the class used for Repositories that are readonly.
|
||||
*/
|
||||
export class ReadonlyRepository<TEntity extends ModelBase<TRawData>, TRawData = object> implements IReadonlyRepository<TEntity>{
|
||||
protected _errorSubject$: Subject<EntityErrorEvent>;
|
||||
error$: Observable<EntityErrorEvent>;
|
||||
|
@ -53,6 +58,11 @@ export class ReadonlyRepository<TEntity extends ModelBase<TRawData>, TRawData =
|
|||
return this.entityBackendConfig.baseUrl instanceof Function ? this.entityBackendConfig.baseUrl(this.config, query) : this.entityBackendConfig.baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* An Observable for all the items of this entity. If the Entity has already loaded all possible items (if `loadAll` is set to `true`, for example), those items are returned.
|
||||
* Otherwise, a query with no DataQuery will be performed to the backend and the data will be fetched.
|
||||
* @returns {Observable<Array<TEntity extends ModelBase<TRawData>>>}
|
||||
*/
|
||||
get allItems$(): Observable<Array<TEntity>> {
|
||||
if (this._allValues)
|
||||
return merge(of(this._allValues), this._allItemsSubject$.asObservable());
|
||||
|
@ -71,10 +81,19 @@ export class ReadonlyRepository<TEntity extends ModelBase<TRawData>, TRawData =
|
|||
return this._cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* The base URL for this Repository's API calls (not including base URL - the domain)
|
||||
* @returns {string}
|
||||
*/
|
||||
get endpointName():string{
|
||||
return this.entityBackendConfig.endpoint instanceof Function ? this.entityBackendConfig.endpoint(this.config) : this.entityBackendConfig.endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full URL for an API call
|
||||
* @param {DataQuery} query
|
||||
* @returns {string}
|
||||
*/
|
||||
getEndpointUrl(query?: DataQuery): string{
|
||||
return `${this.getBaseUrl(query)}/${this.endpointName}`;
|
||||
}
|
||||
|
@ -93,6 +112,11 @@ export class ReadonlyRepository<TEntity extends ModelBase<TRawData>, TRawData =
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the Repository's entity.
|
||||
* All fields will be undefined, except those that have defaultValue or those that are arrays, which will have an empty array as value.
|
||||
* @returns {TEntity}
|
||||
*/
|
||||
createNewItem(): TEntity {
|
||||
let defaultData:{ [index:string]:any } = {};
|
||||
this.entity.fieldsArray.forEach((field:Field) => {
|
||||
|
@ -105,10 +129,34 @@ export class ReadonlyRepository<TEntity extends ModelBase<TRawData>, TRawData =
|
|||
return new this.entityConstructor(defaultData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a full model of this Repository's Entity. Any sub-models that need to be fetched from backend will be fetched (if options.availability === DataAvailability.deep).
|
||||
* This method is used internally when modeling entities and value objects, but may be used externally as well, in case an item should be created programmatically from raw data.
|
||||
* @param {TRawData} rawData The raw data for the entity, as it arrives from backend
|
||||
* @param {DataOptions} options
|
||||
* @param {DataQuery} query
|
||||
* @returns {Observable<TEntity extends ModelBase<TRawData>>}
|
||||
*/
|
||||
createItem(rawData: TRawData, options: DataOptions = { allowCache: true, availability: DataAvailability.available }, query?: DataQuery): Observable<TEntity> {
|
||||
return ReadonlyRepository.getModelData<TEntity>(rawData, this.entity, this.config, this.paris, options, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets multiple items from backend.
|
||||
* The backend may add paging information, such as count, page, etc, so a DataSet object is returned rather than just an Array.
|
||||
*
|
||||
* @example <caption>Get all Todo items</caption>
|
||||
* repository.query()
|
||||
* .subscribe((todoItems:DataSet<TodoItem>) => console.log('Current items: ', todoItems.items));
|
||||
*
|
||||
* @example <caption>Get all Todo items, sorted by name</caption>
|
||||
* repository.query({ sortBy: { field: 'name' }})
|
||||
* .subscribe((todoItems:DataSet<TodoItem>) => console.log('Items by name: ', todoItems.items));
|
||||
*
|
||||
* @param {DataQuery} query
|
||||
* @param {DataOptions} dataOptions
|
||||
* @returns {Observable<DataSet<TEntity extends ModelBase<TRawData>>>}
|
||||
*/
|
||||
query(query?: DataQuery, dataOptions: DataOptions = defaultDataOptions): Observable<DataSet<TEntity>> {
|
||||
if (this.entityConstructor.entityConfig && !this.entityConstructor.entityConfig.supportsGetMethod(EntityGetMethod.query))
|
||||
throw new Error(`Can't query ${this.entityConstructor.singularName}, query isn't supported.`);
|
||||
|
@ -116,6 +164,13 @@ export class ReadonlyRepository<TEntity extends ModelBase<TRawData>, TRawData =
|
|||
return this.paris.callQuery(this.entityConstructor, this.entityBackendConfig, query, dataOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link ReadonlyRepository#query|query}, but returns a single item rather than a {DataSet}.
|
||||
* Useful for when we require to fetch a single model from backend, but it's either a ValueObject (so we can't refer to it by ID) or it's fetched by a more complex data query.
|
||||
* @param {DataQuery} query
|
||||
* @param {DataOptions} dataOptions
|
||||
* @returns {Observable<TEntity extends ModelBase<TRawData>>}
|
||||
*/
|
||||
queryItem(query: DataQuery, dataOptions: DataOptions = defaultDataOptions): Observable<TEntity> {
|
||||
let httpOptions:HttpOptions = this.entityBackendConfig.parseDataQuery ? { params: this.entityBackendConfig.parseDataQuery(query) } : queryToHttpOptions(query);
|
||||
|
||||
|
@ -154,15 +209,28 @@ export class ReadonlyRepository<TEntity extends ModelBase<TRawData>, TRawData =
|
|||
return getItem$;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all cached data in this Repository
|
||||
*/
|
||||
clearCache():void {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the cached values in the Repository, if they were set as a result of using allItems$ or `loadAll: true`.
|
||||
*/
|
||||
clearAllValues():void {
|
||||
this._allValues = null;
|
||||
}
|
||||
|
||||
getItemById(itemId: string | number, options: DataOptions = defaultDataOptions, params?:{ [index:string]:any }): Observable<TEntity> {
|
||||
/**
|
||||
* Fetches an item from backend, for the specified ID, or from cache, if it's available.
|
||||
* @param {string | number} itemId
|
||||
* @param {DataOptions} options
|
||||
* @param {{[p: string]: any}} params
|
||||
* @returns {Observable<TEntity extends ModelBase<TRawData>>}
|
||||
*/
|
||||
getItemById(itemId: EntityId, options: DataOptions = defaultDataOptions, params?:{ [index:string]:any }): Observable<TEntity> {
|
||||
if (!this.entityConstructor.entityConfig.supportsGetMethod(EntityGetMethod.getItem))
|
||||
throw new Error(`Can't get ${this.entityConstructor.singularName}, getItem isn't supported.`);
|
||||
|
||||
|
@ -219,7 +287,7 @@ export class ReadonlyRepository<TEntity extends ModelBase<TRawData>, TRawData =
|
|||
|
||||
/**
|
||||
* Creates a JSON object that can be saved to server, with the reverse logic of getItemModelData
|
||||
* @param {T} item
|
||||
* @param {TEntity} item
|
||||
* @returns {Index}
|
||||
*/
|
||||
serializeItem(item:TEntity, serializationData?:any): TRawData {
|
||||
|
@ -229,8 +297,9 @@ export class ReadonlyRepository<TEntity extends ModelBase<TRawData>, TRawData =
|
|||
}
|
||||
|
||||
/**
|
||||
* Populates the item dataset with any sub @model. For example, if an ID is found for a property whose type is an entity,
|
||||
* Populates the item dataset with any sub-model. For example, if an ID is found for a property whose type is an entity,
|
||||
* the property's value will be an instance of that entity, for the ID, not the ID.
|
||||
* This method does the actual heavy lifting required for modeling an Entity or ValueObject - parses the fields, models sub-models, etc.
|
||||
* @param {Index} rawData
|
||||
* @param {EntityConfigBase} entity
|
||||
* @param {ParisConfig} config
|
||||
|
@ -436,7 +505,7 @@ export class ReadonlyRepository<TEntity extends ModelBase<TRawData>, TRawData =
|
|||
}
|
||||
|
||||
/**
|
||||
* Serializes an object value
|
||||
* Serializes an an entity into raw data, so it can be sent back to backend.
|
||||
* @param item
|
||||
* @returns {Index}
|
||||
*/
|
||||
|
|
|
@ -17,23 +17,23 @@ import {AjaxError} from "rxjs/ajax";
|
|||
import {catchError, map, mergeMap, tap} from "rxjs/operators";
|
||||
import {DataSet} from "../dataset/dataset";
|
||||
|
||||
export class Repository<T extends ModelBase> extends ReadonlyRepository<T> implements IRepository<T> {
|
||||
export class Repository<TEntity extends ModelBase> extends ReadonlyRepository<TEntity> implements IRepository<TEntity> {
|
||||
save$: Observable<SaveEntityEvent>;
|
||||
remove$: Observable<RemoveEntitiesEvent>;
|
||||
|
||||
private _saveSubject$: Subject<SaveEntityEvent>;
|
||||
private _removeSubject$: Subject<RemoveEntitiesEvent>;
|
||||
|
||||
constructor(entity: EntityConfig<T>,
|
||||
constructor(entity: EntityConfig<TEntity>,
|
||||
config: ParisConfig,
|
||||
entityConstructor: DataEntityConstructor<T>,
|
||||
entityConstructor: DataEntityConstructor<TEntity>,
|
||||
dataStore: DataStoreService,
|
||||
paris: Paris) {
|
||||
super(entity, entity, config, entityConstructor, dataStore, paris);
|
||||
|
||||
const getAllItems$: Observable<Array<T>> = defer(() => this.query().pipe(map((dataSet:DataSet<T>) => dataSet.items)));
|
||||
const getAllItems$: Observable<Array<TEntity>> = defer(() => this.query().pipe(map((dataSet:DataSet<TEntity>) => dataSet.items)));
|
||||
|
||||
this._allItemsSubject$ = new Subject<Array<T>>();
|
||||
this._allItemsSubject$ = new Subject<Array<TEntity>>();
|
||||
this._allItems$ = merge(getAllItems$, this._allItemsSubject$.asObservable());
|
||||
|
||||
this._saveSubject$ = new Subject();
|
||||
|
@ -48,7 +48,7 @@ export class Repository<T extends ModelBase> extends ReadonlyRepository<T> imple
|
|||
* @param {any} serializationData Any data to pass to serialize or serializeItem
|
||||
* @returns {Observable<T extends EntityModelBase>}
|
||||
*/
|
||||
save(item: T, options?:HttpOptions, serializationData?:any): Observable<T> {
|
||||
save(item: TEntity, options?:HttpOptions, serializationData?:any): Observable<TEntity> {
|
||||
if (!this.entityBackendConfig.endpoint)
|
||||
throw new Error(`Entity ${this.entityConstructor.entityConfig.singularName || this.entityConstructor.name} can't be saved - it doesn't specify an endpoint.`);
|
||||
|
||||
|
@ -65,7 +65,7 @@ export class Repository<T extends ModelBase> extends ReadonlyRepository<T> imple
|
|||
}),
|
||||
mergeMap((savedItemData: Index) =>
|
||||
savedItemData ? this.createItem(savedItemData) : of(null)),
|
||||
tap((savedItem: T) => {
|
||||
tap((savedItem: TEntity) => {
|
||||
if (savedItem && this._allValues) {
|
||||
this._allValues = [...this._allValues, savedItem];
|
||||
this._allItemsSubject$.next(this._allValues);
|
||||
|
@ -80,7 +80,7 @@ export class Repository<T extends ModelBase> extends ReadonlyRepository<T> imple
|
|||
}
|
||||
}
|
||||
|
||||
private getSaveMethod(item:T):RequestMethod{
|
||||
private getSaveMethod(item:TEntity):RequestMethod{
|
||||
return this.entityBackendConfig.saveMethod
|
||||
? this.entityBackendConfig.saveMethod instanceof Function ? this.entityBackendConfig.saveMethod(item, this.config) : this.entityBackendConfig.saveMethod
|
||||
: item.id === undefined ? "POST" : "PUT";
|
||||
|
@ -91,7 +91,7 @@ export class Repository<T extends ModelBase> extends ReadonlyRepository<T> imple
|
|||
* @param {Array<T extends EntityModelBase>} items
|
||||
* @returns {Observable<Array<T extends EntityModelBase>>}
|
||||
*/
|
||||
saveItems(items:Array<T>, options?:HttpOptions):Observable<Array<T>>{
|
||||
saveItems(items:Array<TEntity>, options?:HttpOptions):Observable<Array<TEntity>>{
|
||||
if (!this.entityBackendConfig.endpoint)
|
||||
throw new Error(`${this.entity.pluralName} can't be saved - it doesn't specify an endpoint.`);
|
||||
|
||||
|
@ -103,16 +103,16 @@ export class Repository<T extends ModelBase> extends ReadonlyRepository<T> imple
|
|||
(saveMethod === "POST" ? newItems : existingItems).items.push(this.serializeItem(item));
|
||||
});
|
||||
|
||||
let saveItemsArray:Array<Observable<Array<T>>> = [newItems, existingItems]
|
||||
let saveItemsArray:Array<Observable<Array<TEntity>>> = [newItems, existingItems]
|
||||
.filter((saveItems:SaveItems) => saveItems.items.length)
|
||||
.map((saveItems:SaveItems) => this.doSaveItems(saveItems.items, saveItems.method, options));
|
||||
|
||||
return combineLatest.apply(this, saveItemsArray).pipe(
|
||||
map((savedItems:Array<Array<T>>) => _.flatMap(savedItems)),
|
||||
tap((savedItems:Array<T>) => {
|
||||
map((savedItems:Array<Array<TEntity>>) => _.flatMap(savedItems)),
|
||||
tap((savedItems:Array<TEntity>) => {
|
||||
if (savedItems && savedItems.length && this._allValues) {
|
||||
let itemsAdded: Array<T> = [];
|
||||
savedItems.forEach((item:T) => {
|
||||
let itemsAdded: Array<TEntity> = [];
|
||||
savedItems.forEach((item:TEntity) => {
|
||||
const originalItemIndex:number = _.findIndex(this._allValues,_item => item.id === _item.id);
|
||||
if (!!~originalItemIndex)
|
||||
this._allValues[originalItemIndex] = item;
|
||||
|
@ -134,7 +134,7 @@ export class Repository<T extends ModelBase> extends ReadonlyRepository<T> imple
|
|||
* @param {"PUT" | "POST"} method
|
||||
* @returns {Observable<Array<T extends EntityModelBase>>}
|
||||
*/
|
||||
private doSaveItems(itemsData:Array<any>, method:"PUT" | "POST", options?:HttpOptions):Observable<Array<T>>{
|
||||
private doSaveItems(itemsData:Array<any>, method:"PUT" | "POST", options?:HttpOptions):Observable<Array<TEntity>>{
|
||||
const saveHttpOptions:HttpOptions = this.entity.parseSaveItemsQuery
|
||||
? this.entity.parseSaveItemsQuery(itemsData, options, this.entity, this.config)
|
||||
: Object.assign({}, options, {data: {items: itemsData}});
|
||||
|
@ -150,13 +150,19 @@ export class Repository<T extends ModelBase> extends ReadonlyRepository<T> imple
|
|||
return of([]);
|
||||
|
||||
let itemsData:Array<any> = savedItemsData instanceof Array ? savedItemsData : savedItemsData.items;
|
||||
let itemCreators:Array<Observable<T>> = itemsData.map(savedItemData => this.createItem(savedItemData));
|
||||
let itemCreators:Array<Observable<TEntity>> = itemsData.map(savedItemData => this.createItem(savedItemData));
|
||||
return combineLatest.apply(this, itemCreators);
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
removeItem(item:T, options?:HttpOptions):Observable<T>{
|
||||
/**
|
||||
* Sends a DELETE request to the backend for deleting an item.
|
||||
* @param {TEntity} item
|
||||
* @param {HttpOptions} options
|
||||
* @returns {Observable<TEntity extends ModelBase>}
|
||||
*/
|
||||
removeItem(item:TEntity, options?:HttpOptions):Observable<TEntity>{
|
||||
if (!item)
|
||||
return of(null);
|
||||
|
||||
|
@ -181,7 +187,7 @@ export class Repository<T extends ModelBase> extends ReadonlyRepository<T> imple
|
|||
}),
|
||||
tap(() => {
|
||||
if (this._allValues) {
|
||||
let itemIndex:number = _.findIndex(this._allValues, (_item:T) => _item.id === item.id);
|
||||
let itemIndex:number = _.findIndex(this._allValues, (_item:TEntity) => _item.id === item.id);
|
||||
if (~itemIndex)
|
||||
this._allValues.splice(itemIndex, 1);
|
||||
|
||||
|
@ -198,7 +204,13 @@ export class Repository<T extends ModelBase> extends ReadonlyRepository<T> imple
|
|||
}
|
||||
}
|
||||
|
||||
remove(items:Array<T>, options?:HttpOptions):Observable<Array<T>>{
|
||||
/**
|
||||
* Sends a DELETE request to the backend for deleting multiple entities.
|
||||
* @param {Array<TEntity extends ModelBase>} items
|
||||
* @param {HttpOptions} options
|
||||
* @returns {Observable<Array<TEntity extends ModelBase>>}
|
||||
*/
|
||||
remove(items:Array<TEntity>, options?:HttpOptions):Observable<Array<TEntity>>{
|
||||
if (!items)
|
||||
throw new Error(`No ${this.entity.pluralName.toLowerCase()} specified for removing.`);
|
||||
|
||||
|
@ -231,8 +243,8 @@ export class Repository<T extends ModelBase> extends ReadonlyRepository<T> imple
|
|||
}),
|
||||
tap(() => {
|
||||
if (this._allValues) {
|
||||
items.forEach((item:T) => {
|
||||
let itemIndex:number = _.findIndex(this._allValues, (_item:T) => _item.id === item.id);
|
||||
items.forEach((item:TEntity) => {
|
||||
let itemIndex:number = _.findIndex(this._allValues, (_item:TEntity) => _item.id === item.id);
|
||||
if (~itemIndex)
|
||||
this._allValues.splice(itemIndex, 1);
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче