From da4f20b161f05f12e0dd2ac107a5c604725488fd Mon Sep 17 00:00:00 2001 From: Thomas Jaede Date: Tue, 8 Aug 2017 14:11:02 -0700 Subject: [PATCH 01/31] Add documentation to pipeline-converter & property-descriptor --- core/converter/pipeline-converter.js | 13 ++++++++++++- core/meta/property-descriptor.js | 2 +- jsdoc.json | 3 ++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/core/converter/pipeline-converter.js b/core/converter/pipeline-converter.js index 6283da5e66..d3c5a1845d 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({ @@ -27,10 +33,15 @@ exports.PipelineConverter = Converter.specialize({ * Convert/Revert */ + /** + * The converters to chain on convert() + * @type {Converter[]} + */ converters: { value: undefined }, + convert: { value: function (value) { return this._convertWithNextConverter(value, this.converters.slice()); diff --git a/core/meta/property-descriptor.js b/core/meta/property-descriptor.js index ba7152929a..c5579621ee 100644 --- a/core/meta/property-descriptor.js +++ b/core/meta/property-descriptor.js @@ -268,7 +268,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/jsdoc.json b/jsdoc.json index f8d052e9d1..7baa8482ef 100644 --- a/jsdoc.json +++ b/jsdoc.json @@ -11,6 +11,7 @@ "montage.js", "composer/", "core/", + "core/converter/", "ui/" ], "includePattern": ".+\\.js(doc)?$", @@ -32,4 +33,4 @@ "collapseSymbols" : false, "inverseNav" : true } -} \ No newline at end of file +} From 1b5b204c32cb61016cad5b420da57573a44e039b Mon Sep 17 00:00:00 2001 From: Thomas Jaede Date: Thu, 10 Aug 2017 10:58:13 -0700 Subject: [PATCH 02/31] Add entire Montage Data repo --- core/event/event-manager.js | 14 +- .../raw-property-value-to-object-converter.js | 300 ++ data/model/data-identifier.js | 117 + data/model/data-object-descriptor.js | 243 ++ data/model/data-ordering.js | 74 + data/model/data-property-descriptor.js | 23 + data/model/data-query.js | 155 + data/model/data-selector.js | 13 + data/model/enumeration.js | 309 ++ data/model/object-descriptor.js | 176 ++ data/model/property-descriptor.js | 25 + data/montage-data.js | 19 + data/service/authorization-manager.js | 363 +++ data/service/authorization-service.js | 20 + data/service/authorization.js | 43 + data/service/blueprint-data-mapping.js | 144 + data/service/connection-descriptor.js | 22 + data/service/data-mapping.js | 94 + data/service/data-operation.js | 160 ++ data/service/data-provider.js | 97 + data/service/data-selector.js | 11 + data/service/data-service.js | 2556 +++++++++++++++++ data/service/data-stream.js | 362 +++ data/service/data-trigger.js | 587 ++++ data/service/expression-data-mapping.js | 952 ++++++ data/service/http-service.js | 557 ++++ data/service/indexed-d-b-data-service.js | 135 + data/service/mapping-rule.js | 91 + data/service/offline-service.js | 1416 +++++++++ data/service/persistent-data-service.js | 1612 +++++++++++ data/service/raw-data-service.js | 1167 ++++++++ data/service/sample.bp | 55 + data/service/snapshot-service.js | 170 ++ jsdoc.json | 7 +- package.json | 7 +- test/all.js | 24 +- test/data/all.js | 22 + test/data/package.json | 30 + test/data/run-browser.js | 112 + test/data/run-karma.html | 14 + test/data/run-karma.js | 103 + test/data/run-montage.html | 14 + test/data/run-node.js | 50 + test/data/run.html | 14 + test/spec/data/data-mapping.js | 32 + test/spec/data/data-object-descriptor.js | 79 + test/spec/data/data-property-descriptor.js | 29 + test/spec/data/data-provider.js | 17 + test/spec/data/data-selector.js | 76 + test/spec/data/data-service.js | 422 +++ test/spec/data/data-stream.js | 308 ++ test/spec/data/enumeration.js | 292 ++ test/spec/data/expression-data-mapping.js | 210 ++ test/spec/data/http-service.js | 33 + test/spec/data/logic/model/category.js | 13 + test/spec/data/logic/model/movie.js | 37 + test/spec/data/logic/model/plot-summary.js | 17 + test/spec/data/logic/model/weather-report.js | 31 + .../data/logic/service/category-service.js | 18 + .../logic/service/plot-summary-service.js | 26 + .../data/logic/service/weather-service.js | 52 + test/spec/data/object-descriptor.js | 133 + test/spec/data/offline-service.js | 7 + test/spec/data/property-descriptor.js | 29 + test/spec/data/raw-data-service.js | 380 +++ test/spec/data/snapshot-service.js | 61 + .../authorization-manager-panel.css | 0 .../authorization-manager-panel.html | 41 + .../authorization-manager-panel.js | 108 + .../authorization-panel.css | 0 .../authorization-panel.html | 20 + .../authorization-panel.js | 30 + 72 files changed, 14965 insertions(+), 15 deletions(-) create mode 100644 data/converter/raw-property-value-to-object-converter.js create mode 100644 data/model/data-identifier.js create mode 100644 data/model/data-object-descriptor.js create mode 100644 data/model/data-ordering.js create mode 100644 data/model/data-property-descriptor.js create mode 100644 data/model/data-query.js create mode 100644 data/model/data-selector.js create mode 100644 data/model/enumeration.js create mode 100644 data/model/object-descriptor.js create mode 100644 data/model/property-descriptor.js create mode 100644 data/montage-data.js create mode 100644 data/service/authorization-manager.js create mode 100644 data/service/authorization-service.js create mode 100644 data/service/authorization.js create mode 100644 data/service/blueprint-data-mapping.js create mode 100644 data/service/connection-descriptor.js create mode 100644 data/service/data-mapping.js create mode 100644 data/service/data-operation.js create mode 100644 data/service/data-provider.js create mode 100644 data/service/data-selector.js create mode 100644 data/service/data-service.js create mode 100644 data/service/data-stream.js create mode 100644 data/service/data-trigger.js create mode 100644 data/service/expression-data-mapping.js create mode 100644 data/service/http-service.js create mode 100644 data/service/indexed-d-b-data-service.js create mode 100644 data/service/mapping-rule.js create mode 100644 data/service/offline-service.js create mode 100644 data/service/persistent-data-service.js create mode 100644 data/service/raw-data-service.js create mode 100644 data/service/sample.bp create mode 100644 data/service/snapshot-service.js create mode 100644 test/data/all.js create mode 100644 test/data/package.json create mode 100644 test/data/run-browser.js create mode 100644 test/data/run-karma.html create mode 100644 test/data/run-karma.js create mode 100644 test/data/run-montage.html create mode 100644 test/data/run-node.js create mode 100644 test/data/run.html create mode 100644 test/spec/data/data-mapping.js create mode 100644 test/spec/data/data-object-descriptor.js create mode 100644 test/spec/data/data-property-descriptor.js create mode 100644 test/spec/data/data-provider.js create mode 100644 test/spec/data/data-selector.js create mode 100644 test/spec/data/data-service.js create mode 100644 test/spec/data/data-stream.js create mode 100644 test/spec/data/enumeration.js create mode 100644 test/spec/data/expression-data-mapping.js create mode 100644 test/spec/data/http-service.js create mode 100644 test/spec/data/logic/model/category.js create mode 100644 test/spec/data/logic/model/movie.js create mode 100644 test/spec/data/logic/model/plot-summary.js create mode 100644 test/spec/data/logic/model/weather-report.js create mode 100644 test/spec/data/logic/service/category-service.js create mode 100644 test/spec/data/logic/service/plot-summary-service.js create mode 100644 test/spec/data/logic/service/weather-service.js create mode 100644 test/spec/data/object-descriptor.js create mode 100644 test/spec/data/offline-service.js create mode 100644 test/spec/data/property-descriptor.js create mode 100644 test/spec/data/raw-data-service.js create mode 100644 test/spec/data/snapshot-service.js create mode 100644 ui/authorization-manager-panel.reel/authorization-manager-panel.css create mode 100644 ui/authorization-manager-panel.reel/authorization-manager-panel.html create mode 100644 ui/authorization-manager-panel.reel/authorization-manager-panel.js create mode 100644 ui/authorization-panel.reel/authorization-panel.css create mode 100644 ui/authorization-panel.reel/authorization-panel.html create mode 100644 ui/authorization-panel.reel/authorization-panel.js diff --git a/core/event/event-manager.js b/core/event/event-manager.js index 4e1c2238be..0a54acfe59 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--; - } + } } }, /** @@ -1055,8 +1055,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); } }, @@ -1421,7 +1421,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); } } @@ -2375,7 +2375,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/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..3982a33dc4 --- /dev/null +++ b/data/converter/raw-property-value-to-object-converter.js @@ -0,0 +1,300 @@ +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"), + compile = require("frb/compile-evaluator"); + +/** + * @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 {?ObjectDescriptor} + **/ + objectDescriptor: { + get: function () { + return this._objectDescriptor ? this._objectDescriptor : + this.owner && this.owner.objectDescriptor ? this.owner.objectDescriptor : + undefined; + }, + set: function (value) { + this._objectDescriptor = 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.service ? this.owner.service : + undefined; + }, + set: function (value) { + this._service = 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), + descriptorPromise = this.foreignDescriptor || Promise.resolve(this.objectDescriptor), + query; + + return descriptorPromise.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.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..85ef55bf23 --- /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..8f707e2f45 --- /dev/null +++ b/data/model/enumeration.js @@ -0,0 +1,309 @@ +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; + // 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); + for (i in constants) { + enumeration[i] = create.apply(enumeration, constants[i]); + } + // 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..5a647b9a11 --- /dev/null +++ b/data/model/object-descriptor.js @@ -0,0 +1,176 @@ +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; + for (i in types) { + descriptor = this.makePropertyDescriptor(); + this.setPropertyDescriptor(i, 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..cf354149fd --- /dev/null +++ b/data/service/authorization-manager.js @@ -0,0 +1,363 @@ +var Montage = require("core/core").Montage, + Promise = require("core/promise").Promise, + Map = require("collections/map"), + application = require("core/application").application, + DataService = require("data/service/data-service").DataService, + 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(); + DataService.authorizationManager = this; + 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.authorization; + }).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, providerName; + for (providerName in exports) { + 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) { + var self = this, + authorizationPromises = []; + + if (dataService.authorizationPolicy === DataService.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 === DataService.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-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..023e26ba8a --- /dev/null +++ b/data/service/data-mapping.js @@ -0,0 +1,94 @@ +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 key; + if (data) { + for (key in data) { + 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. + } + }, + + /*************************************************************************** + * 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); + } + } + +}); 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..bb465f2a8a --- /dev/null +++ b/data/service/data-service.js @@ -0,0 +1,2556 @@ +var Montage = require("core/core").Montage, + 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"); + + + + +/** + * 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 = 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"); + +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, + // 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] || + [], + mappings = child.mappings || []; + + 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 { + debugger; + 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) { + service = this.childServiceForType(query.type); + } + + 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. + for (j = i + 1; j < n && child && (jService = services.get((jOperation = operations[j]))) === child; j += 1) {} + // 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: undefined + } + +}); 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..dc494bcbe2 --- /dev/null +++ b/data/service/data-trigger.js @@ -0,0 +1,587 @@ +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 = {}, + trigger, name; + for (name in type.propertyDescriptors) { + 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 name; + for (name in triggers) { + 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..2a1dce1cef --- /dev/null +++ b/data/service/expression-data-mapping.js @@ -0,0 +1,952 @@ +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; + this.objectDescriptorReference = deserializer.getProperty("objectDescriptor"); + 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 = [], + key; + + for (key in rules) { + 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 rules = this._compiledRawDataMappingRules, + scope = new Scope(object), + rule = rules[property], + propertyDescriptor = rule && rule.propertyDescriptor, + promise; + + + if (propertyDescriptor && propertyDescriptor.valueDescriptor && rule.converter) { + rule.converter.expression = rule.converter.expression || rule.expression; + rule.converter.foreignDescriptor = rule.converter.foreignDescriptor || propertyDescriptor.valueDescriptor; + // rule.converter.service = rule.converter.service || this.service.rootService; + promise = this._convertRelationshipToRawData(object, propertyDescriptor, 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); + } + }, + + /** + * 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, + propertyName, rawRule, rule; + + for (propertyName in rawRules) { + 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, + propertyName, rawRule, rule; + for (propertyName in rawRules) { + 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) { + debugger; + } + 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), + isRelationship = !!(propertyDescriptor && propertyDescriptor.valueDescriptor), + scope = this._scope, + self = this, + result; + + scope.value = data; + if (!propertyDescriptor || propertyDescriptor.definition) { + result = Promise.resolve(); + } else { + self._prepareConverterForRule(rule.converter, rule, propertyDescriptor); + result = isRelationship ? self._resolveRelationship(object, propertyDescriptor, rule, scope) : + self._resolvePrimitive(object, propertyDescriptor, rule, scope); + } + return result; + } + }, + + + _prepareConverterForRule: { + value: function (converter, rule, propertyDescriptor) { + if (converter) { + converter.expression = converter.expression || rule.expression; + + if (!converter.foreignDescriptor && propertyDescriptor.valueDescriptor) { + 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 ? 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..2f9e447c58 --- /dev/null +++ b/data/service/http-service.js @@ -0,0 +1,557 @@ +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, + 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); + + for (i in parsed.headers) { + request.setRequestHeader(i, parsed.headers[i]); + } + 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 { + for (i = 3, n = last; i < n && arguments[i] instanceof exports.HttpService.DataType; i += 1) {} + 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..8d94ea7d00 --- /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 = 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/offline-service.js b/data/service/offline-service.js new file mode 100644 index 0000000000..5d223d5ec9 --- /dev/null +++ b/data/service/offline-service.js @@ -0,0 +1,1416 @@ +var RawDataService = require("data/service/raw-data-service").RawDataService, + DataStream = require("data/service/data-stream").DataStream, + Dexie = require("dexie"), + 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 +* +* @class +* @extends RawDataService +*/ +exports.OfflineService = OfflineService = RawDataService.specialize(/** @lends OfflineService.prototype */ { + + /*************************************************************************** + * Initializing + */ + + constructor: { + value: function OfflineService() { + RawDataService.call(this); + } + }, + + _db : { + value: void 0 + }, + + schema : { + value: void 0 + }, + + name : { + value: void 0 + }, + + /** + * @type {string} + */ + _is_safari_10_1_upgrade_issue_fixed: { + value: "is_IOS_10_3_upgrade_issue_fixed" + }, + + _faulty_safari_versions: { + get: function () { + return [ + ["iPhone OS 10_3_0", "AppleWebKit/603.1.30"], + ["iPhone OS 10_3_1", "AppleWebKit/603.1.30"], + ["Mac OS X 10_12_4", "AppleWebKit/603.1.30"] + ] + } + }, + + _isFaultySafariVersion: { + get: function () { + return this._faulty_safari_versions.some(function (userAgentStrings) { + return userAgentStrings.every(function (userAgentString) { + return navigator.userAgent.indexOf(userAgentString) !== -1; + }) + }); + } + }, + + _isIndexedDBMigrated: { + value: function (name) { + return !!localStorage.getItem(name + "." + this._is_safari_10_1_upgrade_issue_fixed); + } + }, + + _checkAndFixSafariTenOneUpgradeIssue: { + value: function (name) { + var self = this; + return this._databaseExists(name).then(function (isInitialized) { + return isInitialized && + self._isFaultySafariVersion && + !self._isIndexedDBMigrated(name) ? self._recreateDatabase(name) : Promise.resolve(null); + }); + } + }, + + _databaseExists: { + value: function (name) { + return Dexie.exists(name); + } + }, + + _recreateDatabase: { + value: function recreateDatabase (name) { + var self = this, + tempName = name + "_tmp"; + return this._copyDatabase(name, tempName).then(function() { + return Dexie.delete(name); + }).then(function(){ + return self._copyDatabase(tempName, name); + }).then(function(){ + return Dexie.delete(tempName); + }).then(function () { + localStorage.setItem(name + "." + self._is_safari_10_1_upgrade_issue_fixed, true); + return null; + }); + } + }, + + _buildIndexes: { + value: function (indexes) { + return indexes.map(function (index) { + return index.src; + }); + } + }, + + _makeSchema: { + value: function (tables) { + var self = this, + schema = {}, + primaryKey, + indexes, + table; + + for (var i = 0, n = tables.length; i < n; i += 1) { + table = tables[i]; + primaryKey = [table.schema.primKey.src]; + indexes = self._buildIndexes(table.schema.indexes); + if (indexes.length) { + primaryKey = primaryKey.concat(indexes); + } + schema[table.name] = primaryKey.join(","); + } + + return schema; + } + }, + + _copyDatabase: { + value: function copyDatabase(fromDbName, toDbName) { + var self = this, + dexie = new Dexie(fromDbName); + return dexie.open().then(function (db) { + var schema = self._makeSchema(db.tables), + dbCopy = new Dexie(toDbName); + + dbCopy.version(db.verno).stores(schema); + + return dbCopy.open().then(function () { + // dbCopy is now successfully created with same version and schema as source db. + // Now also copy the data + return Promise.all(db.tables.map(function (table) { + return table.toArray().then(function (rows) { + return dbCopy.table(table.name).bulkAdd(rows); + }) + })); + }).catch(function (error) { + console.log("Error copying database (", error, ")"); + }).finally(function(){ + db.close(); + dbCopy.close(); + }); + }) + } + }, + + + /** + * Main initialization method + * + * @method + * @argument {String} name - Defines the name of the database offline service will create/use. + * @argument {Number} version - Storage version + * @argument {Object} scheme - A schema with the following structure: + * + * { + * "Person": { + * primaryKey: "id", + * indexes: ["firstName","lastName], + * versionUpgradeLogic: function() {} + * }, + * "Product": { + * + * } + * } + * + */ + initWithName: { + value: function(name, version, schema) { + var self = this; + if (!this._db) { + this._db = this._checkAndFixSafariTenOneUpgradeIssue(name).then(function() { + return self._initWithName(name, version, schema); + }); + } + return this; + } + }, + + _initWithName: { + value: function(name, version, schema) { + var localVersion = version || 1, + db = new Dexie(name), + table, tableSchema, dbTable, dbSchema, dbIndexes, + shouldUpgradeToNewVersion = false, newDbSchema, + schemaDefinition, tableIndexes, tablePrimaryKey; + + this.name = name; + this.schema = schema; + + //db.open().then(function (db) { + newDbSchema = {}; + + //We automatically create an extra table that will track offline operations the record was last updated + schemaDefinition = "++id"; + schemaDefinition += ","; + schemaDefinition += "dataID"; + schemaDefinition += ","; + schemaDefinition += this.typePropertyName; + schemaDefinition += ","; + schemaDefinition += this.lastFetchedPropertyName; + schemaDefinition += ","; + schemaDefinition += this.lastModifiedPropertyName; + schemaDefinition += ","; + schemaDefinition += this.operationPropertyName; + schemaDefinition += ","; + schemaDefinition += this.changesPropertyName; + schemaDefinition += ","; + schemaDefinition += this.contextPropertyName; + newDbSchema[this.operationTableName] = schemaDefinition; + + if (schema) { + + for (table in schema) { + tableSchema = schema[table]; + tableIndexes = tableSchema.indexes; + tablePrimaryKey = tableSchema.primaryKey; + dbTable = db[table]; + if (dbTable) { + //That table is defined, now let's compare primaryKey and indexes + dbSchema = dbTable.schema; + if (dbSchema.primKey !== tablePrimaryKey) { + //Existing table has different primaryKey, needs new version + shouldUpgradeToNewVersion = true; + } + //test if indexes aren't the same. + dbIndexes = dbSchema.indexes; + if (dbIndexes !== tableSchema.indexes) { + //Existing table has different indexes, needs new version + shouldUpgradeToNewVersion = true; + } + + + } else { + //That table doesn't exists, which means we need to update. + shouldUpgradeToNewVersion = true; + } + if (shouldUpgradeToNewVersion) { + //We automatically add an index for lastUpdatedPropertyName ("montage-online-last-updated") + schemaDefinition = tablePrimaryKey; + for(var i=0, iIndexName;(iIndexName = tableIndexes[i]);i++) { + if(iIndexName !== tablePrimaryKey) { + schemaDefinition += ","; + schemaDefinition += iIndexName; + } + } + newDbSchema[table] = schemaDefinition; + } + } + + if (shouldUpgradeToNewVersion) { + //db.close(); + //Add upgrade here + //console.log("newDbSchema:",newDbSchema); + db.version(db.verno + 1).stores(newDbSchema); + //db.open(); + } + + OfflineService.registerOfflineService(this); + } + return db; + } + }, + + // createObjectStoreFromSample: { + // value: function (objectStoreName, primaryKey, sampleData) { + // if(!sampleData) return; + + // var sampleDataKeys = Object.keys(sampleData), + // db = this._db, + // currentSchema = {}; + + // db.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; + // db.version(db.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 happened when offline. + * We should + * [{ + * // 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. + } + }, + + _tableByName: { + get: function () { + if (!this.__tableByName) { + this.__tableByName = new Map(); + } + return this.__tableByName; + } + }, + + tableNamed: { + value: function(db, tableName) { + var table = this._tableByName.get(tableName); + if(!table) { + table = db[tableName]; + if(!table) { + var tables = db.tables, + iTable, i, length; + for(i = 0, length = tables.length; i < length && !table; i++) { + iTable = tables[i]; + if(iTable.name === tableName) { + table = iTable; + this._tableByName.set(tableName, table); + } + } + } + } + return table; + } + }, + + _operationTable: { + value: void 0 + }, + + operationTable: { + value: function (db) { + if (!this._operationTable) { + this._operationTable = this.tableNamed(db, this.operationTableName); + } + return this._operationTable; + } + }, + + fetchData: { + value: function (selector, stream) { + + var criteria = selector.criteria, + orderings = selector.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. + */ + this._db.then(function (myDB) { + myDB.open().then(function () { + var table = self.tableNamed(myDB, selector.type), + whereProperties = (criteria && criteria.parameters) ? Object.keys(criteria.parameters) : undefined; + + if(whereProperties && whereProperties.length) { + var wherePromise, + resultPromise, + whereProperty = whereProperties.shift(), + whereValue = criteria.parameters[whereProperty]; + + if(whereProperties.length > 0) { + //db.table1.where("key1").between(8,12).and(function (x) { return x.key2 >= 3 && x.key2 < 18; }).toArray(); + + wherePromise = Array.isArray(whereValue) ? table.where(whereProperty).anyOf(whereValue) : + table.where(whereProperty).equals(whereValue); + + resultPromise = wherePromise.and(function (aRecord) { + var result = true; + for(var i = 0, iKey, iValue, iKeyMatchValue, iOrValue; (iKey = whereProperties[i]); i++) { + iValue = criteria.parameters[iKey]; + iKeyMatchValue = false; + if (Array.isArray(iValue)) { + iOrValue = false; + for (var j = 0, jValue; (jValue = iValue[j]); j++) { + if(aRecord[iKey] === jValue) { + if(!iKeyMatchValue) iKeyMatchValue = true; + } + } + } else { + iKeyMatchValue = aRecord[iKey] === iValue; + } + + if(!(result = result && iKeyMatchValue)) { + break; + } + } + return result; + }); + } else { + if (Array.isArray(whereValue)) { + resultPromise = table + .where(whereProperty) + .anyOf(whereValue); + } else { + resultPromise = table + .where(whereProperty) + .equals(whereValue); + + // if(selector.type === "Hazard") { + // global.DEBUG = true; + // //debugger; + // } + + } + } + resultPromise.toArray(function(results) { + 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.addData(results); + stream.dataDone(); + + }); + + } 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; + } + }, + + /** + * 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 + + writeOfflineData: { + value: function (rawDataArray, selector) { + + var self = this, + clonedArray = [], + updateOperationArray = [], + primaryKey; + + // if (selector.type === "ProductThumbnail") { + // console.log("Writing offline data (", rawDataArray, ")"); + // } + + return this._db.then(function (db) { + var tableName = selector.type, + table = self.tableNamed(db, tableName), + lastUpdated = Date.now(), + dataID = this.dataIDPropertyName, + lastUpdatedPropertyName = this.lastFetchedPropertyName, + rawDataStream = new DataStream(), + i, countI, iRawData, iLastUpdated; + + primaryKey = table.schema.primKey.name; + rawDataStream.query = selector; + + //Make a clone of the array and create the record to track the online Last Updated date + for(i = 0, countI = rawDataArray.length; i < countI; i++) { + if((iRawData = rawDataArray[i])) { + clonedArray.push(iRawData); + + //Create the record to track the online Last Updated date + iLastUpdated = {}; + iLastUpdated[dataID] = iRawData[primaryKey]; + iLastUpdated[lastUpdatedPropertyName] = lastUpdated; + iLastUpdated[self.typePropertyName] = tableName; + updateOperationArray.push(iLastUpdated); + } + } + + return self.fetchData(selector, rawDataStream); + + }).then(function (offlineSelectedRecords) { + + var offlineObjectsToClear = [], + iRecord, iRecordPrimaryKey, rawDataMapByPrimaryKey, i, countI; + + for(i = 0, countI = offlineSelectedRecords.length; i < countI; i++) { + + iRecord = offlineSelectedRecords[i]; + iRecordPrimaryKey = iRecord[self.dataIDPropertyName]; + + if(!rawDataMapByPrimaryKey) { + rawDataMapByPrimaryKey = new Map(); + rawDataArray.forEach(function (rawData) { + rawDataMapByPrimaryKey.set(rawData[primaryKey], rawData); + }); + } + + if(!rawDataMapByPrimaryKey.has(iRecord[primaryKey])) { + offlineObjectsToClear.push(primaryKey); + } + + } + + //Now we now what to delete: offlineObjectsToClear, what to put: rawDataArray. + //We need to be able to build a transaction and pass + + // 3) Put new objects + return self.performOfflineSelectorChanges(selector, clonedArray, updateOperationArray, offlineObjectsToClear); + + }).catch(function(e) { + console.log(selector.type + ": performOfflineSelectorChanges failed", e); + console.error(e); + }); + } + }, + + readOfflineOperations: { + value: function (/* operationMapToService */) { + var self = this; + return new Promise(function (resolve, reject) { + self._db.then(function (db) { + db.open().then(function () { + self.operationTable(db).where("operation").anyOf("create", "update", "delete").toArray(function (offlineOperations) { + resolve(offlineOperations); + }).catch(function (e) { + console.log(selector.type + ": performOfflineSelectorChanges failed", e); + console.error(e); + reject(e); + }); + }) + }); + }); + } + }, + + performOfflineSelectorChanges: { + value: function (selector, rawDataArray, updateOperationArray, offlineObjectsToClear) { + var self = this, + clonedRawDataArray = rawDataArray.slice(0), // why clone twice? + clonedUpdateOperationArray = updateOperationArray.slice(0), + clonedOfflineObjectsToClear = offlineObjectsToClear.slice(0), + indexedDB; + + return this._db.then(function (myDb) { + indexedDB = myDb; + return myDb.open(); + }).then(function (db) { + var table = db[selector.type], + operationTable = self.operationTable(indexedDB); + + //Transaction: + //Objects to put: + // rawDataArray + // updateOperationArray + //Objects to delete: + // offlineObjectsToClear in table and operationTable + + return db.transaction('rw', table, operationTable, function () { + return Dexie.Promise.all( + [table.bulkPut(clonedRawDataArray), + operationTable.bulkPut(clonedUpdateOperationArray), + table.bulkDelete(clonedOfflineObjectsToClear), + operationTable.bulkDelete(clonedOfflineObjectsToClear)]); + + // }).then(function(value) { + // //console.log(selector.type + ": performOfflineSelectorChanges succesful: "+rawDataArray.length+" rawDataArray, "+clonedUpdateOperationArray.length+" updateOperationArray"); + }); + }).catch(function(e) { + console.log(selector.type + ": performOfflineSelectorChanges failed", e); + console.error(e); + }); + } + }, + + registerOfflinePrimaryKeyDependenciesForData: { + value: function(data, tableName, primaryKeyPropertyName) { + + if(data.length === 0) return; + + return OfflineService.registerOfflinePrimaryKeyDependenciesForData(data, tableName, primaryKeyPropertyName, this); + } + }, + + //TODO + deleteOfflinePrimaryKeyDependenciesForData: { + value: function(data, tableName, primaryKeyPropertyName) { + if(data.length === 0) return; + + var tableSchema = this.schema[tableName], + //if we don't have a known list of foreign keys, we'll consider all potential candidate + foreignKeys = tableSchema.foreignKeys; + + + OfflineService.deleteOfflinePrimaryKeyDependenciesForData(data, tableName, primaryKeyPropertyName, foreignKeys); + } + }, + + /** + * Save new data passed in objects of type + * + * @method + * @argument {Object} objects - objects whose data should be created. + * @argument {String} type - type of objects, likely to mean a "table" in storage + * @returns {external:Promise} - A promise fulfilled when all of the data in + * the changed object has been saved. + */ + createData: { + value: function (objects, type, context) { + var self = this; + return new Promise(function (resolve, reject) { + self._db.then(function (myDB) { + + var table = self.tableNamed(myDB, type), + operationTable = self.operationTable(myDB), + clonedObjects = [], + operations = [], + primaryKey = table.schema.primKey.name, + dataID = self.dataIDPropertyName, + lastModifiedPropertyName = self.lastModifiedPropertyName, + lastModified = Date.now(), + typePropertyName = self.typePropertyName, + changesPropertyName = self.changesPropertyName, + operationPropertyName = self.operationPropertyName, + operationCreateName = self.operationCreateName, + primaryKeys = []; + + + return myDB.open().then(function (db) { + db.transaction('rw', table, operationTable, function () { + //Assign primary keys and build operations + for(var i = 0, countI = objects.length, iRawData, iPrimaryKey, iOperation; i} + */ + 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); + }) + } + }, + + + /** + * Returns a Promise for the storage used to store objects + * described by all objectDescriptors in the service's model. + * + * @argument {Model} stream + * @returns {Promise} + */ + storage: { + value: function(model) { + return Promise.reject(); + } + }, + + /** + * 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; + + //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 = BlueprintDataMapping.withBlueprint(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 mapping = this.mappingForObject(object), + result; + + if (mapping) { + result = mapping.mapRawDataToObject(record, object, context); + } + + // TODO: Remove -- should be part of if/else block above. + if (record) { + if (result) { + var otherResult = this.mapRawDataToObject(record, object, context); + if (result instanceof Promise && otherResult instanceof Promise) { + result = Promise.all([result, otherResult]); + } else if (otherResult instanceof Promise) { + result = otherResult; + } + } 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..875b758129 --- /dev/null +++ b/data/service/snapshot-service.js @@ -0,0 +1,170 @@ +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") { + result = !!a === !!b; + if (result) { + var aKeys = Object.keys(a).sort(), aValue, + bKeys = Object.keys(b).sort(), bValue, + key; + result = aKeys.filter(function(x) { return a[x] !== null }).length === bKeys.filter(function(x) { return b[x] !== null }).length; + 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 7baa8482ef..eeb6a82c88 100644 --- a/jsdoc.json +++ b/jsdoc.json @@ -4,15 +4,16 @@ }, "opts": { "readme": "README.md", - "destination": "./doc" + "destination": "./doc", + "recurse": true }, "source": { "include": [ "montage.js", "composer/", "core/", - "core/converter/", - "ui/" + "ui/", + "data/" ], "includePattern": ".+\\.js(doc)?$", "excludePattern": "(^|\\/|\\\\)_" diff --git a/package.json b/package.json index bcd2522149..2351616ecf 100644 --- a/package.json +++ b/package.json @@ -50,11 +50,15 @@ "dependencies": { "bluebird": "~3.4.7", "collections": "~5.0.x", + "dexie": "~1.4.1", "frb": "~4.0.x", "htmlparser2": "~3.0.5", "jshint": "^2.9.5", + "moment-timezone": "^0.5.11", "mr": "^17.0.4", - "q-io": "^1.13.3" + "q-io": "^1.13.3", + "weak-map": "^1.0.5", + "xhr2": "^0.1.4" }, "devDependencies": { "concurrently": "^3.4.0", @@ -72,6 +76,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 63547f1f63..c44c2fcd5f 100644 --- a/test/all.js +++ b/test/all.js @@ -18,7 +18,7 @@ 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/claimed-pointer-spec", node: false}, // Core "spec/core/browser-spec", "spec/core/core-spec", @@ -94,7 +94,7 @@ module.exports = require("montage-testing").run(require, [ {name: "spec/ui/modal-overlay-spec", node: false}, {name: "spec/ui/overlay-spec", node: false}, {name: "spec/ui/slot-spec", node: false}, - {name: "spec/ui/substitution-spec", node: false}, + {name: "spec/ui/substitution-spec", node: false}, {name: "spec/ui/text-input-spec", node: false}, {name: "spec/ui/slider-spec", node: false}, {name: "spec/ui/text/text-spec", node: false}, @@ -110,14 +110,30 @@ 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}, - // Bluerint + // Blueprint {name: "spec/meta/converter-blueprint-spec", karma: false}, {name: "spec/meta/module-blueprint-spec", karma: false}, {name: "spec/meta/build-in-component-blueprint-spec", node: false, karma: false}, {name: "spec/meta/component-blueprint-spec", node: false}, {name: "spec/meta/controller-blueprint-spec", node: false}, {name: "spec/meta/event-blueprint-spec", node: false}, - {name: "spec/meta/blueprint-spec"} + {name: "spec/meta/blueprint-spec"}, + + //Montage Data + "spec/data/data-selector", + "spec/data/data-mapping", + "spec/data/data-object-descriptor", + "spec/data/data-property-descriptor", + "spec/data/data-provider", + "spec/data/data-selector", + "spec/data/data-service", + "spec/data/data-stream", + "spec/data/enumeration", + "spec/data/http-service", + "spec/data/object-descriptor", + "spec/data/offline-service", + "spec/data/property-descriptor", + "spec/data/raw-data-service" ]).then(function () { console.log('montage-testing', 'End'); }, function (err) { diff --git a/test/data/all.js b/test/data/all.js new file mode 100644 index 0000000000..adbf3fc96e --- /dev/null +++ b/test/data/all.js @@ -0,0 +1,22 @@ +console.log('montage-testing', 'Start'); +module.exports = require("montage-testing").run(require, [ + "spec/data/data-selector", + "spec/data/data-mapping", + "spec/data/data-object-descriptor", + "spec/data/data-property-descriptor", + "spec/data/data-provider", + "spec/data/data-selector", + "spec/data/data-service", + "spec/data/data-stream", + "spec/data/enumeration", + "spec/data/http-service", + "spec/data/object-descriptor", + "spec/data/offline-service", + "spec/data/property-descriptor", + "spec/data/raw-data-service" +]).then(function () { + console.log('montage-testing', 'End'); +}, function (err) { + console.log('montage-testing', 'Fail', err, err.stack); + throw err; +}); diff --git a/test/data/package.json b/test/data/package.json new file mode 100644 index 0000000000..cb1efecff8 --- /dev/null +++ b/test/data/package.json @@ -0,0 +1,30 @@ +{ + "_comment": [ + "# If you use npm to generate or modify this file, make sure", + "# to restore the file's 4 space indents after the generation", + "# or modification so it matches other source files in the", + "# project. npm only generates files with 2 space indents", + "# (see https://github.com/npm/npm/pull/3180#issuecomment-16336516)." + ], + "name": "run", + "version": "1.0.0-alpha.1", + "license": "BSD-3-Clause", + "author": "Montage Studio, inc. (http://montagestudio.com/)", + "repository": { + "type": "git", + "url": "montagestudio/montage-data-tests" + }, + "scripts": { + "postinstall": "npm link ./.." + }, + "mappings": { + "montage": "../node_modules/montage", + "montage-testing": "../node_modules/montage-testing", + "montage-data": "../" + }, + "dependencies": { + "montage": "~16.0.x", + "moment-timezone": "~0.5.x" + }, + "private": true +} diff --git a/test/data/run-browser.js b/test/data/run-browser.js new file mode 100644 index 0000000000..9fea88e4db --- /dev/null +++ b/test/data/run-browser.js @@ -0,0 +1,112 @@ +/* global global:true, __dirname, jasmineRequire */ + +/*jshint evil:true */ +// reassigning causes eval to not use lexical scope. +var globalEval = eval, + global = globalEval('this'); +/*jshint evil:false */ + +// Init +var jasmine = jasmineRequire.core(jasmineRequire); +var jasmineEnv = jasmine.getEnv(); + +// Export interface +var jasmineInterface = jasmineRequire.interface(jasmine, jasmineEnv); +global.jasmine = jasmine; +for (var property in jasmineInterface) { + if (jasmineInterface.hasOwnProperty(property)) { + global[property] = jasmineInterface[property]; + } +} + +// +// Init Reporter +// + +function queryString(parameter) { + var i, key, value, equalSign; + var loc = location.search.substring(1, location.search.length); + var params = loc.split('&'); + for (i=0; i + + + Montage-Data - Karma + + + + + + + + + + diff --git a/test/data/run-karma.js b/test/data/run-karma.js new file mode 100644 index 0000000000..91d0617052 --- /dev/null +++ b/test/data/run-karma.js @@ -0,0 +1,103 @@ +/* global global:true, __dirname, jasmineRequire */ + +/*jshint evil:true */ +// reassigning causes eval to not use lexical scope. +var globalEval = eval, + global = globalEval('this'); +/*jshint evil:false */ + +// Bootsrap Karma +if (global.__karma__) { + + //jasmine.DEFAULT_TIMEOUT_INTERVAL = 100000; + + global.__karma__.loaded = function() { + console.log('karma loaded'); + }; + +// Bootstrap Browser fallback +} else { + + // Init + var jasmine = jasmineRequire.core(jasmineRequire); + var jasmineEnv = jasmine.getEnv(); + + // Export interface + var jasmineInterface = jasmineRequire.interface(jasmine, jasmineEnv); + global.jasmine = jasmine; + for (var property in jasmineInterface) { + if (jasmineInterface.hasOwnProperty(property)) { + global[property] = jasmineInterface[property]; + } + } + + // Default reporter + jasmineEnv.addReporter(jasmineInterface.jsApiReporter); + + // Html reporter + jasmineRequire.html(jasmine); + var htmlReporter = new jasmine.HtmlReporter({ + env: jasmineEnv, + getContainer: function() { return document.body; }, + createElement: function() { return document.createElement.apply(document, arguments); }, + createTextNode: function() { return document.createTextNode.apply(document, arguments); }, + timer: new jasmine.Timer() + }); + htmlReporter.initialize(); + + jasmineEnv.addReporter(htmlReporter); +} + +global.queryString = function queryString(parameter) { + var i, key, value, equalSign; + var loc = location.search.substring(1, location.search.length); + var params = loc.split('&'); + for (i=0; i + + + Montage-Data - Browser + + + + + + + + + + diff --git a/test/data/run-node.js b/test/data/run-node.js new file mode 100644 index 0000000000..16c971b953 --- /dev/null +++ b/test/data/run-node.js @@ -0,0 +1,50 @@ +/*jshint node:true, browser:false */ +var jasmineRequire = require('jasmine-core/lib/jasmine-core/jasmine.js'); +var JasmineConsoleReporter = require('jasmine-console-reporter'); +global.XMLHttpRequest = require('xhr2'); + +// Init +var jasmine = jasmineRequire.core(jasmineRequire); +var jasmineEnv = jasmine.getEnv(); + +// Export interface +var jasmineInterface = jasmineRequire.interface(jasmine, jasmineEnv); +global.jasmine = jasmine; +global.jasmineRequire = jasmineRequire; +for (var property in jasmineInterface) { + if (jasmineInterface.hasOwnProperty(property)) { + global[property] = jasmineInterface[property]; + } +} + +// Default reporter +jasmineEnv.addReporter(jasmineInterface.jsApiReporter); + +// Html reporter +var consoleReporter = new JasmineConsoleReporter({ + colors: 1, + cleanStack: 1, + verbosity: 4, + listStyle: 'indent', + activity: false +}); +jasmineEnv.addReporter(consoleReporter); + +// Exit code +var exitCode = 0; +jasmineEnv.addReporter({ + specDone: function(result) { + exitCode = exitCode || result.status === 'failed'; + } +}); + +// Execute +var mrRequire = require('mr/bootstrap-node'); +var PATH = require("path"); +mrRequire.loadPackage(PATH.join(__dirname, ".")).then(function (mr) { + return mr.async("all"); +}).then(function () { + console.log('Done'); + process.exit(exitCode); +}).thenReturn(); + diff --git a/test/data/run.html b/test/data/run.html new file mode 100644 index 0000000000..bad9b57ec8 --- /dev/null +++ b/test/data/run.html @@ -0,0 +1,14 @@ + + + + Montage-Data - Browser + + + + + + + + + + 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/offline-service.js b/test/spec/data/offline-service.js new file mode 100644 index 0000000000..669dace337 --- /dev/null +++ b/test/spec/data/offline-service.js @@ -0,0 +1,7 @@ +var OfflineService = require("montage/data/service/offline-service").OfflineService; + +describe("An OfflineService", function() { + + xit("needs to be 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..e95062e1dc --- /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; + } + } + +}); From e33659a90cc5b1876f17e9ada428799da4476584 Mon Sep 17 00:00:00 2001 From: Thomas Jaede Date: Thu, 10 Aug 2017 16:30:07 -0700 Subject: [PATCH 03/31] Add XHR shim to node tests --- karma.conf.js | 5 +++++ test/run-node.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) 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/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(); From 6c432cb23c27adcbb8d795e3e8baa9f31a265bfa Mon Sep 17 00:00:00 2001 From: Harold Thetiot Date: Thu, 10 Aug 2017 17:55:51 -0700 Subject: [PATCH 04/31] jshint montage-data merge --- .../raw-property-value-to-object-converter.js | 2 +- data/model/data-identifier.js | 4 +- data/model/object-descriptor.js | 2 +- data/service/authorization-manager.js | 6 +- data/service/data-mapping.js | 2 +- data/service/data-service.js | 8 +- data/service/data-trigger.js | 2 +- data/service/expression-data-mapping.js | 7 +- data/service/http-service.js | 6 +- data/service/indexed-d-b-data-service.js | 4 +- data/service/offline-service.js | 12 +- data/service/persistent-data-service.js | 13 +- data/service/raw-data-service.js | 10 +- data/service/snapshot-service.js | 14 ++- test/data/all.js | 22 ---- test/data/package.json | 30 ----- test/data/run-browser.js | 112 ------------------ test/data/run-karma.html | 14 --- test/data/run-karma.js | 103 ---------------- test/data/run-montage.html | 14 --- test/data/run-node.js | 50 -------- test/data/run.html | 14 --- .../authorization-manager-panel.js | 2 +- 23 files changed, 49 insertions(+), 404 deletions(-) delete mode 100644 test/data/all.js delete mode 100644 test/data/package.json delete mode 100644 test/data/run-browser.js delete mode 100644 test/data/run-karma.html delete mode 100644 test/data/run-karma.js delete mode 100644 test/data/run-montage.html delete mode 100644 test/data/run-node.js delete mode 100644 test/data/run.html diff --git a/data/converter/raw-property-value-to-object-converter.js b/data/converter/raw-property-value-to-object-converter.js index 3982a33dc4..0ee2096c83 100644 --- a/data/converter/raw-property-value-to-object-converter.js +++ b/data/converter/raw-property-value-to-object-converter.js @@ -262,7 +262,7 @@ exports.RawPropertyValueToObjectConverter = Converter.specialize( /** @lends Raw if (self.serviceIdentifier) { - criteria.parameters.serviceIdentifier = self.serviceIdentifier + criteria.parameters.serviceIdentifier = self.serviceIdentifier; } query = DataQuery.withTypeAndCriteria(type, criteria); diff --git a/data/model/data-identifier.js b/data/model/data-identifier.js index 85ef55bf23..57da2b1c28 100644 --- a/data/model/data-identifier.js +++ b/data/model/data-identifier.js @@ -98,7 +98,7 @@ exports.DataIdentifier = Montage.specialize(/** @lends DataIdentifier.prototype get: function () { if(!this._url) { var _url = "montage-data://"; - _url += this.dataService.identifier + _url += this.dataService.identifier; _url += "/"; _url += this.dataService.connectionDescriptor ? this.dataService.connectionDescriptor.name : "default"; _url += "/"; @@ -110,7 +110,7 @@ exports.DataIdentifier = Montage.specialize(/** @lends DataIdentifier.prototype return this._url; }, set: function (value) { - return this._url = value; + return (this._url = value); } } diff --git a/data/model/object-descriptor.js b/data/model/object-descriptor.js index 5a647b9a11..86c02bb7c0 100644 --- a/data/model/object-descriptor.js +++ b/data/model/object-descriptor.js @@ -169,7 +169,7 @@ exports.ObjectDescriptor = Montage.specialize(/** @lends ObjectDescriptor.protot } } return this._TYPE; - } + }; } } diff --git a/data/service/authorization-manager.js b/data/service/authorization-manager.js index cf354149fd..069dad40a9 100644 --- a/data/service/authorization-manager.js +++ b/data/service/authorization-manager.js @@ -101,7 +101,7 @@ var AuthorizationManager = Montage.specialize(/** @lends AuthorizationManager.pr panel, i, n; for (i = 0, n = exportNames.length; i < n && !panel; ++i) { - panel = self._panelForConstructorAndProvider(exports[exportNames[i]], provider) + panel = self._panelForConstructorAndProvider(exports[exportNames[i]], provider); } panel.service = provider; self._panelsByModuleID.set(panelModuleID, panel); @@ -175,7 +175,7 @@ var AuthorizationManager = Montage.specialize(/** @lends AuthorizationManager.pr self._pendingServicesCount++; return this._managerPanel().then(function (authManagerPanel) { managerPanel = authManagerPanel; - return self._panelForProvider(provider) + return self._panelForProvider(provider); }).then(function (panel) { return managerPanel.authorizeWithPanel(panel); }).then(function (authorization) { @@ -209,7 +209,7 @@ var AuthorizationManager = Montage.specialize(/** @lends AuthorizationManager.pr //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); diff --git a/data/service/data-mapping.js b/data/service/data-mapping.js index 023e26ba8a..4582cdc548 100644 --- a/data/service/data-mapping.js +++ b/data/service/data-mapping.js @@ -51,7 +51,7 @@ exports.DataMapping = Montage.specialize(/** @lends DataMapping.prototype */ { var key; if (data) { for (key in data) { - object[key] = data[key] + object[key] = data[key]; } } } diff --git a/data/service/data-service.js b/data/service/data-service.js index bb465f2a8a..82f1495dfc 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -389,7 +389,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { return child._childServiceRegistrationPromise.then(function () { return self._registerChildServiceTypesAndMappings(child, types, mappings); - }) + }); } }, @@ -1536,7 +1536,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { */ objectForDataIdentifier: { value: function(dataIdentifier) { - return this._objectByDataIdentifier.get(dataIdentifier) + return this._objectByDataIdentifier.get(dataIdentifier); } }, /** @@ -1824,7 +1824,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { stream.query = self.mapSelectorToRawDataQuery(streamSelector); self.fetchRawData(stream); stream.query = streamSelector; - }) + }); } } }, @@ -1892,7 +1892,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { if(Promise.is(rawDataService)) { rawDataService.then(function(service) { self._cancelServiceDataStream(service, dataStream, reason); - }) + }); } else { this._cancelServiceDataStream(rawDataService, dataStream, reason); diff --git a/data/service/data-trigger.js b/data/service/data-trigger.js index dc494bcbe2..2c299a32e4 100644 --- a/data/service/data-trigger.js +++ b/data/service/data-trigger.js @@ -483,7 +483,7 @@ Object.defineProperties(exports.DataTrigger, /** @lends DataTrigger */ { trigger._isGlobal = propertyDescriptor.isGlobal; if(!serviceTriggers) { serviceTriggers = {}; - service._dataObjectTriggers.set(objectDescriptor,serviceTriggers) + service._dataObjectTriggers.set(objectDescriptor,serviceTriggers); } serviceTriggers[name] = trigger; return trigger; diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 2a1dce1cef..cf87e22fab 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -301,10 +301,9 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData } result = Promise.all(promises); } else { - result = Promise.resolve(null) + result = Promise.resolve(null); } - return result; } }, @@ -645,7 +644,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData if (self._isThenable(value)) { value.then(function (data) { self._assignDataToObjectProperty(object, propertyDescriptor, data); - resolve(null) + resolve(null); }); } else { object[propertyDescriptor.name] = value; @@ -723,7 +722,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData return { parsed: parsed, expression: expression - } + }; } }, diff --git a/data/service/http-service.js b/data/service/http-service.js index 2f9e447c58..b6fc29a2c5 100644 --- a/data/service/http-service.js +++ b/data/service/http-service.js @@ -123,7 +123,7 @@ var HttpService = exports.HttpService = RawDataService.specialize(/** @lends Htt if (authorization && authorization.headerValueExpression && this.authorizationHeaderName) { scope = new Scope(authorization); evaluate = compile(parse(authorization.headerValueExpression)); - headers[this.authorizationHeaderName] = evaluate(scope) + headers[this.authorizationHeaderName] = evaluate(scope); } else if (query && this.authorizationHeaderName && this.authorizationHeaderValueExpression) { scope = new Scope(query); evaluate = compile(parse(this.authorizationHeaderValueExpression)); @@ -429,9 +429,9 @@ var HttpService = exports.HttpService = RawDataService.specialize(/** @lends Htt if (last === 5 && arguments[4] instanceof DataQuery) { - parsed.query = arguments[4] + parsed.query = arguments[4]; } else if (last === 4 && arguments[3] instanceof DataQuery) { - parsed.query = arguments[3] + parsed.query = arguments[3]; } // Return the parsed arguments. return last >= 0 ? parsed : undefined; diff --git a/data/service/indexed-d-b-data-service.js b/data/service/indexed-d-b-data-service.js index 8d94ea7d00..f564c364c1 100644 --- a/data/service/indexed-d-b-data-service.js +++ b/data/service/indexed-d-b-data-service.js @@ -17,7 +17,7 @@ var PersistentDataService = require("data/service/persistent-data-service").Pers * @class * @extends RawDataService */ -exports.IndexedDBDataService = IndexedDBDataService = PersistentDataService.specialize(/** @lends PersistentDataService.prototype */ { +exports.IndexedDBDataService = PersistentDataService.specialize(/** @lends PersistentDataService.prototype */ { /*************************************************************************** * Initializing @@ -101,7 +101,7 @@ exports.IndexedDBDataService = IndexedDBDataService = PersistentDataService.spec */ provideStorageForObjectDescriptor: { value: function(objectDescriptor) { - return this.storagePromiseForNameVersion(objectDescriptor.model.name,objectDescriptor.model.version) + return this.storagePromiseForNameVersion(objectDescriptor.model.name,objectDescriptor.model.version); } }, diff --git a/data/service/offline-service.js b/data/service/offline-service.js index 5d223d5ec9..69141fd47b 100644 --- a/data/service/offline-service.js +++ b/data/service/offline-service.js @@ -52,7 +52,7 @@ exports.OfflineService = OfflineService = RawDataService.specialize(/** @lends O ["iPhone OS 10_3_0", "AppleWebKit/603.1.30"], ["iPhone OS 10_3_1", "AppleWebKit/603.1.30"], ["Mac OS X 10_12_4", "AppleWebKit/603.1.30"] - ] + ]; } }, @@ -61,7 +61,7 @@ exports.OfflineService = OfflineService = RawDataService.specialize(/** @lends O return this._faulty_safari_versions.some(function (userAgentStrings) { return userAgentStrings.every(function (userAgentString) { return navigator.userAgent.indexOf(userAgentString) !== -1; - }) + }); }); } }, @@ -152,7 +152,7 @@ exports.OfflineService = OfflineService = RawDataService.specialize(/** @lends O return Promise.all(db.tables.map(function (table) { return table.toArray().then(function (rows) { return dbCopy.table(table.name).bulkAdd(rows); - }) + }); })); }).catch(function (error) { console.log("Error copying database (", error, ")"); @@ -160,7 +160,7 @@ exports.OfflineService = OfflineService = RawDataService.specialize(/** @lends O db.close(); dbCopy.close(); }); - }) + }); } }, @@ -713,7 +713,7 @@ exports.OfflineService = OfflineService = RawDataService.specialize(/** @lends O console.error(e); reject(e); }); - }) + }); }); }); } @@ -1410,7 +1410,7 @@ exports.OfflineService = OfflineService = RawDataService.specialize(/** @lends O console.error("deleteOfflinePrimaryKeys failed",e); reject(e); }); - }) + }); } } }); diff --git a/data/service/persistent-data-service.js b/data/service/persistent-data-service.js index 36f95518eb..50cf248eea 100644 --- a/data/service/persistent-data-service.js +++ b/data/service/persistent-data-service.js @@ -7,7 +7,8 @@ var RawDataService = require("data/service/raw-data-service").RawDataService, DESCENDING = DataOrdering.DESCENDING, evaluate = require("frb/evaluate"), Map = require("collections/map"), - OfflineService; + PersistentDataService, OfflineService; + /** * TODO: Document * @@ -469,7 +470,7 @@ exports.PersistentDataService = PersistentDataService = RawDataService.specializ return storage; } return Promise.reject(null); - }) + }); } }, @@ -764,10 +765,8 @@ exports.PersistentDataService = PersistentDataService = RawDataService.specializ //Need to structure the API to return this.closeTransaction(); - - }) - - }) + }); + }); } }, @@ -1606,7 +1605,7 @@ exports.PersistentDataService = PersistentDataService = RawDataService.specializ console.error("deleteOfflinePrimaryKeys failed",e); reject(e); }); - }) + }); } } }); diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 356c627960..134e4c956f 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -549,9 +549,9 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot */ isOffline: { get: function () { - return this === this.rootService - ? this.superForGet("isOffline")() - : this.rootService.isOffline; + return this === this.rootService ? + this.superForGet("isOffline")() : + this.rootService.isOffline; } }, @@ -739,11 +739,11 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot } if(!dataIdentifier) { - var typeName = type.typeName/*DataDescriptor*/||type.name, + var typeName = type.typeName /*DataDescriptor*/ || type.name; //This should be done by ObjectDescriptor/blueprint using primaryProperties //and extract the corresponsing values from rawData //For now we know here that MileZero objects have an "id" attribute. - dataIdentifier = new DataIdentifier() + dataIdentifier = new DataIdentifier(); dataIdentifier.objectDescriptor = type; dataIdentifier.dataService = this; dataIdentifier.typeName = type.name; diff --git a/data/service/snapshot-service.js b/data/service/snapshot-service.js index 875b758129..0d4e1290b1 100644 --- a/data/service/snapshot-service.js +++ b/data/service/snapshot-service.js @@ -112,12 +112,20 @@ exports.SnapshotService = Montage.specialize(/** @lends SnapshotService# */ { var result = a === b; if (!result) { if (typeof a === "object" && typeof b === "object") { + + // TODO WTF result = !!a === !!b; + if (result) { var aKeys = Object.keys(a).sort(), aValue, bKeys = Object.keys(b).sort(), bValue, key; - result = aKeys.filter(function(x) { return a[x] !== null }).length === bKeys.filter(function(x) { return b[x] !== null }).length; + + 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]; @@ -162,9 +170,7 @@ exports.SnapshotService = Montage.specialize(/** @lends SnapshotService# */ { value: function (object) { return Object.keys(object).sort().filter(function (key) { return object[key] !== null; - }) + }); } } - - }); diff --git a/test/data/all.js b/test/data/all.js deleted file mode 100644 index adbf3fc96e..0000000000 --- a/test/data/all.js +++ /dev/null @@ -1,22 +0,0 @@ -console.log('montage-testing', 'Start'); -module.exports = require("montage-testing").run(require, [ - "spec/data/data-selector", - "spec/data/data-mapping", - "spec/data/data-object-descriptor", - "spec/data/data-property-descriptor", - "spec/data/data-provider", - "spec/data/data-selector", - "spec/data/data-service", - "spec/data/data-stream", - "spec/data/enumeration", - "spec/data/http-service", - "spec/data/object-descriptor", - "spec/data/offline-service", - "spec/data/property-descriptor", - "spec/data/raw-data-service" -]).then(function () { - console.log('montage-testing', 'End'); -}, function (err) { - console.log('montage-testing', 'Fail', err, err.stack); - throw err; -}); diff --git a/test/data/package.json b/test/data/package.json deleted file mode 100644 index cb1efecff8..0000000000 --- a/test/data/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "_comment": [ - "# If you use npm to generate or modify this file, make sure", - "# to restore the file's 4 space indents after the generation", - "# or modification so it matches other source files in the", - "# project. npm only generates files with 2 space indents", - "# (see https://github.com/npm/npm/pull/3180#issuecomment-16336516)." - ], - "name": "run", - "version": "1.0.0-alpha.1", - "license": "BSD-3-Clause", - "author": "Montage Studio, inc. (http://montagestudio.com/)", - "repository": { - "type": "git", - "url": "montagestudio/montage-data-tests" - }, - "scripts": { - "postinstall": "npm link ./.." - }, - "mappings": { - "montage": "../node_modules/montage", - "montage-testing": "../node_modules/montage-testing", - "montage-data": "../" - }, - "dependencies": { - "montage": "~16.0.x", - "moment-timezone": "~0.5.x" - }, - "private": true -} diff --git a/test/data/run-browser.js b/test/data/run-browser.js deleted file mode 100644 index 9fea88e4db..0000000000 --- a/test/data/run-browser.js +++ /dev/null @@ -1,112 +0,0 @@ -/* global global:true, __dirname, jasmineRequire */ - -/*jshint evil:true */ -// reassigning causes eval to not use lexical scope. -var globalEval = eval, - global = globalEval('this'); -/*jshint evil:false */ - -// Init -var jasmine = jasmineRequire.core(jasmineRequire); -var jasmineEnv = jasmine.getEnv(); - -// Export interface -var jasmineInterface = jasmineRequire.interface(jasmine, jasmineEnv); -global.jasmine = jasmine; -for (var property in jasmineInterface) { - if (jasmineInterface.hasOwnProperty(property)) { - global[property] = jasmineInterface[property]; - } -} - -// -// Init Reporter -// - -function queryString(parameter) { - var i, key, value, equalSign; - var loc = location.search.substring(1, location.search.length); - var params = loc.split('&'); - for (i=0; i - - - Montage-Data - Karma - - - - - - - - - - diff --git a/test/data/run-karma.js b/test/data/run-karma.js deleted file mode 100644 index 91d0617052..0000000000 --- a/test/data/run-karma.js +++ /dev/null @@ -1,103 +0,0 @@ -/* global global:true, __dirname, jasmineRequire */ - -/*jshint evil:true */ -// reassigning causes eval to not use lexical scope. -var globalEval = eval, - global = globalEval('this'); -/*jshint evil:false */ - -// Bootsrap Karma -if (global.__karma__) { - - //jasmine.DEFAULT_TIMEOUT_INTERVAL = 100000; - - global.__karma__.loaded = function() { - console.log('karma loaded'); - }; - -// Bootstrap Browser fallback -} else { - - // Init - var jasmine = jasmineRequire.core(jasmineRequire); - var jasmineEnv = jasmine.getEnv(); - - // Export interface - var jasmineInterface = jasmineRequire.interface(jasmine, jasmineEnv); - global.jasmine = jasmine; - for (var property in jasmineInterface) { - if (jasmineInterface.hasOwnProperty(property)) { - global[property] = jasmineInterface[property]; - } - } - - // Default reporter - jasmineEnv.addReporter(jasmineInterface.jsApiReporter); - - // Html reporter - jasmineRequire.html(jasmine); - var htmlReporter = new jasmine.HtmlReporter({ - env: jasmineEnv, - getContainer: function() { return document.body; }, - createElement: function() { return document.createElement.apply(document, arguments); }, - createTextNode: function() { return document.createTextNode.apply(document, arguments); }, - timer: new jasmine.Timer() - }); - htmlReporter.initialize(); - - jasmineEnv.addReporter(htmlReporter); -} - -global.queryString = function queryString(parameter) { - var i, key, value, equalSign; - var loc = location.search.substring(1, location.search.length); - var params = loc.split('&'); - for (i=0; i - - - Montage-Data - Browser - - - - - - - - - - diff --git a/test/data/run-node.js b/test/data/run-node.js deleted file mode 100644 index 16c971b953..0000000000 --- a/test/data/run-node.js +++ /dev/null @@ -1,50 +0,0 @@ -/*jshint node:true, browser:false */ -var jasmineRequire = require('jasmine-core/lib/jasmine-core/jasmine.js'); -var JasmineConsoleReporter = require('jasmine-console-reporter'); -global.XMLHttpRequest = require('xhr2'); - -// Init -var jasmine = jasmineRequire.core(jasmineRequire); -var jasmineEnv = jasmine.getEnv(); - -// Export interface -var jasmineInterface = jasmineRequire.interface(jasmine, jasmineEnv); -global.jasmine = jasmine; -global.jasmineRequire = jasmineRequire; -for (var property in jasmineInterface) { - if (jasmineInterface.hasOwnProperty(property)) { - global[property] = jasmineInterface[property]; - } -} - -// Default reporter -jasmineEnv.addReporter(jasmineInterface.jsApiReporter); - -// Html reporter -var consoleReporter = new JasmineConsoleReporter({ - colors: 1, - cleanStack: 1, - verbosity: 4, - listStyle: 'indent', - activity: false -}); -jasmineEnv.addReporter(consoleReporter); - -// Exit code -var exitCode = 0; -jasmineEnv.addReporter({ - specDone: function(result) { - exitCode = exitCode || result.status === 'failed'; - } -}); - -// Execute -var mrRequire = require('mr/bootstrap-node'); -var PATH = require("path"); -mrRequire.loadPackage(PATH.join(__dirname, ".")).then(function (mr) { - return mr.async("all"); -}).then(function () { - console.log('Done'); - process.exit(exitCode); -}).thenReturn(); - diff --git a/test/data/run.html b/test/data/run.html deleted file mode 100644 index bad9b57ec8..0000000000 --- a/test/data/run.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - Montage-Data - Browser - - - - - - - - - - diff --git a/ui/authorization-manager-panel.reel/authorization-manager-panel.js b/ui/authorization-manager-panel.reel/authorization-manager-panel.js index e95062e1dc..30ae859a0d 100644 --- a/ui/authorization-manager-panel.reel/authorization-manager-panel.js +++ b/ui/authorization-manager-panel.reel/authorization-manager-panel.js @@ -59,7 +59,7 @@ exports.AuthorizationManagerPanel = Component.specialize({ self._panels.set(authorizationPanel, { resolve: resolve, reject: reject - }) + }); }); this.authorizationPanels.push(authorizationPanel); } else { From d953c009b0308d0db007fdb10d316d5f1aa348f7 Mon Sep 17 00:00:00 2001 From: Thomas Jaede Date: Mon, 14 Aug 2017 12:52:58 -0700 Subject: [PATCH 05/31] Remove offline-service & dexie dependency from repo --- data/service/data-service.js | 8 +- data/service/offline-service.js | 1416 ----------------------------- data/service/raw-data-service.js | 9 +- package.json | 1 - test/spec/data/offline-service.js | 7 - 5 files changed, 9 insertions(+), 1432 deletions(-) delete mode 100644 data/service/offline-service.js delete mode 100644 test/spec/data/offline-service.js diff --git a/data/service/data-service.js b/data/service/data-service.js index 82f1495dfc..f7b5c4893c 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -1309,13 +1309,13 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { */ 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) : @@ -1343,7 +1343,6 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { 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); @@ -1781,6 +1780,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { // Use a child service to fetch the data. try { + service = self.childServiceForType(query.type); if (service) { stream = service.fetchData(query, stream) || stream; @@ -1834,8 +1834,8 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { var serviceModuleID = this._serviceIdentifierForQuery(query), service = serviceModuleID && this._childServicesByIdentifier.get(serviceModuleID); - if (!service) { - service = this.childServiceForType(query.type); + if (!service && this._childServicesByType.has(query.type)) { + service = this._childServicesByType.get(query.type); } return service || null; diff --git a/data/service/offline-service.js b/data/service/offline-service.js deleted file mode 100644 index 69141fd47b..0000000000 --- a/data/service/offline-service.js +++ /dev/null @@ -1,1416 +0,0 @@ -var RawDataService = require("data/service/raw-data-service").RawDataService, - DataStream = require("data/service/data-stream").DataStream, - Dexie = require("dexie"), - 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 -* -* @class -* @extends RawDataService -*/ -exports.OfflineService = OfflineService = RawDataService.specialize(/** @lends OfflineService.prototype */ { - - /*************************************************************************** - * Initializing - */ - - constructor: { - value: function OfflineService() { - RawDataService.call(this); - } - }, - - _db : { - value: void 0 - }, - - schema : { - value: void 0 - }, - - name : { - value: void 0 - }, - - /** - * @type {string} - */ - _is_safari_10_1_upgrade_issue_fixed: { - value: "is_IOS_10_3_upgrade_issue_fixed" - }, - - _faulty_safari_versions: { - get: function () { - return [ - ["iPhone OS 10_3_0", "AppleWebKit/603.1.30"], - ["iPhone OS 10_3_1", "AppleWebKit/603.1.30"], - ["Mac OS X 10_12_4", "AppleWebKit/603.1.30"] - ]; - } - }, - - _isFaultySafariVersion: { - get: function () { - return this._faulty_safari_versions.some(function (userAgentStrings) { - return userAgentStrings.every(function (userAgentString) { - return navigator.userAgent.indexOf(userAgentString) !== -1; - }); - }); - } - }, - - _isIndexedDBMigrated: { - value: function (name) { - return !!localStorage.getItem(name + "." + this._is_safari_10_1_upgrade_issue_fixed); - } - }, - - _checkAndFixSafariTenOneUpgradeIssue: { - value: function (name) { - var self = this; - return this._databaseExists(name).then(function (isInitialized) { - return isInitialized && - self._isFaultySafariVersion && - !self._isIndexedDBMigrated(name) ? self._recreateDatabase(name) : Promise.resolve(null); - }); - } - }, - - _databaseExists: { - value: function (name) { - return Dexie.exists(name); - } - }, - - _recreateDatabase: { - value: function recreateDatabase (name) { - var self = this, - tempName = name + "_tmp"; - return this._copyDatabase(name, tempName).then(function() { - return Dexie.delete(name); - }).then(function(){ - return self._copyDatabase(tempName, name); - }).then(function(){ - return Dexie.delete(tempName); - }).then(function () { - localStorage.setItem(name + "." + self._is_safari_10_1_upgrade_issue_fixed, true); - return null; - }); - } - }, - - _buildIndexes: { - value: function (indexes) { - return indexes.map(function (index) { - return index.src; - }); - } - }, - - _makeSchema: { - value: function (tables) { - var self = this, - schema = {}, - primaryKey, - indexes, - table; - - for (var i = 0, n = tables.length; i < n; i += 1) { - table = tables[i]; - primaryKey = [table.schema.primKey.src]; - indexes = self._buildIndexes(table.schema.indexes); - if (indexes.length) { - primaryKey = primaryKey.concat(indexes); - } - schema[table.name] = primaryKey.join(","); - } - - return schema; - } - }, - - _copyDatabase: { - value: function copyDatabase(fromDbName, toDbName) { - var self = this, - dexie = new Dexie(fromDbName); - return dexie.open().then(function (db) { - var schema = self._makeSchema(db.tables), - dbCopy = new Dexie(toDbName); - - dbCopy.version(db.verno).stores(schema); - - return dbCopy.open().then(function () { - // dbCopy is now successfully created with same version and schema as source db. - // Now also copy the data - return Promise.all(db.tables.map(function (table) { - return table.toArray().then(function (rows) { - return dbCopy.table(table.name).bulkAdd(rows); - }); - })); - }).catch(function (error) { - console.log("Error copying database (", error, ")"); - }).finally(function(){ - db.close(); - dbCopy.close(); - }); - }); - } - }, - - - /** - * Main initialization method - * - * @method - * @argument {String} name - Defines the name of the database offline service will create/use. - * @argument {Number} version - Storage version - * @argument {Object} scheme - A schema with the following structure: - * - * { - * "Person": { - * primaryKey: "id", - * indexes: ["firstName","lastName], - * versionUpgradeLogic: function() {} - * }, - * "Product": { - * - * } - * } - * - */ - initWithName: { - value: function(name, version, schema) { - var self = this; - if (!this._db) { - this._db = this._checkAndFixSafariTenOneUpgradeIssue(name).then(function() { - return self._initWithName(name, version, schema); - }); - } - return this; - } - }, - - _initWithName: { - value: function(name, version, schema) { - var localVersion = version || 1, - db = new Dexie(name), - table, tableSchema, dbTable, dbSchema, dbIndexes, - shouldUpgradeToNewVersion = false, newDbSchema, - schemaDefinition, tableIndexes, tablePrimaryKey; - - this.name = name; - this.schema = schema; - - //db.open().then(function (db) { - newDbSchema = {}; - - //We automatically create an extra table that will track offline operations the record was last updated - schemaDefinition = "++id"; - schemaDefinition += ","; - schemaDefinition += "dataID"; - schemaDefinition += ","; - schemaDefinition += this.typePropertyName; - schemaDefinition += ","; - schemaDefinition += this.lastFetchedPropertyName; - schemaDefinition += ","; - schemaDefinition += this.lastModifiedPropertyName; - schemaDefinition += ","; - schemaDefinition += this.operationPropertyName; - schemaDefinition += ","; - schemaDefinition += this.changesPropertyName; - schemaDefinition += ","; - schemaDefinition += this.contextPropertyName; - newDbSchema[this.operationTableName] = schemaDefinition; - - if (schema) { - - for (table in schema) { - tableSchema = schema[table]; - tableIndexes = tableSchema.indexes; - tablePrimaryKey = tableSchema.primaryKey; - dbTable = db[table]; - if (dbTable) { - //That table is defined, now let's compare primaryKey and indexes - dbSchema = dbTable.schema; - if (dbSchema.primKey !== tablePrimaryKey) { - //Existing table has different primaryKey, needs new version - shouldUpgradeToNewVersion = true; - } - //test if indexes aren't the same. - dbIndexes = dbSchema.indexes; - if (dbIndexes !== tableSchema.indexes) { - //Existing table has different indexes, needs new version - shouldUpgradeToNewVersion = true; - } - - - } else { - //That table doesn't exists, which means we need to update. - shouldUpgradeToNewVersion = true; - } - if (shouldUpgradeToNewVersion) { - //We automatically add an index for lastUpdatedPropertyName ("montage-online-last-updated") - schemaDefinition = tablePrimaryKey; - for(var i=0, iIndexName;(iIndexName = tableIndexes[i]);i++) { - if(iIndexName !== tablePrimaryKey) { - schemaDefinition += ","; - schemaDefinition += iIndexName; - } - } - newDbSchema[table] = schemaDefinition; - } - } - - if (shouldUpgradeToNewVersion) { - //db.close(); - //Add upgrade here - //console.log("newDbSchema:",newDbSchema); - db.version(db.verno + 1).stores(newDbSchema); - //db.open(); - } - - OfflineService.registerOfflineService(this); - } - return db; - } - }, - - // createObjectStoreFromSample: { - // value: function (objectStoreName, primaryKey, sampleData) { - // if(!sampleData) return; - - // var sampleDataKeys = Object.keys(sampleData), - // db = this._db, - // currentSchema = {}; - - // db.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; - // db.version(db.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 happened when offline. - * We should - * [{ - * // 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. - } - }, - - _tableByName: { - get: function () { - if (!this.__tableByName) { - this.__tableByName = new Map(); - } - return this.__tableByName; - } - }, - - tableNamed: { - value: function(db, tableName) { - var table = this._tableByName.get(tableName); - if(!table) { - table = db[tableName]; - if(!table) { - var tables = db.tables, - iTable, i, length; - for(i = 0, length = tables.length; i < length && !table; i++) { - iTable = tables[i]; - if(iTable.name === tableName) { - table = iTable; - this._tableByName.set(tableName, table); - } - } - } - } - return table; - } - }, - - _operationTable: { - value: void 0 - }, - - operationTable: { - value: function (db) { - if (!this._operationTable) { - this._operationTable = this.tableNamed(db, this.operationTableName); - } - return this._operationTable; - } - }, - - fetchData: { - value: function (selector, stream) { - - var criteria = selector.criteria, - orderings = selector.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. - */ - this._db.then(function (myDB) { - myDB.open().then(function () { - var table = self.tableNamed(myDB, selector.type), - whereProperties = (criteria && criteria.parameters) ? Object.keys(criteria.parameters) : undefined; - - if(whereProperties && whereProperties.length) { - var wherePromise, - resultPromise, - whereProperty = whereProperties.shift(), - whereValue = criteria.parameters[whereProperty]; - - if(whereProperties.length > 0) { - //db.table1.where("key1").between(8,12).and(function (x) { return x.key2 >= 3 && x.key2 < 18; }).toArray(); - - wherePromise = Array.isArray(whereValue) ? table.where(whereProperty).anyOf(whereValue) : - table.where(whereProperty).equals(whereValue); - - resultPromise = wherePromise.and(function (aRecord) { - var result = true; - for(var i = 0, iKey, iValue, iKeyMatchValue, iOrValue; (iKey = whereProperties[i]); i++) { - iValue = criteria.parameters[iKey]; - iKeyMatchValue = false; - if (Array.isArray(iValue)) { - iOrValue = false; - for (var j = 0, jValue; (jValue = iValue[j]); j++) { - if(aRecord[iKey] === jValue) { - if(!iKeyMatchValue) iKeyMatchValue = true; - } - } - } else { - iKeyMatchValue = aRecord[iKey] === iValue; - } - - if(!(result = result && iKeyMatchValue)) { - break; - } - } - return result; - }); - } else { - if (Array.isArray(whereValue)) { - resultPromise = table - .where(whereProperty) - .anyOf(whereValue); - } else { - resultPromise = table - .where(whereProperty) - .equals(whereValue); - - // if(selector.type === "Hazard") { - // global.DEBUG = true; - // //debugger; - // } - - } - } - resultPromise.toArray(function(results) { - 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.addData(results); - stream.dataDone(); - - }); - - } 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; - } - }, - - /** - * 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 - - writeOfflineData: { - value: function (rawDataArray, selector) { - - var self = this, - clonedArray = [], - updateOperationArray = [], - primaryKey; - - // if (selector.type === "ProductThumbnail") { - // console.log("Writing offline data (", rawDataArray, ")"); - // } - - return this._db.then(function (db) { - var tableName = selector.type, - table = self.tableNamed(db, tableName), - lastUpdated = Date.now(), - dataID = this.dataIDPropertyName, - lastUpdatedPropertyName = this.lastFetchedPropertyName, - rawDataStream = new DataStream(), - i, countI, iRawData, iLastUpdated; - - primaryKey = table.schema.primKey.name; - rawDataStream.query = selector; - - //Make a clone of the array and create the record to track the online Last Updated date - for(i = 0, countI = rawDataArray.length; i < countI; i++) { - if((iRawData = rawDataArray[i])) { - clonedArray.push(iRawData); - - //Create the record to track the online Last Updated date - iLastUpdated = {}; - iLastUpdated[dataID] = iRawData[primaryKey]; - iLastUpdated[lastUpdatedPropertyName] = lastUpdated; - iLastUpdated[self.typePropertyName] = tableName; - updateOperationArray.push(iLastUpdated); - } - } - - return self.fetchData(selector, rawDataStream); - - }).then(function (offlineSelectedRecords) { - - var offlineObjectsToClear = [], - iRecord, iRecordPrimaryKey, rawDataMapByPrimaryKey, i, countI; - - for(i = 0, countI = offlineSelectedRecords.length; i < countI; i++) { - - iRecord = offlineSelectedRecords[i]; - iRecordPrimaryKey = iRecord[self.dataIDPropertyName]; - - if(!rawDataMapByPrimaryKey) { - rawDataMapByPrimaryKey = new Map(); - rawDataArray.forEach(function (rawData) { - rawDataMapByPrimaryKey.set(rawData[primaryKey], rawData); - }); - } - - if(!rawDataMapByPrimaryKey.has(iRecord[primaryKey])) { - offlineObjectsToClear.push(primaryKey); - } - - } - - //Now we now what to delete: offlineObjectsToClear, what to put: rawDataArray. - //We need to be able to build a transaction and pass - - // 3) Put new objects - return self.performOfflineSelectorChanges(selector, clonedArray, updateOperationArray, offlineObjectsToClear); - - }).catch(function(e) { - console.log(selector.type + ": performOfflineSelectorChanges failed", e); - console.error(e); - }); - } - }, - - readOfflineOperations: { - value: function (/* operationMapToService */) { - var self = this; - return new Promise(function (resolve, reject) { - self._db.then(function (db) { - db.open().then(function () { - self.operationTable(db).where("operation").anyOf("create", "update", "delete").toArray(function (offlineOperations) { - resolve(offlineOperations); - }).catch(function (e) { - console.log(selector.type + ": performOfflineSelectorChanges failed", e); - console.error(e); - reject(e); - }); - }); - }); - }); - } - }, - - performOfflineSelectorChanges: { - value: function (selector, rawDataArray, updateOperationArray, offlineObjectsToClear) { - var self = this, - clonedRawDataArray = rawDataArray.slice(0), // why clone twice? - clonedUpdateOperationArray = updateOperationArray.slice(0), - clonedOfflineObjectsToClear = offlineObjectsToClear.slice(0), - indexedDB; - - return this._db.then(function (myDb) { - indexedDB = myDb; - return myDb.open(); - }).then(function (db) { - var table = db[selector.type], - operationTable = self.operationTable(indexedDB); - - //Transaction: - //Objects to put: - // rawDataArray - // updateOperationArray - //Objects to delete: - // offlineObjectsToClear in table and operationTable - - return db.transaction('rw', table, operationTable, function () { - return Dexie.Promise.all( - [table.bulkPut(clonedRawDataArray), - operationTable.bulkPut(clonedUpdateOperationArray), - table.bulkDelete(clonedOfflineObjectsToClear), - operationTable.bulkDelete(clonedOfflineObjectsToClear)]); - - // }).then(function(value) { - // //console.log(selector.type + ": performOfflineSelectorChanges succesful: "+rawDataArray.length+" rawDataArray, "+clonedUpdateOperationArray.length+" updateOperationArray"); - }); - }).catch(function(e) { - console.log(selector.type + ": performOfflineSelectorChanges failed", e); - console.error(e); - }); - } - }, - - registerOfflinePrimaryKeyDependenciesForData: { - value: function(data, tableName, primaryKeyPropertyName) { - - if(data.length === 0) return; - - return OfflineService.registerOfflinePrimaryKeyDependenciesForData(data, tableName, primaryKeyPropertyName, this); - } - }, - - //TODO - deleteOfflinePrimaryKeyDependenciesForData: { - value: function(data, tableName, primaryKeyPropertyName) { - if(data.length === 0) return; - - var tableSchema = this.schema[tableName], - //if we don't have a known list of foreign keys, we'll consider all potential candidate - foreignKeys = tableSchema.foreignKeys; - - - OfflineService.deleteOfflinePrimaryKeyDependenciesForData(data, tableName, primaryKeyPropertyName, foreignKeys); - } - }, - - /** - * Save new data passed in objects of type - * - * @method - * @argument {Object} objects - objects whose data should be created. - * @argument {String} type - type of objects, likely to mean a "table" in storage - * @returns {external:Promise} - A promise fulfilled when all of the data in - * the changed object has been saved. - */ - createData: { - value: function (objects, type, context) { - var self = this; - return new Promise(function (resolve, reject) { - self._db.then(function (myDB) { - - var table = self.tableNamed(myDB, type), - operationTable = self.operationTable(myDB), - clonedObjects = [], - operations = [], - primaryKey = table.schema.primKey.name, - dataID = self.dataIDPropertyName, - lastModifiedPropertyName = self.lastModifiedPropertyName, - lastModified = Date.now(), - typePropertyName = self.typePropertyName, - changesPropertyName = self.changesPropertyName, - operationPropertyName = self.operationPropertyName, - operationCreateName = self.operationCreateName, - primaryKeys = []; - - - return myDB.open().then(function (db) { - db.transaction('rw', table, operationTable, function () { - //Assign primary keys and build operations - for(var i = 0, countI = objects.length, iRawData, iPrimaryKey, iOperation; i Date: Tue, 15 Aug 2017 08:59:56 -0700 Subject: [PATCH 06/31] Remove unused requirements --- data/service/raw-data-service.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index f68e87c01b..195af5b61f 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -1,13 +1,9 @@ var DataService = require("data/service/data-service").DataService, BlueprintDataMapping = require("data/service/blueprint-data-mapping").BlueprintDataMapping, - DataObjectDescriptor = require("data/model/data-object-descriptor").DataObjectDescriptor, DataIdentifier = require("data/model/data-identifier").DataIdentifier, - DataQuery = require("data/model/data-query").DataQuery, - DataStream = require("data/service/data-stream").DataStream, Deserializer = require("core/serialization/deserializer/montage-deserializer").MontageDeserializer, Map = require("collections/map"), Montage = require("montage").Montage, - ObjectDescriptor = require("core/meta/object-descriptor").ObjectDescriptor, WeakMap = require("collections/weak-map"), deprecate = require("core/deprecate"); @@ -550,8 +546,8 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot */ isOffline: { get: function () { - return this === this.rootService ? - this.superForGet("isOffline")() : + return this === this.rootService ? + this.superForGet("isOffline")() : this.rootService.isOffline; } }, From 10fcd6fecf8ba232af1a64a7a1a87f5158bf8c10 Mon Sep 17 00:00:00 2001 From: Thomas Jaede Date: Wed, 16 Aug 2017 11:38:03 -0700 Subject: [PATCH 07/31] Fix child-service identification bug --- data/service/data-service.js | 2 ++ data/service/raw-data-service.js | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index f7b5c4893c..df6179e48f 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -1834,8 +1834,10 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { 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; diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 195af5b61f..49cb18c537 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -403,7 +403,9 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot // if (childService && childService.identifier.indexOf("offline-service") === -1) { // childService._fetchRawData(stream); // } else - if (query.authorization) { + if (childService) { + childService.fetchRawData(stream); + } else if (query.authorization) { stream.query = self.mapSelectorToRawDataQuery(query); self.fetchRawData(stream); stream.query = query; From a1e8afa7781f2ad9337f3450a6133072988f6704 Mon Sep 17 00:00:00 2001 From: Thomas Jaede Date: Wed, 16 Aug 2017 20:48:59 -0700 Subject: [PATCH 08/31] Add null check when assigning inverse properties --- data/service/expression-data-mapping.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index cf87e22fab..6d837b2126 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -706,8 +706,8 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData var self = this; return rule.converter.convert(rule.expression(scope)).then(function (data) { self._assignDataToObjectProperty(object, propertyDescriptor, data); - return rule.inversePropertyName ? self._assignObjectToDataInverseProperty(object, propertyDescriptor, data, rule.inversePropertyName) : - null; + return rule.inversePropertyName && data && data.length ? self._assignObjectToDataInverseProperty(object, propertyDescriptor, data, rule.inversePropertyName) : + null; }); } From f7bfba96e7ddb4908671c06b3625935fc121a466 Mon Sep 17 00:00:00 2001 From: Thomas Jaede Date: Thu, 17 Aug 2017 13:29:10 -0700 Subject: [PATCH 09/31] Refactor to promisify owner & service in RawPropertyValueToObjectConverter --- .../raw-property-value-to-object-converter.js | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/data/converter/raw-property-value-to-object-converter.js b/data/converter/raw-property-value-to-object-converter.js index 0ee2096c83..5f7eea4d0d 100644 --- a/data/converter/raw-property-value-to-object-converter.js +++ b/data/converter/raw-property-value-to-object-converter.js @@ -201,6 +201,15 @@ exports.RawPropertyValueToObjectConverter = Converter.specialize( /** @lends Raw } }, + 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 }, @@ -222,12 +231,12 @@ exports.RawPropertyValueToObjectConverter = Converter.specialize( /** @lends Raw */ service: { get: function () { - return this._service ? this._service : - this.owner && this.owner.service ? this.owner.service : + return this._service ? this._service : + this.owner ? this.owner.then(function (object) { return object.service; }) : undefined; }, set: function (value) { - this._service = value; + this._service = !value || value.then ? value : Promise.resolve(value); } }, @@ -259,16 +268,15 @@ exports.RawPropertyValueToObjectConverter = Converter.specialize( /** @lends Raw return descriptorPromise.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.rootService.fetchData(query) : - null; + return self.service ? self.service.then(function (service) { + return service.rootService.fetchData(query) + }) : null; }); } }, From d06e430e293f8347567525a7c8bc433441fa6f59 Mon Sep 17 00:00:00 2001 From: Thomas Jaede Date: Mon, 21 Aug 2017 15:28:38 -0700 Subject: [PATCH 10/31] Interim --- core/serialization/deserializer/montage-interpreter.js | 4 +++- core/serialization/deserializer/self-deserializer.js | 2 ++ data/service/http-service.js | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/core/serialization/deserializer/montage-interpreter.js b/core/serialization/deserializer/montage-interpreter.js index de214a51e5..788be09165 100644 --- a/core/serialization/deserializer/montage-interpreter.js +++ b/core/serialization/deserializer/montage-interpreter.js @@ -126,6 +126,8 @@ var MontageContext = Montage.specialize({ objects = this._objects, object; + + if (label in objects) { return objects[label]; } else if (label in serialization) { @@ -253,7 +255,7 @@ var MontageContext = Montage.specialize({ (ONE_WAY in value || TWO_WAY in value || ONE_ASSIGNMENT in value)) { bindings[key] = value; delete values[key]; - } + } } } 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/service/http-service.js b/data/service/http-service.js index b6fc29a2c5..a6a0bfb675 100644 --- a/data/service/http-service.js +++ b/data/service/http-service.js @@ -310,7 +310,7 @@ var HttpService = exports.HttpService = RawDataService.specialize(/** @lends Htt request.onreadystatechange = function () { if (request.readyState === 4) { resolve(request); - console.log("Completed request for (", parsed.url, ") in (", ((new Date().getTime() - startTime)), ") ms"); + // console.log("Completed request for (", parsed.url, ") in (", ((new Date().getTime() - startTime)), ") ms"); } }; request.onerror = function () { From 6be59132bdee6db1ba7c4e6eb5c1ca105b9b53c0 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 22 Aug 2017 18:55:33 -1000 Subject: [PATCH 11/31] Fixes bug where converter's owner is not resolved The bug would present itself when the converter was used by a pipeline converter and a foreign descriptor was not supplied. --- .../raw-property-value-to-object-converter.js | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/data/converter/raw-property-value-to-object-converter.js b/data/converter/raw-property-value-to-object-converter.js index 5f7eea4d0d..ee59ff6242 100644 --- a/data/converter/raw-property-value-to-object-converter.js +++ b/data/converter/raw-property-value-to-object-converter.js @@ -188,19 +188,31 @@ exports.RawPropertyValueToObjectConverter = Converter.specialize( /** @lends Raw /** * The descriptor of the source object. It will be used only if it is provided and * .foreignDescriptor is not provided. - * @type {?ObjectDescriptor} + * @type {?ObjectDescriptorReference} **/ objectDescriptor: { get: function () { - return this._objectDescriptor ? this._objectDescriptor : - this.owner && this.owner.objectDescriptor ? this.owner.objectDescriptor : - undefined; + 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; + }); + } + }, + owner: { get: function () { return this._owner ? this._owner.then ? this._owner : Promise.resolve(this._owner) : undefined; @@ -224,8 +236,6 @@ exports.RawPropertyValueToObjectConverter = Converter.specialize( /** @lends Raw } }, - - /** * The service to use to make requests. */ @@ -262,7 +272,7 @@ exports.RawPropertyValueToObjectConverter = Converter.specialize( /** @lends Raw value: function (v) { var self = this, criteria = new Criteria().initWithSyntax(self.convertSyntax, v), - descriptorPromise = this.foreignDescriptor || Promise.resolve(this.objectDescriptor), + descriptorPromise = this.foreignDescriptor || this.objectDescriptor, query; return descriptorPromise.then(function (typeToFetch) { @@ -275,8 +285,8 @@ exports.RawPropertyValueToObjectConverter = Converter.specialize( /** @lends Raw query = DataQuery.withTypeAndCriteria(type, criteria); return self.service ? self.service.then(function (service) { - return service.rootService.fetchData(query) - }) : null; + return service.rootService.fetchData(query) + }) : null; }); } }, From 7e331a9e28a6ad41952529ea0682d0bc3885a8cf Mon Sep 17 00:00:00 2001 From: Thomas Jaede Date: Mon, 28 Aug 2017 14:13:34 -0700 Subject: [PATCH 12/31] Adjust authorization API --- data/service/authorization-manager.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/data/service/authorization-manager.js b/data/service/authorization-manager.js index 069dad40a9..e58646781b 100644 --- a/data/service/authorization-manager.js +++ b/data/service/authorization-manager.js @@ -160,7 +160,7 @@ var AuthorizationManager = Montage.specialize(/** @lends AuthorizationManager.pr provider; return this._providerWithModuleID(moduleID, require).then(function (instance) { provider = instance; - return provider.authorization; + return provider.authorize(); }).then(function (authorization) { return authorization || self._authorizeProviderWithManagerPanel(provider); @@ -261,10 +261,11 @@ var AuthorizationManager = Montage.specialize(/** @lends AuthorizationManager.pr authorizeService: { - value: function (dataService) { + value: function (dataService, didFailAuthorization) { var self = this, authorizationPromises = []; + if (dataService.authorizationPolicy === DataService.AuthorizationPolicy.NONE) { return Promise.resolve(null); } else { @@ -274,8 +275,8 @@ var AuthorizationManager = Montage.specialize(/** @lends AuthorizationManager.pr if (authorizationPromises.length) { return Promise.all(authorizationPromises); - // } else if (dataService.authorizationPolicy === DataService.AuthorizationPolicy.ON_DEMAND && !didFailAuthorization) { - // return Promise.resolve(null); + } else if (dataService.authorizationPolicy === DataService.AuthorizationPolicy.ON_DEMAND && !didFailAuthorization) { + return Promise.resolve(null); } else { authorizationPromises = this._authorizationsForDataService(dataService, true); @@ -300,7 +301,6 @@ var AuthorizationManager = Montage.specialize(/** @lends AuthorizationManager.pr 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); From e879378c256174c8329354f0b48ae6899273ada5 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 3 Sep 2017 10:08:53 -1000 Subject: [PATCH 13/31] Fixes createdDataObjects access from root service DataService#createdDataObjects is a property not a function. --- data/service/data-service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index df6179e48f..7df61a89e0 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -1663,7 +1663,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { return this._createdDataObjects; } else { - return this.rootService.createdDataObjects(); + return this.rootService.createdDataObjects; } } }, From 4d409d14a600e6861d1fb18e0a18849b7d54ba3a Mon Sep 17 00:00:00 2001 From: Thomas Jaede Date: Thu, 7 Sep 2017 18:24:54 -0700 Subject: [PATCH 14/31] Remove circular dependency between data-service & authorization-manager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move AuthorizationPolicy out of DataService and into it’s own module. This removes the need for AuthorizationManager to depend on DataService and returns the dependency tree back to it’s pre-Auth API state. --- data/service/authorization-manager.js | 8 +++--- data/service/authorization-policy.js | 34 +++++++++++++++++++++++ data/service/data-service.js | 40 ++------------------------- 3 files changed, 41 insertions(+), 41 deletions(-) create mode 100644 data/service/authorization-policy.js diff --git a/data/service/authorization-manager.js b/data/service/authorization-manager.js index e58646781b..f78af65147 100644 --- a/data/service/authorization-manager.js +++ b/data/service/authorization-manager.js @@ -2,9 +2,10 @@ var Montage = require("core/core").Montage, Promise = require("core/promise").Promise, Map = require("collections/map"), application = require("core/application").application, - DataService = require("data/service/data-service").DataService, + 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 @@ -20,7 +21,6 @@ var AuthorizationManager = Montage.specialize(/** @lends AuthorizationManager.pr this._providersByModuleID = new Map(); this._panelsByModuleID = new Map(); this._authorizationsByProviderModuleID = new Map(); - DataService.authorizationManager = this; this.defineBinding("hasPendingServices", {"<-": "_pendingServicesCount != 0"}); return this; } @@ -266,7 +266,7 @@ var AuthorizationManager = Montage.specialize(/** @lends AuthorizationManager.pr authorizationPromises = []; - if (dataService.authorizationPolicy === DataService.AuthorizationPolicy.NONE) { + if (dataService.authorizationPolicy === AuthorizationPolicy.NONE) { return Promise.resolve(null); } else { @@ -275,7 +275,7 @@ var AuthorizationManager = Montage.specialize(/** @lends AuthorizationManager.pr if (authorizationPromises.length) { return Promise.all(authorizationPromises); - } else if (dataService.authorizationPolicy === DataService.AuthorizationPolicy.ON_DEMAND && !didFailAuthorization) { + } else if (dataService.authorizationPolicy === AuthorizationPolicy.ON_DEMAND && !didFailAuthorization) { return Promise.resolve(null); } else { 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/data-service.js b/data/service/data-service.js index 7df61a89e0..6cfa6b9849 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -1,4 +1,6 @@ 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, @@ -10,42 +12,6 @@ var Montage = require("core/core").Montage, WeakMap = require("collections/weak-map"); - - -/** - * 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 = 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"); - var AuthorizationPolicyType = new Montage(); AuthorizationPolicyType.NoAuthorizationPolicy = AuthorizationPolicy.NONE; AuthorizationPolicyType.UpfrontAuthorizationPolicy = AuthorizationPolicy.UP_FRONT; @@ -2552,7 +2518,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { }, authorizationManager: { - value: undefined + value: AuthorizationManager } }); From 60026af890cc856af0d7fc77c795f2df2b6e2f00 Mon Sep 17 00:00:00 2001 From: Thomas Jaede Date: Fri, 8 Sep 2017 09:28:08 -0700 Subject: [PATCH 15/31] Update data & service modules to comply with async API Streamline logic in expression-data-mapping, raw-data-service, and raw-property-to-value-converter to remove the need to check whether a given property will return a promise or not. All properties that return object descriptors now return promises. Update DataMapping API signature to return promises for mapRawDataToObject & mapObjectToRawData. --- core/meta/property-descriptor.js | 2 +- .../raw-property-value-to-object-converter.js | 28 ++++++++-- data/service/data-mapping.js | 20 +++++++ data/service/expression-data-mapping.js | 54 ++++++++++++------- data/service/raw-data-service.js | 27 ++++------ 5 files changed, 89 insertions(+), 42 deletions(-) diff --git a/core/meta/property-descriptor.js b/core/meta/property-descriptor.js index 24b61a101c..b318cb3b6e 100644 --- a/core/meta/property-descriptor.js +++ b/core/meta/property-descriptor.js @@ -323,7 +323,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# get: function () { // TODO: Needed for backwards compatibility with ObjectDescriptorReference. // Remove eventually, this can become completely sync - if (typeof this._valueDescriptorReference.promise === "function") { + if (this._valueDescriptorReference && typeof this._valueDescriptorReference.promise === "function") { deprecate.deprecationWarningOnce("valueDescriptor reference via ObjectDescriptorReference", "direct reference via object syntax"); return this._valueDescriptorReference.promise(this.require); } else { diff --git a/data/converter/raw-property-value-to-object-converter.js b/data/converter/raw-property-value-to-object-converter.js index ee59ff6242..16d3d0c876 100644 --- a/data/converter/raw-property-value-to-object-converter.js +++ b/data/converter/raw-property-value-to-object-converter.js @@ -4,8 +4,7 @@ var Converter = require("core/converter/converter").Converter, ObjectDescriptorReference = require("core/meta/object-descriptor-reference").ObjectDescriptorReference, Promise = require("core/promise").Promise, Scope = require("frb/scope"), - parse = require("frb/parse"), - compile = require("frb/compile-evaluator"); + parse = require("frb/parse"); /** * @class RawPropertyValueToObjectConverter @@ -213,6 +212,26 @@ exports.RawPropertyValueToObjectConverter = Converter.specialize( /** @lends Raw } }, + + /** + * 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.then(function (descriptor) { + return descriptor || self.objectDescriptor; + }) + } + return this.__descriptorToFetch; + } + }, + + owner: { get: function () { return this._owner ? this._owner.then ? this._owner : Promise.resolve(this._owner) : undefined; @@ -272,10 +291,9 @@ exports.RawPropertyValueToObjectConverter = Converter.specialize( /** @lends Raw value: function (v) { var self = this, criteria = new Criteria().initWithSyntax(self.convertSyntax, v), - descriptorPromise = this.foreignDescriptor || this.objectDescriptor, query; - return descriptorPromise.then(function (typeToFetch) { + return this._descriptorToFetch.then(function (typeToFetch) { var type = [typeToFetch.module.id, typeToFetch.name].join("/"); if (self.serviceIdentifier) { @@ -291,6 +309,8 @@ exports.RawPropertyValueToObjectConverter = Converter.specialize( /** @lends Raw } }, + + /** * Reverts the relationship back to raw data. * @function diff --git a/data/service/data-mapping.js b/data/service/data-mapping.js index 4582cdc548..3c16440e42 100644 --- a/data/service/data-mapping.js +++ b/data/service/data-mapping.js @@ -54,6 +54,7 @@ exports.DataMapping = Montage.specialize(/** @lends DataMapping.prototype */ { object[key] = data[key]; } } + return this.nullPromise; } }, @@ -64,6 +65,16 @@ exports.DataMapping = Montage.specialize(/** @lends DataMapping.prototype */ { value: function (object, data) { // TO DO: Provide a default mapping based on object.TYPE. // For now, subclasses must override this. + return this.nullPromise; + } + }, + + nullPromise: { + get: function () { + if (!this._nullPromise) { + this._nullPromise = Promise.resolve(null); + } + return this._nullPromise; } }, @@ -91,4 +102,13 @@ exports.DataMapping = Montage.specialize(/** @lends DataMapping.prototype */ { } } +},{ + + withObjectDescriptor: { + value: function (objectDescriptor) { + var mapping = new this(); + mapping._descriptor = objectDescriptor; + return mapping; + } + } }); diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 6d837b2126..6e22f719c0 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -419,27 +419,42 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData */ mapObjectToRawDataProperty: { value: function(object, data, property) { - var rules = this._compiledRawDataMappingRules, + var self = this, + rules = this._compiledRawDataMappingRules, scope = new Scope(object), rule = rules[property], propertyDescriptor = rule && rule.propertyDescriptor, promise; - - if (propertyDescriptor && propertyDescriptor.valueDescriptor && rule.converter) { - rule.converter.expression = rule.converter.expression || rule.expression; - rule.converter.foreignDescriptor = rule.converter.foreignDescriptor || propertyDescriptor.valueDescriptor; - // rule.converter.service = rule.converter.service || this.service.rootService; - promise = this._convertRelationshipToRawData(object, propertyDescriptor, rule, scope); + 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 @@ -548,7 +563,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData _convertRelationshipToRawData: { value: function (object, propertyDescriptor, rule, scope) { if (!rule.converter.revert) { - debugger; + console.log("Converter does not have a revert function for property (" + propertyDescriptor.name + ")"); } return rule.converter.revert(rule.expression(scope)); } @@ -583,32 +598,32 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData var rules = this._compiledObjectMappingRules, rule = rules.hasOwnProperty(propertyName) && rules[propertyName], propertyDescriptor = rule && this.objectDescriptor.propertyDescriptorForName(propertyName), - isRelationship = !!(propertyDescriptor && propertyDescriptor.valueDescriptor), scope = this._scope, self = this, result; scope.value = data; if (!propertyDescriptor || propertyDescriptor.definition) { - result = Promise.resolve(); + result = Promise.resolve(null); } else { - self._prepareConverterForRule(rule.converter, rule, propertyDescriptor); - result = isRelationship ? self._resolveRelationship(object, propertyDescriptor, rule, scope) : - self._resolvePrimitive(object, propertyDescriptor, rule, scope); + 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; } }, - _prepareConverterForRule: { - value: function (converter, rule, propertyDescriptor) { + _prepareRawDataToObjectRule: { + value: function (rule, propertyDescriptor) { + var converter = rule.converter; if (converter) { converter.expression = converter.expression || rule.expression; - - if (!converter.foreignDescriptor && propertyDescriptor.valueDescriptor) { - converter.foreignDescriptor = propertyDescriptor.valueDescriptor; - } + converter.foreignDescriptor = converter.foreignDescriptor || propertyDescriptor.valueDescriptor; converter.objectDescriptor = this.objectDescriptor; converter.serviceIdentifier = rule.serviceIdentifier; } @@ -640,6 +655,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData var value = this._parse(rule, scope), self = this; + return new Promise(function (resolve, reject) { if (self._isThenable(value)) { value.then(function (data) { diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 49cb18c537..149038c9d5 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -1,5 +1,5 @@ var DataService = require("data/service/data-service").DataService, - BlueprintDataMapping = require("data/service/blueprint-data-mapping").BlueprintDataMapping, + DataMapping = require("data/service/data-mapping").DataMapping, DataIdentifier = require("data/model/data-identifier").DataIdentifier, Deserializer = require("core/serialization/deserializer/montage-deserializer").MontageDeserializer, Map = require("collections/map"), @@ -942,7 +942,7 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot if (!mapping && objectDescriptor) { mapping = this._objectDescriptorMappings.get(objectDescriptor); if (!mapping) { - mapping = BlueprintDataMapping.withBlueprint(objectDescriptor); + mapping = DataMapping.withObjectDescriptor(objectDescriptor); this._objectDescriptorMappings.set(objectDescriptor, mapping); } } @@ -1007,25 +1007,16 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot */ _mapRawDataToObject: { value: function (record, object, context) { - var mapping = this.mappingForObject(object), + var self = this, + mapping = this.mappingForObject(object), result; if (mapping) { - result = mapping.mapRawDataToObject(record, object, context); - } - - // TODO: Remove -- should be part of if/else block above. - if (record) { - if (result) { - var otherResult = this.mapRawDataToObject(record, object, context); - if (result instanceof Promise && otherResult instanceof Promise) { - result = Promise.all([result, otherResult]); - } else if (otherResult instanceof Promise) { - result = otherResult; - } - } else { - result = this.mapRawDataToObject(record, object, context); - } + result = mapping.mapRawDataToObject(record, object, context).then(function () { + return self.mapRawDataToObject(record, object, context); + }); + } else { + result = this.mapRawDataToObject(record, object, context); } return result; From e935a13b4477a5de90919276b546cd91711d7484 Mon Sep 17 00:00:00 2001 From: Thomas Jaede Date: Mon, 11 Sep 2017 15:01:19 -0700 Subject: [PATCH 16/31] Add null check on RawPropertyValueToObjectConverter.foreignDescriptor --- data/converter/raw-property-value-to-object-converter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/converter/raw-property-value-to-object-converter.js b/data/converter/raw-property-value-to-object-converter.js index 16d3d0c876..3439bb9906 100644 --- a/data/converter/raw-property-value-to-object-converter.js +++ b/data/converter/raw-property-value-to-object-converter.js @@ -223,9 +223,9 @@ exports.RawPropertyValueToObjectConverter = Converter.specialize( /** @lends Raw get: function () { if (!this.__descriptorToFetch) { var self = this; - this.__descriptorToFetch = this.foreignDescriptor.then(function (descriptor) { + this.__descriptorToFetch = this.foreignDescriptor ? this.foreignDescriptor.then(function (descriptor) { return descriptor || self.objectDescriptor; - }) + }) : Promise.resolve(this.objectDescriptor); } return this.__descriptorToFetch; } From 34f7f9006723d2de3ffda76a596bb3b9fffca22d Mon Sep 17 00:00:00 2001 From: Thomas Jaede Date: Thu, 14 Sep 2017 16:17:37 -0600 Subject: [PATCH 17/31] Adjust raw-data-service.addOneRawData API --- data/service/data-mapping.js | 2 -- data/service/raw-data-service.js | 61 ++++++++++++++++---------------- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/data/service/data-mapping.js b/data/service/data-mapping.js index 3c16440e42..2f07d57eae 100644 --- a/data/service/data-mapping.js +++ b/data/service/data-mapping.js @@ -54,7 +54,6 @@ exports.DataMapping = Montage.specialize(/** @lends DataMapping.prototype */ { object[key] = data[key]; } } - return this.nullPromise; } }, @@ -65,7 +64,6 @@ exports.DataMapping = Montage.specialize(/** @lends DataMapping.prototype */ { value: function (object, data) { // TO DO: Provide a default mapping based on object.TYPE. // For now, subclasses must override this. - return this.nullPromise; } }, diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 149038c9d5..ad557ff5b2 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -625,9 +625,9 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot */ addRawData: { value: function (stream, records, context) { - var offline, object, i, n, + var offline, i, n, streamSelectorType = stream.query.type, - iRecord, iDataIdentifier, result; + iRecord; // Record fetched raw data for offline use if appropriate. offline = records && !this.isOffline && this._streamRawData.get(stream); if (offline) { @@ -639,42 +639,35 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot // will be done in place to avoid creating any unnecessary array. for (i = 0, n = records && records.length; i < n; i += 1) { iRecord = records[i]; - object = this.addOneRawData(stream, iRecord, context, streamSelectorType); - // object = this.objectForTypeRawData(streamSelectorType,iRecord,context); - // // iDataIdentifier = this.dataIdentifierForTypeRawData(streamSelectorType,iRecord); - // // //Record snapshot before we may create an object - // // this.recordSnapshot(iDataIdentifier,iRecord); - // // //iDataIdentifier argument should be all we need later on - // // object = this.getDataObject(streamSelectorType, iRecord, context, iDataIdentifier); - // result = this.mapRawDataToObject(iRecord, object, context); - // if (result && result instanceof Promise) { - // this._addMapDataPromiseForStream(result, stream); - // } - if(object) { - records[i] = object; - this.callDelegateMethod("rawDataServiceDidAddOneRawData", this,stream,iRecord,object); - } + this.addOneRawData(stream, iRecord, context, streamSelectorType).then(function (mappedObject) { + stream.addData([mappedObject]); + }); } - // Add the converted data to the stream. - stream.addData(records); } }, + addOneRawData: { - value: function(stream, rawData, context, _type) { - var object = this.objectForTypeRawData((_type || stream.query.type), rawData, context), + value: function (stream, rawData, context) { + var object = this.objectForTypeRawData(stream.query.type, rawData, context), result; - // iDataIdentifier = this.dataIdentifierForTypeRawData(streamSelectorType,iRecord); - // //Record snapshot before we may create an object - // this.recordSnapshot(iDataIdentifier,iRecord); - // //iDataIdentifier argument should be all we need later on - // object = this.getDataObject(streamSelectorType, iRecord, context, iDataIdentifier); + result = this._mapRawDataToObject(rawData, object, context); + if (result && result instanceof Promise) { - this._addMapDataPromiseForStream(result, stream); + result = result.then(function () { + return object; + }) + } else { + result = Promise.resolve(object); + } + this._addMapDataPromiseForStream(result, stream); + + if (object) { + this.callDelegateMethod("rawDataServiceDidAddOneRawData", this, stream, rawData, object); } - return object; + return result; } }, @@ -848,6 +841,7 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot } dataReadyPromise.then(function (results) { + return dataToPersist ? self.writeOfflineData(dataToPersist, stream.query, context) : null; }).then(function () { stream.dataDone(); @@ -1012,9 +1006,14 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot result; if (mapping) { - result = mapping.mapRawDataToObject(record, object, context).then(function () { - return self.mapRawDataToObject(record, object, context); - }); + 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); } From 81774a0e4062e783af70cd4f185ff55a6eebd511 Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Fri, 15 Sep 2017 15:28:57 -0400 Subject: [PATCH 18/31] Update lodash requires --- core/converter/camel-case-converter.js | 2 +- core/converter/kebab-case-converter.js | 2 +- core/converter/snake-case-converter.js | 2 +- core/converter/trim-converter.js | 2 +- package.json | 5 ++++- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/core/converter/camel-case-converter.js b/core/converter/camel-case-converter.js index cdacfa6eba..8cfde06bd0 100644 --- a/core/converter/camel-case-converter.js +++ b/core/converter/camel-case-converter.js @@ -3,7 +3,7 @@ * @requires montage/core/converter/converter */ var Converter = require("./converter").Converter, - camelCase = require('lodash/fp/camelCase'), + camelCase = require('lodash.camelcase'), singleton; /** diff --git a/core/converter/kebab-case-converter.js b/core/converter/kebab-case-converter.js index 072073a7bb..a418784567 100644 --- a/core/converter/kebab-case-converter.js +++ b/core/converter/kebab-case-converter.js @@ -3,7 +3,7 @@ * @requires montage/core/converter/converter */ var Converter = require("./converter").Converter, - kebabCase = require('lodash/fp/kebabCase'), + kebabCase = require('lodash.kebabcase'), singleton; /** diff --git a/core/converter/snake-case-converter.js b/core/converter/snake-case-converter.js index 0289299cc8..291558d386 100644 --- a/core/converter/snake-case-converter.js +++ b/core/converter/snake-case-converter.js @@ -3,7 +3,7 @@ * @requires montage/core/converter/converter */ var Converter = require("./converter").Converter, - snakeCase = require('lodash/fp/snakeCase'), + snakeCase = require('lodash.snakecase'), singleton; /** diff --git a/core/converter/trim-converter.js b/core/converter/trim-converter.js index e802d51ed7..2e0868ad61 100644 --- a/core/converter/trim-converter.js +++ b/core/converter/trim-converter.js @@ -3,7 +3,7 @@ * @requires montage/core/converter/converter */ var Converter = require("./converter").Converter, - trim = require('lodash/fp/trim'), + trim = require('lodash.trim'), deprecate = require("../deprecate"), shouldMuteWarning = false, singleton; diff --git a/package.json b/package.json index 5fddea079e..b34e727546 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,10 @@ "q-io": "^1.13.3", "weak-map": "^1.0.5", "xhr2": "^0.1.4", - "lodash": "4.17.4", + "lodash.kebabcase": "^4.1.1", + "lodash.camelcase": "^4.3.0", + "lodash.trim": "^4.5.1", + "lodash.snakecase": "^4.1.1", "proxy-polyfill": "~0.1.7" }, "devDependencies": { From c2b183ba011264728d66f1992dba59a3bd32594e Mon Sep 17 00:00:00 2001 From: Harold Thetiot Date: Tue, 19 Sep 2017 18:33:45 -0700 Subject: [PATCH 19/31] fix spec list --- test/all.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test/all.js b/test/all.js index 5b8ae301a8..76e6a98682 100644 --- a/test/all.js +++ b/test/all.js @@ -111,16 +111,6 @@ 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}, - - // Blueprint - {name: "spec/meta/converter-blueprint-spec", karma: false}, - {name: "spec/meta/module-blueprint-spec", karma: false}, - {name: "spec/meta/build-in-component-blueprint-spec", node: false, karma: false}, - {name: "spec/meta/component-blueprint-spec", node: false}, - {name: "spec/meta/controller-blueprint-spec", node: false}, - {name: "spec/meta/event-blueprint-spec", node: false}, - {name: "spec/meta/blueprint-spec"}, - // Data {name: "spec/data/data-selector"}, {name: "spec/data/data-mapping"}, @@ -133,7 +123,6 @@ module.exports = require("montage-testing").run(require, [ {name: "spec/data/enumeration"}, {name: "spec/data/http-service"}, {name: "spec/data/object-descriptor"}, - {name: "spec/data/offline-service"}, {name: "spec/data/property-descriptor"}, {name: "spec/data/raw-data-service"}, From dc97176c91198e3b4de289df0d59f548c78bb8f0 Mon Sep 17 00:00:00 2001 From: Harold Thetiot Date: Tue, 19 Sep 2017 18:53:05 -0700 Subject: [PATCH 20/31] jshint Rrrrr! --- .../raw-property-value-to-object-converter.js | 2 +- data/service/authorization-manager.js | 2 +- data/service/data-service.js | 65 +++--- data/service/expression-data-mapping.js | 2 +- data/service/persistent-data-service.js | 210 ++++++++++-------- data/service/raw-data-service.js | 2 +- data/service/snapshot-service.js | 2 +- 7 files changed, 148 insertions(+), 137 deletions(-) diff --git a/data/converter/raw-property-value-to-object-converter.js b/data/converter/raw-property-value-to-object-converter.js index 3439bb9906..cb81561e52 100644 --- a/data/converter/raw-property-value-to-object-converter.js +++ b/data/converter/raw-property-value-to-object-converter.js @@ -303,7 +303,7 @@ exports.RawPropertyValueToObjectConverter = Converter.specialize( /** @lends Raw query = DataQuery.withTypeAndCriteria(type, criteria); return self.service ? self.service.then(function (service) { - return service.rootService.fetchData(query) + return service.rootService.fetchData(query); }) : null; }); } diff --git a/data/service/authorization-manager.js b/data/service/authorization-manager.js index f78af65147..bdb5848175 100644 --- a/data/service/authorization-manager.js +++ b/data/service/authorization-manager.js @@ -125,7 +125,7 @@ var AuthorizationManager = Montage.specialize(/** @lends AuthorizationManager.pr _canNotifyDataService: { value: function (dataService) { - return dataService.authorizationManagerWillAuthorizeWithService && typeof dataService.authorizationManagerWillAuthorizeWithService == "function"; + return dataService.authorizationManagerWillAuthorizeWithService && typeof dataService.authorizationManagerWillAuthorizeWithService === "function"; } }, diff --git a/data/service/data-service.js b/data/service/data-service.js index 6cfa6b9849..92d339de3f 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -18,9 +18,6 @@ AuthorizationPolicyType.UpfrontAuthorizationPolicy = AuthorizationPolicy.UP_FRON AuthorizationPolicyType.OnDemandAuthorizationPolicy = AuthorizationPolicy.ON_DEMAND; AuthorizationPolicyType.OnFirstFetchAuthorizationPolicy = AuthorizationPolicy.ON_FIRST_FETCH; - - - /** * Provides data objects and manages changes to them. * @@ -340,6 +337,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { 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. @@ -350,8 +348,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { child.model && child.model.objectDescriptors || child.types && Array.isArray(child.types) && child.types || child.types && [child.types] || - [], - mappings = child.mappings || []; + []; return child._childServiceRegistrationPromise.then(function () { return self._registerChildServiceTypesAndMappings(child, types, mappings); @@ -1067,7 +1064,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { */ decacheObjectProperties: { value: function (object, propertyNames) { - if(this.isRootService) { + if (this.isRootService) { var names = Array.isArray(propertyNames) ? propertyNames : arguments, start = names === propertyNames ? 0 : 1, triggers = this._getTriggersForObject(object), @@ -1226,7 +1223,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { */ updateObjectProperties: { value: function (object, propertyNames) { - if(this.isRootService) { + 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, @@ -1396,13 +1393,13 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { */ getDataObject: { value: function (type, data, context, dataIdentifier) { - if(this.isRootService) { + if (this.isRootService) { var dataObject; // TODO [Charles]: Object uniquing. - if(this.isUniquing && dataIdentifier) { + if (this.isUniquing && dataIdentifier) { dataObject = this.objectForDataIdentifier(dataIdentifier); } - if(!dataObject) { + if (!dataObject) { dataObject = this._createDataObject(type, dataIdentifier); } @@ -1543,13 +1540,13 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { //TODO add the creation of a temporary identifier to pass to _createDataObject createDataObject: { value: function (type) { - if(this.isRootService) { + if (this.isRootService) { var object = this._createDataObject(type); this.createdDataObjects.add(object); return object; + } else { + this.rootService.createDataObject(type); } - else this.rootService.createDataObject(type); - } }, @@ -1572,7 +1569,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { //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) { + // if (dataIdentifier && this.isUniquing) { // this.recordDataIdentifierForObject(dataIdentifier, object); // this.recordObjectForDataIdentifier(object, dataIdentifier); // } @@ -1597,7 +1594,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { */ registerUniqueObjectWithDataIdentifier: { value: function(object, dataIdentifier) { - if(object && dataIdentifier && this.isRootService && this.isUniquing) { + if (object && dataIdentifier && this.isRootService && this.isUniquing) { this.recordDataIdentifierForObject(dataIdentifier, object); this.recordObjectForDataIdentifier(object, dataIdentifier); } @@ -1622,7 +1619,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { */ createdDataObjects: { get: function () { - if(this.isRootService) { + if (this.isRootService) { if (!this._createdDataObjects) { this._createdDataObjects = new Set(); } @@ -1648,7 +1645,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { */ changedDataObjects: { get: function () { - if(this.isRootService) { + if (this.isRootService) { this._changedDataObjects = this._changedDataObjects || new Set(); return this._changedDataObjects; } @@ -1752,7 +1749,6 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { stream = service.fetchData(query, stream) || stream; self._dataServiceByDataStream.set(stream, service); } else { - debugger; throw new Error("Can't fetch data of unknown type - " + (query.type.typeName || query.type.name) + "/" + query.type.uuid); } } catch (e) { @@ -1853,11 +1849,11 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { */ cancelDataStream: { value: function (dataStream, reason) { - if(dataStream) { + if (dataStream) { var rawDataService = this._dataServiceByDataStream.get(dataStream), self = this; - if(Promise.is(rawDataService)) { + if (Promise.is(rawDataService)) { rawDataService.then(function(service) { self._cancelServiceDataStream(service, dataStream, reason); }); @@ -1916,7 +1912,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { if (this.parentService && this.parentService._getChildServiceForObject(object) === this) { var record = {}; mappingPromise = this._mapObjectToRawData(object, record); - if(!mappingPromise) { + if (!mappingPromise) { mappingPromise = this.nullPromise; } return mappingPromise.then(function () { @@ -1929,7 +1925,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } else { service = this._getChildServiceForObject(object); - if(service) { + if (service) { return service.saveDataObject(object); } else { @@ -1951,7 +1947,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } else { service = action && this._getChildServiceForObject(object); - if(service) { + if (service) { return service._updateDataObject(object, action); } } @@ -2233,9 +2229,9 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { 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]); + return child ? + child.performOfflineOperations(operations.slice(start, end)) : + self._performAndDeleteOfflineOperation(operations[start]); }); } }, @@ -2249,21 +2245,21 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { tableSchema, foreignKeys, k, countK, kOnlinePrimaryKey, kForeignKey; - if(this.offlineService) { + if (this.offlineService) { tableSchema = this.offlineService.schema[operationType]; foreignKeys = tableSchema.foreignKeys; } - if(!foreignKeys) { - foreignKeys = tableSchema._computedForeignKeys - || (tableSchema._computedForeignKeys = Object.keys(operation.changes)); + if (!foreignKeys) { + foreignKeys = tableSchema._computedForeignKeys || + (tableSchema._computedForeignKeys = Object.keys(operation.changes)); } - for(k=0, countK = foreignKeys.length;k 0 && !dataStreamValue) { + 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) { + if (!dataStream.data || dataStream.length === 0) { object = this.super(stream, rawData, context, _type); } @@ -758,7 +762,7 @@ exports.PersistentDataService = PersistentDataService = RawDataService.specializ .then(function() { self.openTransaction() .then(function() { - var dataStream = self._dataStreamForRawDataStream(stream); + var dataStream = self._dataStreamForRawDataStream(stream), updateOperations = self.objectsToUpdatesForDataStream(dataStream), deleteOperations = self.deletesForDataStream(dataStream), updates = stream.data; @@ -819,8 +823,8 @@ exports.PersistentDataService = PersistentDataService = RawDataService.specializ 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 Date: Fri, 29 Sep 2017 16:49:01 -0700 Subject: [PATCH 21/31] Update property-descriptor & expression-data-mapping to account for changes in deserialization --- core/meta/property-descriptor.js | 6 +++--- data/service/expression-data-mapping.js | 9 +++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/core/meta/property-descriptor.js b/core/meta/property-descriptor.js index b318cb3b6e..15afe86298 100644 --- a/core/meta/property-descriptor.js +++ b/core/meta/property-descriptor.js @@ -117,7 +117,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# deserializeSelf: { value:function (deserializer) { this._name = deserializer.getProperty("name"); - this._owner = deserializer.getProperty("objectDescriptor") || deserializer.getProperty("blueprint"); + this._owner = this._owner || deserializer.getProperty("objectDescriptor") || deserializer.getProperty("blueprint"); this.cardinality = this._getPropertyWithDefaults(deserializer, "cardinality"); if (this.cardinality === -1) { this.cardinality = Infinity; @@ -158,7 +158,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# }, _owner: { - value:null + value: null }, /** @@ -171,7 +171,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# }, _name: { - value:null + value: null }, /** diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index f0a0362b38..7e8bcceb2d 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -42,8 +42,13 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData deserializeSelf: { value: function (deserializer) { - var value; - this.objectDescriptorReference = deserializer.getProperty("objectDescriptor"); + 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"); From 2ce49148546d5e57e35f81831af1bb5acb9ef0bb Mon Sep 17 00:00:00 2001 From: Corentin Debost Date: Mon, 2 Oct 2017 13:25:39 -0700 Subject: [PATCH 22/31] [lint] Change forin loops to iter over Object.keys --- data/model/enumeration.js | 7 ++++--- data/model/object-descriptor.js | 7 ++++--- data/service/authorization-manager.js | 5 +++-- data/service/data-mapping.js | 5 +++-- data/service/data-trigger.js | 10 ++++++---- data/service/expression-data-mapping.js | 15 +++++++++------ data/service/http-service.js | 7 ++++--- 7 files changed, 33 insertions(+), 23 deletions(-) diff --git a/data/model/enumeration.js b/data/model/enumeration.js index 8f707e2f45..c2e714c090 100644 --- a/data/model/enumeration.js +++ b/data/model/enumeration.js @@ -182,7 +182,7 @@ exports.Enumeration = Montage.specialize({}, /** @lends Enumeration */ { prototype = parsed.prototypeDescriptor, constructor = parsed.constructorDescriptor, constants = parsed.constantDescriptors, - enumeration, create, i; + 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 @@ -196,8 +196,9 @@ exports.Enumeration = Montage.specialize({}, /** @lends Enumeration */ { enumeration = Montage.specialize.call(exports.Enumeration, prototype, constructor); // Add the requested constants. create = this._makeCreateFunction(unique, other); - for (i in constants) { - enumeration[i] = create.apply(enumeration, constants[i]); + keys = Object.keys(constants); + for (i = 0; (key = keys[i]); ++i) { + enumeration[key] = create.apply(enumeration, constants[key]); } // Return the created enumeration. return enumeration; diff --git a/data/model/object-descriptor.js b/data/model/object-descriptor.js index 86c02bb7c0..49da464240 100644 --- a/data/model/object-descriptor.js +++ b/data/model/object-descriptor.js @@ -97,10 +97,11 @@ exports.ObjectDescriptor = Montage.specialize(/** @lends ObjectDescriptor.protot */ _setPropertyDescriptorsFromTypes: { value: function (types) { - var i, descriptor; - for (i in types) { + var i, descriptor, key, + keys = Object.keys(types); + for (i = 0; (key = keys[i]); ++i) { descriptor = this.makePropertyDescriptor(); - this.setPropertyDescriptor(i, descriptor); + this.setPropertyDescriptor(key, descriptor); } } }, diff --git a/data/service/authorization-manager.js b/data/service/authorization-manager.js index bdb5848175..d021a92699 100644 --- a/data/service/authorization-manager.js +++ b/data/service/authorization-manager.js @@ -189,8 +189,9 @@ var AuthorizationManager = Montage.specialize(/** @lends AuthorizationManager.pr value: function (moduleID, require) { var self = this; return require.async(moduleID).then(function (exports) { - var provider, providerName; - for (providerName in exports) { + var provider, i, providerName, + providerNames = Object.keys(exports); + for (i = 0; (providerName = providerNames[i]); ++i) { provider = provider || new exports[providerName](); } self.registerAuthorizationService(provider); diff --git a/data/service/data-mapping.js b/data/service/data-mapping.js index 2f07d57eae..d300995839 100644 --- a/data/service/data-mapping.js +++ b/data/service/data-mapping.js @@ -48,9 +48,10 @@ exports.DataMapping = Montage.specialize(/** @lends DataMapping.prototype */ { */ mapRawDataToObject: { value: function (data, object, context) { - var key; + var i, key, + keys = Object.keys(data); if (data) { - for (key in data) { + for (i = 0; (key = keys[i]); ++i) { object[key] = data[key]; } } diff --git a/data/service/data-trigger.js b/data/service/data-trigger.js index 2c299a32e4..aa266c2f35 100644 --- a/data/service/data-trigger.js +++ b/data/service/data-trigger.js @@ -389,8 +389,9 @@ Object.defineProperties(exports.DataTrigger, /** @lends DataTrigger */ { _addTriggersForMontageDataType: { value: function (service, type, prototype) { var triggers = {}, - trigger, name; - for (name in type.propertyDescriptors) { + 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; @@ -564,8 +565,9 @@ Object.defineProperties(exports.DataTrigger, /** @lends DataTrigger */ { */ removeTriggers: { value: function (triggers, prototype) { - var name; - for (name in triggers) { + var triggerNames = Object.keys(triggers), + name, i; + for (i = 0; (name = triggerNames[i]); ++i) { this.removeTrigger(triggers[name], prototype, name); } } diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 7e8bcceb2d..9fbfaf7609 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -339,9 +339,10 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData value: function (object, data) { var rules = this._compiledRawDataMappingRules, promises = [], - key; + keys = Object.keys(rules), + key, i; - for (key in rules) { + for (i = 0; (key = keys[i]); ++i) { promises.push(this.mapObjectToRawDataProperty(object, data, key)); } return promises && promises.length && Promise.all(promises) || Promise.resolve(null); @@ -517,9 +518,10 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData _mapObjectMappingRules: { value: function (rawRules, addOneWayBindings) { var rules = this._compiledObjectMappingRules, - propertyName, rawRule, rule; + propertyNames = Object.keys(rawRules), + propertyName, rawRule, rule, i; - for (propertyName in rawRules) { + for (i = 0; (propertyName = propertyNames[i]); ++i) { rawRule = rawRules[propertyName]; if (this._shouldMapRule(rawRule, addOneWayBindings)) { rule = this._makeRuleFromRawRule(rawRule, propertyName, addOneWayBindings); @@ -532,8 +534,9 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData _mapRawDataMappingRules: { value: function (rawRules, addOneWayBindings) { var rules = this._compiledRawDataMappingRules, - propertyName, rawRule, rule; - for (propertyName in rawRules) { + propertyNames = 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); diff --git a/data/service/http-service.js b/data/service/http-service.js index a6a0bfb675..dbf29e0889 100644 --- a/data/service/http-service.js +++ b/data/service/http-service.js @@ -298,7 +298,7 @@ var HttpService = exports.HttpService = RawDataService.specialize(/** @lends Htt } return new Promise(function (resolve, reject) { - var i, + var i, keys, key, startTime = new Date().getTime(); // Report errors or fetch the requested raw data. @@ -321,8 +321,9 @@ var HttpService = exports.HttpService = RawDataService.specialize(/** @lends Htt self.setHeadersForQuery(parsed.headers, parsed.query, parsed.url); - for (i in parsed.headers) { - request.setRequestHeader(i, parsed.headers[i]); + 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); From 8afb48f9db20f1183a7a7740f2b629dc184de9a3 Mon Sep 17 00:00:00 2001 From: Corentin Debost Date: Mon, 2 Oct 2017 13:26:20 -0700 Subject: [PATCH 23/31] [lint] Get rid of empty for loop blocks --- data/service/data-service.js | 5 ++++- data/service/http-service.js | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index 92d339de3f..40ba381e6e 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -2214,7 +2214,10 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { // Find the service responsible for this operation. child = services.get(operations[i]); // Find the end of a batch of operations for this service. - for (j = i + 1; j < n && child && (jService = services.get((jOperation = operations[j]))) === child; j += 1) {} + 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 = diff --git a/data/service/http-service.js b/data/service/http-service.js index dbf29e0889..0a412e7661 100644 --- a/data/service/http-service.js +++ b/data/service/http-service.js @@ -420,7 +420,11 @@ var HttpService = exports.HttpService = RawDataService.specialize(/** @lends Htt } else if (last < 4 || !(arguments[3] instanceof exports.HttpService.DataType)) { parsed.types = [exports.HttpService.DataType.JSON]; } else { - for (i = 3, n = last; i < n && arguments[i] instanceof exports.HttpService.DataType; i += 1) {} + 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()")); From a72e414e3f5441506599ff81488b58cce24e6859 Mon Sep 17 00:00:00 2001 From: Corentin Debost Date: Mon, 2 Oct 2017 13:26:57 -0700 Subject: [PATCH 24/31] [lint] Put double parentheses around assignment in conditionals --- data/service/expression-data-mapping.js | 2 +- data/service/persistent-data-service.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 9fbfaf7609..ae517fba1b 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -301,7 +301,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData if (requisitePropertyNames.size) { promises = []; - while (propertyName = iterator.next().value) { + while ((propertyName = iterator.next().value)) { promises.push(this.mapRawDataToObjectProperty(data, object, propertyName)); } result = Promise.all(promises); diff --git a/data/service/persistent-data-service.js b/data/service/persistent-data-service.js index 8ccfd864a8..f2e05da77c 100644 --- a/data/service/persistent-data-service.js +++ b/data/service/persistent-data-service.js @@ -1419,7 +1419,7 @@ exports.PersistentDataService = PersistentDataService = RawDataService.specializ jForeignKeyValue = iData[jForeignKey]; //if we have a value in this foreignKey: if (jForeignKeyValue) { - if (updatedRecord = self.addPrimaryKeyDependency(jForeignKeyValue, tableName,iPrimaryKey,jForeignKey, service.name)) { + if ((updatedRecord = self.addPrimaryKeyDependency(jForeignKeyValue, tableName,iPrimaryKey,jForeignKey, service.name))) { updatedRecords = updatedRecords || []; updatedRecords.push(updatedRecord); } From 3267fb7eff834cb5ae969af5f2ec5adb04e52da6 Mon Sep 17 00:00:00 2001 From: Corentin Debost Date: Mon, 2 Oct 2017 13:28:06 -0700 Subject: [PATCH 25/31] [lint] Fix alignment of property keys --- data/service/persistent-data-service.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/service/persistent-data-service.js b/data/service/persistent-data-service.js index f2e05da77c..244a313779 100644 --- a/data/service/persistent-data-service.js +++ b/data/service/persistent-data-service.js @@ -63,16 +63,16 @@ exports.PersistentDataService = PersistentDataService = RawDataService.specializ * @returns {Promise} */ - _storage : { + _storage: { value: undefined }, - storage : { + storage: { get: function() { return this._storage || (this._storage = Promise.reject(new Error('Needs to be implemented by sub classes'))); } }, - name : { + name: { value: void 0 }, From bb430bd3e9b9df6d4f8c0893afc2cdc045a83ed8 Mon Sep 17 00:00:00 2001 From: Corentin Debost Date: Mon, 2 Oct 2017 13:28:39 -0700 Subject: [PATCH 26/31] [lint] Misc whitespace fixes --- data/service/data-service.js | 8 ++++---- data/service/expression-data-mapping.js | 2 +- data/service/persistent-data-service.js | 8 ++++---- data/service/raw-data-service.js | 1 - 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index 40ba381e6e..2996a65e55 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -2232,8 +2232,8 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { value: function(promise, child, operations, start, end) { var self = this; return promise.then(function () { - return child ? - child.performOfflineOperations(operations.slice(start, end)) : + return child ? + child.performOfflineOperations(operations.slice(start, end)) : self._performAndDeleteOfflineOperation(operations[start]); }); } @@ -2254,7 +2254,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } if (!foreignKeys) { - foreignKeys = tableSchema._computedForeignKeys || + foreignKeys = tableSchema._computedForeignKeys || (tableSchema._computedForeignKeys = Object.keys(operation.changes)); } @@ -2333,7 +2333,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { // To be overridden by subclasses as necessary onlinePrimaryKeyForOfflinePrimaryKey: { value: function(offlinePrimaryKey) { - return this.offlineService ? + return this.offlineService ? this.offlineService.onlinePrimaryKeyForOfflinePrimaryKey(offlinePrimaryKey) : null; } }, diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index ae517fba1b..b0c9d31cb3 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -48,7 +48,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData } else { this.objectDescriptor = value; } - + this.schemaReference = deserializer.getProperty("schema"); value = deserializer.getProperty("objectMapping"); diff --git a/data/service/persistent-data-service.js b/data/service/persistent-data-service.js index 244a313779..a4de210dad 100644 --- a/data/service/persistent-data-service.js +++ b/data/service/persistent-data-service.js @@ -995,7 +995,7 @@ exports.PersistentDataService = PersistentDataService = RawDataService.specializ if ((iRawData = objects[i])) { if ( - typeof iRawData[primaryKey] === "undefined" || + typeof iRawData[primaryKey] === "undefined" || iRawData[primaryKey] === "" ) { //Set offline uuid based primary key @@ -1404,7 +1404,7 @@ exports.PersistentDataService = PersistentDataService = RawDataService.specializ self = this; if (!foreignKeys) { - foreignKeys = tableSchema._computedForeignKeys || + foreignKeys = tableSchema._computedForeignKeys || (tableSchema._computedForeignKeys = keys); } @@ -1468,8 +1468,8 @@ exports.PersistentDataService = PersistentDataService = RawDataService.specializ if (dependencies) { for (i=0;(iDependency = dependencies[i]);i++) { if ( - iDependency.tableName === tableName && - iDependency.primaryKey === tablePrimaryKey && + iDependency.tableName === tableName && + iDependency.primaryKey === tablePrimaryKey && iDependency.foreignKeyName === tableForeignKey ) { found = true; diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 2f14b19c6f..f22ba6c251 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -643,7 +643,6 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot stream.addData([mappedObject]); }); } - } }, From 4d2024e2dbbcf7ce8f2bb1a2b6b1636a48e6efb8 Mon Sep 17 00:00:00 2001 From: Corentin Debost Date: Mon, 2 Oct 2017 13:29:29 -0700 Subject: [PATCH 27/31] [lint] Simplify _areSameValues conditions --- data/service/snapshot-service.js | 39 ++++++++++++++------------------ 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/data/service/snapshot-service.js b/data/service/snapshot-service.js index 18f47950f1..08d0dc1705 100644 --- a/data/service/snapshot-service.js +++ b/data/service/snapshot-service.js @@ -111,30 +111,25 @@ exports.SnapshotService = Montage.specialize(/** @lends SnapshotService# */ { value: function(a, b) { var result = a === b; if (!result) { - if (typeof a === "object" && typeof b === "object") { - - // TODO WTF - result = (!!a === !!b); + 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) { - 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; - } + 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; } } } From f205c038654fad63f120ae05240a5ac36194dadd Mon Sep 17 00:00:00 2001 From: Corentin Debost Date: Mon, 2 Oct 2017 16:23:33 -0700 Subject: [PATCH 28/31] [lint] Suppress jshint loop warning --- data/service/raw-data-service.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index f22ba6c251..ac17d19347 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -639,9 +639,15 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot // will be done in place to avoid creating any unnecessary array. for (i = 0, n = records && records.length; i < n; i += 1) { iRecord = records[i]; + /*jshint -W083*/ + // Turning off jshint's function within loop warning because the + // only "outer scoped variable" we're accessing here is stream, + // which is a constant reference and won't cause unexpected + // behavior due to iteration. this.addOneRawData(stream, iRecord, context, streamSelectorType).then(function (mappedObject) { stream.addData([mappedObject]); }); + /*jshint +W083*/ } } }, From 9c01762d7d54e6d0ebc117ba87f67ae5f2ddcd3c Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Tue, 3 Oct 2017 11:34:18 -0400 Subject: [PATCH 29/31] Include every dependencies mentioned in mjson/meta files when mopping an app. --- node.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node.js b/node.js index a2949f6549..655c2bde40 100644 --- a/node.js +++ b/node.js @@ -163,7 +163,7 @@ function parseHtmlDependencies(text/*, location*/) { MontageBoot.TemplateLoader = function (config, load) { return function (id, module) { var html = id.match(/(.*\/)?(?=[^\/]+\.html$)/); - var serialization = id.match(/(?=[^\/]+\.json$)/); // XXX this is not necessarily a strong indicator of a serialization alone + var serialization = id.match(/(?=[^\/]+\.(?:json|mjson|meta)$)/); // XXX this is not necessarily a strong indicator of a serialization alone var reelModule = id.match(/(.*\/)?([^\/]+)\.reel\/\2$/); if (html) { return load(id, module) From 641f439168341d64a3735c3a5da66da8d92a6103 Mon Sep 17 00:00:00 2001 From: Corentin Debost Date: Wed, 4 Oct 2017 19:44:18 -0700 Subject: [PATCH 30/31] Remove duplicate storage property in persistent-data-service --- data/service/persistent-data-service.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/data/service/persistent-data-service.js b/data/service/persistent-data-service.js index a4de210dad..28f8833cd7 100644 --- a/data/service/persistent-data-service.js +++ b/data/service/persistent-data-service.js @@ -476,20 +476,6 @@ exports.PersistentDataService = PersistentDataService = RawDataService.specializ } }, - - /** - * Returns a Promise for the storage used to store objects - * described by all objectDescriptors in the service's model. - * - * @argument {Model} stream - * @returns {Promise} - */ - storage: { - value: function(model) { - return Promise.reject(); - } - }, - /** * Get the first child service that can handle data of the specified type, * or `null` if no such child service exists. From 646c85bda3233bc26db6d7ee87c4a6b9c0d57f50 Mon Sep 17 00:00:00 2001 From: Thomas Jaede Date: Thu, 5 Oct 2017 20:15:17 -0700 Subject: [PATCH 31/31] Fix deserialization of property-descriptor & add null check in expression-data-mapping --- core/meta/property-descriptor.js | 16 +++++++++++----- data/service/expression-data-mapping.js | 6 +++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/core/meta/property-descriptor.js b/core/meta/property-descriptor.js index 793b6a829c..af17ec5f5a 100644 --- a/core/meta/property-descriptor.js +++ b/core/meta/property-descriptor.js @@ -125,11 +125,12 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# if (value !== void 0) { this._owner = value; } - this._overridePropertyWithDefaults(deserializer, "cardinality"); + this._overridePropertyWithDefaults(deserializer, "cardinality", "cardinality"); if (this.cardinality === -1) { this.cardinality = Infinity; } + this._overridePropertyWithDefaults(deserializer, "mandatory", "mandatory"); this._overridePropertyWithDefaults(deserializer, "readOnly", "readOnly"); this._overridePropertyWithDefaults(deserializer, "denyDelete", "denyDelete"); @@ -183,12 +184,17 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# value: function (deserializer, objectKey /*, deserializerKeys... */) { var propertyNames = Array.prototype.slice.call(arguments).slice(2, Infinity), value, i, n; + + // [TJ] Prospective code + // if (propertyNames.length === 0) { + // propertyNames = [objectKey]; + // } + for (i = 0, n = propertyNames.length; i < n && !value; i++) { - this[objectKey] = deserializer.getProperty(propertyNames[i]); - } - if (this[objectKey] === void 0) { - this[objectKey] = Defaults[propertyNames[0]]; + value = deserializer.getProperty(propertyNames[i]); } + + this[objectKey] = value === undefined ? Defaults[propertyNames[0]] : value; } }, diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index b0c9d31cb3..80aa357446 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -518,7 +518,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData _mapObjectMappingRules: { value: function (rawRules, addOneWayBindings) { var rules = this._compiledObjectMappingRules, - propertyNames = Object.keys(rawRules), + propertyNames = rawRules ? Object.keys(rawRules) : [], propertyName, rawRule, rule, i; for (i = 0; (propertyName = propertyNames[i]); ++i) { @@ -534,7 +534,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData _mapRawDataMappingRules: { value: function (rawRules, addOneWayBindings) { var rules = this._compiledRawDataMappingRules, - propertyNames = Object.keys(rawRules), + propertyNames = rawRules ? Object.keys(rawRules) : [], propertyName, rawRule, rule, i; for (i = 0; (propertyName = propertyNames[i]); ++i) { rawRule = rawRules[propertyName]; @@ -700,7 +700,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData object[propertyName] = data; } else if (hasData) { //Cardinality is 1, if data contains more than 1 item, we throw - if (data.length && data.length>1) { + 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];