UNPKG

angular-odata

Version:

Client side OData typescript library for Angular

1,227 lines 177 kB
import { Observable } from 'rxjs'; import { finalize } from 'rxjs/operators'; import { CID_FIELD_NAME, COMPUTED, DEFAULT_VERSION, OPTIMISTIC_CONCURRENCY, EVENT_SPLITTER, } from '../constants'; import { ODataHelper } from '../helper'; import { ODataEntityResource, ODataEntitySetResource, ODataNavigationPropertyResource, ODataPropertyResource, } from '../resources'; import { EdmType } from '../types'; import { Objects, Types } from '../utils'; import { ODataCollection } from './collection'; import { ODataModel } from './model'; import { EventEmitter } from '@angular/core'; import { ODataEntityAnnotations, } from '../annotations'; export var ODataModelEventType; (function (ODataModelEventType) { ODataModelEventType["Change"] = "change"; ODataModelEventType["Reset"] = "reset"; ODataModelEventType["Update"] = "update"; ODataModelEventType["Sort"] = "sort"; ODataModelEventType["Destroy"] = "destroy"; ODataModelEventType["Add"] = "add"; ODataModelEventType["Remove"] = "remove"; ODataModelEventType["Invalid"] = "invalid"; ODataModelEventType["Request"] = "request"; ODataModelEventType["Sync"] = "sync"; ODataModelEventType["Attach"] = "attach"; })(ODataModelEventType || (ODataModelEventType = {})); export class ODataModelEvent { type; value; previous; options; constructor(type, { model, collection, previous, value, attr, options, bubbles, chain, } = {}) { this.type = type; this.model = model; this.collection = collection; this.previous = previous; this.value = value; this.options = options; this.chain = chain ?? [ [ (this.model || this.collection), attr || null, ], ]; this.bubbles = bubbles ?? BUBBLES.indexOf(this.type) !== -1; } chain; push(model, attr) { return new ODataModelEvent(this.type, { model: this.model ?? (model instanceof ODataModel ? model : undefined), collection: this.collection ?? (model instanceof ODataCollection ? model : undefined), previous: this.previous, value: this.value, options: { ...this.options, index: attr instanceof ODataModelAttribute ? attr.name : attr, }, bubbles: this.bubbles, chain: [[model, attr], ...this.chain], }); } bubbles; stopPropagation() { this.bubbles = false; } visited(model) { return (this.chain.some((c) => c[0] === model) && this.chain[this.chain.length - 1][0] !== model); } canContinueWith(self) { return this.bubbles && !this.visited(self); } get path() { return this.chain .map(([, attr], index) => typeof attr === 'number' ? `[${attr}]` : attr instanceof ODataModelAttribute ? index === 0 ? attr.name : `.${attr.name}` : '') .join(''); } //Reference to the model which the event was dispatched model; //Identifies the current model for the event get currentModel() { const link = this.chain.find((c) => ODataModelOptions.isModel(c[0])); return link !== undefined ? link[0] : undefined; } //Reference to the collection which the event was dispatched collection; //Identifies the current collection for the event get currentCollection() { const link = this.chain.find((c) => ODataModelOptions.isCollection(c[0])); return link !== undefined ? link[0] : undefined; } } export class ODataModelEventEmitter extends EventEmitter { model; collection; constructor({ model, collection, } = {}) { super(); this.model = model; this.collection = collection; } trigger(type, { collection, previous, value, attr, options, bubbles, } = {}) { const _trigger = (name) => this.emit(new ODataModelEvent(name, { model: this.model, collection: collection ?? this.collection, previous, value, attr, options, bubbles, })); if (type && EVENT_SPLITTER.test(type)) { for (const name of type.split(EVENT_SPLITTER)) { _trigger(name); } } else { _trigger(type); } } } export const BUBBLES = [ ODataModelEventType.Change, ODataModelEventType.Reset, ODataModelEventType.Update, ODataModelEventType.Destroy, ODataModelEventType.Add, ODataModelEventType.Remove, ]; export const INCLUDE_SHALLOW = { include_concurrency: true, include_computed: true, include_key: true, include_id: false, }; export const INCLUDE_DEEP = { include_navigation: true, include_non_field: true, ...INCLUDE_SHALLOW, }; export var ODataModelState; (function (ODataModelState) { ODataModelState[ODataModelState["Added"] = 0] = "Added"; ODataModelState[ODataModelState["Removed"] = 1] = "Removed"; ODataModelState[ODataModelState["Changed"] = 2] = "Changed"; ODataModelState[ODataModelState["Unchanged"] = 3] = "Unchanged"; })(ODataModelState || (ODataModelState = {})); export function Model({ cid = CID_FIELD_NAME } = {}) { return (constructor) => { const Klass = constructor; if (!Klass.hasOwnProperty('options')) Klass.options = { fields: new Map(), }; Klass.options.cid = cid; return constructor; }; } export function ModelField({ name, ...options } = {}) { return (target, key) => { const Klass = target.constructor; if (!Klass.hasOwnProperty('options')) Klass.options = { fields: new Map(), }; options.field = name ?? key; Klass.options.fields.set(key, options); }; } export class ODataModelField { name; field; parser; options; optionsForType; modelForType; collectionForType; enumForType; structuredForType; default; required; concurrency; maxLength; minLength; min; max; pattern; parserOptions; constructor(options, { name, field, parser, ...opts }) { this.options = options; this.name = name; this.field = field; this.parser = parser; this.default = opts.default || parser.default; this.required = Boolean(opts.required || !parser.nullable); this.concurrency = Boolean(opts.concurrency); this.maxLength = opts.maxLength || parser.maxLength; this.minLength = opts.minLength; this.min = opts.min; this.max = opts.max; this.pattern = opts.pattern; } get type() { return this.parser.type; } get navigation() { return this.parser.navigation; } get collection() { return this.parser.collection; } annotatedValue(term) { return this.parser.annotatedValue(term); } configure({ optionsForType, modelForType, collectionForType, enumForType, structuredForType, concurrency, options, }) { this.optionsForType = optionsForType; this.modelForType = modelForType; this.collectionForType = collectionForType; this.enumForType = enumForType; this.structuredForType = structuredForType; this.parserOptions = options; if (concurrency) this.concurrency = concurrency; if (this.default !== undefined) this.default = this.deserialize(this.default, options); } isKey() { return this.parser.isKey(); } hasReferentials() { return this.parser.hasReferentials(); } get referentials() { return this.parser.referentials; } isStructuredType() { return this.parser.isStructuredType(); } structuredType() { const structuredType = this.structuredForType ? this.structuredForType(this.type) : undefined; //Throw error if not found if (!structuredType) throw new Error(`Could not find structured type for ${this.parser.type}`); return structuredType; } isEnumType() { return this.parser.isEnumType(); } enumType() { const enumType = this.enumForType ? this.enumForType(this.type) : undefined; //Throw error if not found if (!enumType) throw new Error(`Could not find enum type for ${this.parser.type}`); return enumType; } validate(value, { method, navigation = false, } = {}) { if (ODataModelOptions.isModel(value)) { return !value.isValid({ method, navigation }) ? value._errors : undefined; } else if (ODataModelOptions.isCollection(value)) { return value .models() .some((m) => !m.isValid({ method, navigation })) ? value.models().map((m) => m._errors) : undefined; } else { const computed = this.annotatedValue(COMPUTED); const errors = this.parser?.validate(value, { method, navigation }) || []; if (this.required && (value === null || (value === undefined && method !== 'modify')) && // Is null or undefined without patch? !(computed && method === 'create') // Not (Is Computed field and create) ? ) { errors['push'](`required`); } if (this.maxLength !== undefined && typeof value === 'string' && value.length > this.maxLength) { errors['push'](`maxlength`); } if (this.minLength !== undefined && typeof value === 'string' && value.length < this.minLength) { errors['push'](`minlength`); } if (this.min !== undefined && typeof value === 'number' && value < this.min) { errors['push'](`min`); } if (this.max !== undefined && typeof value === 'number' && value > this.max) { errors['push'](`max`); } if (this.pattern !== undefined && typeof value === 'string' && !this.pattern.test(value)) { errors['push'](`pattern`); } return !Types.isEmpty(errors) ? errors : undefined; } } defaults() { const meta = this.optionsForType ? this.optionsForType(this.type) : undefined; return this.isStructuredType() && meta !== undefined ? meta.defaults() : this.default; } deserialize(value, options) { const parserOptions = options ?? this.parserOptions; return this.parser.deserialize(value, parserOptions); } serialize(value, options) { const parserOptions = options ?? this.parserOptions; return this.parser.serialize(value, parserOptions); } encode(value, options) { const parserOptions = options ?? this.parserOptions; return this.parser.encode(value, parserOptions); } resourceFactory(base) { if (!(base instanceof ODataEntityResource || base instanceof ODataNavigationPropertyResource || base instanceof ODataPropertyResource)) throw new Error("Can't build resource for non compatible base type"); return this.navigation ? base.navigationProperty(this.parser.name) : base.property(this.parser.name); } annotationsFactory(base) { return this.parser.collection ? base.property(this.parser.name, 'collection') : base.property(this.parser.name, 'single'); } modelFactory({ parent, value, reset, }) { // Model const annots = this.annotationsFactory(parent.annots()); let Model = this.modelForType ? this.modelForType(this.type) : undefined; if (Model === undefined) throw Error(`No Model type for ${this.name}`); if (value !== undefined) { annots.update(value); } if (annots?.type !== undefined && Model.meta !== null) { const meta = Model.meta.findChildOptions((o) => o.isTypeOf(annots.type))?.structuredType; if (meta !== undefined && meta.model !== undefined) // Change to child model Model = meta.model; } return new Model((value || {}), { annots, reset, parent: [parent, this], }); } collectionFactory({ parent, value, reset, }) { // Collection Factory const annots = this.annotationsFactory(parent.annots()); const Collection = this.collectionForType ? this.collectionForType(this.type) : undefined; if (Collection === undefined) throw Error(`No Collection type for ${this.name}`); return new Collection((value || []), { annots: annots, reset, parent: [parent, this], }); } } export class ODataModelAttribute { _model; _field; state = ODataModelState.Unchanged; value; change; subscription; events$ = new ODataModelEventEmitter(); constructor(_model, _field) { this._model = _model; this._field = _field; } get type() { return this._field.type; } get navigation() { return this._field.navigation; } get computed() { return this._field.annotatedValue(COMPUTED); } get concurrency() { return Boolean(this._field.concurrency); } get referentials() { return this._field.referentials; } get options() { return this._field.options; } get name() { return this._field.name; } get fieldName() { return this._field.field; } get() { return this.state === ODataModelState.Changed ? this.change : this.value; } set(value, reset = false, reparent = false) { const current = this.get(); if (ODataModelOptions.isModel(current) || ODataModelOptions.isCollection(current)) this.unlink(current); const changed = ODataModelOptions.isModel(current) && ODataModelOptions.isModel(value) ? !current.equals(value) : ODataModelOptions.isCollection(current) && ODataModelOptions.isCollection(value) ? !current.equals(value) : !Types.isEqual(current, value); if (reset) { this.value = value; this.state = ODataModelState.Unchanged; } else if (Types.isEqual(value, this.value)) { this.state = ODataModelState.Unchanged; } else if (changed) { this.change = value; this.state = ODataModelState.Changed; } if (ODataModelOptions.isModel(value) || ODataModelOptions.isCollection(value)) { this.link(value, reparent); } return changed; } isChanged({ include_navigation = false, } = {}) { const current = this.get(); return (this.state === ODataModelState.Changed || ((ODataModelOptions.isModel(current) || ODataModelOptions.isCollection(current)) && current.hasChanged({ include_navigation }))); } reset() { if (ODataModelOptions.isModel(this.change) || ODataModelOptions.isCollection(this.change)) this.unlink(this.change); this.state = ODataModelState.Unchanged; if (ODataModelOptions.isModel(this.value) || ODataModelOptions.isCollection(this.value)) this.link(this.value); } link(value, reparent = false) { this.subscription = value.events$.subscribe((e) => this.events$.emit(e)); if (reparent) { value._parent = [this._model, this._field]; } } unlink(value, reparent = false) { this.subscription?.unsubscribe(); this.subscription = undefined; if (reparent) { value._parent = null; } } } export class ODataModelOptions { name; cid; base; _fields = []; structuredType; entitySet; // Hierarchy parent; children = []; events$ = new ODataModelEventEmitter(); constructor({ config, structuredType, }) { this.name = structuredType.name; this.base = structuredType.base; this.structuredType = structuredType; this.cid = config?.cid ?? CID_FIELD_NAME; config.fields.forEach((value, key) => this.addField(key, value)); } get api() { return this.structuredType.api; } type({ alias = false } = {}) { return this.structuredType.type({ alias }); } isOpenType() { return this.structuredType.isOpenType(); } isEntityType() { return this.structuredType.isEntityType(); } isComplexType() { return this.structuredType.isComplexType(); } isTypeOf(type) { return this.structuredType.type() === type; } isModelFor(entity) { // Resolve By Type const type = this.api.options.helper.type(entity); if (type && this.isTypeOf(type)) return true; // Resolve By fields const keys = Object.keys(entity); const names = this.fields({ include_navigation: true, include_parents: true, }).map((f) => f.name); return keys.every((key) => names.includes(key)); } findChildOptions(predicate) { if (predicate(this)) return this; let match; for (const ch of this.children) { match = ch.findChildOptions(predicate); if (match !== undefined) break; } return match; } configure({ options }) { if (this.base) { const parent = this.api.optionsForType(this.base); parent.children.push(this); this.parent = parent; } this.entitySet = this.api.findEntitySetForEntityType(this.type()); let concurrencyFields = []; if (this.entitySet !== undefined) { concurrencyFields = this.entitySet.annotatedValue(OPTIMISTIC_CONCURRENCY) || []; } this._fields.forEach((field) => { const concurrency = concurrencyFields.indexOf(field.field) !== -1; field.configure({ optionsForType: (t) => this.api.optionsForType(t), modelForType: (t) => this.api.modelForType(t), collectionForType: (t) => this.api.collectionForType(t), enumForType: (t) => this.api.findEnumType(t), structuredForType: (t) => this.api.findStructuredType(t), concurrency, options, }); }); } fields({ include_navigation, include_parents, }) { return [ ...(include_parents && this.parent !== undefined ? this.parent.fields({ include_navigation, include_parents }) : []), ...this._fields.filter((field) => include_navigation || !field.navigation), ]; } field(name) { const field = this.findField(name); //Throw error if not found if (!field) throw new Error(`No field with name ${name}`); return field; } findField(name, { reset } = {}) { return this.fields({ include_parents: true, include_navigation: true, }).find((modelField) => (reset && modelField.field === name) || modelField.name === name); } addField(name, options) { const { field, parser, ...opts } = options; if (field === undefined || name === undefined) throw new Error('Model Properties need name and field'); const fieldParser = parser ?? this.structuredType.field(field); if (fieldParser === undefined) throw new Error(`No parser for ${field} with name = ${name}`); const modelField = new ODataModelField(this, { name, field, parser: fieldParser, ...opts, }); this._fields.push(modelField); return modelField; } tsToEdm = { string: EdmType.String, number: EdmType.Int32, bigint: EdmType.Int64, boolean: EdmType.Boolean, }; modelFieldFactory(self, name, type) { const structuredFieldParser = this.structuredType.addField(name, { type, }); structuredFieldParser.configure({ parserForType: (type) => this.api.parserForType(type), options: this.api.options, }); const modelField = this.addField(name, { field: name, parser: structuredFieldParser, }); modelField.configure({ optionsForType: (t) => this.api.optionsForType(t), modelForType: (t) => this.api.modelForType(t), collectionForType: (t) => this.api.collectionForType(t), enumForType: (t) => this.api.findEnumType(t), structuredForType: (t) => this.api.findStructuredType(t), options: this.api.options, concurrency: false, }); Object.defineProperty(self, modelField.name, { configurable: true, get: () => this.get(self, modelField), set: (value) => this.set(self, modelField, value), }); return modelField; } attach(self, resource) { if (self._resource !== null && resource.outgoingType() !== self._resource.outgoingType() && !self._resource.isSubtypeOf(resource)) throw new Error(`Can't attach ${resource.outgoingType()} to ${self._resource.outgoingType()}`); const current = self._resource; if (current === null || !current.isEqualTo(resource)) { self._resource = resource; self.events$.trigger(ODataModelEventType.Attach, { previous: current, value: resource, }); } } //# region Resource static chain(child) { const chain = []; let tuple = [child, null]; while (tuple !== null) { const parent = tuple; if (chain.some((p) => p[0] === parent[0])) break; chain.splice(0, 0, parent); tuple = tuple[0]._parent; } return chain; } static resource(child) { let resource = null; let prevField = null; for (const [model, field] of ODataModelOptions.chain(child)) { resource = resource || model._resource; if (resource === null) break; if (ODataModelOptions.isModel(model) && (prevField === null || prevField.collection)) { const m = model; // Resolve subtype if collection not is from field // FIXME /* if (field === null) { const r = m._meta.modelResourceFactory(resource.cloneQuery<T>()); if (r !== null && !r.isTypeOf(resource) && r.isSubtypeOf(resource)) { resource = r; } } */ // Resolve key const mKey = m.key({ field_mapping: true }); if (mKey !== undefined) { resource = resource instanceof ODataEntitySetResource ? resource.entity(mKey) : resource.key(mKey); } } prevField = field; if (field === null && model._resource !== null) { // Apply the query from model to new resource model._resource.query((qs) => resource?.query((qd) => qd.restore(qs.store()))); } else if (field !== null) { resource = field.resourceFactory(resource); } } return resource; } collectionResourceFactory(query) { if (this.entitySet === undefined) return null; return ODataEntitySetResource.factory(this.api, { path: this.entitySet.name, type: this.entitySet.entityType, query, }); } modelResourceFactory(query) { const resource = this.collectionResourceFactory(query); if (resource instanceof ODataEntitySetResource) return resource.entity(); return resource; } //#endregion bind(self, { parent, resource, annots, } = {}) { // Events self.events$.subscribe((e) => this.events$.emit(e)); // Parent if (parent !== undefined) { self._parent = parent; } // Resource if (self._parent === null && resource === undefined) resource = this.modelResourceFactory(); if (resource) { this.attach(self, resource); } // Annotations self._annotations = annots ?? new ODataEntityAnnotations(ODataHelper[DEFAULT_VERSION]); // Fields this.fields({ include_navigation: true, include_parents: true, }).forEach((field) => { Object.defineProperty(self, field.name, { configurable: true, get: () => this.get(self, field), set: (value) => this.set(self, field, value), }); }); } query(self, resource, func) { resource.query(func); this.attach(self, resource); return self; } resolveKey(value, { field_mapping = false, resolve = true, single = true, } = {}) { const keyTypes = this.structuredType.keys({ include_parents: true }); const key = new Map(); for (const kt of keyTypes) { let v = value; let options = this; let field; for (const name of kt.name.split('/')) { if (options === undefined) break; field = options .fields({ include_navigation: false, include_parents: true }) .find((field) => field.field === name); if (field !== undefined) { v = Types.isPlainObject(v) || ODataModelOptions.isModel(v) ? v[field.name] : v; options = this.api.optionsForType(field.type); } } if (field === undefined) return undefined; let name = field_mapping ? field.field : field.name; if (kt.alias !== undefined) name = kt.alias; key.set(name, v); } if (key.size === 0) return undefined; return resolve ? Objects.resolveKey(key, { single }) : Object.fromEntries(key); } resolveReferential(value, attr, { field_mapping = false, resolve = true, single = false, } = {}) { const referential = new Map(); for (const ref of attr.referentials) { const from = this.fields({ include_navigation: false, include_parents: true, }).find((p) => p.field === ref.referencedProperty); const to = attr.options .fields({ include_navigation: false, include_parents: true }) .find((field) => field.field === ref.property); if (from !== undefined && to !== undefined) { const name = field_mapping ? to.field : to.name; referential.set(name, value && value[from.name]); } } if (referential.size === 0) return undefined; if (referential.size === 1 && Array.from(referential.values())[0] === null) return null; return resolve ? Objects.resolveKey(referential, { single }) : Object.fromEntries(referential); } resolveReferenced(value, attr, { field_mapping = false, resolve = true, single = false, } = {}) { const referenced = new Map(); for (const ref of attr.referentials) { const from = this.fields({ include_navigation: false, include_parents: true, }).find((field) => field.field === ref.property); const meta = this.api.optionsForType(attr.type); const to = meta ?.fields({ include_navigation: false, include_parents: true }) .find((field) => field.field === ref.referencedProperty); if (from !== undefined && to !== undefined) { const name = field_mapping ? to.field : to.name; referenced.set(name, value && value[from.name]); } } if (referenced.size === 0) return undefined; if (referenced.size === 1 && Array.from(referenced.values())[0] === null) return null; return resolve ? Objects.resolveKey(referenced, { single }) : Object.fromEntries(referenced); } validate(self, { method, navigation = false, } = {}) { const errors = this.fields({ include_parents: true, include_navigation: navigation, }).reduce((acc, field) => { const value = self[field.name]; const errs = field.validate(value, { method }); return errs !== undefined ? Object.assign(acc, { [field.name]: errs }) : acc; }, {}); return !Types.isEmpty(errors) ? errors : undefined; } defaults() { const defs = this.fields({ include_navigation: false, include_parents: true, }).reduce((acc, field) => { const value = field.defaults(); return value !== undefined ? Object.assign(acc, { [field.name]: value }) : acc; }, {}); return !Types.isEmpty(defs) ? defs : undefined; } hasChanged(self, { include_navigation = false } = {}) { return [...self._attributes.values()] .filter((attr) => !attr.navigation || include_navigation) .some((attr) => attr.isChanged({ include_navigation })); } hasKey(self) { return this.resolveKey(self) !== undefined; } withResource(self, resource, ctx) { // Push self.pushResource(resource); // Execute function const result = ctx(self); if (result instanceof Observable) { return result.pipe(finalize(() => { // Pop self.popResource(); })); } else { // Pop self.popResource(); return result; } } asEntity(self, ctx) { // Clone query from him or parent let query = self._resource?.cloneQuery(); if (query === undefined && self._parent && self._parent[0] instanceof ODataCollection) query = self._parent[0]._resource?.cloneQuery(); // Build new resource const resource = this.modelResourceFactory(query); return this.withResource(self, resource, ctx); } toEntity(self, { client_id = false, include_navigation = false, include_concurrency = false, include_computed = false, include_key = true, include_id = false, include_non_field = false, changes_only = false, field_mapping = false, chain = [], } = {}) { let entity = [...self._attributes.values()] .filter( // Chain (attr) => chain.every((c) => c !== attr.get())) .filter( // Changes only (attr) => !changes_only || (changes_only && attr.isChanged({ include_navigation }))) .filter((attr) => // Navigation (include_navigation && attr.navigation && attr.get() !== null) || !attr.navigation) .reduce((acc, attr) => { const name = field_mapping ? attr.fieldName : attr.name; let value = attr.get(); const computed = attr.computed; const navigation = attr.navigation; const concurrency = attr.concurrency; if (ODataModelOptions.isModel(value)) { value = value.toEntity({ client_id, include_navigation, include_concurrency, include_computed, include_non_field, field_mapping, changes_only: changes_only && !!navigation, include_key: include_key && !!navigation, include_id: include_id && !!navigation, chain: [self, ...chain], }); } else if (ODataModelOptions.isCollection(value)) { value = value.toEntities({ client_id, include_navigation, include_concurrency, include_computed, include_non_field, field_mapping, changes_only: changes_only && !!navigation, include_key: include_key && !!navigation, include_id: include_id && !!navigation, chain: [self, ...chain], }); } if (include_concurrency && concurrency) { return Object.assign(acc, { [name]: value }); } else if (include_computed && computed) { return Object.assign(acc, { [name]: value }); } else if (changes_only && attr.isChanged()) { return Object.assign(acc, { [name]: value }); } else if (!changes_only && !concurrency && !computed) { return Object.assign(acc, { [name]: value }); } return acc; }, {}); if (include_non_field) { const names = Object.keys(entity); // Attributes from object (attributes for object) const nonFieldAttrs = Object.entries(self) .filter(([k]) => names.indexOf(k) === -1 && !k.startsWith('_') && !k.endsWith('$')) .reduce((acc, [k, v]) => Object.assign(acc, { [k]: v }), {}); entity = { ...entity, ...nonFieldAttrs }; } // Add client_id if (client_id) { entity[this.cid] = self[this.cid]; } // Add key if (include_key) { entity = { ...entity, ...this.resolveKey(self, { field_mapping, resolve: false }), }; } // Add id if (include_id) { self.asEntity((e) => { const resource = e.resource(); if (resource) entity[this.api.options.helper.ODATA_ID] = `${resource.clearQuery()}`; }); } // Add type if (self._parent !== null && ((ODataModelOptions.isModel(self._parent[0]) && self._parent[1] !== null && this.api.optionsForType(self._parent[1].type) !== self._meta) || (ODataModelOptions.isCollection(self._parent[0]) && self._parent[0]._model .meta !== self._meta))) { entity[this.api.options.helper.ODATA_TYPE] = `#${this.structuredType.type()}`; } return entity; } reset(self, { name, silent = false } = {}) { let changes = []; if (name !== undefined) { // Reset value const attribute = self._attributes.get(name); if (attribute !== undefined && attribute.isChanged({ include_navigation: true })) { attribute.reset(); changes = [name]; } else if (attribute?.isChanged()) { attribute.reset(); changes = [name]; } } else { // reset all changes = [...self._attributes.keys()]; //self._changes.clear(); self._attributes.forEach((attr, key) => { if (attr.isChanged({ include_navigation: true })) { attr.reset(); changes.push(key); } }); } if (!silent && changes.length > 0) { self.events$.trigger(ODataModelEventType.Reset, { options: { changes } }); } } assign(self, entity, { add = true, merge = true, remove = true, reset = false, reparent = false, silent = false, } = {}) { const changes = []; // Update annotations self.annots().update(entity); // Update attributes const attrs = self.annots().attributes(entity, 'full'); Object.entries(attrs) .filter(([, value]) => value !== undefined) // Filter undefined .forEach(([key, value]) => { const field = this.findField(key, { reset }); if (field !== undefined || this.isOpenType()) { // Delegated to private setter if (this.set(self, field ?? key, value, { add, merge, remove, reset, reparent, silent, })) { changes.push(field?.name ?? key); } } else { // Basic assignment const current = self[key]; self[key] = value; if (current !== value) changes.push(key); } }); if (!silent && changes.length > 0) { self.events$.trigger(reset ? ODataModelEventType.Reset : ODataModelEventType.Update, { options: { changes } }); } return self; } static isModel(obj) { return Types.rawType(obj) === 'Model'; } static isCollection(obj) { return Types.rawType(obj) === 'Collection'; } get(self, field) { const attr = self._attributes.get(field instanceof ODataModelField ? field.name : field); if (attr !== undefined) { const value = attr.get(); if ((attr.navigation && value === null) || ODataModelOptions.isModel(value)) { // Check for reference const referenced = this.resolveReferenced(self, attr, { resolve: false, }); if (value !== null && referenced !== null && referenced !== undefined) { value.assign(referenced, { silent: true, }); } else if (value !== null && referenced === null) { // New value is null attr.set(null); } else if (value === null && referenced !== null) { // New value is undefined attr.set(undefined); } } return value; } else if (typeof field === 'string' && !field.startsWith('_') && !field.endsWith('$')) { return self[field]; } return undefined; } set(self, field, value, { add, merge, remove, reset, reparent, silent, type, } = {}) { let modelField = field instanceof ODataModelField ? field : this.findField(field); if (modelField === undefined && this.isOpenType() && typeof field === 'string') { type = type ?? this.tsToEdm[typeof value] ?? EdmType.String; modelField = this.modelFieldFactory(self, field, type); } if (modelField === undefined) throw new Error(`No field with name ${field}`); let changed = false; let attr = self._attributes.get(modelField.name); // Ensures that the attribute exists if (attr === undefined) { attr = new ODataModelAttribute(self, modelField); this._link(self, attr); self._attributes.set(modelField.name, attr); } const current = attr.get(); if (modelField.isStructuredType()) { if (value === null) { // New value is null changed = attr.set(value, reset, reparent); } else if (ODataModelOptions.isCollection(current)) { // Current is collection const currentCollection = current; if (ODataModelOptions.isCollection(value)) { // New value is collection changed = attr.set(value, reset, reparent); } else if (Types.isArray(value)) { // New value is array currentCollection._annotations = modelField.annotationsFactory(self.annots()); currentCollection.assign(value, { add, merge, remove, reset, reparent, silent, }); changed = currentCollection.hasChanged(); } } else if (ODataModelOptions.isModel(current)) { // Current is model const currentModel = current; if (ODataModelOptions.isModel(value)) { // New value is model changed = attr.set(value, reset, reparent); } else if (Types.isPlainObject(value)) { currentModel._annotations = modelField.annotationsFactory(self.annots()); currentModel.assign(value, { add, merge, remove, reset, reparent, silent, }); changed = currentModel.hasChanged(); } } else { // Current is null or undefined // create new model/collection for given value changed = attr.set(ODataModelOptions.isCollection(value) || ODataModelOptions.isModel(value) ? value : modelField.collection ? modelField.collectionFactory({ parent: self, value: value, reset: reset, }) : modelField.modelFactory({ parent: self, value: value, reset: reset, }), reset, reparent); } // Resolve referentials if (!ODataModelOptions.isCollection(attr.get())) { const meta = this.api.optionsForType(modelField.type); const ref = meta?.resolveReferential(attr.get(), attr, { resolve: false, }); if (ref !== null && ref !== undefined) { Object.assign(self, ref); } } } else { changed = attr.set(value, reset, reparent); } if (!silent && changed) { self.events$.trigger(ODataModelEventType.Change, { attr, value, previous: current, options: { key: modelField.isKey() }, }); } return changed; } _link(self, attr) { attr.events$.subscribe((event) => { if (event.canContinueWith(self)) { if (event.model === attr.get()) { if (event.type === ODataModelEventType.Change && attr.navigation && event.options?.key) { const ref = attr.get().referential(attr); if (ref !== null && ref !== undefined) { Object.assign(self, ref); } } } self.events$.emit(event.push(self, attr)); } }); } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3B0aW9ucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL2FuZ3VsYXItb2RhdGEvc3JjL2xpYi9tb2RlbHMvb3B0aW9ucy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsVUFBVSxFQUFnQixNQUFNLE1BQU0sQ0FBQztBQUNoRCxPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFDMUMsT0FBTyxFQUNMLGNBQWMsRUFDZCxRQUFRLEVBQ1IsZUFBZSxFQUNmLHNCQUFzQixFQUN0QixjQUFjLEdBQ2YsTUFBTSxjQUFjLENBQUM7QUFDdEIsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLFdBQVcsQ0FBQztBQVF4QyxPQUFPLEVBQ0wsbUJBQW1CLEVBQ25CLHNCQUFzQixFQUN0QiwrQkFBK0IsRUFDL0IscUJBQXFCLEdBQ3RCLE1BQU0sY0FBYyxDQUFDO0FBT3RCLE9BQU8sRUFBRSxPQUFPLEVBQWlCLE1BQU0sVUFBVSxDQUFDO0FBQ2xELE9BQU8sRUFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFLE1BQU0sVUFBVSxDQUFDO0FBQzFDLE9BQU8sRUFBRSxlQUFlLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFDL0MsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUNyQyxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sZUFBZSxDQUFDO0FBQzdDLE9BQU8sRUFFTCxzQkFBc0IsR0FDdkIsTUFBTSxnQkFBZ0IsQ0FBQztBQUV4QixNQUFNLENBQU4sSUFBWSxtQkFZWDtBQVpELFdBQVksbUJBQW1CO0lBQzdCLHdDQUFpQixDQUFBO0lBQ2pCLHNDQUFlLENBQUE7SUFDZix3Q0FBaUIsQ0FBQTtJQUNqQixvQ0FBYSxDQUFBO0lBQ2IsMENBQW1CLENBQUE7SUFDbkIsa0NBQVcsQ0FBQTtJQUNYLHdDQUFpQixDQUFBO0lBQ2pCLDBDQUFtQixDQUFBO0lBQ25CLDBDQUFtQixD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