UNPKG

breeze-client

Version:

Breeze data management for JavaScript clients

948 lines (913 loc) 333 kB
import { core } from './core'; import { assertParam, assertConfig } from './assert-param'; import { config } from './config'; import { BreezeEvent } from './event'; import { EntityAspect } from './entity-aspect'; import { MetadataStore, EntityType, DataProperty, AutoGeneratedKeyType } from './entity-metadata'; import { EntityKey } from './entity-key'; import { EntityAction } from './entity-action'; import { EntityState } from './entity-state'; import { DataService } from './data-service'; import { DataType } from './data-type'; import { ValidationError } from './validate'; import { ValidationOptions } from './validation-options'; import { QueryOptions, MergeStrategy, FetchStrategy } from './query-options'; import { SaveOptions } from './save-options'; import { KeyGenerator } from './key-generator'; import { EntityGroup } from './entity-group'; import { MappingContext } from './mapping-context'; import { EntityQuery } from './entity-query'; import { UnattachedChildrenMap } from './unattached-children-map'; /** Instances of the EntityManager contain and manage collections of entities, either retrieved from a backend datastore or created on the client. **/ export class EntityManager { // events /** A [[BreezeEvent]] that fires whenever a change to any entity in this EntityManager occurs. __Read Only__ @eventArgs - - entityAction - The [[EntityAction]] that occured. - entity - The entity that changed. If this is null, then all entities in the entityManager were affected. - args - Additional information about this event. This will differ based on the entityAction. > let em = new EntityManager( {serviceName: "breeze/NorthwindIBModel" }); > em.entityChanged.subscribe(function(changeArgs) { > // This code will be executed any time any entity within the entityManager > // is added, modified, deleted or detached for any reason. > let action = changeArgs.entityAction; > let entity = changeArgs.entity; > // .. do something to this entity when it is changed. > }); > }); @event **/ entityChanged; /** An [[BreezeEvent]] that fires whenever validationErrors change for any entity in this EntityManager. __Read Only__ @eventArgs - - entity - The entity on which the validation errors have been added or removed. - added - An array containing any newly added [[ValidationError]]s - removed - An array containing any newly removed [[ValidationError]]s. This is those errors that have been 'fixed' > let em = new EntityManager( {serviceName: "breeze/NorthwindIBModel" }); > em.validationErrorsChanged.subscribe(function(changeArgs) { > // This code will be executed any time any entity within the entityManager experiences a change to its validationErrors collection. > function (validationChangeArgs) { > let entity == validationChangeArgs.entity; > let errorsAdded = validationChangeArgs.added; > let errorsCleared = validationChangeArgs.removed; > // ... do something interesting with the order. > }); > }); > }); @event **/ validationErrorsChanged; /** A [[BreezeEvent]] that fires whenever an EntityManager transitions to or from having changes. __Read Only__ @eventArgs - - entityManager - The EntityManager whose 'hasChanges' status has changed. - hasChanges - Whether or not this EntityManager has changes. > let em = new EntityManager( {serviceName: "breeze/NorthwindIBModel" }); > em.hasChangesChanged.subscribe(function(args) { > let hasChangesChanged = args.hasChanges; > let entityManager = args.entityManager; > }); > }); @event **/ hasChangesChanged; /** @hidden @internal */ _pendingPubs; // TODO: refine later /** @hidden @internal */ _hasChangesAction; // TODO refine later /** @hidden @internal */ _hasChanges; /** @hidden @internal */ _entityGroupMap; /** @hidden @internal */ _unattachedChildrenMap; /** @hidden @internal */ _inKeyFixup; helper = { unwrapInstance: unwrapInstance, unwrapOriginalValues: unwrapOriginalValues, unwrapChangedValues: unwrapChangedValues }; /** EntityManager constructor. At its most basic an EntityManager can be constructed with just a service name > let entityManager = new EntityManager( "breeze/NorthwindIBModel"); This is the same as calling it with the following configuration object > let entityManager = new EntityManager( {serviceName: "breeze/NorthwindIBModel" }); Usually however, configuration objects will contain more than just the 'serviceName'; > let metadataStore = new MetadataStore(); > let entityManager = new EntityManager( { > serviceName: "breeze/NorthwindIBModel", > metadataStore: metadataStore > }); or > return new QueryOptions({ > mergeStrategy: obj, > fetchStrategy: this.fetchStrategy > }); > let queryOptions = new QueryOptions({ > mergeStrategy: MergeStrategy.OverwriteChanges, > fetchStrategy: FetchStrategy.FromServer > }); > let validationOptions = new ValidationOptions({ > validateOnAttach: true, > validateOnSave: true, > validateOnQuery: false > }); > let entityManager = new EntityManager({ > serviceName: "breeze/NorthwindIBModel", > queryOptions: queryOptions, > validationOptions: validationOptions > }); @param emConfig - Configuration settings or a service name. **/ constructor(emConfig) { if (arguments.length > 1) { throw new Error("The EntityManager ctor has a single optional argument that is either a 'serviceName' or a configuration object."); } let config; if (arguments.length === 0) { config = { serviceName: "" }; } else if (typeof emConfig === 'string') { config = { serviceName: emConfig }; } else { config = emConfig || {}; } EntityManager._updateWithConfig(this, config, true); this.entityChanged = new BreezeEvent("entityChanged", this); this.validationErrorsChanged = new BreezeEvent("validationErrorsChanged", this); this.hasChangesChanged = new BreezeEvent("hasChangesChanged", this); this.clear(); } /** General purpose property set method. Any of the properties in the [[EntityManagerConfig]] may be set. > // assume em1 is a previously created EntityManager > // where we want to change some of its settings. > em1.setProperties( { > serviceName: "breeze/foo" > }); @param config - An object containing the selected properties and values to set. **/ setProperties(config) { EntityManager._updateWithConfig(this, config, false); } /** @hidden @internal */ static _updateWithConfig(em, config, isCtor) { let defaultQueryOptions = isCtor ? QueryOptions.defaultInstance : em.queryOptions; let defaultSaveOptions = isCtor ? SaveOptions.defaultInstance : em.saveOptions; let defaultValidationOptions = isCtor ? ValidationOptions.defaultInstance : em.validationOptions; let configParam = assertConfig(config) .whereParam("serviceName").isOptional().isString() .whereParam("dataService").isOptional().isInstanceOf(DataService) .whereParam("queryOptions").isInstanceOf(QueryOptions).isOptional().withDefault(defaultQueryOptions) .whereParam("saveOptions").isInstanceOf(SaveOptions).isOptional().withDefault(defaultSaveOptions) .whereParam("validationOptions").isInstanceOf(ValidationOptions).isOptional().withDefault(defaultValidationOptions) .whereParam("keyGeneratorCtor").isFunction().isOptional(); if (isCtor) { configParam = configParam .whereParam("metadataStore").isInstanceOf(MetadataStore).isOptional().withDefault(new MetadataStore()); } configParam.applyAll(em); // insure that entityManager's options versions are completely populated core.updateWithDefaults(em.queryOptions, defaultQueryOptions); core.updateWithDefaults(em.saveOptions, defaultSaveOptions); core.updateWithDefaults(em.validationOptions, defaultValidationOptions); if (config.serviceName) { em.dataService = new DataService({ serviceName: em.serviceName }); } em.serviceName = em.dataService && em.dataService.serviceName; em.keyGeneratorCtor = em.keyGeneratorCtor || KeyGenerator; if (isCtor || config.keyGeneratorCtor) { em.keyGenerator = new em.keyGeneratorCtor(); } } /** Creates a new entity of a specified type and optionally initializes it. By default the new entity is created with an EntityState of Added but you can also optionally specify an EntityState. An EntityState of 'Detached' will insure that the entity is created but not yet added to the EntityManager. > // assume em1 is an EntityManager containing a number of preexisting entities. > // create and add an entity; > let emp1 = em1.createEntity("Employee"); > // create and add an initialized entity; > let emp2 = em1.createEntity("Employee", { lastName: "Smith", firstName: "John" }); > // create and attach (not add) an initialized entity > let emp3 = em1.createEntity("Employee", { id: 435, lastName: "Smith", firstName: "John" }, EntityState.Unchanged); > // create but don't attach an entity; > let emp4 = em1.createEntity("Employee", { id: 435, lastName: "Smith", firstName: "John" }, EntityState.Detached); @param typeName - The name of the EntityType for which an instance should be created. @param entityType - The EntityType of the type for which an instance should be created. @param initialValues - (default=null) Configuration object of the properties to set immediately after creation. @param entityState - (default = [[EntityState.Added]]) The EntityState of the entity after being created and added to this EntityManager. @param mergeStrategy - (default = [[MergeStrategy.Disallowed]]) - How to handle conflicts if an entity with the same key already exists within this EntityManager. @return {Entity} A new Entity of the specified type. */ createEntity(entityType, initialValues, entityState, mergeStrategy) { assertParam(entityType, "entityType").isString().or().isInstanceOf(EntityType).check(); assertParam(entityState, "entityState").isEnumOf(EntityState).isOptional().check(); assertParam(mergeStrategy, "mergeStrategy").isEnumOf(MergeStrategy).isOptional().check(); let et = (typeof entityType === "string") ? this.metadataStore._getStructuralType(entityType) : entityType; entityState = entityState || EntityState.Added; let entity = {}; core.using(this, "isLoading", true, function () { entity = et.createEntity(initialValues); }); if (entityState !== EntityState.Detached) { entity = this.attachEntity(entity, entityState, mergeStrategy); } return entity; } /** Creates a new EntityManager and imports a previously exported result into it. > // assume em1 is an EntityManager containing a number of preexisting entities. > let bundle = em1.exportEntities(); > // can be stored via the web storage api > window.localStorage.setItem("myEntityManager", bundle); > // assume the code below occurs in a different session. > let bundleFromStorage = window.localStorage.getItem("myEntityManager"); > // and imported > let em2 = EntityManager.importEntities(bundleFromStorage); > // em2 will now have a complete copy of what was in em1 @param exportedString - The result of a previous 'exportEntities' call as a string @param exportedData - The result of a previous 'exportEntities' call as an Object. @param config - A configuration object. @param config.mergeStrategy - A [[MergeStrategy]] to use when merging into an existing EntityManager. @param config.metadataVersionFn - A function that takes two arguments (the current metadataVersion and the imported store's 'name') and may be used to perform version checking. @return A new EntityManager. Note that the return value of this method call is different from that provided by the same named method on an EntityManager instance. Use that method if you need additional information regarding the imported entities. **/ static importEntities(exported, config) { let em = new EntityManager(); em.importEntities(exported, config); return em; } // instance methods /** Calls [[EntityAspect.acceptChanges]] on every changed entity in this EntityManager. **/ acceptChanges() { this.getChanges().map(function (entity) { return entity.entityAspect._checkOperation("acceptChanges"); }).forEach(function (aspect) { aspect.acceptChanges(); }); } /** Exports selected entities, all entities of selected types, or an entire EntityManager cache. This method takes a snapshot of an EntityManager that can be stored offline or held in memory. Use the [[EntityManager.importEntities]] method to restore or merge the snapshot into another EntityManager at some later time. > // let em1 be an EntityManager containing a number of existing entities. > // export every entity in em1. > let bundle = em1.exportEntities(); > // save to the browser's local storage > window.localStorage.setItem("myEntityManager", bundle); > // later retrieve the export > let bundleFromStorage = window.localStorage.getItem("myEntityManager"); > // import the retrieved export bundle into another manager > let em2 = em1.createEmptyCopy(); > em2.importEntities(bundleFromStorage); > // em2 now has a complete, faithful copy of the entities that were in em1 You can also control exactly which entities are exported. > // get em1's unsaved changes (an array) and export them. > let changes = em1.getChanges(); > let bundle = em1.exportEntities(changes); > // merge these entities into em2 which may contains some of the same entities. > // do NOT overwrite the entities in em2 if they themselves have unsaved changes. > em2.importEntities(bundle, { mergeStrategy: MergeStrategy.PreserveChanges} ); Metadata are included in an export by default. You may want to exclude the metadata especially if you're exporting just a few entities for local storage. > let bundle = em1.exportEntities(arrayOfSelectedEntities, {includeMetadata: false}); > window.localStorage.setItem("goodStuff", bundle); You may still express this option as a boolean value although this older syntax is deprecated. > // Exclude the metadata (deprecated syntax) > let bundle = em1.exportEntities(arrayOfSelectedEntities, false); You can export all entities of one or more specified EntityTypes. > // Export all Customer and Employee entities (and also exclude metadata) > let bundle = em1.exportEntities(['Customer', 'Employee'], {includeMetadata: false}); All of the above examples return an export bundle as a string which is the default format. You can export the bundle as JSON if you prefer by setting the `asString` option to false. > // Export all Customer and Employee entities as JSON and exclude the metadata > let bundle = em1.exportEntities(['Customer', 'Employee'], > {asString: false, includeMetadata: false}); > // store JSON bundle somewhere ... perhaps indexDb ... and later import as we do here. > em2.importEntities(bundle); @param entities - The entities to export or the EntityType(s) of the entities to export; all entities are exported if this parameter is omitted or null. @param exportConfig - Export configuration options or a boolean - asString - (boolean) - If true (default), return export bundle as a string. - includeMetadata - (boolean) - If true (default), include metadata in the export bundle. @return The export bundle either serialized as a string (default) or as a JSON object. The bundle contains the metadata (unless excluded) and the entity data grouped by type. The entity data include property values, change-state, and temporary key mappings (if any). The export bundle internals are deliberately undocumented. This Breeze-internal representation of entity data is suitable for export, storage, and import. The schema and contents of the bundle may change in future versions of Breeze. Manipulate it at your own risk with appropriate caution. **/ exportEntities(entities, exportConfig) { assertParam(entities, "entities").isArray().isEntity() .or().isNonEmptyArray().isInstanceOf(EntityType) .or().isNonEmptyArray().isString() .or().isOptional().check(); // assertParam(exportConfig, "exportConfig").isObject() // .or().isBoolean() // .or().isOptional().check(); if (exportConfig == null) { exportConfig = { includeMetadata: true, asString: true }; } else if (typeof exportConfig === 'boolean') { // deprecated exportConfig = { includeMetadata: exportConfig, asString: true }; } assertConfig(exportConfig) .whereParam("asString").isBoolean().isOptional().withDefault(true) .whereParam("includeMetadata").isBoolean().isOptional().withDefault(true) .applyAll(exportConfig); let exportBundle = exportEntityGroups(this, entities); let json = core.extend({}, exportBundle, ["tempKeys", "entityGroupMap"]); if (exportConfig.includeMetadata) { json = core.extend(json, this, ["dataService", "saveOptions", "queryOptions", "validationOptions"]); json.metadataStore = this.metadataStore.exportMetadata(); } else { json.metadataVersion = MetadataStore.metadataVersion; json.metadataStoreName = this.metadataStore.name; } let result = exportConfig.asString ? JSON.stringify(json, null, config.stringifyPad) : json; return result; } /** Imports a previously exported result into this EntityManager. This method can be used to make a complete copy of any previously created entityManager, even if created in a previous session and stored in localStorage. The static version of this method performs a very similar process. > // assume em1 is an EntityManager containing a number of existing entities. > let bundle = em1.exportEntities(); > // bundle can be stored in window.localStorage or just held in memory. > let em2 = new EntityManager({ > serviceName: em1.serviceName, > metadataStore: em1.metadataStore > }); > em2.importEntities(bundle); > // em2 will now have a complete copy of what was in em1 It can also be used to merge the contents of a previously created EntityManager with an existing EntityManager with control over how the two are merged. > let bundle = em1.exportEntities(); > // assume em2 is another entityManager containing some of the same entities possibly with modifications. > em2.importEntities(bundle, { mergeStrategy: MergeStrategy.PreserveChanges} ); > // em2 will now contain all of the entities from both em1 and em2. Any em2 entities with previously > // made modifications will not have been touched, but all other entities from em1 will have been imported. @param exportedString - The result of a previous 'export' call. @param importConfig - A configuration object. @param importConfig.mergeStrategy - A [[MergeStrategy]] to use when merging into an existing EntityManager. @param importConfig.metadataVersionFn - A function that takes two arguments (the current metadataVersion and the imported store's 'name') and may be used to perform version checking. @return result - result.entities {Array of Entities} The entities that were imported. - result.tempKeyMap {Object} Mapping from original EntityKey in the import bundle to its corresponding EntityKey in this EntityManager. **/ importEntities(exported, importConfig) { importConfig = importConfig || {}; assertConfig(importConfig) .whereParam("mergeStrategy").isEnumOf(MergeStrategy).isOptional().withDefault(this.queryOptions.mergeStrategy) .whereParam("metadataVersionFn").isFunction().isOptional() .whereParam("mergeAdds").isBoolean().isOptional() .applyAll(importConfig); let json = (typeof exported === "string") ? JSON.parse(exported) : exported; if (json.metadataStore) { this.metadataStore.importMetadata(json.metadataStore); // the || clause is for backwards compat with an earlier serialization format. this.dataService = (json.dataService && DataService.fromJSON(json.dataService)) || new DataService({ serviceName: json.serviceName }); this.saveOptions = new SaveOptions(json.saveOptions); this.queryOptions = QueryOptions.fromJSON(json.queryOptions); this.validationOptions = new ValidationOptions(json.validationOptions); } else { importConfig.metadataVersionFn && importConfig.metadataVersionFn({ metadataVersion: json.metadataVersion, metadataStoreName: json.metadataStoreName }); } let tempKeyMap = {}; json.tempKeys.forEach((k) => { let oldKey = EntityKey.fromJSON(k, this.metadataStore); // try to use oldKey if not already used in this keyGenerator. tempKeyMap[oldKey.toString()] = new EntityKey(oldKey.entityType, this.keyGenerator.generateTempKeyValue(oldKey.entityType, oldKey.values[0])); }); let entitiesToLink = []; let impConfig = importConfig; impConfig.tempKeyMap = tempKeyMap; core.wrapExecution(() => { this._pendingPubs = []; }, (state) => { this._pendingPubs.forEach((fn) => fn()); this._pendingPubs = undefined; this._hasChangesAction && this._hasChangesAction(); }, () => { core.objectForEach(json.entityGroupMap, (entityTypeName, jsonGroup) => { let entityType = this.metadataStore._getStructuralType(entityTypeName, false); let targetEntityGroup = findOrCreateEntityGroup(this, entityType); let entities = importEntityGroup(targetEntityGroup, jsonGroup, impConfig); if (entities && entities.length) { entitiesToLink = entitiesToLink.concat(entities); } }); entitiesToLink.forEach((entity) => { if (!entity.entityAspect.entityState.isDeleted()) { this._linkRelatedEntities(entity); } }); }); return { entities: entitiesToLink, tempKeyMapping: tempKeyMap }; } /** Clears this EntityManager's cache but keeps all other settings. Note that this method is not as fast as creating a new EntityManager via 'new EntityManager'. This is because clear actually detaches all of the entities from the EntityManager. > // assume em1 is an EntityManager containing a number of existing entities. > em1.clear(); > // em1 is will now contain no entities, but all other setting will be maintained. **/ clear() { core.objectMap(this._entityGroupMap, function (key, entityGroup) { return entityGroup._checkOperation('clear'); }).forEach((entityGroup) => { entityGroup._clear(); }); this._entityGroupMap = {}; this._unattachedChildrenMap = new UnattachedChildrenMap(); this.keyGenerator = new this.keyGeneratorCtor(); this.entityChanged.publish({ entityAction: EntityAction.Clear }); this._setHasChanges(false); } /** Creates an empty copy of this EntityManager but with the same DataService, MetadataStore, QueryOptions, SaveOptions, ValidationOptions, etc. > // assume em1 is an EntityManager containing a number of existing entities. > let em2 = em1.createEmptyCopy(); > // em2 is a new EntityManager with all of em1's settings > // but no entities. @return A new EntityManager. **/ createEmptyCopy() { let copy = new EntityManager(core.extend({}, this, ["dataService", "metadataStore", "queryOptions", "saveOptions", "validationOptions", "keyGeneratorCtor"])); return copy; } /** Attaches an entity to this EntityManager with an [[EntityState]] of 'Added'. > // assume em1 is an EntityManager containing a number of existing entities. > let custType = em1.metadataStore.getEntityType("Customer"); > let cust1 = custType.createEntity(); > em1.addEntity(cust1); Note that this is the same as using 'attachEntity' with an [[EntityState]] of 'Added'. > // assume em1 is an EntityManager containing a number of existing entities. > let custType = em1.metadataStore.getEntityType("Customer"); > let cust1 = custType.createEntity(); > em1.attachEntity(cust1, EntityState.Added); @param entity - The entity to add. @return The added entity. **/ addEntity(entity) { return this.attachEntity(entity, EntityState.Added); } /** Attaches an entity to this EntityManager with a specified [[EntityState]]. > // assume em1 is an EntityManager containing a number of existing entities. > let custType = em1.metadataStore.getEntityType("Customer"); > let cust1 = custType.createEntity(); > em1.attachEntity(cust1, EntityState.Added); @param entity - The entity to add. @param entityState - (default=EntityState.Unchanged) The EntityState of the newly attached entity. If omitted this defaults to EntityState.Unchanged. @param mergeStrategy - (default = MergeStrategy.Disallowed) How the specified entity should be merged into the EntityManager if this EntityManager already contains an entity with the same key. @return The attached entity. **/ attachEntity(entity, entityState, mergeStrategy) { assertParam(entity, "entity").isRequired().check(); this.metadataStore._checkEntityType(entity); let esSymbol = assertParam(entityState, "entityState").isEnumOf(EntityState).isOptional().check(EntityState.Unchanged); let msSymbol = assertParam(mergeStrategy, "mergeStrategy").isEnumOf(MergeStrategy).isOptional().check(MergeStrategy.Disallowed); if (entity.entityType.metadataStore !== this.metadataStore) { throw new Error("Cannot attach this entity because the EntityType (" + entity.entityType.name + ") and MetadataStore associated with this entity does not match this EntityManager's MetadataStore."); } let aspect = entity.entityAspect; if (aspect) { // to avoid reattaching an entity in progress if (aspect._inProcessEntity) return aspect._inProcessEntity; } else { // this occur's when attaching an entity created via new instead of via createEntity. aspect = new EntityAspect(entity); } let manager = aspect.entityManager; if (manager) { if (manager === this) { return entity; } else { throw new Error("This entity already belongs to another EntityManager"); } } let attachedEntity = {}; core.using(this, "isLoading", true, () => { if (esSymbol.isAdded()) { checkEntityKey(this, entity); } // attachedEntity === entity EXCEPT in the case of a merge. attachedEntity = this._attachEntityCore(entity, esSymbol, msSymbol); aspect._inProcessEntity = attachedEntity; try { // entity ( not attachedEntity) is deliberate here. attachRelatedEntities(this, entity, esSymbol, msSymbol); } finally { // insure that _inProcessEntity is cleared. aspect._inProcessEntity = undefined; } }); if (this.validationOptions.validateOnAttach) { attachedEntity.entityAspect.validateEntity(); } if (!esSymbol.isUnchanged()) { this._notifyStateChange(attachedEntity, true); } this.entityChanged.publish({ entityAction: EntityAction.Attach, entity: attachedEntity }); return attachedEntity; } /** Detaches an entity from this EntityManager. > // assume em1 is an EntityManager containing a number of existing entities. > // assume cust1 is a customer Entity previously attached to em1 > em1.detachEntity(cust1); > // em1 will now no longer contain cust1 and cust1 will have an > // entityAspect.entityState of EntityState.Detached @param entity - The entity to detach. @return Whether the entity could be detached. This will return false if the entity is already detached or was never attached. **/ detachEntity(entity) { assertParam(entity, "entity").isEntity().check(); let aspect = entity.entityAspect; if (!aspect) { // no aspect means in couldn't appear in any group return false; } if (aspect.entityManager !== this) { throw new Error("This entity does not belong to this EntityManager."); } return aspect.setDetached(); } /** Fetches the metadata associated with the EntityManager's current 'serviceName'. This call occurs internally before the first query to any service if the metadata hasn't already been loaded. __Async__ Usually you will not actually process the results of a fetchMetadata call directly, but will instead ask for the metadata from the EntityManager after the fetchMetadata call returns. > let em1 = new EntityManager( "breeze/NorthwindIBModel"); > em1.fetchMetadata() > .then(function() { > let metadataStore = em1.metadataStore; > // do something with the metadata > }).catch(function(exception) { > // handle exception here > }); @param callback - Function called on success. @param errorCallback - Function called on failure. @return {Promise} - schema {Object} The raw Schema object from metadata provider - Because this schema will differ depending on the metadata provider it is usually better to access metadata via the 'metadataStore' property of the EntityManager instead of using this 'raw' data. **/ fetchMetadata(dataService, callback, errorCallback) { if (typeof (dataService) === "function") { // legacy support for when dataService was not an arg. i.e. first arg was callback errorCallback = callback; callback = dataService; dataService = undefined; } else { assertParam(dataService, "dataService").isInstanceOf(DataService).isOptional().check(); assertParam(callback, "callback").isFunction().isOptional().check(); assertParam(errorCallback, "errorCallback").isFunction().isOptional().check(); } let promise = this.metadataStore.fetchMetadata(dataService || this.dataService); return promiseWithCallbacks(promise, callback, errorCallback); } /** Executes the specified query. __Async__ > let em = new EntityManager(serviceName); > let query = new EntityQuery("Orders"); > em.executeQuery(query).then( function(data) { > let orders = data.results; > ... query results processed here > }).catch( function(err) { > ... query failure processed here > }); or with callbacks > let em = new EntityManager(serviceName); > let query = new EntityQuery("Orders"); > em.executeQuery(query, > function(data) { > let orders = data.results; > ... query results processed here > }, > function(err) { > ... query failure processed here > }); Either way this method is the same as calling the The [[EntityQuery]] 'execute' method. > let em = new EntityManager(serviceName); > let query = new EntityQuery("Orders").using(em); > query.execute().then( function(data) { > let orders = data.results; > ... query results processed here > }).catch( function(err) { > ... query failure processed here > }); @param query - The [[EntityQuery]] or OData query string to execute. @param callback - Function called on success. @param errorCallback - {Function} Function called on failure. @return Promise of - results - An array of entities - retrievedEntities - A array of all of the entities returned by the query. Differs from results (above) when .expand() is used. - query - The original [[EntityQuery]] or query string - entityManager - The EntityManager. - httpResponse - The [[IHttpResponse]] returned from the server. - inlineCount - Only available if 'inlineCount(true)' was applied to the query. Returns the count of items that would have been returned by the query before applying any skip or take operators, but after any filter/where predicates would have been applied. **/ executeQuery(query, callback, errorCallback) { assertParam(query, "query").isInstanceOf(EntityQuery).or().isString().check(); assertParam(callback, "callback").isFunction().isOptional().check(); assertParam(errorCallback, "errorCallback").isFunction().isOptional().check(); let promise; // 'resolve' methods create a new typed object with all of its properties fully resolved against a list of sources. // Thought about creating a 'normalized' query with these 'resolved' objects // but decided not to because the 'query' may not be an EntityQuery (it can be a string) and hence might not have a queryOptions or dataServices property on it. let queryOptions = QueryOptions.resolve([query.queryOptions, this.queryOptions, QueryOptions.defaultInstance]); let dataService = DataService.resolve([query.dataService, this.dataService]); if ((!dataService.hasServerMetadata) || this.metadataStore.hasMetadataFor(dataService.serviceName)) { promise = executeQueryCore(this, query, queryOptions, dataService); } else { promise = this.fetchMetadata(dataService).then(() => { return executeQueryCore(this, query, queryOptions, dataService); }); } return promiseWithCallbacks(promise, callback, errorCallback); } /** Executes the specified query against this EntityManager's local cache. Because this method is executed immediately there is no need for a promise or a callback > let em = new EntityManager(serviceName); > let query = new EntityQuery("Orders"); > let orders = em.executeQueryLocally(query); Note that this can also be accomplished using the 'executeQuery' method with a FetchStrategy of FromLocalCache and making use of the Promise or callback > let em = new EntityManager(serviceName); > let query = new EntityQuery("Orders").using(FetchStrategy.FromLocalCache); > em.executeQuery(query).then( function(data) { > let orders = data.results; > ... query results processed here > }).catch( function(err) { > ... query failure processed here > }); @param query - The [[EntityQuery]] to execute. @return {Array of Entity} Array of entities from cache that satisfy the query **/ executeQueryLocally(query) { return executeQueryLocallyCore(this, query).results; } /** Saves either a list of specified entities or all changed entities within this EntityManager. If there are no changes to any of the entities specified then there will be no server side call made but a valid 'empty' saveResult will still be returned. __Async__ Often we will be saving all of the entities within an EntityManager that are either added, modified or deleted and we will let the 'saveChanges' call determine which entities these are. > // assume em1 is an EntityManager containing a number of preexisting entities. > // This could include added, modified and deleted entities. > em.saveChanges().then(function(saveResult) { > let savedEntities = saveResult.entities; > let keyMappings = saveResult.keyMappings; > }).catch(function (e) { > // e is any exception that was thrown. > }); But we can also control exactly which entities to save and can specify specific SaveOptions > // assume entitiesToSave is an array of entities to save. > let saveOptions = new SaveOptions({ allowConcurrentSaves: true }); > em.saveChanges(entitiesToSave, saveOptions).then(function(saveResult) { > let savedEntities = saveResult.entities; > let keyMappings = saveResult.keyMappings; > }).catch(function (e) { > // e is any exception that was thrown. > }); Callback methods can also be used > em.saveChanges(entitiesToSave, null, > function(saveResult) { > let savedEntities = saveResult.entities; > let keyMappings = saveResult.keyMappings; > }, function (e) { > // e is any exception that was thrown. > } > ); @param entities - The list of entities to save. Every entity in that list will be sent to the server, whether changed or unchanged, as long as it is attached to this EntityManager. If this parameter is omitted, null or empty (the usual case), every entity with pending changes in this EntityManager will be saved. @param saveOptions - [[SaveOptions]] for the save - will default to [[EntityManager.saveOptions]] if null. @param callback - Function called on success. @param errorCallback - Function called on failure. @return {Promise} Promise **/ saveChanges(entities, saveOptions, callback, errorCallback) { assertParam(entities, "entities").isOptional().isArray().isEntity().check(); assertParam(saveOptions, "saveOptions").isInstanceOf(SaveOptions).isOptional().check(); assertParam(callback, "callback").isFunction().isOptional().check(); assertParam(errorCallback, "errorCallback").isFunction().isOptional().check(); saveOptions = saveOptions || this.saveOptions || SaveOptions.defaultInstance; let entitiesToSave = getEntitiesToSave(this, entities ? entities : undefined); if (entitiesToSave.length === 0) { let result = { entities: [], keyMappings: [] }; if (callback) callback(result); return Promise.resolve(result); } if (!saveOptions.allowConcurrentSaves) { let anyPendingSaves = entitiesToSave.some(function (entity) { return entity.entityAspect.isBeingSaved; }); if (anyPendingSaves) { let err = new Error("Concurrent saves not allowed - SaveOptions.allowConcurrentSaves is false"); if (errorCallback) errorCallback(err); return Promise.reject(err); } } clearServerErrors(entitiesToSave); let valError = this.saveChangesValidateOnClient(entitiesToSave); if (valError) { if (errorCallback) errorCallback(valError); return Promise.reject(valError); } let dataService = DataService.resolve([saveOptions.dataService, this.dataService]); let saveContext = { entityManager: this, dataService: dataService, processSavedEntities: processSavedEntities, resourceName: saveOptions.resourceName || this.saveOptions.resourceName || "SaveChanges" }; // TODO: need to check that if we are doing a partial save that all entities whose temp keys // are referenced are also in the partial save group let saveBundle = { entities: entitiesToSave, saveOptions: saveOptions }; try { // Guard against exception thrown in dataservice adapter before it goes async updateConcurrencyProperties(entitiesToSave); return dataService.adapterInstance.saveChanges(saveContext, saveBundle) .then(saveSuccess).then((r) => r, saveFail); } catch (err) { // undo the marking by updateConcurrencyProperties markIsBeingSaved(entitiesToSave, false); if (errorCallback) errorCallback(err); return Promise.reject(err); } function saveSuccess(saveResult) { let em = saveContext.entityManager; markIsBeingSaved(entitiesToSave, false); let savedEntities = saveContext.processSavedEntities(saveResult); saveResult.entities = savedEntities; // update _hasChanges after save. em._setHasChanges(); // can't do this anymore because other changes might have been made while saved entities in flight. // let hasChanges = (isFullSave && haveSameContents(entitiesToSave, savedEntities)) ? false : null; // em._setHasChanges(hasChanges); if (callback) callback(saveResult); return Promise.resolve(saveResult); } function processSavedEntities(saveResult) { let savedEntities = saveResult.entities; let deletedKeys = saveResult.deletedKeys || []; if (savedEntities.length === 0 && deletedKeys.length === 0) { return []; } let keyMappings = saveResult.keyMappings; let em = saveContext.entityManager; // must occur outside of isLoading block fixupKeys(em, keyMappings); core.using(em, "isLoading", true, () => { let mappingContext = new MappingContext({ query: undefined, entityManager: em, mergeOptions: { mergeStrategy: MergeStrategy.OverwriteChanges }, dataService: dataService }); // The visitAndMerge operation has been optimized so that we do not actually perform a merge if the // the save operation did not actually return the entity - i.e. during OData and Mongo updates and deletes. savedEntities = mappingContext.visitAndMerge(savedEntities, { nodeType: "root" }); }); // detach any entities found in the em that appear in the deletedKeys list. deletedKeys.forEach(key => { let entityType = em.metadataStore._getStructuralType(key.entityTypeName); let ekey = new EntityKey(entityType, key.keyValues); let entity = em.getEntityByKey(ekey); if (entity) { entity.entityAspect.setDetached(); } }); return savedEntities; } function saveFail(serverError) { markIsBeingSaved(entitiesToSave, false); let clientError = processServerErrors(saveContext, serverError); if (errorCallback) errorCallback(clientError); return Promise.reject(clientError); } } /** Run the "saveChanges" pre-save client validation logic. This is NOT a general purpose validation method. It is intended for utilities that must know if saveChanges would reject the save due to client validation errors. It only validates entities if the EntityManager's [[ValidationOptions]].validateOnSave is true. @param entitiesToSave {Array of Entity} The list of entities to save (to validate). @return {Error} Validation error or null if no error **/ saveChangesValidateOnClient(entitiesToSave) { if (this.validationOptions.validateOnSave) { let failedEntities = entitiesToSave.filter(function (entity) { let aspect = entity.entityAspect; let isValid = aspect.entityState.isDeleted() || aspect.validateEntity(); return !isValid; }); if (failedEntities.length > 0) { let valError = new Error("Client side validation errors encountered - see the entityErrors collection on this object for more detail"); valError.entityErrors = createEntityErrors(failedEntities); return valError; // TODO: type this. } } return null; } /** @hidden @internal */ _findEntityGroup(entityType) { return this._entityGroupMap[entityType.name]; } /** Attempts to locate an entity within this EntityManager by its [EntityKey]. @param entityKey - The [[EntityKey]] of the Entity to be located. @param type - The [[EntityType]] for this key. @param typeName - The EntityType name for this key. @param keyValues - The values for this key - will usually just be a single value; an array is only needed for multipart keys. @return An Entity or null; **/ getEntityByKey(...args) { let entityKey = createEntityKey(this, args).entityKey; let entityTypes = entityKey._subtypes || [entityKey.entityType]; let e; // hack use of some to simulate mapFirst logic. entityTypes.some((et) => { let group = this._findEntityGroup(et); // group version of findEntityByKey doesn't care about entityType e = group && group.findEntityByKey(entityKey); return e != null; }); return e || null; } /** Attempts to fetch an entity from the server by its [[EntityKey]] with an option to check the local cache first. Note the this EntityManager's queryOptions.mergeStrategy will be used to merge any server side entity returned by this method. > // assume em1 is an EntityManager containing a number of preexisting entities. > let employeeType = em1.metadataStore.getEntityType("Employee"); > let employeeKey = new EntityKey(employeeType, 1); > em1.fetchEntityByKey(employeeKey).then(function(result) { > let employee = result.entity; > let entityKey = result.entityKey; > let fromCache = result.fromCache; > }); @param typeName - The EntityType name for this key. @param entityType - The EntityType for this key. @param keyValues - The values for this key - will usually just be a single value; an array is only needed for multipart keys. @param entityKey - The [[EntityKey]] of the Entity to be located. @param checkLocalCacheFirst - (default = false) - Whether to check this EntityManager first before going to the server. By default, the query will NOT do this. @return {Promise} - Properties on the promise success result - entity {Object} The entity returned or null - entityKey {EntityKey} The entityKey of the entity to fetch. - fromCache {Boolean} Whether this entity was fetched from the server or was found in the local cache.