diff --git a/core/converter/pipeline-converter.js b/core/converter/pipeline-converter.js index ec59dc0e0d..222a4b321b 100644 --- a/core/converter/pipeline-converter.js +++ b/core/converter/pipeline-converter.js @@ -1,7 +1,13 @@ var Converter = require("./converter").Converter, Promise = require("core/promise").Promise; - +/** + * Converter that chains a series of converters together + * + * + * @class PipelineConverter + * @extends Converter + */ exports.PipelineConverter = Converter.specialize({ @@ -31,10 +37,15 @@ exports.PipelineConverter = Converter.specialize({ * Convert/Revert */ + /** + * The converters to chain on convert() + * @type {Converter[]} + */ converters: { value: undefined }, + convert: { value: function (value) { return this._convertWithConverterAtIndex(value, 0); diff --git a/core/event/event-manager.js b/core/event/event-manager.js index c46f817707..0e100a505e 100644 --- a/core/event/event-manager.js +++ b/core/event/event-manager.js @@ -49,7 +49,7 @@ if ( if ( typeof document !== "undefined" && typeof window !== "undefined" && - typeof Touch === "undefined" && + typeof Touch === "undefined" && "ontouchstart" in window ) { Touch = function Touch() {}; // jshint ignore:line @@ -299,13 +299,13 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan spliceOne: { value: function spliceOne(arr, index) { var len = arr.length; - if (len) { + if (len) { while (index < len) { arr[index] = arr[index + 1]; index++; } arr.length--; - } + } } }, /** @@ -1073,8 +1073,8 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan value: function unregisterEventListener(target, eventType, listener, useCapture) { //console.log("EventManager.unregisterEventListener", target, eventType, listener, useCapture); - return useCapture ? - this._unregisterEventListener(target, eventType, listener, this._registeredCaptureEventListeners, this._registeredBubbleEventListeners) : + return useCapture ? + this._unregisterEventListener(target, eventType, listener, this._registeredCaptureEventListeners, this._registeredBubbleEventListeners) : this._unregisterEventListener(target, eventType, listener, this._registeredBubbleEventListeners, this._registeredCaptureEventListeners); } }, @@ -1476,7 +1476,7 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan eventTypeBucket = this._catptureMethodNameByEventTypeIdentifier_.get(eventType) || (this._catptureMethodNameByEventTypeIdentifier_.set(eventType,new Map())).get(eventType); return eventTypeBucket.get(identifier) || (eventTypeBucket.set(identifier,("capture" + (capitalizedIdentifier || identifier.toCapitalized()) + (capitalizedEventType || eventType.toCapitalized())))).get(identifier); } else { - return this._captureMethodNameByEventType_.get(eventType) || + return this._captureMethodNameByEventType_.get(eventType) || (this._captureMethodNameByEventType_.set(eventType, ("capture" + (capitalizedEventType || eventType.toCapitalized())))).get(eventType); } } @@ -2431,7 +2431,7 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan // Event Handling /** - * @property + * @property {Array} * @description the pointer to the current candidate event listeners. * * @private diff --git a/core/meta/property-descriptor.js b/core/meta/property-descriptor.js index 9bcd8ef4a1..4fae287fdc 100644 --- a/core/meta/property-descriptor.js +++ b/core/meta/property-descriptor.js @@ -125,6 +125,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# if (value !== void 0) { this._owner = value; } + this._overridePropertyWithDefaults(deserializer, "cardinality"); if (this.cardinality === -1) { @@ -200,7 +201,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# }, _owner: { - value:null + value: null }, /** @@ -213,7 +214,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# }, _name: { - value:null + value: null }, /** @@ -310,7 +311,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# * @type {string} * Definition can be used to express a property as the result of evaluating an expression * An example would be to flatten/traverse two properties across two objects to make its - * content accessible as a new property name. For example, in a many to many relaational + * content accessible as a new property name. For example, in a many to many relational * style, a Movie would have a toDirector property to a "DirectorRole" which itself would * point through a toTalent property to the actual Person. A "director" property definition * would then be "toDirector.toTalent" diff --git a/core/serialization/deserializer/montage-interpreter.js b/core/serialization/deserializer/montage-interpreter.js index 5f8bdde5d5..1406a91572 100644 --- a/core/serialization/deserializer/montage-interpreter.js +++ b/core/serialization/deserializer/montage-interpreter.js @@ -130,6 +130,8 @@ var MontageContext = Montage.specialize({ objects = this._objects, object; + + if (label in objects) { return objects[label]; } else if (label in serialization) { diff --git a/core/serialization/deserializer/self-deserializer.js b/core/serialization/deserializer/self-deserializer.js index 6f57fb6f67..59bb87939b 100644 --- a/core/serialization/deserializer/self-deserializer.js +++ b/core/serialization/deserializer/self-deserializer.js @@ -53,6 +53,8 @@ var SelfDeserializer = Montage.specialize( { getObjectByLabel: { value: function (label) { + + return this._context.getObject(label); } }, diff --git a/data/converter/raw-property-value-to-object-converter.js b/data/converter/raw-property-value-to-object-converter.js new file mode 100644 index 0000000000..cb81561e52 --- /dev/null +++ b/data/converter/raw-property-value-to-object-converter.js @@ -0,0 +1,338 @@ +var Converter = require("core/converter/converter").Converter, + Criteria = require("core/criteria").Criteria, + DataQuery = require("data/model/data-query").DataQuery, + ObjectDescriptorReference = require("core/meta/object-descriptor-reference").ObjectDescriptorReference, + Promise = require("core/promise").Promise, + Scope = require("frb/scope"), + parse = require("frb/parse"); + +/** + * @class RawPropertyValueToObjectConverter + * @classdesc Converts a property value of raw data to the referenced object. + * @extends Converter + */ +exports.RawPropertyValueToObjectConverter = Converter.specialize( /** @lends RawPropertyValueToObjectConverter# */ { + + + /********************************************************************* + * Serialization + */ + + serializeSelf: { + value: function (serializer) { + + serializer.setProperty("convertExpression", this.convertExpression); + + serializer.setProperty("foreignDescriptor", this._foreignDescriptorReference); + + serializer.setProperty("revertExpression", this.revertExpression); + + serializer.setProperty("root", this.owner); + + serializer.setProperty("serviceIdentifier", this.serviceIdentifier); + serializer.setProperty("service", this.service); + + } + }, + + deserializeSelf: { + value: function (deserializer) { + var value = deserializer.getProperty("convertExpression"); + if (value) { + this.convertExpression = value; + } + + value = deserializer.getProperty("revertExpression"); + if (value) { + this.revertExpression = value; + } + + value = deserializer.getProperty("foreignDescriptor"); + if (value) { + this._foreignDescriptorReference = value; + } + + value = deserializer.getProperty("service"); + if (value) { + this.service = value; + } + + value = deserializer.getObjectByLabel("root"); + if (value) { + this.owner = value; + } + + value = deserializer.getProperty("serviceIdentifier"); + if (value) { + this.serviceIdentifier = value; + } + + deserializer.deserializeUnit("bindings"); + } + }, + + /********************************************************************* + * Initialization + */ + + /** + * @param {string} convertExpression the expression to be used for building a criteria to obtain the object corresponding to the value to convert. + * @return itself + */ + initWithConvertExpression: { + value: function (convertExpression) { + this.convertExpression = convertExpression; + return this; + } + }, + + /********************************************************************* + * Properties + */ + + + _convertExpression: { + value: null + }, + + /** + * The expression used to convert a raw value into a modeled one, for example a foreign property value into the objet it represents. + * @type {string} + * */ + convertExpression: { + get: function() { + return this._convertExpression; + }, + set: function(value) { + if(value !== this._convertExpression) { + this._convertExpression = value; + this._convertSyntax = undefined; + } + } + }, + + _convertSyntax: { + value: undefined + }, + + /** + * Object created by parsing .convertExpression using frb/grammar.js that will + * be used to initialize the convert query criteria + * @type {Object} + * */ + + convertSyntax: { + get: function() { + return this._convertSyntax || (this._convertSyntax = parse(this.convertExpression)); + } + }, + + + + + _revertExpression: { + value: null + }, + + /** + * The expression used to revert the modeled value into a raw one. For example, + * reverting an object into it's primary key. + * @type {string} + * */ + revertExpression: { + get: function() { + return this._revertExpression; + }, + set: function(value) { + if(value !== this._revertExpression) { + this._revertExpression = value; + this._revertSyntax = undefined; + } + } + }, + + _revertSyntax: { + value: undefined + }, + + /** + * Object created by parsing .revertExpression using frb/grammar.js that will + * be used to revert the modeled value into a raw one + * @type {Object} + * */ + revertSyntax: { + get: function() { + return this._revertSyntax || (this._revertSyntax = parse(this.revertExpression)); + } + }, + + + /** + * The descriptor of the destination object. If one is not provided, + * .objectDescriptor will be used. If .objectDescriptor is not provided, + * the value descriptor of the property descriptor that defines the + * relationship will be used. + * @type {?ObjectDescriptorReference} + * */ + foreignDescriptor: { + serializable: false, + get: function () { + return this._foreignDescriptorReference && this._foreignDescriptorReference.promise(require); + }, + set: function (descriptor) { + this._foreignDescriptorReference = new ObjectDescriptorReference().initWithValue(descriptor); + } + }, + + /** + * The descriptor of the source object. It will be used only if it is provided and + * .foreignDescriptor is not provided. + * @type {?ObjectDescriptorReference} + **/ + objectDescriptor: { + get: function () { + return this._objectDescriptor ? Promise.resolve(this._objectDescriptor) : + this.owner && this.owner.objectDescriptor ? Promise.resolve(this.owner.objectDescriptor) : + this.owner && this.owner instanceof Promise ? this._objectDescriptorReference : + undefined; + }, + set: function (value) { + this._objectDescriptor = value; + } + }, + + _objectDescriptorReference: { + get: function () { + var self = this; + return this.owner.then(function (object) { + var objectDescriptor = object.objectDescriptor; + self.objectDescriptor = objectDescriptor; + return objectDescriptor; + }); + } + }, + + + /** + * The descriptor for which to perform the fetch. + * This returns foreignDescriptor, if it exists, and otherwise + * returns objectDescriptor. + * @type {?ObjectDescriptorReference} + **/ + _descriptorToFetch: { + get: function () { + if (!this.__descriptorToFetch) { + var self = this; + this.__descriptorToFetch = this.foreignDescriptor ? this.foreignDescriptor.then(function (descriptor) { + return descriptor || self.objectDescriptor; + }) : Promise.resolve(this.objectDescriptor); + } + return this.__descriptorToFetch; + } + }, + + + owner: { + get: function () { + return this._owner ? this._owner.then ? this._owner : Promise.resolve(this._owner) : undefined; + }, + set: function (value) { + this._owner = value; + } + }, + + __scope: { + value: null + }, + + /** + * Scope with which convert and revert expressions are evaluated. + * @type {?Scope} + **/ + scope: { + get: function() { + return this.__scope || (this.__scope = new Scope(this)); + } + }, + + /** + * The service to use to make requests. + */ + service: { + get: function () { + return this._service ? this._service : + this.owner ? this.owner.then(function (object) { return object.service; }) : + undefined; + }, + set: function (value) { + this._service = !value || value.then ? value : Promise.resolve(value); + } + }, + + /** + * Identifier of the child of .service that the query should be routed to + */ + serviceIdentifier: { + value: undefined + }, + + /********************************************************************* + * Public API + */ + + /** + * Converts the fault for the relationship to an actual object that has an ObjectDescriptor. + * @function + * @param {Property} v The value to format. + * @returns {Promise} A promise for the referenced object. The promise is + * fulfilled after the object is successfully fetched. + */ + convert: { + value: function (v) { + var self = this, + criteria = new Criteria().initWithSyntax(self.convertSyntax, v), + query; + + return this._descriptorToFetch.then(function (typeToFetch) { + var type = [typeToFetch.module.id, typeToFetch.name].join("/"); + + if (self.serviceIdentifier) { + criteria.parameters.serviceIdentifier = self.serviceIdentifier; + } + + query = DataQuery.withTypeAndCriteria(type, criteria); + + return self.service ? self.service.then(function (service) { + return service.rootService.fetchData(query); + }) : null; + }); + } + }, + + + + /** + * Reverts the relationship back to raw data. + * @function + * @param {Scope} v The value to revert. + * @returns {string} v + */ + revert: { + value: function (v) { + if (v) { + if (!this._revertSyntax) { + return Promise.resolve(v); + } else { + var scope = this.scope; + //Parameter is what is accessed as $ in expressions + scope.parameters = v; + return Promise.resolve(this._revertSyntax(scope)); + } + + } + return Promise.resolve(undefined); + } + } + +}); + diff --git a/data/model/data-identifier.js b/data/model/data-identifier.js new file mode 100644 index 0000000000..57da2b1c28 --- /dev/null +++ b/data/model/data-identifier.js @@ -0,0 +1,117 @@ +var Montage = require("core/core").Montage; + +/** + * A DataIdentifier represents a universal identifier for an object managed by + * Montage Data. It provides the support for uniquing in a DataService. + * Whether an object exists in one or more local DataServices in an Application + * or in a remote one, a DataIdentifier encapsulates the information needed to + * uniquely identify an object, like a primary key in a database. A DataIdentifier + * has a URL representation, which is conceptually aligned with the notion of + * resource. It should have: + * + * - a host/source/origin: where the data come from. Automatically generated + * primary keys exists in only one environment - Dev, test, prod, etc..., + * (a user's authorization (if any necessary) should be left to be resolved + * by a client receiving the identifier, only people authenticated and authorized + * would be able to get it and that happens at DataService level) + * + * - a type + * + * - a primary key. This could be a combination of property/value, but it needs + * to be serializable as a valid url + * + * Exact details are not exposed and may vary per specific DataService or RawDataService + * + * @class + * @extends external:Montage + */ +exports.DataIdentifier = Montage.specialize(/** @lends DataIdentifier.prototype */ { + + /** + * The DataService that created this DataIdentifier + * + * @type {DataService} + */ + dataService: { + value: undefined + }, + + /** + * The ObjectDescriptor associated with a dataIdentifier if available + * + * @type {ObjectDescriptor} + */ + objectDescriptor: { + value: undefined + }, + + /** + * The primaryKey of the object the dataIdentifier represents + * + * @type {Object} + */ + primaryKey: { + value: undefined + }, + + /** + * The primaryKey of the object the dataIdentifier represents + * + * @type {String} + */ + _typeName:{ + value: undefined + }, + typeName: { + get: function() { + return this._typeName || (this._typeName = this.objectDescriptor ? this.objectDescriptor.name : "MISSING_TYPE_NAME"); + }, + set: function(value) { + this._typeName = value; + } + }, + + /** + * Whether a DataIdentifier is persistent/final vs temporary when created + * client side. + * + * @type {boolean} + */ + isPersistent: { + value: false + }, + + _identifier: { + value: false + }, + + _url: { + value: undefined + }, + + /** + * The url representation of a dataIdentifier + * + * @type {string} + */ + url: { + get: function () { + if(!this._url) { + var _url = "montage-data://"; + _url += this.dataService.identifier; + _url += "/"; + _url += this.dataService.connectionDescriptor ? this.dataService.connectionDescriptor.name : "default"; + _url += "/"; + _url += this.objectDescriptor.name; + _url += "/"; + _url += this.primaryKey; + this._url = _url; + } + return this._url; + }, + set: function (value) { + return (this._url = value); + } + } + +}); diff --git a/data/model/data-object-descriptor.js b/data/model/data-object-descriptor.js new file mode 100644 index 0000000000..565b765f39 --- /dev/null +++ b/data/model/data-object-descriptor.js @@ -0,0 +1,243 @@ +var ObjectDescriptor = require("./object-descriptor").ObjectDescriptor, + DataPropertyDescriptor = require("./data-property-descriptor").DataPropertyDescriptor; + +/** + * Extends an object descriptor with the additional object information needed by + * Montage Data. + * + * @deprecated + * @class + * @extends ObjectDescriptor + */ +exports.DataObjectDescriptor = ObjectDescriptor.specialize(/** @lends DataObjectDescriptor.prototype */ { + + /** + * The names of the properties containing the identifier for this type of + * data object. + * + * @type {Array.} + */ + identifierNames: { + value: [] + }, + + /** + * Descriptors of the properties of data objects of this type, by property + * name. + * + * The returned map should not be modified. + * [setPropertyDescriptor()]{@link DataObjectDescriptor#setPropertyDescriptor} + * and + * [deletePropertyDescriptor()]{@link ObjectDescriptor#deletePropertyDescriptor} + * should be used instead to modify the property descriptors. + * + * @type {Object.} + */ + propertyDescriptors: { + get: function () { + // This returns the same value as the superclass property of the + // same name, only the JSDoc type is different. + return Object.getOwnPropertyDescriptor(ObjectDescriptor.prototype, "propertyDescriptors").get.call(this); + } + }, + + /** + * Add or replace a property descriptor. + * + * @method + * @argument {string} name - The name of the property. + * @argument {DataPropertyDescriptor} descriptor - Describes the property. + */ + setPropertyDescriptor: { + // This does the same thing as the superclass method of the same name, + // only the JSDoc type is different. + value: function (name, descriptor) { + ObjectDescriptor.prototype.setPropertyDescriptor.call(this, name, descriptor); + } + }, + + /** + * Create a property descriptor. + * + * This overrides the + * [superclass implementation]{@link PropertyDescriptor#makePropertyDescriptor} + * to create a {@link DataPropertyDescriptor} instead of a + * {@link PropertyDescriptor} instance. + * + * @method + * @returns {DataPropertyDescriptor} - The created property descriptor. + */ + makePropertyDescriptor: { + value: function () { + return new DataPropertyDescriptor(); + } + }, + + /** + * @private + * @method + */ + _addRelationships: { + value: function (relationships) { + var names, i, n; + if (relationships) { + names = Object.keys(relationships); + for (i = 0, n = names.length; i < n; i += 1) { + this._addRelationship(names[i], relationships[names[i]]); + } + } + } + }, + + /** + * @private + * @method + */ + _addRelationship: { + value: function (name, relationship) { + // TODO: Add derived properties, relationship criteria, + // relationship targets, and shared fetch information. + if (!this.propertyDescriptors[name]) { + this.setPropertyDescriptor(name, this.makePropertyDescriptor()); + } + this.propertyDescriptors[name].isRelationship = true; + this.propertyDescriptors[name].isGlobal = relationship.isGlobal ? true : false; + } + } + +}, /** @lends DataObjectDescriptor */ { + + /** + * Generates a getter function that will create and cache a data object + * descriptor. + * + * @method + * @argument {Object} exports - A + * [Montage Require]{@link external:Require} + * exports object defining the + * constructor for the object to + * describe. Usually this is + * `exports`. + * @argument {string} constructorName - The name with which that + * constructor can be looked up in + * the exports. This will also be + * used as the created descriptor's + * type name. + * @argument {Object} + * [propertyTypes] - The types of each of the + * described objects' properties, + * by property name. Optional + * except if identifier names or + * relationship information are to + * be specified. Pass in a null + * or undefined value to specify + * identifier names or relationship + * information but no property + * types. If no types array is + * specified the property + * information will be derived from + * the properties of the objects' + * prototype. If an empty types + * array is specified the objects + * will be assumed to have no + * properties. + * @argument {Array.} + * [identifierNames=[]] - The names of the properties of + * described objects whose values, + * when taken together, define + * a unique identifier for each + * such object. If no names are + * specified those objects are + * assumed to not be uniquely + * identifiable. The names can be + * specified as an array or as a + * sequence of strings. + * @argument {Object} + * [relationshipInformation={}] - Information about each of the + * objects' relationships, by + * relationship property name. If + * no information is provided the + * objects are assumed to have no + * relationships. + */ + getterFor: { + value: function (exports, constructorName, propertyTypes, identifierNames, relationshipInformation) { + // The returned getter function has to check + // `this.hasOwnProperty("_TYPE")`, not just `this._TYPE`, because if + // the class using the getter is a subclass of another class using a + // similar getter `this._TYPE` will return the value of the the + // superclass type instead of the desired subclass type. + var self = this, + parsed = this._parseGetterForArguments.apply(this, arguments), + getter = ObjectDescriptor.getterFor.call(this, parsed.exports, parsed.name, parsed.types); + return function () { + + if (!this.hasOwnProperty("_TYPE")) { + this._TYPE = getter.call(this); + this._TYPE.identifierNames = parsed.identifiers; + this._TYPE._addRelationships(parsed.relationships); + } + + return this._TYPE; + }; + } + }, + + /** + * @private + * @method + */ + _parseGetterForArguments: { + value: function (/* exports, constructorName, [propertyTypes,] identifierNames[, relationshipInformation] + exports, constructorName, [propertyTypes,] relationshipInformation + exports, constructorName */) { + var parsed, index, i, n; + // Parse the exports object and constructor name. + parsed = {exports: arguments[0], name: arguments[1]}; + // Parse the property types if they are provided: They must be a + // "real" object (non-array, non-string, non-numeric, and + // non-boolean), and they can't be the last argument (they must + // be followed by either an identifier name or a relationship + // information argument). After this parsing "index" will point to + // the next argument to parse. + if (arguments.length > 3 && this._isRealObject(arguments[2])) { + parsed.types = arguments[2]; + index = 3; + } else { + index = 2; + } + // Parse the identifier names: If provided these will be in an array + // or in a sequence of string. After this parsing "index" will point + // to the next argument to parse. + if (Array.isArray(arguments[index])) { + parsed.identifiers = arguments[index]; + index += 1; + } else { + for (i = index, n = arguments.length; i < n && typeof arguments[i] === "string"; i += 1) { + parsed.identifiers = Array.prototype.slice.call(arguments, index, i); + } + index = i; + } + // Parse the relationship information. + parsed.relationships = arguments[index]; + // Return the parsed arguments. + return parsed; + } + }, + + /** + * @private + * @method + */ + _isRealObject: { + value: function (value) { + return value && + typeof value === "object" && + !Array.isArray(value) && + !(value instanceof String) && + !(value instanceof Number) && + !(value instanceof Boolean); + } + } + +}); diff --git a/data/model/data-ordering.js b/data/model/data-ordering.js new file mode 100644 index 0000000000..d3f6b5c1e3 --- /dev/null +++ b/data/model/data-ordering.js @@ -0,0 +1,74 @@ +var Montage = require("core/core").Montage, + ASCENDING = {name: "Ascending"}, + DESCENDING = {name: "Descending"}; + // parse = require("frb/parse"), + // compile = require("frb/compile-evaluator"), + // evaluate = require("frb/evaluate"), + // Scope = require("frb/scope"); + +/* + * var syntax = parse("a.b"); + * var array = [ + * {foo: "A", bar: "2"}, {foo: "A", bar: "1"}, {foo: "C", bar: "5"}, + * {foo: "D", bar: "3"}, {foo: "B", bar: "2"}, {foo: "B", bar: "4"}, + * {foo: "F", bar: "1"}, {foo: "G", bar: "2"}, {foo: "E", bar: "4"} + * ]; + * var sortExpression = "foo"; + * var evaluatedSortExpression = compile(parse("sorted{foo}")); + * var evaluatedDoubleSortExpression = compile(parse("sorted{foo+bar}")); + * var evaluatedInvertedSortExpression = compile(parse("sorted{foo}.reversed()")); + * var evaluatedSyntax = compile(syntax); + * var c = evaluatedSyntax(new Scope({a: {b: 10}})); + * var sortedArray = evaluatedSortExpression(new Scope(array)); + * var inverseSortedArray = evaluatedInvertedSortExpression(new Scope(array)); + * var doubleSortedArray = evaluatedDoubleSortExpression(new Scope(array)); + */ + +/** + * @class + * @extends external:Montage + */ +exports.DataOrdering = Montage.specialize(/** @lends DataOrdering.prototype */ { + + /** + * An expression to be applied to objects in a set to yield a value + * according to which those objects will be sorted. + * + * @type {String} + */ + expression: { + value: undefined + }, + + /** + * Whether objects to be sorted will be sorted with the + * [expression's]{@link DataOrdering#expression} value + * [ascending]{@link DataOrdering.ASCENDING} or + * [descending]{@link DataOrdering.DESCENDING}. + * + * @type {String} + */ + order: { + value: ASCENDING + } + +}, { + + withExpressionAndOrder: { + value: function (expression, order) { + var ordering = new this(); + ordering.expression = expression; + ordering.order = order; + return ordering; + } + }, + + ASCENDING: { + value: ASCENDING + }, + + DESCENDING: { + value: DESCENDING + } + +}); diff --git a/data/model/data-property-descriptor.js b/data/model/data-property-descriptor.js new file mode 100644 index 0000000000..82916fa5a7 --- /dev/null +++ b/data/model/data-property-descriptor.js @@ -0,0 +1,23 @@ +var PropertyDescriptor = require("data/model/property-descriptor").PropertyDescriptor; + +/** + * Extend {@link PropertyDescriptor} to describes a property of + * [data objects]{@link DataObjectDescriptor} of a certain type. + * + * @deprecated + * @class + * @extends PropertyDescriptor + */ +exports.DataPropertyDescriptor = PropertyDescriptor.specialize(/** @lends DataPropertyDescriptor.prototype */ { + + // TODO: Add support for derived properties, relationship criteria, + // relationship targets, and shared fetches. + + /** + * @type {boolean} + */ + isGlobal: { + value: false + } + +}); diff --git a/data/model/data-query.js b/data/model/data-query.js new file mode 100644 index 0000000000..4038aea05e --- /dev/null +++ b/data/model/data-query.js @@ -0,0 +1,155 @@ +var Montage = require("montage").Montage; + +/** + * Defines the criteria that objects must satisfy to be included in a set of + * data as well as other characteristics that data must possess. + * + * @class + * @extends external:Montage + */ +exports.DataQuery = Montage.specialize(/** @lends DataQuery.prototype */ { + + /** + * The type of the data object to retrieve. + * + * @type {DataObjectDescriptor} + */ + type: { + value: undefined + }, + + /** + * An object defining the criteria that must be satisfied by objects for + * them to be included in the data set defined by this query. + * + * Initially this can be any object and will typically be a set of key-value + * pairs, ultimately this will be a boolean expression to be applied to data + * objects to determine whether they should be in the selected set or not. + * + * @type {Object} + */ + criteria: { + get: function () { + if (!this._criteria) { + this._criteria = {}; + } + return this._criteria; + }, + set: function (criteria) { + this._criteria = criteria; + } + }, + + _criteria: { + value: undefined + }, + + /** + * An array of DataOrdering objects which, combined, define the order + * desired for the data in the set specified by this query. + * + * @type {Array} + */ + orderings: { + get: function () { + if (!this._orderings) { + this._orderings = []; + } + return this._orderings; + }, + set: function (orderings) { + this._orderings = orderings; + } + }, + + _orderings: { + value: undefined + }, + + /** + * An object defining bindings that will be created on the array + * of the dataStream returned by DataService's fetchData. The bindings + * follow the same syntax as used for regular bindings, creating dynamic + * properties that array. Expressions on the right side starts by data as the + * source is automatically set to the DataStream used in a fetchData and + * DataStream's data property is an array containing the results. + * + * For example, if one would want the number of objects fetched, one would do: + * aDataQuery.selectBindings = { + * "count": {"<-": "data.length" + * }; + * + * aDataQuery.selectBindings = { + * "averageAge": {"<-": "data.map{age}.average()" + * }; + * will add on the array passed to the then function following a fetchData + * a property averageAge with the average of the property age of all object in the array + * + * aDataQuery.selectBindings = { + * "clothingByColor": {"<-": "data.group{color}" + * }; + * mainService.fetchData(aDataQuery).then(function(results){ + * //assuming results is [ + * // {type: 'shirt', color: 'blue'}, + * // {type: 'pants', color: 'red'}, + * // {type: 'blazer', color: 'blue'}, + * // {type: 'hat', color: 'red'} + * // ]; + * + * expect(results.clothingByColor).toEqual([ + * ['blue', [ + * {type: 'shirt', color: 'blue'}, + * {type: 'blazer', color: 'blue'} + * ]], + * ['red', [ + * {type: 'pants', color: 'red'}, + * {type: 'hat', color: 'red'} + * ]] + * ]); + * }) + * Since it is a one-way binding, if a DataService is capable of live updating a query, + * the value these properties created on the array will stay current/updated over time. + * + * It is possible that a DataService may obtain the results of these properties from the + * server itself, which is preferred, as fetchData can returns objects in batches. These + * expressions should be built from the whole result set, not the current client view of that + * result set. + * @type {Object} + */ + + selectBindings: { + value: undefined + }, + + selectExpression: { + value: undefined + }, + + + /** + * An object defining a list of expressions to resolve at the same time as the query. + * expressions are based on the content of results described by criteria. A common + * use is to prefetch relationships off fetched objects. + * @type {Array} + */ + + prefetchExpressions: { + value: null + } + +}, /** @lends DataQuery */ { + + /** + * @todo Document. + */ + withTypeAndCriteria: { + value: function (type, criteria) { + var query; + query = new this(); + query.type = type; + query.criteria = criteria; + return query; + } + } + +}); diff --git a/data/model/data-selector.js b/data/model/data-selector.js new file mode 100644 index 0000000000..b844dc08c5 --- /dev/null +++ b/data/model/data-selector.js @@ -0,0 +1,13 @@ + +var DataQuery = require("data/model/data-query").DataQuery; + +/** + * Backward compatibility support for data/model/data-selector after that + * class has been renamed to data/model/data-query. + * + * @class + * @extends external:Montage + * @todo Deprecate. + */ + +exports.DataSelector = DataQuery; diff --git a/data/model/enumeration.js b/data/model/enumeration.js new file mode 100644 index 0000000000..c2e714c090 --- /dev/null +++ b/data/model/enumeration.js @@ -0,0 +1,310 @@ +var Montage = require("core/core").Montage; + +/** + * Subclasses of this class represent enumerated types. Instances of those + * subclasses represent possible values of those types. Enumerated type + * subclasses and their possible values can be defined as in the following: + * + * exports.Suit = Enumeration.specialize("id", "name", { + * SPADES: [0, "Spades"], + * HEARTS: [1, "Hearts"], + * DIAMONDS: [2, "Diamonds"], + * CLUBS: [3, "Clubs"] + * }); + * + * // To be use like this: + * myCard = {value: 12, suit: Suit.HEARTS}; + * + * Enumerated types can also be defined as class properties using the + * [getterFor()]{@link Enumeration.getterFor} constructor, as in the following: + * + * exports.Card = Montage.specialize({ + * + * value: { + * value: undefined + * }, + * + * suit: { + * value: undefined + * } + * + * }, { + * + * Suit: { + * get: Enumeration.getterFor("_Suit", "id", "name", { + * SPADES: [1, "Spade"], + * HEARTS: [2, "Heart"], + * DIAMONDS: [3, "Diamond"], + * CLUBS: [4, "Club"] + * }) + * } + * + * }); + * + * // To be use like this: + * myCard = new Card(); + * myCard.value = 12; + * myCard.suit = Card.Suit.HEARTS; + * + * In addition to being created as shown above, instances of an enumerated + * type subclasses can be created using a `with*()` class method that will be + * automatically generated for each subclass based on the subclass' property + * names, as in the following: + * + * Card.Suit.ROSES = Card.Suit.withIdAndName(5, "Roses"); + * + * Once instances of an enumerated type subclass are created they can be looked + * up using `for*()` class methods that will be automatically generated for each + * of the subclass's unique property names, as in the following: + * + * myCard.value = Math.floor(1 + 13 * Math.random()); + * myCard.suit = Card.Suit.forId(Math.floor(1 + 4 * Math.random())); + * + * @class + * @extends external:Montage + * + * @todo [Charles] Simplify this extremely, notably by making enumerations + * instances of the Enumeration class instead of subclasses of it and by + * reworking the [getterFor()]{@link Enumeration.getterFor} constructor. + * @todo [Charles] Switch to initial-caps enumeration value names like "Spades" + * instead of all-caps names like "NAMES". + * @todo [Charles] Provide a "values" property to the enumeration similar to + * Java's "values()" Enum method. + */ +exports.Enumeration = Montage.specialize({}, /** @lends Enumeration */ { + + /** + * Creates a new enumeration subclass with the specified attributes. + * + * @method + * @argument {Array.|string} [uniquePropertyNames] + * @argument {Array.|...string} [otherPropertyNames] + * @argument {Object} [prototypeDescriptor] + * @argument {Object} [constructorDescriptor] + * @argument {Object} [constants] + * @returns {function()} - The created Enumeration subclass. + * + * @todo [Charles] Simplify this API, possibly by making + * "uniquePropertyNames", "otherPropertyNames", and "constants" + * properties of the derived instance, leaving "specialize()" + * to its "Montage.specialize()" API and meaning. + */ + specialize: { + value: function (uniquePropertyNames, otherPropertyNames, + prototypeDescriptor, constructorDescriptor, + constants) { + return this._specialize(this._parseSpecializeArguments.apply(this, arguments)); + } + }, + + /** + * Creates and returns a getter which, when first called, will create and + * cache a new enumeration subclass with the specified attributes. + * + * @method + * @argument {string} key + * @argument {Array.|string} [uniquePropertyNames] + * @argument {Array.|...string} [otherPropertyNames] + * @argument {Object} [prototypeDescriptor] + * @argument {Object} [constructorDescriptor] + * @argument {Object} [constants] + * @returns {function()} - A getter that will create and cache the desired + * Enumeration subclass. + * + * @todo [Charles] Simplify this API, replacing it with simpler + * methods like Enumeration.withNames(), Enumeration.withValues(), + * and Enumeration.withPrototypeAndValues(). + */ + getterFor: { + value: function (key, uniquePropertyNames, otherPropertyNames, + prototypeDescriptor, constructorDescriptor, + constants) { + var specializeArguments = this._parseSpecializeArguments.apply(this, Array.prototype.slice.call(arguments, 1)); + // Return a function that will create the desired enumeration. + return function () { + if (!this.hasOwnProperty(key)) { + this[key] = exports.Enumeration._specialize(specializeArguments); + } + return this[key]; + }; + } + }, + + _parseSpecializeArguments: { + value: function (/* uniquePropertyNames, otherPropertyNames, + prototypeDescriptor, constructorDescriptor, + constants */) { + var start, unique, other, end, i, n; + // The unique property names array is the first argument if that's + // an array, or an array containing the first argument if that's a + // non-empty string, or an empty array. + start = 0; + if (Array.isArray(arguments[start])) { + unique = arguments[start]; + } else if (typeof arguments[start] === "string" && arguments[start].length > 0) { + unique = [arguments[start]]; + } else if (arguments[start] === null || arguments[start] === undefined || arguments[start] === "") { + unique = []; + } else { + unique = []; + start -= 1; + } + // The other property names array is the next argument if that's an + // array, or an array containing the next argument and all following + // ones that are strings if there are any, or an empty array. + other = Array.isArray(arguments[start + 1]) && arguments[start + 1]; + for (i = start + 1, n = arguments.length; !other; i += 1) { + if (i === n || !arguments[i] || typeof arguments[i] !== "string") { + other = Array.prototype.slice.call(arguments, start + 1, i); + start = i - 2; + } + } + // The remaining argument values come from the remaining arguments. + end = Math.min(arguments.length, start + 5); + return { + uniquePropertyNames: unique, + otherPropertyNames: other, + prototypeDescriptor: end > start + 3 ? arguments[start + 2] : {}, + constructorDescriptor: end > start + 4 ? arguments[start + 3] : {}, + constantDescriptors: end > start + 2 ? arguments[end - 1] : {} + }; + } + }, + + /** + * @private + * @method + */ + _specialize: { + value: function (parsed) { + var unique = parsed.uniquePropertyNames, + other = parsed.otherPropertyNames, + prototype = parsed.prototypeDescriptor, + constructor = parsed.constructorDescriptor, + constants = parsed.constantDescriptors, + enumeration, create, i, keys, key; + // Create the desired enumeration, including a constructor taking + // only unique property values (like `withId()`), and if there are + // other property values a constructor taking values for those too + // (like `withIdAndName()`). + this._addPropertiesToDescriptor(prototype, unique, other); + this._addLookupFunctionsToDescriptor(constructor, unique); + this._addConstructorFunctionToDescriptor(constructor, unique, []); + if (other.length) { + this._addConstructorFunctionToDescriptor(constructor, unique, other); + } + enumeration = Montage.specialize.call(exports.Enumeration, prototype, constructor); + // Add the requested constants. + create = this._makeCreateFunction(unique, other); + keys = Object.keys(constants); + for (i = 0; (key = keys[i]); ++i) { + enumeration[key] = create.apply(enumeration, constants[key]); + } + // Return the created enumeration. + return enumeration; + } + }, + + _addPropertiesToDescriptor: { + value: function(prototypeDescriptor, uniquePropertyNames, otherPropertyNames) { + var i, n; + for (i = 0, n = uniquePropertyNames.length; i < n; i += 1) { + if (!prototypeDescriptor[uniquePropertyNames[i]]) { + prototypeDescriptor[uniquePropertyNames[i]] = {value: undefined}; + } + } + for (i = 0, n = otherPropertyNames.length; i < n; i += 1) { + if (!prototypeDescriptor[otherPropertyNames[i]]) { + prototypeDescriptor[otherPropertyNames[i]] = {value: undefined}; + } + } + } + }, + + _addLookupFunctionsToDescriptor: { + value: function(constructorDescriptor, uniquePropertyNames) { + var name, lookup, i, n; + for (i = 0, n = uniquePropertyNames.length; i < n; i += 1) { + name = "for" + uniquePropertyNames[i][0].toUpperCase() + uniquePropertyNames[i].slice(1); + lookup = this._makeLookupFunction(uniquePropertyNames[i]); + constructorDescriptor[name] = {value: lookup}; + } + } + }, + + _makeLookupFunction: { + value: function(propertyName) { + return function (propertyValue) { + return this._instances && + this._instances[propertyName] && + this._instances[propertyName][propertyValue]; + }; + } + }, + + _addConstructorFunctionToDescriptor: { + value: function(constructorDescriptor, uniquePropertyNames, otherPropertyNames) { + var create, names, name, i, n; + if (uniquePropertyNames.length || otherPropertyNames.length) { + create = this._makeCreateFunction(uniquePropertyNames, otherPropertyNames); + names = ["with"]; + for (i = 0, n = uniquePropertyNames.length; i < n; i += 1) { + names.push(uniquePropertyNames[i][0].toUpperCase()); + names.push(uniquePropertyNames[i].slice(1)); + } + for (i = 0, n = otherPropertyNames.length; i < n; i += 1) { + names.push(otherPropertyNames[i][0].toUpperCase()); + names.push(otherPropertyNames[i].slice(1)); + } + if (names.length > 3) { + names.splice(names.length - 2, 0, "And"); + } + constructorDescriptor[names.join("")] = {value: create}; + } + } + }, + + _makeCreateFunction: { + value: function(uniquePropertyNames, otherPropertyNames) { + var self = this; + return function (propertyValues, propertiesDescriptor) { + var createArguments = self._parseCreateArguments(uniquePropertyNames, otherPropertyNames, arguments); + return self._create(this, uniquePropertyNames, otherPropertyNames, createArguments); + }; + } + }, + + _parseCreateArguments: { + value: function(uniquePropertyNames, otherPropertyNames, zArguments) { + var count = uniquePropertyNames.length + otherPropertyNames.length; + return { + propertyValues: Array.prototype.slice.call(zArguments, 0, count), + propertyDescriptors: zArguments[count] || {} + }; + } + }, + + _create: { + value: function(type, uniquePropertyNames, otherPropertyNames, zArguments) { + var instance, name, i, m, n; + // Create the instance. + instance = new type(); + // Add to the instance the properties specified in the descriptor. + Montage.defineProperties(instance, zArguments.propertyDescriptors); + // Add to the instance the property values specified. + for (i = 0, n = uniquePropertyNames.length, m = otherPropertyNames.length; i < n + m; i += 1) { + name = i < n ? uniquePropertyNames[i] : otherPropertyNames[i - n]; + instance[name] = zArguments.propertyValues[i]; + } + // Record the instance in the appropriate lookup maps. + for (i = 0, n = uniquePropertyNames.length; i < n; i += 1) { + type._instances = type._instances || {}; + type._instances[uniquePropertyNames[i]] = type._instances[uniquePropertyNames[i]] || {}; + type._instances[uniquePropertyNames[i]][zArguments.propertyValues[i]] = instance; + } + // Return the created instance. + return instance; + } + } + +}); diff --git a/data/model/object-descriptor.js b/data/model/object-descriptor.js new file mode 100644 index 0000000000..49da464240 --- /dev/null +++ b/data/model/object-descriptor.js @@ -0,0 +1,177 @@ +var Montage = require("core/core").Montage, + PropertyDescriptor = require("data/model/property-descriptor").PropertyDescriptor; + +/** + * Describes a type of object. + * @deprecated + * @class + * @extends external:Montage + */ +exports.ObjectDescriptor = Montage.specialize(/** @lends ObjectDescriptor.prototype */ { + + /** + * Name of the type of object described by this descriptor. + * + * @type {string} + */ + typeName: { + value: undefined + }, + + /** + * Prototype of objects of this type. + * + * @type {function} + */ + objectPrototype: { + value: Montage.prototype + }, + + /** + * Descriptors of the properties of objects of this type, by property name. + * + * The returned map should not be modified. + * [setPropertyDescriptor()]{@link ObjectDescriptor#setPropertyDescriptor} + * and + * [deletePropertyDescriptor()]{@link ObjectDescriptor#deletePropertyDescriptor} + * should be used instead to modify the property descriptors. + * + * @type {Object.} + */ + propertyDescriptors: { + get: function () { + if (!this._propertyDescriptors) { + this._propertyDescriptors = {}; + } + return this._propertyDescriptors; + } + }, + + /** + * Add or replace a property descriptor. + * + * @method + * @argument {string} name - The name of the property. + * @argument {PropertyDescriptor} descriptor - Describes the property. + */ + setPropertyDescriptor: { + value: function (name, descriptor) { + this.propertyDescriptors[name] = descriptor; + } + }, + + /** + * Remove a property descriptor. + * + * @method + * @argument {string} name - The name of the property whose descriptor + * should be removed. + */ + deletePropertyDescriptor: { + value: function (name) { + delete this.propertyDescriptors[name]; + } + }, + + /** + * Create a property descriptor. + * + * This implementation creates a plain {@link PropertyDescriptor} instance. + * Subclasses can override this to create property descriptors of a + * type more appropriate for the subclass. Any such type should be a + * subclass of {@link PropertyDescriptor}. + * + * @method + * @returns {PropertyDescriptor} - The created property descriptor. + */ + makePropertyDescriptor: { + value: function () { + return new PropertyDescriptor(); + } + }, + + /** + * @private + * @method + * @argument {Object} types + */ + _setPropertyDescriptorsFromTypes: { + value: function (types) { + var i, descriptor, key, + keys = Object.keys(types); + for (i = 0; (key = keys[i]); ++i) { + descriptor = this.makePropertyDescriptor(); + this.setPropertyDescriptor(key, descriptor); + } + } + }, + + /** + * @private + * @method + * @argument {Object} prototype + */ + _setPropertyDescriptorsFromPrototype: { + value: function (prototype) { + var names, descriptor, i, n; + for (; prototype !== Montage.prototype && prototype !== null; prototype = Object.getPrototypeOf(prototype)) { + names = Object.getOwnPropertyNames(prototype); + for (i = 0, n = names.length; i < n; i += 1) { + if (!this.propertyDescriptors[names[i]]) { + descriptor = this.makePropertyDescriptor(); + this.setPropertyDescriptor(names[i], descriptor); + } + } + } + } + } + +}, /** @lends ObjectDescriptor */ { + + /** + * Generates a getter function that will create and cache an object + * descriptor. + * + * @method + * @argument {Object} exports - A + * [Montage Require]{@link external:Require} + * exports object defining the + * constructor for the objects to + * describe. Usually this is `exports`. + * @argument {string} constructorName - The name with which that constructor + * can be looked up in the exports. + * This will also be used as the + * created descriptor's type name. + * @argument {Object} + * [propertyTypes] - The types of each of the described + * objects' properties, by property + * name. If this is not specified the + * property information will be derived + * from the properties of the objects' + * prototype. + */ + getterFor: { + value: function (exports, constructorName, propertyTypes) { + // The returned getter function has to check + // `this.hasOwnProperty("_TYPE")`, not just `this._TYPE`, because if + // the class using the getter is a subclass of another class using a + // similar getter `this._TYPE` will return the value of the the + // superclass type instead of the desired subclass type. + var self = this; + return function () { + if (!this.hasOwnProperty("_TYPE")) { + this._TYPE = new self(); + this._TYPE.typeName = constructorName; + this._TYPE.objectPrototype = exports[constructorName].prototype; + if (propertyTypes) { + this._TYPE._setPropertyDescriptorsFromTypes(propertyTypes); + } else { + this._TYPE._setPropertyDescriptorsFromPrototype(this._TYPE.objectPrototype); + } + } + return this._TYPE; + }; + } + } + +}); diff --git a/data/model/property-descriptor.js b/data/model/property-descriptor.js new file mode 100644 index 0000000000..f2f369879e --- /dev/null +++ b/data/model/property-descriptor.js @@ -0,0 +1,25 @@ +var Montage = require("core/core").Montage; + +/** + * Describes a property of objects of a certain type. + * @deprecated + * @class + * @extends external:Montage + */ +exports.PropertyDescriptor = Montage.specialize(/** @lends PropertyDescriptor.prototype */ { + + /** + * @type {boolean} + */ + isRelationship: { + value: false + }, + + /** + * @type {boolean} + */ + isOptional: { + value: false + } + +}); diff --git a/data/montage-data.js b/data/montage-data.js new file mode 100644 index 0000000000..fa1887c7a2 --- /dev/null +++ b/data/montage-data.js @@ -0,0 +1,19 @@ +var Deserializer = require("core/serialization/deserializer/montage-deserializer").MontageDeserializer; + +module.exports = function (mr, file) { + var deserializer; + return mr.async(file).then(function (descriptor) { + deserializer = new Deserializer().init(JSON.stringify(descriptor), mr); + return deserializer.deserializeObject(); + }); +}; + + + +// appKey = Object.keys(module.require.packages)[0], +// appPackage = module.require.packages[appKey]; + +// appPackage.async("./data/model.mjson").then(function (json) { +// console.log("Hello... (", json, ")"); +// }); + diff --git a/data/service/authorization-manager.js b/data/service/authorization-manager.js new file mode 100644 index 0000000000..d021a92699 --- /dev/null +++ b/data/service/authorization-manager.js @@ -0,0 +1,364 @@ +var Montage = require("core/core").Montage, + Promise = require("core/promise").Promise, + Map = require("collections/map"), + application = require("core/application").application, + AuthorizationPolicy = require("data/service/authorization-policy").AuthorizationPolicy, + MANAGER_PANEL_MODULE = "ui/authorization-manager-panel.reel"; + + +/** + * Helps coordinates the needs for DataServices to get the authorization they + * need to access data. It is meant to be a singleton, so the constructor + * enforces that. + * + * @class + * @extends external:Montage + */ +var AuthorizationManager = Montage.specialize(/** @lends AuthorizationManager.prototype */ { + + constructor: { + value: function () { + this._providersByModuleID = new Map(); + this._panelsByModuleID = new Map(); + this._authorizationsByProviderModuleID = new Map(); + this.defineBinding("hasPendingServices", {"<-": "_pendingServicesCount != 0"}); + return this; + } + }, + + /******************************************** + * Caching + */ + + // Provider Module ID to Authorization Promise + _authorizationsByProviderModuleID: { + value: undefined + }, + + // Module ID to Panel + _panelsByModuleID: { + value: undefined + }, + + // Module ID to Service + _providersByModuleID: { + value: undefined + }, + + /******************************************** + * Panels + */ + + _managerPanel: { + value: function () { + var self = this, + moduleId; + + if (!this._managerPanelPromise && this.authorizationManagerPanel) { + this.authorizationManagerPanel.authorizationManager = this; + this._managerPanelPromise = Promise.resolve(this.authorizationManagerPanel); + } else if (!this._managerPanelPromise) { + moduleId = this.callDelegateMethod("authorizationManagerWillLoadAuthorizationManagerPanel", this, MANAGER_PANEL_MODULE) || MANAGER_PANEL_MODULE; + this._managerPanelPromise = require.async(moduleId).bind(this).then(function (exports) { + var panel = new exports.AuthorizationManagerPanel(); + self.authorizationManagerPanel = panel; + panel.authorizationManager = self; + return panel; + }).catch(function(error) { + console.log(error); + }); + } + + return this._managerPanelPromise; + } + }, + + _panelForProvider: { + value: function (provider) { + var moduleId = this._panelModuleIDForProvider(provider), + panel = this._panelsByModuleID.get(moduleId); + + return panel ? Promise.resolve(panel) : this._makePanelForProvider(moduleId, provider); + } + }, + + _panelModuleIDForProvider: { + value: function (provider) { + var moduleID = provider.authorizationPanel; + return this.callDelegateMethod("authorizationManagerWillAuthorizeServiceWithPanelModuleId", this, provider, moduleID) || moduleID; + } + }, + + _makePanelForProvider: { + value: function (panelModuleID, provider) { + var self = this, + providerInfo = Montage.getInfoForObject(provider), + panelPromise; + + if (panelModuleID) { + panelPromise = providerInfo.require.async(panelModuleID).then(function (exports) { + var exportNames = Object.keys(exports), + panel, i, n; + + for (i = 0, n = exportNames.length; i < n && !panel; ++i) { + panel = self._panelForConstructorAndProvider(exports[exportNames[i]], provider); + } + panel.service = provider; + self._panelsByModuleID.set(panelModuleID, panel); + return panel; + }); + } + + return panelPromise; + } + }, + + _panelForConstructorAndProvider: { + value: function (constructor, provider) { + return this.callDelegateMethod("authorizationManagerWillInstantiateAuthorizationPanelForService", this, constructor, provider) || new constructor(); + } + }, + + /******************************************** + * Services/Providers + */ + + _canNotifyDataService: { + value: function (dataService) { + return dataService.authorizationManagerWillAuthorizeWithService && typeof dataService.authorizationManagerWillAuthorizeWithService === "function"; + } + }, + + _providersForDataService: { + value: function (dataService) { + var promises = [], + dataServiceInfo = Montage.getInfoForObject(dataService), + providerIDs = dataService.authorizationServices, + providerPromise, i, n; + + for (i = 0, n = providerIDs.length; i < n; ++i) { + providerPromise = this._providerWithModuleID(providerIDs[i], dataServiceInfo.require); + promises.push(providerPromise); + } + + return Promise.all(promises); + } + }, + + _providerWithModuleID: { + value: function (moduleID, require) { + var existingService = this._providersByModuleID.get(moduleID); + return existingService ? Promise.resolve(existingService) : + this._makeProviderWithModuleID(moduleID, require); + } + }, + + + _authorizationWithProvider: { + value: function (moduleID, require) { + var self = this, + provider; + return this._providerWithModuleID(moduleID, require).then(function (instance) { + provider = instance; + return provider.authorize(); + }).then(function (authorization) { + return authorization || self._authorizeProviderWithManagerPanel(provider); + + }); + } + }, + + _authorizeProviderWithManagerPanel: { + value: function (provider) { + var self = this, + managerPanel; + self._pendingServicesCount++; + return this._managerPanel().then(function (authManagerPanel) { + managerPanel = authManagerPanel; + return self._panelForProvider(provider); + }).then(function (panel) { + return managerPanel.authorizeWithPanel(panel); + }).then(function (authorization) { + self._pendingServicesCount--; + return authorization; + }); + } + }, + + _makeProviderWithModuleID: { + value: function (moduleID, require) { + var self = this; + return require.async(moduleID).then(function (exports) { + var provider, i, providerName, + providerNames = Object.keys(exports); + for (i = 0; (providerName = providerNames[i]); ++i) { + provider = provider || new exports[providerName](); + } + self.registerAuthorizationService(provider); + return provider; + }); + } + }, + + _notifyDataService: { + value: function (dataService) { + var i, n; + + if (this._canNotifyDataService(dataService)) { + return this._providersForDataService(dataService).then(function (services) { + for (i = 0, n = services.length; i < n; i++) { + //We tell the data service we're authorizing about authorizationService we create and are about to use. + dataService.authorizationManagerWillAuthorizeWithService(this, services[i]); + } + }); + } + + return Promise.resolve(null); + } + }, + + /******************************************** + * Public + */ + + /** + * Module ID of the panel that displays the UI specific to the authorization providers. + * + * + * Each authorization provider has a module ID pointing to a component which displays whatever + * UI is required to get authorization through that provider. The most common UI for + * applications with a single auth provider is a login form. The most common UI for + * applications with multiple providers is the view that shows a button for each provider + * and allows the user to select their preferred provider. For example, a button for each + * of Facebook, Linkedin, and Google. The authorizationManagerPanel is the container + * into which the provider-specific components are placed. + * + * @type {string} + */ + authorizationManagerPanel: { + value: undefined + }, + + /** + * Takes care of obtaining authorization for a DataService. Returns a promise of Authorization + * + * TODO: + * 1. Handle repeated calls: if a DataService authorizes on-demand it's likely + * it would come from fetching data. Multiple independent fetches could trigger repeated + * attempts to authorize: The promise should be cached and returned when pending. + * + * TODO: + * 2. A service could require mandatory authorization from 2 dataService, right now it's implemented + * in a way that we expect user to make a choice in one of aDataService.authorizationServices, + * not a combination of. We need another structure to represent that. + * + * TODO + * right now, Promises for existing objects are resolved, meaning that the loops could see different + * types of objects coming in. Existing objects could be just added to array filled after the Promise.all().then.. + * + * @method + * @argument {DataService} dataService - A dataService for which to get an authorization + */ + + + authorizeService: { + value: function (dataService, didFailAuthorization) { + var self = this, + authorizationPromises = []; + + + if (dataService.authorizationPolicy === AuthorizationPolicy.NONE) { + return Promise.resolve(null); + } else { + + //[TJ] This will only work for data services with a single authorization-service + authorizationPromises = this._authorizationsForDataService(dataService); + + if (authorizationPromises.length) { + return Promise.all(authorizationPromises); + } else if (dataService.authorizationPolicy === AuthorizationPolicy.ON_DEMAND && !didFailAuthorization) { + return Promise.resolve(null); + } else { + + authorizationPromises = this._authorizationsForDataService(dataService, true); + return self._notifyDataService(dataService).then(function () { + var useModal = application.applicationModal && self.authorizationManagerPanel.runModal; + return useModal ? self.authorizationManagerPanel.runModal() : Promise.all(authorizationPromises); + }).then(function(authorizations) { + self.callDelegateMethod("authorizationManagerDidAuthorizeService", self, dataService); + //TODO [TJ] How to concatenate authorizations from different auth services + //TODO into a single Authorization Object for the data-service + return authorizations; + }).catch(function () { + self.hasPendingServices = false; + }); + } + } + } + }, + + _authorizationsForDataService: { + value: function (dataService, requestIfAbsent) { + var promises = [], + dataServiceInfo = Montage.getInfoForObject(dataService), + promise, moduleID, i, n; + for (i = 0, n = dataService.authorizationServices.length; i < n; i++) { + moduleID = dataService.authorizationServices[i]; + promise = this._authorizationForServiceFromProvider(moduleID, dataServiceInfo.require, requestIfAbsent); + if (promise) { + promises.push(promise); + } + } + return promises; + } + }, + + _authorizationForServiceFromProvider: { + value: function (moduleID, require, requestIfAbsent) { + var promise = null; + if (this._authorizationsByProviderModuleID.has(moduleID)) { + promise = this._authorizationsByProviderModuleID.get(moduleID); + } else if (requestIfAbsent) { + promise = this._authorizationWithProvider(moduleID, require); + this._authorizationsByProviderModuleID.set(moduleID, promise); + } + return promise; + } + }, + + + _pendingServicesCount: { + value: 0 + }, + + delegate: { + value: null + }, + + + /** + * Flag to track the number of services pending on the AuthorizationManagerPanel. + * The value is true if 1 or more services is currently pending. It can be used as + * an alternative to runModal()/closeModal() + * @type {boolean} + */ + hasPendingServices: { + value: false + }, + + + /** + * + * @method + * @argument {DataService} dataService - A dataService to be registered as an Authorization provider + */ + registerAuthorizationService: { + value: function(dataService) { + var info = Montage.getInfoForObject(dataService); + this._providersByModuleID.set(info.moduleId, dataService); + } + } + +}); + +exports.AuthorizationManager = new AuthorizationManager(); diff --git a/data/service/authorization-policy.js b/data/service/authorization-policy.js new file mode 100644 index 0000000000..fdfd88c4c5 --- /dev/null +++ b/data/service/authorization-policy.js @@ -0,0 +1,34 @@ +var Montage = require("core/core").Montage; + +/** + * AuthorizationPolicyType + * + * UpfrontAuthorizationPolicy + * Authorization is asked upfront, immediately after data service is + * created / launch of an app. + * + * OnDemandAuthorizationPolicy + * Authorization is required when a request fails because of lack of + * authorization. This is likely to be a good strategy for DataServices + * that offer data to both anonymous and authorized users. + * + */ +var AuthorizationPolicy = exports.AuthorizationPolicy = Montage.specialize({ + + id: { + value: undefined + } + +}, { + withID: { + value: function (id) { + var policy = new this(); + policy.id = id; + return policy; + } + } +}); +AuthorizationPolicy.ON_DEMAND = AuthorizationPolicy.withID("ON_DEMAND"); +AuthorizationPolicy.ON_FIRST_FETCH = AuthorizationPolicy.withID("ON_FIRST_FETCH"); +AuthorizationPolicy.NONE = AuthorizationPolicy.withID("NONE"); +AuthorizationPolicy.UP_FRONT = AuthorizationPolicy.withID("UP_FRONT"); diff --git a/data/service/authorization-service.js b/data/service/authorization-service.js new file mode 100644 index 0000000000..3e40970d77 --- /dev/null +++ b/data/service/authorization-service.js @@ -0,0 +1,20 @@ +var DataService = require("data/service/data-service").DataService; + + +/** + * + * @class + * @extends RawDataService + * @deprecated The Authorization API was moved to DataService itself. + */ +exports.AuthorizationService = DataService.specialize( /** @lends AuthorizationService.prototype */ { + + + constructor: { + value: function AuthorizationService() { + console.warn("AuthorizationService is deprecated. The Authorization API was moved to DataService"); + DataService.call(this); + } + } + +}); diff --git a/data/service/authorization.js b/data/service/authorization.js new file mode 100644 index 0000000000..e66182b45e --- /dev/null +++ b/data/service/authorization.js @@ -0,0 +1,43 @@ +var Montage = require("core/core").Montage; + +/** + * An Authorization represents the details regarding access to a certain DataService. + * Different type of DataServices that offer Authorization will provide different specialzied Authorization + * subtypes. Login/Password and O-Auth are 2 examples. + * + * @class + * @extends external:Montage + */ +var Authorization = exports.Authorization = Montage.specialize(/** @lends AuthorizationManager.prototype */ { + +}); + +/* + An example, GitHub Authorization: + GET /authorizations/:id + Response + + Status: 200 OK + X-RateLimit-Limit: 5000 + X-RateLimit-Remaining: 4999 + { + "id": 1, + "url": "https://api.github.com/authorizations/1", + "scopes": [ + "public_repo" + ], + "token": "", + "token_last_eight": "12345678", + "hashed_token": "25f94a2a5c7fbaf499c665bc73d67c1c87e496da8985131633ee0a95819db2e8", + "app": { + "url": "http://my-github-app.com", + "name": "my github app", + "client_id": "abcde12345fghij67890" + }, + "note": "optional note", + "note_url": "http://optional/note/url", + "updated_at": "2011-09-06T20:39:23Z", + "created_at": "2011-09-06T17:26:27Z", + "fingerprint": "jklmnop12345678" + } +*/ diff --git a/data/service/blueprint-data-mapping.js b/data/service/blueprint-data-mapping.js new file mode 100644 index 0000000000..1ac2a663c2 --- /dev/null +++ b/data/service/blueprint-data-mapping.js @@ -0,0 +1,144 @@ +var DataMapping = require("data/service/data-mapping").DataMapping, + moment = require("moment-timezone"); + +/** + * Maps raw data to data objects of a specific type by using a blueprint. + ** + * @class + * @extends external:Montage + */ +exports.BlueprintDataMapping = DataMapping.specialize(/** @lends BlueprintDataMapping.prototype */ { + + _blueprint: { + value: undefined + }, + + /*************************************************************************** + * 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, data) { + * object.firstName = data.GIVEN_NAME; + * object.lastName = data.FAMILY_NAME; + * } + * } + * + * The default implementation of this method copies the properties defined + * by the raw data object to the data object. + * + * @method + * @argument {Object} object - An object whose properties must be set or + * modified to represent the raw data. + * @argument {Object} data - An object whose properties' values hold + * the raw data. + * @argument {?} context - A value that was passed in to the + * [addRawData()]{@link DataService#addRawData} + * call that invoked this method. + */ + mapRawDataToObject: { + value: function (data, object, context) { + var propertyBlueprints = this._blueprint.propertyDescriptors, + attributes = data.attributes || data, + // relationships = data.relationships, + i, length, property, propertyKey, value; + + + + for (i = 0, length = propertyBlueprints.length; i < length; i++) { + property = propertyBlueprints[i]; + propertyKey = property.synonym || property.name; + if (attributes && propertyKey in attributes) { + value = attributes[propertyKey]; + object[property.name] = property.valueType === "date" ? new moment(Number(value)) : + property.valueType === "duration" ? moment.duration(Number(value)) : + value; + //console.log(iProperty.name +" found in attributes"); + // } + // else if(relationships && property.name in relationships) { + // //object[iProperty.name] = attributes[iProperty.name]; + // //console.log(iProperty.name +" found in relationshios"); + // } + // else { + // //console.log(iProperty.name +" NOT found in data"); + } + + } + } + }, + + /** + * @todo Document. + */ + mapObjectToRawData: { + value: function (object, data) { + // TO DO: Provide a default mapping based on object.TYPE. + // For now, subclasses must override this. + var propertyBlueprints = this._blueprint.propertyDescriptors, + attributes = data.attributes, + // relationships = data.relationships, + i, length, property, propertyKey; + + //console.log("mapFromRawData",object,data,context); + + for (i = 0, length = propertyBlueprints.length; i < length; i++) { + property = propertyBlueprints[i]; + propertyKey = property.synonym || property.name; + if (attributes && property.name in attributes) { + attributes[propertyKey] = object[property.name]; + //console.log(iProperty.name +" found in attributes"); + // } + // else if(relationships && property.name in relationships) { + // //object[iProperty.name] = attributes[iProperty.name]; + // //console.log(iProperty.name +" found in relationships"); + // } + // else { + // //console.log(iProperty.name +" NOT found in data"); + } + + } + + } + }, + + /*************************************************************************** + * Deprecated + */ + + /** + * @todo Document deprecation in favor of + * [mapRawDataToObject()]{@link DataMapping#mapRawDataToObject} + */ + mapFromRawData: { + value: function (object, record, context) { + this.mapRawDataToObject(record, object, context); + } + }, + + /** + * @todo Document deprecation in favor of + * [mapObjectToRawData()]{@link DataMapping#mapObjectToRawData} + */ + mapToRawData: { + value: function (object, record) { + this.mapObjectToRawData(object, record); + } + } + +}, { + + withBlueprint: { + value: function (blueprint) { + var mapping = new this(); + mapping._blueprint = blueprint; + return mapping; + } + } + +}); diff --git a/data/service/connection-descriptor.js b/data/service/connection-descriptor.js new file mode 100644 index 0000000000..47782235ea --- /dev/null +++ b/data/service/connection-descriptor.js @@ -0,0 +1,22 @@ +var Montage = require("core/core").Montage; + +/** + * This object represents the data necessary for a DataService to connect to it's data source, + * but leaves to RawDataServices the role + * to specialize it in a way that reflects what they need. + * + * @class + * @extends external:Montage + */ +exports.ConnectionDescriptor = Montage.specialize(/** @lends DataIdentifier.prototype */ { + + /** + * The name of this descriptor + * + * @type {String} + */ + name: { + value: undefined + } + +}); diff --git a/data/service/data-mapping.js b/data/service/data-mapping.js new file mode 100644 index 0000000000..d300995839 --- /dev/null +++ b/data/service/data-mapping.js @@ -0,0 +1,113 @@ +var Montage = require("core/core").Montage; + +/** + * Maps raw data to data objects of a specific type. + * + * Currently services define their mapping by overriding their + * [mapRawDataToObject()]{@link DataService#mapRawDataToObject} and + * [mapObjectToRawData()]{@link DataService#mapObjectToRawData} methods or by + * using a {@link DataMapping} subclass that overrides its + * [mapRawDataToObject()]{@link DataMapping#mapRawDataToObject} and + * [mapRawDataToObject()]{@link DataMapping#mapRawDataToObject} methods. In the + * future it will be possible to define mappings declaratively through mapping + * descriptors read from blueprint files. + * + * @class + * @extends external:Montage + */ +exports.DataMapping = Montage.specialize(/** @lends DataMapping.prototype */ { + + /*************************************************************************** + * 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, data) { + * object.firstName = data.GIVEN_NAME; + * object.lastName = data.FAMILY_NAME; + * } + * } + * + * The default implementation of this method copies the properties defined + * by the raw data object to the data object. + * + * @method + * @argument {Object} object - An object whose properties must be set or + * modified to represent the raw data. + * @argument {Object} data - An object whose properties' values hold + * the raw data. + * @argument {?} context - A value that was passed in to the + * [addRawData()]{@link DataService#addRawData} + * call that invoked this method. + */ + mapRawDataToObject: { + value: function (data, object, context) { + var i, key, + keys = Object.keys(data); + if (data) { + for (i = 0; (key = keys[i]); ++i) { + object[key] = data[key]; + } + } + } + }, + + /** + * @todo Document. + */ + mapObjectToRawData: { + value: function (object, data) { + // TO DO: Provide a default mapping based on object.TYPE. + // For now, subclasses must override this. + } + }, + + nullPromise: { + get: function () { + if (!this._nullPromise) { + this._nullPromise = Promise.resolve(null); + } + return this._nullPromise; + } + }, + + /*************************************************************************** + * Deprecated + */ + + /** + * @todo Document deprecation in favor of + * [mapRawDataToObject()]{@link DataMapping#mapRawDataToObject} + */ + mapFromRawData: { + value: function (object, record, context) { + this.mapRawDataToObject(record, object, context); + } + }, + + /** + * @todo Document deprecation in favor of + * [mapObjectToRawData()]{@link DataMapping#mapObjectToRawData} + */ + mapToRawData: { + value: function (object, record) { + this.mapObjectToRawData(object, record); + } + } + +},{ + + withObjectDescriptor: { + value: function (objectDescriptor) { + var mapping = new this(); + mapping._descriptor = objectDescriptor; + return mapping; + } + } +}); diff --git a/data/service/data-operation.js b/data/service/data-operation.js new file mode 100644 index 0000000000..3dc7099856 --- /dev/null +++ b/data/service/data-operation.js @@ -0,0 +1,160 @@ +var Montage = require("core/core").Montage; + +/** + * Represents + * + * @class + * @extends external:Montage + */ +exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ { + + /*************************************************************************** + * Constructor + */ + + constructor: { + value: function DataOperation() { + this.time = Date.now(); + this._index = exports.DataOperation.prototype._currentIndex + 1 || 0; + exports.DataOperation.prototype._currentIndex = this._index; + } + }, + + _currentIndex: { + value: undefined + }, + + /*************************************************************************** + * Basic Properties + */ + + /** + * @type {number} + */ + id: { + value: undefined + }, + + /** + * @type {DataOperation.Type.CREATE|DataOperation.Type.READ|DataOperation.Type.UPDATE|DataOperation.Type.DELETE} + */ + type: { + value: undefined + }, + + /** + * A number used to order operations according to when they were created. + * + * This is initialized when an operation is created to the value of + * [Date.now()]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now}. + * The value can then be changed, but it should only be changed to values + * returned by `Date.now()`. + * + * Two operations can have the same `time` value if they were created within + * a millisecond of each other, and if so the operations' + * [index]{@link DataOperation#index} can be used to determine which one was + * created first. + * + * @type {number} + */ + time: { + value: undefined + }, + + /** + * A number that is greater than the index values of all operations created + * before this one in the current application session and less than the + * index values of all operations created after this one in the current + * application session. + * + * This is initialized when an operation is created to a number that is + * zero when the application starts up and then automatically incremented. + * This will provide an appropriate value for this property, and if this + * value is then changed, care should be taken to ensure it is changed to a + * value that also satisfies the conditions above. + * + * This can be used in conjunction with [time]{@link DataOperation#time} to + * order operations according to when they were created: An operation will + * have been created before another operation if and only if the operation's + * time is before the other's time, or if the times are equal and the + * operation's index is before the other's index. + * + * Assuming it takes more than a millisecond to restart an application or + * to switch between running instance of an application, it will be + * impossible for two operations to have both the same `time` value and the + * same `index` value, so sorting operations as described above will + * correctly sort them according to when they were created. + * + * @type {number} + */ + index: { + value: undefined + }, + + /** + * @type {Object} + */ + context: { + value: undefined + }, + + /*************************************************************************** + * Data Properties + */ + + dataID: { + value: undefined + }, + + dataType: { + value: undefined + }, + + /** + * @type {Object} + */ + data: { + value: undefined + }, + + /*************************************************************************** + * Deprecated + */ + + /** + * @todo: Deprecate and remove when appropriate. + */ + changes: { + get: function () { + return this.data; + }, + set: function (data) { + this.data = data; + } + }, + + /** + * @todo: Deprecate and remove when appropriate. + */ + lastModified: { + get: function () { + return this.time; + }, + set: function (time) { + this.time = time; + } + } + +}, /** @lends DataOperation */ { + + Type: { + value: { + CREATE: {isCreate: true}, + READ: {isRead: true}, + UPDATE: {isUpdate: true}, + DELETE: {isDelete: true} + } + } + +}); + diff --git a/data/service/data-provider.js b/data/service/data-provider.js new file mode 100644 index 0000000000..e1c244f04c --- /dev/null +++ b/data/service/data-provider.js @@ -0,0 +1,97 @@ +var Montage = require("core/core").Montage, + Promise = require("core/promise").Promise; + +/** + * This class documents the properties and methods of objects that support the + * DataProvider [protocol]{@linkplain external:Protocol}. Data provider classes + * can extend this class but they aren't required to, they just need to have the + * [data]{@link DataProvider#data} property and the + * [requestData()]{@link DataProvider#requestData} method of this class. + * + * @class + * @extends external:Montage + * + */ +exports.DataProvider = Montage.specialize(/** @lends DataProvider.prototype */ { + + /** + * At any point in time a data provider’s [data]{@link DataProvider#data} + * array and the length of that array represent the state of the data as it + * is then known to the data provider. If a data provider knows that an item + * will need to be in its [data]{@link DataProvider#data} array but doesn’t + * yet know the value of that item, it will place in the array an undefined + * value for that item. Data providers can’t be used with data that includes + * actual `undefined` values, so the presence of an `undefined` value in a + * data provider’s [data]{@link DataProvider#data} array is an unambiguous + * indication that the value of the corresponding item is expected but + * hasn’t yet been received. + * + * As a data provider receives new data, or if its data changes for any + * reason, it will update its [data]{@link DataProvider#data} array + * correspondingly, most notably by replacing `undefined` values with real + * objects and by changing the array’s length. A + * [range change listener]{@linkplain external:RangeChangeListener} + * can be used to monitor those changes, and components like Montage's + * [Repetition]{@linkcode external:Repetition} will automatically do this + * monitoring. + * + * Although the contents of a data provider's + * [data]{@link DataProvider#data} array may change over time, the array + * itself will not change. Subclasses will typically want to create the + * [data]{@link DataProvider#data} array lazilly the first time it is needed + * and then not allow that property to change, with code like the following: + * + * data: { + * get: function() { + * if (!this._data) { + * this._data = []; + * } + * return this._data; + * } + * }, + * + * @type {Array} + */ + data: { + value: undefined + }, + + /** + * Objects using a data provider can call the provider’s + * [requestData()]{@link DataProvider#requestData} method to indicate that + * they want the data in the specified range. If the requested data is not + * in the provider’s [data]{@link DataProvider#data} array yet, and if that + * data can be obtained, it will be obtained synchronously or asynchronously + * and placed in the data array as described in the documentation for the + * [data]{@link DataProvider#data} property above. + * + * When a data provider’s data is obtained asynchronously no guarantee is + * given about exactly when that data will end up in the provider's + * [data]{@link DataProvider#data} array. Also, no guarantee is given that a + * data provider will provide through its [data]{@link DataProvider#data} + * array only the data specifically requested in + * [requestData()]{@link DataProvider#requestData} calls: It may obtain and + * provide more data. + * + * In spite of these lack of guarantees data providers try to be smart about + * what data they provide or withhold based on + * [requestData()]{@link DataProvider#requestData} calls and based on + * algorithms specific to each type of provider. + * + * This class does nothing when this method is called. + * + * @method + * @argument {int} start - The index of the start of the range of the + * requested data. When `undefined`, all available + * data is requested. + * @argument {int} length - The length of the range of the requested data. + * When `undefined`, all available data is + * requested. + */ + requestData: { + value: function (start, length) { + return Promise.resolve(this.data); + } + } + +}); diff --git a/data/service/data-selector.js b/data/service/data-selector.js new file mode 100644 index 0000000000..de3d9a99f9 --- /dev/null +++ b/data/service/data-selector.js @@ -0,0 +1,11 @@ +var DataSelector = require("data/model/data-selector").DataSelector; + +/** + * Backward compatibility support for data/service/data-selector after that + * class has been moved to data/model/data-selector. + * + * @class + * @extends external:Montage + * @todo Deprecate. + */ +exports.DataSelector = DataSelector; diff --git a/data/service/data-service.js b/data/service/data-service.js new file mode 100644 index 0000000000..2996a65e55 --- /dev/null +++ b/data/service/data-service.js @@ -0,0 +1,2522 @@ +var Montage = require("core/core").Montage, + AuthorizationManager = require("data/service/authorization-manager").AuthorizationManager, + AuthorizationPolicy = require("data/service/authorization-policy").AuthorizationPolicy, + 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, + Map = require("collections/map"), + Promise = require("core/promise").Promise, + ObjectDescriptor = require("core/meta/object-descriptor").ObjectDescriptor, + Set = require("collections/set"), + WeakMap = require("collections/weak-map"); + + +var AuthorizationPolicyType = new Montage(); +AuthorizationPolicyType.NoAuthorizationPolicy = AuthorizationPolicy.NONE; +AuthorizationPolicyType.UpfrontAuthorizationPolicy = AuthorizationPolicy.UP_FRONT; +AuthorizationPolicyType.OnDemandAuthorizationPolicy = AuthorizationPolicy.ON_DEMAND; +AuthorizationPolicyType.OnFirstFetchAuthorizationPolicy = AuthorizationPolicy.ON_FIRST_FETCH; + +/** + * Provides data objects and manages changes to them. + * + * Data service subclasses that implement their own constructor should call this + * class' constructor at the beginning of their constructor implementation + * with code like the following: + * + * DataService.call(this); + * + * Currently only one service tree with one + * [root services]{@link DataService#rootService} is supported, and every + * instance of DataService or a DataService subclasses must either be that root + * service or be set as a descendent of that root service. + * + * @class + * @extends external:Montage + */ +exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { + + /*************************************************************************** + * Initializing + */ + + constructor: { + value: function DataService() { + exports.DataService.mainService = exports.DataService.mainService || this; + this._initializeAuthorization(); + this._initializeOffline(); + } + }, + + /*************************************************************************** + * Serialization + */ + + deserializeSelf: { + value:function (deserializer) { + var value; + value = deserializer.getProperty("childServices"); + if (value) { + this.registerChildServices(value); + } + + value = deserializer.getProperty("model") || deserializer.getProperty("binder"); + if (value) { + this.model = value; + } + + value = !this.model && deserializer.getProperty("types"); + if (value) { + Array.prototype.push.apply(this._childServiceTypes, value); + } + + value = deserializer.getProperty("mappings"); + if (value) { + Array.prototype.push.apply(this._childServiceMappings, value); + } + + value = deserializer.getProperty("delegate"); + if (value) { + this.delegate = value; + } + + } + }, + + delegate: { + value: null + }, + + /*************************************************************************** + * Basic Properties + * + * Private properties are defined where they are used, not here. + */ + + /** + * 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. + * + * The default implementation of this property returns the union of all + * types handled by child services of this service. Subclasses without child + * services should override this to directly return an array of the specific + * types they handle. + * + * Applications typically have one [raw data service]{@link RawDataService} + * service for each set of related data types and one + * [main service]{@link DataService.mainService} which is the parent of all + * those other services and delegates work to them based on the type of data + * to which the work applies. It is possible for child data services to have + * children of their own and delegate some or all of their work to them. + * + * A service's types must not be changed after it is added as a child of + * another service. + * + * @type {Array.} + */ + types: { + get: function () { + return this._childServiceTypes; + } + }, + + + /** + * The data mappings used by this service to convert objects to raw + * data and vice-versa. + * + * @type {Array.} + */ + mappings: { + get: function () { + return this._childServiceMappings; + } + }, + + /*************************************************************************** + * Service Hierarchy + */ + + /** + * 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; + } + }, + + /** + * 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 + }, + + /** + * 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; + } + }, + + + /** + * Convenience method to assess if a dataService is the rootService + * + * @type {Boolean} + */ + isRootService: { + get: function () { + return this === this.rootService; + } + }, + + /** + * 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; + } + }, + + /** + * 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 + }, + + /** + * 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."); + } + } + }, + + _addChildService: { + value: function (child, types) { + var children, type, i, n, nIfEmpty = 1; + types = types || child.model && child.model.objectDescriptors || child.types; + // If the new child service already has a parent, remove it from + // that parent. + if (child._parentService) { + child._parentService.removeChildService(child); + } + + // Add the new child to this service's children set. + this.childServices.add(child); + this._childServicesByIdentifier.set(child.identifier, child); + // Add the new child service to the services array of each of its + // types or to the "all types" service array identified by the + // `null` type, and add each of the new child's types to the array + // of child types if they're not already there. + + 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.push(child); + if (children.length === 1) { + this._childServicesByType.set(type, children); + if (type) { + this._childServiceTypes.push(type); + } + } + } + // Set the new child service's parent. + child._parentService = this; + } + }, + + __childServiceRegistrationPromise: { + value: null + }, + + _childServiceRegistrationPromise: { + get: function() { + return this.__childServiceRegistrationPromise || (this.__childServiceRegistrationPromise = Promise.resolve()); + }, + set: function(value) { + this.__childServiceRegistrationPromise = value; + } + }, + + registerChildServices: { + value: function (childServices) { + var self; + if (!this.__childServiceRegistrationPromise) { + self = this; + this.__childServiceRegistrationPromise = Promise.all(childServices.map(function (child) { + return self.registerChildService(child); + })); + } + } + }, + + /** + * 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] || + []; + + return child._childServiceRegistrationPromise.then(function () { + return self._registerChildServiceTypesAndMappings(child, types, mappings); + }); + } + }, + + // #1 resolve asynchronous types + // -- types are arrays + // -- contents of the array can be: + // ---- an objectDescriptor or + // ---- a promise for an objectDescriptor or + // ---- a promise for an array of objectDescriptors + // -- flatten the result + // #2 map module id to object descriptor + // #3 register mapping to objectDescriptor + // -- resolve the mappings references + // -- map objectDescriptor to mapping + // #4 make prototype for object descriptor + // -- map constructor to prototype + // -- map objectDescriptor to prototype + // -- map objectDescriptor to dataTriggers + + // -- TODO: dataTriggers should be derived from all properties - mapping requisitePropertyNames + + _registerChildServiceTypesAndMappings: { + value: function (child, types, mappings) { + var self = this, + objectDescriptors; + return this._resolveAsynchronousTypes(types).then(function (descriptors) { + objectDescriptors = descriptors; + self._registerTypesByModuleId(objectDescriptors); + return self._registerChildServiceMappings(child, mappings); + }).then(function () { + return self._makePrototypesForTypes(objectDescriptors); + }).then(function () { + self.addChildService(child, types); + return null; + }); + } + }, + + _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); + } + }, + + _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; + }); + } + }, + + _registerChildServiceMappings: { + value: function (child, mappings) { + var self = this; + return Promise.all(mappings.map(function (mapping) { + return self._addMappingToChild(mapping, child); + })); + } + }, + + _makePrototypesForTypes: { + value: function (types) { + var self = this; + return Promise.all(types.map(function (objectDescriptor) { + return self._makePrototypeForType(objectDescriptor); + })); + } + }, + + _makePrototypeForType: { + value: function (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 = self.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; + }); + } + }, + + _addMappingToChild: { + 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[type]; + mapping.objectDescriptor = objectDescriptor; + mapping.schemaDescriptor = schemaDescriptor; + mapping.service = child; + child.addMappingForType(mapping, objectDescriptor); + return null; + }); + } + }, + + _objectDescriptorForType: { + value: function (type) { + return this._constructorToObjectDescriptorMap.get(type) || + typeof type === "string" && this._moduleIdToObjectDescriptorMap[type] || + type; + } + }, + + _constructorToObjectDescriptorMap: { + get: function () { + if (!this.__constructorToObjectDescriptorMap) { + this.__constructorToObjectDescriptorMap = new Map(); + } + return this.__constructorToObjectDescriptorMap; + } + }, + + _moduleIdToObjectDescriptorMap: { + get: function () { + if (!this.__moduleIdToObjectDescriptorMap) { + this.__moduleIdToObjectDescriptorMap = {}; + } + return this.__moduleIdToObjectDescriptorMap; + } + }, + + /** + * Remove a raw data service as a child of this service and clear its parent + * if that service is a child of this service. + * + * The performance of this method is O(m) + O(n), where m is the number of + * children of this service handling the same type as the child service to + * remove and n is the number of types handled by all children of this + * service. + * + * @method + * @argument {RawDataService} service + * @argument {Array} [types] Types to use instead of the child's types. + */ + removeChildService: { + value: function (child, types) { + var type, chidren, index, i, n; + types = types || child.types; + // Remove the child service from the services array of each of its + // types or from the "all types" service array identified by the + // `null` type, or remove a type altogether if its service array + // only contains the child service to remove, or remove the "all + // types" service array if it only contains the child service to + // 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); + index = chidren ? chidren.indexOf(child) : -1; + if (index >= 0 && chidren.length > 1) { + chidren.splice(index, 1); + } else if (index === 0) { + this._childServicesByType.delete(type); + index = type ? this._childServiceTypes.indexOf(type) : -1; + if (index >= 0) { + this._childServiceTypes.splice(index, 1); + } + } + } + // Remove the child from this service's children set. + this.childServices.delete(child); + // Clear the service parent if appropriate. + if (child._parentService === this) { + child._parentService = undefined; + } + } + }, + + /** + * Alternative to [removeChildService()]{@link DataService#removeChildService}. + * While removeChildService is synchronous, unregisterChildService 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 removeChildService. + * + * @method + * @return {Promise} + */ + unregisterChildService: { + value: function (child) { + var self = this; + return new Promise(function (resolve, reject) { + self.removeChildService(child, child.types); + resolve(); + }); + } + }, + + /** + * 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: function () { + if (!this.__childServicesByType) { + this.__childServicesByType = new Map(); + } + return this.__childServicesByType; + } + }, + + __childServicesByType: { + value: undefined + }, + + _childServicesByIdentifier: { + get: function () { + if (!this.__childServicesByIdentifier) { + this.__childServicesByIdentifier = new Map(); + } + return this.__childServicesByIdentifier; + } + }, + + __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}. + * + * @private + * @type {Array.} + */ + _childServiceTypes: { + get: function() { + if (!this.__childServiceTypes) { + this.__childServiceTypes = []; + } + return this.__childServiceTypes; + } + }, + + __childServiceTypes: { + value: undefined + }, + + /** + * Get the first child service that can handle the specified object, + * or `null` if no such child service exists. + * + * @private + * @method + * @argument {Object} object + * @returns DataService + */ + _getChildServiceForObject: { + value: function (object) { + return this.childServiceForType(this.rootService._getObjectType(object)); + } + }, + + /** + * 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; + } + }, + + + /*************************************************************************** + * 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) { + this._mappingByType.set(type, mapping); + } + }, + + /** + * 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. + */ + mappingWithType: { + value: function (type) { + var mapping; + type = this._objectDescriptorForType(type); + mapping = this._mappingByType.has(type) && this._mappingByType.get(type); + return mapping || null; + } + }, + + _mappingByType: { + get: function () { + if (!this.__mappingByType) { + this.__mappingByType = new Map(); + } + return this.__mappingByType; + } + }, + + __mappingByType: { + value: undefined + }, + + _childServiceMappings: { + get: function () { + if (!this.__childServiceMappings) { + this.__childServiceMappings = []; + } + return this.__childServiceMappings; + } + }, + + __childServiceMappings: { + value: undefined + }, + + /*************************************************************************** + * 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. + */ + model: { + value: undefined + }, + + /** + * 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 + }, + + /*************************************************************************** + * 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(); + } + } + }, + + + + /** + * Returns the AuthorizationPolicyType used by this DataService. + * + * @type {AuthorizationPolicyType} + */ + authorizationPolicy: { + value: AuthorizationPolicyType.NoAuthorizationPolicy + }, + + /** + * holds authorization object after a successfull authorization + * + * @type {Object} + */ + + authorization: { + value: undefined + }, + + authorizationPromise: { + value: Promise.resolve() + }, + + /** + * 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 {string[]} + */ + authorizationServices: { + value: null + }, + + /** + * @type {string} + * @description Module ID of the panel component used to gather necessary authorization information + */ + authorizationPanel: { + value: undefined + }, + + /** + * 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 + }, + + + /** + * + * + * @method + * @returns Promise + */ + authorize: { + value: function () { + console.warn("DataService.authorize() must be overridden by the implementing service", arguments); + return this.nullPromise; + } + }, + /** + * + * @method + * @returns Promise + */ + logOut: { + value: function () { + console.warn("DataService.logOut() must be overridden by the implementing service"); + return this.nullPromise; + } + }, + + /*************************************************************************** + * 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. + * @param {object} + * @returns {ObjectDescriptor|null} if an object descriptor is not found this + * method will return null. + */ + 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 || this.parentService && this.parentService.objectDescriptorForObject(object); + } + }, + + /** + * Get the type of the specified data object. + * + * @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. + */ + _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; + } + }, + + _getModuleIdForObject: { + value: function (object) { + var info = Montage.getInfoForObject(object); + return [info.moduleId, info.objectName].join("/"); + } + }, + + /** + * Register the type of the specified data object if necessary. + * + * @private + * @method + * @argument {Object} object + * @argument {DataObjectDescriptor} type + */ + _setObjectType: { + value: function (object, type) { + if (this._getObjectType(object) !== type){ + this._typeRegistry.set(object, type); + } + } + }, + + _typeRegistry: { + get: function () { + if (!this.__typeRegistry){ + this.__typeRegistry = new WeakMap(); + } + return this.__typeRegistry; + } + }, + + /*************************************************************************** + * Data Object Triggers + */ + + /** + * 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 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); + this._dataObjectTriggers.set(type, DataTrigger.addTriggers(this, type, prototype)); + + //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 + // } + // }); + + } + 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. + * + * @private + * @method + * @argument {Object} object + * @returns {Object} + */ + _getTriggersForObject: { + value: function (object) { + var type = this._getObjectType(object); + return type && this._dataObjectTriggers.get(type); + } + }, + + _dataObjectPrototypes: { + get: function () { + if (!this.__dataObjectPrototypes){ + this.__dataObjectPrototypes = new Map(); + } + return this.__dataObjectPrototypes; + } + }, + + __dataObjectPrototypes: { + value: undefined + }, + + _dataObjectTriggers: { + get: function () { + if (!this.__dataObjectTriggers){ + this.__dataObjectTriggers = new Map(); + } + return this.__dataObjectTriggers; + } + }, + + __dataObjectTriggers: { + value: undefined + }, + + /*************************************************************************** + * Data Object Properties + */ + + /** + * Since root services are responsible for triggering data objects fetches, + * subclasses whose instances will not be root services should override this + * method to call their root service's implementation of it. + * + * @todo Rename and document API and implementation. + * + * @method + */ + decacheObjectProperties: { + value: function (object, propertyNames) { + if (this.isRootService) { + var names = Array.isArray(propertyNames) ? propertyNames : arguments, + start = names === propertyNames ? 0 : 1, + triggers = this._getTriggersForObject(object), + trigger, i, n; + for (i = start, n = names.length; i < n; i += 1) { + trigger = triggers && triggers[names[i]]; + if (trigger) { + trigger.decacheObjectProperty(object); + } + } + } + else { + this.rootService.decacheObjectProperties(object, propertyNames); + } + + } + }, + + /** + * 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 + * requested. + * + * To force an update of a value that was previously obtained or set, use + * [updateObjectProperties()]{@link DataService#updateObjectProperties} + * instead of this method. + * + * Since root services are responsible for determining when to fetch or + * update data objects values, subclasses whose instances will not be root + * services should override this method to call their root service's + * implementation of it. + * + * Subclasses should define how property values are obtained by overriding + * [fetchObjectProperty()]{@link DataService#fetchObjectProperty} instead + * of this method. That method will be called by this method when needed. + * + * Although this method returns a promise, the requested data will not be + * passed in to the promise's callback. Instead that callback will received + * a `null` value and the requested values will be set on the specified + * properties of the object passed in. Those values can be accessed there + * when the returned promise is fulfilled, as in the following code: + * + * myService.getObjectProperties(myObject, "x", "y").then(function () { + * someFunction(myObject.x, myObject.y); + * } + * + * @method + * @argument {object} object - The object whose property values are + * being requested. + * @argument {string[]} propertyNames - The names of each of 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. + */ + getObjectProperties: { + value: function (object, propertyNames) { + 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 names = Array.isArray(propertyNames) ? propertyNames : arguments, + start = names === propertyNames ? 0 : 1; + return this._getOrUpdateObjectProperties(object, names, start, false); + } + else { + return this.rootService.getObjectProperties(object, propertyNames); + } + } + }, + + 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, + * forcing asynchronous values to be re-fetched and updated even if they + * had previously been fetched or set. + * + * Except for the forced update, this method behaves exactly like + * [getObjectProperties()]{@link DataService#getObjectProperties}. + * + * Since root services are responsible for determining when to fetch or + * update data objects values, subclasses whose instances will not be root + * services should override this method to call their root service's + * implementation of it. + * + * Subclasses should define how property values are obtained by overriding + * [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. + * @argument {string[]} propertyNames - The names of each of 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. + */ + updateObjectProperties: { + value: function (object, propertyNames) { + 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 names = Array.isArray(propertyNames) ? propertyNames : arguments, + start = names === propertyNames ? 0 : 1; + return this._getOrUpdateObjectProperties(object, names, start, true); + } + else { + return this.rootService.updateObjectProperties(object, propertyNames); + } + } + }, + + /** + * 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. + * + * @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._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; + } + }, + + _delegateFunctionForPropertyName: { + value: function (propertyName) { + var capitalized = propertyName.charAt(0).toUpperCase() + propertyName.slice(1), + functionName = "fetch" + capitalized + "Property"; + return typeof this[functionName] === "function" && this[functionName]; + } + }, + + _fetchObjectPropertyWithPropertyDescriptor: { + value: function (object, propertyName, propertyDescriptor) { + var self = this, + objectDescriptor = propertyDescriptor.owner, + mapping = objectDescriptor && this.mappingWithType(objectDescriptor), + data = {}; + + if (mapping) { + + Object.assign(data, this.snapshotForObject(object)); + + return mapping.mapObjectToCriteriaSourceForProperty(object, data, propertyName).then(function() { + Object.assign(data, self.snapshotForObject(object)); + return mapping.mapRawDataToObjectProperty(data, object, propertyName); + }); + } else { + return this.nullPromise; + } + + + //return mapping. + // (object,{}, propertyName); + + } + }, + + /** + * @private + * @method + */ + _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); + } + } + } + // if (names.indexOf("geometryType")) { + // + // } + // 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 + */ + + /** + * Find an existing data object corresponding to the specified raw data, or + * if no such object exists, create one. + * + * 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 {DataObjectDescriptor} 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} + * method, that can help in getting + * or creating the object. + * @returns {Object} - The existing object with the unique identifier + * specified in the raw data, or if no such object exists a newly created + * object of the specified type. + */ + getDataObject: { + value: function (type, data, context, dataIdentifier) { + if (this.isRootService) { + var dataObject; + // TODO [Charles]: Object uniquing. + if (this.isUniquing && dataIdentifier) { + dataObject = this.objectForDataIdentifier(dataIdentifier); + } + if (!dataObject) { + dataObject = this._createDataObject(type, dataIdentifier); + } + + return dataObject; + } + else { + return this.rootService.getDataObject(type, data, context, dataIdentifier); + } + + } + }, + + isUniquing: { + value: false + }, + + _identifier: { + value: undefined + }, + + identifier: { + get: function() { + return this._identifier || (this._identifier = Montage.getInfoForObject(this).moduleId); + } + }, + + __dataIdentifierByObject: { + value: null + }, + + _dataIdentifierByObject: { + get: function() { + return this.__objectsByDataIdentifier || (this.__objectsByDataIdentifier = new WeakMap()); + } + }, + + /** + * 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); + } + }, + + /** + * Records an object's DataIdentifier + * + * @method + * @argument {object} object - an Object. + * @argument {DataIdentifier} dataIdentifier - The object whose property values are + */ + recordDataIdentifierForObject: { + value: function(dataIdentifier, object) { + this._dataIdentifierByObject.set(object, dataIdentifier); + } + }, + + /** + * Remove an object's DataIdentifier + * + * @method + * @argument {object} object - an object + */ + removeDataIdentifierForObject: { + value: function(object) { + this._dataIdentifierByObject.delete(object); + } + }, + + __objectByDataIdentifier: { + value: null + }, + + _objectByDataIdentifier: { + get: function() { + return this.__objectByDataIdentifier || (this.__objectByDataIdentifier = new WeakMap()); + } + }, + /** + * 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 - object + * @returns {DataIdentifier} - object's DataIdentifier + */ + objectForDataIdentifier: { + value: function(dataIdentifier) { + return this._objectByDataIdentifier.get(dataIdentifier); + } + }, + /** + * Records an object's DataIdentifier + * + * @method + * @argument {DataIdentifier} dataIdentifier - DataIdentifier + * @argument {object} object - object represented by dataIdentifier + */ + recordObjectForDataIdentifier: { + value: function(object, dataIdentifier) { + this._objectByDataIdentifier.set(dataIdentifier, object); + } + }, + + /** + * Remove an object's DataIdentifier + * + * @method + * @argument {object} object - an object + */ + removeObjectForDataIdentifier: { + value: function(dataIdentifier) { + this._objectByDataIdentifier.delete(dataIdentifier); + } + }, + + /** + * 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 {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; + } else { + this.rootService.createDataObject(type); + } + } + }, + + /** + * Create a data object without registering it in the new object map. + * + * @private + * @method + * @argument {DataObjectDescriptor} type - The type of object to create. + * @returns {Object} - The created object. + */ + _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); + // } + + object = object.constructor.call(object) || object; + if (object) { + this._setObjectType(object, objectDescriptor); + } + } + return object; + } + }, + + /** + * Register an object with its dataIdentifier for uniquing reasons + * + * @private + * @method + * @argument {Object} object - object to register. + * @argument {DataIdentifier} dataIdentifier - dataIdentifier of object to register. + * @returns {void} + */ + registerUniqueObjectWithDataIdentifier: { + value: function(object, dataIdentifier) { + if (object && dataIdentifier && this.isRootService && this.isUniquing) { + this.recordDataIdentifierForObject(dataIdentifier, object); + this.recordObjectForDataIdentifier(object, dataIdentifier); + } + } + }, + + /*************************************************************************** + * Data Object Changes + */ + + /** + * 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.} + */ + createdDataObjects: { + get: function () { + if (this.isRootService) { + if (!this._createdDataObjects) { + this._createdDataObjects = new Set(); + } + return this._createdDataObjects; + } + else { + return this.rootService.createdDataObjects; + } + } + }, + + /** + * 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(); + } + } + }, + + _changedDataObjects: { + value: undefined + }, + + /*************************************************************************** + * Fetching Data + */ + + /** + * Fetch data from the service using its child services. + * + * This method accept [types]{@link DataObjectDescriptor} 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 + * [stream]{DataStream}, creating one if necessary, and the stream will + * include a reference to the query. Also, if a child service's + * implementation of this method return `undefined` or `null`, this method + * will return the stream passed in to the call to that child. + * + * The requested data may be fetched asynchronously, in which case the data + * stream will be returned immediately but the stream's data will be added + * to the stream at a later time. + * + * @method + * @argument {DataQuery|DataObjectDescriptor|ObjectDescriptor|Function|String} + * queryOrType - If this argument's value is a query + * it will define what type of data should + * be returned and what criteria that data + * should satisfy. If the value is a type + * it will only define what type of data + * should be returned, and the criteria + * that data should satisfy can be defined + * using the `criteria` argument. A type + * is defined as either a DataObjectDesc- + * riptor, an Object Descriptor, a Construct- + * or the string module id. The method will + * convert the passed in type to a Data- + * ObjectDescriptor (deprecated) or an + * ObjectDescriptor. This is true whether + * passing in a DataQuery or a type. + * @argument {?Object} + * optionalCriteria - If the first argument's value is a + * type this argument can optionally be + * provided to defines the criteria which + * the returned data should satisfy. + * If the first argument's value is a + * query this argument should be + * omitted and will be ignored if it is + * provided. + * @argument {?DataStream} + * optionalStream - The stream to which the provided data + * should be added. If no stream is + * provided a stream will be created and + * returned by this method. + * @returns {?DataStream} - The stream to which the fetched data objects + * were or will be added, whether this stream was provided to or created by + * this method. + */ + fetchData: { + value: function (queryOrType, optionalCriteria, optionalStream) { + var self = this, + isSupportedType = !(queryOrType instanceof DataQuery), + type = isSupportedType && queryOrType, + criteria = optionalCriteria instanceof DataStream ? undefined : optionalCriteria, + query = type ? DataQuery.withTypeAndCriteria(type, criteria) : queryOrType, + stream = optionalCriteria instanceof DataStream ? optionalCriteria : optionalStream; + + // make sure type is an object descriptor or a data object descriptor. + query.type = this._objectDescriptorForType(query.type); + // Set up the stream. + stream = stream || new DataStream(); + stream.query = query; + stream.dataExpression = query.selectExpression; + + this._dataServiceByDataStream.set(stream, this._childServiceRegistrationPromise.then(function() { + 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 + //the service to answer that to itself + if (self.parentService && self.parentService.childServiceForType(query.type) === self && typeof self.fetchRawData === "function") { + service = self; + service._fetchRawData(stream); + } else { + + // Use a child service to fetch the data. + try { + + service = self.childServiceForType(query.type); + if (service) { + stream = service.fetchData(query, stream) || stream; + self._dataServiceByDataStream.set(stream, service); + } else { + throw new Error("Can't fetch data of unknown type - " + (query.type.typeName || query.type.name) + "/" + query.type.uuid); + } + } catch (e) { + stream.dataError(e); + } + } + + return service; + })); + // Return the passed in or created stream. + return stream; + } + }, + + _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); + } + }, + + /** + * 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 {DataStream} [dataStream] - The DataStream to cancel + * @argument {Object} [reason] - An object indicating the reason to cancel. + * + */ + 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); + } + } + + } + }, + + _cancelServiceDataStream: { + value: function (rawDataService, dataStream, reason) { + rawDataService.cancelRawDataStream(dataStream, reason); + this._dataServiceByDataStream.delete(dataStream); + } + }, + + /*************************************************************************** + * Saving Data + */ + + /** + * 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 saved = !this.createdDataObjects.has(object); + return this._updateDataObject(object, saved && "deleteDataObject"); + } + }, + + /** + * Save changes made to a data object. + * + * @method + * @argument {Object} object - The object whose data should be saved. + * @returns {external:Promise} - A promise fulfilled when all of the data in + * the changed object has been saved. + */ + saveDataObject: { + value: function (object) { + //return this._updateDataObject(object, "saveDataObject"); + + var self = this, + service, + promise = this.nullPromise, + 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 () { + self.rootService.createdDataObjects.delete(object); + return null; + }); + }); + } + else { + service = this._getChildServiceForObject(object); + if (service) { + return service.saveDataObject(object); + } + else { + return promise; + } + } + } + }, + + + _updateDataObject: { + value: function (object, action) { + var self = this, + service, + promise = this.nullPromise; + + if (this.parentService && this.parentService._getChildServiceForObject(object) === this) { + service = action && this; + } + else { + service = action && this._getChildServiceForObject(object); + if (service) { + return service._updateDataObject(object, action); + } + } + + if (!action) { + self.createdDataObjects.delete(object); + } else if (service) { + promise = service[action](object).then(function () { + self.createdDataObjects.delete(object); + return null; + }); + } + return promise; + } + }, + + _saveDataObject: { + value: function (object) { + var record = {}; + this._mapObjectToRawData(object, record); + return this.saveRawData(record, object); + } + }, + // _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 + */ + + _initializeOffline: { + value: function () { + // TODO: This code assumes that the first instance of DataService or + // of one of its subclasses is either the + // root service, and that no instance of DataService subclasses are. + // This needs to be fixed to allow DataService child services and + // DataService subclass root services. + var self = this; + if ( + typeof global.addEventListener === 'function' && + !exports.DataService.prototype._isOfflineInitialized + ) { + exports.DataService.prototype._isOfflineInitialized = true; + global.addEventListener('online', function (event) { + self.rootService.isOffline = false; + }); + global.addEventListener('offline', function (event) { + self.rootService.isOffline = true; + }); + } + } + }, + + _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; + return this.readOfflineOperations().then(function (operations) { + operations.sort(this._compareOfflineOperations); + return self.performOfflineOperations(operations); + }).catch(function (e) { + console.error(e); + }); + } + }, + + _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; + } + }, + + /** + * 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; + } + }, + + /** + * @private + * @type {Map} + */ + _offlineOperationServices: { + get:function() { + if (!this.__offlineOperationServices) { + this.__offlineOperationServices = new WeakMap(); + } + return this.__offlineOperationServices; + } + }, + + __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; + return promise.then(function () { + return child ? + child.performOfflineOperations(operations.slice(start, end)) : + self._performAndDeleteOfflineOperation(operations[start]); + }); + } + }, + + _performAndDeleteOfflineOperation: { + value: function(operation) { + //Before we perform an operation, we need to look a foreignKeys in jOperation changes to update if needed before performing the operation + //if we don't have a known list of foreign keys, we'll consider all potential candidate + var self = this, + operationType = operation.type, + tableSchema, foreignKeys, + k, countK, kOnlinePrimaryKey, kForeignKey; + + if (this.offlineService) { + tableSchema = this.offlineService.schema[operationType]; + foreignKeys = tableSchema.foreignKeys; + } + + if (!foreignKeys) { + foreignKeys = tableSchema._computedForeignKeys || + (tableSchema._computedForeignKeys = Object.keys(operation.changes)); + } + + for (k=0, countK = foreignKeys.length;k} 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; + } + }, + + /*************************************************************************** + * Utilities + */ + + /** + * 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; + } + }, + + _nullPromise: { + value: undefined + }, + + /** + * @todo Document. + */ + emptyArrayPromise: { + get: function () { + if (!exports.DataService._emptyArrayPromise) { + exports.DataService._emptyArrayPromise = Promise.resolve([]); + } + return exports.DataService._emptyArrayPromise; + } + }, + + _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 + * complete. This is useful for services that need to buffer up actions so + * they're committed only once in a given event loop. + * + * @type {external:Promise} + */ + eventLoopPromise: { + get: function () { + var self = this; + if (!this._eventLoopPromise) { + this._eventLoopPromise = new Promise(function (resolve, reject) { + setTimeout(function () { + self._eventLoopPromise = undefined; + resolve(); + }, 0); + }); + } + return this._eventLoopPromise; + } + }, + + /** + * Splice an array into another array. + * + * @method + * @argument {Array} array - The array to modify. + * @argument {Array} insert - The items to splice into that array. + * @argument {number} index - The index at which to splice those items, by + * default `0`. + * @argument {number} length - The number of items of the original array to + * replace with items from the spliced array, by + * default `array.length`. + */ + spliceWithArray: { + value: function (array, insert, index, length) { + index = index || 0; + length = length || length === 0 ? length : Infinity; + return insert ? array.splice.apply(array, [index, length].concat(insert)) : + array.splice(index, length); + } + } + + +}, /** @lends DataService */ { + + /*************************************************************************** + * Service Hierarchy + */ + + /** + * A reference to the application's main service. + * + * Applications typically have one and only one + * [root service]{@link DataService#rootService} to which all data requests + * are sent, and this is called the application's main service. That service + * can in turn delegate handling of different types of data to child + * services specialized by type. + * + * This property will be set automatically if the {@link DataService} + * constructor is called and if the first service created is either the + * main service or a descendent of the main service. + * + * @type {DataService} + */ + mainService: { + get: function () { + if (this._mainService && this._mainService.parentService) { + this._mainService = this._mainService.rootService; + } + return this._mainService; + }, + set: function (service) { + this._mainService = service; + } + }, + + /*************************************************************************** + * Authorization + */ + + AuthorizationPolicyType: { + value: AuthorizationPolicyType + }, + + AuthorizationPolicy: { + value: AuthorizationPolicy + }, + + authorizationManager: { + value: AuthorizationManager + } + +}); diff --git a/data/service/data-stream.js b/data/service/data-stream.js new file mode 100644 index 0000000000..758b55974b --- /dev/null +++ b/data/service/data-stream.js @@ -0,0 +1,362 @@ +// Note: Montage's promises are used even if ECMAScript 6 promises are available. +var DataProvider = require("data/service/data-provider").DataProvider, + DataObjectDescriptor = require("data/model/data-object-descriptor").DataObjectDescriptor, + DataQuery = require("data/model/data-query").DataQuery, + Promise = require("core/promise").Promise, + deprecate = require("core/deprecate"), + parse = require("frb/parse"), + Scope = require("frb/scope"), + compile = require("frb/compile-evaluator"); + +/** + * A [DataProvider]{@link DataProvider} whose data is received sequentially. + * A DataStream is also a [promise]{@linkcode external:Promise} which is + * fulfilled when all the data it expects has been received. + * + * Objects receiving data from a stream will use its + * [data]{@link DataStream#data} property to access that data. Alternatively + * they can use its [then()]{@link DataStream#then} method to get that data or + * to handle errors, or its [catch()]{@link DataStream#catch} method to handle + * errors. + * + * Objects feeding data to a stream will use its + * [addData()]{@link DataStream#addData} method to add that data and its + * [dataDone()]{@link DataStream#dataDone} method to indicate that all available + * data has been added or its [dataError()]{@link DataStream#dataError} method + * to indicate an error occurred. + * + * Objects can either receive data from a stream or add data to it, but not + * both. Additionally, only one object can ever add data to a particular + * stream. Typically that object will be a [Service]{@link DataService}. + * + * Each stream is also a [promise]{@linkcode external:Promise} that becomes + * fulfilled when all the data expected for it is first received and + * [dataDone()]{@link DataStream#dataDone} is called, or rejected when an + * error is first encountered and [dataError()]{@link DataStream#dataError} + * is called. Each such promise is fulfilled or rejected only once and will + * not be fulfilled or rejected again if the stream's data changes or if an + * error is encountered subsequently for any reason. + * + * @class + * @extends DataProvider + * + */ +exports.DataStream = DataProvider.specialize(/** @lends DataStream.prototype */ { + + /*************************************************************************** + * Basic properties + */ + + /** + * The query defining the data returned in this stream. + * + * @type {DataQuery} + */ + query: { + value: undefined + }, + + /** + * The selector defining the data returned in this stream. + * + * @type {DataQuery} + */ + selector: { + get: deprecate.deprecateMethod(void 0, function () { + return this.query; + }, "selector", "query"), + set: deprecate.deprecateMethod(void 0, function (value) { + this.query = value; + }, "selector", "query") + }, + /*************************************************************************** + * DataProvider behavior + */ + + /** + * The objects that have been added to the stream, as defined in this class' + * [DataProvider]{@link DataProvider} superclass. This array is created + * lazilly the first time it is needed and then not allowed to change, + * though its contents can and typically will change. + * + * @type {Array} + */ + data: { + get: function() { + if (!this._data) { + this._data = []; + } + return this._data; + } + }, + + /** + * Request specific data, as defined in this class' + * [DataProvider]{@link DataProvider} superclass. Calling this method has + * no effect as data will come in the order in which it is added to the + * stream and this order cannot be changed. + * + * @method + * @argument {int} start - See [superclass]{@link DataProvider#requestData}. + * @argument {int} length - See [superclass]{@link DataProvider#requestData}. + */ + requestData: { + value: function (start, length) { + // Don't do anything. + return this; + } + }, + + /*************************************************************************** + * Promise behavior + */ + + _resolve: { + value: function (value) { + if (!this.__promise) { + this.__promise = Promise.resolve(value); + } + } + }, + + _reject: { + value: function (reason) { + // Defers the creation of the rejection promise by setting __promise + // to a function that will create the appropriate promise when it + // is needed. This way if the promise is not needed it won't be + // created. This avoids the "unhandled rejection" error that + // Montage's Promises logs for promises that are rejected but whose rejection + // is not handled. + if (!this.__promise) { + this.__promise = function () {return Promise.reject(reason);}; + } + } + }, + + _promise: { + get: function () { + var self = this; + if (typeof this.__promise === "function") { + this.__promise = this.__promise(); + } else if (!this.__promise) { + this.__promise = new Promise(function(resolve, reject) { + self._resolve = resolve; + self._reject = reject; + }); + } + return this.__promise; + } + }, + + /** + * Method of the [Promise]{@linkcode external:Promise} class used to + * kick off additional processing when all the data expected by this + * stream has been received or when an error has been encountered. + * + * @method + * @argument {OnFulfilled} onFulfilled - Called when the stream's + * [dataDone()]{@link DataStream#dataDone} + * method is called, usually after all + * the data expected for the stream + * has been sent to it. Because a + * stream's selector can change after + * that, or changes in the service + * data can occur for other reasons, + * it is possible for a stream's + * [data]{@link DataStream#data} array + * contents to change after this + * callback is called. If that happens + * this callback will not be called + * again. This callback therefore only + * provides an indication of when the + * first set of data expected by a + * stream is received. The value + * passed in to this callback is the + * stream's {@link DataStream#data}. + * @argument {OnRejected} [onRejected] - Called when the stream's + * [dataError()]{@link DataStream#dataError} + * method is called, usually after + * an error is encountered while + * fetching data for the stream. + * The value passed in to this + * callback will be the `reason` + * received by the stream's + * [dataError()]{@link DataStream#dataError} + * method. Because + * [catch()]{@link DataStream#catch} + * also handles the case where + * exceptions are encountered + * in the `onFulfilled` + * callback, this argument is + * usually not provided and + * [catch()]{@link DataStream#catch} + * is usually used instead to specify + * the `onRejected` callback. + */ + then: { + value: function (onFulfilled, onRejected) { + return this._promise.then(onFulfilled, onRejected); + } + }, + + /** + * Method of the [Promise]{@linkcode external:Promise} class used to + * kick off additional processing when an error has been encountered. + * + * @method + * @argument {OnRejected} onRejected - Called when the stream's + * [dataError()]{@link DataStream#dataError} + * method is called, usually after + * an error is encountered while + * fetching data for the stream. + * The value passed in to this + * callback will be the `reason` + * received by the stream's + * [dataError()]{@link DataStream#dataError} + * method. + */ + catch: { + value: function (onRejected) { + return this._promise.catch(onRejected); + } + }, + + /*************************************************************************** + * Feeding the stream + */ + + /** + * Add some object to the stream's [data]{@link DataStream#data} array. + * + * @method + * @argument {Array} objects - An array of objects to add to the stream's + * data. If this array is empty, `null`, or + * `undefined`, no objects are added. + */ + addData: { + value: function (objects) { + var data = objects; + + if (this.dataExpression && objects) { + data = this._compiledDataExpression(new Scope(objects)); + } + + if (data && Array.isArray(data)) { + this.data.push.apply(this.data, data); + } else if (data) { + this.data.push(data); + } + } + }, + + /** + * 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 + * all subsequent calls to [dataDone()]{@link DataStream#dataDone} + * or [dataError()]{@link DataStream#dataError} will be ignored. + * + * @method + */ + dataDone: { + value: function () { + this._resolve(this.data); + delete this._resolve; + delete this._reject; + } + }, + + /** + * To be called when a problem is encountered while trying to + * fetch data for this stream. After this is called all subsequent + * calls to [dataError()]{@link DataStream#dataError} or + * [dataDone()]{@link DataStream#dataDone} will be ignored. + * + * @method + * @argument {Object} [reason] - An object, usually an {@link Error}, + * indicating what caused the problem. + * This will be passed in to any + * {@link external:onRejected} + * callback specified in + * [then()]{@link DataStream#then} or + * [catch()]{@link DataStream#catch} calls + * to the stream. + */ + dataError: { + value: function (reason) { + this._reject(reason); + delete this._reject; + delete this._resolve; + } + }, + + _compiledDataExpression: { + get: function () { + return this.__compiledDataExpression || (this.__compiledDataExpression = compile(this._dataExpressionSyntax)); + } + }, + + _dataExpressionSyntax: { + get: function () { + return this.__dataExpressionSyntax || (this.__dataExpressionSyntax = parse(this.dataExpression)); + } + }, + + dataExpression: { + value: undefined + }, + + evaluateDataExpression: { + value: function(value) { + return this._compiledDataExpression(value); + } + }, + + /** + * The time at which data was received by the DataStream + * + * @type {Date} + */ + dataReceptionTime: { + value: undefined + }, + + /** + * The maximum amount of time a DataStream's data will be considered fresh. + * This should take precedence over an ObjectDescriptor's maxAge which should + * take precedence over a DataService's dataMaxAge global default value. + * + * @type {Number} + */ + _dataMaxAge: { + value: undefined + }, + dataMaxAge: { + get: function() { + //The third default should be the service's dataMaxAge, but: + //DataService.[mainService||rootService].dataServiceForDataStream(this) should work + //but is maintained on a per DataService basis and there's no cascading lookup. + //#FixMe So we need to fix this as a DataStream doesn't know which service created it. + return this._dataMaxAge || this.query.type.maxAge; + }, + set: function(value) { + this._dataMaxAge = value; + } + } + + +}, /** @lends DataStream */ { + + /** + * @todo Document. + */ + withTypeOrSelector: { + value: function (typeOrSelector) { + var type = typeOrSelector instanceof DataObjectDescriptor && typeOrSelector, + selector = type && DataQuery.withTypeAndCriteria(type) || typeOrSelector, + stream = new this(); + stream.query = selector; + return stream; + } + } + +}); diff --git a/data/service/data-trigger.js b/data/service/data-trigger.js new file mode 100644 index 0000000000..aa266c2f35 --- /dev/null +++ b/data/service/data-trigger.js @@ -0,0 +1,589 @@ +var Montage = require("core/core").Montage, + DataObjectDescriptor = require("data/model/data-object-descriptor").DataObjectDescriptor, + ObjectDescriptor = require("data/model/object-descriptor").ObjectDescriptor, + WeakMap = require("collections/weak-map"), + DataTrigger; + +/** + * Intercepts all calls to get and set an object's property and triggers any + * Montage Data action warranted by these calls. + * + * DataTrigger is a JavaScript Objects subclass rather than a Montage subclass + * so data triggers can be as lightweight as possible: They need to be + * lightweight because many will be created (one for each relationship or + * lazily loaded property of each model class) and there's no benefit for them + * to be derived from the Montage prototype because they don't use any of the + * Montage class functionality. + * + * @private + * @class + * @extends Object + */ +DataTrigger = exports.DataTrigger = function () {}; + +exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototype */ { + + /** + * The constructor function for all trigger instances. + * + * @type {function} + */ + constructor: { + configurable: true, + writable: true, + value: exports.DataTrigger + }, + + /** + * The service used by this trigger to perform Montage Data actions. + * + * Typically a number of triggers use the same service so this property + * is defined in a single DataTrigger instance for each service and all + * triggers that share that service are then derived from that instance. + * This avoids the need for each trigger to have a reference to the service + * it uses and saves memory. See + * [_getTriggerPrototype()]{@link DataTrigger._getTriggerPrototype} for + * the implementation of this behavior. + * + * @private + * @type {Service} + */ + _service: { + configurable: true, + writable: true, + value: undefined + }, + + /** + * The prototype of objects whose property is managed by this trigger. + * + * @private + * @type {Object} + */ + _objectPrototype: { + configurable: true, + writable: true, + value: undefined + }, + + /** + * The name of the property managed by this trigger. + * + * @private + * @type {string} + */ + _propertyName: { + configurable: true, + writable: true, + value: undefined + }, + + /** + * The name of the private property corresponding to the public property + * managed by this trigger. + * + * The private property name is the + * [_propertyName]{@link DataTrigger#_propertyName} value prefixed with an + * underscore. To minimize the time and memory used by a trigger's call + * intercepts this private property name is generated lazilly the first time + * it is needed and then cached. + * + * @private + * @type {string} + */ + _privatePropertyName: { + configurable: true, + get: function () { + if (!this.__privatePropertyName && this._propertyName) { + this.__privatePropertyName = "_" + this._propertyName; + } + return this.__privatePropertyName; + } + }, + + /** + * Whether this trigger is for a global property or not. + * + * When a trigger is global and the value of the trigger's property is + * obtained or set for one object managed by the trigger then that + * property's value is assumed to have also been obtained or set for all + * objects managed by the trigger. + * + * Setting this value clears the + * [_valueStatus]{@link DataTrigger#_valueStatus} and should only be done + * before a trigger is used. + * + * @private + * @type {string} + */ + _isGlobal: { + configurable: true, + get: function () { + // To save memory a separate _isGlobal boolean is not maintained and + // the _isGlobal value is derived from the _valueStatus type. + return !(this._valueStatus instanceof WeakMap); + }, + set: function (global) { + global = global ? true : false; + if (global !== this._isGlobal) { + this._valueStatus = global ? undefined : new WeakMap(); + } + } + }, + + /** + * For global triggers, holds the status of this trigger's property value. + * For other triggers, holds a map from objects whose property is managed + * by this trigger to the status of that property's value for each of those + * objects. + * + * See [_getValueStatus()]{@link DataTrigger#_getValueStatus} for the + * possible status values. + * + * @private + * @type {Object|WeakMap} + */ + _valueStatus: { + configurable: true, + writable: true, + value: undefined + }, + + /** + * Gets the status of a property value managed by this trigger: + * + * - The status will be `undefined` when the value has not yet been + * requested or set. + * + * - The status will be `null` when the value was requested and has already + * been obtained or when it has been set. + * + * - When the value has been requested but is still in the process of being + * obtained, the status will be an object with a "promise" property set to + * a promise that will be resolved when the value is obtained and a + * "resolve" property set to a function that will resolve that promise. + * + * @private + * @method + * @argument {Object} object + * @returns {Object} + */ + _getValueStatus: { + configurable: true, + value: function (object) { + return this._isGlobal ? this._valueStatus : this._valueStatus.get(object); + } + }, + + /** + * Sets the status of a property value managed by this trigger. + * + * See [_getValueStatus()]{@link DataTrigger#_getValueStatus} for the + * possible status values. + * + * @private + * @method + * @argument {Object} object + * @argument {Object} status + */ + _setValueStatus: { + configurable: true, + value: function (object, status) { + if (this._isGlobal) { + this._valueStatus = status; + } else if (status !== undefined) { + this._valueStatus.set(object, status); + } else { + this._valueStatus.delete(object); + } + } + }, + + /** + * @method + * @argument {Object} object + * @returns {Object} + * + * #Performance #ToDo: Looks like the same walk-up logic + * is going to be done many times for individual instances, + * we should improve that. + */ + _getValue: { + configurable: true, + writable: true, + value: function (object) { + var prototype, descriptor, getter; + // Start an asynchronous fetch of the property's value if necessary. + this.getObjectProperty(object); + // Search the prototype chain for a getter for this property, + // starting just after the prototype that called this method. + prototype = Object.getPrototypeOf(this._objectPrototype); + while (prototype) { + descriptor = Object.getOwnPropertyDescriptor(prototype, this._propertyName); + getter = descriptor && descriptor.get; + prototype = !getter && Object.getPrototypeOf(prototype); + } + // Return the property's current value. + return getter ? getter.call(object) : object[this._privatePropertyName]; + } + }, + + /** + * Note that if a trigger's property value is set after that values is + * requested but before it is obtained from the trigger's service the + * property's value will only temporarily be set to the specified value: + * When the service finishes obtaining the value the property's value will + * be reset to that obtained value. + * + * @method + * @argument {Object} object + * @argument {} value + */ + _setValue: { + configurable: true, + writable: true, + value: function (object, value) { + var status, prototype, descriptor, getter, setter, writable; + // Get the value's current status and update that status to indicate + // the value has been obtained. This way if the setter called below + // requests the property's value it will get the value the property + // had before it was set, and it will get that value immediately. + status = this._getValueStatus(object); + this._setValueStatus(object, null); + // Search the prototype chain for a setter for this trigger's + // property, starting just after the trigger prototype that caused + // this method to be called. + prototype = Object.getPrototypeOf(this._objectPrototype); + while (prototype) { + descriptor = Object.getOwnPropertyDescriptor(prototype, this._propertyName); + getter = descriptor && descriptor.get; + setter = getter && descriptor.set; + writable = !descriptor || setter || descriptor.writable; + prototype = writable && !setter && Object.getPrototypeOf(prototype); + } + // Set this trigger's property to the desired value, but only if + // that property is writable. + if (setter) { + setter.call(object, value); + } else if (writable) { + object[this._privatePropertyName] = value; + } + // Resolve any pending promise for this trigger's property value. + if (status) { + status.resolve(null); + } + } + }, + + /** + * @todo Rename and document API and implementation. + * + * @method + */ + decacheObjectProperty: { + value: function (object) { + this._setValueStatus(object, undefined); + } + }, + /** + * Request a fetch of the value of this trigger's property for the + * specified object but only if that data isn't already in the process + * of being obtained and only if it wasn't previously obtained or + * set. To unconditionally request a fetch of this property data use + * [updateObjectProperty()]{@link DataTrigger#updateObjectProperty}. + * + * @method + * @argument {Object} object + * @returns {external:Promise} + */ + getObjectProperty: { + value: function (object) { + var status = this._getValueStatus(object); + return status ? status.promise : + status === null ? this._service.nullPromise : + this.updateObjectProperty(object); + } + }, + + /** + * If the value of this trigger's property for the specified object isn't in + * the process of being obtained, request the most up to date value of that + * data from this trigger's service. + * + * @method + * @argument {Object} object + * @returns {external:Promise} + */ + updateObjectProperty: { + value: function (object) { + var self = this, + status = this._getValueStatus(object) || {}; + if (!status.promise) { + this._setValueStatus(object, status); + status.promise = new Promise(function (resolve, reject) { + status.resolve = resolve; + status.reject = reject; + self._fetchObjectProperty(object); + }); + } + // Return the existing or just created promise for this data. + return status.promise; + } + }, + + /** + * @private + * @method + * @argument {Object} object + * @returns {external:Promise} + */ + _fetchObjectProperty: { + value: function (object) { + var self = this; + this._service.fetchObjectProperty(object, this._propertyName).then(function () { + return self._fulfillObjectPropertyFetch(object); + }).catch(function (error) { + console.error(error); + return self._fulfillObjectPropertyFetch(object, error); + }); + } + }, + + _fulfillObjectPropertyFetch: { + value: function (object, error) { + var status = this._getValueStatus(object); + this._setValueStatus(object, null); + if (status && !error) { + status.resolve(null); + } else if (status && error) { + console.error(error); + status.reject(error); + } + return null; + } + } + +}); + +Object.defineProperties(exports.DataTrigger, /** @lends DataTrigger */ { + + /** + * @method + * @argument {DataService} service + * @argument {Object} prototype + * @argument {Set} property names to exclude from triggers. + * @returns {Object.} + */ + addTriggers: { + value: function (service, type, prototype, requisitePropertyNames) { + // This function was split into two to provide backwards compatibility + // to existing Montage data projects. Future montage data projects + // should base their object descriptors on Montage's version of object + // descriptor. + var isMontageDataType = type instanceof DataObjectDescriptor || type instanceof ObjectDescriptor; + return isMontageDataType ? this._addTriggersForMontageDataType(service, type, prototype, name) : + this._addTriggers(service, type, prototype, requisitePropertyNames); + } + }, + + _addTriggersForMontageDataType: { + value: function (service, type, prototype) { + var triggers = {}, + names = Object.keys(type.propertyDescriptors), + trigger, name, i; + for (i = 0; (name = names[i]); ++i) { + trigger = this.addTrigger(service, type, prototype, name); + if (trigger) { + triggers[name] = trigger; + } + } + return triggers; + } + }, + + _addTriggers: { + value: function (service, objectDescriptor, prototype, requisitePropertyNames) { + var triggers = {}, propertyDescriptor, trigger, name, i, n; + for (i = 0, n = objectDescriptor.propertyDescriptors.length; i < n; i += 1) { + propertyDescriptor = objectDescriptor.propertyDescriptors[i]; + if (!requisitePropertyNames.has(propertyDescriptor.name)) { + name = propertyDescriptor.name; + trigger = this.addTrigger(service, objectDescriptor, prototype, name); + if (trigger) { + triggers[name] = trigger; + } + } + } + return triggers; + } + }, + + /** + * @method + * @argument {DataService} service + * @argument {Object} prototype + * @argument {string} name + * @returns {?DataTrigger} + */ + addTrigger: { + value: function (service, type, prototype, name) { + // This function was split into two to provide backwards compatibility + // to existing Montage data projects. Future montage data projects + // should base their object descriptors on Montage's version of object + // descriptor. + var isMontageDataType = type instanceof DataObjectDescriptor || type instanceof ObjectDescriptor; + return isMontageDataType ? this._addTriggerForMontageDataType(service, type, prototype, name) : + this._addTrigger(service, type, prototype, name); + } + }, + + _addTriggerForMontageDataType: { + value: function (service, type, prototype, name) { + var descriptor = type.propertyDescriptors[name], + trigger; + if (descriptor && descriptor.isRelationship) { + trigger = Object.create(this._getTriggerPrototype(service)); + trigger._objectPrototype = prototype; + trigger._propertyName = name; + trigger._isGlobal = descriptor.isGlobal; + Montage.defineProperty(prototype, name, { + get: function () { + return trigger._getValue(this); + }, + set: function (value) { + trigger._setValue(this, value); + } + }); + } + return trigger; + } + }, + + /** + * #Performance #ToDO: Here we're creating a trigger instance right away + * when we could do it only when the defineProperty get/set are called. + * This means this method wouldn't return the trigger which is added + * by caller in service._getTriggersForObject. Instead, when created in the + * get/set, we would added it to the service's ._getTriggersForObject. + * First draft is bellow, working with bugs, need baking + * + * @private + * @method + * @argument {DataService} service + * @argument {ObjectDescriptor} objectDescriptor + * @argument {Object} prototype + * @argument {String} name + * @returns {DataTrigger} + */ + _createTrigger: { + value: function(service, objectDescriptor, prototype, name, propertyDescriptor) { + var trigger = Object.create(this._getTriggerPrototype(service)), + serviceTriggers = service._dataObjectTriggers.get(objectDescriptor); + trigger._objectPrototype = prototype; + trigger._propertyName = name; + trigger._isGlobal = propertyDescriptor.isGlobal; + if(!serviceTriggers) { + serviceTriggers = {}; + service._dataObjectTriggers.set(objectDescriptor,serviceTriggers); + } + serviceTriggers[name] = trigger; + return trigger; + } + }, + _addTrigger: { + value: function (service, objectDescriptor, prototype, name) { + var descriptor = objectDescriptor.propertyDescriptorForName(name), + trigger; + if (descriptor) { + trigger = Object.create(this._getTriggerPrototype(service)); + trigger._objectPrototype = prototype; + trigger._propertyName = name; + trigger._isGlobal = descriptor.isGlobal; + if (descriptor.definition) { + Montage.defineProperty(prototype, name, { + get: function () { + if (!this.getBinding(name)) { + this.defineBinding(name, {"<-": descriptor.definition}); + } + return trigger._getValue(this); + // return (trigger||(trigger = DataTrigger._createTrigger(service, objectDescriptor, prototype, name,descriptor)))._getValue(this); + }, + set: function (value) { + trigger._setValue(this, value); + // (trigger||(trigger = DataTrigger._createTrigger(service, objectDescriptor, prototype, name,descriptor)))._setValue(this, value); + } + }); + } else { + Montage.defineProperty(prototype, name, { + get: function () { + return trigger._getValue(this); + // return (trigger||(trigger = DataTrigger._createTrigger(service, objectDescriptor, prototype, name,descriptor)))._getValue(this); + }, + set: function (value) { + trigger._setValue(this, value); + // (trigger||(trigger = DataTrigger._createTrigger(service, objectDescriptor, prototype, name,descriptor)))._setValue(this, value); + } + }); + } + trigger = Object.create(this._getTriggerPrototype(service)); + trigger._objectPrototype = prototype; + trigger._propertyName = name; + trigger._isGlobal = descriptor.isGlobal; + + } + return trigger; + } + }, + + /** + * To avoid having each trigger hold a reference to the service it uses, all + * triggers that use a service are derived from a prototype that contains + * this references. See [_service]{@link DataTrigger#_service} for details. + * + * @private + * @method + * @argument {DataService} service + * @returns {DataTrigger} + */ + _getTriggerPrototype: { + value: function (service) { + var trigger = this._triggerPrototypes && this._triggerPrototypes.get(service); + if (!trigger) { + trigger = new this(); + trigger._service = service; + this._triggerPrototypes = this._triggerPrototypes || new WeakMap(); + this._triggerPrototypes.set(service, trigger); + } + return trigger; + } + }, + + /** + * @method + * @argument {Object.} triggers + * @argument {Object} prototype + */ + removeTriggers: { + value: function (triggers, prototype) { + var triggerNames = Object.keys(triggers), + name, i; + for (i = 0; (name = triggerNames[i]); ++i) { + this.removeTrigger(triggers[name], prototype, name); + } + } + }, + + /** + * @method + * @argument {DataTrigger} trigger + * @argument {Object} prototype + */ + removeTrigger: { + value: function (trigger, prototype) { + if (trigger) { + delete prototype[trigger.name]; + } + } + } + +}); diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js new file mode 100644 index 0000000000..80aa357446 --- /dev/null +++ b/data/service/expression-data-mapping.js @@ -0,0 +1,975 @@ +var DataMapping = require("./data-mapping").DataMapping, + assign = require("frb/assign"), + compile = require("frb/compile-evaluator"), + ObjectDescriptorReference = require("core/meta/object-descriptor-reference").ObjectDescriptorReference, + parse = require("frb/parse"), + MappingRule = require("data/service/mapping-rule").MappingRule, + Promise = require("core/promise").Promise, + Scope = require("frb/scope"), + Set = require("collections/set"); + + + +var ONE_WAY_BINDING = "<-"; +var TWO_WAY_BINDING = "<->"; + +/** + * Maps raw data to data objects, using FRB expressions, of a specific type. + * + * TODO: Write more thorough description. + * + * @class + * @extends external:DataMapping + */ +exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionDataMapping.prototype */ { + + /*************************************************************************** + * Serialization + */ + + serializeSelf: { + value: function (serializer) { + // serializer.setProperty("name", this.name); + // if ((this._model) && (!this.model.isDefault)) { + // serializer.setProperty("model", this._model, "reference"); + // } + // + // if (this.objectDescriptorInstanceModule) { + // serializer.setProperty("objectDescriptorModule", this.objectDescriptorInstanceModule); + // } + } + }, + + deserializeSelf: { + value: function (deserializer) { + var value = deserializer.getProperty("objectDescriptor"); + if (value instanceof ObjectDescriptorReference) { + this.objectDescriptorReference = value; + } else { + this.objectDescriptor = value; + } + + this.schemaReference = deserializer.getProperty("schema"); + + value = deserializer.getProperty("objectMapping"); + if (value) { + this._objectMappingRules = value.rules; + } + value = deserializer.getProperty("rawDataMapping"); + if (value) { + this._rawDataMappingRules = value.rules; + } + value = deserializer.getProperty("requisitePropertyNames"); + if (value) { + this.addRequisitePropertyName.apply(this, value); + } + + value = deserializer.getProperty("rawDataPrimaryKeys"); + if (value) { + this.rawDataPrimaryKeys = value; + } + + } + }, + + + + + /** + * @param {ObjectDescriptor} objectDescriptor - the definition of the objects + * mapped by this mapping. + * @param {DataService} service - the data service this mapping should use. + * @return itself + */ + initWithServiceObjectDescriptorAndSchema: { + value: function (service, objectDescriptor, schema) { + this.service = service; + this.objectDescriptor = objectDescriptor; + this.schemaDescriptor = schema; + return this; + } + }, + + resolveReferences: { + value: function () { + var self = this; + return this._resolveObjectDescriptorReferenceIfNecessary().then(function () { + return self._resolveSchemaReferenceIfNecessary(); + }); + } + }, + + _resolveObjectDescriptorReferenceIfNecessary: { + value: function () { + var self = this, + requiresInitialization = !this.objectDescriptor && this.objectDescriptorReference, + promise = requiresInitialization ? this.objectDescriptorReference.promise(require) : + Promise.resolve(null); + return promise.then(function (objectDescriptor) { + if (objectDescriptor) { + self.objectDescriptor = objectDescriptor; + } + return null; + }); + } + }, + + _resolveSchemaReferenceIfNecessary: { + value: function () { + var self = this, + requiresInitialization = !this.schemaDescriptor && this.schemaDescriptorReference, + promise = requiresInitialization ? this.schemaReference.promise(require) : + Promise.resolve(null); + return promise.then(function (objectDescriptor) { + if (objectDescriptor) { + self.schemaDescriptor = objectDescriptor; + } + return null; + }); + } + }, + + /*************************************************************************** + * Properties + */ + + /** + * The descriptor of the objects that are mapped to by this + * data mapping. + * @type {ObjectDescriptor} + */ + objectDescriptor: { + get: function () { + return this._objectDescriptor; + }, + set: function (value) { + this._objectDescriptor = value; + this._objectDescriptorReference = new ObjectDescriptorReference().initWithValue(value); + } + }, + + /** + * The descriptor of the "raw data" mapped from by this + * data mapping. + * @type {ObjectDescriptor} + */ + schemaDescriptor: { + get: function () { + return this._schemaDescriptor; + }, + set: function (value) { + this._schemaDescriptor = value; + this._schemaDescriptorReference = new ObjectDescriptorReference().initWithValue(value); + } + }, + + /** + * A reference to the object descriptor that is used + * by this mapping. Used by serialized data mappings. + * @type {ObjectDescriptorReference} + */ + objectDescriptorReference: { + get: function () { + return this._objectDescriptorReference ? this._objectDescriptorReference.promise(require) : + Promise.resolve(null); + }, + set: function (value) { + this._objectDescriptorReference = value; + } + }, + + /** + * A reference to the object descriptor of the "raw data" that + * is used by this mapping. Used by serialized data mappings. + * @type {ObjectDescriptorReference} + */ + schemaDescriptorReference: { + get: function () { + return this._schemaDescriptorReference ? this._schemaDescriptorReference.promise(require) : + Promise.resolve(null); + }, + set: function (value) { + this._schemaDescriptorReference = value; + } + }, + + /** + * The service that owns this mapping object. + * Used to create fetches for relationships. + * @type {DataService} + */ + service: { + value: undefined + }, + + /** + * Adds a name to the list of properties that will participate in + * eager mapping. The requisite property names will be mapped + * during the map from raw data phase. + * @param {...string} propertyName + */ + addRequisitePropertyName: { + value: function () { + // TODO: update after changing requisitePropertyNames to a set. + var i, length, arg; + for (i = 0, length = arguments.length; i < length; i += 1) { + arg = arguments[i]; + if (!this._requisitePropertyNames.has(arg)) { + this._requisitePropertyNames.add(arg); + } + } + } + }, + + /** + * @return {Set} + */ + requisitePropertyNames: { + get: function () { + return this._requisitePropertyNames; + } + }, + + /*************************************************************************** + * Mapping + */ + + /** + * Adds a rule to be used for mapping objects to raw data. + * @param {string} targetPath - The path to assign on the target + * @param {object} rule - The rule to be used when processing + * the mapping. The rule must contain + * the direction and path of the properties + * to map. Optionally can include + * a converter. + */ + addObjectMappingRule: { + value: function (targetPath, rule) { + var rawRule = {}; + rawRule[targetPath] = rule; + this._mapObjectMappingRules(rawRule, true); + this._mapRawDataMappingRules(rawRule); + } + }, + + /** + * Adds a rule to be used for mapping raw data to objects. + * @param {string} targetPath - The path to assign on the target + * @param {object} rule - The rule to be used when processing + * the mapping. The rule must contain + * the direction and path of the properties + * to map. Optionally can include + * a converter. + */ + addRawDataMappingRule: { + value: function (targetPath, rule) { + var rawRule = {}; + rawRule[targetPath] = rule; + this._mapRawDataMappingRules(rawRule, true); + this._mapObjectMappingRules(rawRule); + } + }, + + /** + * 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 (data, object) { + * object.firstName = data.GIVEN_NAME; + * object.lastName = data.FAMILY_NAME; + * } + * } + * + * The default implementation of this method copies the properties defined + * by the raw data object to the data object. + * + * @method + * @argument {Object} data - 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. + + */ + mapRawDataToObject: { + value: function (data, object) { + var requisitePropertyNames = this.requisitePropertyNames, + iterator = requisitePropertyNames.values(), + promises, propertyName, result; + + if (requisitePropertyNames.size) { + promises = []; + while ((propertyName = iterator.next().value)) { + promises.push(this.mapRawDataToObjectProperty(data, object, propertyName)); + } + result = Promise.all(promises); + } else { + result = Promise.resolve(null); + } + + return result; + } + }, + + /** + * Convert model objects to raw data objects of an appropriate type. + * + * Subclasses should override this method to map properties of the model objects + * to raw data, as in the following: + * + * mapObjectToRawData: { + * value: function (object, data) { + * data.GIVEN_NAME = object.firstName; + * data.FAMILY_NAME = object.lastName; + * } + * } + * + * The default implementation of this method copies the properties defined + * by the model object to the raw data object. + * + * @method + * @argument {Object} object - An object whose properties' values + * hold the model data. + * @argument {Object} data - An object whose properties must be set or + * modified to represent the model data + */ + mapObjectToRawData: { + value: function (object, data) { + var rules = this._compiledRawDataMappingRules, + promises = [], + keys = Object.keys(rules), + key, i; + + for (i = 0; (key = keys[i]); ++i) { + promises.push(this.mapObjectToRawDataProperty(object, data, key)); + } + return promises && promises.length && Promise.all(promises) || Promise.resolve(null); + } + }, + + /** + * Convert model object properties to the raw data properties present in the requirements + * for a given propertyName + * + * @method + * @argument {Object} object - An object whose properties' values + * hold the model data. + * @argument {Object} data - An object whose properties must be set or + * modified to represent the model data. + * @argument {string} propertyName - The name of the property whose requirements + * need to be populated in the raw data. + */ + mapObjectToCriteriaSourceForProperty: { + value: function (object, data, propertyName) { + var rules = this._compiledRawDataMappingRules, + rule = this._compiledObjectMappingRules[propertyName], + requiredRawProperties = rule ? rule.requirements : [], + rawRequirementsToMap = new Set(requiredRawProperties), + promises = [], key; + + + for (key in rules) { + if (rules.hasOwnProperty(key) && rawRequirementsToMap.has(key)) { + promises.push(this._getAndMapObjectProperty(object, data, key, propertyName)); + } + } + return promises && promises.length && Promise.all(promises) || Promise.resolve(null); + } + }, + + _propertiesRequestedForLayer: { + get: function () { + if (!this.__propertiesRequestedForLayer) { + this.__propertiesRequestedForLayer = new Map(); + } + return this.__propertiesRequestedForLayer; + } + }, + + _getAndMapObjectProperty: { + value: function (object, data, propertyName) { + var self = this, + rules = this._compiledRawDataMappingRules, + rule = rules[propertyName], + requiredObjectProperties = rule ? rule.requirements : [], + result; + + + + result = this.service.rootService.getObjectPropertyExpressions(object, requiredObjectProperties); + + + + if (result && typeof result.then === "function") { + return result.then(function () { + return self.mapObjectToRawDataProperty(object, data, propertyName); + }); + } else { + return this.mapObjectToRawDataProperty(object, data, propertyName); + } + } + }, + + + /** + * Returns the value of a single raw data property evaluated against the model object + * + * @method + * @argument {Object} object - An object whose properties' values + * hold the model data. + * @argument {Object} data - The object on which to assign the property + * @argument {string} propertyName - The name of the raw property to which + * to assign the values. + */ + mapObjectToRawDataProperty: { + value: function(object, data, property) { + var self = this, + rules = this._compiledRawDataMappingRules, + scope = new Scope(object), + rule = rules[property], + propertyDescriptor = rule && rule.propertyDescriptor, + promise; + + if (propertyDescriptor) { + promise = propertyDescriptor.valueDescriptor.then(function (descriptor) { + self._prepareObjectToRawDataRule(rule); + return descriptor && rule.converter ? self._convertRelationshipToRawData(object, propertyDescriptor, rule, scope) : + self._parse(rule, scope); + }); + } else /*if (propertyDescriptor)*/ { //relaxing this for now + promise = Promise.resolve(this._parse(rule, scope)); + } + + + return promise && promise.then(function(value){ + data[property] = value; + }) || Promise.resolve(null); + } + }, + + _prepareObjectToRawDataRule: { + value: function (rule) { + var converter = rule.converter, + propertyDescriptor = rule.propertyDescriptor; + + if (converter) { + converter.expression = converter.expression || rule.expression; + converter.foreignDescriptor = converter.foreignDescriptor || propertyDescriptor.valueDescriptor; + } + } + }, + + /** + * Returns the identifier of the child service of .service that is used to + * fetch propertyName + * + * @method + * @argument {string} propertyName - The name of a model property + */ + serviceIdentifierForProperty: { + value: function (propertyName) { + var rule = this._compiledObjectMappingRules[propertyName]; + return rule && rule.serviceIdentifier; + } + }, + + _compiledObjectMappingRules: { + get: function () { + if (!this.__compiledObjectMappingRules) { + this.__compiledObjectMappingRules = {}; + this._mapObjectMappingRules(this._rawDataMappingRules); + this._mapObjectMappingRules(this._objectMappingRules, true); + } + + return this.__compiledObjectMappingRules; + } + }, + + _compiledRawDataMappingRules: { + get: function () { + if (!this.__compiledRawDataMappingRules) { + this.__compiledRawDataMappingRules = {}; + this._mapRawDataMappingRules(this._objectMappingRules); + this._mapRawDataMappingRules(this._rawDataMappingRules, true); + } + return this.__compiledRawDataMappingRules; + } + }, + + _rawDataMappingRules: { + value: undefined + }, + + rawDataPrimaryKeys: { + value: undefined + }, + + _requisitePropertyNames: { + get: function () { + if (!this.__requisitePropertyNames) { + this.__requisitePropertyNames = new Set(); + } + return this.__requisitePropertyNames; + } + }, + + _mapObjectMappingRules: { + value: function (rawRules, addOneWayBindings) { + var rules = this._compiledObjectMappingRules, + propertyNames = rawRules ? Object.keys(rawRules) : [], + propertyName, rawRule, rule, i; + + for (i = 0; (propertyName = propertyNames[i]); ++i) { + rawRule = rawRules[propertyName]; + if (this._shouldMapRule(rawRule, addOneWayBindings)) { + rule = this._makeRuleFromRawRule(rawRule, propertyName, addOneWayBindings); + rules[rule.targetPath] = rule; + } + } + } + }, + + _mapRawDataMappingRules: { + value: function (rawRules, addOneWayBindings) { + var rules = this._compiledRawDataMappingRules, + propertyNames = rawRules ? Object.keys(rawRules) : [], + propertyName, rawRule, rule, i; + for (i = 0; (propertyName = propertyNames[i]); ++i) { + rawRule = rawRules[propertyName]; + if (this._shouldMapRule(rawRule, addOneWayBindings)) { + rule = this._makeRuleFromRawRule(rawRule, propertyName, addOneWayBindings); + rules[rule.targetPath] = rule; + } + } + } + }, + + _makeRuleFromRawRule: { + value: function (rawRule, propertyName, addOneWayBindings) { + var propertyDescriptorName = addOneWayBindings ? rawRule[ONE_WAY_BINDING] || rawRule[TWO_WAY_BINDING] : propertyName, + propertyDescriptor = this.objectDescriptor.propertyDescriptorForName(propertyDescriptorName), + sourcePath = addOneWayBindings ? rawRule[ONE_WAY_BINDING] || rawRule[TWO_WAY_BINDING] : propertyName, + targetPath = addOneWayBindings && propertyName || rawRule[TWO_WAY_BINDING], + compiled = this._compileRuleExpression(sourcePath), + rule = new MappingRule(); + + rule.converter = rawRule.converter || this._defaultConverter(sourcePath, targetPath); + rule.expression = compiled.expression; + rule.inversePropertyName = rawRule.inversePropertyName; + rule.isReverter = rawRule.converter && !addOneWayBindings; + rule.propertyDescriptor = propertyDescriptor; + rule.requirements = this._parseRequirementsFromParsedExpression(compiled.parsed); + rule.serviceIdentifier = rawRule.serviceIdentifier; + rule.targetPath = targetPath; + + return rule; + } + }, + + _convertRelationshipToRawData: { + value: function (object, propertyDescriptor, rule, scope) { + if (!rule.converter.revert) { + console.log("Converter does not have a revert function for property (" + propertyDescriptor.name + ")"); + } + return rule.converter.revert(rule.expression(scope)); + } + }, + + + __scope: { + value: null + }, + + _scope: { + get: function() { + return this.__scope || new Scope(); + } + }, + + + /** + * Returns the value of a single model property evaluated against the raw data 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 values. + */ + + mapRawDataToObjectProperty: { + value: function (data, object, propertyName) { + //We should probably shift rules to be a Map rather than an anonymous object. + var rules = this._compiledObjectMappingRules, + rule = rules.hasOwnProperty(propertyName) && rules[propertyName], + propertyDescriptor = rule && this.objectDescriptor.propertyDescriptorForName(propertyName), + scope = this._scope, + self = this, + result; + + scope.value = data; + if (!propertyDescriptor || propertyDescriptor.definition) { + result = Promise.resolve(null); + } else { + result = propertyDescriptor.valueDescriptor.then(function (descriptor) { + var isRelationship = !!descriptor; + self._prepareRawDataToObjectRule(rule, propertyDescriptor); + return isRelationship ? self._resolveRelationship(object, propertyDescriptor, rule, scope) : + self._resolvePrimitive(object, propertyDescriptor, rule, scope); + }); + } + return result; + } + }, + + + _prepareRawDataToObjectRule: { + value: function (rule, propertyDescriptor) { + var converter = rule.converter; + if (converter) { + converter.expression = converter.expression || rule.expression; + converter.foreignDescriptor = converter.foreignDescriptor || propertyDescriptor.valueDescriptor; + converter.objectDescriptor = this.objectDescriptor; + converter.serviceIdentifier = rule.serviceIdentifier; + } + } + }, + + /** + * Pre-fetches the model properties that are required to map another model property + * + * @method + * @argument {Object} object - The object on which to prefetch properties + * @argument {string} propertyName - The name of the model property for which + * there are prerequisites + */ + resolvePrerequisitesForProperty: { + value: function (object, propertyName) { + var rule = this._compiledObjectMappingRules[propertyName], + prerequisites = rule && rule.prerequisitePropertyNames || null; + if (!rule) { + console.log("No Rule For:", propertyName); + } + + return prerequisites ? this.service.rootService.getObjectProperties(object, prerequisites) : Promise.resolve(null); + } + }, + + _resolvePrimitive: { + value: function (object, propertyDescriptor, rule, scope) { + var value = this._parse(rule, scope), + self = this; + + + return new Promise(function (resolve, reject) { + if (self._isThenable(value)) { + value.then(function (data) { + self._assignDataToObjectProperty(object, propertyDescriptor, data); + resolve(null); + }); + } else { + object[propertyDescriptor.name] = value; + resolve(null); + } + }); + } + }, + + _isThenable: { + value: function (object) { + return object && object.then && typeof object.then === "function"; + } + }, + + _assignDataToObjectProperty: { + value: function (object, propertyDescriptor, data) { + var hasData = data && data.length, + isToMany = propertyDescriptor.cardinality !== 1, + propertyName = propertyDescriptor.name; + + //Add checks to make sure that data matches expectations of propertyDescriptor.cardinality + // + + if (Array.isArray(data)) { + if (isToMany && Array.isArray(object[propertyName])) { + object[propertyName].splice.apply(object[propertyName], [0, Infinity].concat(data)); + } else if (isToMany) { + object[propertyName] = data; + } else if (hasData) { + //Cardinality is 1, if data contains more than 1 item, we throw + if (data.length && data.length > 1) { + throw new Error("ExpressionDataMapping for property \""+ this.objectDescriptor.name + "." + propertyName+"\" expects a cardinality of 1 but data to map doesn't match: "+data); + } + object[propertyName] = data[0]; + } + } else { + object[propertyName] = data; + } + } + }, + + _assignObjectToDataInverseProperty: { + value: function (object, propertyDescriptor, data, inversePropertyName) { + return propertyDescriptor.valueDescriptor.then(function (valueDescriptor) { + var inversePropertyDescriptor = valueDescriptor.propertyDescriptorForName(inversePropertyName); + if (inversePropertyDescriptor.cardinality === 1) { + data.forEach(function (item) { + item[inversePropertyName] = object; + }); + } + return null; + }); + } + }, + + _resolveRelationship: { + value: function (object, propertyDescriptor, rule, scope) { + var self = this; + return rule.converter.convert(rule.expression(scope)).then(function (data) { + self._assignDataToObjectProperty(object, propertyDescriptor, data); + return rule.inversePropertyName && data && data.length ? self._assignObjectToDataInverseProperty(object, propertyDescriptor, data, rule.inversePropertyName) : + null; + + }); + } + }, + + _compileRuleExpression: { + value: function (rule) { + var parsed = parse(rule), + expression = compile(parsed); + + + return { + parsed: parsed, + expression: expression + }; + } + }, + + _parseRequirementsFromParsedExpression: { + value: function (parsedExpression, requirements) { + var args = parsedExpression.args, + type = parsedExpression.type; + + requirements = requirements || []; + + if (type === "property" && args[0].type === "value") { + requirements.push(args[1].value); + } else if (type === "property" && args[0].type === "property") { + var subProperty = [args[1].value]; + this._parseRequirementsFromParsedExpression(args[0], subProperty); + requirements.push(subProperty.reverse().join(".")); + } else if (type === "record") { + this._parseRequirementsFromParsedRecord(parsedExpression, requirements); + } + + return requirements; + } + }, + + + _parseRequirementsFromParsedRecord: { + value: function (parsedExpression, requirements) { + var self = this, + args = parsedExpression.args, + keys = Object.keys(args); + + keys.forEach(function (key) { + self._parseRequirementsFromParsedExpression(args[key], requirements); + }); + } + }, + + _parse: { + value: function (rule, scope) { + var value = rule.expression(scope); + return rule.converter ? rule.isReverter ? + rule.converter.revert(value) : + rule.converter.convert(value) : + value; + } + }, + + _parseObject: { + value: function (rule, scope) { + var value = rule.expression(scope); + return rule.converter && rule.converter.revert(value) || value; + } + }, + + _shouldMapRule: { + value: function (rawRule, addOneWayBindings) { + var isOneWayBinding = rawRule.hasOwnProperty(ONE_WAY_BINDING), + isTwoWayBinding = !isOneWayBinding && rawRule.hasOwnProperty(TWO_WAY_BINDING); + return isOneWayBinding && addOneWayBindings || isTwoWayBinding; + } + }, + + _defaultConverter: { + value: function (sourcePath, targetPath, isObjectMappingRule) { + var sourceObjectDescriptor = isObjectMappingRule ? this.schemaDescriptor : this.objectDescriptor, + targetObjectDescriptor = isObjectMappingRule ? this.objectDescriptor : this.schemaDescriptor, + sourceDescriptor = sourceObjectDescriptor && sourceObjectDescriptor.propertyDescriptorForName(sourcePath), + targetDescriptor = targetObjectDescriptor && targetObjectDescriptor.propertyDescriptorForName(targetPath), + sourceDescriptorValueType = sourceDescriptor && sourceDescriptor.valueType, + targetDescriptorValueType = targetDescriptor && targetDescriptor.valueType, + shouldUseDefaultConverter = sourceDescriptor && targetDescriptor && + sourceDescriptorValueType !== targetDescriptorValueType; + + return shouldUseDefaultConverter ? this._converterForValueTypes(targetDescriptorValueType, sourceDescriptorValueType) : + null; + + } + }, + + + _converterForValueTypes: { + value: function (sourceType, destinationType) { + var converters = exports.ExpressionDataMapping.defaultConverters; + return converters[sourceType] && converters[sourceType][destinationType] || null; + } + }, + + /*************************************************************************** + * Deprecated + */ + + /** + * @todo Document deprecation in favor of + * [mapRawDataToObject()]{@link DataMapping#mapRawDataToObject} + */ + mapFromRawData: { + value: function (object, record, context) { + return this.mapRawDataToObject(record, object, context); + } + }, + + /** + * @todo Document deprecation in favor of + * [mapObjectToRawData()]{@link DataMapping#mapObjectToRawData} + */ + mapToRawData: { + value: function (object, record) { + this.mapObjectToRawData(object, record); + } + } + +}, { + + defaultConverters: { + get: function () { + if (!exports.ExpressionDataMapping._defaultConverters) { + var defaultConverters = {}; + exports.ExpressionDataMapping._addDefaultConvertersToMap(defaultConverters); + exports.ExpressionDataMapping._defaultConverters = defaultConverters; + } + return exports.ExpressionDataMapping._defaultConverters; + } + }, + + _addDefaultConvertersToMap: { + value: function (converters) { + exports.ExpressionDataMapping._addDefaultBooleanConvertersToConverters(converters); + exports.ExpressionDataMapping._addDefaultNumberConvertersToConverters(converters); + exports.ExpressionDataMapping._addDefaultStringConvertersToConverters(converters); + } + }, + + _addDefaultBooleanConvertersToConverters: { + value: function (converters) { + var booleanConverters = {}; + booleanConverters["string"] = Object.create({}, { + convert: { + value: function (value) { + return Boolean(value); + } + }, + revert: { + value: function (value) { + return String(value); + } + } + }); + booleanConverters["number"] = Object.create({}, { + convert: { + value: function (value) { + return Boolean(value); + } + }, + revert: { + value: function (value) { + return Number(value); + } + } + }); + converters["boolean"] = booleanConverters; + } + }, + + _addDefaultNumberConvertersToConverters: { + value: function (converters) { + var numberConverters = {}; + numberConverters["string"] = Object.create({}, { + convert: { + value: function (value) { + return Number(value); + } + }, + revert: { + value: function (value) { + return String(value); + } + } + }); + numberConverters["boolean"] = Object.create({}, { + convert: { + value: function (value) { + return Number(value); + } + }, + revert: { + value: function (value) { + return Boolean(value); + } + } + }); + converters["number"] = numberConverters; + } + }, + + _addDefaultStringConvertersToConverters: { + value: function (converters) { + var stringConverters = {}; + stringConverters["number"] = Object.create({}, { + convert: { + value: function (value) { + return String(value); + } + }, + revert: { + value: function (value) { + return Number(value); + } + } + }); + stringConverters["boolean"] = Object.create({}, { + convert: { + value: function (value) { + return String(value); + } + }, + revert: { + value: function (value) { + return Boolean(value); + } + } + }); + converters["string"] = stringConverters; + } + } + +}); diff --git a/data/service/http-service.js b/data/service/http-service.js new file mode 100644 index 0000000000..0a412e7661 --- /dev/null +++ b/data/service/http-service.js @@ -0,0 +1,562 @@ +var RawDataService = require("data/service/raw-data-service").RawDataService, + DataQuery = require("data/model/data-query").DataQuery, + Enumeration = require("data/model/enumeration").Enumeration, + Map = require("collections/map"), + Montage = require("montage").Montage, + parse = require("frb/parse"), + compile = require("frb/compile-evaluator"), + evaluate = require("frb/evaluate"), + Scope = require("frb/scope"), + Promise = require("core/promise").Promise; + + +var HttpError = exports.HttpError = Montage.specialize({ + + constructor: { + value: function HttpError() { + this.stack = (new Error()).stack; + } + }, + + isAuthorizationError: { + get: function () { + return this._isAuthorizationError || (this.statusCode === 401 || this.statusCode === 403); + }, + set: function (value) { + this._isAuthorizationError = value; + } + }, + + message: { + get: function () { + if (!this._message) { + this._message = "Status " + this.statusCode + " received for url: " + this.url; + } + return this._message; + } + }, + + name: { + value: "HttpError" + }, + + url: { + value: undefined + }, + + statusCode: { + value: undefined + } + +}, { + + withMessage: { + value: function (message) { + var error = new this(); + error._message = message; + return error; + } + }, + + + withRequestAndURL: { + value: function (request, url) { + var error = new this(); + error.statusCode = request.status; + error.url = url; + return error; + } + } + +}); + +/** + * Superclass for services communicating using HTTP, usually REST services. + * + * @class + */ +/* + * TODO: Restore @extends when parent class has been cleaned up to not provide + * so many unnecessary properties and methods. + * + * @extends RawDataService + */ +var HttpService = exports.HttpService = RawDataService.specialize(/** @lends HttpService.prototype */ { + + /*************************************************************************** + * Constants + */ + + /** + * The Content-Type header corresponding to + * [application/x-www-form-urlencoded]{@link https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1}, + * the default format of form data. + * + * @type {Object} + */ + FORM_URL_ENCODED: { + value: {"Content-Type": "application/x-www-form-urlencoded"} + }, + + /** + * @type {Object} + * @deprecated in favor of + * [FORM_URL_ENCODED]{@link HttpService#FORM_URL_ENCODED}. + */ + FORM_URL_ENCODED_CONTENT_TYPE_HEADER: { + get: function () { + console.warn("HttpService.FORM_URL_ENCODED_CONTENT_TYPE_HEADER is deprecated - use HttpService.FORM_URL_ENCODED instead"); + return this.FORM_URL_ENCODED; + } + }, + + + /*************************************************************************** + * Authorization + */ + + setHeadersForQuery: { + value: function (headers, query) { + var authorization = query && query.authorization && query.authorization[0], + evaluate, scope; + + if (authorization && authorization.headerValueExpression && this.authorizationHeaderName) { + scope = new Scope(authorization); + evaluate = compile(parse(authorization.headerValueExpression)); + headers[this.authorizationHeaderName] = evaluate(scope); + } else if (query && this.authorizationHeaderName && this.authorizationHeaderValueExpression) { + scope = new Scope(query); + evaluate = compile(parse(this.authorizationHeaderValueExpression)); + headers[this.authorizationHeaderName] = evaluate(scope); + } else if (this.authorizationHeaderName && this.authorizationHeaderValue) { + headers[this.authorizationHeaderName] = this.authorizationHeaderValue; + } + } + }, + + /** + * @type {string} + * @description Name of header to be passed to all requests along with authorizationHeaderValue + * + */ + authorizationHeaderName: { + value: "Authorization" + }, + + /** + * @type {string} + * @description Value of header with name authorizationHeaderName to include with all requests from this service + * + */ + authorizationHeaderValue: { + value: undefined + }, + + /** + * @type {string} + * @description FRB Expression defining the authorizationHeaderValue when evaluated against a DataQuery + * passed to this service. + * + */ + authorizationHeaderValueExpression: { + value: undefined + }, + + /*************************************************************************** + * Getting property data + */ + + fetchHttpObjectProperty: { + value: function (type, object, propertyName, prerequisitePropertyNames, criteria) { + var self, selector, prerequisites, stream; + // Create and cache a new fetch promise if necessary. + if (!this._getCachedFetchPromise(object, propertyName)) { + // Parse arguments. + if (arguments.length >= 4) { + selector = DataQuery.withTypeAndCriteria(type, arguments[arguments.length - 1]);//RDW unclear if there's any special change required here for formal Criteria + + } else { + selector = DataQuery.withTypeAndCriteria(type); + } + if (arguments.length < 5 || !prerequisitePropertyNames) { + prerequisites = []; + } else if (!Array.isArray(prerequisitePropertyNames)) { + prerequisites = Array.prototype.slice.call(arguments, 3, -1); + } else { + prerequisites = prerequisitePropertyNames; + } + // Create and cache a new fetch promise + self = this; + this._setCachedFetchPromise(object, propertyName, this.nullPromise.then(function () { + // First get prerequisite data if necessary... + return self.rootService.getObjectProperties(object, prerequisites); + }).then(function () { + // Then fetch the requested data... + stream = self.rootService.fetchData(selector); + return stream; + }).then(function () { + // Then wait until the next event loop to ensure only one + // fetch is dispatched per event loop (caching ensures all + // subsequent requests for the same fetch promise within the + // same event loop will return the same promise)... + return self.eventLoopPromise; + }).then(function () { + // Then removes the promise from the cache so subsequent + // requests for this fetch promise generate new fetches. + self._setCachedFetchPromise(object, propertyName, null); + return stream.data; + })); + } + // Return the created or cached fetch promise. + return this._getCachedFetchPromise(object, propertyName); + } + }, + + /** + * @private + * @method + */ + _getCachedFetchPromise: { + value: function (object, propertyName) { + this._cachedFetchPromises = this._cachedFetchPromises || {}; + this._cachedFetchPromises[propertyName] = this._cachedFetchPromises[propertyName] || new Map(); + return this._cachedFetchPromises[propertyName].get(object); + } + }, + + /** + * @private + * @method + */ + _setCachedFetchPromise: { + value: function (object, propertyName, promise) { + this._cachedFetchPromises = this._cachedFetchPromises || {}; + this._cachedFetchPromises[propertyName] = this._cachedFetchPromises[propertyName] || new Map(); + this._cachedFetchPromises[propertyName].set(object, promise); + } + }, + + /*************************************************************************** + * Getting raw data + */ + + /** + * Fetches raw data from an HTTP REST endpoint. + * + * @method + * @argument {String} url - The URL of the endpoint. + * @argument {Object} + * [headers={}] - HTTP header names and + * values. Optional except + * if a body or types are to + * be specified. Pass in an + * empty, null, or undefined + * header to specify a body + * or types but no header. + * @argument [body] - The body to send with the + * XMLHttpRequest. Optional + * except if types are to be + * specified. Pass in a null + * or undefined body to + * specify types but no + * body. + * @argument {Array} + * [types=[HttpService.DataType.JSON]] - The possible types of + * the data expected in + * responses. These will + * be used to parse the + * response data. Currently + * only the first type is + * taken into account. The + * types can be specified as + * an array or as a sequence + * of + * [DataType]{@link HttpService.DataType} + * arguments. + * @argument {boolean} [sendCredentials=true] - Determines whether + * credentials are sent with + * the request. + * @returns {external:Promise} - A promise settled when the fetch is + * complete. On success the promise will be fulfilled with the data returned + * from the fetch, parsed according to the specified or detaul types. On + * error the promise will be rejected with the error. + */ + + _authRegexp: { + value: new RegExp(/error=\"([^&]*)\"/) + }, + + _fetchHttpRawDataWithParsedArguments: { + value: function (parsed) { + var self = this, + error, request; + + if (!parsed) { + error = new Error("Invalid arguments to fetchHttpRawData()"); + } else if (!parsed.url) { + error = new Error("No URL provided to fetchHttpRawData()"); + } + + return new Promise(function (resolve, reject) { + var i, keys, key, + startTime = new Date().getTime(); + + // Report errors or fetch the requested raw data. + if (error) { + console.warn(error); + reject(error); + } else { + request = new XMLHttpRequest(); + request.onreadystatechange = function () { + if (request.readyState === 4) { + resolve(request); + // console.log("Completed request for (", parsed.url, ") in (", ((new Date().getTime() - startTime)), ") ms"); + } + }; + request.onerror = function () { + error = HttpError.withRequestAndURL(request, parsed.url); + reject(error); + }; + request.open(parsed.body ? "POST" : "GET", parsed.url, true); + + self.setHeadersForQuery(parsed.headers, parsed.query, parsed.url); + + keys = Object.keys(parsed.headers); + for (i = 0; (key = keys[i]); ++i) { + request.setRequestHeader(key, parsed.headers[key]); + } + request.withCredentials = parsed.credentials; + request.send(parsed.body); + } + }).then(function () { + // The response status can be 0 initially even for successful + // requests, so defer the processing of this response until the + // next event loop to give the status time to be set correctly. + return self.eventLoopPromise; + }).then(function () { + // Log a warning for error status responses. + // TODO: Reject the promise for error statuses. + if (self._isRequestUnauthorized(request) && typeof self.authorize === "function") { + return self.authorize().then(function () { + return self._fetchHttpRawDataWithParsedArguments(parsed); + }); + } else if (!error && (request.status >= 300 || request.status === 0)) { + // error = new Error("Status " + request.status + " received for REST URL " + parsed.url); + // console.warn(error); + throw HttpError.withRequestAndURL(request, parsed.url); + } + // Return null for errors or return the results of parsing the + // request response according to the specified types. + // TODO: Support multiple alternate types. + + return parsed.types[0].parseResponse(request, parsed.url); + }); + } + }, + + _isRequestUnauthorized: { + value: function (request) { + return request.status === 401 || (typeof this.didAuthorizationFail === "function" && this.didAuthorizationFail(request)); + } + }, + + fetchHttpRawData: { + value: function (url, headers, body, types, query, sendCredentials) { + var parsed = this._parseFetchHttpRawDataArguments.apply(this, arguments); + + // Create and return a promise for the fetch results. + return this._fetchHttpRawDataWithParsedArguments(parsed); + + } + }, + + /** + * @private + * @method + */ + _parseFetchHttpRawDataArguments: { + value: function (/* url [, headers [, body [, types]]][, sendCredentials] */) { + var parsed, last, i, n; + // Parse the url argument, setting the "last" argument index to -1 + // if the URL is invalid. + parsed = {url: arguments[0]}; + last = typeof parsed.url === "string" ? arguments.length - 1 : -1; + if (last < 0) { + console.warn(new Error("Invalid URL for fetchHttpRawData()")); + } + // Parse the sendCredentials argument, which must be the last + // argument if it is provided, and set the "last" argument index to + // point just past the last non-sendCredentials argument. + parsed.credentials = last < 1 || arguments[last]; + if (parsed.credentials instanceof Boolean) { + parsed.credentials = parsed.credentials.valueOf(); + } else if (typeof parsed.credentials !== "boolean") { + parsed.credentials = true; + last += 1; + } + // Parse the headers argument, which cannot be a boolean. + var headers = last > 1 && arguments[1] || {}; + parsed.headers = {}; + if (typeof headers === "object") { + Object.assign(parsed.headers, headers); + } + if (this._isBoolean(headers)) { + console.warn(new Error("Invalid headers for fetchHttpRawData()")); + last = -1; + } + // Parse the body argument, which cannot be a boolean. + if (last > 2 && arguments[2]) { + parsed.body = arguments[2]; + if (this._isBoolean(parsed.body)) { + console.warn(new Error("Invalid body for fetchHttpRawData()")); + last = -1; + } + } + + // Parse the types, which can be provided as an array or as a + // sequence of DataType arguments. + if (last === 4 && Array.isArray(arguments[3])) { + parsed.types = arguments[3]; + } else if (last < 4 || !(arguments[3] instanceof exports.HttpService.DataType)) { + parsed.types = [exports.HttpService.DataType.JSON]; + } else { + i = 3; + n = last; + while (i < n && arguments[i] instanceof exports.HttpService.DataType) { + ++i; + } + parsed.types = Array.prototype.slice.call(arguments, 3, i); + if (i < n) { + console.warn(new Error("Invalid types for fetchHttpRawData()")); + last = -1; + } + } + + + if (last === 5 && arguments[4] instanceof DataQuery) { + parsed.query = arguments[4]; + } else if (last === 4 && arguments[3] instanceof DataQuery) { + parsed.query = arguments[3]; + } + // Return the parsed arguments. + return last >= 0 ? parsed : undefined; + } + }, + + /** + * @private + * @method + */ + _isBoolean: { + value: function (value) { + return typeof value === "boolean" || value instanceof Boolean; + } + }, + + /*************************************************************************** + * Utilities + */ + + formUrlEncode: { + value: function (string) { + return encodeURIComponent(string).replace(/ /g, "+").replace(/[!'()*]/g, function(c) { + return '%' + c.charCodeAt(0).toString(16); + }); + } + } + +}, /** @lends HttpService */ { + + /*************************************************************************** + * Types + */ + + /** + * @class + */ + DataType: { + get: Enumeration.getterFor("_DataType", /** @lends HttpService.DataType */ { + + /** + * @type {DataType} + */ + BINARY: [{ + // TO DO. + }], + + /** + * @type {DataType} + */ + JSON: [{ + parseResponse: { + value: function (request, url) { + var text = request && request.responseText, + data = null; + if (text) { + try { + data = JSON.parse(text); + } catch (error) { + console.warn(new Error("Can't parse JSON received from " + url)); + } + } else if (request) { + console.warn(new Error("No JSON response received from " + url)); + } + return data; + } + } + }], + + /** + * @type {DataType} + */ + JSONP: [{ + parseResponse: { + value: function (request, url) { + var text = request && request.responseText, + start = text && text.indexOf("(") + 1, + end = text && Math.max(text.lastIndexOf(")"), 0), + data = null; + if (start && end) { + try { + data = text && JSON.parse(text.slice(start, end)); + } catch (error) { + console.warn(new Error("Can't parse JSONP received from " + url)); + console.warn("Response text:", text); + } + } else if (text) { + console.warn(new Error("Can't parse JSONP received from " + url)); + console.warn("Response text:", text); + } else if (request) { + console.warn(new Error("No JSONP response received from " + url)); + } + return data; + } + } + }], + + /** + * @type {DataType} + */ + TEXT: [{ + parseResponse: { + value: function (request, url) { + var text = request && request.responseText; + if (!text && request) { + console.warn(new Error("No text response received from " + url)); + } + return text; + } + } + }], + + /** + * @type {DataType} + */ + XML: [{ + // TO DO. + }] + + }) + } + +}); diff --git a/data/service/indexed-d-b-data-service.js b/data/service/indexed-d-b-data-service.js new file mode 100644 index 0000000000..f564c364c1 --- /dev/null +++ b/data/service/indexed-d-b-data-service.js @@ -0,0 +1,135 @@ +var PersistentDataService = require("data/service/persistent-data-service").PersistentDataService, + DataStream = require("data/service/data-stream").DataStream, + DataOperation= require("data/service/data-operation").DataOperation, + Promise = require("core/promise").Promise, + uuid = require("core/uuid"), + DataOrdering = require("data/model/data-ordering").DataOrdering, + DESCENDING = DataOrdering.DESCENDING, + evaluate = require("frb/evaluate"), + Map = require("collections/map"), + OfflineService; +/** + * TODO: Document + * + * !!!!!THIS IS A WORK IN PROGRESS. This is built at the same time as PersistentDataService + * to test abstraction/specialization together + * + * @class + * @extends RawDataService + */ +exports.IndexedDBDataService = PersistentDataService.specialize(/** @lends PersistentDataService.prototype */ { + + /*************************************************************************** + * Initializing + */ + + constructor: { + value: function PersistentDataService() { + PersistentDataService.call(this); + } + }, + + deserializeSelf: { + value:function (deserializer) { + this.super(deserializer); + } + }, + + _db : { + value: undefined + }, + + + provideDatabaseForModel : { + value: function(model) { + var databasePromiseResolve, + databasePromiseReject, + self = this, + database = new Promise(function(resolve, reject) { + databasePromiseResolve = resolve; + databasePromiseReject = reject; + var request = window.indexedDB.open(model.name, model.version); + if (!request) { + reject(new Error("IndexedDB API not available")); // May happen in Safari private mode + } + else { + request.identifier = "openDatabase"; + request.model = model; + request.addEventListener("upgradeneeded",self,false); + // request.addEventListener("complete",this,false); + // request.addEventListener("abort",this,false); + request.addEventListener("success",self,false); + request.addEventListener("error",self,false); + request.addEventListener("blocked",self,false); + // request.addEventListener("versionchange",this,false); + // request.addEventListener("close",this,false); + } + + }); + database.resolve = databasePromiseResolve; + database.reject = databasePromiseReject; + + return database; + } + }, + + _storage : { + value: undefined + }, + storage : { + get: function() { + if(!this._storage) { + if (!global.indexedDB) { + this._storage = Promise.reject(new Error("Your environment doesn't support IndexedDB.")); + } + else { + this._storage = this.storagePromiseForNameVersion(this.model.name,this.model.version); + } + } + return this._storage; + } + }, + /** + * Returns a Promise for the persistence storage used to store objects + * described by the objectDescriptor passed as an argument. + * + * may need to introduce an _method used internally to minimize + * use of super() + * + * @argument {ObjectDescriptor} stream + * @returns {Promise} + */ + provideStorageForObjectDescriptor: { + value: function(objectDescriptor) { + return this.storagePromiseForNameVersion(objectDescriptor.model.name,objectDescriptor.model.version); + } + }, + + handleOpenDatabaseError: { + value: function(event) { + this.databaseForModel(event.target.model).reject(event); + } + }, + handleOpenDatabaseBlocked: { + value: function(event) { + this.databaseForModel(event.target.model).reject(event); + } + }, + handleOpenDatabaseSuccess: { + value: function(event) { + this._db = event.target.result; + this.databaseForModel(event.target.model).resolve(this._db); + } + }, + handleOpenDatabaseUpgradeneeded: { + value: function(event) { + //TODO + this.databaseForModel(event.target.model).reject(event); + } + }, + + schema : { + value: void 0 + } + + }); diff --git a/data/service/mapping-rule.js b/data/service/mapping-rule.js new file mode 100644 index 0000000000..33ac106a15 --- /dev/null +++ b/data/service/mapping-rule.js @@ -0,0 +1,91 @@ +var Montage = require("montage").Montage; + +/** + * Instructions to map raw data to model objects or model objects to model objects + * + * @class + * @extends external:Montage + */ +exports.MappingRule = Montage.specialize(/** @lends MappingRule.prototype */ { + + + /** + * A converter that takes in the the output of #expression and returns the destination value. + * @type {Converter} + */ + converter: { + value: undefined + }, + + /** + * The expression that defines the input to be passed to .converter. If converter is not provided, + * the output of the expression is assigned directly to the destination value. + * @type {string} + */ + expression: { + value: undefined + }, + + /** + * The name of the property on the destination value that the destination object represents. + * For example, consider: + * + * The MappingRule for Foo.bars will have inversePropertyName = foo. + * + * @type {string} + */ + inversePropertyName: { + value: undefined + }, + + + /** + * Flag defining the direction of the conversion. If true, .expression + * will be evaluated in reverse (evaluate the expression against the + * destination & assign it to the source). + * @type {boolean} + */ + isReverter: { + value: undefined + }, + + + /** + * The descriptor for the property that this rule applies to + * @type {PropertyDescriptor} + */ + propertyDescriptor: { + value: undefined + }, + + /** + * The names of the properties required to evaluate .expression + * + * The raw data that .expression is evaluated against may not + * have all of the properties referenced in .expression before the + * the MappingRule is used. This array is used at the time of mapping to + * populate the raw data with any properties that are missing. + * + * @type {string[]} + */ + requirements: { + value: undefined + }, + + /** + * Identifier for the child service of ExpressionDataMapping.service + * that the destination value should be fetched from. + * @type {string} + */ + serviceIdentifier: { + value: undefined + }, + + /** + * Path of the property to which the value of the expression should be assigned. + * @type {string} + */ + targetPath: { + value: undefined + } +}); diff --git a/data/service/persistent-data-service.js b/data/service/persistent-data-service.js new file mode 100644 index 0000000000..28f8833cd7 --- /dev/null +++ b/data/service/persistent-data-service.js @@ -0,0 +1,1613 @@ +var RawDataService = require("data/service/raw-data-service").RawDataService, + DataStream = require("data/service/data-stream").DataStream, + DataOperation= require("data/service/data-operation").DataOperation, + Promise = require("core/promise").Promise, + uuid = require("core/uuid"), + DataOrdering = require("data/model/data-ordering").DataOrdering, + DESCENDING = DataOrdering.DESCENDING, + evaluate = require("frb/evaluate"), + Map = require("collections/map"), + PersistentDataService, OfflineService; + +/* global Dexie */ + +/** + * TODO: Document + * + * !!!!!THIS IS A WORK IN PROGRESS generalizing OfflineService, so code started from that + * and is evolving from there. + * + * @class + * @extends RawDataService + */ +exports.PersistentDataService = PersistentDataService = RawDataService.specialize(/** @lends PersistentDataService.prototype */ { + + /*************************************************************************** + * Initializing + */ + + constructor: { + value: function PersistentDataService() { + RawDataService.call(this); + } + }, + + deserializeSelf: { + value:function (deserializer) { + this.super(deserializer); + + var value; + value = deserializer.getProperty("persistingObjectDescriptorNames"); + if (value) { + this.persistingObjectDescriptorNames = value; + } + + } + }, + + serializeSelf: { + value:function (serializer) { + this.super(serializer); + + if (this.persistingObjectDescriptorNames) { + serializer.setProperty("persistingObjectDescriptorNames", this.persistingObjectDescriptorNames); + } + + } + }, + + /** + * returns a Promise that resolves to the object used by the service to + * store data. This is meant to be an abstraction of the "Database" + * + * @returns {Promise} + */ + + _storage: { + value: undefined + }, + storage: { + get: function() { + return this._storage || (this._storage = Promise.reject(new Error('Needs to be implemented by sub classes'))); + } + }, + + name: { + value: void 0 + }, + + + // createObjectStoreFromSample: { + // value: function (objectStoreName, primaryKey, sampleData) { + // if (!sampleData) return; + + // var sampleDataKeys = Object.keys(sampleData), + // storage = this._storage, + // currentSchema = {}; + + // storage.tables.forEach(function (table) { + // currentSchema[table.name] = JSON.stringify(table.schema); + // }); + + // var schemaDefinition = primaryKey; + // for (var i=0, iKey;(iKey = sampleDataKeys[i]);i++) { + // if (iKey !== primaryKey) { + // schemaDefinition += ","; + // schemaDefinition += iKey; + // } + // } + // currentSchema[objectStoreName] = schemaDefinition; + // storage.version(storage.verno+1).stores(currentSchema); + + // } + // }, + + /** + * table/property/index name that tracks the date the record was last updated + * + * @returns {String} + */ + operationTableName: { + value: "Operation" + }, + /** + * name of the schema property that stores the name of the type/object store + * of the object the operation impacts + * + * @returns {String} + */ + typePropertyName: { + value: "type" + }, + + /** + * name of the schema property that stores the last time the operation's object + * (dataID) was last fetched. + * + * @returns {String} + */ + lastFetchedPropertyName: { + value: "lastFetched" + }, + + /** + * name of the schema property that stores the last time the operation's + * object was last modified. + * + * @returns {String} + */ + lastModifiedPropertyName: { + value: "lastModified" + }, + + /** + * name of the schema property that stores the type of operation: + * This will be create or update or delete + * + * @returns {String} + */ + operationPropertyName: { + value: "operation" + }, + operationCreateName: { + value: "create" + }, + operationUpdateName: { + value: "update" + }, + operationDeleteName: { + value: "delete" + }, + + /** + * name of the schema property that stores the changes made to the object in this operation + * + * @returns {String} + */ + changesPropertyName: { + value: "changes" //This contains + }, + + /** + * name of the schema property that stores the primary key of the object the operation impacts + * + * @returns {String} + */ + dataIDPropertyName: { + value: "dataID" + }, + + /** + * name of the schema property that stores unstructured/custom data for a service + * to stash what it may need for further use. + * + * @returns {String} + */ + contextPropertyName: { + value: "context" + }, + + + /* + returns all records, ordered by time, that reflect what hapened when offline. + We shoud + [{ + // NO primaryKey: uuid-uuid-uuid, + lastFetched: Date("08-02-2016"), + lastModified: Date("08-02-2016"), + operation: "create", + data: { + hazard_id: uuid-uuid-uuid, + "foo":"abc", + "bla":23 + } + }, + { + lastFetched: Date("08-02-2016"), + lastModified: Date("08-02-2016"), + operation: "update" + }, + { + lastFetched: Date("08-02-2016"), + lastModified: Date("08-02-2016"), + operation: "update" + }, + ] + */ + + offlineOperations: { + get: function() { + //Fetch + } + + }, + + clearOfflineOperations: { + value: function(operations) { + //Fetch + + } + }, + /* Feels like that should return the data, in case it's called with null and created inside?*/ + mapToRawSelector: { + value: function (object, data) { + // TO DO: Provide a default mapping based on object.TYPE. + // For now, subclasses must override this. + } + }, + + /* Benoit: this coming from offline-service and will be replaced by storageByObjectDescriptor + */ + + _tableByName: { + value: void 0 + }, + tableNamed: { + value: function(tableName) { + var table; + if (!this._tableByName) { + this._tableByName = new Map(); + } + table = this._tableByName.get(tableName); + if (!table) { + table = this._storage[tableName]; + if (!table) { + var tables = this._storage.tables; + for (var i=0, iTable; (iTable = tables[i]); i++) { + if (iTable.name === tableName) { + this._tableByName.set(tableName,(table = iTable)); + break; + } + } + } + } + return table; + } + }, + + _operationTable: { + value: void 0 + }, + operationTable: { + get:function() { + if (!this._operationTable) { + this._operationTable = this.tableNamed(this.operationTableName); + } + return this._operationTable; + } + }, + + /** + * returns the set of all ObjectDescriptors names that should persist. + * + * @argument {ObjectDescriptor} objectDescriptor + * @returns {Set} + */ + _persistingObjectDescriptorNames: { + value: undefined + }, + persistingObjectDescriptorNames: { + set: function(value) { + this._persistingObjectDescriptorNames = new Set(value); + }, + get: function(value) { + return this._persistingObjectDescriptorNames; + } + }, + + /** + * returns true or false depending on wether PersistentDataService has been instructed + * to persist the objectDescriptor passed as an argument. + * + * @argument {ObjectDescriptor} objectDescriptor + * @returns {Boolean} + */ + persistsObjectDescriptor: { + value: function(objectDescriptor) { + return objectDescriptor && this._persistingObjectDescriptorNames && this._persistingObjectDescriptorNames.has(objectDescriptor.name); + } + }, + + persistsObject: { + value: function(object) { + return this.persistsObjectDescriptor(this.objectDescriptorForObject(object)); + } + }, + + + /** + * returns the list of all property descriptors that should persist for the + * objectDescriptor passed as an argument. By default, return all propertyDescriptors. + * This can be customized and configured when a PersistentDataService is instanciated. + * + * @argument {ObjectDescriptor} objectDescriptor + * @returns {Array.} + */ + persistentPropertyDescriptors: { + value: function(objectDescriptor) { + return objectDescriptor.propertyDescriptors; + } + }, + + + /** + * This is the opportunity for a PersistentDataService to lazily create the storage needed + * to execute this query, or to optimize it if it turns out indexes don't exist for optimally execute a passed query. This could also be lazily and on frequency of request being used, as well as time spent executing it without. Traversing the query's criteria's syntactic tree and looking up + * property descriptors' valueDescriptors to navigate the set of persistentStorage needed + * and make sure they exit before attempting to fetch from it. + * + * @argument {DataStream} stream + * @returns {Promise} + */ + storageForQuery: { + value: function(query) { + //Walk stream's criteria's syntax, + // call storageForObjectDescriptor() for type of query + // plus criteria's properties' valueDescriptor + // if such property should persist (see persistentPropertyDescriptors) + var message = "PersistentDataService.storageForQuery is not implemented", + type = query && query.type; + + if (type && typeof type === "string") { + message = message + " (" + type + ")"; + } else if (type) { + message = message + " (" + (type.name || type.exportName) + ")"; + } + return Promise.reject(new Error(message)); + } + }, + + _databaseByModel: { + value: undefined + }, + /** + * Returns the WeakMap keeping track of the storage objects + * used for objectDescriptors + * + * @returns {WeakMap} + */ + + databaseByModel: { + get: function() { + return this._databaseByModel || (this._databaseByModel = new WeakMap); + } + }, + registerDatabaseForModel: { + value: function(database,model) { + this._databaseByModel.set(model,database); + } + }, + unregisterDatabaseForModel: { + value: function(model) { + this._databaseByModel.delete(model); + } + }, + + /** + * Benoit: 8/8/2017. We are going to use a single database for an App model-group. + * If a persistent service is used for a single model, no pbm, to workaround possible + * name conflicts in ObjectDescriptors coming from different packages, we'll use the + * full moduleId of these ObjectDescriptors to name object stores / tables avoid name conflicts. + * Even if different databases end up being used, this choice will work as well. + * + * This API allows for one subclass to decide to use differrent databases for storing different + * ObjectDescriptors, or a subclass can decide to use only one. + * Returns a Promise for the persistence storage used to store objects + * described by the objectDescriptor passed as an argument. + * + * may need to introduce an _method used internally to minimize + * use of super() + * + * @argument {ObjectDescriptor} stream + * @returns {Promise} + */ + databaseForModel: { + value: function(model) { + if (this.persistsModel(model)) { + var database = this._databaseByModel.get(model); + if (!database) { + database = this.provideDatabaseForModel(model) || Promise.reject(null); + this.registerDatabaseForModel(database,model); + } + return database; + } + return Promise.reject(null); + } + }, + + databaseForObjectDescriptor: { + value: function(objectDescriptor) { + return this.databaseForModel(objectDescriptor.model); + } + }, + + _storageByObjectDescriptor: { + value: undefined + }, + /** + * Returns the WeakMap keeping track of the storage objects + * used for objectDescriptors + * + * @returns {WeakMap} + */ + + storageByObjectDescriptor: { + get: function() { + return this._storageByObjectDescriptor || (this._storageByObjectDescriptor = new WeakMap); + } + }, + registerStorageForObjectDescriptor: { + value: function(storage,objectDescriptor) { + this._storageByObjectDescriptor.set(objectDescriptor,storage); + } + }, + unregisterStorageForObjectDescriptor: { + value: function(objectDescriptor) { + this._storageByObjectDescriptor.delete(objectDescriptor); + } + }, + + /** + * Returns a Promise for the persistence storage used to store objects + * described by the objectDescriptor passed as an argument. + * + * Benoit: 8/8/2017: Ideally we want to create these storage lazily, on-demand. + * + * may need to introduce an _method used internally to minimize + * use of super() + * + * @argument {ObjectDescriptor} stream + * @returns {Promise} + */ + storageForObjectDescriptor: { + value: function(objectDescriptor) { + return this.databaseForObjectDescriptor(objectDescriptor) + .then(function(database) { + if (this.persistsObjectDescriptor(objectDescriptor)) { + var storage = this._storageByObjectDescriptor.get(objectDescriptor); + if (!storage) { + storage = this.provideStorageForObjectDescriptor(objectDescriptor) || Promise.reject(null); + this.registerStorageForObjectDescriptor(storage,objectDescriptor); + } + return storage; + } + return Promise.reject(null); + }); + } + }, + + /** + * Get the first child service that can handle data of the specified type, + * or `null` if no such child service exists. + * + * Overrides super to set itself as the delegate + * + * @private + * @method + * @argument {DataObjectDescriptor} type + * @returns {Set.} + */ + childServiceForType: { + value: function (type) { + var service = this.super(type); + if (service && this.persistsObjectDescriptor(type)) { + service.delegate = this; + } + return service; + } + }, + + fetchData: { + value: function (queryOrType, optionalCriteria, optionalStream) { + //We let the super logic apply, which will attempt to obtain data through any existing child service + var dataStream = this.super(queryOrType, optionalCriteria, optionalStream), + rawDataStream, + promise = dataStream, + rawPromise, + self = this; + + if (this.persistsObjectDescriptor(dataStream.query.type)) { + + promise = promise.then(function(data) { + var rawDataStream = new DataStream(); + rawDataStream.type = dataStream.type; + rawDataStream.query = dataStream.query; + self._registerDataStreamForRawDataStream(dataStream,rawDataStream); + self.fetchRawData(rawDataStream); + + return rawDataStream; + }); + } + + return promise; + } + }, + + _dataStreamObjectsByPrimaryKey: { + value: new WeakMap() + }, + objectsByPrimaryKeyForDataStream: { + value: function(dataStream) { + var value = this._dataStreamObjectsByPrimaryKey.get(dataStream); + if (!value) { + value = new Map(); + this._dataStreamObjectsByPrimaryKey.set(dataStream,value); + } + return value; + } + }, + __dataStreamForRawDataStream: { + value: new WeakMap() + }, + + _dataStreamForRawDataStream: { + value: function(rawDataStream) { + return this.__dataStreamForRawDataStream.get(rawDataStream); + } + }, + _registerDataStreamForRawDataStream: { + value: function(dataStream,rawDataStream) { + return this.__dataStreamForRawDataStream.set(rawDataStream,dataStream); + } + }, + + fetchRawData: { + value: function (stream) { + var query = stream.query, + queryObjectDescriptor = query.type, + storage = this.storageForObjectDescriptor(queryObjectDescriptor), + criteria = query.criteria, + whereProperties = Object.keys(criteria), + orderings = query.orderings, + + self = this; + + /* + The idea here (to finish) is to use the first criteria in the where, assuming it's the most + important, and then filter the rest in memory by looping on remaining + whereProperties amd whereEqualValues, index matches. Not sure how Dexie's Collection fits there + results in the then is an Array... This first pass fetches offline Hazards with status === "A", + which seems to be the only fetch for Hazards on load when offline. + */ + storage.then(function (storage) { + + var resultPromise = self.tableNamed(query.type); + resultPromise.toArray(function(results) { + //Creates an infinite loop, we don't need what's there + //self.addRawData(stream, results); + //self.rawDataDone(stream); + if (orderings) { + var expression = ""; + //Build combined expression + for (var i=0,iDataOrdering,iExpression;(iDataOrdering = orderings[i]);i++) { + iExpression = iDataOrdering.expression; + + if (expression.length) { + expression += "."; + } + + expression += "sorted{"; + expression += iExpression; + expression += "}"; + + if (iDataOrdering.order === DESCENDING) { + expression += ".reversed()"; + } + } + results = evaluate(expression, results); + } + + stream.addRawData(results); + stream.rawDataDone(); + + }); + + //} + // else { + // table.toArray() + // .then(function(results) { + // stream.addData(results); + // stream.dataDone(); + // }); + // } + + }).catch('NoSuchDatabaseError', function(e) { + // Database with that name did not exist + stream.dataError(e); + }).catch(function (e) { + stream.dataError(e); + }); + + // Return the passed in or created stream. + return stream; + } + }, + + openTransaction: { + value: function() { + return Promise.resolve(); + } + }, + + closeTransaction: { + value: function() { + return Promise.resolve(); + } + }, + + _updateOperationsByDataStream: { + value: new Map() + }, + + //ToDo: + //The dataStream will need to be removed from this structure when the cycle is completed. + updateOperationsForDataStream: { + value: function(dataStream) { + var operations = this._updateOperationsByDataStream.get(dataStream); + if (!operations) { + this._updateOperationsByDataStream.set(dataStream,(operations = [])); + } + return operations; + } + }, + _objectsToUpdatesForDataStream: { + value: new Map() + }, + + objectsToUpdatesForDataStream: { + value: function(dataStream) { + var objects = this._objectsToUpdatesForDataStream.get(dataStream); + if (!objects) { + this._objectsToUpdatesForDataStream.set(dataStream,(objects = [])); + } + return objects; + } + }, + /** + * Delegate method allowing the persistent service to do the ground work + * as objects are created, avoiding to loop again later + * + * @method + * @argument {DataService} dataService + * @argument {DataStream} dataStream + * @argument {Object} rawData + * @argument {Object} object + * @returns {void} + */ + rawDataServiceDidAddOneRawData: { + value: function(dataService,dataStream,rawData,object) { + if (this.persistsObject(object)) { + var dataIdentifier = this.dataIdentifierForObject(object), + dataStreamPrimaryKeyMap = this.objectsByPrimaryKeyForDataStream(dataStream), + dataOperation; + + //Register the object by primarykey, which we'll need later + dataStreamPrimaryKeyMap.set(dataIdentifier.primaryKey,object); + + //The operation should be created atomically in the method that + //will actually save the data itself. + //Create the record to track the online Last Updated date + dataOperation = {}; + dataOperation.dataID = dataIdentifier.primaryKey; + //We previously had the exact same time cached for all objects + //Need to keep an eye out for possible consequences due to that change + dataOperation[this.lastFetchedPropertyName] = Date.now(); + dataOperation[this.typePropertyName] = dataStream.query.type; + + this.updateOperationsForDataStream(dataStream).push(dataOperation); + //Pseudo code. + this.objectsToUpdatesForDataStream(dataStream).push(object); + } + + } + }, + + // addRawData: { + // value: function (stream, records, context) { + // this.super(stream, records, context); + // } + // }, + + addOneRawData: { + value: function(stream, rawData, context, _type) { + var dataIdentifier = this.dataIdentifierForTypeRawData(stream.query.type,rawData), + primaryKey = dataIdentifier.primaryKey, + dataStream = this._dataStreamForRawDataStream(stream), + dataStreamPrimaryKeyMap = this.objectsByPrimaryKeyForDataStream(dataStream), + object = null, + dataOperation, dataStreamValue; + + //Register the object by primarykey, which we'll need later + dataStreamValue = dataStreamPrimaryKeyMap.get(primaryKey); + //If results were returned by childServices but that primaryKey isn't found + //it means that this obect doesn't match stream's query criteria as it used to. + //We were removing it from storage in general, which could cause that object to disapear for other queries that it still matches. It should be done eventually + //for a per query cache + if (dataStream.data && dataStream.length > 0 && !dataStreamValue) { + this.deletesForDataStream(dataStream).push(primaryKey); + } + + //If no data was returned, we go on and create the object + if (!dataStream.data || dataStream.length === 0) { + object = this.super(stream, rawData, context, _type); + } + + + //Do we have + + + + return object; + } + }, + + rawDataDone: { + value: function (stream, context) { + var self = this; + this.super(stream, context) + .then(function() { + self.openTransaction() + .then(function() { + var dataStream = self._dataStreamForRawDataStream(stream), + updateOperations = self.objectsToUpdatesForDataStream(dataStream), + deleteOperations = self.deletesForDataStream(dataStream), + updates = stream.data; + + //Need to structure the API to + return this.closeTransaction(); + }); + }); + } + }, + + + /** + * Called every time [addRawData()]{@link RawDataService#addRawData} is + * called while online to optionally cache 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 cache data fetched when + * online so [fetchOfflineData]{@link RawDataSource#fetchOfflineData} can + * use that data when offline. + * + * @method + * @argument {DataStream} stream - The stream to which the fetched data is + * being added. + * @argument {Array} rawDataArray - An array of objects whose properties' + * values hold the raw data. + * @argument {?} context - The context value passed to the + * [addRawData()]{@link DataMapping#addRawData} + * call that is invoking this method. + */ + + //writeOfflineData/readOfflineOperation + + _persistFetchedDataStream: { + value: function (dataStream, rawData) { + + var self = this, + dataArray = dataStream.data, + query = dataStream.query, + tableName = query.type, + table = this.tableNamed(tableName), + clonedArray = [], + i,countI,iRawData, iLastUpdated, + lastUpdated = Date.now(), + updateOperationArray = [], + dataID = this.dataIDPropertyName, + primaryKey = table.schema.primKey.name, + lastUpdatedPropertyName = this.lastFetchedPropertyName, + j, jRawData, + rawDataMapByPrimaryKey, + offlineObjectsToClear = [], + rawDataStream = new DataStream(); + + rawDataStream.type = dataStream.type; + rawDataStream.query = query; + + //Make a clone of the array and create the record to track the online Last Updated date + for (i=0, countI = dataArray.length; i} + */ + _streamRawData: { + get: function () { + if (!this.__streamRawData) { + this.__streamRawData = new WeakMap(); + } + return this.__streamRawData; + } + }, + + __streamRawData: { + value: undefined + }, + + /*************************************************************************** + * Mapping Raw Data + */ + + /** + * Convert a selector for data objects to a selector for raw data. + * + * The selector returned by this method will be the selector used by methods + * that deal with raw data, like + * [fetchRawData()]{@link RawDataService#fetchRawData]}, + * [addRawData()]{@link RawDataService#addRawData]}, + * [rawDataDone()]{@link RawDataService#rawDataDone]}, and + * [writeOfflineData()]{@link RawDataService#writeOfflineData]}. Any + * [stream]{@link DataStream} available to these methods will have their + * selector references temporarly replaced by references to the mapped + * selector returned by this method. + * + * The default implementation of this method returns the passed in selector. + * + * @method + * @argument {DataQuery} selector - A selector defining data objects to + * select. + * @returns {DataQuery} - A selector defining raw data to select. + */ + mapSelectorToRawDataQuery: { + value: function (query) { + return query; + } + }, + + mapSelectorToRawDataSelector: { + value: deprecate.deprecateMethod(void 0, function (selector) { + return this.mapSelectorToRawDataQuery(selector); + }, "mapSelectorToRawDataSelector", "mapSelectorToRawDataQuery"), + }, + + /** + * Convert raw data to data objects of an appropriate type. + * + * + * @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. + */ + 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, record, 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; + } + }, + + /*************************************************************************** + * 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 new file mode 100644 index 0000000000..4dd77ce48f --- /dev/null +++ b/data/service/sample.bp @@ -0,0 +1,55 @@ +/** + * 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/data/service/snapshot-service.js b/data/service/snapshot-service.js new file mode 100644 index 0000000000..08d0dc1705 --- /dev/null +++ b/data/service/snapshot-service.js @@ -0,0 +1,171 @@ +var Montage = require("core/core").Montage, + Map = require("collections/map"); + +/** + * @class SnapshotService + * @extends Montage + */ +exports.SnapshotService = Montage.specialize(/** @lends SnapshotService# */ { + + constructor: { + value: function SnapshotService() { + this._cache = new Map(); + } + }, + + _cache: { + value: null + }, + + saveSnapshotForTypeNameAndId: { + value: function (snapshot, typeName, id) { + if (!this._cache.has(typeName)) { + this._cache.set(typeName, new Map()); + } + + this._cache.get(typeName).set(id, this._getClone(snapshot)); + } + }, + + getSnapshotForTypeNameAndId: { + value: function (typeName, id) { + var result; + if (this._cache.has(typeName)) { + result = this._cache.get(typeName).get(id); + } + return result; + } + }, + + removeSnapshotForTypeNameAndId: { + value: function (typeName, id) { + if (this._cache.has(typeName)) { + this._cache.get(typeName).delete(id); + } + } + }, + + getDifferenceWithSnapshotForTypeNameAndId: { + value: function (rawData, typeName, id) { + var difference = this._getClone(rawData), + cachedVersion, cachedKeys, key; + if (this._cache.has(typeName)) { + cachedVersion = this._cache.get(typeName).get(id); + } + if (cachedVersion) { + cachedKeys = Object.keys(cachedVersion); + for (var i = 0, length = cachedKeys.length; i < length; i++) { + key = cachedKeys[i]; + if (this._areSameValues(rawData[key], cachedVersion[key])) { + delete difference[key]; + } + } + } + return difference; + } + }, + + _getClone: { + value: function (object) { + var result, keys, key; + /*, temp, j, arrayLength, arrayKeys*/ + if (object) { + result = Object.create(null); + keys = Object.keys(object); + for (var i = 0, length = keys.length; i < length; i++) { + key = keys[i]; + if (Array.isArray(object[key])) { + result[key] = this._getArrayClone(object[key]); + } else if (typeof object[key] === "object") { + result[key] = this._getClone(object[key]); + } else { + result[key] = object[key]; + } + } + } else { + result = object; + } + return result; + } + }, + + _getArrayClone: { + value: function (array) { + var result = [], + value; + for (var i = 0, length = array.length; i < length; i++) { + value = array[i]; + if (Array.isArray(value)) { + result.push(this._getArrayClone(value)); + } else if (typeof value === "object") { + result.push(this._getClone(value)); + } else { + result.push(value); + } + } + return result; + } + }, + + _areSameValues: { + value: function(a, b) { + var result = a === b; + if (!result) { + if (typeof a === "object" && typeof b === "object" && + a !== null && b !== null) { + var aKeys = Object.keys(a).sort(), aValue, + bKeys = Object.keys(b).sort(), bValue, + key; + + var aResult = aKeys.filter(function(x) { return a[x] !== null; }).length, + bResult = bKeys.filter(function(x) { return b[x] !== null; }).length; + + result = aResult === bResult; + + if (result) { + for (var i = 0, length = bKeys.length; i < length; i++) { + key = bKeys[i]; + aValue = a[key]; + bValue = b[key]; + if (!this._areSameValues(aValue, bValue)) { + result = false; + break; + } + } + } + } + } + return result; + } + }, + + _equals: { + value: function (a, b) { + return typeof a === "object" && a !== null && + typeof b === "object" && b !== null ? this._compareObjects(a, b) : a === b; + } + }, + + _compareObjects: { + value: function (a, b) { + var aKeys = this._sortedNonNullKeysForObject(a), + bKeys = this._sortedNonNullKeysForObject(b), + areEqual = aKeys.length === bKeys.length, + i, length, key; + + for (i = 0, length = bKeys.length; i < length && areEqual; i += 1) { + key = bKeys[i]; + areEqual = this._equals(a[key], b[key]); + } + return areEqual; + } + }, + + _sortedNonNullKeysForObject: { + value: function (object) { + return Object.keys(object).sort().filter(function (key) { + return object[key] !== null; + }); + } + } +}); diff --git a/jsdoc.json b/jsdoc.json index f8d052e9d1..eeb6a82c88 100644 --- a/jsdoc.json +++ b/jsdoc.json @@ -4,14 +4,16 @@ }, "opts": { "readme": "README.md", - "destination": "./doc" + "destination": "./doc", + "recurse": true }, "source": { "include": [ "montage.js", "composer/", "core/", - "ui/" + "ui/", + "data/" ], "includePattern": ".+\\.js(doc)?$", "excludePattern": "(^|\\/|\\\\)_" @@ -32,4 +34,4 @@ "collapseSymbols" : false, "inverseNav" : true } -} \ No newline at end of file +} diff --git a/karma.conf.js b/karma.conf.js index c08a73878c..5bd3b201f9 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -37,6 +37,10 @@ module.exports = function(config) { pattern: 'composer/**/*.js', included: false }, + { + pattern: 'data/**/*.js', + included: false + }, { pattern: 'ui/**/*.js', included: false @@ -103,6 +107,7 @@ module.exports = function(config) { preprocessors: { 'montage.js': 'coverage', 'core/**/*.js': 'coverage', + 'data/**/*.js': 'coverage', 'ui/**/*.js': 'coverage', 'composer/**/*.js': 'coverage', 'window-loader/**/*.js': 'coverage' diff --git a/package.json b/package.json index 23b34b63d0..937be4377d 100644 --- a/package.json +++ b/package.json @@ -53,13 +53,16 @@ "frb": "~4.0.x", "htmlparser2": "~3.0.5", "jshint": "^2.9.5", - "lodash.camelcase": "^4.3.0", + "moment-timezone": "^0.5.11", + "mr": "^17.0.8", + "q-io": "^1.13.3", + "weak-map": "^1.0.5", + "xhr2": "^0.1.4", "lodash.kebabcase": "^4.1.1", - "lodash.snakecase": "^4.1.1", + "lodash.camelcase": "^4.3.0", "lodash.trim": "^4.5.1", - "mr": "^17.0.8", - "proxy-polyfill": "~0.1.7", - "q-io": "^1.13.3" + "lodash.snakecase": "^4.1.1", + "proxy-polyfill": "~0.1.7" }, "devDependencies": { "concurrently": "^3.4.0", @@ -76,6 +79,7 @@ "karma-safari-launcher": "^1.0.0", "montage-testing": "git://github.com/montagejs/montage-testing.git#master", "mop-integration": "git://github.com/montagejs/mop-integration.git#master", + "mr": "git://github.com/montagejs/mr.git#master", "open": "0.0.5" }, "scripts": { diff --git a/test/all.js b/test/all.js index 3d4fd55ab8..757cc4e6c0 100644 --- a/test/all.js +++ b/test/all.js @@ -18,8 +18,8 @@ module.exports = require("montage-testing").run(require, [ "spec/bindings/converter-spec", "spec/bindings/self-spec", {name: "spec/document-resources-spec", node: false}, - { name: "spec/claimed-pointer-spec", node: false }, - { name: "spec/montage-custom-element-spec", node: false }, + {name: "spec/claimed-pointer-spec", node: false }, + {name: "spec/montage-custom-element-spec", node: false }, // Core "spec/core/browser-spec", "spec/core/core-spec", @@ -111,6 +111,21 @@ module.exports = require("montage-testing").run(require, [ {name: "spec/ui/repetition-binding-spec", node: false}, {name: "spec/core/localizer-spec", node: false, karma: false}, {name: "spec/core/localizer/serialization-spec", node: false, karma: false}, + // Data + {name: "spec/data/data-selector"}, + {name: "spec/data/data-mapping"}, + {name: "spec/data/data-object-descriptor"}, + {name: "spec/data/data-property-descriptor"}, + {name: "spec/data/data-provider"}, + {name: "spec/data/data-selector"}, + {name: "spec/data/data-service"}, + {name: "spec/data/data-stream"}, + {name: "spec/data/enumeration"}, + {name: "spec/data/http-service"}, + {name: "spec/data/object-descriptor"}, + {name: "spec/data/property-descriptor"}, + {name: "spec/data/raw-data-service"}, + // Meta {name: "spec/meta/converter-object-descriptor-spec"}, {name: "spec/meta/module-object-descriptor-spec"}, @@ -119,6 +134,7 @@ module.exports = require("montage-testing").run(require, [ {name: "spec/meta/controller-object-descriptor-spec", node: false}, {name: "spec/meta/event-descriptor-spec", node: false}, {name: "spec/meta/object-descriptor-spec"} + ]).then(function () { console.log('montage-testing', 'End'); }, function (err) { diff --git a/test/run-node.js b/test/run-node.js index 3a570f98bf..91bec49208 100644 --- a/test/run-node.js +++ b/test/run-node.js @@ -3,7 +3,7 @@ var jasmineRequire = require('jasmine-core/lib/jasmine-core/jasmine.js'); var JasmineConsoleReporter = require('jasmine-console-reporter'); var Montage = require('../montage'); var PATH = require("path"); - +global.XMLHttpRequest = require('xhr2'); // Init var jasmine = jasmineRequire.core(jasmineRequire); var jasmineEnv = jasmine.getEnv(); diff --git a/test/spec/data/data-mapping.js b/test/spec/data/data-mapping.js new file mode 100644 index 0000000000..81dcb03507 --- /dev/null +++ b/test/spec/data/data-mapping.js @@ -0,0 +1,32 @@ +var DataMapping = require("montage/data/service/data-mapping").DataMapping; + +describe("A DataMapping", function() { + + function ClassA(a, b, c, d) { + this.a = a; + this.b = b; + this.c = c; + this.d = d; + } + + function ClassB(a, b, c, d) { + this.a = a; + this.b = b; + this.c = c; + this.d = d; + } + + it("can be created", function () { + expect(new DataMapping()).toBeDefined(); + }); + + it("copies raw data properties by default", function () { + var object = {x: 42}, + random = Math.random(), + data = new ClassA(1, 2, object, random), + mapped = new ClassB(); + new DataMapping().mapRawDataToObject(data, mapped); + expect(mapped).toEqual(new ClassB(1, 2, object, random)); + }); + +}); diff --git a/test/spec/data/data-object-descriptor.js b/test/spec/data/data-object-descriptor.js new file mode 100644 index 0000000000..f03e76bd6f --- /dev/null +++ b/test/spec/data/data-object-descriptor.js @@ -0,0 +1,79 @@ +var DataObjectDescriptor = require("montage/data/model/data-object-descriptor").DataObjectDescriptor, + DataPropertyDescriptor = require("montage/data/model/data-property-descriptor").DataPropertyDescriptor, + Montage = require("montage").Montage; + +describe("A DataObjectDescriptor", function() { + + it("can be created", function () { + expect(new DataObjectDescriptor()).toBeDefined(); + }); + + it("initially has no type name", function () { + expect(new DataObjectDescriptor().typeName).toBeUndefined(); + }); + + it("preserves its type name", function () { + var descriptor = new DataObjectDescriptor(), + name = "String" + Math.random(); + descriptor.typeName = name; + expect(descriptor.typeName).toEqual(name); + }); + + it("has Montage.prototype as its initial object prototype value", function () { + expect(new DataObjectDescriptor().objectPrototype).toEqual(Montage.prototype); + }); + + it("preserves its object prototype value", function () { + var descriptor = new DataObjectDescriptor(), + prototype = Object.create({}); + descriptor.objectPrototype = prototype; + expect(descriptor.objectPrototype).toBe(prototype); + }); + + it("initially has no property descriptors", function () { + expect(new DataObjectDescriptor().propertyDescriptors).toEqual({}); + }); + + it("preserves its property descriptors", function () { + var descriptor = new DataObjectDescriptor(), + properties = {}, + i; + properties["property" + Math.random()] = new DataPropertyDescriptor(); + properties["property" + Math.random()] = new DataPropertyDescriptor(); + properties["property" + Math.random()] = new DataPropertyDescriptor(); + for (i in properties) { + descriptor.setPropertyDescriptor(i, properties[i]); + } + expect(descriptor.propertyDescriptors).toEqual(properties); + }); + + it("can be created with a getter", function () { + var className1 = "Class" + Math.random(), + className2 = "Class" + Math.random(), + propertyName1 = "property" + Math.random(), + propertyName2 = "property" + Math.random(), + propertyName3 = "property" + Math.random(), + propertyName4 = "property" + Math.random(), + exports = {}, + descriptors1 = {}, + descriptors2 = {}, + descriptor1, + descriptor2; + descriptors1[propertyName1] = {value: Math.random()}; + descriptors1[propertyName2] = {value: Math.random()}; + descriptors2[propertyName3] = {value: Math.random()}; + descriptors2[propertyName4] = {value: Math.random()}; + exports[className1] = Montage.specialize(descriptors1); + exports[className2] = Montage.specialize(descriptors2); + descriptor1 = DataObjectDescriptor.getterFor(exports, className1).call({}); + descriptor2 = DataObjectDescriptor.getterFor(exports, className2).call({}); + expect(descriptor1).not.toEqual(descriptor2); + expect(descriptor1.typeName).toEqual(className1); + expect(descriptor2.typeName).toEqual(className2); + expect(Object.keys(descriptor1.propertyDescriptors).sort()).toEqual(['_serializableAttributeProperties', propertyName1, propertyName2].sort()); + expect(Object.keys(descriptor2.propertyDescriptors).sort()).toEqual(['_serializableAttributeProperties', propertyName3, propertyName4].sort()); + }); + + xit("needs to be further tested", function () {}); + +}); diff --git a/test/spec/data/data-property-descriptor.js b/test/spec/data/data-property-descriptor.js new file mode 100644 index 0000000000..416f4fcb95 --- /dev/null +++ b/test/spec/data/data-property-descriptor.js @@ -0,0 +1,29 @@ +var DataPropertyDescriptor = require("montage/data/model/data-property-descriptor").DataPropertyDescriptor; + +describe("A DataPropertyDescriptor", function() { + + it("can be created", function () { + expect(new DataPropertyDescriptor()).toBeDefined(); + }); + + it("initially is not a relationship", function () { + expect(new DataPropertyDescriptor().isRelationship).toEqual(false); + }); + + it("preserves its relationship status", function () { + var descriptor = new DataPropertyDescriptor(); + descriptor.isRelationship = true; + expect(descriptor.isRelationship).toEqual(true); + }); + + it("initially is not optional", function () { + expect(new DataPropertyDescriptor().isOptional).toEqual(false); + }); + + it("preserves its optional status", function () { + var descriptor = new DataPropertyDescriptor(); + descriptor.isOptional = true; + expect(descriptor.isOptional).toEqual(true); + }); + +}); diff --git a/test/spec/data/data-provider.js b/test/spec/data/data-provider.js new file mode 100644 index 0000000000..b5f324514c --- /dev/null +++ b/test/spec/data/data-provider.js @@ -0,0 +1,17 @@ +var DataProvider = require("montage/data/service/data-provider").DataProvider; + +describe("A DataProvider", function() { + + it("can be created", function () { + expect(new DataProvider()).toBeDefined(); + }); + + it("initially has an undefined data array", function () { + expect(new DataProvider().data).not.toBeDefined(); + }); + + it("accepts requests for data", function () { + expect(new DataProvider().requestData).toEqual(jasmine.any(Function)); + }); + +}); diff --git a/test/spec/data/data-selector.js b/test/spec/data/data-selector.js new file mode 100644 index 0000000000..1aa40b6b06 --- /dev/null +++ b/test/spec/data/data-selector.js @@ -0,0 +1,76 @@ +var DataQuery = require("montage/data/model/data-query").DataQuery, + ObjectDescriptor = require("montage/data/model/object-descriptor").ObjectDescriptor, + Criteria = require("montage/core/criteria").Criteria, + WeatherReportType = require("./logic/model/weather-report").Type, + WeatherReport = require("./logic/model/weather-report").WeatherReport, + serialize = require("montage/core/serialization/serializer/montage-serializer").serialize, + deserialize = require("montage/core/serialization/deserializer/montage-deserializer").deserialize; + +describe("A DataQuery", function() { + + it("can be created", function () { + expect(new DataQuery()).toBeDefined(); + }); + + it("initially has no type", function () { + expect(new DataQuery().type).toBeUndefined(); + }); + + it("preserves its type", function () { + var selector = new DataQuery(), + type = new ObjectDescriptor(), + name = "String" + Math.random(); + type.name = name; + selector.type = type; + expect(selector.type).toBe(type); + expect(selector.type.name).toEqual(name); + }); + + xit("initially has no criteria", function () { + expect(new DataQuery().criteria).toBeUndefined(); + }); + + xit("preserves its criteria", function () { + var selector = new DataQuery(), + criteria = {a: Math.random(), b: Math.random(), c: Math.random()}; + selector.criteria.a = criteria.a; + selector.criteria.b = criteria.b; + selector.criteria.c = criteria.c; + expect(selector.criteria).toEqual(criteria); + }); + + it("can serialize and deserialize", function (done) { + + var dataExpression = "city = $city && unit = $unit && country = $country"; + var dataParameters = { + city: 'San-Francisco', + country: 'us', + unit: 'imperial' + }; + + var dataType = WeatherReport; + var dataCriteria = new Criteria().initWithExpression(dataExpression, dataParameters); + + var dataQuerySource = DataQuery.withTypeAndCriteria(dataType, dataCriteria); + var dataQueryJson = serialize(dataQuerySource, require); + + try { + expect(dataQueryJson).toBeDefined(); + var dataQueryJsonObj = JSON.parse(dataQueryJson); + expect(dataQueryJsonObj.weatherreport).toBeDefined(); + expect(dataQueryJsonObj.weatherreport.object).toBe('spec/data/logic/model/weather-report'); + + expect(dataQueryJsonObj.criteria).toBeDefined(); + expect(dataQueryJsonObj.criteria.prototype).toBe('montage/core/criteria'); + var dataQuery = deserialize(dataQueryJson, require).then(function (dataQueryFromJson) { + expect(dataQueryJson).toBeDefined(); + done(); + }, function (err) { + fail(err); + }); + + } catch (err) { + fail(err); + } + }); +}); diff --git a/test/spec/data/data-service.js b/test/spec/data/data-service.js new file mode 100644 index 0000000000..8a737f2544 --- /dev/null +++ b/test/spec/data/data-service.js @@ -0,0 +1,422 @@ +var DataService = require("montage/data/service/data-service").DataService, + DataObjectDescriptor = require("montage/data/model/data-object-descriptor").DataObjectDescriptor, + ModuleObjectDescriptor = require("montage/core/meta/module-object-descriptor").ModuleObjectDescriptor, + ModuleReference = require("montage/core/module-reference").ModuleReference, + RawDataService = require("montage/data/service/raw-data-service").RawDataService; + +describe("A DataService", function() { + + it("can be created", function () { + expect(new DataService()).toBeDefined(); + }); + + it("initially has no parent, is a root service, and is the main service", function () { + var service; + + // Create the service after resetting the main service. + DataService.mainService = undefined; + service = new DataService(); + service.NAME = "SERVICE"; + service.jasmineToString = function () { return "SERVICE"; }; + + // Verify that the service has no parent and is the root and main. + expect(service.parentService).toBeUndefined(); + expect(service.rootService).toEqual(service); + expect(DataService.mainService).toEqual(service); + + // Try to set a parent and verify again. + service.parentService = new DataService(); + expect(service.parentService).toBeUndefined(); + expect(service.rootService).toEqual(service); + expect(DataService.mainService).toEqual(service); + }); + + it("can be a parent, child, or grandchild service", function () { + var parent, child, grandchild; + + // Create the parent, child, and grandchild after resetting the main + // service, and tie them all together. + DataService.mainService = undefined; + parent = new RawDataService(); + parent.NAME = "PARENT"; + parent.jasmineToString = function () { return "PARENT"; }; + child = new RawDataService(); + child.NAME = "CHILD"; + child.jasmineToString = function () { return "CHILD"; }; + grandchild = new RawDataService(); + grandchild.NAME = "GRANDCHILD"; + grandchild.jasmineToString = function () { return "GRANDCHILD"; }; + parent.addChildService(child); + child.addChildService(grandchild); + + // Verify that the parents, roots, and main are correct. + expect(parent.parentService).toBeUndefined(); + expect(parent.rootService).toEqual(parent); + expect(child.parentService).toEqual(parent); + expect(child.rootService).toEqual(parent); + expect(grandchild.parentService).toEqual(child); + expect(grandchild.rootService).toEqual(parent); + expect(DataService.mainService).toEqual(parent); + + // Try to set new parents and verify again. + parent.parentService = new RawDataService(); + child.parentService = new RawDataService(); + grandchild.parentService = new RawDataService(); + expect(parent.parentService).toBeUndefined(); + expect(parent.rootService).toEqual(parent); + expect(child.parentService).toEqual(parent); + expect(child.rootService).toEqual(parent); + expect(grandchild.parentService).toEqual(child); + expect(grandchild.rootService).toEqual(parent); + expect(DataService.mainService).toEqual(parent); + }); + + it("manages children correctly", function () { + var toString, Types, objects, Child, children, parent; + + // Define test types with ObjectDescriptors. + toString = function () { return "TYPE-" + this.id; }; + Types = [0, 1, 2, 3].map(function () { return function () {}; }); + Types.forEach(function (type) { type.TYPE = new DataObjectDescriptor(); }); + Types.forEach(function (type) { type.TYPE.jasmineToString = toString; }); + Types.forEach(function (type, index) { type.TYPE.id = index; }); + + // Define test objects for each of the test types. + toString = function () { return "OBJECT-" + this.id; }; + objects = Types.map(function (type) { return new type(); }); + objects.forEach(function (object) { object.jasmineToString = toString; }); + objects.forEach(function (object, index) { object.id = index; }); + + // Create test children with unique identifiers to help with debugging. + toString = function () { return "CHILD-" + this.id; }; + children = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(function () { return new RawDataService(); }); + children.forEach(function (child) { child.jasmineToString = toString; }); + children.forEach(function (child, index) { child.id = index; }); + + // Define a variety of types for the test children. Children with an + // undefined, null, or empty types array will be "all types" children. + children.forEach(function (child) { Object.defineProperty(child, "types", {writable: true}); }); + children[0].types = [Types[0].TYPE]; + children[1].types = [Types[0].TYPE]; + children[2].types = [Types[1].TYPE]; + children[3].types = [Types[0].TYPE, Types[1].TYPE]; + children[4].types = [Types[0].TYPE, Types[2].TYPE]; + children[5].types = [Types[1].TYPE, Types[2].TYPE]; + children[6].types = [Types[0].TYPE, Types[1].TYPE, Types[2].TYPE]; + children[7].types = undefined; + children[8].types = null; + children[9].types = []; + + // Create a service with the desired children. + parent = new DataService(); + parent.jasmineToString = function () { return "PARENT"; }; + children.forEach(function (child) { parent.addChildService(child); }); + + // Verify the initial parents, types, and type-to-child mapping. + expect(parent.parentService).toBeUndefined(); + expect(children[0].parentService).toEqual(parent); + expect(children[1].parentService).toEqual(parent); + expect(children[2].parentService).toEqual(parent); + expect(children[3].parentService).toEqual(parent); + expect(children[4].parentService).toEqual(parent); + expect(children[5].parentService).toEqual(parent); + expect(children[6].parentService).toEqual(parent); + expect(children[7].parentService).toEqual(parent); + expect(children[8].parentService).toEqual(parent); + expect(children[9].parentService).toEqual(parent); + expect(parent.types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE, Types[2].TYPE]); + expect(children[0].types.sort()).toEqual([Types[0].TYPE]); + expect(children[1].types.sort()).toEqual([Types[0].TYPE]); + expect(children[2].types.sort()).toEqual([Types[1].TYPE]); + expect(children[3].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE]); + expect(children[4].types.sort()).toEqual([Types[0].TYPE, Types[2].TYPE]); + expect(children[5].types.sort()).toEqual([Types[1].TYPE, Types[2].TYPE]); + expect(children[6].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE, Types[2].TYPE]); + expect(children[7].types).toBeUndefined(); + expect(children[8].types).toBeNull(); + expect(children[9].types).toEqual([]); + expect(parent.childServiceForType(Types[0].TYPE)).toEqual(children[0]); + 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]); + + // Modify the children and verify the resulting service parent, types, + // and type-to-child mapping. + parent.removeChildService(children[0]); + parent.removeChildService(children[1]); + expect(parent.parentService).toBeUndefined(); + expect(children[0].parentService).toBeUndefined(); + expect(children[1].parentService).toBeUndefined(); + expect(children[2].parentService).toEqual(parent); + expect(children[3].parentService).toEqual(parent); + expect(children[4].parentService).toEqual(parent); + expect(children[5].parentService).toEqual(parent); + expect(children[6].parentService).toEqual(parent); + expect(children[7].parentService).toEqual(parent); + expect(children[8].parentService).toEqual(parent); + expect(children[9].parentService).toEqual(parent); + expect(parent.types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE, Types[2].TYPE]); + expect(children[0].types.sort()).toEqual([Types[0].TYPE]); + expect(children[1].types.sort()).toEqual([Types[0].TYPE]); + expect(children[2].types.sort()).toEqual([Types[1].TYPE]); + expect(children[3].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE]); + expect(children[4].types.sort()).toEqual([Types[0].TYPE, Types[2].TYPE]); + expect(children[5].types.sort()).toEqual([Types[1].TYPE, Types[2].TYPE]); + expect(children[6].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE, Types[2].TYPE]); + expect(children[7].types).toBeUndefined(); + expect(children[8].types).toBeNull(); + expect(children[9].types).toEqual([]); + expect(parent.childServiceForType(Types[0].TYPE)).toEqual(children[3]); + 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]); + + // Modify and verify some more. + parent.removeChildService(children[3]); + expect(parent.parentService).toBeUndefined(); + expect(children[0].parentService).toBeUndefined(); + expect(children[1].parentService).toBeUndefined(); + expect(children[2].parentService).toEqual(parent); + expect(children[3].parentService).toBeUndefined(); + expect(children[4].parentService).toEqual(parent); + expect(children[5].parentService).toEqual(parent); + expect(children[6].parentService).toEqual(parent); + expect(children[7].parentService).toEqual(parent); + expect(children[8].parentService).toEqual(parent); + expect(children[9].parentService).toEqual(parent); + expect(parent.types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE, Types[2].TYPE]); + expect(children[0].types.sort()).toEqual([Types[0].TYPE]); + expect(children[1].types.sort()).toEqual([Types[0].TYPE]); + expect(children[2].types.sort()).toEqual([Types[1].TYPE]); + expect(children[3].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE]); + expect(children[4].types.sort()).toEqual([Types[0].TYPE, Types[2].TYPE]); + expect(children[5].types.sort()).toEqual([Types[1].TYPE, Types[2].TYPE]); + expect(children[6].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE, Types[2].TYPE]); + expect(children[7].types).toBeUndefined(); + expect(children[8].types).toBeNull(); + expect(children[9].types).toEqual([]); + expect(parent.childServiceForType(Types[0].TYPE)).toEqual(children[4]); + 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]); + + // 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 + // returned for that type. + parent.removeChildService(children[4]); + parent.removeChildService(children[6]); + expect(parent.parentService).toBeUndefined(); + expect(children[0].parentService).toBeUndefined(); + expect(children[1].parentService).toBeUndefined(); + expect(children[2].parentService).toEqual(parent); + expect(children[3].parentService).toBeUndefined(); + expect(children[4].parentService).toBeUndefined(); + expect(children[5].parentService).toEqual(parent); + expect(children[6].parentService).toBeUndefined(); + expect(children[7].parentService).toEqual(parent); + expect(children[8].parentService).toEqual(parent); + expect(children[9].parentService).toEqual(parent); + expect(parent.types.sort()).toEqual([Types[1].TYPE, Types[2].TYPE]); + expect(children[0].types.sort()).toEqual([Types[0].TYPE]); + expect(children[1].types.sort()).toEqual([Types[0].TYPE]); + expect(children[2].types.sort()).toEqual([Types[1].TYPE]); + expect(children[3].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE]); + expect(children[4].types.sort()).toEqual([Types[0].TYPE, Types[2].TYPE]); + expect(children[5].types.sort()).toEqual([Types[1].TYPE, Types[2].TYPE]); + expect(children[6].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE, Types[2].TYPE]); + expect(children[7].types).toBeUndefined(); + expect(children[8].types).toBeNull(); + expect(children[9].types).toEqual([]); + expect(parent.childServiceForType(Types[0].TYPE)).toEqual(children[7]); + 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]); + + // Modify and verify some more. + parent.removeChildService(children[5]); + parent.removeChildService(children[7]); + expect(parent.parentService).toBeUndefined(); + expect(children[0].parentService).toBeUndefined(); + expect(children[1].parentService).toBeUndefined(); + expect(children[2].parentService).toEqual(parent); + expect(children[3].parentService).toBeUndefined(); + expect(children[4].parentService).toBeUndefined(); + expect(children[5].parentService).toBeUndefined(); + expect(children[6].parentService).toBeUndefined(); + expect(children[7].parentService).toBeUndefined(); + expect(children[8].parentService).toEqual(parent); + expect(children[9].parentService).toEqual(parent); + expect(parent.types.sort()).toEqual([Types[1].TYPE]); + expect(children[0].types.sort()).toEqual([Types[0].TYPE]); + expect(children[1].types.sort()).toEqual([Types[0].TYPE]); + expect(children[2].types.sort()).toEqual([Types[1].TYPE]); + expect(children[3].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE]); + expect(children[4].types.sort()).toEqual([Types[0].TYPE, Types[2].TYPE]); + expect(children[5].types.sort()).toEqual([Types[1].TYPE, Types[2].TYPE]); + expect(children[6].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE, Types[2].TYPE]); + expect(children[7].types).toBeUndefined(); + expect(children[8].types).toBeNull(); + expect(children[9].types).toEqual([]); + expect(parent.childServiceForType(Types[0].TYPE)).toEqual(children[8]); + 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]); + + // Modify and verify some more. + parent.removeChildService(children[2]); + parent.removeChildService(children[8]); + expect(parent.parentService).toBeUndefined(); + expect(children[0].parentService).toBeUndefined(); + expect(children[1].parentService).toBeUndefined(); + expect(children[2].parentService).toBeUndefined(); + expect(children[3].parentService).toBeUndefined(); + expect(children[4].parentService).toBeUndefined(); + expect(children[5].parentService).toBeUndefined(); + expect(children[6].parentService).toBeUndefined(); + expect(children[7].parentService).toBeUndefined(); + expect(children[8].parentService).toBeUndefined(); + expect(children[9].parentService).toEqual(parent); + expect(parent.types.sort()).toEqual([]); + expect(children[0].types.sort()).toEqual([Types[0].TYPE]); + expect(children[1].types.sort()).toEqual([Types[0].TYPE]); + expect(children[2].types.sort()).toEqual([Types[1].TYPE]); + expect(children[3].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE]); + expect(children[4].types.sort()).toEqual([Types[0].TYPE, Types[2].TYPE]); + expect(children[5].types.sort()).toEqual([Types[1].TYPE, Types[2].TYPE]); + expect(children[6].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE, Types[2].TYPE]); + expect(children[7].types).toBeUndefined(); + expect(children[8].types).toBeNull(); + expect(children[9].types).toEqual([]); + expect(parent.childServiceForType(Types[0].TYPE)).toEqual(children[9]); + 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]); + + // Modify and verify some more. + parent.removeChildService(children[9]); + expect(parent.parentService).toBeUndefined(); + expect(children[0].parentService).toBeUndefined(); + expect(children[1].parentService).toBeUndefined(); + expect(children[2].parentService).toBeUndefined(); + expect(children[3].parentService).toBeUndefined(); + expect(children[4].parentService).toBeUndefined(); + expect(children[5].parentService).toBeUndefined(); + expect(children[6].parentService).toBeUndefined(); + expect(children[7].parentService).toBeUndefined(); + expect(children[8].parentService).toBeUndefined(); + expect(children[9].parentService).toBeUndefined(); + expect(parent.types.sort()).toEqual([]); + expect(children[0].types.sort()).toEqual([Types[0].TYPE]); + expect(children[1].types.sort()).toEqual([Types[0].TYPE]); + expect(children[2].types.sort()).toEqual([Types[1].TYPE]); + expect(children[3].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE]); + expect(children[4].types.sort()).toEqual([Types[0].TYPE, Types[2].TYPE]); + expect(children[5].types.sort()).toEqual([Types[1].TYPE, Types[2].TYPE]); + expect(children[6].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE, Types[2].TYPE]); + expect(children[7].types).toBeUndefined(); + expect(children[8].types).toBeNull(); + expect(children[9].types).toEqual([]); + expect(parent.childServiceForType(Types[0].TYPE)).toBeNull(); + 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(); + }); + + it("can handle child services with an async types property using the register/unregister API", function (done) { + var parent, syncChild, asyncChild, moduleReference, type, registerPromises, unregisterPromises; + + // Create a sample type + // type = Function.noop; + // type.TYPE = new DataObjectDescriptor(); + // type.TYPE.jasmineToString = function () { return "TYPE-" + this.id; }; + // type.TYPE.id = 1; + + moduleReference = new ModuleReference().initWithIdAndRequire("spec/data/logic/model/movie", require); + type = new ModuleObjectDescriptor().initWithModuleAndExportName(moduleReference, "Movie"); + // Create the main service + parent = new RawDataService(); + parent.NAME = "PARENT"; + parent.jasmineToString = function () { return "PARENT"; }; + + // Create a child with regular sync types + syncChild = new RawDataService; + Object.defineProperty(syncChild, "types", { + get: function () { + return [type]; + } + }); + + // Create a child with an async types property + asyncChild = new RawDataService; + Object.defineProperty(asyncChild, "types", { + get: function () { + return Promise.resolve([type]); + } + }); + + // Test that parent references are added and removed correctly + registerPromises = [syncChild, asyncChild].map(function (c) { + return parent.registerChildService(c); + }); + return Promise.all(registerPromises).then(function () { + expect(syncChild.parentService).toBe(parent); + expect(asyncChild.parentService).toBe(parent); + unregisterPromises = [syncChild, asyncChild].map(function (c) { + return parent.unregisterChildService(c); + }); + + return Promise.all(unregisterPromises); + }) + .then(function () { + expect(syncChild.parentService).toBeUndefined(); + expect(asyncChild.parentService).toBeUndefined(); + done(); + }); + }); + + it("has a fetchData() method", function () { + expect(new DataService().fetchData).toEqual(jasmine.any(Function)); + }); + + xit("has a fetchData() method that uses the passed in stream when one is specified", function () { + }); + + xit("has a fetchData() method that creates and return a new stream when none is passed in", function () { + }); + + xit("has a fetchData() method that sets its stream's selector", function () { + }); + + xit("has a registerService() method that needs to be further tested", function () {}); + + xit("has a mainService class variable that needs to be further tested", function () {}); + +}); diff --git a/test/spec/data/data-stream.js b/test/spec/data/data-stream.js new file mode 100644 index 0000000000..f822602487 --- /dev/null +++ b/test/spec/data/data-stream.js @@ -0,0 +1,308 @@ +var DataStream = require("montage/data/service/data-stream").DataStream, + Montage = require("montage").Montage; + +describe("A DataStream", function() { + + var DATA1 = [{a: 1, b: 2}, {a: 3, b: 4}], + DATA2 = [{a: 5, b: 6}]; + + function makeCounter(counters, index) { + return function (value) { + counters[index] += 1; + return value; + }; + } + + function makeArray(length, fill) { + var array = Array(length); + while (length > 0) { + array[length - 1] = fill; + length -= 1; + } + return array; + } + + it("can be created", function () { + expect(new DataStream()).toBeDefined(); + }); + + it("has a an initially empty data array", function () { + var stream = new DataStream(), + data = stream.data; + expect(Array.isArray(data)).toBe(true); + expect(data.length).toEqual(0); + }); + + it("has a data array which doesn't change even when its contents do", function () { + var stream = new DataStream(), + data = stream.data; + stream.addData(DATA1); + expect(stream.data).toBe(data); + }); + + it("provides the data it receives through its data array", function () { + var stream = new DataStream(); + stream.addData(DATA1); + expect(stream.data).toEqual(DATA1); + }); + + it("provides the data it receives to objects bound to its data array", function () { + var stream = new DataStream(), + bound = new (Montage.specialize({}))(); + bound.stream = stream; + bound.defineBinding("data", {"<-": "stream.data"}); + bound.defineBinding("foos", {"<-": "stream.data.map{foo}"}); + stream.addData([{foo: 1, bar: 2}, {foo: 3, bar: 4}]); + expect(bound.data).toEqual([{foo: 1, bar: 2}, {foo: 3, bar: 4}]); + expect(bound.foos).toEqual([1, 3]); + }); + + it("accepts requests for data", function () { + expect(new DataStream().requestData).toEqual(jasmine.any(Function)); + }); + + it("is a promise-like thenable and catchable", function () { + var stream = new DataStream(); + expect(stream.then).toEqual(jasmine.any(Function)); + expect(stream.catch).toEqual(jasmine.any(Function)); + }); + + it("gets fulfilled with the data it receives", function (done) { + var stream = new DataStream(), + counters = [0, 0, 0, 0], + promises = []; + // Feed data to the stream, setting up fulfillment callback counters + // before, during, and after this process. + promises.push(stream.then(makeCounter(counters, 0))); + stream.addData(DATA1); + promises.push(stream.then(makeCounter(counters, 1))); + stream.addData(DATA2); + promises.push(stream.then(makeCounter(counters, 2))); + stream.dataDone(); + promises.push(stream.then(makeCounter(counters, 3))); + // Verify that the stream was fulfilled with the expected data. + Promise.all(promises).then(function (value) { + var expected = DATA1.concat(DATA2); + expect(value).toEqual([expected, expected, expected, expected]); + return null; + }).catch(function (reason) { + fail(reason); + return null; + }); + // Verify that all of the stream fulfillment callbacks were called once + // and only once. Use a timeout to allow any possible fulfillments and + // rejection to be triggered before the verification. + setTimeout(function () { + expect(counters).toEqual([1, 1, 1, 1]); + done(); + }, 10); + }); + + it("can have its fulfillment handling deferred", function (done) { + var stream = new DataStream(); + // Feed data to the stream without setting up any callbacks. + stream.addData(DATA1); + stream.addData(DATA2); + stream.dataDone(); + // Verify that the stream was fulfilled with the expected data but defer + // the verification using a timeout. + setTimeout(function () { + stream.then(function (value) { + expect(value).toEqual(DATA1.concat(DATA2)); + done(); + return null; + }).catch(function (reason) { + fail(reason); + done(); + return null; + }); + }, 10); + }); + + it("doesn't get fulfilled more than once", function (done) { + var stream = new DataStream(), + counters = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + promises = []; + // Attempt to trigger the stream fulfillment and rejection multiple + // times, setting up callback counters before and after each attempt. + promises.push(stream.then(makeCounter(counters, 0), makeCounter(counters, 1))); + stream.dataDone(); + promises.push(stream.then(makeCounter(counters, 2), makeCounter(counters, 3))); + stream.dataError(); + promises.push(stream.then(makeCounter(counters, 4), makeCounter(counters, 5))); + stream.addData(DATA1); + stream.dataError(); + promises.push(stream.then(makeCounter(counters, 6), makeCounter(counters, 7))); + stream.addData(DATA2); + stream.dataDone(); + promises.push(stream.then(makeCounter(counters, 8), makeCounter(counters, 9))); + // Verify that the stream was fulfilled with the expected data. + Promise.all(promises).then(function (value) { + expect(value).toEqual(makeArray(5, DATA1.concat(DATA2))); + return null; + }).catch(function (reason) { + fail(reason); + return null; + }); + // Verify that all of the stream fulfillment callbacks were called once + // and only once and that none of the stream rejection callbacks were + // called. Use a timeout to allow any possible fulfillment and + // rejection to be triggered before the verification. + setTimeout(function () { + expect(counters).toEqual([1, 0, 1, 0, 1, 0, 1, 0, 1, 0]); + done(); + }, 10); + }); + + + it("can be rejected with an error", function (done) { + var stream = new DataStream(), + error = new Error("Sample error"), + counters = [0, 0, 0, 0, 0, 0], + promises = []; + // Report an error in the middle of feeding data to the stream, setting + // up callback counters before, during, and after this process. + promises.push(stream.then(makeCounter(counters, 0), makeCounter(counters, 1))); + stream.addData(DATA1); + promises.push(stream.then(makeCounter(counters, 2), makeCounter(counters, 3))); + stream.dataError(error); + promises.push(stream.then(makeCounter(counters, 4), makeCounter(counters, 5))); + // Verify that the stream was rejected with the expected error. Note + // that although the stream was rejected, the promises resulting from + // setting up its rejection callbacks will be fulfilled and not rejected + // because these callbacks convert rejections to fulfillments. + Promise.all(promises).then(function (value) { + expect(value).toEqual([error, error, error]); + return null; + }).catch(function (reason) { + fail(reason); + return null; + }); + // Verify that none of the stream fulfillment callbacks were called and + // that all of the stream rejection callbacks were called once and only + // once. Use a timeout to allow any possible fulfillment and rejection + // to be triggered before the verification. + setTimeout(function () { + expect(counters).toEqual([0, 1, 0, 1, 0, 1]); + done(); + }, 10); + }); + + it("can have its rejection handled with a catch()", function (done) { + var streams = [new DataStream(), new DataStream()], + errors = [new Error("Sample error 1"), new Error("Sample error 2")], + counters = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + promises = []; + // Report an error in the middle of feeding data to a stream, setting + // up callback counters using both then() and catch() before, during, + // and after this process. + promises.push(streams[0].then(makeCounter(counters, 0), makeCounter(counters, 1))); + promises.push(streams[0].catch(makeCounter(counters, 2))); + streams[0].addData(DATA1); + promises.push(streams[0].then(makeCounter(counters, 3), makeCounter(counters, 4))); + promises.push(streams[0].catch(makeCounter(counters, 5))); + streams[0].dataError(errors[0]); + promises.push(streams[0].then(makeCounter(counters, 6), makeCounter(counters, 7))); + promises.push(streams[0].catch(makeCounter(counters, 8))); + // Report an error in the middle of feeding data to another stream, + // setting up callback counters using catch() only before, during, and + // after this process. + promises.push(streams[1].catch(makeCounter(counters, 9))); + streams[1].addData(DATA1); + promises.push(streams[1].catch(makeCounter(counters, 10))); + streams[1].dataError(errors[1]); + promises.push(streams[1].catch(makeCounter(counters, 11))); + // Verify that the stream was rejected with the expected error. Note + // that although the stream was rejected, the promises resulting from + // setting up its rejection callbacks will be fulfilled and not rejected + // because these callbacks convert rejections to fulfillments. + Promise.all(promises).then(function (value) { + expect(value).toEqual(makeArray(6, errors[0]).concat(makeArray(3, errors[1]))); + return null; + }).catch(function (reason) { + fail(reason); + return null; + }); + // Verify that none of the stream fulfillment callbacks were called and + // that all of the stream rejection callbacks were called once and only + // once. Use a timeout to allow any possible fulfillment and rejection + // to be triggered before the verification. + setTimeout(function () { + expect(counters).toEqual([0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1]); + done(); + }, 10); + }); + + it("can have its rejection handling deferred", function (done) { + var streams = [new DataStream(), new DataStream()], + errors = [new Error("Sample error 1"), new Error("Sample error 2")], + counters = [0, 0, 0, 0], + promises = []; + // Report an error in the middle of feeding data to two streams, without + // setting up any callbacks beforehand. streams[0] will have both then() + // and catch() callbacks, stream[1] will only have catch() callbacks. + streams[0].addData(DATA1); + streams[0].dataError(errors[0]); + streams[1].addData(DATA1); + streams[1].dataError(errors[1]); + // Verify that the stream was rejected with the expected error, but + // defer setting up the callback counters used for the verification, + // and defer the verification itself, using timeouts. + setTimeout(function () { + promises.push(streams[0].then(makeCounter(counters, 0), makeCounter(counters, 1))); + promises.push(streams[0].catch(makeCounter(counters, 2))); + promises.push(streams[1].catch(makeCounter(counters, 3))); + Promise.all(promises).then(function (value) { + expect(value).toEqual([errors[0], errors[0], errors[1]]); + return null; + }).catch(function (reason) { + fail(reason); + return null; + }); + setTimeout(function () { + expect(counters).toEqual([0, 1, 1, 1]); + done(); + }, 10); + }, 10); + }); + + it("can't be rejected more than once", function (done) { + var stream = new DataStream(), + counters = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + promises = [], + errors = [new Error("Sample error 1"), new Error("Sample error 2")]; + // Attempt to trigger the promise rejection and fulfillment multiple + // times, setting up callback counters before and after each attempt. + promises.push(stream.then(makeCounter(counters, 0), makeCounter(counters, 1))); + stream.dataError(errors[0]); + promises.push(stream.then(makeCounter(counters, 2), makeCounter(counters, 3))); + stream.dataDone(); + promises.push(stream.then(makeCounter(counters, 4), makeCounter(counters, 5))); + stream.addData(DATA1); + stream.dataDone(); + promises.push(stream.then(makeCounter(counters, 6), makeCounter(counters, 7))); + stream.addData(DATA2); + stream.dataError(errors[1]); + promises.push(stream.then(makeCounter(counters, 8), makeCounter(counters, 9))); + // Verify that the promise was rejected with the expected error. Note + // that although the stream promise was rejected, the promises resulting + // from setting up the rejection callbacks will be fulfilled and not + // rejected because these callbacks convert rejections to fulfillments. + Promise.all(promises).then(function (value) { + expect(value).toEqual(makeArray(5, errors[0])); + return null; + }).catch(function (reason) { + fail(reason); + return null; + }); + // Verify that none of the promise fulfillment callbacks were called + // and that all of the promise rejection callbacks were called once and + // only once. Use a timeout to allow all the possible fulfillments and + // rejections to be triggered before the verification. + setTimeout(function () { + expect(counters).toEqual([0, 1, 0, 1, 0, 1, 0, 1, 0, 1]); + done(); + }, 10); + }); + +}); diff --git a/test/spec/data/enumeration.js b/test/spec/data/enumeration.js new file mode 100644 index 0000000000..8155539a67 --- /dev/null +++ b/test/spec/data/enumeration.js @@ -0,0 +1,292 @@ +var Enumeration = require("montage/data/model/enumeration").Enumeration, + Montage = require("montage").Montage; + +describe("An Enumeration", function() { + + it("allows types to be defined by specialization", function () { + var Color, Suit, black, red, spades, hearts, diamonds, clubs, unknown; + // Define Color. + Color = Enumeration.specialize("id", "name", { + alternative: { + get: function () { + return Color.COLORS[1 - Color.COLORS.indexOf(this)]; + } + } + }, { + COLORS: { + get: function () { + return [Color.BLACK, Color.RED]; + } + } + }, { + BLACK: ["B", "Black"], + RED: ["R", "Red"] + }); + // Verify Color. + black = Color.BLACK; + expect(Color.BLACK).toBe(black); + expect(Color.BLACK.id).toEqual("B"); + expect(Color.BLACK.name).toEqual("Black"); + expect(Color.BLACK.alternative).toBe(Color.RED); + red = Color.RED; + expect(Color.RED).toBe(red); + expect(Color.RED.id).toEqual("R"); + expect(Color.RED.name).toEqual("Red"); + expect(Color.RED.alternative).toBe(Color.BLACK); + expect(Color.COLORS).toEqual([Color.BLACK, Color.RED]); + // Define Suit. + Suit = Enumeration.specialize("id", "name", { + color: { + value: undefined + } + }, { + SUITS: { + get: function () { + return [Suit.SPADE, Suit.HEART, Suit.DIAMOND, Suit.CLUB]; + } + }, + BLACKS: { + get: function () { + return Suit.SUITS.filter(function (suit) { + return suit.color === Color.BLACK; + }); + } + }, + REDS: { + get: function () { + return Suit.SUITS.filter(function (suit) { + return suit.color === Color.RED; + }); + } + } + }, { + SPADE: ["S", "Spade", {color: {value: Color.BLACK}}], + HEART: ["H", "Heart", {color: {value: Color.RED}}], + DIAMOND: ["D", "Diamond", {color: {value: Color.RED}}], + CLUB: ["C", "Club", {color: {value: Color.BLACK}}], + UNKNOWN: ["U", "Unknown"] + }); + // Verify Suit. + spades = Suit.SPADE; + expect(Suit.SPADE).toBe(spades); + expect(Suit.SPADE.id).toEqual("S"); + expect(Suit.SPADE.name).toEqual("Spade"); + expect(Suit.SPADE.color).toEqual(Color.BLACK); + hearts = Suit.HEART; + expect(Suit.HEART).toBe(hearts); + expect(Suit.HEART.id).toEqual("H"); + expect(Suit.HEART.name).toEqual("Heart"); + expect(Suit.HEART.color).toEqual(Color.RED); + diamonds = Suit.DIAMOND; + expect(Suit.DIAMOND).toBe(diamonds); + expect(Suit.DIAMOND.id).toEqual("D"); + expect(Suit.DIAMOND.name).toEqual("Diamond"); + expect(Suit.DIAMOND.color).toEqual(Color.RED); + clubs = Suit.CLUB; + expect(Suit.CLUB).toBe(clubs); + expect(Suit.CLUB.id).toEqual("C"); + expect(Suit.CLUB.name).toEqual("Club"); + expect(Suit.CLUB.color).toEqual(Color.BLACK); + unknown = Suit.UNKNOWN; + expect(Suit.UNKNOWN).toBe(unknown); + expect(Suit.UNKNOWN.id).toEqual("U"); + expect(Suit.UNKNOWN.name).toEqual("Unknown"); + expect(Suit.UNKNOWN.color).toBeUndefined(); + expect(Suit.SUITS).toEqual([Suit.SPADE, Suit.HEART, Suit.DIAMOND, Suit.CLUB]); + expect(Suit.BLACKS).toEqual([Suit.SPADE, Suit.CLUB]); + expect(Suit.REDS).toEqual([Suit.HEART, Suit.DIAMOND]); + }); + + it("allows types to be defined by specialization with arguments missing", function () { + var Color, Suit; + // Define Color with two arguments missing. + Color = Enumeration.specialize("id", "name", { + BLACK: ["B", "Black"], + RED: ["R", "Red"] + }); + // Verify Color. + expect(Color.BLACK.id).toEqual("B"); + expect(Color.BLACK.name).toEqual("Black"); + expect(Color.RED.id).toEqual("R"); + expect(Color.RED.name).toEqual("Red"); + // Define Suit with one argument missing. + Suit = Enumeration.specialize("id", "name", { + color: { + value: undefined + } + }, { + SPADE: ["S", "Spade", {color: {value: Color.BLACK}}], + HEART: ["H", "Heart", {color: {value: Color.RED}}], + DIAMOND: ["D", "Diamond", {color: {value: Color.RED}}], + CLUB: ["C", "Club", {color: {value: Color.BLACK}}], + UNKNOWN: ["U", "Unknown"] + }); + // Verify Suit. + spades = Suit.SPADE; + expect(Suit.SPADE).toBe(spades); + expect(Suit.SPADE.id).toEqual("S"); + expect(Suit.SPADE.name).toEqual("Spade"); + expect(Suit.SPADE.color).toEqual(Color.BLACK); + hearts = Suit.HEART; + expect(Suit.HEART).toBe(hearts); + expect(Suit.HEART.id).toEqual("H"); + expect(Suit.HEART.name).toEqual("Heart"); + expect(Suit.HEART.color).toEqual(Color.RED); + diamonds = Suit.DIAMOND; + expect(Suit.DIAMOND).toBe(diamonds); + expect(Suit.DIAMOND.id).toEqual("D"); + expect(Suit.DIAMOND.name).toEqual("Diamond"); + expect(Suit.DIAMOND.color).toEqual(Color.RED); + clubs = Suit.CLUB; + expect(Suit.CLUB).toBe(clubs); + expect(Suit.CLUB.id).toEqual("C"); + expect(Suit.CLUB.name).toEqual("Club"); + expect(Suit.CLUB.color).toEqual(Color.BLACK); + unknown = Suit.UNKNOWN; + expect(Suit.UNKNOWN).toBe(unknown); + expect(Suit.UNKNOWN.id).toEqual("U"); + expect(Suit.UNKNOWN.name).toEqual("Unknown"); + expect(Suit.UNKNOWN.color).toBeUndefined(); + }); + + it("allows types to be defined through a getter", function () { + var Coin, heads, tails; + // Define Side through a getter. + Coin = Montage.specialize({}, { + Side: { + get: Enumeration.getterFor("_Side", "id", "name", { + reverse: { + get: function () { + return Coin.Side.SIDES[1 - Coin.Side.SIDES.indexOf(this)]; + } + } + }, { + SIDES: { + get: function () { + return [Coin.Side.HEADS, Coin.Side.TAILS]; + } + } + }, { + HEADS: ["H", "Heads"], + TAILS: ["T", "Tails"] + }) + } + }); + // Verify Sides. + heads = Coin.Side.HEADS; + expect(Coin.Side.HEADS).toBe(heads); + expect(Coin.Side.HEADS.id).toEqual("H"); + expect(Coin.Side.HEADS.name).toEqual("Heads"); + expect(Coin.Side.HEADS.reverse).toBe(Coin.Side.TAILS); + tails = Coin.Side.TAILS; + expect(Coin.Side.TAILS).toBe(tails); + expect(Coin.Side.TAILS.id).toEqual("T"); + expect(Coin.Side.TAILS.name).toEqual("Tails"); + expect(Coin.Side.TAILS.reverse).toBe(Coin.Side.HEADS); + expect(Coin.Side.SIDES).toEqual([Coin.Side.HEADS, Coin.Side.TAILS]); + }); + + it("allows types to be defined through a getter with arguments missing", function () { + var Coin, heads, tails; + // Define Side through a getter with one arguments missing. + Coin = Montage.specialize({}, { + Side: { + get: Enumeration.getterFor("_Side", "id", "name", { + reverse: { + get: function () { + return this === Coin.Side.HEADS ? Coin.Side.TAILS : Coin.Side.HEADS; + } + } + }, { + HEADS: ["H", "Heads"], + TAILS: ["T", "Tails"] + }) + } + }); + // Verify Sides. + heads = Coin.Side.HEADS; + expect(Coin.Side.HEADS).toBe(heads); + expect(Coin.Side.HEADS.id).toEqual("H"); + expect(Coin.Side.HEADS.name).toEqual("Heads"); + expect(Coin.Side.HEADS.reverse).toBe(Coin.Side.TAILS); + tails = Coin.Side.TAILS; + expect(Coin.Side.TAILS).toBe(tails); + expect(Coin.Side.TAILS.id).toEqual("T"); + expect(Coin.Side.TAILS.name).toEqual("Tails"); + expect(Coin.Side.TAILS.reverse).toBe(Coin.Side.HEADS); + // Define Side through a getter with two arguments missing. + Coin = Montage.specialize({}, { + Side: { + get: Enumeration.getterFor("_Side", "id", "name", { + HEADS: ["H", "Heads"], + TAILS: ["T", "Tails"] + }) + } + }); + // Verify Sides. + heads = Coin.Side.HEADS; + expect(Coin.Side.HEADS).toBe(heads); + expect(Coin.Side.HEADS.id).toEqual("H"); + expect(Coin.Side.HEADS.name).toEqual("Heads"); + tails = Coin.Side.TAILS; + expect(Coin.Side.TAILS).toBe(tails); + expect(Coin.Side.TAILS.id).toEqual("T"); + expect(Coin.Side.TAILS.name).toEqual("Tails"); + }); + + it("allows types to be created with a method call", function () { + // Define Side. + var Side = Enumeration.specialize("id", "name", {}, {}, { + HEADS: ["H", "Heads"], + TAILS: ["T", "Tails"] + }); + // Verify Sides. + expect(Side.HEADS.id).toEqual("H"); + expect(Side.HEADS.name).toEqual("Heads"); + expect(Side.TAILS.id).toEqual("T"); + expect(Side.TAILS.name).toEqual("Tails"); + // Add and test a new type of side. + Side.EDGE = Side.withIdAndName("E", "Edge"); + expect(Side.EDGE.id).toEqual("E"); + expect(Side.EDGE.name).toEqual("Edge"); + }); + + it("allows types without unique properties to be defined", function () { + var side + // Test with a number of possible no-unique-properties values. + [[], "", null, undefined].forEach(function (uniqueProperties) { + // Define Sides. + Side = Enumeration.specialize(uniqueProperties, "id", "name", { + HEADS: ["H", "Heads"], + TAILS: ["T", "Tails"] + }); + // Verify Sides. + expect(Side.HEADS.id).toEqual("H"); + expect(Side.HEADS.name).toEqual("Heads"); + expect(Side.TAILS.id).toEqual("T"); + expect(Side.TAILS.name).toEqual("Tails"); + }); + }); + + it("allows types to be looked up by unique property value", function () { + // Define Side with a single unique property. + var Side = Enumeration.specialize("initial", "name", { + HEADS: ["H", "Heads"], + TAILS: ["T", "Tails"] + }); + // Lookup Sides by unique property names. + expect(Side.forInitial("H")).toBe(Side.HEADS); + expect(Side.forInitial("T")).toBe(Side.TAILS); + // Define Side with multiple unique properties. + Side = Enumeration.specialize(["letter", "number"] , "name", { + HEADS: ["H", 1, "Heads"], + TAILS: ["T", 2, "Tails"] + }); + // Lookup Sides by unique property names. + expect(Side.forLetter("H")).toBe(Side.HEADS); + expect(Side.forLetter("T")).toBe(Side.TAILS); + expect(Side.forNumber(1)).toBe(Side.HEADS); + expect(Side.forNumber(2)).toBe(Side.TAILS); + }); + +}); diff --git a/test/spec/data/expression-data-mapping.js b/test/spec/data/expression-data-mapping.js new file mode 100644 index 0000000000..ce12d5f3df --- /dev/null +++ b/test/spec/data/expression-data-mapping.js @@ -0,0 +1,210 @@ +var ExpressionDataMapping = require("montage/data/service/expression-data-mapping").ExpressionDataMapping, + CategoryService = require("spec/data/service/category-service").CategoryService, + DataService = require("montage/data/service/data-service").DataService, + DateConverter = require("montage/core/converter/date-converter").DateConverter, + ModuleObjectDescriptor = require("montage/core/meta/module-object-descriptor").ModuleObjectDescriptor, + ModuleReference = require("montage/core/module-reference").ModuleReference, + Promise = require("montage/core/promise").Promise, + PropertyDescriptor = require("montage/core/meta/property-descriptor").PropertyDescriptor, + RawDataService = require("montage/data/service/raw-data-service").RawDataService, + RawPropertyValueToObjectConverter = require("montage/data/converter/raw-property-value-to-object-converter").RawPropertyValueToObjectConverter; + +describe("An Expression Data Mapping", function() { + + var categoryMapping, + categoryModuleReference, + categoryObjectDescriptor, + categoryPropertyDescriptor, + categoryService, + dateConverter, + mainService, + isFeaturedPropertyDescriptor, + movieBudgetPropertyDescriptor, + movieMapping, + movieModuleReference, + movieObjectDescriptor, + movieReleaseDatePropertyDescriptor, + movieSchema, + movieSchemaModuleReference, + movieService, + plotSummaryModuleReference, + plotSummaryObjectDescriptor, + plotSummaryPropertyDescriptor, + registrationPromise, + schemaBudgetPropertyDescriptor, + schemaIsFeaturedPropertyDescriptor, + schemaReleaseDatePropertyDescriptor; + + + dateConverter = Object.create({}, { + converter: { + value: new DateConverter() + }, + formatString: { + value: "MM/dd/yyyy" + }, + convert: { + value: function (rawValue) { + return new Date(Date.parse(rawValue)); + } + }, + revert: { + value: function (date) { + this.converter.pattern = this.formatString; + return this.converter.convert(date); + } + } + }); + + DataService.mainService = undefined; + mainService = new DataService(); + mainService.NAME = "Movies"; + movieService = new RawDataService(); + movieModuleReference = new ModuleReference().initWithIdAndRequire("spec/data/model/logic/movie", require); + movieObjectDescriptor = new ModuleObjectDescriptor().initWithModuleAndExportName(movieModuleReference, "Movie"); + movieObjectDescriptor.addPropertyDescriptor(new PropertyDescriptor().initWithNameObjectDescriptorAndCardinality("title", movieObjectDescriptor, 1)); + movieSchemaModuleReference = new ModuleReference().initWithIdAndRequire("spec/data/schema/logic/movie", require); + movieSchema = new ModuleObjectDescriptor().initWithModuleAndExportName(movieSchemaModuleReference, "Movie"); + categoryService = new CategoryService(); + + categoryModuleReference = new ModuleReference().initWithIdAndRequire("spec/data/model/logic/category", require); + categoryObjectDescriptor = new ModuleObjectDescriptor().initWithModuleAndExportName(categoryModuleReference, "Category"); + categoryObjectDescriptor.addPropertyDescriptor(new PropertyDescriptor().initWithNameObjectDescriptorAndCardinality("name", categoryObjectDescriptor, 1)); + categoryPropertyDescriptor = new PropertyDescriptor().initWithNameObjectDescriptorAndCardinality("category", movieObjectDescriptor, 1); + categoryPropertyDescriptor.valueDescriptor = categoryObjectDescriptor; + movieObjectDescriptor.addPropertyDescriptor(categoryPropertyDescriptor); + + plotSummaryModuleReference = new ModuleReference().initWithIdAndRequire("spec/data/model/logic/plot-summary", require); + plotSummaryObjectDescriptor = new ModuleObjectDescriptor().initWithModuleAndExportName(plotSummaryModuleReference, "PlotSummary"); + plotSummaryObjectDescriptor.addPropertyDescriptor(new PropertyDescriptor().initWithNameObjectDescriptorAndCardinality("summary", plotSummaryObjectDescriptor, 1)); + plotSummaryPropertyDescriptor = new PropertyDescriptor().initWithNameObjectDescriptorAndCardinality("plotSummary", movieObjectDescriptor, 1); + plotSummaryPropertyDescriptor.valueDescriptor = plotSummaryObjectDescriptor; + movieObjectDescriptor.addPropertyDescriptor(plotSummaryPropertyDescriptor); + + schemaBudgetPropertyDescriptor = new PropertyDescriptor().initWithNameObjectDescriptorAndCardinality("budget", movieSchema, 1); + movieSchema.addPropertyDescriptor(schemaBudgetPropertyDescriptor); + movieBudgetPropertyDescriptor = new PropertyDescriptor().initWithNameObjectDescriptorAndCardinality("budget", movieObjectDescriptor, 1); + movieBudgetPropertyDescriptor.valueType = "number"; + movieObjectDescriptor.addPropertyDescriptor(movieBudgetPropertyDescriptor); + + movieReleaseDatePropertyDescriptor = new PropertyDescriptor().initWithNameObjectDescriptorAndCardinality("releaseDate", movieObjectDescriptor, 1); + movieObjectDescriptor.addPropertyDescriptor(movieReleaseDatePropertyDescriptor); + + isFeaturedPropertyDescriptor = new PropertyDescriptor().initWithNameObjectDescriptorAndCardinality("isFeatured", movieObjectDescriptor, 1); + isFeaturedPropertyDescriptor.valueType = "boolean"; + movieObjectDescriptor.addPropertyDescriptor(isFeaturedPropertyDescriptor); + schemaIsFeaturedPropertyDescriptor = new PropertyDescriptor().initWithNameObjectDescriptorAndCardinality("is_featured", movieSchema, 1); + schemaIsFeaturedPropertyDescriptor.valueType = "string"; + movieSchema.addPropertyDescriptor(schemaIsFeaturedPropertyDescriptor); + + movieMapping = new ExpressionDataMapping().initWithServiceObjectDescriptorAndSchema(movieService, movieObjectDescriptor, movieSchema); + movieMapping.addRequisitePropertyName("title", "category", "budget", "isFeatured", "releaseDate"); + movieMapping.addObjectMappingRule("title", {"<->": "name"}); + movieMapping.addObjectMappingRule("category", { + "<-": "category_id", + converter: new RawPropertyValueToObjectConverter().initWithConvertExpression("category_id") + }); + movieMapping.addObjectMappingRule("releaseDate", { + "<->": "release_date", + converter: dateConverter + }); + movieMapping.addObjectMappingRule("budget", {"<-": "budget"}); + movieMapping.addObjectMappingRule("isFeatured", {"<-": "is_featured"}); + movieMapping.addRawDataMappingRule("budget", {"<-": "budget"}); + movieMapping.addRawDataMappingRule("is_featured", {"<-": "isFeatured"}); + movieService.addMappingForType(movieMapping, movieObjectDescriptor); + categoryMapping = new ExpressionDataMapping().initWithServiceObjectDescriptorAndSchema(categoryService, categoryObjectDescriptor); + categoryMapping.addObjectMappingRule("name", {"<->": "name"}); + categoryMapping.addRequisitePropertyName("name"); + categoryService.addMappingForType(categoryMapping, categoryObjectDescriptor); + + it("can be created", function () { + expect(new ExpressionDataMapping()).toBeDefined(); + }); + + registrationPromise = Promise.all([ + mainService.registerChildService(movieService, movieObjectDescriptor), + mainService.registerChildService(categoryService, categoryObjectDescriptor) + ]); + + it("properly registers the object descriptor type to the mapping object in a service", function (done) { + return registrationPromise.then(function () { + expect(movieService.parentService).toBe(mainService); + expect(movieService.mappingWithType(movieObjectDescriptor)).toBe(movieMapping); + done(); + }); + }); + + it("can map raw data to object properties", function (done) { + var movie = {}, + data = { + name: "Star Wars", + category_id: 1, + budget: "14000000.00", + is_featured: "true", + release_date: "05/25/1977" + }; + return movieMapping.mapRawDataToObject(data, movie).then(function () { + expect(movie.title).toBe("Star Wars"); + expect(movie.category).toBeDefined(); + expect(movie.category && movie.category.name === "Action").toBeTruthy(); + expect(typeof movie.releaseDate === "object").toBeTruthy(); + expect(movie.releaseDate.getDate()).toBe(25); + expect(movie.releaseDate.getMonth()).toBe(4); + expect(movie.releaseDate.getFullYear()).toBe(1977); + done(); + }); + }); + + it("can automatically convert raw data to the correct type", function (done) { + var movie = {}, + data = { + name: "Star Wars", + category_id: 1, + budget: "14000000.00", + is_featured: "true" + + }; + return movieMapping.mapRawDataToObject(data, movie).then(function () { + expect(typeof movie.budget === "number").toBeTruthy(); + expect(typeof movie.category === "object").toBeTruthy(); + expect(typeof movie.isFeatured === "boolean").toBeTruthy(); + expect(typeof movie.title === "string").toBeTruthy(); + done(); + }); + }); + + it("can map objects to raw data", function (done) { + var movie = { + title: "Star Wars", + budget: 14000000.00, + isFeatured: true, + releaseDate: new Date(1977, 4, 25) + }, + data = {}; + movieMapping.mapObjectToRawData(movie, data).then(function () { + expect(data.name).toBe("Star Wars"); + expect(data.budget).toBe("14000000"); + expect(data.is_featured).toBe("true"); + expect(data.release_date).toBe("05/25/1977"); + done(); + }); + }); + + it("can automatically revert objects to raw data of the correct type", function (done) { + var movie = { + title: "Star Wars", + budget: 14000000.00, + isFeatured: true + }, + data = {}; + movieMapping.mapObjectToRawData(movie, data).then(function () { + expect(typeof data.budget === "string").toBeTruthy(); + expect(typeof data.is_featured === "string").toBeTruthy(); + expect(typeof data.name === "string").toBeTruthy(); + done(); + }); + }); + + +}); diff --git a/test/spec/data/http-service.js b/test/spec/data/http-service.js new file mode 100644 index 0000000000..2eb1800431 --- /dev/null +++ b/test/spec/data/http-service.js @@ -0,0 +1,33 @@ +var DataService = require("montage/data/service/data-service").DataService, + HttpService = require("montage/data/service/http-service").HttpService, + DataSelector = require("montage/data/service/data-selector").DataSelector, + Criteria = require("montage/core/criteria").Criteria, + WeatherReport = require("./logic/model/weather-report").WeatherReport, + WeatherService = require("./logic/service/weather-service").WeatherService, + MontageSerializer = require("montage/core/serialization/serializer/montage-serializer").MontageSerializer; + +describe("An HttpService", function() { + + it("needs to be tested", function (done) { + + var dataExpression = "city = $city && unit = $unit && country = $country"; + var dataParameters = { + city: 'San-Francisco', + country: 'us', + unit: 'imperial' + }; + var dataCriteria = new Criteria().initWithExpression(dataExpression, dataParameters); + var dataType = WeatherReport.TYPE; + var dataQuery = DataSelector.withTypeAndCriteria(dataType, dataCriteria); + + var mainService = new DataService(); + //TODO: Test with addChildService in addition to registerChildService + mainService.registerChildService(new WeatherService()).then(function () { + mainService.fetchData(dataQuery).then(function (weatherReports) { + expect(typeof weatherReports[0].temp).toBe('number'); + done(); + }); + }) + }); + +}); diff --git a/test/spec/data/logic/model/category.js b/test/spec/data/logic/model/category.js new file mode 100644 index 0000000000..4dfdc7fb2b --- /dev/null +++ b/test/spec/data/logic/model/category.js @@ -0,0 +1,13 @@ +var Montage = require("montage").Montage; + +/** + * @class Category + * @extends Montage + */ +exports.Category = Montage.specialize({ + + name: { + value: undefined + } + +}); diff --git a/test/spec/data/logic/model/movie.js b/test/spec/data/logic/model/movie.js new file mode 100644 index 0000000000..abe92a31b1 --- /dev/null +++ b/test/spec/data/logic/model/movie.js @@ -0,0 +1,37 @@ +var Montage = require("montage").Montage; + +/** + * @class Movie + * @extends Montage + */ +exports.Movie = Montage.specialize({ + + /** + * @type {boolean} + */ + isFeatured: { + value: undefined + }, + + /** + * @type {PlotSummary} + */ + plotSummary: { + value: undefined + }, + + /** + * @type {Date} + */ + releaseDate: { + value: undefined + }, + + /** + * @type {string} + */ + title: { + value: undefined + } + +}); diff --git a/test/spec/data/logic/model/plot-summary.js b/test/spec/data/logic/model/plot-summary.js new file mode 100644 index 0000000000..36545a52c4 --- /dev/null +++ b/test/spec/data/logic/model/plot-summary.js @@ -0,0 +1,17 @@ +var Montage = require("montage").Montage; + +/** + * @class PlotSummary + * @extends Montage + */ +exports.PlotSummary = Montage.specialize({ + + movie: { + value: undefined + }, + + summary: { + value: undefined + } + +}); diff --git a/test/spec/data/logic/model/weather-report.js b/test/spec/data/logic/model/weather-report.js new file mode 100644 index 0000000000..4d00a7d886 --- /dev/null +++ b/test/spec/data/logic/model/weather-report.js @@ -0,0 +1,31 @@ +var Montage = require("montage").Montage, + ObjectDescriptor = require("montage/core/meta/object-descriptor").ObjectDescriptor, + ModuleReference = require("montage/core/module-reference").ModuleReference; + +exports.WeatherReport = WeatherReport = Montage.specialize(/** @lends AreaBriefReport.prototype */ { + temp: { + value: null + }, + constructor: { + value: function WeatherReport() {} + } +}, { + + TYPE: { + //get: DataObjectDescriptor.getterFor(exports, "WeatherReport"), + get: function () { + if (!this._TYPE) { + var descriptor = new ObjectDescriptor(); + descriptor._name = "WeatherReport"; + descriptor.exportName = "WeatherReport"; + var info = Montage.getInfoForObject(WeatherReport); + descriptor.module = (new ModuleReference()).initWithIdAndRequire(info.moduleId, info.require); + descriptor.propertyDescriptors = []; + descriptor.objectPrototype = WeatherReport; + this._TYPE = descriptor; + } + + return this._TYPE; + } + } +}); diff --git a/test/spec/data/logic/service/category-service.js b/test/spec/data/logic/service/category-service.js new file mode 100644 index 0000000000..9db03f2b3b --- /dev/null +++ b/test/spec/data/logic/service/category-service.js @@ -0,0 +1,18 @@ +var RawDataService = require("montage-data/logic/service/raw-data-service").RawDataService, + CategoryNames = ["Action"]; + +exports.CategoryService = RawDataService.specialize(/** @lends CategoryService.prototype */ { + + fetchRawData: { + value: function (stream) { + var categoryId = stream.query.criteria.parameters.value || -1, + isValidCategory = categoryId > 0 && CategoryNames.length >= categoryId, + categoryName = isValidCategory && CategoryNames[categoryId - 1] || "Unknown"; + this.addRawData(stream, [{ + name: categoryName + }]); + stream.dataDone(); + } + } + +}); diff --git a/test/spec/data/logic/service/plot-summary-service.js b/test/spec/data/logic/service/plot-summary-service.js new file mode 100644 index 0000000000..134b21a86e --- /dev/null +++ b/test/spec/data/logic/service/plot-summary-service.js @@ -0,0 +1,26 @@ +var RawDataService = require("montage-data/logic/service/raw-data-service").RawDataService; + +exports.PlotSummaryService = RawDataService.specialize(/** @lends PlotSummaryService.prototype */ { + + fetchRawData: { + value: function (stream) { + this.addRawData(stream, [{ + summary: exports.PlotSummaryService.STAR_WARS_PLOT_SUMMARY + }]); + stream.dataDone(); + } + } + +}, { + + STAR_WARS_PLOT_SUMMARY: { + value: "The Imperial Forces -- under orders from cruel Darth Vader (David Prowse) -- " + + "hold Princess Leia (Carrie Fisher) hostage, in their efforts to quell the " + + "rebellion against the Galactic Empire. Luke Skywalker (Mark Hamill) and Han Solo " + + "(Harrison Ford), captain of the Millennium Falcon, work together with the " + + "companionable droid duo R2-D2 (Kenny Baker) and C-3PO (Anthony Daniels) to rescue " + + "the beautiful princess, help the Rebel Alliance, and restore freedom and justice to " + + "the Galaxy." + } + +}); diff --git a/test/spec/data/logic/service/weather-service.js b/test/spec/data/logic/service/weather-service.js new file mode 100644 index 0000000000..de05199911 --- /dev/null +++ b/test/spec/data/logic/service/weather-service.js @@ -0,0 +1,52 @@ +var HttpService = require("montage/data/service/http-service").HttpService, + DataService = require("montage/data/service/data-service").DataService, + DataSelector = require("montage/data/service/data-selector").DataSelector, + WeatherReport = require("../model/weather-report").WeatherReport; + +/** + * Provides area briefs data for Contour applications. + * + * @class + * @extends external:DataService + */ + var WeatherService = exports.WeatherService = HttpService.specialize(/** @lends AreaBriefService.prototype */ { + + API_KEY: { + value: '7593a575795cbd73f54c69a321ed86fe' + }, + + ENDPOINT: { + value: 'http://api.openweathermap.org/data/2.5/' + }, + + types: { + value: [WeatherReport.TYPE] + }, + + fetchRawData: { + value: function (stream) { + var self = this, + criteria = stream.query.criteria; + + return self.fetchHttpRawData(this._getUrl(criteria, true), false).then(function (data) { + if (data) { + self.addRawData(stream, [data], criteria); + self.rawDataDone(stream); + } + }); + } + }, + + mapRawDataToObject: { + value: function (rawData, object, criteria) { + object.temp = rawData.main.temp; + } + }, + + _getUrl: { + value: function (criteria, detect) { + var parameters = criteria.parameters; + return this.ENDPOINT + "weather?q=" + parameters.city + "," + parameters.country + "&units=" + parameters.unit + "&appid=" + this.API_KEY; + } + } +}); diff --git a/test/spec/data/object-descriptor.js b/test/spec/data/object-descriptor.js new file mode 100644 index 0000000000..6216db6ec4 --- /dev/null +++ b/test/spec/data/object-descriptor.js @@ -0,0 +1,133 @@ +var ObjectDescriptor = require("montage/data/model/object-descriptor").ObjectDescriptor, + Montage = require("montage").Montage, + PropertyDescriptor = require("montage/data/model/property-descriptor").PropertyDescriptor; + +describe("An ObjectDescriptor", function() { + + it("can be created", function () { + expect(new ObjectDescriptor()).toBeDefined(); + }); + + it("initially has no type name", function () { + expect(new ObjectDescriptor().typeName).toBeUndefined(); + }); + + it("preserves its type name", function () { + var descriptor = new ObjectDescriptor(), + name = "String" + Math.random(); + descriptor.typeName = name; + expect(descriptor.typeName).toEqual(name); + }); + + it("has Montage.prototype as its initial object prototype value", function () { + expect(new ObjectDescriptor().objectPrototype).toEqual(Montage.prototype); + }); + + it("preserves its object prototype value", function () { + var descriptor = new ObjectDescriptor(), + prototype = Object.create({}); + descriptor.objectPrototype = prototype; + expect(descriptor.objectPrototype).toBe(prototype); + }); + + it("initially has no property descriptors", function () { + expect(new ObjectDescriptor().propertyDescriptors).toEqual({}); + }); + + it("preserves its property descriptors", function () { + var descriptor = new ObjectDescriptor(), + properties = {}, + i; + properties["property" + Math.random()] = new PropertyDescriptor(); + properties["property" + Math.random()] = new PropertyDescriptor(); + properties["property" + Math.random()] = new PropertyDescriptor(); + for (i in properties) { + descriptor.setPropertyDescriptor(i, properties[i]); + } + expect(descriptor.propertyDescriptors).toEqual(properties); + }); + + it("can be created with a getter", function () { + var className1 = "Class" + Math.random(), + className2 = "Class" + Math.random(), + propertyName1 = "property" + Math.random(), + propertyName2 = "property" + Math.random(), + propertyName3 = "property" + Math.random(), + propertyName4 = "property" + Math.random(), + exports = {}, + descriptors1 = {}, + descriptors2 = {}, + descriptor1, + descriptor2; + descriptors1[propertyName1] = {value: Math.random()}; + descriptors1[propertyName2] = {value: Math.random()}; + descriptors2[propertyName3] = {value: Math.random()}; + descriptors2[propertyName4] = {value: Math.random()}; + exports[className1] = Montage.specialize(descriptors1); + exports[className2] = Montage.specialize(descriptors2); + descriptor1 = ObjectDescriptor.getterFor(exports, className1).call({}); + descriptor2 = ObjectDescriptor.getterFor(exports, className2).call({}); + expect(descriptor1).not.toEqual(descriptor2); + expect(descriptor1.typeName).toEqual(className1); + expect(descriptor2.typeName).toEqual(className2); + expect(Object.keys(descriptor1.propertyDescriptors).sort()).toEqual(['_serializableAttributeProperties', propertyName1, propertyName2].sort()); + expect(Object.keys(descriptor2.propertyDescriptors).sort()).toEqual(['_serializableAttributeProperties', propertyName3, propertyName4].sort()); + }); + + // TODO [Charles]: Update this for API changes. + xit("can add relationships", function () { + // Generate test data. + var descriptor = new ObjectDescriptor(), + type1 = new ObjectDescriptor(), + type2 = new ObjectDescriptor(), + expressions1 = {foo: "String" + Math.random(), bar: "String" + Math.random()}, + expressions2 = {a: "String" + Math.random(), b: "String" + Math.random()}, + expressions3 = {x: "String" + Math.random(), y: "String" + Math.random()}, + expressions4 = {}, + relationship1; + // Add one relationship. + type1.name = "String" + Math.random(); + descriptor._addRelationship({ + destinationType: type1, + valueExpressions: expressions1, + criteriaExpressions: expressions2, + }); + // Verify that the corresponding relationship descriptors were added. + expect(Object.keys(descriptor.properties).sort()).toEqual(["bar", "foo"]); + expect(descriptor.properties.foo).toEqual(jasmine.any(PropertyDescriptor)); + expect(descriptor.properties.foo.relationship).toEqual(jasmine.any(RelationshipDescriptor)); + expect(descriptor.properties.foo.relationship.destinationType).toBe(type1); + expect(descriptor.properties.foo.relationship.valueExpressions).toEqual(expressions1); + expect(descriptor.properties.foo.relationship.criteriaExpressions).toEqual(expressions2); + expect(descriptor.properties.bar).toEqual(jasmine.any(PropertyDescriptor)); + expect(descriptor.properties.bar.relationship).toBe(descriptor.properties.foo.relationship); + // Record some state for later testing. + property1 = descriptor.properties.foo; + property2 = descriptor.properties.bar; + // Add another relationship. + type2.name = "String" + Math.random(); + descriptor._addRelationship({ + destinationType: type2, + valueExpressions: expressions3, + criteriaExpressions: expressions4, + }); + // Verify that the properties corresponding to the originally added + // relationship have not been affected. + expect(descriptor.properties.foo).toBe(property1); + expect(descriptor.properties.bar).toBe(property2); + // Verify that the correspondingproperty descriptors have been added, + // and that they all includes a reference to the added relationship. + expect(Object.keys(descriptor.properties).sort()).toEqual(["bar", "foo", "x", "y"]); + expect(descriptor.properties.x).toEqual(jasmine.any(PropertyDescriptor)); + expect(descriptor.properties.x.relationship).toEqual(jasmine.any(RelationshipDescriptor)); + expect(descriptor.properties.x.relationship.destinationType).toBe(type2); + expect(descriptor.properties.x.relationship.valueExpressions).toEqual(expressions3); + expect(descriptor.properties.x.relationship.criteriaExpressions).toEqual(expressions4); + expect(descriptor.properties.y).toEqual(jasmine.any(PropertyDescriptor)); + expect(descriptor.properties.y.relationship).toBe(descriptor.properties.x.relationship); + }); + + + xit("needs to be further tested", function () {}); + +}); diff --git a/test/spec/data/property-descriptor.js b/test/spec/data/property-descriptor.js new file mode 100644 index 0000000000..782dd710ac --- /dev/null +++ b/test/spec/data/property-descriptor.js @@ -0,0 +1,29 @@ +var PropertyDescriptor = require("montage/data/model/property-descriptor").PropertyDescriptor; + +describe("A PropertyDescriptor", function() { + + it("can be created", function () { + expect(new PropertyDescriptor()).toBeDefined(); + }); + + it("initially is not a relationship", function () { + expect(new PropertyDescriptor().isRelationship).toEqual(false); + }); + + it("preserves its relationship status", function () { + var descriptor = new PropertyDescriptor(); + descriptor.isRelationship = true; + expect(descriptor.isRelationship).toEqual(true); + }); + + it("initially is not optional", function () { + expect(new PropertyDescriptor().isOptional).toEqual(false); + }); + + it("preserves its optional status", function () { + var descriptor = new PropertyDescriptor(); + descriptor.isOptional = true; + expect(descriptor.isOptional).toEqual(true); + }); + +}); diff --git a/test/spec/data/raw-data-service.js b/test/spec/data/raw-data-service.js new file mode 100644 index 0000000000..bc25603adc --- /dev/null +++ b/test/spec/data/raw-data-service.js @@ -0,0 +1,380 @@ +var RawDataService = require("montage/data/service/raw-data-service").RawDataService, + DataService = require("montage/data/service/data-service").DataService, + DataStream = require("montage/data/service/data-stream").DataStream, + DataObjectDescriptor = require("montage/data/model/data-object-descriptor").DataObjectDescriptor; + +describe("A RawDataService", function() { + + it("can be created", function () { + expect(new RawDataService()).toBeDefined(); + }); + + it("initially has no parent, is a root service, and is the main service", function () { + var service; + + // Create the service after resetting the main service. + DataService.mainService = undefined; + service = new DataService(); + + // Verify that the service has no parent and is the root and main. + expect(service.parentService).toBeUndefined(); + expect(service.rootService).toEqual(service); + expect(DataService.mainService).toEqual(service); + + // Try to set a parent and verify again. + service.parentService = new DataService(); + expect(service.parentService).toBeUndefined(); + expect(service.rootService).toEqual(service); + expect(DataService.mainService).toEqual(service); + }); + + it("can be a parent, child, or grandchild service", function () { + var parent, child, grandchild; + + // Create the parent, child, and grandchild after resetting the main + // service, and tie them all together. + DataService.mainService = undefined; + parent = new RawDataService(), + parent.jasmineToString = function () { return "PARENT"; }; + child = new RawDataService(); + child.jasmineToString = function () { return "CHILD"; }; + grandchild = new RawDataService(); + grandchild.jasmineToString = function () { return "GRANDCHILD"; }; + parent.addChildService(child); + child.addChildService(grandchild); + + // Verify that the parents, roots, and main are correct. + expect(parent.parentService).toBeUndefined(); + expect(parent.rootService).toEqual(parent); + expect(child.parentService).toEqual(parent); + expect(child.rootService).toEqual(parent); + expect(grandchild.parentService).toEqual(child); + expect(grandchild.rootService).toEqual(parent); + expect(DataService.mainService).toEqual(parent); + + // Try to set new parents and verify again. + parent.parentService = new RawDataService(); + child.parentService = new RawDataService(); + grandchild.parentService = new RawDataService(); + expect(parent.parentService).toBeUndefined(); + expect(parent.rootService).toEqual(parent); + expect(child.parentService).toEqual(parent); + expect(child.rootService).toEqual(parent); + expect(grandchild.parentService).toEqual(child); + expect(grandchild.rootService).toEqual(parent); + expect(DataService.mainService).toEqual(parent); + }); + + it("manages children correctly", function () { + var toString, Types, objects, Child, children, parent; + + // Define test types with ObjectDescriptors. + toString = function () { return "Type" + this.id; }; + Types = [0, 1, 2, 3].map(function () { return function () {}; }); + Types.forEach(function (type) { type.TYPE = new DataObjectDescriptor(); }); + Types.forEach(function (type) { type.TYPE.toString = toString; }); + Types.forEach(function (type) { type.TYPE.jasmineToString = toString; }); + Types.forEach(function (type, index) { type.TYPE.id = index; }); + + // Define test objects for each of the test types. + toString = function () { return "Object" + this.id; }; + objects = Types.map(function (type) { return new type(); }); + objects.forEach(function (object) { object.toString = toString; }); + objects.forEach(function (object) { object.jasmineToString = toString; }); + objects.forEach(function (object, index) { object.id = index; }); + + // Create test children with unique identifiers to help with debugging. + toString = function () { return "Child" + this.id; }; + children = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(function () { return new RawDataService(); }); + children.forEach(function (child) { child.toString = toString; }); + children.forEach(function (child) { child.jasmineToString = toString; }); + children.forEach(function (child, index) { child.id = index; }); + + // Define a variety of types for the test children. Children with an + // undefined, null, or empty types array will be "all types" children. + children.forEach(function (child) { Object.defineProperty(child, "types", {writable: true}); }); + children[0].types = [Types[0].TYPE]; + children[1].types = [Types[0].TYPE]; + children[2].types = [Types[1].TYPE]; + children[3].types = [Types[0].TYPE, Types[1].TYPE]; + children[4].types = [Types[0].TYPE, Types[2].TYPE]; + children[5].types = [Types[1].TYPE, Types[2].TYPE]; + children[6].types = [Types[0].TYPE, Types[1].TYPE, Types[2].TYPE]; + children[7].types = undefined; + children[8].types = null; + children[9].types = []; + + // Create a service with the desired children. + parent = new RawDataService(); + parent.toString = function () { return "PARENT"; }; + parent.jasmineToString = parent.toString; + children.forEach(function (child) { parent.addChildService(child); }); + + // Verify the initial parents, types, and type-to-child mapping. + expect(parent.parentService).toBeUndefined(); + expect(children[0].parentService).toEqual(parent); + expect(children[1].parentService).toEqual(parent); + expect(children[2].parentService).toEqual(parent); + expect(children[3].parentService).toEqual(parent); + expect(children[4].parentService).toEqual(parent); + expect(children[5].parentService).toEqual(parent); + expect(children[6].parentService).toEqual(parent); + expect(children[7].parentService).toEqual(parent); + expect(children[8].parentService).toEqual(parent); + expect(children[9].parentService).toEqual(parent); + expect(parent.types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE, Types[2].TYPE]); + expect(children[0].types.sort()).toEqual([Types[0].TYPE]); + expect(children[1].types.sort()).toEqual([Types[0].TYPE]); + expect(children[2].types.sort()).toEqual([Types[1].TYPE]); + expect(children[3].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE]); + expect(children[4].types.sort()).toEqual([Types[0].TYPE, Types[2].TYPE]); + expect(children[5].types.sort()).toEqual([Types[1].TYPE, Types[2].TYPE]); + expect(children[6].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE, Types[2].TYPE]); + expect(children[7].types).toBeUndefined(); + expect(children[8].types).toBeNull(); + expect(children[9].types).toEqual([]); + expect(parent.childServiceForType(Types[0].TYPE)).toEqual(children[0]); + 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]); + + // Modify the children and verify the resulting service parent, types, + // and type-to-child mapping. + parent.removeChildService(children[0]); + parent.removeChildService(children[1]); + expect(parent.parentService).toBeUndefined(); + expect(children[2].parentService).toEqual(parent); + expect(children[3].parentService).toEqual(parent); + expect(children[4].parentService).toEqual(parent); + expect(children[5].parentService).toEqual(parent); + expect(children[6].parentService).toEqual(parent); + expect(children[7].parentService).toEqual(parent); + expect(children[8].parentService).toEqual(parent); + expect(children[9].parentService).toEqual(parent); + expect(parent.types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE, Types[2].TYPE]); + expect(children[0].types.sort()).toEqual([Types[0].TYPE]); + expect(children[1].types.sort()).toEqual([Types[0].TYPE]); + expect(children[2].types.sort()).toEqual([Types[1].TYPE]); + expect(children[3].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE]); + expect(children[4].types.sort()).toEqual([Types[0].TYPE, Types[2].TYPE]); + expect(children[5].types.sort()).toEqual([Types[1].TYPE, Types[2].TYPE]); + expect(children[6].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE, Types[2].TYPE]); + expect(children[7].types).toBeUndefined(); + expect(children[8].types).toBeNull(); + expect(children[9].types).toEqual([]); + expect(parent.childServiceForType(Types[0].TYPE)).toEqual(children[3]); + 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]); + + // Modify and verify some more. + parent.removeChildService(children[3]); + expect(parent.parentService).toBeUndefined(); + expect(children[2].parentService).toEqual(parent); + expect(children[4].parentService).toEqual(parent); + expect(children[5].parentService).toEqual(parent); + expect(children[6].parentService).toEqual(parent); + expect(children[7].parentService).toEqual(parent); + expect(children[8].parentService).toEqual(parent); + expect(children[9].parentService).toEqual(parent); + expect(parent.types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE, Types[2].TYPE]); + expect(children[0].types.sort()).toEqual([Types[0].TYPE]); + expect(children[1].types.sort()).toEqual([Types[0].TYPE]); + expect(children[2].types.sort()).toEqual([Types[1].TYPE]); + expect(children[3].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE]); + expect(children[4].types.sort()).toEqual([Types[0].TYPE, Types[2].TYPE]); + expect(children[5].types.sort()).toEqual([Types[1].TYPE, Types[2].TYPE]); + expect(children[6].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE, Types[2].TYPE]); + expect(children[7].types).toBeUndefined(); + expect(children[8].types).toBeNull(); + expect(children[9].types).toEqual([]); + expect(parent.childServiceForType(Types[0].TYPE)).toEqual(children[4]); + 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]); + + // 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 + // returned for that type. + parent.removeChildService(children[4]); + parent.removeChildService(children[6]); + expect(parent.parentService).toBeUndefined(); + expect(children[2].parentService).toEqual(parent); + expect(children[5].parentService).toEqual(parent); + expect(children[7].parentService).toEqual(parent); + expect(children[8].parentService).toEqual(parent); + expect(children[9].parentService).toEqual(parent); + expect(parent.types.sort()).toEqual([Types[1].TYPE, Types[2].TYPE]); + expect(children[0].types.sort()).toEqual([Types[0].TYPE]); + expect(children[1].types.sort()).toEqual([Types[0].TYPE]); + expect(children[2].types.sort()).toEqual([Types[1].TYPE]); + expect(children[3].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE]); + expect(children[4].types.sort()).toEqual([Types[0].TYPE, Types[2].TYPE]); + expect(children[5].types.sort()).toEqual([Types[1].TYPE, Types[2].TYPE]); + expect(children[6].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE, Types[2].TYPE]); + expect(children[7].types).toBeUndefined(); + expect(children[8].types).toBeNull(); + expect(children[9].types).toEqual([]); + expect(parent.childServiceForType(Types[0].TYPE)).toEqual(children[7]); + 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]); + + // Modify and verify some more. + parent.removeChildService(children[5]); + parent.removeChildService(children[7]); + expect(parent.parentService).toBeUndefined(); + expect(children[2].parentService).toEqual(parent); + expect(children[8].parentService).toEqual(parent); + expect(children[9].parentService).toEqual(parent); + expect(parent.types.sort()).toEqual([Types[1].TYPE]); + expect(children[0].types.sort()).toEqual([Types[0].TYPE]); + expect(children[1].types.sort()).toEqual([Types[0].TYPE]); + expect(children[2].types.sort()).toEqual([Types[1].TYPE]); + expect(children[3].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE]); + expect(children[4].types.sort()).toEqual([Types[0].TYPE, Types[2].TYPE]); + expect(children[5].types.sort()).toEqual([Types[1].TYPE, Types[2].TYPE]); + expect(children[6].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE, Types[2].TYPE]); + expect(children[7].types).toBeUndefined(); + expect(children[8].types).toBeNull(); + expect(children[9].types).toEqual([]); + expect(parent.childServiceForType(Types[0].TYPE)).toEqual(children[8]); + 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]); + + // Modify and verify some more. + parent.removeChildService(children[2]); + parent.removeChildService(children[8]); + expect(parent.parentService).toBeUndefined(); + expect(children[9].parentService).toEqual(parent); + expect(parent.types.sort()).toEqual([]); + expect(children[0].types.sort()).toEqual([Types[0].TYPE]); + expect(children[1].types.sort()).toEqual([Types[0].TYPE]); + expect(children[2].types.sort()).toEqual([Types[1].TYPE]); + expect(children[3].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE]); + expect(children[4].types.sort()).toEqual([Types[0].TYPE, Types[2].TYPE]); + expect(children[5].types.sort()).toEqual([Types[1].TYPE, Types[2].TYPE]); + expect(children[6].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE, Types[2].TYPE]); + expect(children[7].types).toBeUndefined(); + expect(children[8].types).toBeNull(); + expect(children[9].types).toEqual([]); + expect(parent.childServiceForType(Types[0].TYPE)).toEqual(children[9]); + 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]); + + // Modify and verify some more. + parent.removeChildService(children[9]); + expect(parent.parentService).toBeUndefined(); + expect(parent.types.sort()).toEqual([]); + expect(children[0].types.sort()).toEqual([Types[0].TYPE]); + expect(children[1].types.sort()).toEqual([Types[0].TYPE]); + expect(children[2].types.sort()).toEqual([Types[1].TYPE]); + expect(children[3].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE]); + expect(children[4].types.sort()).toEqual([Types[0].TYPE, Types[2].TYPE]); + expect(children[5].types.sort()).toEqual([Types[1].TYPE, Types[2].TYPE]); + expect(children[6].types.sort()).toEqual([Types[0].TYPE, Types[1].TYPE, Types[2].TYPE]); + expect(children[7].types).toBeUndefined(); + expect(children[8].types).toBeNull(); + expect(children[9].types).toEqual([]); + expect(parent.childServiceForType(Types[0].TYPE)).toBeNull(); + 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(); + }); + + it("has a fetchData() method", function () { + expect(new RawDataService().fetchData).toEqual(jasmine.any(Function)); + }); + + xit("has a fetchData() method that uses the passed in stream when one is specified", function () { + }); + + xit("has a fetchData() method that creates and return a new stream when none is passed in", function () { + }); + + xit("has a fetchData() method that sets its stream's selector", function () { + }); + + xit("has a fetchData() method that calls the service's fetchRawData() when appropriate", function () { + }); + + xit("has a fetchData() xmethod that calls a child service's fetchRawData() when appropraite", function () { + }); + + it("has a fetchRawData() method", function () { + expect(new RawDataService().fetchRawData).toEqual(jasmine.any(Function)); + }); + + it("has a fetchRawData() method that fetches empty data by default", function (done) { + // Call fetchRawData() and verify the resulting stream's initial data. + var stream = new DataStream(); + new RawDataService().fetchRawData(stream); + expect(stream.data).toEqual([]); + // Make sure the stream's promise is fulfilled with the same data. + stream.then(function (data) { + expect(data).toBe(stream.data); + expect(data).toEqual([]); + done(); + }); + }); + + it("has a addRawData() method", function () { + expect(new RawDataService().addRawData).toEqual(jasmine.any(Function)); + }); + + xit("has a addRawData() method that maps the data it receives", function () { + }); + + xit("has a addRawData() method that calls the specified stream's addData() with the mapped data", function () { + }); + + xit("has a addRawData() method that needs to be further tested", function () {}); + + it("has a mapFromRawData() method", function () { + expect(new RawDataService().mapFromRawData).toEqual(jasmine.any(Function)); + }); + + xit("has a mapFromRawData() method that needs to be further tested", function () {}); + + it("has a rawDataDone() method", function () { + expect(new RawDataService().rawDataDone).toEqual(jasmine.any(Function)); + }); + + xit("has a rawDataDone() method that calls the specified stream's dataDone()", function () { + }); + + xit("has a registerService() method that needs to be further tested", function () {}); + + xit("has a mainService class variable that needs to be further tested", function () {}); + +}); diff --git a/test/spec/data/snapshot-service.js b/test/spec/data/snapshot-service.js new file mode 100644 index 0000000000..884dc3e83c --- /dev/null +++ b/test/spec/data/snapshot-service.js @@ -0,0 +1,61 @@ +var SnapshotService = require("montage/data/service/snapshot-service").SnapshotService; + + +describe("A Snapshot Service", function () { + + it("can be created", function () { + expect(new SnapshotService()).toBeDefined(); + }); + + it("can compare save snapshots correctly", function () { + expect(true).toBeTruthy(); + }); + + it("can compare get differences between snapshots correctly", function () { + expect(true).toBeTruthy(); + // var service = new SnapshotService(), + // s1 = { + // familyName: "Bond", + // givenName: "James" + // }, + // s2 = { + // familyName: "Bond", + // givenName: "James" + // }; + + }); + + it("can compare snapshots correctly", function () { + var service = new SnapshotService(), + s1 = { + familyName: "Bond", + givenName: "James" + }, + s2 = { + familyName: "Bond", + givenName: "James" + }, + s3 = { + a: "Trigger", + b: "Pussy Galore", + c: null, + d: "Vesper Lynd" + }, + s4 = { + a: "Trigger", + b: "Pussy Galore", + c: undefined, + d: "Vesper Lynd" + }; + expect(service._equals(s1, s2)).toBeTruthy(); + s2.familyName = "Bray"; + s2.givenName = "Hilary"; + expect(service._equals(s1, s2)).toBeFalsy(); + s2.familyName = "Bond"; + s2.givenName = s1.givenName = null; + expect(service._equals(s1, s2)).toBeTruthy(); + expect(service._areSameValues(s3, s4)).toBeTruthy(); + + }); + +}); diff --git a/ui/authorization-manager-panel.reel/authorization-manager-panel.css b/ui/authorization-manager-panel.reel/authorization-manager-panel.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ui/authorization-manager-panel.reel/authorization-manager-panel.html b/ui/authorization-manager-panel.reel/authorization-manager-panel.html new file mode 100644 index 0000000000..9bc6f9aea0 --- /dev/null +++ b/ui/authorization-manager-panel.reel/authorization-manager-panel.html @@ -0,0 +1,41 @@ + + + + + + + + +
+
+
+
+
+ + diff --git a/ui/authorization-manager-panel.reel/authorization-manager-panel.js b/ui/authorization-manager-panel.reel/authorization-manager-panel.js new file mode 100644 index 0000000000..30ae859a0d --- /dev/null +++ b/ui/authorization-manager-panel.reel/authorization-manager-panel.js @@ -0,0 +1,108 @@ +var Component = require("ui/component").Component, + Promise = require("core/promise").Promise, + application = require("core/application").application, + Map = require("collections/map"); + +/** + * @class Main + * @extends Component + */ +exports.AuthorizationManagerPanel = Component.specialize({ + + authorizationPanels: { + get: function () { + if (!this._authorizationPanels) { + this._authorizationPanels = []; + } + return this._authorizationPanels; + }, + set: function (value) { + this._authorizationPanels = value; + } + }, + + authorizationManager: { + value: undefined + }, + + approveAuthorization: { + value: function (authorization, authorizationPanel) { + + var index = this.authorizationPanels.indexOf(authorizationPanel); + if (index !== -1) { + this.authorizationPanels.splice(index, 1); + } + this._panels.get(authorizationPanel).resolve(authorization); + + if (!this.authorizationPanels.length && this._authorizationResolve) { + this._authorizationResolve(authorization); + } + } + }, + + _panels: { + get: function () { + if (!this.__panels) { + this.__panels = new Map(); + } + return this.__panels; + } + }, + + authorizeWithPanel: { + value: function (authorizationPanel) { + var self = this, + promise; + + if (!this._panels.has(authorizationPanel)) { + promise = new Promise(function (resolve, reject) { + self._panels.set(authorizationPanel, { + resolve: resolve, + reject: reject + }); + }); + this.authorizationPanels.push(authorizationPanel); + } else { + promise = Promise.resolve(null); + } + + return promise; + } + }, + + _authorizationResolve: { + value: void 0 + }, + + cancelAuthorization: { + value: function() { + if (application.applicationModal) { + application.applicationModal.hide(this); + } + if (this._authorizationResolve) { + this._authorizationReject("CANCEL"); + } + } + }, + + _authorizationReject: { + value: void 0 + }, + + runModal: { + value: function() { + var self = this; + return new Promise(function(resolve, reject) { + self._authorizationResolve = resolve; + self._authorizationReject = reject; + // FIXME This is temporary shortcut for FreeNAS while we fix Montage's modal. + if (application.applicationModal) { + application.applicationModal.show(self); + } + }); + } + } + +}); + +// FIXME: Selection needs to be managed by a selection controller diff --git a/ui/authorization-panel.reel/authorization-panel.css b/ui/authorization-panel.reel/authorization-panel.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ui/authorization-panel.reel/authorization-panel.html b/ui/authorization-panel.reel/authorization-panel.html new file mode 100644 index 0000000000..df0529c13f --- /dev/null +++ b/ui/authorization-panel.reel/authorization-panel.html @@ -0,0 +1,20 @@ + + + + + + + + +
+
+ + diff --git a/ui/authorization-panel.reel/authorization-panel.js b/ui/authorization-panel.reel/authorization-panel.js new file mode 100644 index 0000000000..5e8c50409d --- /dev/null +++ b/ui/authorization-panel.reel/authorization-panel.js @@ -0,0 +1,30 @@ +var Component = require("ui/component").Component, + AuthorizationManager = require("data/service/authorization-manager").AuthorizationManager, + deprecate = require("core/deprecate"); + +/** + * @class Main + * @extends Component + */ +exports.AuthorizationPanel = Component.specialize({ + + dataService: { + get: deprecate.deprecateMethod(void 0, function () { + return !!this.service; + }, "dataService", "service"), + set: deprecate.deprecateMethod(void 0, function () { + return !!this.service; + }, "dataService", "service") + }, + + service: { + value: null + }, + + authorizationManagerPanel: { + get: function() { + return AuthorizationManager.authorizationManagerPanel; + } + } + +});