diff --git a/core/deprecate.js b/core/deprecate.js index 744ef1470e..892fa1d00f 100644 --- a/core/deprecate.js +++ b/core/deprecate.js @@ -1,5 +1,5 @@ /* global console */ -var Montage = require("./core").Montage; +var Montage = require("./core").Montage, Map = require("collections/map"); var deprecatedFeaturesOnceMap = new Map(); diff --git a/core/meta/property-descriptor.js b/core/meta/property-descriptor.js index 7ae0ccc1bc..90cb497d3d 100644 --- a/core/meta/property-descriptor.js +++ b/core/meta/property-descriptor.js @@ -125,12 +125,17 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# if (value !== void 0) { this._owner = value; } - + this._overridePropertyWithDefaults(deserializer, "cardinality"); - + if (this.cardinality === -1) { this.cardinality = Infinity; } + + value = deserializer.getProperty("isDerived"); + if (value !== void 0) { + this._isDerived = value; + } this._overridePropertyWithDefaults(deserializer, "mandatory"); this._overridePropertyWithDefaults(deserializer, "readOnly"); @@ -303,7 +308,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# */ isDerived: { get: function () { - return false; + return this._isDerived || false; } }, diff --git a/data/converter/raw-property-value-to-object-converter.js b/data/converter/raw-property-value-to-object-converter.js index 6031a617e8..7a543115f1 100644 --- a/data/converter/raw-property-value-to-object-converter.js +++ b/data/converter/raw-property-value-to-object-converter.js @@ -162,7 +162,10 @@ exports.RawPropertyValueToObjectConverter = Converter.specialize( /** @lends Raw * */ revertSyntax: { get: function() { - return this._revertSyntax || (this._revertSyntax = parse(this.revertExpression)); + if (!this._revertSyntax && this.revertExpression) { + this._revertSyntax = parse(this.revertExpression); + } + return this._revertSyntax; } }, @@ -172,7 +175,10 @@ exports.RawPropertyValueToObjectConverter = Converter.specialize( /** @lends Raw compiledRevertSyntax: { get: function () { - return this._compiledRevertSyntax || (this._compiledRevertSyntax = compile(this.revertSyntax)); + if (!this._compiledRevertSyntax && this.revertSyntax) { + this._compiledRevertSyntax = compile(this.revertSyntax); + } + return this._compiledRevertSyntax; } }, diff --git a/data/model/data-object-descriptor.js b/data/model/data-object-descriptor.js index 565b765f39..54dd0b6de5 100644 --- a/data/model/data-object-descriptor.js +++ b/data/model/data-object-descriptor.js @@ -1,5 +1,6 @@ var ObjectDescriptor = require("./object-descriptor").ObjectDescriptor, - DataPropertyDescriptor = require("./data-property-descriptor").DataPropertyDescriptor; + DataPropertyDescriptor = require("./data-property-descriptor").DataPropertyDescriptor, + deprecate = require("core/deprecate"); /** * Extends an object descriptor with the additional object information needed by @@ -10,7 +11,7 @@ var ObjectDescriptor = require("./object-descriptor").ObjectDescriptor, * @extends ObjectDescriptor */ exports.DataObjectDescriptor = ObjectDescriptor.specialize(/** @lends DataObjectDescriptor.prototype */ { - + /** * The names of the properties containing the identifier for this type of * data object. diff --git a/data/service/data-mapping.js b/data/service/data-mapping.js index d300995839..d901e87757 100644 --- a/data/service/data-mapping.js +++ b/data/service/data-mapping.js @@ -48,9 +48,10 @@ exports.DataMapping = Montage.specialize(/** @lends DataMapping.prototype */ { */ mapRawDataToObject: { value: function (data, object, context) { - var i, key, - keys = Object.keys(data); + var i, key, keys; + if (data) { + keys = Object.keys(data); for (i = 0; (key = keys[i]); ++i) { object[key] = data[key]; } @@ -58,6 +59,25 @@ exports.DataMapping = Montage.specialize(/** @lends DataMapping.prototype */ { } }, + /** + * Maps the value of a single raw data property onto the model object + * + * @method + * @argument {Object} data - An object whose properties' values + * hold the raw data. + * @argument {Object} object - The object on which to assign the property + * @argument {string} propertyName - The name of the model property to which + * to assign the value(s). + * @returns {DataStream|Promise|?} - Either the value or a "promise" for it + * + */ + mapRawDataToObjectProperty: { + value: function (data, object, propertyName) { + // TO DO: Provide a default mapping based on object descriptor and prpoerty name + // For now, subclasses must override this. + } + }, + /** * @todo Document. */ diff --git a/data/service/data-service.js b/data/service/data-service.js index 61a2137b57..348a0e00da 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -1,13 +1,17 @@ var Montage = require("core/core").Montage, AuthorizationManager = require("data/service/authorization-manager").AuthorizationManager, AuthorizationPolicy = require("data/service/authorization-policy").AuthorizationPolicy, + DataIdentifier = require("data/model/data-identifier").DataIdentifier, + DataMapping = require("data/service/data-mapping").DataMapping, DataObjectDescriptor = require("data/model/data-object-descriptor").DataObjectDescriptor, DataQuery = require("data/model/data-query").DataQuery, DataStream = require("data/service/data-stream").DataStream, DataTrigger = require("data/service/data-trigger").DataTrigger, + deprecate = require("core/deprecate"), Map = require("collections/map"), Promise = require("core/promise").Promise, ObjectDescriptor = require("core/meta/object-descriptor").ObjectDescriptor, + Scope = require("frb/scope"), Set = require("collections/set"), WeakMap = require("collections/weak-map"); @@ -46,6 +50,8 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { exports.DataService.mainService = exports.DataService.mainService || this; this._initializeAuthorization(); this._initializeOffline(); + this._typeIdentifierMap = new Map(); + this._descriptorToRawDataTypeMappings = new Map(); } }, @@ -86,6 +92,12 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { if (value) { this.delegate = value; } + + value = deserializer.getProperty("rawDataTypeMappings"); + this._registerRawDataTypeMappings(value || []); + + value = deserializer.getProperty("batchAddsDataToStream"); + this.batchAddsDataToStream = !!value; return result; } @@ -101,6 +113,12 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { * Private properties are defined where they are used, not here. */ + identifier: { + get: function() { + return this._identifier || (this._identifier = Montage.getInfoForObject(this).moduleId); + } + }, + /** * The types of data handled by this service. If this `undefined`, `null`, * or an empty array this service is assumed to handled all types of data. @@ -120,7 +138,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { * A service's types must not be changed after it is added as a child of * another service. * - * @type {Array.} + * @type {Array.} */ types: { get: function () { @@ -141,128 +159,74 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, + /*************************************************************************** - * Service Hierarchy + * Snapshotting */ - /** - * A read-only reference to the parent of this service. - * - * This value is modified by calls to - * [addChildService()]{@link DataService#addChildService} and - * [removeChildService()]{@link DataService#removeChildService} and cannot - * be modified directly. - * - * Data services that have no parents are called - * [root services]{@link DataService#rootService}. - * - * @type {?DataService} - */ - parentService: { - get: function () { - return this._parentService; - } + __snapshot: { + value: null }, - /** - * Private settable parent service reference. - * - * This property's value should not be modified outside of - * [addChildService()]{@link DataService#addChildService} and - * [removeChildService()]{@link DataService#removeChildService}. - * - * @private - * @type {?DataService} - */ - _parentService: { - value: undefined + _snapshot: { + get: function() { + return this.__snapshot || (this.__snapshot = new Map()); + } }, /** - * Convenience read-only reference to the root of the service tree - * containing this service. Most applications have only one root service, - * the application's [main service]{@link DataService.mainService}. + * Records the snapshot of the values of record known for a DataIdentifier * - * @type {DataService} + * @private + * @argument {DataIdentifier} dataIdentifier + * @argument {Object} rawData */ - rootService: { - get: function () { - return this.parentService ? this.parentService.rootService : this; + recordSnapshot: { + value: function (dataIdentifier, rawData) { + this._snapshot.set(dataIdentifier, rawData); } }, /** - * Convenience method to assess if a dataService is the rootService + * Removes the snapshot of the values of record for the DataIdentifier argument * - * @type {Boolean} + * @private + * @argument {DataIdentifier} dataIdentifier */ - isRootService: { - get: function () { - return this === this.rootService; + removeSnapshot: { + value: function (dataIdentifier) { + this._snapshot.delete(dataIdentifier); } }, /** - * The child services of this service. - * - * This value is modified by calls to - * [addChildService()]{@link DataService#addChildService} and - * [removeChildService()]{@link DataService#removeChildService} and must not - * be modified directly. + * Returns the snapshot associated with the DataIdentifier argument if available * - * @type {Set.} + * @private + * @argument {DataIdentifier} dataIdentifier */ - childServices: { - get: function() { - if (!this._childServices) { - this._childServices = new Set(); - } - return this._childServices; + snapshotForDataIdentifier: { + value: function (dataIdentifier) { + return this._snapshot.get(dataIdentifier); } }, /** - * Private settable child service set. - * - * This property should not be modified outside of the - * [childServices getter]{@link DataService#childServices}, and its contents - * should not be modified outside of - * [addChildService()]{@link DataService#addChildService} and - * [removeChildService()]{@link DataService#removeChildService} + * Returns the snapshot associated with the DataIdentifier argument if available * * @private - * @type {?Set.} + * @argument {DataIdentifier} dataIdentifier */ - _childServices: { - value: undefined + snapshotForObject: { + value: function (object) { + return this.snapshotForDataIdentifier(this.dataIdentifierForObject(object)); + } }, - /** - * Adds a raw data service as a child of this data service and set it to - * handle data of the types defined by its [types]{@link DataService#types} - * property. - * - * Child services must have their [types]{@link DataService#types} property - * value or their [model]{@link DataService#model} set before they are passed in to - * this method, and that value cannot change after that. The model property takes - * priority of the types property. If the model is defined the service will handle - * all the object descriptors associated to the model. - * - * @method - * @argument {RawDataService} service - * @argument {Array} [types] Types to use instead of the child's types. + + /*************************************************************************** + * Service Hierarchy */ - addChildService: { - value: function (child, types) { - if (child instanceof exports.DataService && - child.constructor !== exports.DataService) { - this._addChildService(child, types); - } else { - console.warn("Cannot add child -", child); - console.warn("Children must be instances of DataService subclasses."); - } - } - }, _addChildService: { value: function (child, types) { @@ -284,10 +248,10 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { for (i = 0, n = types && types.length || nIfEmpty; i < n; i += 1) { type = types && types.length && types[i] || null; - children = this._childServicesByType.get(type) || []; + children = this._childServicesByObjectDescriptor.get(type) || []; children.push(child); if (children.length === 1) { - this._childServicesByType.set(type, children); + this._childServicesByObjectDescriptor.set(type, children); if (type) { this._childServiceTypes.push(type); } @@ -298,10 +262,6 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, - __childServiceRegistrationPromise: { - value: null - }, - _childServiceRegistrationPromise: { get: function() { return this.__childServiceRegistrationPromise || (this.__childServiceRegistrationPromise = Promise.resolve()); @@ -311,54 +271,125 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, - registerChildServices: { - value: function (childServices) { - var self; - if (!this.__childServiceRegistrationPromise) { - self = this; - this.__childServiceRegistrationPromise = Promise.all(childServices.map(function (child) { - return self.registerChildService(child); - })); + /** + * Private settable child service set. + * + * This property should not be modified outside of the + * [childServices getter]{@link DataService#childServices}, and its contents + * should not be modified outside of + * [addChildService()]{@link DataService#addChildService} and + * [removeChildService()]{@link DataService#removeChildService} + * + * @private + * @type {?Set.} + */ + _childServices: { + value: undefined + }, + + /** + * Private settable parent service reference. + * + * This property's value should not be modified outside of + * [addChildService()]{@link DataService#addChildService} and + * [removeChildService()]{@link DataService#removeChildService}. + * + * @private + * @type {?DataService} + */ + _parentService: { + value: undefined + }, + + /** + * A map from each of the data types handled by this service to an array + * of the child services that can handle that type, with each such array + * ordered according to the order in which the services in it were + * [added]{@link DataService#addChildService} as children of this service. + * + * If one or more child services of this service are defined as handling all + * types (their [types]{@link DataService#types} property is `undefined`, + * `null`, or an empty array), the child service map also include a `null` + * key whose corresponding value is an array of all those services defined + * to handle all types. + * + * The contents of this map should not be modified outside of + * [addChildService()]{@link DataService#addChildService} and + * [removeChildService()]{@link DataService#removeChildService}. + * + * @private + * @type {Map>} + */ + _childServicesByType: { + get: deprecate.deprecateMethod(void 0, function () { + return this._childServicesByObjectDescriptor; + }, "_childServicesByType", "_childServicesByObjectDescriptor") + }, + + _childServicesByObjectDescriptor: { + get: function () { + if (!this.__childServicesByObjectDescriptor) { + this.__childServicesByObjectDescriptor = new Map(); + } + return this.__childServicesByObjectDescriptor; + } + }, + + _childServicesByIdentifier: { + get: function () { + if (!this.__childServicesByIdentifier) { + this.__childServicesByIdentifier = new Map(); } + return this.__childServicesByIdentifier; } }, /** - * Alternative to [addChildService()]{@link DataService#addChildService}. - * While addChildService is synchronous, registerChildService is asynchronous - * and may take a child whose [types]{@link DataService#types} property is - * a promise instead of an array. - * - * This is useful for example if the child service does not know its types - * immediately, e.g. if it must fetch them from a .mjson descriptors file. + * Get the first child service that can handle the specified object, + * or `null` if no such child service exists. * - * If the child's types is an array, it is guaranteed to behave exactly - * like addChildService. + * @private + * @method + * @argument {Object} object + * @returns DataService + */ + _childServiceForObject: { + value: function (object) { + return this.childServiceForType(this.rootService.objectDescriptorForObject(object)); + } + }, + + /** + * Get the first child service that can handle the specified object, + * or `null` if no such child service exists. * + * @deprecated * @method - * @param {DataService} child service to add to this service. - * @param {?Promise|ObjectDescriptor|Array} - * @return {Promise} + * @argument {Object} object + * @returns DataService */ - registerChildService: { - value: function (child, types) { - var self = this, - mappings = child.mappings || []; - // possible types - // -- types is passed in as an array or a single type. - // -- a model is set on the child. - // -- types is set on the child. - // any type can be asychronous or synchronous. - types = types && Array.isArray(types) && types || - types && [types] || - child.model && child.model.objectDescriptors || - child.types && Array.isArray(child.types) && child.types || - child.types && [child.types] || - []; + _getChildServiceForObject: { + value: deprecate.deprecateMethod(void 0, function (object) { + return this._childServiceForObject(object); + }, "_getChildServiceForObject", "_childServiceForObject", true) + }, - return child._childServiceRegistrationPromise.then(function () { - return self._registerChildServiceTypesAndMappings(child, types, mappings); - }); + /** + * An array of the data types handled by all child services of this service. + * + * The contents of this map should not be modified outside of + * [addChildService()]{@link DataService#addChildService} and + * [removeChildService()]{@link DataService#removeChildService}. + * + * @private + * @type {Array.} + */ + _childServiceTypes: { + get: function() { + if (!this.__childServiceTypes) { + this.__childServiceTypes = []; + } + return this.__childServiceTypes; } }, @@ -386,10 +417,10 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { objectDescriptors; return this._resolveAsynchronousTypes(types).then(function (descriptors) { objectDescriptors = descriptors; - self._registerTypesByModuleId(objectDescriptors); + self._registerObjectDescriptorsByModuleId(objectDescriptors); return self._registerChildServiceMappings(child, mappings); }).then(function () { - return self._makePrototypesForTypes(child, objectDescriptors); + return self._prototypesForModuleObjectDescriptors(objectDescriptors); }).then(function () { self.addChildService(child, types); return null; @@ -397,105 +428,163 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, - _resolveAsynchronousTypes: { - value: function (types) { - var self = this; - return Promise.all(this._flattenArray(types).map(function (type) { - return type instanceof Promise ? type : Promise.resolve(type); - })).then(function (descriptors) { - return self._flattenArray(descriptors); - }); - } - }, - - _flattenArray: { - value: function (array) { - return Array.prototype.concat.apply([], array); + /** + * Adds a raw data service as a child of this data service and set it to + * handle data of the types defined by its [types]{@link DataService#types} + * property. + * + * Child services must have their [types]{@link DataService#types} property + * value or their [model]{@link DataService#model} set before they are passed in to + * this method, and that value cannot change after that. The model property takes + * priority of the types property. If the model is defined the service will handle + * all the object descriptors associated to the model. + * + * @method + * @argument {RawDataService} service + * @argument {Array} [types] Types to use instead of the child's types. + */ + addChildService: { + value: function (child, types) { + if (child instanceof exports.DataService && + child.constructor !== exports.DataService) { + this._addChildService(child, types); + } else { + console.warn("Cannot add child -", child); + console.warn("Children must be instances of DataService subclasses."); + } } }, - _registerTypesByModuleId: { - value: function (types) { - var map = this._moduleIdToObjectDescriptorMap; - types.forEach(function (objectDescriptor) { - var module = objectDescriptor.module, - moduleId = [module.id, objectDescriptor.exportName].join("/"); - map[moduleId] = objectDescriptor; - }); + + /** + * The child services of this service. + * + * This value is modified by calls to + * [addChildService()]{@link DataService#addChildService} and + * [removeChildService()]{@link DataService#removeChildService} and must not + * be modified directly. + * + * @type {Set.} + */ + childServices: { + get: function() { + if (!this._childServices) { + this._childServices = new Set(); + } + return this._childServices; } }, - _registerChildServiceMappings: { - value: function (child, mappings) { - var self = this; - return Promise.all(mappings.map(function (mapping) { - return self._addMappingToChild(mapping, child); - })); + /** + * Get the first child service that can handle data of the specified type, + * or `null` if no such child service exists. + * + * @method + * @argument {ObjectDescriptor|Function|String} type - ObjectDescriptor, constructor, or moduleId of a type + * @returns {DataService} + */ + childServiceForType: { + value: function (type) { + var descriptor = this._objectDescriptorForType(type), + services = this._childServicesByObjectDescriptor.get(descriptor) || this._childServicesByObjectDescriptor.get(null); + + return services && services[0] || null; } }, + - _makePrototypesForTypes: { - value: function (childService, types) { - var self = this; - return Promise.all(types.map(function (objectDescriptor) { - return self._makePrototypeForType(childService, objectDescriptor); - })); + /** + * Convenience method to assess if a dataService is the rootService + * + * @type {Boolean} + */ + isRootService: { + get: function () { + return this === this.rootService; } }, - _makePrototypeForType: { - value: function (childService, objectDescriptor) { - var self = this, - module = objectDescriptor.module; - return module.require.async(module.id).then(function (exports) { - var constructor = exports[objectDescriptor.exportName], - prototype = Object.create(constructor.prototype), - mapping = childService.mappingWithType(objectDescriptor), - requisitePropertyNames = mapping && mapping.requisitePropertyNames || new Set(), - dataTriggers = DataTrigger.addTriggers(self, objectDescriptor, prototype, requisitePropertyNames); - self._dataObjectPrototypes.set(constructor, prototype); - self._dataObjectPrototypes.set(objectDescriptor, prototype); - self._dataObjectTriggers.set(objectDescriptor, dataTriggers); - self._constructorToObjectDescriptorMap.set(constructor, objectDescriptor); - return null; - }); + /** + * A read-only reference to the parent of this service. + * + * This value is modified by calls to + * [addChildService()]{@link DataService#addChildService} and + * [removeChildService()]{@link DataService#removeChildService} and cannot + * be modified directly. + * + * Data services that have no parents are called + * [root services]{@link DataService#rootService}. + * + * @type {?DataService} + */ + parentService: { + get: function () { + return this._parentService; } }, - - _addMappingToChild: { - value: function (mapping, child) { - var service = this; - return Promise.all([ - mapping.objectDescriptor, - mapping.schemaDescriptor - ]).spread(function (objectDescriptor, schemaDescriptor) { - // TODO -- remove looking up by string to unique. - var type = [objectDescriptor.module.id, objectDescriptor.name].join("/"); - objectDescriptor = service._moduleIdToObjectDescriptorMap[type]; - mapping.objectDescriptor = objectDescriptor; - mapping.schemaDescriptor = schemaDescriptor; - mapping.service = child; - child.addMappingForType(mapping, objectDescriptor); - return null; - }); + + /** + * Convenience read-only reference to the root of the service tree + * containing this service. Most applications have only one root service, + * the application's [main service]{@link DataService.mainService}. + * + * @type {DataService} + */ + rootService: { + get: function () { + return this.parentService ? this.parentService.rootService : this; } }, - _objectDescriptorForType: { - value: function (type) { - var descriptor = this._constructorToObjectDescriptorMap.get(type) || - typeof type === "string" && this._moduleIdToObjectDescriptorMap[type]; - - return descriptor || type; + registerChildServices: { + value: function (childServices) { + var self; + if (!this.__childServiceRegistrationPromise) { + self = this; + this.__childServiceRegistrationPromise = Promise.all(childServices.map(function (child) { + return self.registerChildService(child); + })); + } } }, - _constructorToObjectDescriptorMap: { - value: new Map() - }, + /** + * Alternative to [addChildService()]{@link DataService#addChildService}. + * While addChildService is synchronous, registerChildService is asynchronous + * and may take a child whose [types]{@link DataService#types} property is + * a promise instead of an array. + * + * This is useful for example if the child service does not know its types + * immediately, e.g. if it must fetch them from a .mjson descriptors file. + * + * If the child's types is an array, it is guaranteed to behave exactly + * like addChildService. + * + * @method + * @param {DataService} child service to add to this service. + * @param {?Promise|ObjectDescriptor|Array} + * @return {Promise} + */ + registerChildService: { + value: function (child, types) { + var self = this, + mappings = child.mappings || []; + // possible types + // -- types is passed in as an array or a single type. + // -- a model is set on the child. + // -- types is set on the child. + // any type can be asychronous or synchronous. + types = types && Array.isArray(types) && types || + types && [types] || + child.model && child.model.objectDescriptors || + child.types && Array.isArray(child.types) && child.types || + child.types && [child.types] || + []; - _moduleIdToObjectDescriptorMap: { - value: {} + return child._childServiceRegistrationPromise.then(function () { + return self._registerChildServiceTypesAndMappings(child, types, mappings); + }); + } }, /** @@ -523,12 +612,13 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { // remove. for (i = 0, n = types && types.length || 1; i < n; i += 1) { type = types && types.length && types[i] || null; - chidren = this._childServicesByType.get(type); + type = type && this._objectDescriptorForType(type); + chidren = this._childServicesByObjectDescriptor.get(type); index = chidren ? chidren.indexOf(child) : -1; if (index >= 0 && chidren.length > 1) { chidren.splice(index, 1); } else if (index === 0) { - this._childServicesByType.delete(type); + this._childServicesByObjectDescriptor.delete(type); index = type ? this._childServiceTypes.indexOf(type) : -1; if (index >= 0) { this._childServiceTypes.splice(index, 1); @@ -569,355 +659,492 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, - /** - * A map from each of the data types handled by this service to an array - * of the child services that can handle that type, with each such array - * ordered according to the order in which the services in it were - * [added]{@link DataService#addChildService} as children of this service. - * - * If one or more child services of this service are defined as handling all - * types (their [types]{@link DataService#types} property is `undefined`, - * `null`, or an empty array), the child service map also include a `null` - * key whose corresponding value is an array of all those services defined - * to handle all types. - * - * The contents of this map should not be modified outside of - * [addChildService()]{@link DataService#addChildService} and - * [removeChildService()]{@link DataService#removeChildService}. - * - * @private - * @type {Map>} + /*************************************************************************** + * Mappings */ - _childServicesByType: { - get: function () { - if (!this.__childServicesByType) { - this.__childServicesByType = new Map(); + + /** + * Add Promise generated from mapRawDataToObject for the DataStream associated + * to this fetch. The resulting array of promises is used to determine when the + * DataStream is ready to be resolved with cooked objects. + * + * @method + * @argument {Promise} - promise + * @argument {DataStream} - stream + * @return {void} + */ + _addMappingPromiseForStream: { + value: function (promise, stream) { + if (!this._streamMappingPromises.has(stream)) { + this._streamMappingPromises.set(stream, [promise]); + } else { + this._streamMappingPromises.get(stream).push(promise); } - return this.__childServicesByType; } }, - __childServicesByType: { - value: undefined + /** + * Resolves the object descriptor and schema descriptor + * references for a mapping and adds that mapping to + * a child service with those descriptors + * + * @method + * @argument {DataMapping} - mapping + * @argument {DataService} - child + * @return {Promise} + */ + _addMappingToService: { + value: function (mapping, child) { + var service = this; + return Promise.all([ + mapping.objectDescriptorReference, + mapping.schemaDescriptorReference + ]).spread(function (objectDescriptor, schemaDescriptor) { + // TODO -- remove looking up by string to unique. + var type = [objectDescriptor.module.id, objectDescriptor.name].join("/"); + objectDescriptor = service._moduleIdToObjectDescriptorMap.get(type); + mapping.objectDescriptor = objectDescriptor; + mapping.schemaDescriptor = schemaDescriptor; + mapping.service = child; + child.addMappingForType(mapping, objectDescriptor); + return null; + }); + } }, - _childServicesByIdentifier: { + /** + * The DataMappings for this service and all of it's children + * + * @property {Array} + */ + _childServiceMappings: { get: function () { - if (!this.__childServicesByIdentifier) { - this.__childServicesByIdentifier = new Map(); + if (!this.__childServiceMappings) { + this.__childServiceMappings = []; } - return this.__childServicesByIdentifier; + return this.__childServiceMappings; } }, - __childServicesByIdentifier: { - value: undefined - }, /** - * An array of the data types handled by all child services of this service. - * - * The contents of this map should not be modified outside of - * [addChildService()]{@link DataService#addChildService} and - * [removeChildService()]{@link DataService#removeChildService}. + * Map from a parent class to the mappings used by the service to + * determine what subclass to create an instance of for a particular + * rawData object * - * @private - * @type {Array.} + * For example, say a class 'Person' has 2 subclasses 'Employee' & 'Customer'. + * RawDataService would evaluate each person rawData object against each item + * in _rawDataTypeMappings and determine if that rawData should be an instance + * of 'Employee' or 'Customer'. + * @type {Map} */ - _childServiceTypes: { - get: function() { - if (!this.__childServiceTypes) { - this.__childServiceTypes = []; - } - return this.__childServiceTypes; - } - }, - - __childServiceTypes: { + _descriptorToRawDataTypeMappings: { value: undefined }, /** - * Get the first child service that can handle the specified object, - * or `null` if no such child service exists. + * Map cooked object to rawData for use in write operations such as + * create, delete, save and update. + * + * @todo Make this method overridable by type name with methods like + * `mapHazardToRawData()` and `mapProductToRawData()`. * - * @private * @method - * @argument {Object} object - * @returns DataService - */ - _getChildServiceForObject: { - value: function (object) { - return this.childServiceForType(this.rootService._getObjectType(object)); - } - }, + * @argument {Object} object - object + * @argument {Object} record - record + * @argument {?} + * + */ + _mapObjectToRawData: { + value: function (object, record, context) { + var mapping = this.mappingForObject(object), + result; - /** - * Get the first child service that can handle data of the specified type, - * or `null` if no such child service exists. - * - * @private - * @method - * @argument {DataObjectDescriptor} type - * @returns {Set.} - */ - childServiceForType: { - value: function (type) { - var services; - type = type instanceof ObjectDescriptor ? type : this._objectDescriptorForType(type); - services = this._childServicesByType.get(type) || this._childServicesByType.get(null); - return services && services[0] || null; + if (mapping) { + result = mapping.mapObjectToRawData(object, record, context); + } + + if (record) { + if (result) { + var otherResult = this.mapObjectToRawData(object, record, context); + if (result instanceof Promise && otherResult instanceof Promise) { + result = Promise.all([result, otherResult]); + } else if (otherResult instanceof Promise) { + result = otherResult; + } + } else { + result = this.mapObjectToRawData(object, record, context); + } + } + + return result; } }, - /*************************************************************************** - * Mappings - */ - /** - * Adds a mapping to the service for the specified - * type. - * @param {DataMapping} mapping. The mapping to use. - * @param {ObjectDescriptor} type. The object type. - */ - addMappingForType: { - value: function (mapping, type) { - mapping.service = mapping.service || this; - this._mappingByType.set(type, mapping); + * Convert raw data to data objects of an appropriate type. + * + * Subclasses should override this method to map properties of the raw data + * to data objects, as in the following: + * + * mapRawDataToObject: { + * value: function (object, record) { + * object.firstName = record.GIVEN_NAME; + * object.lastName = record.FAMILY_NAME; + * } + * } + * + * Alternatively, subclasses can define a + * [mapping]{@link DataService#mapping} to do this mapping. + * + * The default implementation of this method uses the service's mapping if + * the service has one, and otherwise calls the deprecated + * [mapFromRawData()]{@link RawDataService#mapFromRawData}, whose default + * implementation does nothing. + * + * @todo Make this method overridable by type name with methods like + * `mapRawDataToHazard()` and `mapRawDataToProduct()`. + * + * @method + * @argument {Object} record - An object whose properties' values hold + * the raw data. + * @argument {Object} object - An object whose properties must be set or + * modified to represent the raw data. + * @argument {?} context - The value that was passed in to the + * [addRawData()]{@link RawDataService#addRawData} + * call that invoked this method. + */ + _mapRawDataToObject: { + value: function (record, object, context) { + var self = this, + mapping = this.mappingForObject(object, !!this.parentService), + result; + + + if (mapping) { + result = mapping.mapRawDataToObject(record, object, context); + if (result) { + result = result.then(function () { + return self.mapRawDataToObject(record, object, context); + }); + } else { + result = this.mapRawDataToObject(record, object, context); + } + } else { + result = this.mapRawDataToObject(record, object, context); + } + + return result; + } }, /** - * Return the mapping to use for the specified type. - * @param {ObjectDescriptor} type. - * @returns {DataMapping|null} returns the specified mapping or null - * if a mapping is not defined for the specified type. + * + * @method + * @param {Map} */ - mappingWithType: { - value: function (type) { - var mapping; - type = this._objectDescriptorForType(type); - mapping = this._mappingByType.has(type) && this._mappingByType.get(type); - return mapping || null; + _objectDescriptorToMappingMap: { + get: function () { + if (!this.__objectDescriptorToMappingMap) { + this.__objectDescriptorToMappingMap = new Map(); + } + return this.__objectDescriptorToMappingMap; } }, - - _mappingByType: { - get: function () { - if (!this.__mappingByType) { - this.__mappingByType = new Map(); + /** + * Adds each mapping passed in to _descriptorToRawDataTypeMappings + * + * @method + * @argument {Array} mappings + */ + _registerRawDataTypeMappings: { + value: function (mappings) { + var mapping, parentType, + i, n; + + for (i = 0, n = mappings ? mappings.length : 0; i < n; i++) { + mapping = mappings[i]; + parentType = mapping.type.parent; + if (!this._descriptorToRawDataTypeMappings.has(parentType)) { + this._descriptorToRawDataTypeMappings.set(parentType, []); + } + this._descriptorToRawDataTypeMappings.get(parentType).push(mapping); } - return this.__mappingByType; } }, - __mappingByType: { - value: undefined + _streamMapDataPromises: { + get: deprecate.deprecateMethod(void 0, function () { + return this._streamMappingPromises; + }, "_streamMapDataPromises", "_streamMappingPromises") }, - _childServiceMappings: { + + /** + * Map of DataStreams to Array of Promises which will + * be resolved when the raw-data-to-object mapping is + * complete + * + * @property {Map>} + */ + _streamMappingPromises: { get: function () { - if (!this.__childServiceMappings) { - this.__childServiceMappings = []; + if (!this.__streamMappingPromises) { + this.__streamMappingPromises = new Map(); } - return this.__childServiceMappings; + return this.__streamMappingPromises; } }, - __childServiceMappings: { - value: undefined + /** + * Adds a mapping to the service for the specified + * type. + * @param {DataMapping} mapping. The mapping to use. + * @param {ObjectDescriptor} type. The object type. + */ + addMappingForType: { + value: function (mapping, type) { + var descriptor = this._objectDescriptorForType(type); + mapping.service = mapping.service || this; + this._objectDescriptorToMappingMap.set(descriptor, mapping); + } }, - /*************************************************************************** - * Models - */ /** - * The [model]{@link ObjectModel} that this service supports. If the model is - * defined the service supports all the object descriptors contained within the model. + * Indicates whether this service implements mapObjectToRawData. Allows + * [saveDataObject()]{@link DataService#saveDataObject} to determine whether this file + * is responsible for data mapping or not. + * + * + * @property {Boolean} */ - model: { - value: undefined + implementsMapObjectToRawData: { + get: function () { + return exports.DataService.prototype.mapObjectToRawData !== this.mapObjectToRawData; + } }, /** - * The maximum amount of time a DataService's data will be considered fresh. - * ObjectDescriptor's maxAge should take precedence over this and a DataStream's dataMaxAge should - * take precedence over a DataService's dataMaxAge global default value. + * Indicates whether this service implements mapRawDataToObject. Allows + * [fetchData()]{@link DataService#fetchData} to determine whether this file + * is responsible for data mapping or not. * - * @type {Number} + * @property {Boolean} */ - dataMaxAge: { - value: undefined + implementsMapRawDataToObject: { + get: function () { + return exports.DataService.prototype.mapRawDataToObject !== this.mapRawDataToObject; + } }, - /*************************************************************************** - * Authorization + /** + * Retrieve DataMapping for this object. + * + * @method + * @argument {Object} object - An object whose object descriptor has a DataMapping */ + mappingForObject: { + value: function (object, canReturnNull) { + var objectDescriptor = this.objectDescriptorForObject(object), + mapping = objectDescriptor && this.mappingWithType(objectDescriptor); - _initializeAuthorization: { - value: function () { - if (this.providesAuthorization) { - exports.DataService.authorizationManager.registerAuthorizationService(this); - } - if (this.authorizationPolicy === AuthorizationPolicyType.UpfrontAuthorizationPolicy) { - var self = this; - exports.DataService.authorizationManager.authorizeService(this).then(function(authorization) { - self.authorization = authorization; - return authorization; - }).catch(function(error) { - console.log(error); - }); - } else { - //Service doesn't need anything upfront, so we just go through - this.authorizationPromise = Promise.resolve(); + if (!mapping && objectDescriptor && !canReturnNull) { + mapping = this._objectDescriptorToMappingMap.get(objectDescriptor); + if (!mapping) { + mapping = DataMapping.withObjectDescriptor(objectDescriptor); + this._objectDescriptorToMappingMap.set(objectDescriptor, mapping); + } } + + return mapping; } }, /** - * Returns the AuthorizationPolicyType used by this DataService. - * - * @type {AuthorizationPolicyType} + * Return the mapping to use for the specified type. + * @param {ObjectDescriptor} type. + * @returns {DataMapping|null} returns the specified mapping or null + * if a mapping is not defined for the specified type. */ - authorizationPolicy: { - value: AuthorizationPolicyType.NoAuthorizationPolicy + mappingWithType: { + value: function (type) { + var descriptor = this._objectDescriptorForType(type), + service = this.rootService.childServiceForType(descriptor), + mapping; + if (service === this) { + mapping = this._objectDescriptorToMappingMap.has(type) && this._objectDescriptorToMappingMap.get(type); + } else if (service) { + mapping = service.mappingWithType(type); + } + return mapping || null; + } }, /** - * holds authorization object after a successfull authorization + * Public method invoked by the framework during the conversion from + * an object to a raw data. + * Designed to be overriden by concrete RawDataServices to allow fine-graine control + * when needed, beyond transformations offered by an ObjectDescriptorDataMapping or + * an ExpressionDataMapping * - * @type {Object} + * @method + * @argument {Object} object - An object whose properties must be set or + * modified to represent the raw data. + * @argument {Object} record - An object whose properties' values hold + * the raw data. + * @argument {?} context - The value that was passed in to the + * [addRawData()]{@link RawDataService#addRawData} + * call that invoked this method. */ - - authorization: { - value: undefined + mapObjectToRawData: { + value: function (object, record, context) { + //Overridden in child classes + } }, - authorizationPromise: { - value: Promise.resolve() + mapToRawData: { + value: deprecate.deprecateMethod(undefined, function (rawData, object, context) { + //Overridden in child classes + }, "mapToRawData", "mapObjectToRawData", true) }, /** - * Returns the list of moduleIds of DataServices a service accepts to provide - * authorization on its behalf. If an array has multiple - * authorizationServices, the final choice will be up to the App user - * regarding which one to use. This array is expected to return moduleIds, - * not objects, allowing the AuthorizationManager to manage unicity + * Public method invoked by the framework during the conversion from + * raw data to an object + * Designed to be overridden by concrete DataService to allow fine-graine control + * when needed, beyond transformations offered by an ObjectDescriptorDataMapping or + * an ExpressionDataMapping * - * @type {string[]} + * @method + * @argument {Object} object - An object whose properties must be set or + * modified to represent the raw data. + * @argument {Object} record - An object whose properties' values hold + * the raw data. + * @argument {?} context - The value that was passed in to the + * [addRawData()]{@link RawDataService#addRawData} + * call that invoked this method. */ - authorizationServices: { - value: null + mapRawDataToObject: { + value: function (rawData, object, context) { + //Overridden in child classes + } }, - /** - * @type {string} - * @description Module ID of the panel component used to gather necessary authorization information - */ - authorizationPanel: { - value: undefined + mapFromRawData: { + value: deprecate.deprecateMethod(undefined, function (rawData, object, context) { + //Overridden in child classes + }, "mapFromRawData", "mapRawDataToObject", true) }, + /*************************************************************************** + * Model / Types / Object Descriptors + */ + /** - * Indicates whether a service can provide user-level authorization to its - * data. Defaults to false. Concrete services need to override this as - * needed. - * - * @type {boolean} + * Map from constructor to ObjectDescriptor. + * Used to allow fetches with the constructor. e.g. + * + * var Foo = require("logic/model/foo").Foo; + * mainService.fetchData(Foo); + * + * @private + * @property > */ - providesAuthorization: { - value: false + _constructorToObjectDescriptorMap: { + value: new Map() }, /** - * Performs whatever tasks are necessary to authorize - * this service and returns a Promise that resolves with - * an Authorization object. + * Map from moduleID to ObjectDescriptor. * - * @method - * @returns Promise + * @private + * @property > */ - authorize: { - value: undefined + _moduleIdToObjectDescriptorMap: { + value: new Map() }, - /** + * Map from data object to ObjectDescriptor. * - * @method - * @returns Promise + * @private + * @property > */ - logOut: { - value: function () { - console.warn("DataService.logOut() must be overridden by the implementing service"); - return this.nullPromise; - } + _objectToObjectDescriptorMap: { + value: new WeakMap() }, - /*************************************************************************** - * Data Object Types - */ - /** - * Returns an object descriptor for the provided object. If this service - * does not have an object descriptor for this object it will ask its - * parent for one. + * Returns an object descriptor for the provided object by getting + * the object's moduleId and inquiring with this._moduleIdToObjectDescriptorMap + * & this.types array. * @param {object} * @returns {ObjectDescriptor|null} if an object descriptor is not found this * method will return null. */ - objectDescriptorForObject: { + _objectDescriptorForObject: { value: function (object) { var types = this.types, objectInfo = Montage.getInfoForObject(object), moduleId = objectInfo.moduleId, objectName = objectInfo.objectName, - module, exportName, objectDescriptor, i, n; + module, exportName, i, n, objectDescriptor; + + if (object.constructor.TYPE instanceof DataObjectDescriptor) { + objectDescriptor = object.constructor.TYPE; + } else { + objectDescriptor = this._moduleIdToObjectDescriptorMap.get(moduleId); + } + for (i = 0, n = types.length; i < n && !objectDescriptor; i += 1) { module = types[i].module; exportName = module && types[i].exportName; + objectDescriptor =this._moduleIdToObjectDescriptorMap.get(moduleId); if (module && moduleId === module.id && objectName === exportName) { objectDescriptor = types[i]; } } - return objectDescriptor || this.parentService && this.parentService.objectDescriptorForObject(object); + + return objectDescriptor; } }, /** - * Get the type of the specified data object. + * Returns an object descriptor for a type where type is + * a module id, constructor, or an object descriptor. * - * @private - * @method - * @argument {Object} object - The object whose type is sought. - * @returns {DataObjectDescriptor} - The type of the object, or undefined if - * no type can be determined. + * @param {Function|String|ObjectDescriptor} type + * @returns {ObjectDescriptor|null} if an object descriptor is not found this + * method will return null. */ - _getObjectType: { - value: function (object) { - var type = this._typeRegistry.get(object), - moduleId = typeof object === "string" ? object : this._getModuleIdForObject(object); - while (!type && object) { - if (object.constructor.TYPE instanceof DataObjectDescriptor) { - type = object.constructor.TYPE; - } else if (this._moduleIdToObjectDescriptorMap[moduleId]) { - type = this._moduleIdToObjectDescriptorMap[moduleId]; - } else { - object = Object.getPrototypeOf(object); - } - } - return type; + _objectDescriptorForType: { + value: function (type) { + return this._constructorToObjectDescriptorMap.get(type) || + typeof type === "string" && this._moduleIdToObjectDescriptorMap.get(type) || + type; } }, - _getModuleIdForObject: { - value: function (object) { - var info = Montage.getInfoForObject(object); - return [info.moduleId, info.objectName].join("/"); + /** + * Adds DataMappings to a child DataService + * + * @param {DataService} child + * @param {Array} mappings + * @returns {Promise} + */ + _registerChildServiceMappings: { + value: function (child, mappings) { + var self = this; + return Promise.all(mappings.map(function (mapping) { + return Promise.all([ + self._addMappingToService(mapping, child) + ]); + })); } }, @@ -927,89 +1154,203 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { * @private * @method * @argument {Object} object - * @argument {DataObjectDescriptor} type + * @argument {ObjectDescriptor} objectDescriptor */ - _setObjectType: { - value: function (object, type) { - if (this._getObjectType(object) !== type){ - this._typeRegistry.set(object, type); + _registerObjectWithObjectDescriptor: { + value: function (object, objectDescriptor) { + if (this._objectToObjectDescriptorMap.get(object) !== objectDescriptor){ + this._objectToObjectDescriptorMap.set(object, objectDescriptor); } } }, - _typeRegistry: { - get: function () { - if (!this.__typeRegistry){ - this.__typeRegistry = new WeakMap(); - } - return this.__typeRegistry; + /** + * Register ObjectDescriptors by module ID + * + * @param {Array} mappings + * @returns {Promise} + */ + _registerObjectDescriptorsByModuleId: { + value: function (types) { + var map = this._moduleIdToObjectDescriptorMap; + types.forEach(function (objectDescriptor) { + var module = objectDescriptor.module, + moduleId = [module.id, objectDescriptor.exportName].join("/"); + map.set(moduleId, objectDescriptor); + }); } }, - /*************************************************************************** - * Data Object Triggers - */ + _registerTypesByModuleId: { + value: deprecate.deprecateMethod(void 0, function (types) { + return this._registerObjectDescriptorsByModuleId(types); + }, "_registerTypesByModuleId", "_registerObjectDescriptorsByModuleId", true) + }, - /** - * Returns a prototype for objects of the specified type. The returned - * prototype will have a [data trigger]{@link DataTrigger} defined for each - * lazy relationships and properties of that type. A single prototype will - * be created for all objects of a given type. + /** + * Resolve mixed array of Promises and ObjectDescriptors down to + * a single Promise that returns array of ObjectDescriptors * - * @private + * @param {Array} mappings + * @returns {Promise} + */ + _resolveAsynchronousTypes: { + value: function (types) { + var self = this; + return Promise.all(this._flattenArray(types).map(function (type) { + return type instanceof Promise ? type : Promise.resolve(type); + })).then(function (descriptors) { + return self._flattenArray(descriptors); + }); + } + }, + + /** + * The maximum amount of time a DataService's data will be considered fresh. + * ObjectDescriptor's maxAge should take precedence over this and a DataStream's dataMaxAge should + * take precedence over a DataService's dataMaxAge global default value. + * + * @type {Number} + */ + dataMaxAge: { + value: undefined + }, + + /** + * The [model]{@link ObjectModel} that this service supports. If the model is + * defined the service supports all the object descriptors contained within the model. + */ + model: { + value: undefined + }, + + + /** + * Returns an object descriptor for the provided object. If this service + * does not have an object descriptor for this object it will ask its + * parent for one. + * @param {object} + * @returns {ObjectDescriptor|null} if an object descriptor is not found this + * method will return null. + */ + objectDescriptorForObject: { + value: function (object) { + return this._objectToObjectDescriptorMap.get(object) || + this._objectDescriptorForObject(object) || + this.parentService && this.parentService.objectDescriptorForObject(object); + } + }, + + + /*************************************************************************** + * Data Triggers + */ + + + _prototypesForModuleObjectDescriptors: { + value: function (objectDescriptors) { + var self = this; + return Promise.all(objectDescriptors.map(function (objectDescriptor) { + return self._prototypeForModuleObjectDescriptor(objectDescriptor); + })); + } + }, + + /** + * Builds a prototype that includes DataTriggers on the properties defined + * on the ObjectDescriptor and registers it with the DataService. + * + * + * @method + * @argument {DataService} childService - The childService that controls the mapping + * for this objectDescriptor. + * @argument {ObjectDescriptor} objectDescriptor - ObjectDescriptor with the propertyDescriptors for which + * DataTriggers will be added on the prototype + * @returns {Promise} + */ + _prototypeForModuleObjectDescriptor: { + value: function (objectDescriptor) { + var self = this, + module = objectDescriptor.module; + + return module.require.async(module.id).then(function (exports) { + var constructor = exports[objectDescriptor.exportName], + prototype = self._prototypeForType(objectDescriptor, constructor); + return null; + }); + } + }, + + /** + * Map of object descriptors to their prototypes + * + * @type {Map} + */ + _objectPrototypes: { + get: function () { + if (!this.__objectPrototypes){ + this.__objectPrototypes = new Map(); + } + return this.__objectPrototypes; + } + }, + + /** + * Map of object descriptors to their prototypes + * + * @type {Map} + */ + _objectTriggers: { + get: function () { + if (!this.__objectTriggers){ + this.__objectTriggers = new Map(); + } + return this.__objectTriggers; + } + }, + + /** + * Returns a prototype for objects of the specified type. The returned + * prototype will have a [data trigger]{@link DataTrigger} defined for each + * lazy relationships and properties of that type. A single prototype will + * be created for all objects of a given type. + * + * @private * @method * @argument {DataObjectDescriptor|ObjectDescriptor} type * @returns {Object} */ - _getPrototypeForType: { - value: function (type) { - var info, triggers, prototype; - type = this._objectDescriptorForType(type); - prototype = this._dataObjectPrototypes.get(type); - if (type && !prototype) { - prototype = Object.create(type.objectPrototype || Montage.prototype); - this._dataObjectPrototypes.set(type, prototype); - if (type instanceof ObjectDescriptor || type instanceof DataObjectDescriptor) { - triggers = DataTrigger.addTriggers(this, type, prototype); + _prototypeForType: { + value: function (type, constructor) { + var descriptor = this._objectDescriptorForType(type), + prototype = this._objectPrototypes.get(descriptor), + info, mapping, triggers, requisites; + + if (descriptor && !prototype) { + prototype = constructor ? constructor.prototype : + type.objectPrototype ? type.objectPrototype : + Montage.prototype; + prototype = Object.create(prototype); + if (this._isObjectDescriptor(descriptor)) { + mapping = this.mappingWithType(descriptor); + requisites = mapping ? mapping.requisitePropertyNames : new Set(); + triggers = DataTrigger.addTriggers(this, descriptor, prototype, requisites); } else { - info = Montage.getInfoForObject(type.prototype); + info = Montage.getInfoForObject(descriptor.prototype); console.warn("Data Triggers cannot be created for this type. (" + (info && info.objectName) + ") is not an ObjectDescriptor"); triggers = []; } - this._dataObjectTriggers.set(type, triggers); - //We add a property that returns an object's snapshot - //We add a property that returns an object's primaryKey - //Let's postponed this for now and revisit when we need - //add more properties/logic to automatically track changes - //on objects - - // Object.defineProperties(prototype, { - // "montageDataSnapshot": { - // get: this.__object__snapshotMethodImplementation - // }, - // "montageDataPrimaryKey": { - // get: this.__object_primaryKeyMethodImplementation - // } - // }); - + if (constructor) { + this._objectPrototypes.set(constructor, prototype); + this._constructorToObjectDescriptorMap.set(constructor, descriptor); + } + this._objectPrototypes.set(descriptor, prototype); + this._objectTriggers.set(descriptor, triggers); } return prototype; } }, - // __object__snapshotMethodImplementation: { - // value: function() { - // debugger; - // return exports.DataService.mainService._getChildServiceForObject(this).snapshotForObject(this); - // } - // }, - // __object_primaryKeyMethodImplementation: { - // value: function() { - // debugger; - // return exports.DataService.mainService.dataIdentifierForObject(this).primaryKey; - // } - // }, - /** * Returns the [data triggers]{@link DataTrigger} set up for objects of the * specified type. @@ -1019,42 +1360,168 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { * @argument {Object} object * @returns {Object} */ - _getTriggersForObject: { + _triggersForObject: { value: function (object) { - var type = this._getObjectType(object); - return type && this._dataObjectTriggers.get(type); + var type = this.objectDescriptorForObject(object); + return type && this._objectTriggers.get(type); } }, - _dataObjectPrototypes: { - get: function () { - if (!this.__dataObjectPrototypes){ - this.__dataObjectPrototypes = new Map(); - } - return this.__dataObjectPrototypes; + /*************************************************************************** + * Data Object Properties + */ + + /** + * Returns the delegate method for a given property. + * E.g. + * propertyName: features + * returns: fetchFeaturesProperty() + * + * propertyName: location + * returns: fetchLocationProperty() + * + * @private + * @method + * @argument {String} propertyName + * @returns {Function} + */ + _delegateFunctionForPropertyName: { + value: function (propertyName) { + var capitalized = propertyName.charAt(0).toUpperCase() + propertyName.slice(1), + functionName = "fetch" + capitalized + "Property"; + return typeof this[functionName] === "function" && this[functionName]; } }, + - __dataObjectPrototypes: { - value: undefined - }, + /** + * Fetch a property on an object using an objectDescriptor mapping and a propertyDescriptor + * + * @private + * @method + * @argument {Object} object + * @argument {String} propertyName + * @argument {PropertyDescriptor} propertyDescriptor + * @returns {Promise} + */ + _fetchObjectPropertyWithPropertyDescriptor: { + value: function (object, propertyName, propertyDescriptor) { + var self = this, + objectDescriptor = propertyDescriptor.owner, + mapping = objectDescriptor && this.mappingWithType(objectDescriptor), + data = {}, + result; - _dataObjectTriggers: { - get: function () { - if (!this.__dataObjectTriggers){ - this.__dataObjectTriggers = new Map(); + if (mapping) { + Object.assign(data, this.snapshotForObject(object)); + if (typeof mapping.mapObjectToCriteriaSourceForProperty !== "function") { + result = mapping.mapRawDataToObjectProperty(data, object, propertyName); + if (!result || typeof result.then !== "function") { + result = Promise.resolve(result); + } + } else { + result = mapping.mapObjectToCriteriaSourceForProperty(object, data, propertyName).then(function() { + Object.assign(data, self.snapshotForObject(object)); + return mapping.mapRawDataToObjectProperty(data, object, propertyName); + }); + } + } else { + result = this.nullPromise; } - return this.__dataObjectTriggers; + + return result; } }, - __dataObjectTriggers: { - value: undefined + /** + * Retrieve the DataTriggers for object properties and get the property values + * softly or forcefully depending on whether this is an update + * + * @private + * @method + * @argument {Object} object - the object whose properties will be fetched + * @argument {Array} names - the property names to fetch + * @argument {Integer} start - index of the names array at which to start looping + * @argument {Boolean} isUpdate - whether or not the properties should be forcefully retrieved + * @returns {Array} + */ + _getOrUpdateObjectProperties: { + value: function (object, names, start, isUpdate) { + var triggers, trigger, promises, promise, i, n; + // Request each data value separately, collecting unique resulting + // promises into an array and a set, but avoid creating any array + // or set unless that's necessary. + triggers = this._triggersForObject(object); + for (i = start, n = names.length; i < n; i += 1) { + trigger = triggers && triggers[names[i]]; + promise = !trigger ? this.nullPromise : + isUpdate ? trigger.updateObjectProperty(object) : + trigger.getObjectProperty(object); + if (promise !== this.nullPromise) { + if (!promises) { + promises = {array: [promise]}; + } else if (!promises.set && promises.array[0] !== promise) { + promises.set = new Set(); + promises.set.add(promises.array[0]); + promises.set.add(promise); + promises.array.push(promise); + } else if (promises.set && !promises.set.has(promise)) { + promises.set.add(promise); + promises.array.push(promise); + } + } + } + + // Return a promise that will be fulfilled only when all of the + // requested data has been set on the object. If possible do this + // without creating any additional promises. + return !promises ? this.nullPromise : + !promises.set ? promises.array[0] : + Promise.all(promises.array).then(this.nullFunction); + } }, + - /*************************************************************************** - * Data Object Properties + /** + * Recursively resolve all properties along a path from a given starting object. + * E.g. to get a property named user.employer.location.city, + * + * object = user + * propertiesToRequest = ["employer", "location", "city"] + * + * + * @private + * @method + * @argument {Object} object - the object whose properties will be fetched + * @argument {Array} propertiesToRequest - the property names to fetch + * @returns {Promise} */ + _getPropertiesOnPath: { + value: function (object, propertiesToRequest) { + var self = this, + propertyName = propertiesToRequest.shift(), + promise = this.getObjectProperties(object, propertyName); + + if (promise) { + return promise.then(function () { + var result = null; + if (propertiesToRequest.length && object[propertyName]) { + result = self._getPropertiesOnPath(object[propertyName], propertiesToRequest); + } + return result; + }); + } else { + return this.nullPromise; + } + } + }, + + _propertyDescriptorForObjectAndName: { + value: function (object, propertyName) { + var objectDescriptor = this.objectDescriptorForObject(object); + return objectDescriptor && objectDescriptor.propertyDescriptorForName(propertyName); + } + }, /** * Since root services are responsible for triggering data objects fetches, @@ -1070,7 +1537,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { if (this.isRootService) { var names = Array.isArray(propertyNames) ? propertyNames : arguments, start = names === propertyNames ? 0 : 1, - triggers = this._getTriggersForObject(object), + triggers = this._triggersForObject(object), trigger, i, n; for (i = start, n = names.length; i < n; i += 1) { trigger = triggers && triggers[names[i]]; @@ -1086,6 +1553,57 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, + /** + * Fetch the value of a data object's property, possibly asynchronously. + * + * The default implementation of this method delegates the fetching to a + * child services, or does nothing but return a fulfilled promise for `null` + * if no child service can be found to handle the specified object. + * + * [Data service]{@link DataService} subclasses should override + * this method to perform any fetch or other operation required to get the + * requested data. The subclass implementations of this method should use + * only [fetchData()]{@link DataService#fetchData} calls to fetch data. + * + * This method should never be called directly: + * [getObjectProperties()]{@link DataService#getObjectProperties} or + * [updateObjectProperties()]{@link DataService#updateObjectProperties} + * should be called instead as those methods handles some required caching, + * fetch aggregation, and [data trigger]{@link DataTrigger}. Those methods + * will call this method if and when that is necessary. + * + * Like the promise returned by + * [getObjectProperties()]{@link DataService#getObjectProperties}, the + * promise returned by this method should not pass the requested value to + * its callback: That value must instead be set on the object passed in to + * this method. + * + * @method + * @argument {object} object - The object whose property value is being + * requested. + * @argument {string} name - The name of the single property whose value + * is being requested. + * @returns {external:Promise} - A promise fulfilled when the requested + * value has been received and set on the specified property of the passed + * in object. + */ + fetchObjectProperty: { + value: function (object, propertyName) { + var isHandler = this.parentService && this.parentService._childServiceForObject(object) === this, + useDelegate = isHandler && typeof this.fetchRawObjectProperty === "function", + delegateFunction = !useDelegate && isHandler && this._delegateFunctionForPropertyName(propertyName), + propertyDescriptor = !useDelegate && !delegateFunction && isHandler && this._propertyDescriptorForObjectAndName(object, propertyName), + childService = !isHandler && this._childServiceForObject(object); + + var result = useDelegate ? this.fetchRawObjectProperty(object, propertyName) : + delegateFunction ? delegateFunction.call(this, object) : + isHandler && propertyDescriptor ? this._fetchObjectPropertyWithPropertyDescriptor(object, propertyName, propertyDescriptor) : + childService ? childService.fetchObjectProperty(object, propertyName) : + this.nullPromise; + return result; + } + }, + /** * Request possibly asynchronous values of a data object's properties. These * values will only be fetched if necessary and only the first time they are @@ -1142,56 +1660,51 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, - getObjectPropertyExpressions: { - value: function (object, propertyValueExpressions) { - if (this.isRootService) { - // Get the data, accepting property names as an array or as a list - // of string arguments while avoiding the creation of any new array. - var expressions = Array.isArray(propertyValueExpressions) ? propertyValueExpressions : arguments, - start = expressions === propertyValueExpressions ? 0 : 1, - promises = [], - self = this; - + /** + * Request possibly asynchronous values of a data object's properties defined as + * as frb expressions. + * + * myService.getObjectPropertyExpressions(myObject, "x.a", "y.b").then(function () { + * someFunction(myObject.x.a, myObject.y.b); + * } + * + * @method + * @argument {object} object - The object whose property values are + * being requested. + * @argument {string[]} propertyValueExpressions - The expressions defining the properties + * whose values are being requested. + * These can be provided as an array of + * strings or as a list of string + * arguments following the object + * argument. + * @returns {external:Promise} - A promise fulfilled when all of the + * requested data has been received and set on the specified properties of + * the passed in object. + */ + getObjectPropertyExpressions: { + value: function (object, propertyValueExpressions) { + if (this.isRootService) { + // Get the data, accepting property names as an array or as a list + // of string arguments while avoiding the creation of any new array. + var expressions = Array.isArray(propertyValueExpressions) ? propertyValueExpressions : arguments, + start = expressions === propertyValueExpressions ? 0 : 1, + promises = [], + self = this; + expressions.forEach(function (expression) { var split = expression.split("."); - // if (split.length == 1) { - // promises.push(self.getObjectProperties(object, split[0])); - // } else { promises.push(self._getPropertiesOnPath(object, split)); - // } - }); return Promise.all(promises); - - } else { return this.rootService.getObjectPropertyExpressions(object, propertyValueExpressions); } } }, - _getPropertiesOnPath: { - value: function (object, propertiesToRequest) { - var self = this, - propertyName = propertiesToRequest.shift(), - promise = this.getObjectProperties(object, propertyName); - - if (promise) { - return promise.then(function () { - var result = null; - if (propertiesToRequest.length && object[propertyName]) { - result = self._getPropertiesOnPath(object[propertyName], propertiesToRequest); - } - return result; - }); - } else { - return this.nullPromise; - } - } - }, /** * Request possibly asynchronous values of a data object's properties, @@ -1238,144 +1751,142 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, - /** - * Fetch the value of a data object's property, possibly asynchronously. - * - * The default implementation of this method delegates the fetching to a - * child services, or does nothing but return a fulfilled promise for `null` - * if no child service can be found to handle the specified object. - * - * [Raw data service]{@link RawDataService} subclasses should override - * this method to perform any fetch or other operation required to get the - * requested data. The subclass implementations of this method should use - * only [fetchData()]{@link DataService#fetchData} calls to fetch data. - * - * This method should never be called directly: - * [getObjectProperties()]{@link DataService#getObjectProperties} or - * [updateObjectProperties()]{@link DataService#updateObjectProperties} - * should be called instead as those methods handles some required caching, - * fetch aggregation, and [data trigger]{@link DataTrigger}. Those methods - * will call this method if and when that is necessary. - * - * Like the promise returned by - * [getObjectProperties()]{@link DataService#getObjectProperties}, the - * promise returned by this method should not pass the requested value to - * its callback: That value must instead be set on the object passed in to - * this method. + + /*************************************************************************** + * Data Object Creation / Changes + */ + + /** + * Create a data object without registering it in the new object map. * + * @private * @method - * @argument {object} object - The object whose property value is being - * requested. - * @argument {string} name - The name of the single property whose value - * is being requested. - * @returns {external:Promise} - A promise fulfilled when the requested - * value has been received and set on the specified property of the passed - * in object. + * @argument {ObjectDescriptor} type - The type of object to create. + * @returns {Object} - The created object. */ - fetchObjectProperty: { - value: function (object, propertyName) { - var isHandler = this.parentService && this.parentService._getChildServiceForObject(object) === this, - useDelegate = isHandler && typeof this.fetchRawObjectProperty === "function", - delegateFunction = !useDelegate && isHandler && this._delegateFunctionForPropertyName(propertyName), - propertyDescriptor = !useDelegate && !delegateFunction && isHandler && this._propertyDescriptorForObjectAndName(object, propertyName), - childService = !isHandler && this._getChildServiceForObject(object); - - return useDelegate ? this.fetchRawObjectProperty(object, propertyName) : - delegateFunction ? delegateFunction.call(this, object) : - isHandler && propertyDescriptor ? this._fetchObjectPropertyWithPropertyDescriptor(object, propertyName, propertyDescriptor) : - childService ? childService.fetchObjectProperty(object, propertyName) : - this.nullPromise; + _createDataObject: { + value: function (type, dataIdentifier) { + var objectDescriptor = this._objectDescriptorForType(type), + object = Object.create(this._prototypeForType(objectDescriptor)); + if (object) { + //This needs to be done before a user-land code can attempt to do + //anyting inside its constructor, like creating a binding on a relationships + //causing a trigger to fire, not knowing about the match between identifier + //and object... If that's feels like a real situation, it is. + this.registerUniqueObjectWithDataIdentifier(object, dataIdentifier); + object = object.constructor.call(object) || object; + if (object) { + this._registerObjectWithObjectDescriptor(object, objectDescriptor); + } + } + return object; } }, - _delegateFunctionForPropertyName: { - value: function (propertyName) { - var capitalized = propertyName.charAt(0).toUpperCase() + propertyName.slice(1), - functionName = "fetch" + capitalized + "Property"; - return typeof this[functionName] === "function" && this[functionName]; - } + _dataIdentifierByObject: { + // This property is shared with all child services. + // If created lazily the wrong data identifier will be returned when + // accessed by a child service. + value: new WeakMap() }, - _isAsync: { - value: function (object) { - return object && object.then && typeof object.then === "function"; + _objectByDataIdentifier: { + get: function() { + return this.__objectByDataIdentifier || (this.__objectByDataIdentifier = new WeakMap()); } }, - _fetchObjectPropertyWithPropertyDescriptor: { - value: function (object, propertyName, propertyDescriptor) { - var self = this, - objectDescriptor = propertyDescriptor.owner, - mapping = objectDescriptor && this.mappingWithType(objectDescriptor), - data = {}, - result; - + /** + * A set of the data objects managed by this service or any other descendent + * of this service's [root service]{@link DataService#rootService} that have + * been changed since that root service's data was last saved, or since the + * root service was created if that service's data hasn't been saved yet + * + * Since root services are responsible for tracking data objects, subclasses + * whose instances will not be root services should override this property + * to return their root service's value for it. + * + * @type {Set.} + */ + changedDataObjects: { + get: function () { + if (this.isRootService) { + this._changedDataObjects = this._changedDataObjects || new Set(); + return this._changedDataObjects; + } + else { + return this.rootService.changedDataObjects; + } + } + }, - if (mapping) { - Object.assign(data, this.snapshotForObject(object)); - result = mapping.mapObjectToCriteriaSourceForProperty(object, data, propertyName); - if (this._isAsync(result)) { - return result.then(function() { - Object.assign(data, self.snapshotForObject(object)); - return mapping.mapRawDataToObjectProperty(data, object, propertyName); - }); - } else { - Object.assign(data, self.snapshotForObject(object)); - result = mapping.mapRawDataToObjectProperty(data, object, propertyName); - if (!this._isAsync(result)) { - result = this.nullPromise; - } - return result; - } + /** + * Create a new data object of the specified type. + * + * Since root services are responsible for tracking and creating data + * objects, subclasses whose instances will not be root services should + * override this method to call their root service's implementation of it. + * + * @method + * @argument {ObjectDescriptor} type - The type of object to create. + * @returns {Object} - The created object. + */ + //TODO add the creation of a temporary identifier to pass to _createDataObject + createDataObject: { + value: function (type) { + if (this.isRootService) { + var object = this._createDataObject(type); + this.createdDataObjects.add(object); + return object; } else { - return this.nullPromise; + this.rootService.createDataObject(type); } } }, /** - * @private - * @method + * A set of the data objects created by this service or any other descendent + * of this service's [root service]{@link DataService#rootService} since + * that root service's data was last saved, or since the root service was + * created if that service's data hasn't been saved yet. + * + * Since root services are responsible for tracking data objects, subclasses + * whose instances will not be root services should override this property + * to return their root service's value for it. + * + * @type {Set.} */ - _getOrUpdateObjectProperties: { - value: function (object, names, start, isUpdate) { - var triggers, trigger, promises, promise, i, n; - // Request each data value separately, collecting unique resulting - // promises into an array and a set, but avoid creating any array - // or set unless that's necessary. - triggers = this._getTriggersForObject(object); - for (i = start, n = names.length; i < n; i += 1) { - trigger = triggers && triggers[names[i]]; - promise = !trigger ? this.nullPromise : - isUpdate ? trigger.updateObjectProperty(object) : - trigger.getObjectProperty(object); - if (promise !== this.nullPromise) { - if (!promises) { - promises = {array: [promise]}; - } else if (!promises.set && promises.array[0] !== promise) { - promises.set = new Set(); - promises.set.add(promises.array[0]); - promises.set.add(promise); - promises.array.push(promise); - } else if (promises.set && !promises.set.has(promise)) { - promises.set.add(promise); - promises.array.push(promise); - } + createdDataObjects: { + get: function () { + if (this.isRootService) { + if (!this._createdDataObjects) { + this._createdDataObjects = new Set(); } + return this._createdDataObjects; + } + else { + return this.rootService.createdDataObjects; } - - // Return a promise that will be fulfilled only when all of the - // requested data has been set on the object. If possible do this - // without creating any additional promises. - return !promises ? this.nullPromise : - !promises.set ? promises.array[0] : - Promise.all(promises.array).then(this.nullFunction); } }, - /*************************************************************************** - * Data Object Creation + + /** + * Returns a unique object for a DataIdentifier + * [fetchObjectProperty()]{@link DataService#fetchObjectProperty} instead + * of this method. That method will be called by this method when needed. + * + * @method + * @argument {object} object - The object whose property values are + * being requested. + * + * @returns {DataIdentifier} - An object's DataIdentifier */ + dataIdentifierForObject: { + value: function (object) { + return this._dataIdentifierByObject.get(object); + } + }, /** * Find an existing data object corresponding to the specified raw data, or @@ -1386,15 +1897,15 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { * override this method to call their root service's implementation of it. * * @method - * @argument {DataObjectDescriptor} type - The type of object to find or + * @argument {ObjectDescriptor} type - The type of object to find or * create. * @argument {Object} data - An object whose property values * hold the object's raw data. That * data will be used to determine * the object's unique identifier. * @argument {?} context - A value, usually passed in to a - * [raw data service's]{@link RawDataService} - * [addRawData()]{@link RawDataService#addRawData} + * [data service's]{@link DataServoce} + * [addRawData()]{@link DataServoce#addRawData} * method, that can help in getting * or creating the object. * @returns {Object} - The existing object with the unique identifier @@ -1405,7 +1916,6 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { value: function (type, data, context, dataIdentifier) { if (this.isRootService) { var dataObject; - // TODO [Charles]: Object uniquing. if (this.isUniquing && dataIdentifier) { dataObject = this.objectForDataIdentifier(dataIdentifier); } @@ -1422,58 +1932,47 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, + /** + * Flag determining whether objects will be stored be registered and identified by + * a dataIdentifier. This is required for relationships to work properly. + * + * + * @property {Boolean} + */ isUniquing: { value: false }, - _identifier: { - value: undefined - }, - - identifier: { - get: function() { - return this._identifier || (this._identifier = Montage.getInfoForObject(this).moduleId); - } - }, - - __dataIdentifierByObject: { - value: null - }, - - _dataIdentifierByObject: { - // This property is shared with all child services. - // If created lazily the wrong data identifier will be returned when - // accessed by a child service. - value: new WeakMap() - }, - /** - * Returns a unique object for a DataIdentifier + * Returns a unique object for a DataIdentifier * [fetchObjectProperty()]{@link DataService#fetchObjectProperty} instead * of this method. That method will be called by this method when needed. * * @method - * @argument {object} object - The object whose property values are - * being requested. - * - * @returns {DataIdentifier} - An object's DataIdentifier + * @argument {object} object - object + * @returns {DataIdentifier} - object's DataIdentifier */ - dataIdentifierForObject: { - value: function (object) { - return this._dataIdentifierByObject.get(object); + objectForDataIdentifier: { + value: function(dataIdentifier) { + return this._objectByDataIdentifier.get(dataIdentifier); } }, /** - * Records an object's DataIdentifier + * Register an object with its dataIdentifier for uniquing reasons * + * @private * @method - * @argument {object} object - an Object. - * @argument {DataIdentifier} dataIdentifier - The object whose property values are + * @argument {Object} object - object to register. + * @argument {DataIdentifier} dataIdentifier - dataIdentifier of object to register. + * @returns {void} */ - recordDataIdentifierForObject: { - value: function(dataIdentifier, object) { - this._dataIdentifierByObject.set(object, dataIdentifier); + registerUniqueObjectWithDataIdentifier: { + value: function(object, dataIdentifier) { + if (object && dataIdentifier && this.isRootService && this.isUniquing) { + this._dataIdentifierByObject.set(object, dataIdentifier); + this._objectByDataIdentifier.set(dataIdentifier, object); + } } }, @@ -1489,195 +1988,450 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, - __objectByDataIdentifier: { - value: null + /** + * Remove an object's DataIdentifier + * + * @method + * @argument {object} object - an object + */ + removeObjectForDataIdentifier: { + value: function(dataIdentifier) { + this._objectByDataIdentifier.delete(dataIdentifier); + } }, + + /*************************************************************************** + * Fetching Data + */ - _objectByDataIdentifier: { - get: function() { - return this.__objectByDataIdentifier || (this.__objectByDataIdentifier = new WeakMap()); + _cancelServiceDataStream: { + value: function (rawDataService, dataStream, reason) { + rawDataService.cancelRawDataStream(dataStream, reason); + this._dataServiceByDataStream.delete(dataStream); } }, + /** - * Returns a unique object for a DataIdentifier - * [fetchObjectProperty()]{@link DataService#fetchObjectProperty} instead - * of this method. That method will be called by this method when needed. + * Get or make a DataIdentifier for an ObjectDescriptor and a primary key * * @method - * @argument {object} object - object - * @returns {DataIdentifier} - object's DataIdentifier + * @argument {ObjectDescriptor} [type] - ObjectDescriptor for the type that this rawData represents + * @argument {String} [primaryKey] - The primaryKey for the rawData for which the method will + * build a dataIdentifier. + * + * @returns {DataIdentifier} */ - objectForDataIdentifier: { - value: function(dataIdentifier) { - return this._objectByDataIdentifier.get(dataIdentifier); + _dataIdentifierForTypeAndPrimaryKey: { + value: function (type, primaryKey) { + var typeName = type.typeName /*DataDescriptor*/ || type.name, + dataIdentifierMap = this._typeIdentifierMap.get(type), + dataIdentifier; + + if (!dataIdentifierMap) { + this._typeIdentifierMap.set(type,(dataIdentifierMap = new Map())); + } + + dataIdentifier = dataIdentifierMap.get(primaryKey); + if (!dataIdentifier) { + //This should be done by ObjectDescriptor/blueprint using primaryProperties + //and extract the corresponsing values from rawData + //For now we know here that MileZero objects have an "id" attribute. + dataIdentifier = new DataIdentifier(); + dataIdentifier.objectDescriptor = type; + dataIdentifier.dataService = this; + dataIdentifier.typeName = type.name; + dataIdentifier._identifier = dataIdentifier.primaryKey = primaryKey; + + dataIdentifierMap.set(primaryKey, dataIdentifier); + } + + return dataIdentifier; } }, + /** - * Records an object's DataIdentifier + * Cache storing which service owns each DataStream * - * @method - * @argument {DataIdentifier} dataIdentifier - DataIdentifier - * @argument {object} object - object represented by dataIdentifier + * @private + * @property {WeakMap>} */ - recordObjectForDataIdentifier: { - value: function(object, dataIdentifier) { - this._objectByDataIdentifier.set(dataIdentifier, object); + _dataServiceByDataStream: { + get: function () { + return this.__dataServiceByDataStream || (this.__dataServiceByDataStream = new WeakMap()); } }, /** - * Remove an object's DataIdentifier + * Evaluates a rawData object against the RawDataTypeMappings for the fetched + * class and returns the subclass for the first mapping that evaluates to true. * * @method - * @argument {object} object - an object - */ - removeObjectForDataIdentifier: { - value: function(dataIdentifier) { - this._objectByDataIdentifier.delete(dataIdentifier); + * @param {ObjectDescriptor} parent Fetched class for which to look for subclasses + * @param {Object} rawData rawData to evaluate against the RawDataTypeMappings + * @return {ObjectDescriptor} + */ + _descriptorForParentAndRawData: { + value: function (parent, rawData) { + var mappings = this._descriptorToRawDataTypeMappings.get(parent), + compiled, mapping, subType, + i, n; + + if (mappings && mappings.length) { + for (i = 0, n = mappings.length; i < n && !subType; ++i) { + mapping = mappings[i]; + subType = mapping.criteria.evaluate(rawData) && mapping.type; + } + } + + return subType ? this._descriptorForParentAndRawData(subType, rawData) : parent; } }, + /** - * Create a new data object of the specified type. - * - * Since root services are responsible for tracking and creating data - * objects, subclasses whose instances will not be root services should - * override this method to call their root service's implementation of it. + * Get cooked object for this ObjectDescriptor and rawData and + * map the rawData to it. + * + * This is in contrast to the synchronous objectForTypeRawData which + * will not map the rawData to the object. + * + * Returns the same object in memory for rawData with matching primaryKeys. * * @method - * @argument {DataObjectDescriptor} type - The type of object to create. - * @returns {Object} - The created object. - */ - //TODO add the creation of a temporary identifier to pass to _createDataObject - createDataObject: { - value: function (type) { - if (this.isRootService) { - var object = this._createDataObject(type); - this.createdDataObjects.add(object); - return object; + * @argument {ObjectDescriptor} [type] - ObjectDescriptor for the type that this rawData represents + * @argument {Object} [rawData] - An anonymnous object whose properties' + * values hold the raw data. + * @argument {Object} [context] - An anonymnous object whose properties' + * values hold the raw data. + * @returns {Promise} - object fully mapped with the rawData + */ + _mappedObjectForTypeAndRawData: { + value: function (type, rawData, context) { + var object = this.objectForTypeRawData(type, rawData, context), + mapResult = this._mapRawDataToObject(rawData, object, context), + result; + + if (mapResult instanceof Promise) { + result = mapResult.then(function () { + return object; + }); } else { - this.rootService.createDataObject(type); + result = Promise.resolve(object); } + + return result; } }, + /** - * Create a data object without registering it in the new object map. + * Return the module id for the DataService for this query. + * The module id is defined either on the query parameters or + * or on the mapping rule for this property. * - * @private * @method - * @argument {DataObjectDescriptor} type - The type of object to create. - * @returns {Object} - The created object. + * @argument {DataQuery} query - a DataQuery + * @return {String} serviceModuleID - Module ID of a data service */ - _createDataObject: { - value: function (type, dataIdentifier) { - var objectDescriptor = this._objectDescriptorForType(type), - object = Object.create(this._getPrototypeForType(objectDescriptor)); - if (object) { - - //This needs to be done before a user-land code can attempt to do - //anyting inside its constructor, like creating a binding on a relationships - //causing a trigger to fire, not knowing about the match between identifier - //and object... If that's feels like a real situation, it is. - this.registerUniqueObjectWithDataIdentifier(object, dataIdentifier); - // if (dataIdentifier && this.isUniquing) { - // this.recordDataIdentifierForObject(dataIdentifier, object); - // this.recordObjectForDataIdentifier(object, dataIdentifier); - // } + _serviceIdentifierForQuery: { + value: function (query) { + var parameters = query.criteria.parameters, + serviceModuleID = parameters && parameters.serviceIdentifier, + mapping, propertyName; - object = object.constructor.call(object) || object; - if (object) { - this._setObjectType(object, objectDescriptor); - } + if (!serviceModuleID) { + mapping = this.mappingWithType(query.type); + propertyName = mapping && parameters && parameters.propertyName; + serviceModuleID = propertyName && mapping.serviceIdentifierForProperty(propertyName); } - return object; + + return serviceModuleID; } }, - /** - * Register an object with its dataIdentifier for uniquing reasons + /** + * Records in the process of being written to streams (after + * [addRawData()]{@link RawDataService#addRawData} has been called and + * before [rawDataDone()]{@link RawDataService#rawDataDone} is called for + * any given stream). This is used to collect raw data that needs to be + * stored for offline use. * * @private - * @method - * @argument {Object} object - object to register. - * @argument {DataIdentifier} dataIdentifier - dataIdentifier of object to register. - * @returns {void} + * @type {Object.} */ - registerUniqueObjectWithDataIdentifier: { - value: function(object, dataIdentifier) { - if (object && dataIdentifier && this.isRootService && this.isUniquing) { - this.recordDataIdentifierForObject(dataIdentifier, object); - this.recordObjectForDataIdentifier(object, dataIdentifier); + _streamRawData: { + get: function () { + if (!this.__streamRawData) { + this.__streamRawData = new WeakMap(); } + return this.__streamRawData; } }, - /*************************************************************************** - * Data Object Changes - */ + /** + * Called by [addRawData()]{@link DataService#addRawData} to add an object + * for the passed record to the stream. This method both takes care of doing + * mapRawDataToObject and add the object to the stream. + * + * @method + * @argument {DataStream} stream + * - The stream to which the data objects created + * from the raw data should be added. + * @argument {Object} rawData - An anonymnous object whose properties' + * values hold the raw data. This array + * will be modified by this method. + * @argument {?} context - An arbitrary value that will be passed to + * [getDataObject()]{@link DataService#getDataObject} + * and + * [mapRawDataToObject()]{@link DataService#mapRawDataToObject} + * if it is provided. + * @argument {Boolean} canMap - Indicate whether this service is eligible to map + * the rawData. + * @returns {Promise} - A promise resolving to the mapped object. + * + */ + addOneRawData: { + value: function (stream, rawData, context, canMap) { + var type = this._descriptorForParentAndRawData(stream.query.type, rawData), + objectDescriptor = rawData && this.objectDescriptorForObject(rawData), + // canMap is a new argument. + // To support backwards compatibility, we assume that + // calls to addOneRawData without canMap expect + // the function to perform the mapping + shouldMap = arguments.length < 4 || (canMap && !objectDescriptor), + object = true, result; + + + if (shouldMap) { + result = this._mappedObjectForTypeAndRawData(type, rawData, context).then(function (object) { + stream.addData(object); + }); + } else { + stream.addData(rawData); + result = this.nullPromise; + } + + this._addMappingPromiseForStream(result, stream); + + if (object) { + this.callDelegateMethod("rawDataServiceDidAddOneRawData", this, stream, rawData, object); + } + + return result; + } + }, + /** - * A set of the data objects created by this service or any other descendent - * of this service's [root service]{@link DataService#rootService} since - * that root service's data was last saved, or since the root service was - * created if that service's data hasn't been saved yet. + * To be called by [fetchData()]{@link DataService#fetchData} or + * [fetchRawData()]{@link DataService#fetchRawData} when raw data records + * are received. This method should never be called outside of those + * methods. * - * Since root services are responsible for tracking data objects, subclasses - * whose instances will not be root services should override this property - * to return their root service's value for it. + * This method creates and registers the data objects that + * will represent the raw records with repeated calls to + * [getDataObject()]{@link DataService#getDataObject}, maps + * the raw data to those objects with repeated calls to + * [mapRawDataToObject()]{@link DataService#mapRawDataToObject}, + * and then adds those objects to the specified stream. * - * @type {Set.} - */ - createdDataObjects: { - get: function () { - if (this.isRootService) { - if (!this._createdDataObjects) { - this._createdDataObjects = new Set(); + * Subclasses should not override this method and instead override their + * [getDataObject()]{@link DataService#getDataObject} method, their + * [mapRawDataToObject()]{@link DataService#mapRawDataToObject} method, + * their [mapping]{@link DataService#mapping}'s + * [mapRawDataToObject()]{@link DataService#mapRawDataToObject} method, + * or several of these. + * + * @method + * @argument {DataStream} stream + * - The stream to which the data objects created + * from the raw data should be added. + * @argument {Array} records - An array of objects whose properties' + * values hold the raw data. This array + * will be modified by this method. + * @argument {?} context - An arbitrary value that will be passed to + * [getDataObject()]{@link DataService#getDataObject} + * and + * [mapRawDataToObject()]{@link DataService#mapRawDataToObject} + * if it is provided. + */ + addRawData: { + value: function (stream, records, context, shouldBatch) { + var offline, i, n, + streamQueryType = stream.query.type, + ownMapping = this.mappingWithType(streamQueryType), + serviceID = this._serviceIdentifierForQuery(stream.query), + canMap = (ownMapping || this.implementsMapRawDataToObject || this.isRootService) && !serviceID, + iRecord; + + // Record fetched raw data for offline use if appropriate. + offline = records && !this.isOffline && this._streamRawData.get(stream); + if (offline) { + offline.push.apply(offline, records); + } else if (records && !this.isOffline) { + //Do we really need to make a shallow copy of the array for bookeeping? + //this._streamRawData.set(stream, records.slice()); + this._streamRawData.set(stream, records); + } + + if ((this.batchAddsDataToStream || shouldBatch) && canMap) { + this._addAllRawData(stream, records, context); + } else { + for (i = 0, n = records && records.length; i < n; i++) { + this.addOneRawData(stream, records[i], context, canMap); } - return this._createdDataObjects; } - else { - return this.rootService.createdDataObjects; + } + }, + + _addAllRawData: { + value: function (stream, records, context) { + var promises = [], + promise, i, n; + + for (i = 0, n = records && records.length; i < n; i++) { + promise = this._mappedObjectForTypeAndRawData(stream.query.type, records[i], context); + promises.push(promise); + this._addMappingPromiseForStream(promise, stream); } + Promise.all(promises).then(function (objects) { + stream.addData(objects); + }); } }, /** - * A set of the data objects managed by this service or any other descendent - * of this service's [root service]{@link DataService#rootService} that have - * been changed since that root service's data was last saved, or since the - * root service was created if that service's data hasn't been saved yet + * Determines whether rawData passed to addRawData is added to the + * stream one-by-one or with the entire array. + * + * E.g. This rawData + * + * [{id: 1}, {id: 2}, {id: 3}, {id: 4}] + * + * can be mapped to the cooked object in these amounts of time: + * ID TIME + * 1 100ms + * 2 50ms + * 3 300ms + * 4 200ms + * + * + * Note that mappings are done in parallel. + * + * If batchAddsDataToStream is false, each item will be + * added to the stream as soon as it is mapped. The following + * timeline shows when mapping and stream add occur: + * 50ms: item 2 finished mapping. item 2 is added + * 100ms: item 1 finished mapping. item 1 is added + * 200ms: item 4 finished mapping. item 4 is added + * 300ms: item 3 finished mapping. item 3 is added + * + * + * If batchAddsDataToStream is true, all items will be + * added to the stream only once the last item is mapped. + * The following timeline shows when mapping and stream + * add occur: + * 50ms: item 2 finished mapping. + * 100ms: item 1 finished mapping. + * 200ms: item 4 finished mapping. + * 300ms: item 3 finished mapping. items 1, 2, 3, & 4 are added + * + * @property {Boolean} + * + */ + batchAddsDataToStream: { + value: false + }, + + /** + * To be called to indicates that the consumer has lost interest in the passed DataStream. + * This will allow the RawDataService feeding the stream to take appropriate measures. * - * Since root services are responsible for tracking data objects, subclasses - * whose instances will not be root services should override this property - * to return their root service's value for it. + * @method + * @argument {DataStream} [dataStream] - The DataStream to cancel + * @argument {Object} [reason] - An object indicating the reason to cancel. * - * @type {Set.} */ - changedDataObjects: { - get: function () { - if (this.isRootService) { - this._changedDataObjects = this._changedDataObjects || new Set(); - return this._changedDataObjects; - } - else { - return this.rootService.changedDataObjects(); + cancelDataStream: { + value: function (dataStream, reason) { + if (dataStream) { + var rawDataService = this._dataServiceByDataStream.get(dataStream), + self = this; + + if (Promise.is(rawDataService)) { + rawDataService.then(function(service) { + self._cancelServiceDataStream(service, dataStream, reason); + }); + } + else { + this._cancelServiceDataStream(rawDataService, dataStream, reason); + } } + } }, - _changedDataObjects: { - value: undefined + + /** + * To be called to indicates that the consumer has lost interest in the passed DataStream. + * This will allow the RawDataService feeding the stream to take appropriate measures. + * + * @method + * @argument {ObjectDescriptor} [type] - ObjectDescriptor for the type that this rawData represents + * @argument {Object} [rawData] - An anonymnous object whose properties' + * values hold the raw data. + * + */ + dataIdentifierForTypeRawData: { + value: function (type, rawData) { + var mapping = this.mappingWithType(type), + rawDataPrimaryKeys = mapping ? mapping.rawDataPrimaryKeyExpressions : null, + scope = new Scope(rawData), + rawDataPrimaryKeysValues, + dataIdentifier, dataIdentifierMap, primaryKey, + expression, i; + + if (rawDataPrimaryKeys && rawDataPrimaryKeys.length) { + + dataIdentifierMap = this._typeIdentifierMap.get(type); + + if (!dataIdentifierMap) { + this._typeIdentifierMap.set(type,(dataIdentifierMap = new Map())); + } + + for (i = 0; (expression = rawDataPrimaryKeys[i]); i++) { + rawDataPrimaryKeysValues = rawDataPrimaryKeysValues || []; + rawDataPrimaryKeysValues[i] = expression(scope); + } + if (rawDataPrimaryKeysValues) { + primaryKey = rawDataPrimaryKeysValues.join("/"); + + } + + return this._dataIdentifierForTypeAndPrimaryKey(type, primaryKey); + } + return undefined; + } }, - /*************************************************************************** - * Fetching Data + + /** + * Return the DataService responsible for a DataStream + * + * @method + * @argument {DataStream} dataStream + * @returns {DataService} */ + dataServiceForDataStream: { + get: function(dataStream) { + return this._dataServiceByDataStream.get(dataStream); + } + }, /** * Fetch data from the service using its child services. * - * This method accept [types]{@link DataObjectDescriptor} as alternatives to + * This method accept [types]{@link ObjectDescriptor} as alternatives to * [queries]{@link DataQuery}, and its [stream]{DataStream} argument is * optional, but when it calls its child services it will provide them with * a [query]{@link DataQuery}, it provide them with a @@ -1745,7 +2499,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { var service; //This is a workaround, we should clean that up so we don't //have to go up to answer that question. The difference between - //.TYPE and Objectdescriptor still creeps-in when it comes to + //.TYPE and ObjectDescriptor still creeps-in when it comes to //the service to answer that to itself if (self.parentService && self.parentService.childServiceForType(query.type) === self && typeof self.fetchRawData === "function") { service = self; @@ -1757,8 +2511,16 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { service = self.childServiceForType(query.type); if (service) { - stream = service.fetchData(query, stream) || stream; - self._dataServiceByDataStream.set(stream, service); + + var rawDataStream = new DataStream(); + rawDataStream.query = query; + rawDataStream.dataExpression = query.selectExpression; + rawDataStream = service.fetchData(query, rawDataStream) || rawDataStream; + self._dataServiceByDataStream.set(rawDataStream, service); + rawDataStream.then(function (rawData) { + self.addRawData(stream, rawData, null, service.batchAddsDataToStream); + self.rawDataDone(stream); + }); } else { throw new Error("Can't fetch data of unknown type - " + (query.type.typeName || query.type.name) + "/" + query.type.uuid); } @@ -1774,113 +2536,75 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, - _fetchRawData: { - value: function (stream) { - var self = this, - childService = this._childServiceForQuery(stream.query); - if (childService) { - childService._fetchRawData(stream); - } else { - if (this.authorizationPolicy === AuthorizationPolicy.ON_DEMAND) { - if (typeof this.shouldAuthorizeForQuery === "function" && this.shouldAuthorizeForQuery(stream.query) && !this.authorization) { - this.authorizationPromise = exports.DataService.authorizationManager.authorizeService(this).then(function(authorization) { - self.authorization = authorization; - return authorization; - }).catch(function(error) { - console.log(error); - }); - } - } - this.authorizationPromise.then(function (authorization) { - var streamSelector = stream.query; - stream.query = self.mapSelectorToRawDataQuery(streamSelector); - self.fetchRawData(stream); - stream.query = streamSelector; - }); - } - } - }, - - _childServiceForQuery: { - value: function (query) { - var serviceModuleID = this._serviceIdentifierForQuery(query), - service = serviceModuleID && this._childServicesByIdentifier.get(serviceModuleID); - - - if (!service && this._childServicesByType.has(query.type)) { - service = this._childServicesByType.get(query.type); - service = service && service[0]; - } - - return service || null; - } - }, - - _serviceIdentifierForQuery: { - value: function (query) { - var parameters = query.criteria.parameters, - serviceModuleID = parameters && parameters.serviceIdentifier, - mapping, propertyName; - - if (!serviceModuleID) { - mapping = this.mappingWithType(query.type); - propertyName = mapping && parameters && parameters.propertyName; - serviceModuleID = propertyName && mapping.serviceIdentifierForProperty(propertyName); - } - - return serviceModuleID; - } - }, - - __dataServiceByDataStream: { - value: null - }, - - _dataServiceByDataStream: { - get: function() { - return this.__dataServiceByDataStream || (this.__dataServiceByDataStream = new WeakMap()); - } - }, - - dataServiceForDataStream: { - get: function(dataStream) { - return this._dataServiceByDataStream.get(dataStream); + /** + * Return cooked, unmapped object for this ObjectDescriptor and rawData. + * + * Returns the same object in memory for rawData with matching primaryKeys. + * + * This is in contrast to the asynchronous _mappedObjectForTypeAndRawData + * which will also map the rawData to the object + * + * @method + * @argument {ObjectDescriptor} [type] - ObjectDescriptor for the type that this rawData represents + * @argument {Object} [rawData] - An anonymnous object whose properties' + * values hold the raw data. + * @argument {Object} [context] - An anonymnous object whose properties' + * values hold the raw data. + * @returns {Object} - object representing the dataIdentifier + */ + objectForTypeRawData: { + value:function(type, rawData, context) { + var dataIdentifier = this.dataIdentifierForTypeRawData(type, rawData); + //Record snapshot before we may create an object + this.recordSnapshot(dataIdentifier, rawData); + //iDataIdentifier argument should be all we need later on + return this.getDataObject(type, rawData, context, dataIdentifier); } }, /** - * To be called to indicates that the consumer has lost interest in the passed DataStream. - * This will allow the RawDataService feeding the stream to take appropriate measures. + * To be called once for each [fetchData()]{@link DataService#fetchData} + * or [fetchRawData()]{@link DataService#fetchRawData} call received to + * indicate that all the raw data meant for the specified stream has been + * added to that stream. * - * @method - * @argument {DataStream} [dataStream] - The DataStream to cancel - * @argument {Object} [reason] - An object indicating the reason to cancel. + * Subclasses should not override this method. * - */ - cancelDataStream: { - value: function (dataStream, reason) { - if (dataStream) { - var rawDataService = this._dataServiceByDataStream.get(dataStream), - self = this; + * @method + * @argument {DataStream} stream - The stream to which the data objects + * corresponding to the raw data have been + * added. + * @argument {?} context - An arbitrary value that will be passed to + * [writeOfflineData()]{@link DataService#writeOfflineData} + * if it is provided. + */ + rawDataDone: { + value: function (stream, context) { + var self = this, + dataToPersist = this._streamRawData.get(stream), + mappingPromises = this._streamMappingPromises.get(stream), + dataReadyPromise = mappingPromises ? Promise.all(mappingPromises) : this.nullPromise; - if (Promise.is(rawDataService)) { - rawDataService.then(function(service) { - self._cancelServiceDataStream(service, dataStream, reason); - }); - } - else { - this._cancelServiceDataStream(rawDataService, dataStream, reason); - } + if (mappingPromises) { + this._streamMappingPromises.delete(stream); } - } - }, + if (dataToPersist) { + this._streamRawData.delete(stream); + } + + dataReadyPromise.then(function (results) { + //TODO Figure out if writeOfflineData needs to be handled in DataService + return dataToPersist ? self.writeOfflineData(dataToPersist, stream.query, context) : null; + // return null; + }).then(function () { + stream.dataDone(); + return null; + }).catch(function (e) { + console.error(e); + }); - _cancelServiceDataStream: { - value: function (rawDataService, dataStream, reason) { - rawDataService.cancelRawDataStream(dataStream, reason); - this._dataServiceByDataStream.delete(dataStream); } }, @@ -1888,20 +2612,40 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { * Saving Data */ - /** - * Delete a data object. + /** + * Subclasses should override this method to delete a data object when that + * object's raw data wouldn't be useful to perform the deletion. + * + * The default implementation maps the data object to raw data and calls + * [deleteRawData()]{@link DataService#deleteRawData} with the data + * object passed in as the `context` argument of that method. * * @method - * @argument {Object} object - The object whose data should be deleted. + * @argument {Object} object - The object to delete. * @returns {external:Promise} - A promise fulfilled when the object has - * been deleted. + * been deleted. The promise's fulfillment value is not significant and will + * usually be `null`. */ - deleteDataObject: { + _deleteDataObject: { value: function (object) { - var saved = !this.createdDataObjects.has(object); - return this._updateDataObject(object, saved && "deleteDataObject"); + var self = this, + record = {}, + mapResult = this._mapObjectToRawData(object, record), + result; + + if (mapResult instanceof Promise) { + result = mapResult.then(function () { + return self.deleteRawData(record, object); + }); + } else { + result = this.deleteRawData(record, object); + } + + return result; } }, + + /** * Save changes made to a data object. @@ -1911,53 +2655,46 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { * @returns {external:Promise} - A promise fulfilled when all of the data in * the changed object has been saved. */ - saveDataObject: { + _saveDataObject: { value: function (object) { - //return this._updateDataObject(object, "saveDataObject"); + var record = {}; + this._mapObjectToRawData(object, record); + return this.saveRawData(record, object); + } + }, + _saveRawData: { + value: function (rawData, object) { var self = this, service, promise = this.nullPromise, + shouldSaveRawData = !!(this.parentService && this.parentService._childServiceForObject(object) === this), mappingPromise; - if (this.parentService && this.parentService._getChildServiceForObject(object) === this) { - var record = {}; - mappingPromise = this._mapObjectToRawData(object, record); - if (!mappingPromise) { - mappingPromise = this.nullPromise; - } - return mappingPromise.then(function () { - return self.saveRawData(record, object) - .then(function (data) { - self.rootService.createdDataObjects.delete(object); - return data; - }); - }); - } - else { - service = this._getChildServiceForObject(object); + if (shouldSaveRawData) { + return self.saveRawData(rawData, object); + } else { + service = this._childServiceForObject(object); if (service) { - return service.saveDataObject(object); - } - else { + return service._saveRawData(rawData, object); + } else { return promise; } } } }, - _updateDataObject: { value: function (object, action) { var self = this, service, promise = this.nullPromise; - if (this.parentService && this.parentService._getChildServiceForObject(object) === this) { + if (this.parentService && this.parentService._childServiceForObject(object) === this) { service = action && this; - } - else { - service = action && this._getChildServiceForObject(object); + action = "_" + action; + } else { + service = action && this._childServiceForObject(object); if (service) { return service._updateDataObject(object, action); } @@ -1975,35 +2712,98 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, - _saveDataObject: { + /** + * Delete a data object. + * + * @method + * @argument {Object} object - The object whose data should be deleted. + * @returns {external:Promise} - A promise fulfilled when the object has + * been deleted. + */ + deleteDataObject: { value: function (object) { - var record = {}; - this._mapObjectToRawData(object, record); - return this.saveRawData(record, object); + var saved = !this.createdDataObjects.has(object); + return this._updateDataObject(object, saved && "deleteDataObject"); + + //From RawDataService + + } + }, + + implementsSaveDataObject: { + get: function () { + return exports.DataService.prototype.saveDataObject !== this.saveDataObject; + } + }, + + /** + * Save a data object. + * + * + * Navigate the service tree to the service responsible for mapping the object. + * + * Upon mapping completion, navigate service tree from the root to the service + * responsible for saving this rawData + * + * @method + * @argument {Object} object - The object whose data should be deleted. + * @returns {external:Promise} - A promise fulfilled when the object has + * been saved + */ + saveDataObject: { + value: function (object) { + var self = this, + promise = this.nullPromise, + ownMapping = this.mappingForObject(object, true), + hasParent = !!this.parentService, + service = this._childServiceForObject(object), + childHasMapping = service && service.implementsMapObjectToRawData, + shouldMap = !!(ownMapping || this.implementsMapObjectToRawData || !hasParent) && !childHasMapping, + childHasSaveDataObject = !!(service && service.implementsSaveDataObject), + mappingPromise; + + + if (shouldMap) { + var record = {}; + mappingPromise = this._mapObjectToRawData(object, record) || this.nullPromise; + return mappingPromise.then(function () { + return self.rootService._saveRawData(record, object).then(function (result) { + self.rootService.createdDataObjects.delete(object); + return result; + }); + }); + } else if (service) { + return service.saveDataObject(object); + } else { + return promise; + } + } + }, + + saveRawData: { + value: function () { + return this.nullPromise; } }, - // _updateDataObject: { - // value: function (object, action) { - // var self = this, - // service = action && this._getChildServiceForObject(object), - // promise = this.nullPromise; - - // if (!action) { - // self.createdDataObjects.delete(object); - // } else if (service) { - // promise = service[action](object).then(function () { - // self.createdDataObjects.delete(object); - // return null; - // }); - // } - // return promise; - // } - // }, /*************************************************************************** * Offline */ + _compareOfflineOperations: { + value: function(operation1, operation2) { + // TODO: Remove reference to `lastModified` once child services have + // been udpated to use `time` instead. + return operation1.lastModified < operation2.lastModified ? -1 : + operation1.lastModified > operation2.lastModified ? 1 : + operation1.time < operation2.time ? -1 : + operation1.time > operation2.time ? 1 : + operation1.index < operation2.index ? -1 : + operation1.index > operation2.index ? 1 : + 0; + } + }, + _initializeOffline: { value: function () { // TODO: This code assumes that the first instance of DataService or @@ -2027,69 +2827,11 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, - _isOfflineInitialized: { - value: false - }, - - /** - * Returns a value derived from and continuously updated with the value of - * [navigator.onLine]{@link https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine/onLine}. - * - * Root services are responsible for tracking offline status, and subclasses - * not designed to be root services should override this property to get - * its value from their root service. - * - * @type {boolean} - */ - isOffline: { - get: function () { - if (this._isOffline === undefined) { - // Determine the initial value from the navigator state and call - // the public setter so _goOnline() is invoked if appropriate. - this.isOffline = !navigator.onLine; - } - return this._isOffline; - }, - set: function (offline) { - var self = this; - if (this._willBeOffline === null) { - // _goOnline() just finished, set _isOffline to the desired - // value and clear the "just finished" flag in _willBeOffline. - this._isOffline = offline ? true : false; - this._willBeOffline = undefined; - } else if (this._willBeOffline !== undefined) { - // _goOnline() is in progress, just record the future value. - this._willBeOffline = offline ? true : false; - } else if (this._isOffline === false) { - // Already online and not starting up, no need for _goOnline(). - this._isOffline = offline ? true : false; - } else if (!offline) { - // Going from offline to online, or starting up online, so - // assume we were last offline, call _goOnline(), and only - // change the value when that's done. - this._isOffline = true; - this._willBeOffline = false; - this._goOnline().then(function () { - var offline = self._willBeOffline; - self._willBeOffline = null; - self.isOffline = offline; - return null; - }); - } - } - }, - _isOffline: { // `undefined` on startup, otherwise always `true` or `false`. value: false }, - _willBeOffline: { - // `true` or `false` while _goOnline() is in progress, `null` just after - // it's done, `undefined` otherwise. - value: undefined - }, - _goOnline: { value: function() { var self = this; @@ -2102,63 +2844,29 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, - _compareOfflineOperations: { - value: function(operation1, operation2) { - // TODO: Remove reference to `lastModified` once child services have - // been udpated to use `time` instead. - return operation1.lastModified < operation2.lastModified ? -1 : - operation1.lastModified > operation2.lastModified ? 1 : - operation1.time < operation2.time ? -1 : - operation1.time > operation2.time ? 1 : - operation1.index < operation2.index ? -1 : - operation1.index > operation2.index ? 1 : - 0; - } + _isOfflineInitialized: { + value: false }, - /** - * Reads all the offline operations recorded on behalf of this service. - * - * The default implementation aggregates this service children's offline - * operations, keeping track of which child service is responsible for each - * operation. - * - * Subclasses that provide offline support should override this method to - * return the operations that have been performed while offline. - * - * @method - */ - readOfflineOperations: { - value: function () { - // TODO: Get rid of the dummy WeakMap passed to children once the - // children's readOfflineOperations code has been updated to not - // expect it. - // This implementation avoids creating promises for services with no - // children or whose children don't have offline operations. - var self = this, - dummy = new WeakMap(), - services = this._offlineOperationServices, - array, promises; - this.childServices.forEach(function (child) { - var promise = child.readOfflineOperations(dummy); - if (promise !== self.emptyArrayPromise) { - array = array || []; - promises = promises || []; - promises.push(promise.then(function(operations) { - var i, n; - for (i = 0, n = operations && operations.length; i < n; i += 1) { - services.set(operations[i], child); - array.push(operations[i]); - } - return null; - })); - } - }); - return promises ? Promise.all(promises).then(function () { return array; }) : - this.emptyArrayPromise; + _offlineOperationMethodName: { + value: function(type) { + var isString = typeof type === "string", + name = isString && this._offlineOperationMethodNames.get(type); + if (isString && !name) { + name = "perform"; + name += type[0].toUpperCase(); + name += type.slice(1); + name += "OfflineOperation"; + this._offlineOperationMethodNames.set(type, name); + } + return name; } }, + _offlineOperationMethodNames: { + value: new Map() + }, + /** * @private * @type {Map} @@ -2172,73 +2880,6 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, - __offlineOperationServices: { - value: undefined - }, - - /** - * Perform operations recorded while offline. This will be invoked when the - * service comes online after being offline. - * - * The default implementation delegates performance of each operation to - * the child service responsible for that operation, as determined by - * [readOfflineOperations()]{@link DataService#readOfflineOperations}. It - * will batch operations if several consecutive operations belong to the - * same child service. - * - * For each operation not handled by a child service, the default - * implementation calls a method named `performFooOfflineOperation()`, if - * such a method exists in this service where `foo` is the operation's - * [data type]{@link DataOperation#dataType}. If no such method exists, - * [readOfflineOperation()]{@link DataService#readOfflineOperation} is - * called instead. - * - * Subclasses that provide offline support should implement these - * `performFooOfflineOperation()` methods or override the - * `readOfflineOperation()` method to perform each operation, or they can - * override this `performOfflineOperations()` method instead. - * - * Subclass overriding this method are responsible for - * [deleting]{@link DataService#deleteOfflineOperations} operations after - * they have been performed. Subclasses implementing - * `performFooOfflineOperation()` methods or overriding the - * `readOfflineOperation()` method are not. - * - * @method - * @argument {Array.} - operations - * @returns {Promise} - A promise fulfilled with a null value when the - * operations have been performed, or rejected if a problem occured that - * should prevent following operations from being performed. - */ - performOfflineOperations: { - value: function(operations) { - var services = this._offlineOperationServices, - promise = this.nullPromise, - child, - i, j, n, jOperation, jOperationChanges, jService, jOperationType, jTableSchema, jForeignKeys, - OfflineService = OfflineService, - k, countK, kForeignKey,kOnlinePrimaryKey; - - // Perform each operation, batching if possible, and collecting the - // results in a chain of promises. - for (i = 0, n = operations.length; i < n; i = j) { - // Find the service responsible for this operation. - child = services.get(operations[i]); - // Find the end of a batch of operations for this service. - j = i + 1; - while (j < n && child && (jService = services.get((jOperation = operations[j]))) === child) { - ++j; - } - // Add the promise to perform this batch of operations to the - // end of the chain of promises to fulfill all operations. - promise = - this._performOfflineOperationsBatch(promise, child, operations, i, j); - } - // Return a promise for the sequential fulfillment of all operations. - return promise; - } - }, - _performOfflineOperationsBatch: { value: function(promise, child, operations, start, end) { var self = this; @@ -2289,29 +2930,109 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { // TODO: Remove support for operation.type once all child services // have been updated to provide an operation.dataType instead. var type = operation.dataType || operation.type, - method = type && this[this._getOfflineOperationMethodName(type)]; + method = type && this[this._offlineOperationMethodName(type)]; return typeof(method) === "function" ? method.call(this, operation) : this.performOfflineOperation(operation); } }, - _getOfflineOperationMethodName: { - value: function(type) { - var isString = typeof type === "string", - name = isString && this._offlineOperationMethodNames.get(type); - if (isString && !name) { - name = "perform"; - name += type[0].toUpperCase(); - name += type.slice(1); - name += "OfflineOperation"; - this._offlineOperationMethodNames.set(type, name); + _selfIsOffline: { + value: function (offline) { + var self = this; + if (this._willBeOffline === null) { + // _goOnline() just finished, set _isOffline to the desired + // value and clear the "just finished" flag in _willBeOffline. + this._isOffline = offline ? true : false; + this._willBeOffline = undefined; + } else if (this._willBeOffline !== undefined) { + // _goOnline() is in progress, just record the future value. + this._willBeOffline = offline ? true : false; + } else if (this._isOffline === false) { + // Already online and not starting up, no need for _goOnline(). + this._isOffline = offline ? true : false; + } else if (!offline) { + // Going from offline to online, or starting up online, so + // assume we were last offline, call _goOnline(), and only + // change the value when that's done. + this._isOffline = true; + this._willBeOffline = false; + this._goOnline().then(function () { + var offline = self._willBeOffline; + self._willBeOffline = null; + self.isOffline = offline; + return null; + }); } - return name; } }, - _offlineOperationMethodNames: { - value: new Map() + _willBeOffline: { + // `true` or `false` while _goOnline() is in progress, `null` just after + // it's done, `undefined` otherwise. + value: undefined + }, + + /** + * Delete operations recorded while offline. + * + * Services overriding the (plural) + * [performOfflineOperations()]{@link DataService#performOfflineOperations} + * method must invoke this method after each operation they perform is + * successfully performed. + * + * This method will be called automatically for services that perform + * operations by implementing a + * [performOfflineOperation()]{@link DataService#performOfflineOperation} + * or `performFooOfflineOperation()` methods (where `foo` is an operation + * [data type]{@link DataOperation#dataType}). + * + * Subclasses that provide offline operations support must override this + * method to delete the specified offline operations from their records. + * + * @method + * @argument {Array.} operations + * @returns {Promise} - A promise fulfilled with a null value when the + * operations have been deleted. + */ + deleteOfflineOperations: { + value: function(operations) { + // To be overridden by subclasses that use offline operations. + return this.nullPromise; + } + }, + + /** + * Returns a value derived from and continuously updated with the value of + * [navigator.onLine]{@link https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine/onLine}. + * + * Root services are responsible for tracking offline status, and subclasses + * not designed to be root services should override this property to get + * its value from their root service. + * + * @type {boolean} + */ + isOffline: { + get: function () { + if (this._isOffline === undefined) { + // Determine the initial value from the navigator state and call + // the public setter so _goOnline() is invoked if appropriate. + this.isOffline = this === this.rootService ? !navigator.onLine : this.rootService.isOffline; + } + return this._isOffline; + }, + set: function (offline) { + if (this === this.rootService) { + this._selfIsOffline(offline); + } + } + }, + + // To be overridden by subclasses as necessary + onlinePrimaryKeyForOfflinePrimaryKey: { + value: function(offlinePrimaryKey) { + return this.offlineService ? + this.offlineService.onlinePrimaryKeyForOfflinePrimaryKey(offlinePrimaryKey) : null; + } }, /** @@ -2341,87 +3062,272 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, - // To be overridden by subclasses as necessary - onlinePrimaryKeyForOfflinePrimaryKey: { - value: function(offlinePrimaryKey) { - return this.offlineService ? - this.offlineService.onlinePrimaryKeyForOfflinePrimaryKey(offlinePrimaryKey) : null; - } - }, - /** - * Delete operations recorded while offline. + * Perform operations recorded while offline. This will be invoked when the + * service comes online after being offline. * - * Services overriding the (plural) - * [performOfflineOperations()]{@link DataService#performOfflineOperations} - * method must invoke this method after each operation they perform is - * successfully performed. + * The default implementation delegates performance of each operation to + * the child service responsible for that operation, as determined by + * [readOfflineOperations()]{@link DataService#readOfflineOperations}. It + * will batch operations if several consecutive operations belong to the + * same child service. * - * This method will be called automatically for services that perform - * operations by implementing a - * [performOfflineOperation()]{@link DataService#performOfflineOperation} - * or `performFooOfflineOperation()` methods (where `foo` is an operation - * [data type]{@link DataOperation#dataType}). + * For each operation not handled by a child service, the default + * implementation calls a method named `performFooOfflineOperation()`, if + * such a method exists in this service where `foo` is the operation's + * [data type]{@link DataOperation#dataType}. If no such method exists, + * [readOfflineOperation()]{@link DataService#readOfflineOperation} is + * called instead. * - * Subclasses that provide offline operations support must override this - * method to delete the specified offline operations from their records. + * Subclasses that provide offline support should implement these + * `performFooOfflineOperation()` methods or override the + * `readOfflineOperation()` method to perform each operation, or they can + * override this `performOfflineOperations()` method instead. + * + * Subclass overriding this method are responsible for + * [deleting]{@link DataService#deleteOfflineOperations} operations after + * they have been performed. Subclasses implementing + * `performFooOfflineOperation()` methods or overriding the + * `readOfflineOperation()` method are not. * * @method - * @argument {Array.} operations + * @argument {Array.} - operations * @returns {Promise} - A promise fulfilled with a null value when the - * operations have been deleted. + * operations have been performed, or rejected if a problem occured that + * should prevent following operations from being performed. */ - deleteOfflineOperations: { + performOfflineOperations: { value: function(operations) { - // To be overridden by subclasses that use offline operations. + var services = this._offlineOperationServices, + promise = this.nullPromise, + child, + i, j, n, jOperation, jOperationChanges, jService, jOperationType, jTableSchema, jForeignKeys, + OfflineService = OfflineService, + k, countK, kForeignKey,kOnlinePrimaryKey; + + // Perform each operation, batching if possible, and collecting the + // results in a chain of promises. + for (i = 0, n = operations.length; i < n; i = j) { + // Find the service responsible for this operation. + child = services.get(operations[i]); + // Find the end of a batch of operations for this service. + j = i + 1; + while (j < n && child && (jService = services.get((jOperation = operations[j]))) === child) { + ++j; + } + // Add the promise to perform this batch of operations to the + // end of the chain of promises to fulfill all operations. + promise = + this._performOfflineOperationsBatch(promise, child, operations, i, j); + } + // Return a promise for the sequential fulfillment of all operations. + return promise; + } + }, + + /** + * Reads all the offline operations recorded on behalf of this service. + * + * The default implementation aggregates this service children's offline + * operations, keeping track of which child service is responsible for each + * operation. + * + * Subclasses that provide offline support should override this method to + * return the operations that have been performed while offline. + * + * @method + */ + readOfflineOperations: { + value: function () { + // TODO: Get rid of the dummy WeakMap passed to children once the + // children's readOfflineOperations code has been updated to not + // expect it. + // This implementation avoids creating promises for services with no + // children or whose children don't have offline operations. + var self = this, + dummy = new WeakMap(), + services = this._offlineOperationServices, + array, promises; + this.childServices.forEach(function (child) { + var promise = child.readOfflineOperations(dummy); + if (promise !== self.emptyArrayPromise) { + array = array || []; + promises = promises || []; + promises.push(promise.then(function(operations) { + var i, n; + for (i = 0, n = operations && operations.length; i < n; i += 1) { + services.set(operations[i], child); + array.push(operations[i]); + } + return null; + })); + } + }); + return promises ? Promise.all(promises).then(function () { return array; }) : + this.emptyArrayPromise; + } + }, + + /** + * Called with all the data passed to + * [addRawData()]{@link RawDataService#addRawData} to allow storing of that + * data for offline use. + * + * The default implementation does nothing. This is appropriate for + * subclasses that do not support offline operation or which operate the + * same way when offline as when online. + * + * Other subclasses may override this method to store data fetched when + * online so [fetchData]{@link RawDataSource#fetchData} can use that data + * when offline. + * + * @method + * @argument {Object} records - An array of objects whose properties' values + * hold the raw data. + * @argument {?DataQuery} selector + * - Describes how the raw data was selected. + * @argument {?} context - The value that was passed in to the + * [rawDataDone()]{@link RawDataService#rawDataDone} + * call that invoked this method. + * @returns {external:Promise} - A promise fulfilled when the raw data has + * been saved. The promise's fulfillment value is not significant and will + * usually be `null`. + */ + writeOfflineData: { + value: function (records, selector, context) { + // Subclasses should override this to do something useful. return this.nullPromise; } }, + /*************************************************************************** - * Utilities + * Authorization */ + _initializeAuthorization: { + value: function () { + if (this.providesAuthorization) { + exports.DataService.authorizationManager.registerAuthorizationService(this); + } + + if (this.authorizationPolicy === AuthorizationPolicyType.UpfrontAuthorizationPolicy) { + var self = this; + exports.DataService.authorizationManager.authorizeService(this).then(function(authorization) { + self.authorization = authorization; + return authorization; + }).catch(function(error) { + console.log(error); + }); + } else { + //Service doesn't need anything upfront, so we just go through + this.authorizationPromise = Promise.resolve(); + } + } + }, + /** - * A function that does nothing but returns null, useful for terminating - * a promise chain that needs to return null, as in the following code: + * Returns the AuthorizationPolicyType used by this DataService. * - * var self = this; - * return this.fetchSomethingAsynchronously().then(function (data) { - * return self.doSomethingAsynchronously(data.part); - * }).then(this.nullFunction); + * @type {AuthorizationPolicyType} + */ + authorizationPolicy: { + value: AuthorizationPolicyType.NoAuthorizationPolicy + }, + + /** + * holds authorization object after a successfull authorization * - * @type {function} + * @type {Object} */ - nullFunction: { - value: function () { - return null; - } + + authorization: { + value: undefined + }, + + authorizationPromise: { + value: Promise.resolve() }, /** - * A shared promise resolved with a value of - * `null`, useful for returning from methods like - * [fetchObjectProperty()]{@link DataService#fetchObjectProperty} - * when the requested data is already there. + * Returns the list of moduleIds of DataServices a service accepts to provide + * authorization on its behalf. If an array has multiple + * authorizationServices, the final choice will be up to the App user + * regarding which one to use. This array is expected to return moduleIds, + * not objects, allowing the AuthorizationManager to manage unicity * - * @type {external:Promise} + * @type {string[]} */ - nullPromise: { - get: function () { - if (!exports.DataService._nullPromise) { - exports.DataService._nullPromise = Promise.resolve(null); - } - return exports.DataService._nullPromise; - } + authorizationServices: { + value: null }, - _nullPromise: { + /** + * @type {string} + * @description Module ID of the panel component used to gather necessary authorization information + */ + authorizationPanel: { value: undefined }, /** - * @todo Document. + * Indicates whether a service can provide user-level authorization to its + * data. Defaults to false. Concrete services need to override this as + * needed. + * + * @type {boolean} + */ + providesAuthorization: { + value: false + }, + + /** + * Performs whatever tasks are necessary to authorize + * this service and returns a Promise that resolves with + * an Authorization object. + * + * @method + * @returns Promise + */ + authorize: { + value: undefined + }, + + + /** + * + * @method + * @returns Promise + */ + logOut: { + value: function () { + console.warn("DataService.logOut() must be overridden by the implementing service"); + return this.nullPromise; + } + }, + + /*************************************************************************** + * Utilities + */ + + _flattenArray: { + value: function (array) { + return Array.prototype.concat.apply([], array); + } + }, + + _isObjectDescriptor: { + value: function (type) { + return type instanceof ObjectDescriptor || type instanceof DataObjectDescriptor; + } + }, + + /** + * Convenience property to return a Promise that resolves to an + * empty array. + * + * @property + * @return {Promise} */ emptyArrayPromise: { get: function () { @@ -2432,10 +3338,6 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, - _emptyArrayPromise: { - value: undefined - }, - /** * A possibly shared promise resolved in the next cycle of the event loop * or soon thereafter, at which point the current event handling will be @@ -2459,6 +3361,40 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, + /** + * A function that does nothing but returns null, useful for terminating + * a promise chain that needs to return null, as in the following code: + * + * var self = this; + * return this.fetchSomethingAsynchronously().then(function (data) { + * return self.doSomethingAsynchronously(data.part); + * }).then(this.nullFunction); + * + * @type {function} + */ + nullFunction: { + value: function () { + return null; + } + }, + + /** + * A shared promise resolved with a value of + * `null`, useful for returning from methods like + * [fetchObjectProperty()]{@link DataService#fetchObjectProperty} + * when the requested data is already there. + * + * @type {external:Promise} + */ + nullPromise: { + get: function () { + if (!exports.DataService._nullPromise) { + exports.DataService._nullPromise = Promise.resolve(null); + } + return exports.DataService._nullPromise; + } + }, + /** * Splice an array into another array. * diff --git a/data/service/data-stream.js b/data/service/data-stream.js index 758b55974b..904ebe13cd 100644 --- a/data/service/data-stream.js +++ b/data/service/data-stream.js @@ -248,7 +248,7 @@ exports.DataStream = DataProvider.specialize(/** @lends DataStream.prototype */ } } }, - + /** * To be called when all the data expected by this stream has been added * to its [data]{@link DataStream#data} array. After this is called diff --git a/data/service/data-trigger.js b/data/service/data-trigger.js index 36716bcbcc..6a73cc9718 100644 --- a/data/service/data-trigger.js +++ b/data/service/data-trigger.js @@ -276,6 +276,16 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy } }, + __overwriteCacheOnUpdate: { + value: false + }, + + _overwriteCacheOnUpdate: { + writable: true, + configurable: true, + value: false + }, + /** * @todo Rename and document API and implementation. * @@ -319,7 +329,7 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy value: function (object) { var self = this, status = this._getValueStatus(object) || {}; - if (!status.promise) { + if (this._overwriteCacheOnUpdate || !status.promise) { this._setValueStatus(object, status); status.promise = new Promise(function (resolve, reject) { status.resolve = resolve; @@ -327,6 +337,7 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy self._fetchObjectProperty(object); }); } + // Return the existing or just created promise for this data. return status.promise; } @@ -500,6 +511,7 @@ Object.defineProperties(exports.DataTrigger, /** @lends DataTrigger */ { trigger._objectPrototype = prototype; trigger._propertyName = name; trigger._isGlobal = descriptor.isGlobal; + trigger._overwriteCacheOnUpdate = descriptor.isDerived; if (descriptor.definition) { Montage.defineProperty(prototype, name, { get: function () { diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 0471b973ae..eb2f24dc09 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -769,7 +769,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData converter.expression = converter.expression || rule.expression; converter.foreignDescriptor = converter.foreignDescriptor || propertyDescriptor.valueDescriptor; converter.objectDescriptor = this.objectDescriptor; - converter.serviceIdentifier = rule.serviceIdentifier; + converter.serviceIdentifier = converter.serviceIdentifier || rule.serviceIdentifier; } } }, diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 14df6020dd..a9b5a34615 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -43,8 +43,6 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot constructor: { value: function RawDataService() { DataService.call(this); - this._typeIdentifierMap = new Map(); - this._descriptorToRawDataTypeMappings = new Map(); } }, @@ -71,8 +69,6 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot deserializeSelf: { value:function (deserializer) { this.super(deserializer); - var value = deserializer.getProperty("rawDataTypeMappings"); - this._registerRawDataTypeMappings(value || []); } }, @@ -95,66 +91,35 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot }, /*************************************************************************** - * Data Object Properties + * Fetching Data */ - _propertyDescriptorForObjectAndName: { - value: function (object, propertyName) { - var objectDescriptor = this.objectDescriptorForObject(object); - return objectDescriptor && objectDescriptor.propertyDescriptorForName(propertyName); - } - }, + /** + * Return the child service that should handle this query. + * The service is identified by the serviceIdentifier query + * parameter, if it exists, or the query.type. + * + * @method + * @argument {DataQuery} query - a DataQuery + * @returns {DataService} + */ + _childServiceForQuery: { + value: function (query) { + var serviceModuleID = this._serviceIdentifierForQuery(query), + service = serviceModuleID && this._childServicesByIdentifier.get(serviceModuleID), + descriptor = this._objectDescriptorForType(query.type); - _objectDescriptorForObject: { - value: function (object) { - var types = this.types, - objectInfo = Montage.getInfoForObject(object), - moduleId = objectInfo.moduleId, - objectName = objectInfo.objectName, - module, exportName, objectDescriptor, i, n; - for (i = 0, n = types.length; i < n && !objectDescriptor; i += 1) { - module = types[i].module; - exportName = module && types[i].exportName; - if (module && moduleId === module.id && objectName === exportName) { - objectDescriptor = types[i]; - } - } - return objectDescriptor; - } - }, - _mapObjectPropertyValue: { - value: function (object, propertyDescriptor, value) { - var propertyName = propertyDescriptor.name; - if (propertyDescriptor.cardinality === Infinity) { - this.spliceWithArray(object[propertyName], value); - } else { - object[propertyName] = value[0]; + if (!service && this._childServicesByObjectDescriptor.has(descriptor)) { + service = this._childServicesByObjectDescriptor.get(descriptor); + service = service && service[0]; } - if (propertyDescriptor.inversePropertyName && value && value[0]) { - var inverseBlueprint = this._propertyDescriptorForObjectAndName(value[0], propertyDescriptor.inversePropertyName); - if (inverseBlueprint && inverseBlueprint.cardinality === 1) { - value.forEach(function (inverseObject) { - inverseObject[propertyDescriptor.inversePropertyName] = object; - }); - } - } - return value; + return service || null; } }, - _objectDescriptorTypeForValueDescriptor: { - value: function (valueDescriptor) { - return valueDescriptor.then(function (objectDescriptor) { - return objectDescriptor.module.require.async(objectDescriptor.module.id); - }); - } - }, - /*************************************************************************** - * Fetching Data - */ /** * Fetch the raw data of this service. @@ -198,10 +163,6 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot childService = this._childServiceForQuery(stream.query), query = stream.query; - //TODO [TJ] Determine purpose of this check... - // if (childService && childService.identifier.indexOf("offline-service") === -1) { - // childService._fetchRawData(stream); - // } else if (childService) { childService._fetchRawData(stream); } else if (query.authorization) { @@ -250,39 +211,6 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot * Saving Data */ - /** - * Subclasses should override this method to delete a data object when that - * object's raw data wouldn't be useful to perform the deletion. - * - * The default implementation maps the data object to raw data and calls - * [deleteRawData()]{@link RawDataService#deleteRawData} with the data - * object passed in as the `context` argument of that method. - * - * @method - * @argument {Object} object - The object to delete. - * @returns {external:Promise} - A promise fulfilled when the object has - * been deleted. The promise's fulfillment value is not significant and will - * usually be `null`. - */ - deleteDataObject: { - value: function (object) { - var self = this, - record = {}, - mapResult = this._mapObjectToRawData(object, record), - result; - - if (mapResult instanceof Promise) { - result = mapResult.then(function () { - return self.deleteRawData(record, object); - }); - } else { - result = this.deleteRawData(record, object); - } - - return result; - } - }, - /** * Subclasses should override this method to delete a data object when that * object's raw data would be useful to perform the deletion. @@ -329,19 +257,6 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot * Offline */ - /* - * Returns the [root service's offline status]{@link DataService#isOffline}. - * - * @type {boolean} - */ - isOffline: { - get: function () { - return this === this.rootService ? - this.superForGet("isOffline")() : - this.rootService.isOffline; - } - }, - /** * Called with all the data passed to * [addRawData()]{@link RawDataService#addRawData} to allow storing of that @@ -374,325 +289,6 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot } }, - /*************************************************************************** - * Collecting Raw Data - */ - - /** - * To be called by [fetchData()]{@link RawDataService#fetchData} or - * [fetchRawData()]{@link RawDataService#fetchRawData} when raw data records - * are received. This method should never be called outside of those - * methods. - * - * This method creates and registers the data objects that - * will represent the raw records with repeated calls to - * [getDataObject()]{@link DataService#getDataObject}, maps - * the raw data to those objects with repeated calls to - * [mapRawDataToObject()]{@link RawDataService#mapRawDataToObject}, - * and then adds those objects to the specified stream. - * - * Subclasses should not override this method and instead override their - * [getDataObject()]{@link DataService#getDataObject} method, their - * [mapRawDataToObject()]{@link RawDataService#mapRawDataToObject} method, - * their [mapping]{@link RawDataService#mapping}'s - * [mapRawDataToObject()]{@link RawDataMapping#mapRawDataToObject} method, - * or several of these. - * - * @method - * @argument {DataStream} stream - * - The stream to which the data objects created - * from the raw data should be added. - * @argument {Array} records - An array of objects whose properties' - * values hold the raw data. This array - * will be modified by this method. - * @argument {?} context - An arbitrary value that will be passed to - * [getDataObject()]{@link RawDataService#getDataObject} - * and - * [mapRawDataToObject()]{@link RawDataService#mapRawDataToObject} - * if it is provided. - */ - addRawData: { - value: function (stream, records, context) { - var offline, i, n, - streamSelectorType = stream.query.type, - iRecord; - // Record fetched raw data for offline use if appropriate. - offline = records && !this.isOffline && this._streamRawData.get(stream); - if (offline) { - offline.push.apply(offline, records); - } else if (records && !this.isOffline) { - //Do we really need to make a shallow copy of the array for bookeeping? - //this._streamRawData.set(stream, records.slice()); - this._streamRawData.set(stream, records); - } - // Convert the raw data to appropriate data objects. The conversion - // will be done in place to avoid creating any unnecessary array. - for (i = 0, n = records && records.length; i < n; i++) { - /*jshint -W083*/ - // Turning off jshint's function within loop warning because the - // only "outer scoped variable" we're accessing here is stream, - // which is a constant reference and won't cause unexpected - // behavior due to iteration. - // if (streamSelectorType.name && streamSelectorType.name.toUpperCase().indexOf("BSP") !== -1) { - // debugger; - // } - this.addOneRawData(stream, records[i], context, streamSelectorType); - /*jshint +W083*/ - } - } - }, - - /** - * Called by [addRawData()]{@link RawDataService#addRawData} to add an object - * for the passed record to the stream. This method both takes care of doing - * mapRawDataToObject and add the object to the stream. - * - * @method - * @argument {DataStream} stream - * - The stream to which the data objects created - * from the raw data should be added. - * @argument {Object} rawData - An anonymnous object whose properties' - * values hold the raw data. This array - * will be modified by this method. - * @argument {?} context - An arbitrary value that will be passed to - * [getDataObject()]{@link RawDataService#getDataObject} - * and - * [mapRawDataToObject()]{@link RawDataService#mapRawDataToObject} - * if it is provided. - * - * @returns {Promise} - A promise resolving to the mapped object. - * - */ - - addOneRawData: { - value: function (stream, rawData, context) { - var type = this._descriptorForParentAndRawData(stream.query.type, rawData), - object = this.objectForTypeRawData(type, rawData, context), - result = this._mapRawDataToObject(rawData, object, context); - - if (result && result instanceof Promise) { - result = result.then(function () { - stream.addData(object); - return object; - }); - } else { - stream.addData(object); - result = Promise.resolve(object); - } - this._addMapDataPromiseForStream(result, stream); - - if (object) { - this.callDelegateMethod("rawDataServiceDidAddOneRawData", this, stream, rawData, object); - } - return result; - } - }, - - _addMapDataPromiseForStream: { - value: function (promise, stream) { - if (!this._streamMapDataPromises.has(stream)) { - this._streamMapDataPromises.set(stream, [promise]); - } else { - this._streamMapDataPromises.get(stream).push(promise); - } - } - }, - - _streamMapDataPromises: { - get: function () { - if (!this.__streamMapDataPromises) { - this.__streamMapDataPromises = new Map(); - } - return this.__streamMapDataPromises; - } - }, - - objectForTypeRawData: { - value:function(type, rawData, context) { - var dataIdentifier = this.dataIdentifierForTypeRawData(type,rawData); - //Record snapshot before we may create an object - this.recordSnapshot(dataIdentifier, rawData); - //iDataIdentifier argument should be all we need later on - return this.getDataObject(type, rawData, context, dataIdentifier); - } - }, - - _typeIdentifierMap: { - value: undefined - }, - - //This should belong on the - //Gives us an indirection layer to deal with backward compatibility. - dataIdentifierForTypeRawData: { - value: function (type, rawData) { - var mapping = this.mappingWithType(type), - rawDataPrimaryKeys = mapping ? mapping.rawDataPrimaryKeyExpressions : null, - scope = new Scope(rawData), - rawDataPrimaryKeysValues, - dataIdentifier, dataIdentifierMap, primaryKey; - - if(rawDataPrimaryKeys && rawDataPrimaryKeys.length) { - - dataIdentifierMap = this._typeIdentifierMap.get(type); - - if(!dataIdentifierMap) { - this._typeIdentifierMap.set(type,(dataIdentifierMap = new Map())); - } - - for(var i=0, expression; (expression = rawDataPrimaryKeys[i]); i++) { - rawDataPrimaryKeysValues = rawDataPrimaryKeysValues || []; - rawDataPrimaryKeysValues[i] = expression(scope); - } - if(rawDataPrimaryKeysValues) { - primaryKey = rawDataPrimaryKeysValues.join("/"); - dataIdentifier = dataIdentifierMap.get(primaryKey); - } - - if(!dataIdentifier) { - var typeName = type.typeName /*DataDescriptor*/ || type.name; - //This should be done by ObjectDescriptor/blueprint using primaryProperties - //and extract the corresponsing values from rawData - //For now we know here that MileZero objects have an "id" attribute. - dataIdentifier = new DataIdentifier(); - dataIdentifier.objectDescriptor = type; - dataIdentifier.dataService = this; - dataIdentifier.typeName = type.name; - dataIdentifier._identifier = dataIdentifier.primaryKey = primaryKey; - - dataIdentifierMap.set(primaryKey,dataIdentifier); - } - return dataIdentifier; - } - return undefined; - } - }, - - __snapshot: { - value: null - }, - - _snapshot: { - get: function() { - return this.__snapshot || (this.__snapshot = new Map()); - } - }, - - - /** - * Records the snapshot of the values of record known for a DataIdentifier - * - * @private - * @argument {DataIdentifier} dataIdentifier - * @argument {Object} rawData - */ - recordSnapshot: { - value: function (dataIdentifier, rawData) { - this._snapshot.set(dataIdentifier, rawData); - } - }, - - /** - * Removes the snapshot of the values of record for the DataIdentifier argument - * - * @private - * @argument {DataIdentifier} dataIdentifier - */ - removeSnapshot: { - value: function (dataIdentifier) { - this._snapshot.delete(dataIdentifier); - } - }, - - /** - * Returns the snapshot associated with the DataIdentifier argument if available - * - * @private - * @argument {DataIdentifier} dataIdentifier - */ - snapshotForDataIdentifier: { - value: function (dataIdentifier) { - return this._snapshot.get(dataIdentifier); - } - }, - - /** - * Returns the snapshot associated with the DataIdentifier argument if available - * - * @private - * @argument {DataIdentifier} dataIdentifier - */ - snapshotForObject: { - value: function (object) { - return this.snapshotForDataIdentifier(this.dataIdentifierForObject(object)); - } - }, - - /** - * To be called once for each [fetchData()]{@link RawDataService#fetchData} - * or [fetchRawData()]{@link RawDataService#fetchRawData} call received to - * indicate that all the raw data meant for the specified stream has been - * added to that stream. - * - * Subclasses should not override this method. - * - * @method - * @argument {DataStream} stream - The stream to which the data objects - * corresponding to the raw data have been - * added. - * @argument {?} context - An arbitrary value that will be passed to - * [writeOfflineData()]{@link RawDataService#writeOfflineData} - * if it is provided. - */ - rawDataDone: { - value: function (stream, context) { - var self = this, - dataToPersist = this._streamRawData.get(stream), - mappingPromises = this._streamMapDataPromises.get(stream), - dataReadyPromise = mappingPromises ? Promise.all(mappingPromises) : this.nullPromise; - - if (mappingPromises) { - this._streamMapDataPromises.delete(stream); - } - - if (dataToPersist) { - this._streamRawData.delete(stream); - } - - dataReadyPromise.then(function (results) { - - return dataToPersist ? self.writeOfflineData(dataToPersist, stream.query, context) : null; - }).then(function () { - stream.dataDone(); - return null; - }).catch(function (e) { - console.error(e); - }); - - } - }, - - /** - * Records in the process of being written to streams (after - * [addRawData()]{@link RawDataService#addRawData} has been called and - * before [rawDataDone()]{@link RawDataService#rawDataDone} is called for - * any given stream). This is used to collect raw data that needs to be - * stored for offline use. - * - * @private - * @type {Object.} - */ - _streamRawData: { - get: function () { - if (!this.__streamRawData) { - this.__streamRawData = new WeakMap(); - } - return this.__streamRawData; - } - }, - - __streamRawData: { - value: undefined - }, /*************************************************************************** * Mapping Raw Data @@ -728,302 +324,5 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot value: deprecate.deprecateMethod(void 0, function (selector) { return this.mapSelectorToRawDataQuery(selector); }, "mapSelectorToRawDataSelector", "mapSelectorToRawDataQuery"), - }, - - /** - * Retrieve DataMapping for this object. - * - * @method - * @argument {Object} object - An object whose object descriptor has a DataMapping - */ - mappingForObject: { - value: function (object) { - var objectDescriptor = this.objectDescriptorForObject(object), - mapping = objectDescriptor && this.mappingWithType(objectDescriptor); - - - if (!mapping && objectDescriptor) { - mapping = this._objectDescriptorMappings.get(objectDescriptor); - if (!mapping) { - mapping = DataMapping.withObjectDescriptor(objectDescriptor); - this._objectDescriptorMappings.set(objectDescriptor, mapping); - } - } - - return mapping; - } - }, - - /** - * Convert raw data to data objects of an appropriate type. - * - * Subclasses should override this method to map properties of the raw data - * to data objects: - * @method - * @argument {Object} record - An object whose properties' values hold - * the raw data. - * @argument {Object} object - An object whose properties must be set or - * modified to represent the raw data. - * @argument {?} context - The value that was passed in to the - * [addRawData()]{@link RawDataService#addRawData} - * call that invoked this method. - */ - - - mapRawDataToObject: { - value: function (rawData, object, context) { - return this.mapFromRawData(object, rawData, context); - } - }, - /** - * Convert raw data to data objects of an appropriate type. - * - * Subclasses should override this method to map properties of the raw data - * to data objects, as in the following: - * - * mapRawDataToObject: { - * value: function (object, record) { - * object.firstName = record.GIVEN_NAME; - * object.lastName = record.FAMILY_NAME; - * } - * } - * - * Alternatively, subclasses can define a - * [mapping]{@link DataService#mapping} to do this mapping. - * - * The default implementation of this method uses the service's mapping if - * the service has one, and otherwise calls the deprecated - * [mapFromRawData()]{@link RawDataService#mapFromRawData}, whose default - * implementation does nothing. - * - * @todo Make this method overridable by type name with methods like - * `mapRawDataToHazard()` and `mapRawDataToProduct()`. - * - * @method - * @argument {Object} record - An object whose properties' values hold - * the raw data. - * @argument {Object} object - An object whose properties must be set or - * modified to represent the raw data. - * @argument {?} context - The value that was passed in to the - * [addRawData()]{@link RawDataService#addRawData} - * call that invoked this method. - */ - _mapRawDataToObject: { - value: function (record, object, context) { - var self = this, - mapping = this.mappingForObject(object), - result; - - if (mapping) { - result = mapping.mapRawDataToObject(record, object, context); - if (result) { - result = result.then(function () { - return self.mapRawDataToObject(record, object, context); - }); - } else { - result = this.mapRawDataToObject(record, object, context); - } - } else { - result = this.mapRawDataToObject(record, object, context); - } - - return result; - - } - }, - - /** - * Public method invoked by the framework during the conversion from - * an object to a raw data. - * Designed to be overriden by concrete RawDataServices to allow fine-graine control - * when needed, beyond transformations offered by an ObjectDescriptorDataMapping or - * an ExpressionDataMapping - * - * @method - * @argument {Object} object - An object whose properties must be set or - * modified to represent the raw data. - * @argument {Object} record - An object whose properties' values hold - * the raw data. - * @argument {?} context - The value that was passed in to the - * [addRawData()]{@link RawDataService#addRawData} - * call that invoked this method. - */ - mapObjectToRawData: { - value: function (object, record, context) { - // this.mapToRawData(object, record, context); - } - }, - - /** - * @todo Document. - * @todo Make this method overridable by type name with methods like - * `mapHazardToRawData()` and `mapProductToRawData()`. - * - * @method - */ - _mapObjectToRawData: { - value: function (object, record, context) { - var mapping = this.mappingForObject(object), - result; - - if (mapping) { - result = mapping.mapObjectToRawData(object, record, context); - } - - if (record) { - if (result) { - var otherResult = this.mapObjectToRawData(object, record, context); - if (result instanceof Promise && otherResult instanceof Promise) { - result = Promise.all([result, otherResult]); - } else if (otherResult instanceof Promise) { - result = otherResult; - } - } else { - result = this.mapObjectToRawData(object, record, context); - } - } - - return result; - } - }, - - // /** - // * If defined, used by - // * [mapRawDataToObject()]{@link RawDataService#mapRawDataToObject} and - // * [mapObjectToRawData()]{@link RawDataService#mapObjectToRawData} to map - // * between the raw data on which this service is based and the typed data - // * objects which this service provides and manages. - // * - // * @type {?DataMapping} - // */ - // mapping: { - // value: undefined - // }, - - _mappingsPromise: { - get: function () { - if (!this.__mappingsPromise) { - this.__mappingsPromise = Promise.all(this.mappings.map(function (mapping) { - return mapping.objectDescriptor; - })).then(function (values) { - - }); - } - return this.__mappingsPromise; - } - }, - - _objectDescriptorMappings: { - get: function () { - if (!this.__objectDescriptorMappings) { - this.__objectDescriptorMappings = new Map(); - } - return this.__objectDescriptorMappings; - } - }, - - /** - * Map from a parent class to the mappings used by the service to - * determine what subclass to create an instance of for a particular - * rawData object - * - * For example, say a class 'Person' has 2 subclasses 'Employee' & 'Customer'. - * RawDataService would evaluate each person rawData object against each item - * in _rawDataTypeMappings and determine if that rawData should be an instance - * of 'Employee' or 'Customer'. - * @type {Map} - */ - - _descriptorToRawDataTypeMappings: { - value: undefined - }, - - /** - * Adds each mapping passed in to _descriptorToRawDataTypeMappings - * - * @method - * @param {Array} mappings - */ - _registerRawDataTypeMappings: { - value: function (mappings) { - var mapping, parentType, - i, n; - - for (i = 0, n = mappings ? mappings.length : 0; i < n; i++) { - mapping = mappings[i]; - parentType = mapping.type.parent; - if (!this._descriptorToRawDataTypeMappings.has(parentType)) { - this._descriptorToRawDataTypeMappings.set(parentType, []); - } - this._descriptorToRawDataTypeMappings.get(parentType).push(mapping); - } - } - }, - - /** - * Evaluates a rawData object against the RawDataTypeMappings for the fetched - * class and returns the subclass for the first mapping that evaluates to true. - * - * @method - * @param {ObjectDescriptor} parent Fetched class for which to look for subclasses - * @param {Object} rawData rawData to evaluate against the RawDataTypeMappings - * @return {ObjectDescriptor} - */ - _descriptorForParentAndRawData: { - value: function (parent, rawData) { - var mappings = this._descriptorToRawDataTypeMappings.get(parent), - compiled, mapping, subType, - i, n; - - if (mappings && mappings.length) { - for (i = 0, n = mappings.length; i < n && !subType; ++i) { - mapping = mappings[i]; - subType = mapping.criteria.evaluate(rawData) && mapping.type; - } - } - - return subType ? this._descriptorForParentAndRawData(subType, rawData) : parent; - } - }, - - /*************************************************************************** - * Deprecated - */ - - /** - * @todo Document deprecation in favor of - * [mapRawDataToObject()]{@link RawDataService#mapRawDataToObject} - * - * @deprecated - * @method - */ - mapFromRawData: { - value: function (object, record, context) { - // Implemented by subclasses. - } - }, - - /** - * @todo Document deprecation in favor of - * [mapObjectToRawData()]{@link RawDataService#mapObjectToRawData} - * - * @deprecated - * @method - */ - mapToRawData: { - value: function (object, record) { - // Implemented by subclasses. - } - }, - - /** - * @todo Remove any dependency and delete. - * - * @deprecated - * @type {OfflineService} - */ - offlineService: { - value: undefined } - }); diff --git a/data/service/sample.bp b/data/service/sample.bp deleted file mode 100644 index 4dd77ce48f..0000000000 --- a/data/service/sample.bp +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Sample blueprint file. Kept here as a record of the current thinking on - * blueprint file properties and structure. - * - * If this were a blueprint file this comments would have to be removed to allow - * the file to be valid JSON. - * - * Information that still needs to be represented: - * - * - Locking strategies and which properties to use for locking. - * - * - Whether properties should be fetched. By default non-derived properties - * would be fetched and others not. - * - * - Whether properties should be saved. By default non-derived properties would - * be saved and others not. - * - * - Raw data to data object mappings for properties. - * - * - Raw data to data property Mappings for dependency fetches. - */ -{ - objects: { - movie: { - properties: { - id: "?string" - director: "Person", - crew: "Array.", - reviews: "Array.", - ratings: "Array.", - averageRating: "number", - expenses: "?number", - income: "?number", - realProfit: "?number", - contractualProfit: "?number", - flag: "number" - } - } - }, - services: { - movie: { - identifiers: ["id"], - relationships: { - director: {criteria: "Person.id = Movie.directorId"}, - crew: {criteria: "Person.movies.id.has(Movie.id)"}, - reviews: {criteria: "type = Review && Review.movieId = Movie.id"}, - ratings: {"*": "review"}, /* Same fetch as review. */ - averageRating: {"<-": "ratings"}, /* Derived from ratings. */ - realProfit: {"<-": ["expenses", "income"]}, /* Derived from both expenses and income. */ - contractualProfit: {"<-": ["expenses", "income"]} /* Also derived from both expenses and income. */ - flag: {"isGlobal": true, criteria: "..."} /* Fetching one instances fetches values for all instances. */ - } - } - } -} diff --git a/test/all.js b/test/all.js index 37607e0bd9..49fa52fff0 100644 --- a/test/all.js +++ b/test/all.js @@ -80,8 +80,8 @@ module.exports = require("montage-testing").run(require, [ "spec/serialization/serialization-inspector-spec", "spec/serialization/serialization-merger-spec", {name: "spec/serialization/montage-serializer-spec"}, - { name: "spec/serialization/montage-deserializer-spec" }, {name: "spec/serialization/montage-serializer-element-spec", node: false}, + { name: "spec/serialization/montage-deserializer-spec", node: false }, { name: "spec/serialization/montage-deserializer-element-spec", node: false }, // Trigger {name: "spec/trigger/trigger-spec", node: false}, @@ -106,7 +106,7 @@ module.exports = require("montage-testing").run(require, [ // Reel {name: "spec/reel/template-spec", node: false, karma: true}, // UI - repetition - {name: "spec/ui/repetition-spec", node: false, karma: false}, + { name: "spec/ui/repetition-spec", node: false, karma: false}, {name: "spec/ui/repetition-selection-spec", node: false, karma: false}, {name: "spec/ui/repetition-binding-spec", node: false}, {name: "spec/core/localizer-spec", node: false, karma: false}, @@ -125,17 +125,18 @@ module.exports = require("montage-testing").run(require, [ {name: "spec/data/object-descriptor"}, {name: "spec/data/property-descriptor"}, {name: "spec/data/raw-data-service"}, - {name: "spec/data/raw-data-type-mapping-spec"}, + {name: "spec/data/raw-data-type-mapping-spec", node: false}, {name: "spec/data/integration", node: false}, + {name: "spec/data/data-service-mapping", node: false}, - // Meta - {name: "spec/meta/module-object-descriptor-spec"}, - {name: "spec/meta/object-descriptor-spec"}, - {name: "spec/meta/converter-object-descriptor-spec", node: false}, + // // Meta + { name: "spec/meta/converter-object-descriptor-spec", node: false}, + { name: "spec/meta/module-object-descriptor-spec", node: false}, {name: "spec/meta/build-in-component-object-descriptor-spec", node: false}, {name: "spec/meta/component-object-descriptor-spec", node: false}, {name: "spec/meta/controller-object-descriptor-spec", node: false}, {name: "spec/meta/event-descriptor-spec", node: false}, + { name: "spec/meta/object-descriptor-spec", node: false} ]).then(function () { console.log('montage-testing', 'End'); }, function (err) { diff --git a/test/spec/data/data-service-mapping.js b/test/spec/data/data-service-mapping.js new file mode 100644 index 0000000000..d3689268f2 --- /dev/null +++ b/test/spec/data/data-service-mapping.js @@ -0,0 +1,264 @@ +var ExpressionDataMapping = require("montage/data/service/expression-data-mapping").ExpressionDataMapping, + MainPersonService = require("spec/data/logic/service/main-person-service").MainPersonService, + Person = require("spec/data/logic/model/person").Person, + PersonB = require("spec/data/logic/model/person-b").PersonB, + RawPersonService = require("spec/data/logic/service/raw-person-service").RawPersonService, + RawPersonServiceB = require("spec/data/logic/service/raw-person-service-b").RawPersonServiceB, + RawPersonServiceC = require("spec/data/logic/service/raw-person-service-c").RawPersonServiceC, + RawPersonServiceD = require("spec/data/logic/service/raw-person-service-d").RawPersonServiceD, + RawPersonChildService = require("spec/data/logic/service/raw-person-child-service").RawPersonChildService, + ModuleObjectDescriptor = require("montage/core/meta/module-object-descriptor").ModuleObjectDescriptor, + ModuleReference = require("montage/core/module-reference").ModuleReference, + PropertyDescriptor = require("montage/core/meta/property-descriptor").PropertyDescriptor, + DateConverter = require("montage/core/converter/date-converter").DateConverter, + RawPropertyValueToObjectConverter = require("montage/data/converter/raw-property-value-to-object-converter").RawPropertyValueToObjectConverter; + +var DataMapping = require("montage/data/service/data-mapping").DataMapping; + +describe("DataMapping in the MainService", function() { + var mainService = new MainPersonService(), + rawService = new RawPersonService(), + rawServiceB = new RawPersonServiceB(), + rawServiceC = new RawPersonServiceC(), + rawServiceD = new RawPersonServiceD(), + rawChildService = new RawPersonChildService(), + registrationPromise, + personMapping, personReference, personDescriptor, + personBMapping, personBReference, personBDescriptor, + personCMapping, personCReference, personCDescriptor, + personDMapping, personDReference, personDDescriptor, + employerConverter, positionConverter; + + var dateConverter = Object.create({}, { + convert: { + value: function (rawValue) { + return rawValue !== undefined ? new Date(rawValue) : undefined; + } + }, + revert: { + value: function (date) { + return date.getTime(); + } + } + }); + + // rawService + personReference = new ModuleReference().initWithIdAndRequire("spec/data/logic/model/person", require); + personDescriptor = new ModuleObjectDescriptor().initWithModuleAndExportName(personReference, "Person"); + personDescriptor.addPropertyDescriptor(new PropertyDescriptor().initWithNameObjectDescriptorAndCardinality("name", personDescriptor, 1)); + personDescriptor.addPropertyDescriptor(new PropertyDescriptor().initWithNameObjectDescriptorAndCardinality("birthday", personDescriptor, 1)); + personDescriptor.addPropertyDescriptor(new PropertyDescriptor().initWithNameObjectDescriptorAndCardinality("location", personDescriptor, 1)); + + personMapping = new ExpressionDataMapping().initWithServiceObjectDescriptorAndSchema(rawService, personDescriptor); + personMapping.addRequisitePropertyName("name", "birthday"); + personMapping.addObjectMappingRule("name", { + "<->": "name" + }); + personMapping.addObjectMappingRule("birthday", { + "<->": "birth_date", + converter: dateConverter + }); + + + personBReference = new ModuleReference().initWithIdAndRequire("spec/data/logic/model/person-b", require); + personBDescriptor = new ModuleObjectDescriptor().initWithModuleAndExportName(personBReference, "PersonB"); + personBDescriptor.addPropertyDescriptor(new PropertyDescriptor().initWithNameObjectDescriptorAndCardinality("name", personBDescriptor, 1)); + personBDescriptor.addPropertyDescriptor(new PropertyDescriptor().initWithNameObjectDescriptorAndCardinality("birthday", personBDescriptor, 1)); + + personBMapping = new ExpressionDataMapping().initWithServiceObjectDescriptorAndSchema(rawServiceB, personBDescriptor); + personBMapping.addRequisitePropertyName("name", "birthday"); + personBMapping.addObjectMappingRule("name", { + "<->": "name" + }); + personBMapping.addObjectMappingRule("birthday", { + "<->": "birth_date", + converter: dateConverter + }); + + personCReference = new ModuleReference().initWithIdAndRequire("spec/data/logic/model/person-c", require); + personCDescriptor = new ModuleObjectDescriptor().initWithModuleAndExportName(personCReference, "PersonC"); + personCDescriptor.addPropertyDescriptor(new PropertyDescriptor().initWithNameObjectDescriptorAndCardinality("name", personCDescriptor, 1)); + personCDescriptor.addPropertyDescriptor(new PropertyDescriptor().initWithNameObjectDescriptorAndCardinality("birthday", personCDescriptor, 1)); + + personCMapping = new ExpressionDataMapping().initWithServiceObjectDescriptorAndSchema(rawServiceC, personCDescriptor); + personCMapping.addRequisitePropertyName("name", "birthday"); + personCMapping.addObjectMappingRule("name", { + "<->": "name" + }); + personCMapping.addObjectMappingRule("birthday", { + "<->": "birth_date", + converter: dateConverter + }); + + personDReference = new ModuleReference().initWithIdAndRequire("spec/data/logic/model/person-d", require); + personDDescriptor = new ModuleObjectDescriptor().initWithModuleAndExportName(personDReference, "PersonD"); + personDDescriptor.addPropertyDescriptor(new PropertyDescriptor().initWithNameObjectDescriptorAndCardinality("name", personDDescriptor, 1)); + personDDescriptor.addPropertyDescriptor(new PropertyDescriptor().initWithNameObjectDescriptorAndCardinality("birthday", personDDescriptor, 1)); + personDDescriptor.addPropertyDescriptor(new PropertyDescriptor().initWithNameObjectDescriptorAndCardinality("employer", personDDescriptor, 1)); + personDDescriptor.addPropertyDescriptor(new PropertyDescriptor().initWithNameObjectDescriptorAndCardinality("position", personDDescriptor, 1)); + + personDMapping = new ExpressionDataMapping().initWithServiceObjectDescriptorAndSchema(rawServiceD, personDDescriptor); + personDMapping.addRequisitePropertyName("name", "birthday", "employer", "position"); + personDMapping.addObjectMappingRule("name", { + "<->": "name" + }); + personDMapping.addObjectMappingRule("birthday", { + "<->": "birth_date", + converter: dateConverter + }); + + employerConverter = new RawPropertyValueToObjectConverter().initWithConvertExpression("employer_name == employer"); + employerConverter.service = rawServiceD; + employerConverter.serviceIdentifier = "spec/data/logic/service/raw-person-child-service"; + employerConverter.isEmployerConverter = true; + personDMapping.addObjectMappingRule("employer", { + "<-": "{employer_name: employer_name}", + converter: employerConverter + }); + positionConverter = new RawPropertyValueToObjectConverter().initWithConvertExpression("position_name == position"); + positionConverter.service = rawServiceD; + positionConverter.serviceIdentifier = "spec/data/logic/service/raw-person-child-service"; + personDMapping.addObjectMappingRule("position", { + "<-": "{position_name: position_name}", + converter: positionConverter + }); + + mainService.addMappingForType(personMapping, personDescriptor); + mainService.addMappingForType(personBMapping, personBDescriptor); + mainService.addMappingForType(personCMapping, personCDescriptor); + mainService.addMappingForType(personDMapping, personDDescriptor); + rawServiceD.addChildService(rawChildService); + + registrationPromise = Promise.all([ + mainService.registerChildService(rawService, [personDescriptor]), + mainService.registerChildService(rawServiceB, [personBDescriptor]), + mainService.registerChildService(rawServiceC, [personCDescriptor]), + mainService.registerChildService(rawServiceD, [personDDescriptor]), + ]); + + + describe("can map RawData to Object", function () { + + it("with mapping in MainService", function (done) { + registrationPromise.then(function () { + mainService.fetchData(personDescriptor).then(function (results) { + var test = function (person) { + expect(person.name).toBeDefined(); + expect(person.birthday).toBeDefined(); + expect(person.birthday instanceof Date).toBe(true); + expect(person.birth_date).toBeUndefined(); + } + + expect(results).toBeDefined(); + expect(Array.isArray(results)).toBeTruthy(); + results.forEach(test); + done(); + }); + }); + }); + + it("with method in RawDataService", function (done) { + registrationPromise.then(function () { + mainService.fetchData(personBDescriptor).then(function (results) { + var test = function (person) { + expect(person.name).toBeDefined(); + expect(person.birthday).toBeDefined(); + expect(person.birthday instanceof Date).toBe(true); + expect(person.birth_date).toBeUndefined(); + }; + + expect(results).toBeDefined(); + expect(Array.isArray(results)).toBeTruthy(); + results.forEach(test); + done(); + }); + }); + }); + + it("can map raw data returned from child of rawData service", function (done) { + registrationPromise.then(function () { + mainService.fetchData(personDDescriptor).then(function (results) { + var test = function (person) { + expect(person.name).toBeDefined(); + expect(person.person_name).toBeUndefined(); + expect(person.birthday instanceof Date).toBe(true); + expect(person.birth_date).toBeUndefined(); + expect(person.employer).toBeTruthy(); + expect(person.position).toBeTruthy(); + } + + expect(results).toBeDefined(); + expect(Array.isArray(results)).toBeTruthy(); + results.forEach(test); + done(); + }); + }); + }); + }); + + + describe("can return already mapped Data", function () { + it("from rawDataService", function (done) { + registrationPromise.then(function () { + mainService.fetchData(personCDescriptor).then(function (results) { + var test = function (person) { + expect(person.name).toBeDefined(); + expect(person.person_name).toBeUndefined(); + expect(person.birthday).toBeDefined(); + expect(person.birthday instanceof Date).toBe(true); + expect(person.birth_date).toBeUndefined(); + } + + expect(results).toBeDefined(); + expect(Array.isArray(results)).toBeTruthy(); + results.forEach(test); + done(); + }); + }); + }); + }); + + + describe("can map Object to RawData", function () { + + it("can map properties to rawData with mapping in MainService", function (done) { + registrationPromise.then(function () { + var person = mainService.createDataObject(Person), + date = new Date(), + dateMS; + + date.setFullYear(1994); + date.setMonth(2); + date.setDate(7); + dateMS = date.getTime(); + person.name = "Andre The Giant"; + person.birthday = date; + mainService.saveDataObject(person).then(function (result) { + expect(result.name).toBe("Andre The Giant"); + expect(result.birth_date).toBe(dateMS); + done(); + }); + }); + }); + + it("can map properties to rawData with method in RawDataService", function (done) { + registrationPromise.then(function () { + var person = mainService.createDataObject(PersonB), + date = new Date(), + dateMS; + + date.setFullYear(1984); + date.setMonth(4); + date.setDate(10); + dateMS = date.getTime(); + person.name = "Hulk Hogan"; + person.birthday = date; + mainService.saveDataObject(person).then(function (result) { + expect(result.name).toBe("Hulk Hogan"); + expect(result.birth_date).toBe(dateMS); + done(); + }); + }); + }); + }); +}); diff --git a/test/spec/data/data-service.js b/test/spec/data/data-service.js index 8a737f2544..5bddc1a764 100644 --- a/test/spec/data/data-service.js +++ b/test/spec/data/data-service.js @@ -139,10 +139,10 @@ describe("A DataService", function() { expect(parent.childServiceForType(Types[1].TYPE)).toEqual(children[2]); expect(parent.childServiceForType(Types[2].TYPE)).toEqual(children[4]); expect(parent.childServiceForType(Types[3].TYPE)).toEqual(children[7]); - expect(parent._getChildServiceForObject(objects[0])).toEqual(children[0]); - expect(parent._getChildServiceForObject(objects[1])).toEqual(children[2]); - expect(parent._getChildServiceForObject(objects[2])).toEqual(children[4]); - expect(parent._getChildServiceForObject(objects[3])).toEqual(children[7]); + expect(parent._childServiceForObject(objects[0])).toEqual(children[0]); + expect(parent._childServiceForObject(objects[1])).toEqual(children[2]); + expect(parent._childServiceForObject(objects[2])).toEqual(children[4]); + expect(parent._childServiceForObject(objects[3])).toEqual(children[7]); // Modify the children and verify the resulting service parent, types, // and type-to-child mapping. @@ -174,10 +174,10 @@ describe("A DataService", function() { expect(parent.childServiceForType(Types[1].TYPE)).toEqual(children[2]); expect(parent.childServiceForType(Types[2].TYPE)).toEqual(children[4]); expect(parent.childServiceForType(Types[3].TYPE)).toEqual(children[7]); - expect(parent._getChildServiceForObject(objects[0])).toEqual(children[3]); - expect(parent._getChildServiceForObject(objects[1])).toEqual(children[2]); - expect(parent._getChildServiceForObject(objects[2])).toEqual(children[4]); - expect(parent._getChildServiceForObject(objects[3])).toEqual(children[7]); + expect(parent._childServiceForObject(objects[0])).toEqual(children[3]); + expect(parent._childServiceForObject(objects[1])).toEqual(children[2]); + expect(parent._childServiceForObject(objects[2])).toEqual(children[4]); + expect(parent._childServiceForObject(objects[3])).toEqual(children[7]); // Modify and verify some more. parent.removeChildService(children[3]); @@ -207,10 +207,10 @@ describe("A DataService", function() { expect(parent.childServiceForType(Types[1].TYPE)).toEqual(children[2]); expect(parent.childServiceForType(Types[2].TYPE)).toEqual(children[4]); expect(parent.childServiceForType(Types[3].TYPE)).toEqual(children[7]); - expect(parent._getChildServiceForObject(objects[0])).toEqual(children[4]); - expect(parent._getChildServiceForObject(objects[1])).toEqual(children[2]); - expect(parent._getChildServiceForObject(objects[2])).toEqual(children[4]); - expect(parent._getChildServiceForObject(objects[3])).toEqual(children[7]); + expect(parent._childServiceForObject(objects[0])).toEqual(children[4]); + expect(parent._childServiceForObject(objects[1])).toEqual(children[2]); + expect(parent._childServiceForObject(objects[2])).toEqual(children[4]); + expect(parent._childServiceForObject(objects[3])).toEqual(children[7]); // Modify and verify some more. After the modification there will be no // more children for Types[0] so the first "all types" child should be @@ -243,10 +243,10 @@ describe("A DataService", function() { expect(parent.childServiceForType(Types[1].TYPE)).toEqual(children[2]); expect(parent.childServiceForType(Types[2].TYPE)).toEqual(children[5]); expect(parent.childServiceForType(Types[3].TYPE)).toEqual(children[7]); - expect(parent._getChildServiceForObject(objects[0])).toEqual(children[7]); - expect(parent._getChildServiceForObject(objects[1])).toEqual(children[2]); - expect(parent._getChildServiceForObject(objects[2])).toEqual(children[5]); - expect(parent._getChildServiceForObject(objects[3])).toEqual(children[7]); + expect(parent._childServiceForObject(objects[0])).toEqual(children[7]); + expect(parent._childServiceForObject(objects[1])).toEqual(children[2]); + expect(parent._childServiceForObject(objects[2])).toEqual(children[5]); + expect(parent._childServiceForObject(objects[3])).toEqual(children[7]); // Modify and verify some more. parent.removeChildService(children[5]); @@ -277,10 +277,10 @@ describe("A DataService", function() { expect(parent.childServiceForType(Types[1].TYPE)).toEqual(children[2]); expect(parent.childServiceForType(Types[2].TYPE)).toEqual(children[8]); expect(parent.childServiceForType(Types[3].TYPE)).toEqual(children[8]); - expect(parent._getChildServiceForObject(objects[0])).toEqual(children[8]); - expect(parent._getChildServiceForObject(objects[1])).toEqual(children[2]); - expect(parent._getChildServiceForObject(objects[2])).toEqual(children[8]); - expect(parent._getChildServiceForObject(objects[3])).toEqual(children[8]); + expect(parent._childServiceForObject(objects[0])).toEqual(children[8]); + expect(parent._childServiceForObject(objects[1])).toEqual(children[2]); + expect(parent._childServiceForObject(objects[2])).toEqual(children[8]); + expect(parent._childServiceForObject(objects[3])).toEqual(children[8]); // Modify and verify some more. parent.removeChildService(children[2]); @@ -311,10 +311,10 @@ describe("A DataService", function() { expect(parent.childServiceForType(Types[1].TYPE)).toEqual(children[9]); expect(parent.childServiceForType(Types[2].TYPE)).toEqual(children[9]); expect(parent.childServiceForType(Types[3].TYPE)).toEqual(children[9]); - expect(parent._getChildServiceForObject(objects[0])).toEqual(children[9]); - expect(parent._getChildServiceForObject(objects[1])).toEqual(children[9]); - expect(parent._getChildServiceForObject(objects[2])).toEqual(children[9]); - expect(parent._getChildServiceForObject(objects[3])).toEqual(children[9]); + expect(parent._childServiceForObject(objects[0])).toEqual(children[9]); + expect(parent._childServiceForObject(objects[1])).toEqual(children[9]); + expect(parent._childServiceForObject(objects[2])).toEqual(children[9]); + expect(parent._childServiceForObject(objects[3])).toEqual(children[9]); // Modify and verify some more. parent.removeChildService(children[9]); @@ -344,10 +344,10 @@ describe("A DataService", function() { expect(parent.childServiceForType(Types[1].TYPE)).toBeNull(); expect(parent.childServiceForType(Types[2].TYPE)).toBeNull(); expect(parent.childServiceForType(Types[3].TYPE)).toBeNull(); - expect(parent._getChildServiceForObject(objects[0])).toBeNull(); - expect(parent._getChildServiceForObject(objects[1])).toBeNull(); - expect(parent._getChildServiceForObject(objects[2])).toBeNull(); - expect(parent._getChildServiceForObject(objects[3])).toBeNull(); + expect(parent._childServiceForObject(objects[0])).toBeNull(); + expect(parent._childServiceForObject(objects[1])).toBeNull(); + expect(parent._childServiceForObject(objects[2])).toBeNull(); + expect(parent._childServiceForObject(objects[3])).toBeNull(); }); it("can handle child services with an async types property using the register/unregister API", function (done) { diff --git a/test/spec/data/logic/model/person-b.js b/test/spec/data/logic/model/person-b.js new file mode 100644 index 0000000000..31698a5790 --- /dev/null +++ b/test/spec/data/logic/model/person-b.js @@ -0,0 +1,17 @@ +var Montage = require("montage").Montage; + +/** + * @class Person + * @extends Montage + */ +exports.PersonB = Montage.specialize({ + + name: { + value: undefined + }, + + birthday: { + value: undefined + } + +}); diff --git a/test/spec/data/logic/model/person-c.js b/test/spec/data/logic/model/person-c.js new file mode 100644 index 0000000000..2ddf74f533 --- /dev/null +++ b/test/spec/data/logic/model/person-c.js @@ -0,0 +1,17 @@ +var Montage = require("montage").Montage; + +/** + * @class Person + * @extends Montage + */ +exports.PersonC = Montage.specialize({ + + name: { + value: undefined + }, + + birthday: { + value: undefined + } + +}); diff --git a/test/spec/data/logic/model/person-d.js b/test/spec/data/logic/model/person-d.js new file mode 100644 index 0000000000..d213b437c3 --- /dev/null +++ b/test/spec/data/logic/model/person-d.js @@ -0,0 +1,25 @@ +var Montage = require("montage").Montage; + +/** + * @class PersonD + * @extends Montage + */ +exports.PersonD = Montage.specialize({ + + name: { + value: undefined + }, + + birthday: { + value: undefined + }, + + employer: { + value: undefined + }, + + position: { + value: undefined + } + +}); diff --git a/test/spec/data/logic/model/person.js b/test/spec/data/logic/model/person.js new file mode 100644 index 0000000000..f81f7c247d --- /dev/null +++ b/test/spec/data/logic/model/person.js @@ -0,0 +1,17 @@ +var Montage = require("montage").Montage; + +/** + * @class Person + * @extends Montage + */ +exports.Person = Montage.specialize({ + + name: { + value: undefined + }, + + birthday: { + value: undefined + } + +}); diff --git a/test/spec/data/logic/service/main-person-service.js b/test/spec/data/logic/service/main-person-service.js new file mode 100644 index 0000000000..87a1cd9e06 --- /dev/null +++ b/test/spec/data/logic/service/main-person-service.js @@ -0,0 +1,8 @@ +var DataService = require("montage/data/service/data-service").DataService; + + +exports.MainPersonService = DataService.specialize({ + + + +}); \ No newline at end of file diff --git a/test/spec/data/logic/service/raw-person-child-service.js b/test/spec/data/logic/service/raw-person-child-service.js new file mode 100644 index 0000000000..418bbb4b41 --- /dev/null +++ b/test/spec/data/logic/service/raw-person-child-service.js @@ -0,0 +1,19 @@ +var RawDataService = require("montage/data/service/raw-data-service").RawDataService; + +exports.RawPersonChildService = RawDataService.specialize({ + + + fetchRawData: { + value: function (stream) { + stream.addData(MOCK_DATA); + stream.dataDone(); + } + } + + +}); + +var MOCK_DATA = { + employer_name: "Kaazing", + position_name: "software engineer" +}; \ No newline at end of file diff --git a/test/spec/data/logic/service/raw-person-service-b.js b/test/spec/data/logic/service/raw-person-service-b.js new file mode 100644 index 0000000000..c4847d36cb --- /dev/null +++ b/test/spec/data/logic/service/raw-person-service-b.js @@ -0,0 +1,52 @@ +var RawDataService = require("montage/data/service/raw-data-service").RawDataService; + +exports.RawPersonServiceB = RawDataService.specialize({ + + + fetchRawData: { + value: function (stream) { + this.addRawData(stream, MOCK_DATA); + this.rawDataDone(stream); + } + }, + + mapRawDataToObject: { + value: function (rawData, object) { + object.name = rawData.name; + object.birthday = new Date(rawData.birth_date); + } + }, + + mapObjectToRawData: { + value: function (object, rawData) { + rawData.name = object.name; + rawData.birth_date = object.birthday.getTime(); + } + }, + + saveRawData: { + value: function (rawData, object) { + + return Promise.resolve(rawData); // Return mapped RawData so it can be validated in spec + } + } + +}); + +var MOCK_DATA = [ + { + name: "Robert Johnson", + birth_date: 647283226915, + lat_lng: {latitude: 39.9042, longitude: 116.4074} + }, + { + name: "Karl Towns", + birth_date: 402258116915, + lat_lng: {latitude: 40.7128, longitude: 74} + }, + { + name: "Anderson Cooper", + birth_date: 982591626915, + lat_lng: {latitude: -27.4698, longitude: 153.0251} + } +]; \ No newline at end of file diff --git a/test/spec/data/logic/service/raw-person-service-c.js b/test/spec/data/logic/service/raw-person-service-c.js new file mode 100644 index 0000000000..63aca31811 --- /dev/null +++ b/test/spec/data/logic/service/raw-person-service-c.js @@ -0,0 +1,54 @@ +var RawDataService = require("montage/data/service/raw-data-service").RawDataService, + PersonC = require("spec/data/logic/model/person-c").PersonC; + +exports.RawPersonServiceC = RawDataService.specialize({ + + + fetchRawData: { + value: function (stream) { + stream.addData(this._mockData); + stream.dataDone(); + } + }, + + _mockData: { + get: function () { + if (!this.__mockData) { + this.__mockData = MOCK_DATA.map(function (rawPerson) { + var person = new PersonC(), + date = new Date(); + person.name = rawPerson.person_name; + date.setTime(rawPerson.birth_date); + person.birthday = date; + return person; + }); + } + return this.__mockData; + } + }, + + saveDataObject: { + value: function (object) { + return this.nullPromise; + } + } + +}); + +var MOCK_DATA = [ + { + person_name: "Jared Leto", + birth_date: 647283226915, + lat_lng: {latitude: 39.9042, longitude: 116.4074} + }, + { + person_name: "Addison Reed", + birth_date: 402269116915, + lat_lng: {latitude: 40.7128, longitude: 74} + }, + { + person_name: "Wolf Blitzer", + birth_date: 982587226915, + lat_lng: {latitude: -27.4698, longitude: 153.0251} + } +]; \ No newline at end of file diff --git a/test/spec/data/logic/service/raw-person-service-d.js b/test/spec/data/logic/service/raw-person-service-d.js new file mode 100644 index 0000000000..4cf1fdf9e6 --- /dev/null +++ b/test/spec/data/logic/service/raw-person-service-d.js @@ -0,0 +1,29 @@ +var RawDataService = require("montage/data/service/raw-data-service").RawDataService; + +exports.RawPersonServiceD = RawDataService.specialize({ + + + fetchRawData: { + value: function (stream) { + this.addRawData(stream, MOCK_DATA); + this.rawDataDone(stream); + } + } + + +}); + +var MOCK_DATA = [ + { + name: "Jon Favreau", + birth_date: 647203221465 + }, + { + name: "Jerry Garcia", + birth_date: 401259226915 + }, + { + name: "Bryan Williams", + birth_date: 982311626915 + } +]; \ No newline at end of file diff --git a/test/spec/data/logic/service/raw-person-service.js b/test/spec/data/logic/service/raw-person-service.js new file mode 100644 index 0000000000..0c4caf168a --- /dev/null +++ b/test/spec/data/logic/service/raw-person-service.js @@ -0,0 +1,39 @@ +var RawDataService = require("montage/data/service/raw-data-service").RawDataService, + Person = require("spec/data/logic/model/person").Person; + +exports.RawPersonService = RawDataService.specialize({ + + + fetchRawData: { + value: function (stream) { + this.addRawData(stream, MOCK_DATA); + this.rawDataDone(stream); + } + }, + + saveRawData: { + value: function (rawData, object) { + return Promise.resolve(rawData); // Return mapped RawData so it can be validated in spec + } + } + + +}); + +var MOCK_DATA = [ + { + name: "Phil Smith", + birth_date: 647203226915, + lat_lng: {latitude: 39.9042, longitude: 116.4074} + }, + { + name: "Jerry Garcia", + birth_date: 402259226915, + lat_lng: {latitude: 40.7128, longitude: 74} + }, + { + name: "Bryan Williams", + birth_date: 982611626915, + lat_lng: {latitude: -27.4698, longitude: 153.0251} + } +]; \ No newline at end of file diff --git a/test/spec/data/raw-data-service.js b/test/spec/data/raw-data-service.js index 9bb9a3a237..f32b1ed8db 100644 --- a/test/spec/data/raw-data-service.js +++ b/test/spec/data/raw-data-service.js @@ -141,10 +141,10 @@ describe("A RawDataService", function() { expect(parent.childServiceForType(Types[1].TYPE)).toEqual(children[2]); expect(parent.childServiceForType(Types[2].TYPE)).toEqual(children[4]); expect(parent.childServiceForType(Types[3].TYPE)).toEqual(children[7]); - expect(parent._getChildServiceForObject(objects[0])).toEqual(children[0]); - expect(parent._getChildServiceForObject(objects[1])).toEqual(children[2]); - expect(parent._getChildServiceForObject(objects[2])).toEqual(children[4]); - expect(parent._getChildServiceForObject(objects[3])).toEqual(children[7]); + expect(parent._childServiceForObject(objects[0])).toEqual(children[0]); + expect(parent._childServiceForObject(objects[1])).toEqual(children[2]); + expect(parent._childServiceForObject(objects[2])).toEqual(children[4]); + expect(parent._childServiceForObject(objects[3])).toEqual(children[7]); // Modify the children and verify the resulting service parent, types, // and type-to-child mapping. @@ -174,10 +174,10 @@ describe("A RawDataService", function() { expect(parent.childServiceForType(Types[1].TYPE)).toEqual(children[2]); expect(parent.childServiceForType(Types[2].TYPE)).toEqual(children[4]); expect(parent.childServiceForType(Types[3].TYPE)).toEqual(children[7]); - expect(parent._getChildServiceForObject(objects[0])).toEqual(children[3]); - expect(parent._getChildServiceForObject(objects[1])).toEqual(children[2]); - expect(parent._getChildServiceForObject(objects[2])).toEqual(children[4]); - expect(parent._getChildServiceForObject(objects[3])).toEqual(children[7]); + expect(parent._childServiceForObject(objects[0])).toEqual(children[3]); + expect(parent._childServiceForObject(objects[1])).toEqual(children[2]); + expect(parent._childServiceForObject(objects[2])).toEqual(children[4]); + expect(parent._childServiceForObject(objects[3])).toEqual(children[7]); // Modify and verify some more. parent.removeChildService(children[3]); @@ -204,10 +204,10 @@ describe("A RawDataService", function() { expect(parent.childServiceForType(Types[1].TYPE)).toEqual(children[2]); expect(parent.childServiceForType(Types[2].TYPE)).toEqual(children[4]); expect(parent.childServiceForType(Types[3].TYPE)).toEqual(children[7]); - expect(parent._getChildServiceForObject(objects[0])).toEqual(children[4]); - expect(parent._getChildServiceForObject(objects[1])).toEqual(children[2]); - expect(parent._getChildServiceForObject(objects[2])).toEqual(children[4]); - expect(parent._getChildServiceForObject(objects[3])).toEqual(children[7]); + expect(parent._childServiceForObject(objects[0])).toEqual(children[4]); + expect(parent._childServiceForObject(objects[1])).toEqual(children[2]); + expect(parent._childServiceForObject(objects[2])).toEqual(children[4]); + expect(parent._childServiceForObject(objects[3])).toEqual(children[7]); // Modify and verify some more. After the modification there will be no // more children for Types[0] so the first "all types" child should be @@ -235,10 +235,10 @@ describe("A RawDataService", function() { expect(parent.childServiceForType(Types[1].TYPE)).toEqual(children[2]); expect(parent.childServiceForType(Types[2].TYPE)).toEqual(children[5]); expect(parent.childServiceForType(Types[3].TYPE)).toEqual(children[7]); - expect(parent._getChildServiceForObject(objects[0])).toEqual(children[7]); - expect(parent._getChildServiceForObject(objects[1])).toEqual(children[2]); - expect(parent._getChildServiceForObject(objects[2])).toEqual(children[5]); - expect(parent._getChildServiceForObject(objects[3])).toEqual(children[7]); + expect(parent._childServiceForObject(objects[0])).toEqual(children[7]); + expect(parent._childServiceForObject(objects[1])).toEqual(children[2]); + expect(parent._childServiceForObject(objects[2])).toEqual(children[5]); + expect(parent._childServiceForObject(objects[3])).toEqual(children[7]); // Modify and verify some more. parent.removeChildService(children[5]); @@ -262,10 +262,10 @@ describe("A RawDataService", function() { expect(parent.childServiceForType(Types[1].TYPE)).toEqual(children[2]); expect(parent.childServiceForType(Types[2].TYPE)).toEqual(children[8]); expect(parent.childServiceForType(Types[3].TYPE)).toEqual(children[8]); - expect(parent._getChildServiceForObject(objects[0])).toEqual(children[8]); - expect(parent._getChildServiceForObject(objects[1])).toEqual(children[2]); - expect(parent._getChildServiceForObject(objects[2])).toEqual(children[8]); - expect(parent._getChildServiceForObject(objects[3])).toEqual(children[8]); + expect(parent._childServiceForObject(objects[0])).toEqual(children[8]); + expect(parent._childServiceForObject(objects[1])).toEqual(children[2]); + expect(parent._childServiceForObject(objects[2])).toEqual(children[8]); + expect(parent._childServiceForObject(objects[3])).toEqual(children[8]); // Modify and verify some more. parent.removeChildService(children[2]); @@ -287,10 +287,10 @@ describe("A RawDataService", function() { expect(parent.childServiceForType(Types[1].TYPE)).toEqual(children[9]); expect(parent.childServiceForType(Types[2].TYPE)).toEqual(children[9]); expect(parent.childServiceForType(Types[3].TYPE)).toEqual(children[9]); - expect(parent._getChildServiceForObject(objects[0])).toEqual(children[9]); - expect(parent._getChildServiceForObject(objects[1])).toEqual(children[9]); - expect(parent._getChildServiceForObject(objects[2])).toEqual(children[9]); - expect(parent._getChildServiceForObject(objects[3])).toEqual(children[9]); + expect(parent._childServiceForObject(objects[0])).toEqual(children[9]); + expect(parent._childServiceForObject(objects[1])).toEqual(children[9]); + expect(parent._childServiceForObject(objects[2])).toEqual(children[9]); + expect(parent._childServiceForObject(objects[3])).toEqual(children[9]); // Modify and verify some more. parent.removeChildService(children[9]); @@ -310,41 +310,41 @@ describe("A RawDataService", function() { expect(parent.childServiceForType(Types[1].TYPE)).toBeNull(); expect(parent.childServiceForType(Types[2].TYPE)).toBeNull(); expect(parent.childServiceForType(Types[3].TYPE)).toBeNull(); - expect(parent._getChildServiceForObject(objects[0])).toBeNull(); - expect(parent._getChildServiceForObject(objects[1])).toBeNull(); - expect(parent._getChildServiceForObject(objects[2])).toBeNull(); - expect(parent._getChildServiceForObject(objects[3])).toBeNull(); + expect(parent._childServiceForObject(objects[0])).toBeNull(); + expect(parent._childServiceForObject(objects[1])).toBeNull(); + expect(parent._childServiceForObject(objects[2])).toBeNull(); + expect(parent._childServiceForObject(objects[3])).toBeNull(); }); - // it("manages type mappings correcty", function () { - // var service = new RawDataService(), - // parentDescriptor = new ObjectDescriptor(), - // subDescriptorA = new ObjectDescriptor(), - // subDescriptorB = new ObjectDescriptor(), - // criteriaA = new Criteria().initWithExpression("type == $paramType", { - // paramType: "type_a" - // }), - // criteriaB = new Criteria().initWithExpression("type == $paramType", { - // paramType: "type_b" - // }), - // mappingA = RawDataTypeMapping.withTypeAndCriteria(subDescriptorA, criteriaA), - // mappingB = RawDataTypeMapping.withTypeAndCriteria(subDescriptorB, criteriaB), - // rawA = {type: "type_a"}, - // rawB = {type: "type_b"}, - // rawC = {type: "type_c"}; - - // subDescriptorB.parent = parentDescriptor; - // subDescriptorA.parent = parentDescriptor; + it("manages type mappings correcty", function () { + var service = new RawDataService(), + parentDescriptor = new ObjectDescriptor(), + subDescriptorA = new ObjectDescriptor(), + subDescriptorB = new ObjectDescriptor(), + criteriaA = new Criteria().initWithExpression("type == $paramType", { + paramType: "type_a" + }), + criteriaB = new Criteria().initWithExpression("type == $paramType", { + paramType: "type_b" + }), + mappingA = RawDataTypeMapping.withTypeAndCriteria(subDescriptorA, criteriaA), + mappingB = RawDataTypeMapping.withTypeAndCriteria(subDescriptorB, criteriaB), + rawA = {type: "type_a"}, + rawB = {type: "type_b"}, + rawC = {type: "type_c"}; + + subDescriptorB.parent = parentDescriptor; + subDescriptorA.parent = parentDescriptor; - // service._registerRawDataTypeMappings([mappingA, mappingB]); - // expect(service._descriptorForParentAndRawData(parentDescriptor, rawA)).toBe(subDescriptorA); - // expect(service._descriptorForParentAndRawData(parentDescriptor, rawB)).toBe(subDescriptorB); - // expect(service._descriptorForParentAndRawData(parentDescriptor, rawC)).toBe(parentDescriptor); + service._registerRawDataTypeMappings([mappingA, mappingB]); + expect(service._descriptorForParentAndRawData(parentDescriptor, rawA)).toBe(subDescriptorA); + expect(service._descriptorForParentAndRawData(parentDescriptor, rawB)).toBe(subDescriptorB); + expect(service._descriptorForParentAndRawData(parentDescriptor, rawC)).toBe(parentDescriptor); - // }); + }); it("traverses inheritance chain with type mappings", function () { diff --git a/ui/slider.reel/slider.js b/ui/slider.reel/slider.js index ec8edb7dba..eae6c7fc24 100644 --- a/ui/slider.reel/slider.js +++ b/ui/slider.reel/slider.js @@ -830,7 +830,7 @@ Should introduce a validate method /** Description TODO @function - @param {Event Handler} event TODO + @param {EventHandler} event TODO */ handleChange: { enumerable: false,