This commit is contained in:
Shai Rose 2018-07-24 13:08:25 +03:00
Родитель 898aa1b56e 350f495c85
Коммит 679e87d4a7
6 изменённых файлов: 156 добавлений и 25 удалений

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

@ -1,11 +1,11 @@
import {EntityFields} from "./entity-fields"; import {EntityFields} from "./entity-fields";
import {Field} from "./entity-field";
import {Immutability} from "../services/immutability"; import {Immutability} from "../services/immutability";
import {DataEntityConstructor} from "./data-entity.base"; import {DataEntityConstructor} from "./data-entity.base";
import {ParisConfig} from "../config/paris-config"; import {ParisConfig} from "../config/paris-config";
import {ModelBase} from "../models/model.base"; import {ModelBase} from "../models/model.base";
import {HttpOptions} from "../services/http.service"; import {HttpOptions} from "../services/http.service";
import {EntityId} from "../models/entity-id.type"; import {EntityId} from "../models/entity-id.type";
import {Field} from "./entity-field";
const DEFAULT_VALUE_ID = "__default"; const DEFAULT_VALUE_ID = "__default";

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

@ -0,0 +1,141 @@
import {DataEntityType} from "./data-entity.base";
import {ParisConfig} from "../config/paris-config";
import {DataQuery} from "../dataset/data-query";
/**
* Configuration for a model field decorator
*/
export interface FieldConfig{
/**
* An ID for the field. By default, the ID is the property's name.
*/
id?:string,
/**
* Optional name to assign to the field. May be used for reflection, debugging, etc.
*/
name?:string,
/**
* Specifies which property in the raw data should be assigned to the model's property.
* By default, Paris looks for a property of the same name as the property in the model. i.e:
*
* If an entity has the following property definition:
*
* @EntityField()
* name:string;
*
* Then when creating the model, if the raw data contains a `name` property with value 'Anna', the resulting model will have a `name` property with value 'Anna'.
*
* @example <caption>Mapping from a different raw data property with `data`</caption>
* If your raw data has properties in snake-case rather than camel-case, you'd need to map the properties:
*
* @EntityField({ data: "creation_date" })
* creationData: Date;
*
* @example <caption>Using the first available value from the raw data for the model's property</caption>
* If an array of strings is provided for `data`, Paris will assign to the model's property value the first value from the raw data which isn't undefined or null:
*
* @EntityField({ data: ['creation_date', 'init_date', 'start_date'] })
* date: Date;
*
* If the raw data is:
* {
* "creation_date": null,
* "start_date": 1532422166428
* }
*
* Then the model's `date` property will have a value of Date(1532422166428), since both creation_date and init_date have no value in the data.
*
* @example <caption>Using '__self' for data to pass the whole raw data</caption>
* In the case when we want to separate some properties of the raw data to a sub-model, it's possible to use the special value '__self' for the `data` field configuration.
* This passes the whole raw data object to the field's creation, rather than just the value of a property. e.g:
*
* Person extends EntityModelBase{
* @EntityField()
* name:string;
*
* @EntityField({ data: '__self' })
* address:Address;
* }
*
* In case we want to separate all address properties from a user into an encapsulated object, for the following raw data:
*
* {
* "name": "Anna",
* "street": "Prinsengracht 263-267",
* "zip": "1016 GV",
* "city": "Amsterdam",
* "country": "Holland"
* }
*/
data?:"__self" | string | Array<string>,
/**
* A value to assign to the property if the raw data is `null` or undefined.
*/
defaultValue?:any,
/**
* `arrayOf` is required when the property's type is an array of a sub-model type.
* It's required because the ES6 Reflect-metadata module that Paris uses to infer the types of properties doesn't support generics.
*
* @example <caption>Using a model field's arrayOf configuration for assigning an array sub-model</caption>
* // Without the arrayOf, addresses won't be modeled by Paris.
* @EntityField({ arrayOf: Address })
* addresses: Array<Address>
*/
arrayOf?:DataEntityType,
/**
* If a field's `required` is set to `true`, it means that the property must have a value for the whole model to be created.
* If `required` is `true` and the property has a value of `null` or `undefined`, then the model itself will be null.
* @default false
*/
required?:boolean,
/**
* A condition that has to be satisfied in order to assign value to the property.
*
* @example <caption>Assigning ZIP code only if street exists</caption>
* @EntityField({ require: "street" })
* zip:string;
*
* @example <caption>Assigning ZIP code only if both street and country exist</caption>
* @EntityField({ require: (data:AddressRawData) => data.street && data.country })
* zip:string;
*/
require?:((data:any, config?:ParisConfig) => any) | string,
/**
* Parses the raw data before it's used by Paris to create the property's value
* Sometimes the value in the raw data is not formatted as we'd like, or more information might be needed to create the desired value. A field's `parse` configuration is available for changing the raw data before it's passed to Paris.
* Important: `parse` should return a new RAW data, not a Paris model.
*
* @example <caption>Parsing a bitwise value into an array</caption>
* @EntityField({
* arrayOf: NotificationFormat,
* parse: (formatBitWise: number) => {
* return notificationFormatValues.reduce((formats: Array<number>, notificationFormat) => {
* return notificationFormat.id > 0 && (formatBitWise & notificationFormat.id) ? [...formats, notificationFormat.id] : formats;
* }, []);
* },
* })
* formatFlavor: Array<NotificationFormat>;
*
* @param fieldData The field's data from the raw data
* @param itemData The whole object's raw data
* @param {DataQuery} query The query (if any) that was used for getting the data
* @returns {any} new raw data.
*/
parse?:(fieldData?:any, itemData?:any, query?: DataQuery) => any,
/**
* A method used to serialize the model field's data back into raw data, to be used when saving the model to backend.
* `serialize` may also be set to `false`, in which case the field won't be included in the serialized model.
*/
serialize?: false | ((itemData:any, serializationData?:any) => any)
}
export const FIELD_DATA_SELF = "__self";
export type EntityFieldConfigFunctionOrValue = ((data:any, config?:ParisConfig) => string) | string;

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

@ -1,18 +1,19 @@
import {DataEntityType} from "./data-entity.base"; import {DataEntityType} from "./data-entity.base";
import {Field} from "./entity-field"; import {FieldConfig} from "./entity-field.config";
import {entityFieldsService} from "../services/entity-fields.service"; import {entityFieldsService} from "../services/entity-fields.service";
import {Field} from "./entity-field";
export function EntityField(fieldConfig?:Field):PropertyDecorator { export function EntityField(fieldConfig?:FieldConfig):PropertyDecorator {
return function (entityPrototype: DataEntityType, propertyKey: string | symbol) { return function (entityPrototype: DataEntityType, propertyKey: string | symbol) {
let propertyConstructor:Function = (<any>Reflect).getMetadata("design:type", entityPrototype, propertyKey); let propertyConstructor:Function = (<any>Reflect).getMetadata("design:type", entityPrototype, propertyKey);
fieldConfig = fieldConfig || {}; fieldConfig = fieldConfig || {};
let fieldConfigCopy:Field = Object.assign({}, fieldConfig); let field:Field = Object.assign({}, fieldConfig);
if (!fieldConfigCopy.id) if (!field.id)
fieldConfigCopy.id = String(propertyKey); field.id = String(propertyKey);
fieldConfigCopy.type = fieldConfig.arrayOf || propertyConstructor; field.type = fieldConfig.arrayOf || propertyConstructor;
fieldConfigCopy.isArray = propertyConstructor === Array; field.isArray = propertyConstructor === Array;
entityFieldsService.addField(entityPrototype, fieldConfigCopy); entityFieldsService.addField(entityPrototype, field);
} }
} }

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

@ -1,21 +1,8 @@
import {FieldConfig} from "./entity-field.config";
import {DataEntityType} from "./data-entity.base"; import {DataEntityType} from "./data-entity.base";
import {ParisConfig} from "../config/paris-config";
import {DataQuery} from "../dataset/data-query";
export interface Field{ export interface Field extends FieldConfig{
id?:string,
name?:string,
data?:"__self" | string | Array<string>,
entity?:DataEntityType, entity?:DataEntityType,
type?:Function, type?:Function,
defaultValue?:any,
arrayOf?:DataEntityType,
isArray?:boolean, isArray?:boolean,
required?:boolean,
require?:((data:any, config?:ParisConfig) => any) | string,
parse?:(fieldData?:any, itemData?:any, query?: DataQuery) => any,
serialize?: false | ((itemData:any, serializationData?:any) => any)
} }
export const FIELD_DATA_SELF = "__self";
export type EntityFieldConfigFunctionOrValue = ((data:any, config?:ParisConfig) => string) | string;

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

@ -28,6 +28,7 @@ export {EntityEvent} from "./events/entity.event";
export {SaveEntityEvent} from "./events/save-entity.event"; export {SaveEntityEvent} from "./events/save-entity.event";
export {RemoveEntitiesEvent} from "./events/remove-entities.event"; export {RemoveEntitiesEvent} from "./events/remove-entities.event";
export {EntityErrorEvent, EntityErrorTypes} from "./events/entity-error.event"; export {EntityErrorEvent, EntityErrorTypes} from "./events/entity-error.event";
export {FieldConfig} from "./entity/entity-field.config";
export {Field} from "./entity/entity-field"; export {Field} from "./entity/entity-field";
export {ApiCall} from "./entity/api-call.decorator"; export {ApiCall} from "./entity/api-call.decorator";
export {ApiCallBackendConfigInterface} from "./models/api-call-backend-config.interface"; export {ApiCallBackendConfigInterface} from "./models/api-call-backend-config.interface";

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

@ -12,7 +12,7 @@ import {EntityBackendConfig, ModelEntity} from "../entity/entity.config";
import {DataEntityConstructor, DataEntityType} from "../entity/data-entity.base"; import {DataEntityConstructor, DataEntityType} from "../entity/data-entity.base";
import {Paris} from "../services/paris"; import {Paris} from "../services/paris";
import {DataAvailability} from "../dataset/data-availability.enum"; import {DataAvailability} from "../dataset/data-availability.enum";
import {Field, FIELD_DATA_SELF} from "../entity/entity-field"; import {Field} from "../entity/entity-field";
import {DataCache} from "../services/cache"; import {DataCache} from "../services/cache";
import {Index} from "../models"; import {Index} from "../models";
import {EntityConfigBase, EntityGetMethod, IEntityConfigBase} from "../entity/entity-config.base"; import {EntityConfigBase, EntityGetMethod, IEntityConfigBase} from "../entity/entity-config.base";
@ -24,6 +24,7 @@ import {AjaxError} from "rxjs/ajax";
import {EntityErrorEvent, EntityErrorTypes} from "../events/entity-error.event"; import {EntityErrorEvent, EntityErrorTypes} from "../events/entity-error.event";
import {catchError, map, mergeMap, tap} from "rxjs/operators"; import {catchError, map, mergeMap, tap} from "rxjs/operators";
import {IReadonlyRepository} from "./repository.interface"; import {IReadonlyRepository} from "./repository.interface";
import {FIELD_DATA_SELF} from "../entity/entity-field.config";
export class ReadonlyRepository<TEntity extends ModelBase<TRawData>, TRawData = object> implements IReadonlyRepository<TEntity>{ export class ReadonlyRepository<TEntity extends ModelBase<TRawData>, TRawData = object> implements IReadonlyRepository<TEntity>{
protected _errorSubject$: Subject<EntityErrorEvent>; protected _errorSubject$: Subject<EntityErrorEvent>;