diff --git a/core/converter/collection-iteration-converter.js b/core/converter/collection-iteration-converter.js index 10208d841..76309113c 100644 --- a/core/converter/collection-iteration-converter.js +++ b/core/converter/collection-iteration-converter.js @@ -3,6 +3,7 @@ * @requires mod/core/converter/converter */ const Converter = require("./converter").Converter, + evaluate = require("../frb/evaluate"), Promise = require("../promise").Promise; @@ -18,6 +19,10 @@ exports.CollectionIterationConverter = Converter.specialize( /** @lends Collecti serializeSelf: { value: function (serializer) { + serializer.setProperty("convertedValueIteratorExpression", this.convertedValueIteratorExpression); + serializer.setProperty("iterator", this.iterator); + serializer.setProperty("iterationConverter", this.iterationConverter); + serializer.setProperty("iterationReverter", this.iterationReverter); serializer.setProperty("mapConverter", this.keysConverter); serializer.setProperty("mapReverter", this.keysConverter); @@ -27,6 +32,27 @@ exports.CollectionIterationConverter = Converter.specialize( /** @lends Collecti deserializeSelf: { value: function (deserializer) { + let value = deserializer.getProperty("iterator"); + if (value) { + this.iterator = value; + } + + value = deserializer.getProperty("convertedValueIteratorExpression"); + if (value) { + this.convertedValueIteratorExpression = value; + } + + + value = deserializer.getProperty("iterationConverter"); + if (value) { + this.iterationConverter = value; + } + value = deserializer.getProperty("iterationReverter"); + if (value) { + this.iterationReverter = value; + } + + value = deserializer.getProperty("mapConverter"); if (value) { this.mapConverter = value; @@ -39,6 +65,64 @@ exports.CollectionIterationConverter = Converter.specialize( /** @lends Collecti } }, + /** + * Sometimes it might be more practocal to get an iterator from the value to be converted, like for an array or a map. A map especially + * offers both keys() and values() iterators. So setting "keys" as the value for convertedValueIteratorExpression, will lead a CollectionIterationConverter + * to evaluate that expression on the value being converted and get the iterator it needs. + * + * @property {Iterator|function} + * @default {Iterator} undefined + */ + _convertedValueIteratorExpression: { + value: undefined + }, + convertedValueIteratorExpression: { + get: function() { + return this._convertedValueIteratorExpression; + }, + set: function(value) { + if(value !== this._convertedValueIteratorExpression) { + this._convertedValueIteratorExpression = value; + } + } + }, + + /** + * The iterator object to be used to iterate over the collection to be converted. The iterator can be what turns one object into a collection + * For example, a single object with an ExpressionIterator will produce a collection of values to convert. + * + * @property {Iterator|function} + * @default {Iterator} undefined + */ + _iterator: { + value: undefined + }, + iterator: { + get: function() { + return this._iterator; + }, + set: function(value) { + if(value !== this._iterator) { + this._iterator = value; + } + } + }, + + + /** + * @property {Converter|function} + * @default {Converter} undefined + */ + iterationConverter: { + get: function() { + return this._iterationConverter; + }, + set: function(value) { + this._iterationConverter = value; + this._convert = this._convertCollection; + } + }, + /** * @property {Converter|function} * @default {Converter} undefined @@ -49,8 +133,8 @@ exports.CollectionIterationConverter = Converter.specialize( /** @lends Collecti }, set: function(value) { this._iterationConverter = value; - this._convert = this._convertElementIndexCollection; - this._revert = this._revertElementIndexCollection; + this._convert = this._convertCollection; + this._revert = this._revertCollection; } }, @@ -64,8 +148,8 @@ exports.CollectionIterationConverter = Converter.specialize( /** @lends Collecti }, set: function(value) { this._iterationReverter = value; - this._convert = this._convertElementIndexCollection; - this._revert = this._revertElementIndexCollection; + this._convert = this._convertCollection; + this._revert = this._revertCollection; } }, @@ -136,13 +220,34 @@ exports.CollectionIterationConverter = Converter.specialize( /** @lends Collecti * @param {Collection} value - a collection where this._iterationConverter is applied on each value * @returns {Collection} a collection of the same type as the input containing each value converted. */ - _convertElementIndexCollection: { + _convertCollection: { value: function (value) { if(!this._iterationConverter || !value ) return value; - var values = value.values(), - converter = this._iterationConverter, + //If value is not a collection, we make an effort to treat it as an iteration object + // if(isNaN(value.length) || isNaN(value.size)) { + // return this._iterationConverter.convert(value); + // } + + /* + A pre-set iterator can't know the argument valuet is what it needs to iterate on, + so we use the .from() method to make it aware of it. + + However the other methods are asking value for it, so using .from(value) is not needed. + */ + var valueIterator = this._iterator + ? this._iterator.from(value) + : this.convertedValueIteratorExpression + ? evaluate(this.convertedValueIteratorExpression) + : value[Symbol.iterator](), + isValueCollection = (!isNaN(value.length) || !isNaN(value.size)); + + if(!valueIterator) { + throw "No Iterator found for value:", value; + } + + var converter = this._iterationConverter, iteration, isConverterFunction = typeof converter === "function", iValue, @@ -151,7 +256,7 @@ exports.CollectionIterationConverter = Converter.specialize( /** @lends Collecti promises, result; - while(!(iteration = values.next()).done) { + while(!(iteration = valueIterator.next()).done) { iValue = iteration.value; iConvertedValue = isConverterFunction @@ -161,7 +266,18 @@ exports.CollectionIterationConverter = Converter.specialize( /** @lends Collecti if(Promise.is(iConvertedValue)) { (promises || (promises = [])).push(iConvertedValue); } else { - (result || (result = new value.constructor)).add(iConvertedValue); + /* + If we don't have result yet, we create it to be of the same type of the value we received + TODO: We might need to add another property to fully control that type from the outside if needed + Like for receiving an array but returning a set + */ + if(!isValueCollection) { + if(!result) { + result = value; + } + } else { + (result || (result = new value.constructor)).add(iConvertedValue); + } } index++; } @@ -180,13 +296,13 @@ exports.CollectionIterationConverter = Converter.specialize( /** @lends Collecti * @param {Collection} value - a collection where this._iterationReverter is applied on each value * @returns {Collection} a collection of the same type as the input containing each value reverted. */ - _revertElementIndexCollection: { + _revertCollection: { enumerable: false, value: function(value) { if(!this._iterationReverter || !value) return value; - var values = value.values(), + var valueIterator = value.values(), reverter = this._iterationReverter, iteration, isReverterFunction = typeof reverter === "function", @@ -196,11 +312,11 @@ exports.CollectionIterationConverter = Converter.specialize( /** @lends Collecti promises, result; - if(!isReverterFunction && typeof reverter.revert !== "function") { + if(!isReverterFunction && typeof g.revert !== "function") { return value; } - while(!(iteration = values.next()).done) { + while(!(iteration = valueIterator.next()).done) { iValue = iteration.value; iConvertedValue = isReverterFunction diff --git a/core/converter/pipeline-converter.js b/core/converter/pipeline-converter.js index 48ed729c9..013362ebb 100644 --- a/core/converter/pipeline-converter.js +++ b/core/converter/pipeline-converter.js @@ -94,7 +94,10 @@ exports.PipelineConverter = Converter.specialize({ if (isFinalOutput) { - result = isPromise ? output : Promise.resolve(output); + //Potentially breaking change here. The code was introducing a Promise in the output when none existed in any of the converter involved + //WAS: result = isPromise ? output : Promise.resolve(output); + //NOW: respecting the natural outcome of the pipeline's converters + result = output; } else if (isPromise) { result = output.then(function (value) { return self._convertWithConverterAtIndex(value, index); diff --git a/core/expression-iterator.js b/core/expression-iterator.js index f8cc3f89a..1ea4f682f 100644 --- a/core/expression-iterator.js +++ b/core/expression-iterator.js @@ -27,10 +27,6 @@ exports.ExpressionIterator = class ExpressionIterator extends Object { if(value) { this._value = value; this._expression = expression; - /* - Initially, during the creation of the iterator, we need to call it because the next method is actually a generator, so by invoking it we return new instance of the generator. - */ - this._iterator = this._generateNext(this._expression, value); } } @@ -41,6 +37,12 @@ exports.ExpressionIterator = class ExpressionIterator extends Object { * @private * @type {object} */ + __iterator: { + value: null, + }, + _expression: { + value: null, + }, _syntax: { value: null, }, @@ -58,6 +60,47 @@ exports.ExpressionIterator = class ExpressionIterator extends Object { } + /** + * Serializes the ExpressionIterator's properties using the provided serializer. + * @param {Serializer} serializer - The serializer instance. + */ + serializeSelf(serializer) { + super.serializeSelf(serializer); + serializer.setProperty("expression", this.expression); + } + + /** + * Deserializes the ExpressionIterator's properties using the provided deserializer. + * @param {Deserializer} deserializer - The deserializer instance. + */ + deserializeSelf(deserializer) { + this.expression = deserializer.getProperty("expression"); + } + + + /* + * Borrowed from Iterator.from() static method + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/from + * + * Allows a configured instance to iterate over a specific value + * @param {Iterable} value - An objec to iterate on. + * @return {this} + */ + from(value) { + this._value = value; + return this; + } + + get _iterator() { + return this.__iterator || (this.__iterator = this._generateNext(this._expression)); + } + + /** + * TEST ME - to see if expression were changed while + * iteration is happening if it does the right thing + * + * @type {object} + */ _reset() { this._expression = null; this._compiledSyntax = null; @@ -68,6 +111,17 @@ exports.ExpressionIterator = class ExpressionIterator extends Object { this._syntax = null; } + get expression() { + return this._expression; + } + set expression (value) { + if (value !== this._expression) { + //We need to reset: + this._reset(); + this._expression= value; + } + } + /** * The parsed expression, a syntactic tree. * Now mutable to avoid creating new objects when appropriate @@ -135,9 +189,17 @@ exports.ExpressionIterator = class ExpressionIterator extends Object { } else { this._current = this.evaluateExpression(this._current); } - yield this._current; + + /* + To have the yiels return {value:..., done: true}, + the last yield needs to be the one to cary + the last actual value, done: will be false + the function needs to end without a yield + then {value:undefined, done: true} is returned by next() + */ + if(this._current) { + yield this._current; + } } - } - } \ No newline at end of file diff --git a/core/extras/function.js b/core/extras/function.js index 99fb4faa1..adf503ed8 100644 --- a/core/extras/function.js +++ b/core/extras/function.js @@ -77,3 +77,13 @@ Object.defineProperty(Function.prototype, "isClass", { configurable: true }); +Object.defineProperty(Function.prototype, "debounceWithDelay", { + value: function (delay) { + let timer; + return (...args) => { + clearTimeout(timer); + timer = setTimeout(() => this(...args), delay) + } + }, + configurable: true +}); diff --git a/data/converter/data-collection-iteration-converter.js b/data/converter/data-collection-iteration-converter.js index c3746d2d4..870c6fd5b 100644 --- a/data/converter/data-collection-iteration-converter.js +++ b/data/converter/data-collection-iteration-converter.js @@ -46,6 +46,24 @@ exports.DataCollectionIterationConverter = class DataCollectionIterationConverte this._iterationConverter.foreignDescriptor = value; } } + + convert(value) { + if(this.currentRule?.propertyDescriptor.cardinality === 1) { + if(Array.isArray(value)) { + if(value.length === 1) { + return super.convert(value.one()); + } else { + throw `convert value with length > 1 for property ${this.currentRule.propertyDescriptor.name} with a cardinality of 1` + } + + } else { + throw `Collection other than array are not handled for a property ${this.currentRule.propertyDescriptor.name} with a cardinality of 1: ${value}`; + } + + } else { + return super.convert(value); + } + } } diff --git a/data/converter/raw-foreign-value-to-object-converter.js b/data/converter/raw-foreign-value-to-object-converter.js index d078a41d8..0425951a2 100644 --- a/data/converter/raw-foreign-value-to-object-converter.js +++ b/data/converter/raw-foreign-value-to-object-converter.js @@ -121,7 +121,7 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( } }, _fetchConvertedDataForObjectDescriptorCriteria: { - value: function(typeToFetch, criteria, currentRule) { + value: function(typeToFetch, criteria, currentRule, registerMappedPropertiesAsChanged) { var self = this; return this.service ? this.service.then(function (service) { @@ -203,6 +203,10 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( query.hints = {rawDataService: service}; + if(registerMappedPropertiesAsChanged){ + query.hints.registerMappedPropertiesAsChanged = registerMappedPropertiesAsChanged; + } + if(sourceObjectSnapshot?.originDataSnapshot) { query.hints.originDataSnapshot = sourceObjectSnapshot.originDataSnapshot; } @@ -342,7 +346,8 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( if(!queryParts) { queryParts = { criteria: [], - readExpressions: [] + readExpressions: []/*, + hints: {}*/ }; self._pendingCriteriaByTypeToCombine.set(typeToFetch, queryParts); } @@ -351,18 +356,39 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( /* Sounds twisted, but this is to deal with the case where we need to fetch to resolve a property of the object itself. added check to avoid duplicates + + 11/29/2025 FIXME - Running into a case where a single raw property value is used to fetch multiple different object properties. + So the only way to handle that in an HTTP Service to decide what URL/API End point to reach is to have the readExpressions. + It turns out that in that case, both properties points to the same type, which is the same as the type of the data instance for + which we're resolving a property. So to not risk a regression, I'm adding an or of + + currentRule.propertyDescriptor._valueDescriptorReference === typeToFetch + + To make sure we're not breaking existing behavior. BUT this needs to be re-assessed and simplified, I think now we should always + that extra bit of important information and I don't think it will create a problem, but we need to test and assess + with a current working setup, and eventually add test/specs. */ - if((currentRule && (!currentRule.propertyDescriptor._valueDescriptorReference || !currentRule.propertyDescriptor.valueDescriptor)) && !(queryParts.readExpressions.includes(currentRule.targetPath))) { + //if((currentRule && (!currentRule.propertyDescriptor._valueDescriptorReference || !currentRule.propertyDescriptor.valueDescriptor)) && !(queryParts.readExpressions.includes(currentRule.targetPath))) { + if(((currentRule && (!currentRule.propertyDescriptor._valueDescriptorReference || !currentRule.propertyDescriptor.valueDescriptor)) && !(queryParts.readExpressions.includes(currentRule.targetPath))) || (currentRule.propertyDescriptor._valueDescriptorReference === currentRule.propertyDescriptor.owner)) { + //if(((currentRule && (!currentRule.propertyDescriptor._valueDescriptorReference || !currentRule.propertyDescriptor.valueDescriptor)) && !(queryParts.readExpressions.includes(currentRule.targetPath))) || (currentRule.propertyDescriptor._valueDescriptorReference === typeToFetch)) { queryParts.readExpressions.push(currentRule.targetPath); } + + /* + + queryParts.hints.dataInstance = + queryParts.hints.dataInstancePropertyName = currentRule.targetPath; + + */ + /* Now we need to scheduled a queueMicrotask() if it's not done. */ if(!self.constructor.prototype._isCombineFetchDataMicrotaskQueued) { self.constructor.prototype._isCombineFetchDataMicrotaskQueued = true; queueMicrotask(function() { - self._combineFetchDataMicrotask(service) + self._combineFetchDataMicrotask(service, registerMappedPropertiesAsChanged) }); } @@ -415,7 +441,7 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( }, _combineFetchDataMicrotaskFunctionForTypeQueryParts: { - value: function(type, queryParts, service, rootService) { + value: function(type, queryParts, service, rootService, registerMappedPropertiesAsChanged) { var self = this, combinedCriteria = queryParts.criteria.length > 1 ? Criteria.or(queryParts.criteria) : queryParts.criteria[0], //query = DataQuery.withTypeAndCriteria(type, combinedCriteria), @@ -460,6 +486,10 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( query.readExpressions = queryParts.readExpressions; } + if(registerMappedPropertiesAsChanged) { + query.hints.registerMappedPropertiesAsChanged = registerMappedPropertiesAsChanged; + } + //console.log("_combineFetchDataMicrotaskFunctionForTypeQueryParts query:",query); mapIterationFetchPromise = rootService.fetchData(query) @@ -556,7 +586,7 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( }, _combineFetchDataMicrotask: { - value: function(service) { + value: function(service, registerMappedPropertiesAsChanged) { //console.log("_combineFetchDataMicrotask("+this._pendingCriteriaByTypeToCombine.size+")"); var mapIterator = this._pendingCriteriaByTypeToCombine.entries(), @@ -568,7 +598,7 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( mapIterationType = mapIterationEntry[0]; mapIterationQueryParts = mapIterationEntry[1]; - this._combineFetchDataMicrotaskFunctionForTypeQueryParts(mapIterationType, mapIterationQueryParts, service, service.rootService); + this._combineFetchDataMicrotaskFunctionForTypeQueryParts(mapIterationType, mapIterationQueryParts, service, service.rootService, registerMappedPropertiesAsChanged); } this.constructor.prototype._isCombineFetchDataMicrotaskQueued = false; @@ -663,6 +693,7 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( var self = this, //We put it in a local variable so we have the right value in the closure currentRule = this.currentRule, + registerMappedPropertiesAsChanged = this.registerMappedPropertiesAsChanged, criteria, query; @@ -696,7 +727,7 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( aCriteria; while (anObjectDescriptor = mapIterator.next().value) { aCriteria = this.convertCriteriaForValue(groupMap.get(anObjectDescriptor)); - promises.push(this._fetchConvertedDataForObjectDescriptorCriteria(anObjectDescriptor, aCriteria)); + promises.push(this._fetchConvertedDataForObjectDescriptorCriteria(anObjectDescriptor, aCriteria, currentRule, registerMappedPropertiesAsChanged)); } @@ -723,7 +754,7 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( foreignKeyValue = v[rawDataProperty], aCriteria = this.convertCriteriaForValue(foreignKeyValue); - return this._fetchConvertedDataForObjectDescriptorCriteria(valueDescriptor, aCriteria); + return this._fetchConvertedDataForObjectDescriptorCriteria(valueDescriptor, aCriteria, currentRule, registerMappedPropertiesAsChanged); } else { return Promise.resolve(null); @@ -737,7 +768,7 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( return this._descriptorToFetch.then(function (typeToFetch) { - return self._fetchConvertedDataForObjectDescriptorCriteria(typeToFetch, criteria, currentRule); + return self._fetchConvertedDataForObjectDescriptorCriteria(typeToFetch, criteria, currentRule, registerMappedPropertiesAsChanged); // if (self.serviceIdentifier) { // criteria.parameters.serviceIdentifier = self.serviceIdentifier; diff --git a/data/converter/time-zone-identifier-to-time-zone-converter.js b/data/converter/time-zone-identifier-to-time-zone-converter.js index 126909054..15f06f1f6 100644 --- a/data/converter/time-zone-identifier-to-time-zone-converter.js +++ b/data/converter/time-zone-identifier-to-time-zone-converter.js @@ -27,14 +27,20 @@ var TimeZoneIdentifierToTimeZoneConverter = exports.TimeZoneIdentifierToTimeZone } }, + /* + this doesn't feel right - convertCriteriaForValue: { - value: function(value) { - var criteria = new Criteria().initWithSyntax(this.convertSyntax, value); - criteria._expression = this.convertExpression; - return criteria; - } - }, + this converter doesn't inherit convertSyntax nor convertExpression properties + so this can't really work when called via a RawDataService's mapReadOperationToRawReadOperation method + */ + + // convertCriteriaForValue: { + // value: function(value) { + // var criteria = new Criteria().initWithSyntax(this.convertSyntax, value); + // criteria._expression = this.convertExpression; + // return criteria; + // } + // }, /** * Converts the TimeZone identifier string to a TimeZone. diff --git a/data/service/data-operation.js b/data/service/data-operation.js index 971a62041..d09654f73 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -201,7 +201,7 @@ var Montage = require("../../core/core").Montage, exports.DataOperationType = DataOperationType = new Enum().initWithMembersAndValues(dataOperationTypes,dataOperationTypes); -var dataOperationErrorNames = ["DatabaseMissing", "ObjectDescriptorStoreMissing", "PropertyDescriptorStoreMissing", "InvalidInput", "SyntaxError", "PropertyDescriptorNotFound", "PropertyMappingNotFound"]; +var dataOperationErrorNames = ["DatabaseMissing", "ObjectDescriptorStoreMissing", "PropertyDescriptorStoreMissing", "InvalidInput", "SyntaxError", "PropertyDescriptorNotFound", "PropertyMappingNotFound", "TransactionDeadlock"]; exports.DataOperationErrorNames = DataOperationErrorNames = new Enum().initWithMembersAndValues(dataOperationErrorNames,dataOperationErrorNames); // exports.DataOperationError.ObjectDescriptorStoreMissingError = Error.specialize({ diff --git a/data/service/data-service.js b/data/service/data-service.js index 245aa5e8f..a14937aa5 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -35,6 +35,7 @@ const Object = global.Object, //Cache for scope traversal performance require("../../core/extras/string"); require("../../core/extras/date"); +require("core/extras/function"); var AuthorizationPolicyType = new Montage(); AuthorizationPolicyType.NoAuthorizationPolicy = AuthorizationPolicy.NONE; @@ -4446,6 +4447,10 @@ DataService.addClassProperties( }, }, + debouncedQueueMicrotaskWithDelay: { + value: queueMicrotask.debounceWithDelay(500) + }, + registerDataObjectChangesFromEvent: { value: function (changeEvent, shouldTrackChangesWhileBeingMapped) { var dataObject = changeEvent.target, @@ -4459,14 +4464,22 @@ DataService.addClassProperties( return; } - if (!isDataObjectBeingMapped && this.autosaves && !this.isAutosaveScheduled) { - this.isAutosaveScheduled = true; - queueMicrotask(() => { + if (!isDataObjectBeingMapped && this.autosaves /* && !this.isAutosaveScheduled*/) { + //this.isAutosaveScheduled = true; + this.debouncedQueueMicrotaskWithDelay(() => { this.isAutosaveScheduled = false; this.saveChanges(); }); } + // if (!isDataObjectBeingMapped && this.autosaves && !this.isAutosaveScheduled) { + // this.isAutosaveScheduled = true; + // queueMicrotask(() => { + // this.isAutosaveScheduled = false; + // this.saveChanges(); + // }); + // } + var inversePropertyName = propertyDescriptor.inversePropertyName, inversePropertyDescriptor; @@ -4704,7 +4717,7 @@ DataService.addClassProperties( } else { for (i = 0, countI = removedValues.length; i < countI; i++) { if (!isDataObjectBeingMapped) { - registeredRemovedValues.delete(removedValues[i]); + registeredRemovedValues.add(removedValues[i]); } self._removeDataObjectPropertyDescriptorValueForInversePropertyDescriptor( dataObject, @@ -5371,6 +5384,21 @@ DataService.addClassProperties( self._buildInvalidityStateForObjects(deletedDataObjects), ]) .then(([createdInvalidityStates, changedInvalidityStates, deletedInvalidityStates]) => { + + + /* + + Benoit 12/26/2025 + + The following is half-baked: + 1. createdInvalidityStates, changedInvalidityStates, deletedInvalidityStates get entries for object descriptors and instances have no actual invalidity + 2. The dispatch is only done for changed objects and not created nor deleted ones + + temporarily commenting it out until we address it + + */ + + /* Benoit 12/26/2025 // self._dispatchObjectsInvalidity(createdDataObjectInvalidity); self._dispatchObjectsInvalidity(changedInvalidityStates); @@ -5387,8 +5415,12 @@ DataService.addClassProperties( // Exit, can't move on resolve(validatefailedOperation); } else { + + Benoit 12/26/2025 */ return transactionObjectDescriptors; + /* Benoit 12/26/2025 } + Benoit 12/26/2025 */ }, reject) .then(function (_transactionObjectDescriptors) { var operationCount = diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index f0d7c94d6..289838909 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -1784,7 +1784,17 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData if(lastReadSnapshot[rawDataPropertyName] !== rawDataPropertValue) { rawData[rawDataPropertyName] = rawDataPropertValue; - if(lastReadSnapshot[rawDataPropertyName] !== undefined) { + /* + For add/remove, this is potentially called twice: + - once for added values + - once for removed values + + So to avoid, on the second call when called twice to override the actual + correct last known values with the upcominh one, + we add a test to verify that rawDataSnapshot doesn't already have the property set + + */ + if((lastReadSnapshot[rawDataPropertyName] !== undefined) && (!rawDataSnapshot.hasOwnProperty(rawDataPropertyName))) { rawDataSnapshot[rawDataPropertyName] = lastReadSnapshot[rawDataPropertyName]; //assuming is now pendingSnapshot, we record the new value for next one: diff --git a/data/service/fetch-resource-data-mapping.js b/data/service/fetch-resource-data-mapping.js index 8cabf3ed6..d028c92d4 100644 --- a/data/service/fetch-resource-data-mapping.js +++ b/data/service/fetch-resource-data-mapping.js @@ -6,6 +6,7 @@ const Montage = require("core/core").Montage, parse = require("core/frb/parse"), compile = require("core/frb/compile-evaluator"), assign = require("core/frb/assign"), + Promise = require("core/promise").Promise, Scope = require("core/frb/scope"); /** @@ -322,25 +323,62 @@ exports.FetchResourceDataMapping = class FetchResourceDataMapping extends Expres return fetchRequests; } - fetchResponseRawDataMappingFunctionForCriteria(aCriteria) { - let value = this.fetchResponseRawDataMappingExpressionByCriteria.get(aCriteria); + /** + * Historically started with just an expression to evaluate on a scope containing fetchResponse + * Evolved to be able to use a converter which opens the door for more flexobility. + * + * @public + * @argument {Object} fetchResponse - The response to map + * @argument {Array} rawData - The array conraing raw data, each entry destrined to become one object + * @argument {Criteria} aCriteria - a criteria for which a specific mapping was configured for that response + */ + mapFetchResponseToRawDataMatchingCriteria(fetchResponse, rawData, aCriteria) { + + let value = this.fetchResponseRawDataMappingExpressionByCriteria.get(aCriteria), + rawDataResult; if(!value) { throw new Error("No Fetch Response Mapping found for Criteria: "+ aCriteria); } - if(typeof value !== "function") { - //We parse and compile the expression so we can evaluate it: - try { - value = compile(parse(value)); - } catch(compileError) { - throw new Error("Fetch Response Mapping Expression Compile error: "+ compileError+", for Criteria: "+ aCriteria); + //Use of a converter + if(typeof value.convert === "function") { + //This is not coded to handle the return of a promise + rawDataResult = value.convert(fetchResponse); + if(Promise.is(rawDataResult)) { + throw "Mapping fetchResponse to raw data with a comverter isn't coded to handle a Promise returned by converter" } + } + //Use of a direct expression, but we're looking for a comp + else { + let compiledExpressionFunction; + + if(typeof value !== "function") { + //We parse and compile the expression so we can evaluate it: + try { + compiledExpressionFunction = compile(parse(value)); + } catch(compileError) { + throw new Error("Fetch Response Mapping Expression Compile error: "+ compileError+", for Criteria: "+ aCriteria); + } - this.fetchResponseRawDataMappingExpressionByCriteria.set(aCriteria, value); + this.fetchResponseRawDataMappingExpressionByCriteria.set(aCriteria, compiledExpressionFunction); + } else { + compiledExpressionFunction = value; + } + + //Now run the function to get the value: + let fetchResponseScope = this._scope.nest(fetchResponse); + + rawDataResult = compiledExpressionFunction(fetchResponseScope); } - return value; + if(rawDataResult) { + Array.isArray(rawDataResult) + ? rawData.push(...rawDataResult) + : rawData.push(rawDataResult); + } + + return; } /** @@ -367,21 +405,24 @@ exports.FetchResourceDataMapping = class FetchResourceDataMapping extends Expres */ if(this.fetchResponseRawDataMappingExpressionByCriteria) { let criteriaIterator = this.fetchResponseRawDataMappingExpressionByCriteria.keys(), - fetchResponseScope = this._scope.nest(fetchResponse), + // fetchResponseScope = this._scope.nest(fetchResponse), iCriteria; while ((iCriteria = criteriaIterator.next().value)) { if(iCriteria.evaluate(fetchResponse)) { //We have a match, we need to evaluate the rules to - let fetchResponseRawDataMappingFunction = this.fetchResponseRawDataMappingFunctionForCriteria(iCriteria), - result = fetchResponseRawDataMappingFunction(fetchResponseScope); + // let fetchResponseRawDataMappingFunction = this.fetchResponseRawDataMappingFunctionForCriteria(iCriteria), + // result = fetchResponseRawDataMappingFunction(fetchResponseScope); + // if(result) { + // Array.isArray(result) + // ? rawData.push(...result) + // : rawData.push(result); + // } - if(result) { - Array.isArray(result) - ? rawData.push(...result) - : rawData.push(result); - } + //We have a match, we map what we have in store for this criteria, + //An expression to evaluate, or a converter + this.mapFetchResponseToRawDataMatchingCriteria(fetchResponse, rawData, iCriteria) } } } else if(Array.isArray(fetchResponse)) { diff --git a/data/service/http-service.js b/data/service/http-service.js index 982efdcff..22351493d 100644 --- a/data/service/http-service.js +++ b/data/service/http-service.js @@ -623,7 +623,7 @@ var HttpService = exports.HttpService = class HttpService extends RawDataService } } - } else if (!rawDataOperations.has(dataOperation)) { + } else if (iObjectRule && !rawDataOperations.has(dataOperation)) { rawDataOperations.push(dataOperation); } diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index c3aacf856..6e03bf3bd 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -4,6 +4,7 @@ var DataService = require("./data-service").DataService, Criteria = require("../../core/criteria").Criteria, DataMapping = require("./data-mapping").DataMapping, DataIdentifier = require("../model/data-identifier").DataIdentifier, + RawDataIdentifier = require("../model/raw-data-identifier").RawDataIdentifier, UserIdentity = require("../model/app/user-identity").UserIdentity, Deserializer = require("../../core/serialization/deserializer/montage-deserializer").MontageDeserializer, Map = require("../../core/collections/map"), @@ -1301,10 +1302,18 @@ RawDataService.addClassProperties({ resolveObjectForTypeRawData: { value: function (type, rawData, context) { - var dataIdentifier = this.dataIdentifierForTypeRawData(type, rawData), + var dataIdentifier, //Retrieves an existing object is responsible data service is uniquing, or creates one object, result; + + try { + dataIdentifier = this.dataIdentifierForTypeRawData(type, rawData); + } catch(error) { + console.warn(`Error creating required dataIdentifer for type ${type.name}, rawData: ${JSON.stringify(rawData)}: ${error.message}`); + dataIdentifier = null; + return Promise.resolveNull; + } //Retrieves an existing object is responsible data service is uniquing, or creates one object = this.getDataObject(type, rawData, dataIdentifier, context); @@ -2918,7 +2927,7 @@ RawDataService.addClassProperties({ This might be overreaching? Let's see */ if(valueDescriptor && !objectRuleConverter) { - console.warn("won't map property '"+propertyName+"' as no comverter is specified for valueDescriptor " +valueDescriptor.name); + console.warn("won't map property '"+propertyName+"' as no converter is specified for valueDescriptor " +valueDescriptor.name); } return ( @@ -4693,7 +4702,7 @@ RawDataService.addClassProperties({ dataOperationsByObject.set(object, operation); } - console.debug("###### _saveDataOperation ("+operationType+") forObject "+ this.dataIdentifierForObject(object)+ " in commitTransactionOperation "+commitTransactionOperation.id + " is ", operation) + //console.debug("###### _saveDataOperation ("+operationType+") forObject "+ this.dataIdentifierForObject(object)+ " in commitTransactionOperation "+commitTransactionOperation.id + " is ", operation) return operation; });