diff --git a/lib/entity/entity-config.base.ts b/lib/entity/entity-config.base.ts index 8c8fa78..ba8cac5 100644 --- a/lib/entity/entity-config.base.ts +++ b/lib/entity/entity-config.base.ts @@ -1,11 +1,11 @@ import {EntityFields} from "./entity-fields"; -import {Field} from "./entity-field"; import {Immutability} from "../services/immutability"; import {DataEntityConstructor} from "./data-entity.base"; import {ParisConfig} from "../config/paris-config"; import {ModelBase} from "../models/model.base"; import {HttpOptions} from "../services/http.service"; import {EntityId} from "../models/entity-id.type"; +import {Field} from "./entity-field"; const DEFAULT_VALUE_ID = "__default"; diff --git a/lib/entity/entity-field.config.ts b/lib/entity/entity-field.config.ts new file mode 100644 index 0000000..29b2f46 --- /dev/null +++ b/lib/entity/entity-field.config.ts @@ -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 Mapping from a different raw data property with `data` + * 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 Using the first available value from the raw data for the model's property + * 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 Using '__self' for data to pass the whole raw data + * 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, + + /** + * 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 Using a model field's arrayOf configuration for assigning an array sub-model + * // Without the arrayOf, addresses won't be modeled by Paris. + * @EntityField({ arrayOf: Address }) + * addresses: Array
+ */ + 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 Assigning ZIP code only if street exists + * @EntityField({ require: "street" }) + * zip:string; + * + * @example Assigning ZIP code only if both street and country exist + * @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 Parsing a bitwise value into an array + * @EntityField({ + * arrayOf: NotificationFormat, + * parse: (formatBitWise: number) => { + * return notificationFormatValues.reduce((formats: Array, notificationFormat) => { + * return notificationFormat.id > 0 && (formatBitWise & notificationFormat.id) ? [...formats, notificationFormat.id] : formats; + * }, []); + * }, + * }) + * formatFlavor: Array; + * + * @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; diff --git a/lib/entity/entity-field.decorator.ts b/lib/entity/entity-field.decorator.ts index 09ff6cb..4f7cf3e 100644 --- a/lib/entity/entity-field.decorator.ts +++ b/lib/entity/entity-field.decorator.ts @@ -1,18 +1,19 @@ 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 {Field} from "./entity-field"; -export function EntityField(fieldConfig?:Field):PropertyDecorator { +export function EntityField(fieldConfig?:FieldConfig):PropertyDecorator { return function (entityPrototype: DataEntityType, propertyKey: string | symbol) { let propertyConstructor:Function = (Reflect).getMetadata("design:type", entityPrototype, propertyKey); fieldConfig = fieldConfig || {}; - let fieldConfigCopy:Field = Object.assign({}, fieldConfig); - if (!fieldConfigCopy.id) - fieldConfigCopy.id = String(propertyKey); + let field:Field = Object.assign({}, fieldConfig); + if (!field.id) + field.id = String(propertyKey); - fieldConfigCopy.type = fieldConfig.arrayOf || propertyConstructor; - fieldConfigCopy.isArray = propertyConstructor === Array; - entityFieldsService.addField(entityPrototype, fieldConfigCopy); + field.type = fieldConfig.arrayOf || propertyConstructor; + field.isArray = propertyConstructor === Array; + entityFieldsService.addField(entityPrototype, field); } } diff --git a/lib/entity/entity-field.ts b/lib/entity/entity-field.ts index f9f8cca..e16e8c6 100644 --- a/lib/entity/entity-field.ts +++ b/lib/entity/entity-field.ts @@ -1,21 +1,8 @@ +import {FieldConfig} from "./entity-field.config"; import {DataEntityType} from "./data-entity.base"; -import {ParisConfig} from "../config/paris-config"; -import {DataQuery} from "../dataset/data-query"; -export interface Field{ - id?:string, - name?:string, - data?:"__self" | string | Array, +export interface Field extends FieldConfig{ entity?:DataEntityType, type?:Function, - defaultValue?:any, - arrayOf?:DataEntityType, 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; diff --git a/lib/main.ts b/lib/main.ts index d557687..266a0fa 100644 --- a/lib/main.ts +++ b/lib/main.ts @@ -28,6 +28,7 @@ export {EntityEvent} from "./events/entity.event"; export {SaveEntityEvent} from "./events/save-entity.event"; export {RemoveEntitiesEvent} from "./events/remove-entities.event"; export {EntityErrorEvent, EntityErrorTypes} from "./events/entity-error.event"; +export {FieldConfig} from "./entity/entity-field.config"; export {Field} from "./entity/entity-field"; export {ApiCall} from "./entity/api-call.decorator"; export {ApiCallBackendConfigInterface} from "./models/api-call-backend-config.interface"; diff --git a/lib/repository/readonly-repository.ts b/lib/repository/readonly-repository.ts index 3e8432e..49cf26d 100644 --- a/lib/repository/readonly-repository.ts +++ b/lib/repository/readonly-repository.ts @@ -12,7 +12,7 @@ import {EntityBackendConfig, ModelEntity} from "../entity/entity.config"; import {DataEntityConstructor, DataEntityType} from "../entity/data-entity.base"; import {Paris} from "../services/paris"; 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 {Index} from "../models"; 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 {catchError, map, mergeMap, tap} from "rxjs/operators"; import {IReadonlyRepository} from "./repository.interface"; +import {FIELD_DATA_SELF} from "../entity/entity-field.config"; export class ReadonlyRepository, TRawData = object> implements IReadonlyRepository{ protected _errorSubject$: Subject;