From 986968ca824d111ea4729898fbd484c46d3f5a16 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 23 Dec 2025 01:14:14 +0100 Subject: [PATCH 01/25] fix issue where a "replace" - an add and a remove wouldn't actually do the remove, leading to an erroneus add --- data/service/data-service.js | 2 +- data/service/expression-data-mapping.js | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index 245aa5e8f..06d08bb5f 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -4704,7 +4704,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, 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: From 8b98650e79ea632244cb86690ddf704ea7b6ea35 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 23 Dec 2025 01:14:39 +0100 Subject: [PATCH 02/25] fix log typo --- data/service/raw-data-service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index c3aacf856..c06867fe2 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -2918,7 +2918,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 ( From 5feb58c1288b7ca208b06995c9f9ed30f1ddbe20 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 8 Dec 2025 00:24:54 -0800 Subject: [PATCH 03/25] - add ability to be serialized - fix end of iteration so now the iteration returned has done: true as expected --- core/expression-iterator.js | 76 +++++++++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 7 deletions(-) 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 From 1f8b75996cd4a7a9383b08de55e1b38a0f142eee Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 18 Dec 2025 16:30:23 +0100 Subject: [PATCH 04/25] WIP for enabling more sophisticated mapping needed by azure.mod --- .../collection-iteration-converter.js | 142 ++++++++++++++++-- .../data-collection-iteration-converter.js | 18 +++ .../raw-foreign-value-to-object-converter.js | 51 +++++-- data/service/fetch-resource-data-mapping.js | 77 +++++++--- data/service/http-service.js | 2 +- data/service/raw-data-service.js | 11 +- 6 files changed, 258 insertions(+), 43 deletions(-) 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/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/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 c06867fe2..af76f5391 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); From 71b08a72559d603b01d93cb81f34253985dc9657 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 26 Dec 2025 17:02:07 +0100 Subject: [PATCH 05/25] Fix a design issue regfarding async/sync. This is likely breaking backward compatibiloity in theory, but unlikely in practice --- core/converter/pipeline-converter.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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); From c0baa3c0d1f6e907f6ce8ad8de105730366f785a Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 26 Dec 2025 17:03:06 +0100 Subject: [PATCH 06/25] add a debounceWithDelay() method to function that returns a debounced version of the funtion it's called on --- core/extras/function.js | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 +}); From 4a8e48e21e99776bf830bf127a5c9056b004c01e Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 26 Dec 2025 17:03:59 +0100 Subject: [PATCH 07/25] remove a method that had no business being there and was causing issues --- ...-zone-identifier-to-time-zone-converter.js | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) 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. From 2ccc8283efa197c2c2ab73d9d076bedf188b4a7d Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 26 Dec 2025 17:05:28 +0100 Subject: [PATCH 08/25] add debouncing to autosave --- data/service/data-service.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index 06d08bb5f..a8441a076 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; From d36854ce5f2a26a9aa01fc8fe890359124397d47 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 26 Dec 2025 17:05:53 +0100 Subject: [PATCH 09/25] silence debug log --- data/service/raw-data-service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index af76f5391..6e03bf3bd 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -4702,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; }); From 80a2e6d3564ce40674761211e0882149fa3e47ef Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 26 Dec 2025 23:54:25 +0100 Subject: [PATCH 10/25] disable invalidity state handling while we fix it --- data/service/data-service.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/data/service/data-service.js b/data/service/data-service.js index a8441a076..a14937aa5 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -5384,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); @@ -5400,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 = From 1a8c6d912e8e5a7d54ac10662208686163acdf0d Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 26 Dec 2025 23:54:58 +0100 Subject: [PATCH 11/25] add TransactionDeadlock dataOperationErrorName --- data/service/data-operation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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({ From 1da9689450813cb2ca8268cef02da61e50b4cdbc Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Mon, 5 Jan 2026 11:01:51 +0100 Subject: [PATCH 12/25] Revert "disable invalidity state handling while we fix it" This reverts commit 80a2e6d3564ce40674761211e0882149fa3e47ef. --- data/service/data-service.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index a14937aa5..a8441a076 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -5384,21 +5384,6 @@ 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); @@ -5415,12 +5400,8 @@ 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 = From cca65a1fc3845d7e300c1b03536d340dc2d8bb8a Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Mon, 5 Jan 2026 12:20:31 +0100 Subject: [PATCH 13/25] fix: missing requires --- ui/button.mod/button.js | 1 + ui/text-input.js | 1 + 2 files changed, 2 insertions(+) diff --git a/ui/button.mod/button.js b/ui/button.mod/button.js index a15537634..5f7a01549 100644 --- a/ui/button.mod/button.js +++ b/ui/button.mod/button.js @@ -5,6 +5,7 @@ const { VisualPosition } = require("core/enums/visual-position"); const { PressComposer } = require("composer/press-composer"); const { KeyComposer } = require("composer/key-composer"); const { Control } = require("ui/control"); +const { Montage } = require("core/core"); // TODO: migrate away from using undefinedGet and undefinedSet diff --git a/ui/text-input.js b/ui/text-input.js index 40db0f2ff..c85ebbb8a 100644 --- a/ui/text-input.js +++ b/ui/text-input.js @@ -4,6 +4,7 @@ var Control = require("ui/control").Control, Button = require("ui/button.mod").Button, deprecate = require("core/deprecate"); +const { Montage } = require("core/core"); /** The base class for all text-based input components. You typically won't create instances of this prototype. From 78fd3cf767f7fd9acea59d34d0adf1c86394bec2 Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Mon, 5 Jan 2026 12:21:51 +0100 Subject: [PATCH 14/25] chore: format --- data/service/data-service.js | 47 +++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index a8441a076..98519496d 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -70,7 +70,7 @@ AuthenticationPolicy.OnFirstFetchAuthenticationPolicy = AuthenticationPolicy.ON_ * @class * @extends external:Montage */ -const DataService = exports.DataService = class DataService extends Target { +const DataService = (exports.DataService = class DataService extends Target { /** @lends DataService */ constructor() { super(); @@ -119,7 +119,7 @@ const DataService = exports.DataService = class DataService extends Target { }, }); } -}; +}); // DataService = exports.DataService = Target.specialize(/** @lends DataService.prototype */ { @@ -1316,7 +1316,7 @@ DataService.addClassProperties( type = type instanceof ObjectDescriptor ? type : this.objectDescriptorForType(type); //services = this._childServicesByType.get(type) || this._childServicesByType.get(null); services = this._childServicesByType.get(type); - /* + /* Fixing bug were a catch-all data service, one that states it handles all by having no type could end up multiple times in services as the following block was executed at every call with the same type argument. @@ -1325,7 +1325,7 @@ DataService.addClassProperties( */ if (services) { let catchAllServices = this._childServicesByType.get(null); - if (catchAllServices && !Object.isFrozen(services)) { + if (catchAllServices && !Object.isFrozen(services)) { services.push(...catchAllServices); Object.freeze(services); } @@ -2448,13 +2448,11 @@ DataService.addClassProperties( childServicesFetchObjectProperty: { value: function (object, propertyName, isObjectCreated) { - //Workaround for now: - if(object.dataIdentifier.dataService) { + if (object.dataIdentifier.dataService) { return object.dataIdentifier.dataService.fetchObjectProperty(object, propertyName, isObjectCreated); } - throw "Data Services with multiple child services per ObjectDescriptor have to implement this method"; // /* // If there's more than one, we're entering the realm of decisions about how to deal with them. @@ -3091,7 +3089,7 @@ DataService.addClassProperties( service.dataIdentifierForNewObjectWithObjectDescriptor(this.objectDescriptorForType(type)) ); - this.registerCreatedDataObject(object); + this.registerCreatedDataObject(object); return object; } else { @@ -3904,10 +3902,14 @@ DataService.addClassProperties( }, registerChangedDataObject: { value: function (dataObject) { - var objectDescriptor = this.objectDescriptorForObject(dataObject), changedDataObjects, value; + var objectDescriptor = this.objectDescriptorForObject(dataObject), + changedDataObjects, + value; if (this.isObjectCreated(dataObject)) { - console.warn(`DataService can't register a new object (${objectDescriptor.name} in changedDataObjects`); + console.warn( + `DataService can't register a new object (${objectDescriptor.name} in changedDataObjects` + ); return; } @@ -4448,7 +4450,7 @@ DataService.addClassProperties( }, debouncedQueueMicrotaskWithDelay: { - value: queueMicrotask.debounceWithDelay(500) + value: queueMicrotask.debounceWithDelay(500), }, registerDataObjectChangesFromEvent: { @@ -4530,7 +4532,12 @@ DataService.addClassProperties( }, _registerDataObjectChangesFromEvent: { - value: function (changeEvent, propertyDescriptor, inversePropertyDescriptor, shouldTrackChangesWhileBeingMapped) { + value: function ( + changeEvent, + propertyDescriptor, + inversePropertyDescriptor, + shouldTrackChangesWhileBeingMapped + ) { var dataObject = changeEvent.target, isCreatedObject = this.isObjectCreated(dataObject), key = changeEvent.key, @@ -4551,9 +4558,9 @@ DataService.addClassProperties( ? #TODO TEST!! */ - // if (dataObject.objectDescriptor.name === "EmploymentPositionStaffing") { - // debugger; - // } + // if (dataObject.objectDescriptor.name === "EmploymentPositionStaffing") { + // debugger; + // } if (!isCreatedObject && (!isDataObjectBeingMapped || shouldTrackChangesWhileBeingMapped)) { //this.changedDataObjects.add(dataObject); @@ -6015,10 +6022,10 @@ DataService.addClassProperties( ? key === "" ? JSON.stringify(value, this._criteriaParametersReplacer) : value?.dataIdentifier - ? value.dataIdentifier - : !!value - ? value.toString() - : null + ? value.dataIdentifier + : !!value + ? value.toString() + : null : Array.isArray(value) ? value.map(this._criteriaParametersReplacer) : value; @@ -6375,7 +6382,7 @@ DataService.addClassProperties( readOperation.hints = query.hints; } - /* + /* this is half-assed, we're mapping full objects to RawData, but not the properties in the expression. phront-service does it, but we need to stop doing it half way there and the other half over there. SaveChanges is cleaner, but the job is also easier there. From 916067bc5a4e0818f8e0ea74a060cbc586e4f9c5 Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Mon, 5 Jan 2026 18:48:20 +0100 Subject: [PATCH 15/25] fix: improve validation error handling in DataService --- data/service/data-service.js | 69 ++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index 98519496d..cea7f185e 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -4941,21 +4941,14 @@ DataService.addClassProperties( }, /** - * Dispatches invalidity events for each object in the provided map. - * @param {Map>} invalidityStates - A map where each key is an object and its value is its invalidity state. + * Dispatches validation error events for each object in the provided map. + * @param {Map>} objectInvalidityMap - A map where keys are objects and values are their invalidity states. * @returns {void} */ - _dispatchObjectsInvalidity: { - value: function (invalidityStates) { - const invalidObjectIterator = invalidityStates.keys(); - let invalidObject; - - while ((invalidObject = invalidObjectIterator.next().value)) { - this.dispatchDataEventTypeForObject( - DataEvent.invalid, - invalidObject, - invalidityStates.get(invalidObject) - ); + _dispatchValidationErrors: { + value: function (objectInvalidityMap) { + for (const [dataObject, invalidity] of objectInvalidityMap) { + this.dispatchDataEventTypeForObject(DataEvent.invalid, dataObject, invalidity); } }, }, @@ -5390,25 +5383,39 @@ DataService.addClassProperties( // 3. Validate deleted objects (e.g., check for rules that block deletion). self._buildInvalidityStateForObjects(deletedDataObjects), ]) - .then(([createdInvalidityStates, changedInvalidityStates, deletedInvalidityStates]) => { - // self._dispatchObjectsInvalidity(createdDataObjectInvalidity); - self._dispatchObjectsInvalidity(changedInvalidityStates); - - if (changedInvalidityStates.size > 0) { - // Do we really need the DataService itself to dispatch another event with - // all invalid data together at once? - // self.mainService.dispatchDataEventTypeForObject(DataEvent.invalid, self, detail); - - var validatefailedOperation = new DataOperation(); - validatefailedOperation.type = DataOperation.Type.ValidateFailedOperation; - // At this point, it's the dataService - validatefailedOperation.target = self.mainService; - validatefailedOperation.data = changedInvalidityStates; - // Exit, can't move on - resolve(validatefailedOperation); - } else { - return transactionObjectDescriptors; + .then((validationResults) => { + const aggregatedValidationErrors = new Map(); + + // Merge validation results from all operations. (created, changed, deleted) + for (const result of validationResults) { + if (!result) continue; + + for (const [dataObject, invalidity] of result) { + // Only add if there are actual validation errors + if (invalidity && invalidity.size > 0) { + aggregatedValidationErrors.set(dataObject, invalidity); + } + } } + + if (aggregatedValidationErrors.size > 0) { + self._dispatchValidationErrors(aggregatedValidationErrors); + + const validateFailedOperation = new DataOperation(); + validateFailedOperation.type = DataOperation.Type.ValidateFailedOperation; + validateFailedOperation.target = self.mainService; + + // Pass the complete set of errors + validateFailedOperation.data = aggregatedValidationErrors; + + // Stop the transaction and resolve with the failure operation + resolve(validateFailedOperation); + + // TODO: @benoit should we not stop/reject the inner promise ? + return null; + } + + return transactionObjectDescriptors; }, reject) .then(function (_transactionObjectDescriptors) { var operationCount = From 141317e3f8740ec453acdf5fe084ab8ed1a61daf Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Mon, 5 Jan 2026 18:49:50 +0100 Subject: [PATCH 16/25] feat: add data service test saveChanges should not report errors for an object when its descriptor has no validation rules --- test/spec/data/data-service.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/spec/data/data-service.js b/test/spec/data/data-service.js index 04a9ac13a..ccbd038dc 100644 --- a/test/spec/data/data-service.js +++ b/test/spec/data/data-service.js @@ -6,6 +6,7 @@ var DataService = require("mod/data/service/data-service").DataService, defaultEventManager = require("mod/core/event/event-manager").defaultEventManager; const AnimatedMovieDescriptor = require("spec/data/logic/model/animated-movie.mjson").montageObject; +const CategoyDescriptor = require("spec/data/logic/model/category.mjson").montageObject; const movieDescriptor = require("spec/data/logic/model/movie.mjson").montageObject; describe("A DataService", function () { @@ -460,6 +461,7 @@ describe("A DataService", function () { let mainService; let movie; let animatedMovie; + let categoryService; beforeEach(async () => { mainService = new DataService(); @@ -467,8 +469,11 @@ describe("A DataService", function () { movieService = new RawDataService(); animatedMovieService = new RawDataService(); + categoryService = new RawDataService(); + await mainService.registerChildService(movieService, movieDescriptor); await mainService.registerChildService(animatedMovieService, AnimatedMovieDescriptor); + await mainService.registerChildService(categoryService, CategoyDescriptor); movie = mainService.createDataObject(movieDescriptor); animatedMovie = mainService.createDataObject(AnimatedMovieDescriptor); @@ -617,6 +622,16 @@ describe("A DataService", function () { expect(styleErrors.length).toBe(1); expect(styleErrors[0].message).toBe("Animation style must be either '2d' or '3d'."); }); + + it("should not report errors for an object when its descriptor has no validation rules", async () => { + const category = mainService.createDataObject(CategoyDescriptor); + + // Verify valid state with no additional data. + await mainService.saveChanges(); + + expect(category.invalidityState).toBeDefined(); + expect(category.invalidityState.size).toBe(0); + }); }); }); }); From 755ce1e468d2ace852f69d3b3bcfe20c439c3ab0 Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Tue, 6 Jan 2026 09:11:58 +0100 Subject: [PATCH 17/25] fix: performance improvements --- data/service/data-service.js | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index cea7f185e..b0e85926a 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -4940,19 +4940,6 @@ DataService.addClassProperties( }, }, - /** - * Dispatches validation error events for each object in the provided map. - * @param {Map>} objectInvalidityMap - A map where keys are objects and values are their invalidity states. - * @returns {void} - */ - _dispatchValidationErrors: { - value: function (objectInvalidityMap) { - for (const [dataObject, invalidity] of objectInvalidityMap) { - this.dispatchDataEventTypeForObject(DataEvent.invalid, dataObject, invalidity); - } - }, - }, - _promisesByPendingTransactions: { value: undefined, }, @@ -5394,13 +5381,18 @@ DataService.addClassProperties( // Only add if there are actual validation errors if (invalidity && invalidity.size > 0) { aggregatedValidationErrors.set(dataObject, invalidity); + + // Dispatch invalid data event for each invalid object + this.dispatchDataEventTypeForObject( + DataEvent.invalid, + dataObject, + invalidity + ); } } } if (aggregatedValidationErrors.size > 0) { - self._dispatchValidationErrors(aggregatedValidationErrors); - const validateFailedOperation = new DataOperation(); validateFailedOperation.type = DataOperation.Type.ValidateFailedOperation; validateFailedOperation.target = self.mainService; From cd9656434975772e1adfcbcc9312005873c82411 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 5 Jan 2026 21:28:56 +0100 Subject: [PATCH 18/25] fix typo --- 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 b0e85926a..2082d7a73 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -5278,7 +5278,7 @@ DataService.addClassProperties( this.deletedDataObjects.size === 0 ) { /* - If we have pending transation(s), then it means some logical saves got combined, so until we offer an API for intentional separatin of changes, + If we have pending transation(s), then it means some logical saves got combined, so until we offer an API for intentional separation of changes, best we can do is return a promise encompasing all pending. If anothe saveChanges were to be called, it may not use that same promise From 09597e0e3e4c917610bdf8c08cac769e87d15bc0 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 6 Jan 2026 22:20:50 -0800 Subject: [PATCH 19/25] add engineeering notes --- data/model/transaction-event.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/data/model/transaction-event.js b/data/model/transaction-event.js index 9681efa7a..3254c7e21 100644 --- a/data/model/transaction-event.js +++ b/data/model/transaction-event.js @@ -29,6 +29,22 @@ var MutableEvent = require("../../core/event/mutable-event").MutableEvent, * */ +/* + #TODO #REFINE: Benoit, 1/5/2026: Now that Transaction are a Data typw, we shouldn't need + any kind of a special event: + - create is covered by regular create data operation + - reateProgress? createComplete? + - createFail shouldn't be specific to a Transaction + - Validate, ValidateStart, ValidateProgress, ValidateComplete, ValidateFail - same, not unique to transactions + - transactionPrepare -> transactionPrepareFail ? + - transactionRollback ? That's low level: from the client's perspective, it doesn't exsist, unless you reset all objects to last fetched state, and lose changes. + - shoud it be a RawTransaction (New, to be created if ...) aspect? + - commit: same. It's a mean to cumulate changes from a client to a server.tp represent the fact + that all changes desired to be in one transaction may not fit in one "message" between client and server. + + To be thought through more +*/ + const transactionEventTypes = [ "transactionCreate", From a843eddd7b01bd2b8a9370438255bf9501462f55 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 6 Jan 2026 22:27:15 -0800 Subject: [PATCH 20/25] add logic so a RawDataService doesn't execute handleReadUpdateOperation() if the operation.rawDataService isn't itself, which was happening when multiple RawDataServices handle a same data type --- data/service/raw-data-service.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 6e03bf3bd..6285155a8 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -3454,6 +3454,17 @@ RawDataService.addClassProperties({ handleReadUpdateOperation: { value: function (operation) { + + //We need to find a way to take care of this... + /* + Client side, with a synchronization data service in place, + an readUpdateOperation dispatched by its destination service + is handled here by an origin service that handles that readUpdateOperation's target + */ + if(operation.rawDataService !== this) { + return; + } + var referrer = operation.referrerId, objectDescriptor = operation.target, records = operation.data, From ee49afa40aa8a99336cb4a91278cc08858aedee4 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 6 Jan 2026 22:29:06 -0800 Subject: [PATCH 21/25] refactor to add orcherstration of transation's execution so a transaction with an update to a data object always follow a comitted transaction that created it --- data/service/data-service.js | 89 ++++++++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 10 deletions(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index 2082d7a73..0e9d3d178 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -5297,7 +5297,7 @@ DataService.addClassProperties( var transaction = new Transaction(), self = this, - //Ideally, this should be saved in IndexedDB so if something happen + //Ideally, this should be saved in IndexedDB/PGLite so if something happen //we can at least try to recover. createdDataObjects = (transaction.createdDataObjects = new Map(this.createdDataObjects)), //Map changedDataObjects = (transaction.updatedDataObjects = new Map(this.changedDataObjects)), //Map @@ -5317,7 +5317,8 @@ DataService.addClassProperties( deletedDataObjects ); - this.addPendingTransaction(transaction); + // move to _saveChangesForTransaction() + //this.addPendingTransaction(transaction); //We've made copies, so we clear right away to make room for a new cycle: this.discardChanges(); @@ -5327,6 +5328,16 @@ DataService.addClassProperties( // this.dataObjectChanges.clear(); // this.objectDescriptorsWithChanges.clear(); + return this._saveChangesForTransaction(transaction); + }, + }, + + _saveChangesForTransaction: { + value: function(transaction) { + let self = this; + + this.addPendingTransaction(transaction); + let pendingTransactionPromise = new Promise(function (resolve, reject) { try { var deletedDataObjectsIterator, @@ -5334,7 +5345,10 @@ DataService.addClassProperties( transactionObjectDescriptors = transaction.objectDescriptors, iObject, transactionPrepareEvent, - transactionCommitEvent; + transactionCommitEvent, + createdDataObjects = transaction.createdDataObjects, //Map + changedDataObjects = transaction.updatedDataObjects, //Map + deletedDataObjects = transaction.deletedDataObjects; //Map // We first remove from create and update objects that are also deleted: deletedDataObjectsIterator = deletedDataObjects.values(); @@ -5410,6 +5424,60 @@ DataService.addClassProperties( return transactionObjectDescriptors; }, reject) .then(function (_transactionObjectDescriptors) { + + //Before we can get to it, we need to verify that "transaction"'s content has no dependency to a pending transaction + //for example an update to an object that's created in a pending transaction that has not completed yet, which can lead to deadlocks. + //We're going to start by handling that use-case only + + + + let pendingTransactions = this._pendingTransactions; + + if (pendingTransactions && pendingTransactions.length) { + + let changedDataObjects = transaction.changedDataObjects, //Map + changedDataObjectsIterator = deletedDataObjects.values(), + firstPendingTransactionsCreatingChangedDataObjectsPromise, + pendingTransactionsCreatingChangedDataObjectsPromises, + iObject; + + /* + TODO WIP + Nested loop, really need to be optimized as we build up the transactions, + but let's get to work first and then we'll optimize. + + There can only be one pending transaction with the creation of iObject + so we bail out if we find it + */ + + while ((iObject = changedDataObjectsIterator.next().value)) { + for (let i = 0, countI = pendingTransactions.length; i < countI; i++) { + + if (pendingTransactions[i].createdDataObjects.has(iObject)) { + if(!firstPendingTransactionsCreatingChangedDataObjectsPromise) { + firstPendingTransactionsCreatingChangedDataObjectsPromise = this.registeredPromiseForPendingTransaction(transaction); + } else { + (pendingTransactionsCreatingChangedDataObjectsPromises || (pendingTransactionsCreatingChangedDataObjectsPromises = new Set(firstPendingTransactionsCreatingChangedDataObjectsPromise))).add(this.registeredPromiseForPendingTransaction(transaction)); + } + break; + } + } + } + + if(!pendingTransactionsCreatingChangedDataObjectsPromises) { + if(firstPendingTransactionsCreatingChangedDataObjectsPromise) { + return firstPendingTransactionsCreatingChangedDataObjectsPromise; + } else { + return Promise.resolveUndefined; + } + } else { + return Promise.all(Array.from(pendingTransactionsCreatingChangedDataObjectsPromises)) + } + } else { + return Promise.resolveUndefined + } + }) + .then(function () { var operationCount = createdDataObjects.size + changedDataObjects.size + deletedDataObjects.size, currentPropagationPromise, @@ -5418,13 +5486,13 @@ DataService.addClassProperties( propagationPromises = []; /* - Now that we passed validation, we're going to start the transaction - */ + Now that we passed validation, and handling transaction dependencies, we're going to start the transaction + */ /* - Make sure we listen on ourselve (as events from RawDataServices will bubble to main) - for "transactionPrepareStart" - */ + Make sure we listen on ourselve (as events from RawDataServices will bubble to main) + for "transactionPrepareStart" + */ //addEventListener(TransactionEvent.transactionCreateStart, self, false); //console.log("saveChanges: dispatchEvent transactionCreateEvent transaction-"+this.identifier, transaction); @@ -5557,10 +5625,11 @@ DataService.addClassProperties( } }); - self.registerPendingTransactionPromise(transaction, pendingTransactionPromise); + this.registerPendingTransactionPromise(transaction, pendingTransactionPromise); return pendingTransactionPromise; - }, + + } }, _cancelTransaction: { From 84bfcac4b8f7346f0b6020eadb0aae1409b22a6f Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 7 Jan 2026 14:11:18 -0800 Subject: [PATCH 22/25] iteration on handling dependencies between transactions --- data/service/data-service.js | 44 ++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index 0e9d3d178..974fbfb62 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -5431,15 +5431,15 @@ DataService.addClassProperties( - let pendingTransactions = this._pendingTransactions; + let pendingTransactions = self._pendingTransactions; if (pendingTransactions && pendingTransactions.length) { - let changedDataObjects = transaction.changedDataObjects, //Map - changedDataObjectsIterator = deletedDataObjects.values(), + let updatedDataObjectsByObjectDescriptor = transaction.updatedDataObjects, //Map + updatedDataObjectDescriptorsIterator = updatedDataObjectsByObjectDescriptor.keys(), firstPendingTransactionsCreatingChangedDataObjectsPromise, pendingTransactionsCreatingChangedDataObjectsPromises, - iObject; + iObjectDescriptor; /* TODO WIP @@ -5450,18 +5450,38 @@ DataService.addClassProperties( so we bail out if we find it */ - while ((iObject = changedDataObjectsIterator.next().value)) { - for (let i = 0, countI = pendingTransactions.length; i < countI; i++) { + //Loop on ObjectDescriptors with instances being updated + while ((iObjectDescriptor = updatedDataObjectDescriptorsIterator.next().value)) { + let updatedDataObjects = updatedDataObjectsByObjectDescriptor.get(iObjectDescriptor), + updatedDataObjectsIterator = updatedDataObjects.values(), + iObject; + + while ((iObject = updatedDataObjectsIterator.next().value)) { + + for (let i = 0, countI = pendingTransactions.length; i < countI; i++) { + let iPendingTransaction = pendingTransactions[i]; + + if (iPendingTransaction.createdDataObjects.has(iObjectDescriptor)) { + let createdDataObjects = iPendingTransaction.createdDataObjects.get(iObjectDescriptor); + + if(createdDataObjects.has(iObject)) { + + if(!firstPendingTransactionsCreatingChangedDataObjectsPromise) { + firstPendingTransactionsCreatingChangedDataObjectsPromise = self.registeredPromiseForPendingTransaction(iPendingTransaction); + } else { + if(!pendingTransactionsCreatingChangedDataObjectsPromises) { + pendingTransactionsCreatingChangedDataObjectsPromises = new Set(); + pendingTransactionsCreatingChangedDataObjectsPromises.add(firstPendingTransactionsCreatingChangedDataObjectsPromise); + } + pendingTransactionsCreatingChangedDataObjectsPromises.add(self.registeredPromiseForPendingTransaction(iPendingTransaction)); + } + break; + } - if (pendingTransactions[i].createdDataObjects.has(iObject)) { - if(!firstPendingTransactionsCreatingChangedDataObjectsPromise) { - firstPendingTransactionsCreatingChangedDataObjectsPromise = this.registeredPromiseForPendingTransaction(transaction); - } else { - (pendingTransactionsCreatingChangedDataObjectsPromises || (pendingTransactionsCreatingChangedDataObjectsPromises = new Set(firstPendingTransactionsCreatingChangedDataObjectsPromise))).add(this.registeredPromiseForPendingTransaction(transaction)); } - break; } } + } if(!pendingTransactionsCreatingChangedDataObjectsPromises) { From 7d105046e511b458be3304d19e8236ba04cffb5b Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 7 Jan 2026 14:12:24 -0800 Subject: [PATCH 23/25] Fix an exception happening when a type's mapping doesn't have a raw Data Primary Key --- data/service/expression-data-mapping.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 289838909..ec0f29a7d 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -3504,8 +3504,10 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData /* If the object has been fetched, we should have the values in the snapshot, otherwise if they are natural primiary keys, we might be able to get them by mapping back + + Except: for objects that are stored embedded into another one, they don't need to have a primaryKey. */ - for(i=0, countI = rawDataPrimaryKeyCompiledExpressions.length; (i Date: Wed, 7 Jan 2026 14:14:02 -0800 Subject: [PATCH 24/25] - fix missing responseOperation variable declarations - better handle case where a read operation can't be mapped to a request --- data/service/http-service.js | 213 ++++++++++++++++++----------------- 1 file changed, 112 insertions(+), 101 deletions(-) diff --git a/data/service/http-service.js b/data/service/http-service.js index 22351493d..d3e7efdf0 100644 --- a/data/service/http-service.js +++ b/data/service/http-service.js @@ -355,130 +355,141 @@ var HttpService = exports.HttpService = class HttpService extends RawDataService this.mapDataOperationToRawDataOperations(readOperation, readOperations) .then(() => { - for (let i = 0, iReadOperation; (iReadOperation = readOperations[i]); i++) { + if(readOperations?.length === 0) { + let responseOperation = this.responseOperationForReadOperation(readOperation.referrer ? readOperation.referrer : readOperation, null, []); - if (iReadOperation.type === DataOperation.Type.ReadCompletedOperation) { - console.log("\t" + this.identifier + " handleReadOperation dispatch mapped ReadCompletedOperation " + iReadOperation.id, " for " + iReadOperation.referrer.target.name + " like " + iReadOperation.referrer.criteria); + responseOperation.target.dispatchEvent(responseOperation); - iReadOperation.target.dispatchEvent(iReadOperation); + //Resolve once dispatchEvent() is completed, including any pending progagationPromise. + responseOperation.propagationPromise.then(() => { + readOperationCompletionPromiseResolve?.(responseOperation); + }); + } else { + for (let i = 0, iReadOperation; (iReadOperation = readOperations[i]); i++) { - iReadOperation.propagationPromise.then(() => { - readOperationCompletionPromiseResolve?.(iReadOperation); - }); + if (iReadOperation.type === DataOperation.Type.ReadCompletedOperation) { + console.log("\t" + this.identifier + " handleReadOperation dispatch mapped ReadCompletedOperation " + iReadOperation.id, " for " + iReadOperation.referrer.target.name + " like " + iReadOperation.referrer.criteria); - } else { + iReadOperation.target.dispatchEvent(iReadOperation); - let iMapping = this.mappingForObjectDescriptor(iReadOperation.target); - if (typeof iMapping.mapDataOperationToFetchRequests === "function") { - iMapping.mapDataOperationToFetchRequests(iReadOperation, fetchRequests); - - if (fetchRequests.length > 0) { - - for (let i = 0, iRequest; (iRequest = fetchRequests[i]); i++) { - console.debug(iRequest.url); - // console.debug("iRequest headers: ",iRequest.headers); - // console.debug("iRequest body: ",iRequest.body); - // .finally((value) => { - // console.debug("finally objectDescriptor.dispatchEvent("+responseOperation+");"); - // objectDescriptor.dispatchEvent(responseOperation); - // }) - this._fetchReadOperationRequest(iReadOperation, iRequest, readOperationCompletionPromiseResolve); - } - } else { - let criteriaParameters = readOperation?.criteria?.parameters, - qualifiedProperties = readOperation?.criteria?.qualifiedProperties, - rawDataPrimaryKeyProperties = mapping.rawDataPrimaryKeyProperties, - rawData = []; - - if (readOperation.data.readExpressions && readOperation.data.readExpressions.length > 0 && qualifiedProperties?.length == 1) { - /* - The test obove is for a query initiated by mod's data-triggers to resolve values of one object at a time and that qualifiedProperties only is the primary key. - So far mod supports a single primary key, that can be an object. - - We should more carefully use the syntax to match the property to it's value - */ - // let readExpressions = readOperation.data.readExpressions, - // rawDataObject = {}; - - // rawData.push(rawDataObject); - // //Set the primary key: - // rawDataObject[qualifiedProperties[0]] = criteriaParameters; - - // console.once.warn("No Mapping found for readOperation on "+ readOperation.target.name+ " for "+ readExpressions); - - // //console.warn("No Mapping found for readOperation on "+ readOperation.target.name+ " for "+ readExpressions+" and criteria: ",readOperation.criteria); - // for(let i = 0, countI = readExpressions.length, iReadExpression, iPropertyDescriptor; (i < countI); i++ ) { - // iReadExpression = readExpressions[i] - // iPropertyDescriptor = objectDescriptor.propertyDescriptorNamed(iReadExpression); - // rawDataObject[iReadExpression] = iPropertyDescriptor.defaultValue || iPropertyDescriptor.defaultFalsyValue; - // } - - responseOperation = this.responseOperationForReadOperation(readOperation.referrer ? readOperation.referrer : readOperation, null, rawData); - console.log("\t" + this.identifier + " handleReadOperation dispatch A responseOperation " + responseOperation.id, " for " + responseOperation.referrer.target.name + " like " + responseOperation.referrer.criteria); - - responseOperation.target.dispatchEvent(responseOperation); - - //Resolve once dispatchEvent() is completed, including any pending progagationPromise. - responseOperation.propagationPromise.then(() => { - readOperationCompletionPromiseResolve?.(responseOperation); - }); + iReadOperation.propagationPromise.then(() => { + readOperationCompletionPromiseResolve?.(iReadOperation); + }); - } else { - let error = new Error("No Mapping found " + readOperation.target.name + " " + readOperation.data.readExpressions); + } else { - console.once.error(error.message); - if (readOperation.clientId) { - error.stack = null; + let iMapping = this.mappingForObjectDescriptor(iReadOperation.target); + if (typeof iMapping.mapDataOperationToFetchRequests === "function") { + iMapping.mapDataOperationToFetchRequests(iReadOperation, fetchRequests); + + if (fetchRequests.length > 0) { + + for (let i = 0, iRequest; (iRequest = fetchRequests[i]); i++) { + console.debug(iRequest.url); + // console.debug("iRequest headers: ",iRequest.headers); + // console.debug("iRequest body: ",iRequest.body); + // .finally((value) => { + // console.debug("finally objectDescriptor.dispatchEvent("+responseOperation+");"); + // objectDescriptor.dispatchEvent(responseOperation); + // }) + this._fetchReadOperationRequest(iReadOperation, iRequest, readOperationCompletionPromiseResolve); } - // responseOperation = this.responseOperationForReadOperation(readOperation.referrer ? readOperation.referrer : readOperation, error, null); - //Send an empty response instead - responseOperation = this.responseOperationForReadOperation(readOperation.referrer ? readOperation.referrer : readOperation, null, []); - console.log("\t" + this.identifier + " handleReadOperation dispatch B responseOperation " + responseOperation.id, " for " + responseOperation.referrer.target.name + " like " + responseOperation.referrer.criteria); - - responseOperation.target.dispatchEvent(responseOperation); + } else { + let criteriaParameters = readOperation?.criteria?.parameters, + qualifiedProperties = readOperation?.criteria?.qualifiedProperties, + rawDataPrimaryKeyProperties = mapping.rawDataPrimaryKeyProperties, + rawData = []; + + if (readOperation.data.readExpressions && readOperation.data.readExpressions.length > 0 && qualifiedProperties?.length == 1) { + /* + The test obove is for a query initiated by mod's data-triggers to resolve values of one object at a time and that qualifiedProperties only is the primary key. + So far mod supports a single primary key, that can be an object. + + We should more carefully use the syntax to match the property to it's value + */ + // let readExpressions = readOperation.data.readExpressions, + // rawDataObject = {}; + + // rawData.push(rawDataObject); + // //Set the primary key: + // rawDataObject[qualifiedProperties[0]] = criteriaParameters; + + // console.once.warn("No Mapping found for readOperation on "+ readOperation.target.name+ " for "+ readExpressions); + + // //console.warn("No Mapping found for readOperation on "+ readOperation.target.name+ " for "+ readExpressions+" and criteria: ",readOperation.criteria); + // for(let i = 0, countI = readExpressions.length, iReadExpression, iPropertyDescriptor; (i < countI); i++ ) { + // iReadExpression = readExpressions[i] + // iPropertyDescriptor = objectDescriptor.propertyDescriptorNamed(iReadExpression); + // rawDataObject[iReadExpression] = iPropertyDescriptor.defaultValue || iPropertyDescriptor.defaultFalsyValue; + // } + + let responseOperation = this.responseOperationForReadOperation(readOperation.referrer ? readOperation.referrer : readOperation, null, rawData); + console.log("\t" + this.identifier + " handleReadOperation dispatch A responseOperation " + responseOperation.id, " for " + responseOperation.referrer.target.name + " like " + responseOperation.referrer.criteria); + + responseOperation.target.dispatchEvent(responseOperation); + + //Resolve once dispatchEvent() is completed, including any pending progagationPromise. + responseOperation.propagationPromise.then(() => { + readOperationCompletionPromiseResolve?.(responseOperation); + }); + + } else { + let error = new Error("No Mapping found " + readOperation.target.name + " " + readOperation.data.readExpressions); + + console.once.error(error.message); + if (readOperation.clientId) { + error.stack = null; + } + // responseOperation = this.responseOperationForReadOperation(readOperation.referrer ? readOperation.referrer : readOperation, error, null); + //Send an empty response instead + let responseOperation = this.responseOperationForReadOperation(readOperation.referrer ? readOperation.referrer : readOperation, null, []); + console.log("\t" + this.identifier + " handleReadOperation dispatch B responseOperation " + responseOperation.id, " for " + responseOperation.referrer.target.name + " like " + responseOperation.referrer.criteria); + + responseOperation.target.dispatchEvent(responseOperation); + + //Resolve once dispatchEvent() is completed, including any pending progagationPromise. + responseOperation.propagationPromise.then(() => { + readOperationCompletionPromiseResolve?.(responseOperation); + }); - //Resolve once dispatchEvent() is completed, including any pending progagationPromise. - responseOperation.propagationPromise.then(() => { - readOperationCompletionPromiseResolve?.(responseOperation); - }); + } } } + else { + + console.warn(this.name + ": No Rule found to map a read operation for " + iReadOperation.target.name + " to a fetchRequest"); + readOperationCompletionPromiseResolve(); + /* + Benoit 11/13/2025 commented it as this is it an error: some RawDataService can map raw data to objects that may be nested in + a read for another type, but don't actually have an API to get it on its own. + + We need to eventually need to introduce that semantic more clearly, but the recent mapping from operation to fetchRequests is + a step in that direction. + */ + // let error = new Error(this.name+": No Mapping for "+ iReadOperation.target.name+ " lacks mapDataOperationToFetchRequests(readOperation, fetchRequests) method"); + // responseOperation = this.responseOperationForReadOperation(iReadOperation.referrer ? iReadOperation.referrer : iReadOperation, error, null); + // console.log("\t"+this.identifier+" handleReadOperation dispatch C responseOperation " + responseOperation.id, " for "+responseOperation.referrer.target.name+ " like "+ responseOperation.referrer.criteria); + + // responseOperation.target.dispatchEvent(responseOperation); + + // //Resolve once dispatchEvent() is completed, including any pending progagationPromise. + // responseOperation.propagationPromise.then(() => { + // readOperationCompletionPromiseResolve?.(responseOperation); + // }); + } } - else { - - console.warn(this.name + ": No Rule found to map a read operation for " + iReadOperation.target.name + " to a fetchRequest"); - readOperationCompletionPromiseResolve(); - /* - Benoit 11/13/2025 commented it as this is it an error: some RawDataService can map raw data to objects that may be nested in - a read for another type, but don't actually have an API to get it on its own. - - We need to eventually need to introduce that semantic more clearly, but the recent mapping from operation to fetchRequests is - a step in that direction. - */ - // let error = new Error(this.name+": No Mapping for "+ iReadOperation.target.name+ " lacks mapDataOperationToFetchRequests(readOperation, fetchRequests) method"); - // responseOperation = this.responseOperationForReadOperation(iReadOperation.referrer ? iReadOperation.referrer : iReadOperation, error, null); - // console.log("\t"+this.identifier+" handleReadOperation dispatch C responseOperation " + responseOperation.id, " for "+responseOperation.referrer.target.name+ " like "+ responseOperation.referrer.criteria); - // responseOperation.target.dispatchEvent(responseOperation); - - // //Resolve once dispatchEvent() is completed, including any pending progagationPromise. - // responseOperation.propagationPromise.then(() => { - // readOperationCompletionPromiseResolve?.(responseOperation); - // }); - - } } - } }); }) .catch((error) => { - responseOperation = this.responseOperationForReadOperation(readOperation.referrer ? readOperation.referrer : readOperation, error, null); + let responseOperation = this.responseOperationForReadOperation(readOperation.referrer ? readOperation.referrer : readOperation, error, null); console.error(error); console.log("\t" + this.identifier + " handleReadOperation ERROR dispatch D responseOperation " + responseOperation.id, " for " + responseOperation.referrer.target.name + " like " + responseOperation.referrer.criteria); responseOperation.target.dispatchEvent(responseOperation); From 7c88e0fe796e20aa0fcd7552d21c92f6c7b24014 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 7 Jan 2026 14:15:10 -0800 Subject: [PATCH 25/25] refactor warning that no snapshot is found to be more specific --- data/service/raw-data-service.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 6285155a8..ec35580bf 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -1848,9 +1848,7 @@ RawDataService.addClassProperties({ if(!pendingSnapshot) { let snapshot = this.snapshotForDataIdentifier(dataIdentifier); - if(!snapshot) { - console.warn("pendingSnapshotForDataIdentifier: NO SNAPSHOT FOUND FOR "+dataIdentifier); - } else { + if(snapshot) { pendingSnapshot = Object.create(snapshot); this._pendingSnapshot.set(dataIdentifier, pendingSnapshot); } @@ -4648,6 +4646,9 @@ RawDataService.addClassProperties({ // } snapshot = this.pendingSnapshotForDataIdentifier(dataIdentifier); + if(!snapshot && !isNewObject) { + console.warn("pendingSnapshotForDataIdentifier: NO SNAPSHOT FOUND FOR "+dataIdentifier); + } if (localizableProperties && localizableProperties.size) { operation.locales = this.localesForObject(object)