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/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", 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..974fbfb62 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; @@ -69,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(); @@ -118,7 +119,7 @@ const DataService = exports.DataService = class DataService extends Target { }, }); } -}; +}); // DataService = exports.DataService = Target.specialize(/** @lends DataService.prototype */ { @@ -1315,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. @@ -1324,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); } @@ -2447,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. @@ -3090,7 +3089,7 @@ DataService.addClassProperties( service.dataIdentifierForNewObjectWithObjectDescriptor(this.objectDescriptorForType(type)) ); - this.registerCreatedDataObject(object); + this.registerCreatedDataObject(object); return object; } else { @@ -3903,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; } @@ -4446,6 +4449,10 @@ DataService.addClassProperties( }, }, + debouncedQueueMicrotaskWithDelay: { + value: queueMicrotask.debounceWithDelay(500), + }, + registerDataObjectChangesFromEvent: { value: function (changeEvent, shouldTrackChangesWhileBeingMapped) { var dataObject = changeEvent.target, @@ -4459,14 +4466,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; @@ -4517,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, @@ -4538,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); @@ -4704,7 +4724,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, @@ -4920,26 +4940,6 @@ 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. - * @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) - ); - } - }, - }, - _promisesByPendingTransactions: { value: undefined, }, @@ -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 @@ -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(); @@ -5370,27 +5384,120 @@ 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); + + // Dispatch invalid data event for each invalid object + this.dispatchDataEventTypeForObject( + DataEvent.invalid, + dataObject, + invalidity + ); + } + } } + + if (aggregatedValidationErrors.size > 0) { + 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) { + + //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 = self._pendingTransactions; + + if (pendingTransactions && pendingTransactions.length) { + + let updatedDataObjectsByObjectDescriptor = transaction.updatedDataObjects, //Map + updatedDataObjectDescriptorsIterator = updatedDataObjectsByObjectDescriptor.keys(), + firstPendingTransactionsCreatingChangedDataObjectsPromise, + pendingTransactionsCreatingChangedDataObjectsPromises, + iObjectDescriptor; + + /* + 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 + */ + + //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(!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, @@ -5399,13 +5506,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); @@ -5538,10 +5645,11 @@ DataService.addClassProperties( } }); - self.registerPendingTransactionPromise(transaction, pendingTransactionPromise); + this.registerPendingTransactionPromise(transaction, pendingTransactionPromise); return pendingTransactionPromise; - }, + + } }, _cancelTransaction: { @@ -6002,10 +6110,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; @@ -6362,7 +6470,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. diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index f0d7c94d6..ec0f29a7d 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: @@ -3494,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 { - 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); @@ -623,7 +634,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..ec35580bf 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); @@ -1839,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); } @@ -2918,7 +2925,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 ( @@ -3445,6 +3452,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, @@ -4628,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) @@ -4693,7 +4714,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; }); 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); + }); }); }); }); 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.