From 5adfdb487c4008293a005a302177ff30fcafedc5 Mon Sep 17 00:00:00 2001 From: Jelle De Loecker Date: Fri, 22 Mar 2024 16:53:37 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Use=20new=20`OperationalCo?= =?UTF-8?q?ntext`-based=20classes=20for=20handling=20`Datasource`=20operat?= =?UTF-8?q?ions,=20getting=20rid=20of=20callbacks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + lib/app/behaviour/publishable_behaviour.js | 4 +- lib/app/behaviour/revision_behaviour.js | 55 +- lib/app/behaviour/sluggable_behaviour.js | 16 +- lib/app/datasource/mongo_datasource.js | 604 +++++++++--------- .../10-datasource_operational_context.js | 153 +++++ .../read_operational_context.js | 196 ++++++ .../helper_datasource/remote_datasource.js | 75 ++- .../remove_operational_context.js | 10 + .../save_operational_context.js | 194 ++++++ lib/app/helper_field/11-date_field.js | 29 +- .../helper_field/15-local_temporal_field.js | 29 +- lib/app/helper_field/20-decimal_field.js | 27 +- lib/app/helper_field/big_int_field.js | 29 +- lib/app/helper_field/datetime_field.js | 15 +- lib/app/helper_field/mixed_field.js | 27 +- lib/app/helper_field/object_field.js | 13 +- lib/app/helper_field/password_field.js | 21 +- lib/app/helper_field/schema_field.js | 312 ++++----- lib/app/helper_field/settings_field.js | 12 +- lib/app/helper_model/model.js | 162 ++++- lib/class/behaviour.js | 26 +- lib/class/datasource.js | 312 +++------ lib/class/field.js | 176 +++-- lib/class/model.js | 46 +- lib/scripts/create_shared_constants.js | 12 +- test/03-model.js | 185 +++--- test/04-field.js | 21 +- test/06-document.js | 2 +- 29 files changed, 1674 insertions(+), 1090 deletions(-) create mode 100644 lib/app/helper_datasource/10-datasource_operational_context.js create mode 100644 lib/app/helper_datasource/read_operational_context.js create mode 100644 lib/app/helper_datasource/remove_operational_context.js create mode 100644 lib/app/helper_datasource/save_operational_context.js diff --git a/CHANGELOG.md b/CHANGELOG.md index ab8aedd8..1d320a3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ * Make `Datasource#toApp()` work without a callback * Print a warning when a stage takes over 10 seconds to finish * Add `OperationalContext` class for keeping track of complex operations +* Use new `OperationalContext`-based classes for handling `Datasource` operations, getting rid of callbacks ## 1.4.0-alpha.3 (2024-02-25) diff --git a/lib/app/behaviour/publishable_behaviour.js b/lib/app/behaviour/publishable_behaviour.js index a33fa78b..39e6412e 100644 --- a/lib/app/behaviour/publishable_behaviour.js +++ b/lib/app/behaviour/publishable_behaviour.js @@ -8,9 +8,7 @@ * @since 0.1.0 * @version 0.3.0 */ -var Publish = Function.inherits('Alchemy.Behaviour', function PublishableBehaviour(model, options) { - PublishableBehaviour.super.call(this, model, options); -}); +var Publish = Function.inherits('Alchemy.Behaviour', 'PublishableBehaviour'); /** * Listen to attachments to schema's diff --git a/lib/app/behaviour/revision_behaviour.js b/lib/app/behaviour/revision_behaviour.js index 0c9bf2cd..ab69510f 100644 --- a/lib/app/behaviour/revision_behaviour.js +++ b/lib/app/behaviour/revision_behaviour.js @@ -65,7 +65,7 @@ Revision.setStatic(function getRevisionModel(model) { this.schema.remove('updated'); this.addField('record_id', 'ObjectId'); - this.addField('revision', 'Number'); + this.addField('revision', 'Integer'); this.addField('delta', 'Object'); if (Classes.Alchemy.Model.User) { @@ -100,7 +100,7 @@ Revision.setStatic(function attached(schema, new_options) { const context = schema.model_class; // Add the revision number to the main model - context.addField('__r', 'Number', { + context.addField('__r', 'Integer', { title: 'Revision', }); @@ -217,30 +217,42 @@ Revision.setMethod(function beforeSave(record, options, creating) { return; } - let that = this, - next = this.wait('series'); + let that = this; - // Find the original record - Model.get(this.model.model_name).findById(record.$pk, async function gotRecord(err, result) { + let pledge = new Swift(); - if (result) { + // Find the original record + Model.get(this.model.model_name).findById(record.$pk, function gotRecord(err, result) { - // Get the original data - let ori = await that.model.convertRecordToDatasourceFormat(result); + if (err || !result) { + return pledge.resolve(); + } - // Store the original data in a weakmap for later - revision_before.set(options, ori); + let conversion; - // Increase the revision count by 1 - if (ori.__r) { - main.__r = ori.__r+1; - } else { - main.__r = 1; - } + try { + conversion = that.model.convertRecordToDatasourceFormat(result); + } catch (err) { + return pledge.reject(err); } - next(); + pledge.resolve(Swift.waterfall( + conversion, + ori => { + // Store the original data in a weakmap for later + revision_before.set(options, ori); + + // Increase the revision count by 1 + if (ori.__r) { + main.__r = ori.__r+1; + } else { + main.__r = 1; + } + } + )); }); + + return pledge; }); /** @@ -266,9 +278,10 @@ Revision.setMethod(function afterSave(record, options, created) { } let doc = this.model.createDocument(record); - let next = this.wait(); const that = this; + let pledge = new Swift(); + // Find the complete saved item Model.get(this.model.model_name).findByPk(doc.$pk, async function gotRecord(err, result) { @@ -312,6 +325,8 @@ Revision.setMethod(function afterSave(record, options, created) { } } - next(); + pledge.resolve(); }); + + return pledge; }); \ No newline at end of file diff --git a/lib/app/behaviour/sluggable_behaviour.js b/lib/app/behaviour/sluggable_behaviour.js index 93f9556a..0af6509b 100644 --- a/lib/app/behaviour/sluggable_behaviour.js +++ b/lib/app/behaviour/sluggable_behaviour.js @@ -153,8 +153,7 @@ Sluggable.setMethod(async function beforeSave(data, options, creating) { has_new_value, old_record, new_value, - old_value, - next; + old_value; // Get the actual record data if (data[that.model.name]) { @@ -170,9 +169,6 @@ Sluggable.setMethod(async function beforeSave(data, options, creating) { has_new_value = true; } - // Let other event callbacks wait for this one - next = this.wait('series'); - if (!creating) { old_record = await this.model.findById(data._id); @@ -186,7 +182,7 @@ Sluggable.setMethod(async function beforeSave(data, options, creating) { // and we're not explicitly setting a new slug value, // then do nothing if (!creating && old_value && !has_new_value) { - return next(); + return; } let new_data = {}; @@ -198,19 +194,23 @@ Sluggable.setMethod(async function beforeSave(data, options, creating) { Object.assign(new_data, data); } + let pledge = new Swift(); + // Try creating a new slug that.createSlug(new_data, new_value, function createdSlug(err, result) { if (err) { - return next(err); + return pledge.reject(err); } if (!Object.isEmpty(result)) { data[that.target_field.name] = result; } - next(); + pledge.resolve(); }); + + return pledge; }); /** diff --git a/lib/app/datasource/mongo_datasource.js b/lib/app/datasource/mongo_datasource.js index e3570f02..1dd959b1 100644 --- a/lib/app/datasource/mongo_datasource.js +++ b/lib/app/datasource/mongo_datasource.js @@ -1,6 +1,10 @@ const mongo = alchemy.use('mongodb'), MongoClient = mongo.MongoClient; +const CONNECTION = Symbol('connection'), + CONNECTION_ERROR = Symbol('connection_error'), + MONGO_CLIENT = Symbol('mongo_client'); + /** * MongoDb Datasource * @@ -13,8 +17,14 @@ const Mongo = Function.inherits('Alchemy.Datasource.Nosql', function Mongo(name, var options, uri; - // Caught connection err - this.connection_error = null; + // Possible connection error + this[CONNECTION_ERROR] = null; + + // The actual DB connection + this[CONNECTION] = null; + + // The MongoDB Client instance + this[MONGO_CLIENT] = null; // Define default options this.options = { @@ -231,38 +241,41 @@ Mongo.setMethod(function normalizeFindOptions(options) { * * @author Jelle De Loecker * @since 0.2.0 - * @version 1.3.16 + * @version 1.4.0 + * + * @param {Pledge} */ -Mongo.decorateMethod(Blast.Decorators.memoize({ignore_arguments: true}), function connect() { +Mongo.setMethod(function connect() { - if (this.connection) { - return Pledge.resolve(this.connection); + if (this[CONNECTION]) { + return this[CONNECTION]; } - if (this.connection_error) { - return Pledge.reject(this.connection_error); + if (this[CONNECTION_ERROR]) { + throw this[CONNECTION_ERROR]; } - let that = this, - pledge = new Pledge(); + let pledge = this[CONNECTION] = new Swift(); // Create the connection to the database - Pledge.done(MongoClient.connect(that.uri, that.mongoOptions), function connected(err, client) { + Pledge.done(MongoClient.connect(this.uri, this.mongoOptions), (err, client) => { + + this[CONNECTION] = null; if (err) { - that.connection_error = err; + this[CONNECTION_ERROR] = err; alchemy.printLog(alchemy.SEVERE, 'Could not create connection to Mongo server', {err: err}); return pledge.reject(err); } else { - log.info('Created connection to Mongo datasource', that.name); + log.info('Created connection to Mongo datasource', this.name); } if (client) { - that.mongo_client = client; - that.connection = client.db(); + this[MONGO_CLIENT] = client; + this[CONNECTION] = client.db(); } - pledge.resolve(client); + pledge.resolve(this[CONNECTION]); }); return pledge; @@ -273,47 +286,32 @@ Mongo.decorateMethod(Blast.Decorators.memoize({ignore_arguments: true}), functio * * @author Jelle De Loecker * @since 0.2.0 - * @version 1.3.16 + * @version 1.4.0 * * @param {string} name - * @param {Function} callback * - * @return {Pledge} + * @return {Pledge|Collection} */ -Mongo.setMethod(function collection(name, callback) { - - const pledge = new Pledge(); - - pledge.done(callback); +Mongo.setMethod(function collection(name) { if (this.collections[name]) { - pledge.resolve(this.collections[name]); - return pledge; + return this.collections[name]; } - const that = this; - - this.connect().done(function gotConnection(err, db) { - - if (err) { - return pledge.reject(err); + let result = Swift.waterfall( + this.connect(), + db => db.collection(name), + collection => { + this.collections[name] = collection; + return collection; } + ); - Pledge.done(that.connection.collection(name), function createdCollection(err, collection) { - - if (err) { - return pledge.reject(err); - } - - // Cache the collection - that.collections[name] = collection; - - // Return it to the callback - pledge.resolve(collection); - }); - }); + if (!this.collections[name]) { + this.collections[name] = result; + } - return pledge; + return result; }); /** @@ -321,144 +319,153 @@ Mongo.setMethod(function collection(name, callback) { * * @author Jelle De Loecker * @since 0.2.0 - * @version 1.3.16 + * @version 1.4.0 + * + * @param {Alchemy.OperationalContext.ReadDocumentFromDatasource} context + * + * @return {Pledge} */ -Mongo.setMethod(function _read(model, criteria, callback) { - - var that = this; - - this.collection(model.table, async function gotCollection(err, collection) { +Mongo.setMethod(function _read(context) { - if (err != null) { - return callback(err); - } - - let compiled, - options; - - await criteria.normalize(); - - compiled = await that.compileCriteria(criteria); - options = that.compileCriteriaOptions(criteria); - - if (compiled.pipeline) { - - // Sorting should happen in the pipeline - if (options.sort && options.sort.length) { - let sort_object = {}; - - for (let entry of options.sort) { - sort_object[entry[0]] = entry[1]; - } - - compiled.pipeline.unshift({$sort: sort_object}); - } - - // Skipping also happens in the pipeline - if (options.skip) { - compiled.pipeline.push({$skip: options.skip}); - } - - let aggregate_options = {}; - - Function.parallel({ - available: function getAvailable(next) { + const that = this; - if (criteria.options.available === false) { - return next(null, null); + const model = context.getModel(), + criteria = context.getCriteria(); + + return Swift.waterfall( + this.collection(model.table), + async collection => { + + let compiled, + options, + pledge = new Swift(); + + await criteria.normalize(); + + compiled = await that.compileCriteria(criteria); + options = that.compileCriteriaOptions(criteria); + + if (compiled.pipeline) { + + // Sorting should happen in the pipeline + if (options.sort && options.sort.length) { + let sort_object = {}; + + for (let entry of options.sort) { + sort_object[entry[0]] = entry[1]; } - - let pipeline = JSON.clone(compiled.pipeline), - cloned_options = JSON.clone(aggregate_options); - - pipeline.push({$count: 'available'}); - - // Expensive aggregate just to get the available count... - Pledge.done(collection.aggregate(pipeline, cloned_options), function gotAggregate(err, cursor) { - - if (err) { - return next(err); + + compiled.pipeline.unshift({$sort: sort_object}); + } + + // Skipping also happens in the pipeline + if (options.skip) { + compiled.pipeline.push({$skip: options.skip}); + } + + let aggregate_options = {}; + + Function.parallel({ + available: function getAvailable(next) { + + if (criteria.options.available === false) { + return next(null, null); } - - Pledge.done(cursor.toArray(), function gotAvailableArray(err, items) { - + + let pipeline = JSON.clone(compiled.pipeline), + cloned_options = JSON.clone(aggregate_options); + + pipeline.push({$count: 'available'}); + + // Expensive aggregate just to get the available count... + Pledge.done(collection.aggregate(pipeline, cloned_options), function gotAggregate(err, cursor) { + if (err) { return next(err); } - - if (!items || !items.length) { - return next(null, null); - } - - let available = items[0].available; - - if (options.skip) { - available += options.skip; + + Pledge.done(cursor.toArray(), function gotAvailableArray(err, items) { + + if (err) { + return next(err); + } + + if (!items || !items.length) { + return next(null, null); + } + + let available = items[0].available; + + if (options.skip) { + available += options.skip; + } + + return next(null, available); + }); + }); + }, + rows: function getRows(next) { + + let pipeline = JSON.clone(compiled.pipeline); + + // Limits also have to be set in the pipeline now + // (We have to do it here, so the `available` count is correct) + if (options.limit) { + pipeline.push({$limit: options.limit}); + } + + Pledge.done(collection.aggregate(pipeline, aggregate_options), function gotAggregate(err, cursor) { + + if (err) { + return next(err); } - - return next(null, available); + + Pledge.done(cursor.toArray(), next); }); - }); - }, - items: function getItems(next) { - - let pipeline = JSON.clone(compiled.pipeline); - - // Limits also have to be set in the pipeline now - // (We have to do it here, so the `available` count is correct) - if (options.limit) { - pipeline.push({$limit: options.limit}); } - - Pledge.done(collection.aggregate(pipeline, aggregate_options), function gotAggregate(err, cursor) { - - if (err) { - return next(err); - } - - Pledge.done(cursor.toArray(), next); - }); + }, function done(err, data) { + + if (err) { + return pledge.reject(err); + } + + data.rows = that.organizeResultItems(model, data.rows); + + pledge.resolve(data); + }); + + return pledge; + } + + // Create the cursor + let cursor = collection.find(compiled, options); + + Function.parallel({ + available: function getAvailable(next) { + + if (criteria.options.available === false) { + return next(null, null); + } + + Pledge.done(collection.countDocuments(compiled), next); + }, + rows: function getRows(next) { + Pledge.done(cursor.toArray(), next); } }, function done(err, data) { - + if (err) { return callback(err); } - - data.items = that.organizeResultItems(model, data.items); - - callback(err, data.items, data.available); + + data.rows = that.organizeResultItems(model, data.rows); + + pledge.resolve(data); }); - return; + return pledge; } - - // Create the cursor - let cursor = collection.find(compiled, options); - - Function.parallel({ - available: function getAvailable(next) { - - if (criteria.options.available === false) { - return next(null, null); - } - - Pledge.done(collection.countDocuments(compiled), next); - }, - items: function getItems(next) { - Pledge.done(cursor.toArray(), next); - } - }, function done(err, data) { - - if (err) { - return callback(err); - } - - data.items = that.organizeResultItems(model, data.items); - - callback(err, data.items, data.available); - }); - }); + ); }); /** @@ -466,49 +473,59 @@ Mongo.setMethod(function _read(model, criteria, callback) { * * @author Jelle De Loecker * @since 0.2.0 - * @version 1.3.16 + * @version 1.4.0 + * + * @param {Alchemy.OperationalContext.SaveToDatasource} context + * + * @return {Pledge} */ -Mongo.setMethod(function _create(model, data, options, callback) { +Mongo.setMethod(function _create(context) { - this.collection(model.table, function gotCollection(err, collection) { + const model = context.getModel(); - if (err != null) { - return callback(err); - } + return Swift.waterfall( + this.collection(model.table), + collection => { - Pledge.done(collection.insertOne(data, {w: 1, fullResult: true}), function afterInsert(err, result) { + const data = context.getConvertedData(); + const pledge = new Swift(); - // Clear the cache - model.nukeCache(); + Pledge.done(collection.insertOne(data, {w: 1, fullResult: true}), function afterInsert(err, result) { - if (err != null) { - return callback(err, result); - } + // Clear the cache + model.nukeCache(); + + if (err != null) { + return pledge.reject(err); + } - // @TODO: fix because of mongodb 6 - let write_errors = result.message?.documents?.[0]?.writeErrors; + // @TODO: fix because of mongodb 6 + let write_errors = result.message?.documents?.[0]?.writeErrors; - if (write_errors) { - let violations = new Classes.Alchemy.Error.Validation.Violations(); + if (write_errors) { + let violations = new Classes.Alchemy.Error.Validation.Violations(); - if (write_errors.length) { - let entry; + if (write_errors.length) { + let entry; - for (entry of write_errors) { - let violation = new Classes.Alchemy.Error.Validation.Violation(); - violation.message = entry.errmsg || entry.message || entry.code; - violations.add(violation); + for (entry of write_errors) { + let violation = new Classes.Alchemy.Error.Validation.Violation(); + violation.message = entry.errmsg || entry.message || entry.code; + violations.add(violation); + } + } else { + violations.add(new Error('Unknown database error')); } - } else { - violations.add(new Error('Unknown database error')); + + return pledge.reject(violations); } - return callback(violations); - } + pledge.resolve(Object.assign({}, data)); + }); - callback(null, Object.assign({}, data)); - }); - }); + return pledge; + } + ); }); /** @@ -516,112 +533,117 @@ Mongo.setMethod(function _create(model, data, options, callback) { * * @author Jelle De Loecker * @since 0.2.0 - * @version 1.3.16 + * @version 1.4.0 + * + * @param {Alchemy.OperationalContext.SaveToDatasource} context + * + * @return {Pledge} */ -Mongo.setMethod(function _update(model, data, options, callback) { +Mongo.setMethod(function _update(context) { - this.collection(model.table, function gotCollection(err, collection) { + const model = context.getModel(), + options = context.getSaveOptions(); - var updateObject, - no_flatten, - to_flatten, - unset, - field, - flat, - doc, - key, - id; + return Swift.waterfall( + this.collection(model.table), + collection => { - if (err != null) { - return callback(err); - } + let key; - // Get the id to update, it should always be inside the given data - id = data._id; + // Get the converted data + const data = context.getConvertedData(); - // Clone the data object - doc = Object.assign({}, data); + // Get the id to update, it should always be inside the given data + let id = data._id; - // Values that will get flattened - to_flatten = {}; + // Clone the data object + let doc = {...data}; - // Field names that won't get flattened - no_flatten = {}; + // Values that will get flattened + let to_flatten = {}; - // Remove fields that should not be updated - delete doc._id; + // Field names that won't get flattened + let no_flatten = {}; - if (!options.override_created) { - delete doc.created; - } + // Remove the id + delete doc._id; + + if (!options.override_created) { + delete doc.created; + } - // Iterate over the fields - for (key in doc) { - field = model.getField(key); + // Iterate over the fields + for (key in doc) { + let field = model.getField(key); - if (field && (field.is_self_contained || field.is_translatable)) { - no_flatten[key] = doc[key]; - } else { - to_flatten[key] = doc[key]; + if (field && (field.is_self_contained || field.is_translatable)) { + no_flatten[key] = doc[key]; + } else { + to_flatten[key] = doc[key]; + } } - } - // Flatten the object, using periods & NOT flattening arrays - flat = Object.flatten(to_flatten, '.', false); + // Flatten the object, using periods & NOT flattening arrays + let flat = Object.flatten(to_flatten, '.', false); - // Assign the no-flatten values, too - Object.assign(flat, no_flatten); + // Assign the no-flatten values, too + Object.assign(flat, no_flatten); - unset = {}; + let unset = {}; - for (key in flat) { - // Undefined or null means we want to delete the value - // We can't set null, because that could interfere with dot notation updates - if (flat[key] == null) { + for (key in flat) { + // Undefined or null means we want to delete the value + // We can't set null, because that could interfere with dot notation updates + if (flat[key] == null) { - // Add the key to the unset object - unset[key] = ''; + // Add the key to the unset object + unset[key] = ''; - // Remove it from the flat object - delete flat[key]; + // Remove it from the flat object + delete flat[key]; + } } - } - updateObject = { - $set: flat - }; + let update_object = { + $set: flat + }; - if (!Object.isEmpty(unset)) { - updateObject.$unset = unset; - } + if (!Object.isEmpty(unset)) { + update_object.$unset = unset; + } - if (options.debug) { - console.log('Updating with obj', id, updateObject); - } + if (options.debug) { + console.log('Updating with obj', id, update_object); + } - let promise; + let promise; - if (collection.findOneAndUpdate) { - promise = collection.findOneAndUpdate({_id: id}, updateObject, {upsert: true}); - } else if (collection.findAndModify) { - promise = collection.findAndModify({_id: id}, [['_id', 1]], updateObject, {upsert: true}); - } else { - // If it's not available (like nedb) - promise = collection.update({_id: ''+id}, updateObject, {upsert: true}); - } + if (collection.findOneAndUpdate) { + promise = collection.findOneAndUpdate({_id: id}, update_object, {upsert: true}); + } else if (collection.findAndModify) { + promise = collection.findAndModify({_id: id}, [['_id', 1]], update_object, {upsert: true}); + } else { + // If it's not available (like nedb) + promise = collection.update({_id: ''+id}, update_object, {upsert: true}); + } - Pledge.done(promise, function afterUpdate(err, result) { + let pledge = new Swift(); - // Clear the cache - model.nukeCache(); + Pledge.done(promise, function afterUpdate(err, result) { - if (err != null) { - return callback(err, result); - } + // Clear the cache + model.nukeCache(); - callback(null, Object.assign({}, data)); - }); - }); + if (err != null) { + return pledge.reject(err); + } + + pledge.resolve(Object.assign({}, data)); + }); + + return pledge; + } + ); }); /** @@ -630,28 +652,38 @@ Mongo.setMethod(function _update(model, data, options, callback) { * @author Kjell Keisse * @author Jelle De Loecker * @since 0.2.0 - * @version 1.3.16 + * @version 1.4.0 + * + * @param {Alchemy.OperationalContext.RemoveFromDatasource} context + * + * @return {Pledge} */ -Mongo.setMethod(function _remove(model, query, options, callback) { +Mongo.setMethod(function _remove(context) { - this.collection(model.table, function gotCollection(err, collection) { + const model = context.getModel(), + query = context.getQuery(); - if (err != null) { - return callback(err); - } + return Swift.waterfall( + this.collection(model.table), + collection => { - Pledge.done(collection.findOneAndDelete(query), function _deleted(err, result){ + let pledge = new Swift(); - //clear cache - model.nukeCache(); + Swift.done(collection.findOneAndDelete(query), function _deleted(err, result){ - if (err != null) { - return callback(err, result); - } + //clear cache + model.nukeCache(); + + if (err != null) { + return pledge.reject(err); + } + + pledge.resolve(!!result); + }); - callback(null, !!result); - }); - }); + return pledge; + } + ); }); /** diff --git a/lib/app/helper_datasource/10-datasource_operational_context.js b/lib/app/helper_datasource/10-datasource_operational_context.js new file mode 100644 index 00000000..6eaa02c2 --- /dev/null +++ b/lib/app/helper_datasource/10-datasource_operational_context.js @@ -0,0 +1,153 @@ +/** + * The base OperationalContext class for Datasource related operations + * + * @constructor + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +const DatasourceOperationalContext = Function.inherits('Alchemy.OperationalContext', 'DatasourceOperationalContext'); + +/** + * Make this an abtract class + */ +DatasourceOperationalContext.makeAbstractClass(); + +/** + * Set the datasource this is for + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Alchemy.Datasource} datasource + */ +DatasourceOperationalContext.setContextProperty('datasource'); + +/** + * Set the model of the data that is being saved + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {string|Model} model + */ +DatasourceOperationalContext.setContextProperty('model'); + +/** + * Set the original query (if any) + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {*} query + */ +DatasourceOperationalContext.setContextProperty('query'); + +/** + * Get the schema. + * No schema will return false, an invalid schema will throw. + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +DatasourceOperationalContext.setContextProperty('schema', function getSchema(schema) { + + if (schema || schema === false) { + return schema; + } + + let model = this.getModel(); + + if (model) { + schema = this.getDatasource().getSchema(model); + + if (Object.isPlainObject(schema)) { + throw new Error('The provided schema was a regular object'); + } + + this.setSchema(schema); + } + + if (!schema) { + if (alchemy.settings.debugging.debug) { + alchemy.distinctProblem('schema-not-found', 'Schema not found: not normalizing data'); + } + } + + return schema; +}); + +/** + * Set the root data that is being saved + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Object} data + */ +DatasourceOperationalContext.setContextProperty('root_data'); + +/** + * Get the query options + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Object} data + */ +DatasourceOperationalContext.setContextProperty('query_options'); + +/** + * Get the current active holder. + * This is probbaly the root document, + * but can be a sub document in case of nested schemas. + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {*} datasource_value + */ +DatasourceOperationalContext.setContextProperty('holder', function getHolder(holder) { + + if (!holder) { + holder = this.getRootData(); + } + + return holder; +}); + +/** + * Get the current data in the process of being + * converted to the datasource format + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +DatasourceOperationalContext.setContextProperty('working_data', function getWorkingData(working_data) { + + if (!working_data) { + working_data = {...this.getRootData()}; + } + + return working_data; +}); + +/** + * Is there a valid schema? + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +DatasourceOperationalContext.setMethod(function hasValidSchema() { + return !!this.getSchema(); +}); \ No newline at end of file diff --git a/lib/app/helper_datasource/read_operational_context.js b/lib/app/helper_datasource/read_operational_context.js new file mode 100644 index 00000000..73272d91 --- /dev/null +++ b/lib/app/helper_datasource/read_operational_context.js @@ -0,0 +1,196 @@ +/** + * The Read OperationalContext class + * + * @constructor + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +const ReadDocumentFromDatasource = Function.inherits('Alchemy.OperationalContext.DatasourceOperationalContext', 'ReadDocumentFromDatasource'); + +/** + * Set the root data that is being converted + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Object} data + */ +ReadDocumentFromDatasource.setContextProperty('root_data'); + +/** + * Set the criteria instance + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Alchemy.Criteria} criteria + */ +ReadDocumentFromDatasource.setContextProperty('criteria'); + +/** + * Create a new instance for the given document data + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @return {Alchemy.OperationalContext.ReadDocumentFromDatasource|null} + */ +ReadDocumentFromDatasource.setMethod(function withDatasourceEntry(entry) { + + let result = this.createChild(); + result.setRootData(entry); + + return result; +}); + +/** + * Get a field-specific context instance + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @return {Alchemy.OperationalContext.ReadFieldFromDatasource|null} + */ +ReadDocumentFromDatasource.setMethod(function getFieldContext(field_name) { + + let field = this.getSchema().getField(field_name); + + if (!field) { + return; + } + + let context = new ReadFieldFromDatasource(this); + context.setField(field); + + let field_value = this.getWorkingData()[field_name]; + context.setFieldValue(field_value); + + return context; +}); + +/** + * The field-specific read operational class + * + * @constructor + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +const ReadFieldFromDatasource = Function.inherits('Alchemy.OperationalContext.ReadDocumentFromDatasource', 'ReadFieldFromDatasource'); + +/** + * Set the field + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Alchemy.Field} field + */ +ReadFieldFromDatasource.setContextProperty('field'); + +/** + * Set the original field value. + * This might be a value wrapped in an object (for translations) + * or in an array (for arrayable fields) + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {*} field_value + */ +ReadFieldFromDatasource.setContextProperty('field_value'); + +/** + * Set the custom holder of this value. + * This is probbaly the root document, + * but can be a sub document in case of nested schemas + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {*} datasource_value + */ +ReadFieldFromDatasource.setContextProperty('holder', function getHolder(holder) { + + if (!holder) { + holder = this.getRootData(); + } + + return holder; +}); + +/** + * The current working field value + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {*} working_value + */ +ReadFieldFromDatasource.setContextProperty('working_value', function getWorkingValue(value) { + + if (value === undefined) { + value = this.getFieldValue(); + } + + return value; +}); + +/** + * Get this with the given working value + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {*} value + * + * @return {Alchemy.OperationalContext.SaveFieldToDatasource} + */ +ReadFieldFromDatasource.setMethod(function withWorkingValue(value, holder) { + + let context = new ReadFieldFromDatasource(this); + context.setWorkingValue(value); + + if (holder) { + context.setHolder(holder); + } + + return context; +}); + +/** + * With data for a subschema + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {*} value + * + * @return {Alchemy.OperationalContext.SaveFieldToDatasource} + */ +ReadFieldFromDatasource.setMethod(function withValueOfSubSchema(value, sub_schema) { + + // The value becomes the holder + let context = this.withWorkingValue(null, value); + + // The sub_schema becomes the schema + context.setSchema(sub_schema); + + // And the holder also becomes the working data + context.setWorkingData(value); + + return context; +}); \ No newline at end of file diff --git a/lib/app/helper_datasource/remote_datasource.js b/lib/app/helper_datasource/remote_datasource.js index 89e97b07..199c7a88 100644 --- a/lib/app/helper_datasource/remote_datasource.js +++ b/lib/app/helper_datasource/remote_datasource.js @@ -104,19 +104,24 @@ Remote.setMethod(async function doServerCommand(action, model, data, callback) { * * @author Jelle De Loecker * @since 1.1.0 - * @version 1.1.0 + * @version 1.4.0 + * + * @param {Alchemy.OperationalContext.ReadDocumentFromDatasource} context + * + * @return {Pledge} */ -Remote.setMethod(function read(model, criteria, callback) { +Remote.setMethod(function read(context) { - var that = this, - data = { - criteria : criteria + let criteria = context.getCriteria(), + model = context.getModel(); + + let data = { + criteria : criteria, }; - // Add cache? - // {max_age: 10000, ignore_callbacks: true, static: true, cache_key: 'cache'} + let pledge = new Swift(); - return this.doServerCommand('readDatasource', model, data, function done(err, items, available) { + this.doServerCommand('readDatasource', model, data, function done(err, items, available) { if (err) { if (err.status == 502 || err.status == 408) { @@ -124,14 +129,16 @@ Remote.setMethod(function read(model, criteria, callback) { //return that.datasource.read('records', conditions, options, callback); } - return callback(err); + return pledge.reject(err); } - callback(null, { + pledge.resolve({ items : items, available : available }); }); + + return pledge; }); /** @@ -139,18 +146,26 @@ Remote.setMethod(function read(model, criteria, callback) { * * @author Jelle De Loecker * @since 1.1.0 - * @version 1.1.0 + * @version 1.4.0 + * + * @param {Alchemy.OperationalContext.SaveToDatasource} context + * + * @return {Pledge} */ -Remote.setMethod(function create(model, data, options, callback) { +Remote.setMethod(function create(context) { - var that = this; + let model = context.getModel(), + data = context.getRootData(), + options = context.getSaveOptions(); data = { data : data, - options : options + options : options, }; - return this.doServerCommand('saveRecord', model, data, function done(err, record) { + let pledge = new Swift(); + + this.doServerCommand('saveRecord', model, data, function done(err, record) { if (err) { if (err.status == 502 || err.status == 408) { @@ -158,11 +173,13 @@ Remote.setMethod(function create(model, data, options, callback) { //return that.datasource.read('records', conditions, options, callback); } - return callback(err); + return pledge.reject(err); } - callback(null, record); + pledge.resolve(record); }); + + return pledge; }); /** @@ -170,11 +187,17 @@ Remote.setMethod(function create(model, data, options, callback) { * * @author Jelle De Loecker * @since 1.1.0 - * @version 1.1.0 + * @version 1.4.0 + * + * @param {Alchemy.OperationalContext.SaveToDatasource} context + * + * @return {Pledge} */ -Remote.setMethod(function update(model, data, options, callback) { +Remote.setMethod(function update(context) { - var that = this; + let model = context.getModel(), + data = context.getRootData(), + options = context.getSaveOptions(); if (!options) { options = {}; @@ -184,10 +207,12 @@ Remote.setMethod(function update(model, data, options, callback) { data = { data : data, - options : options + options : options, }; - return this.doServerCommand('saveRecord', model, data, function done(err, record) { + let pledge = new Swift(); + + this.doServerCommand('saveRecord', model, data, function done(err, record) { if (err) { if (err.status == 502 || err.status == 408) { @@ -195,9 +220,11 @@ Remote.setMethod(function update(model, data, options, callback) { //return that.datasource.read('records', conditions, options, callback); } - return callback(err); + return pledge.reject(err); } - callback(null, record); + pledge.resolve(record); }); + + return pledge; }); \ No newline at end of file diff --git a/lib/app/helper_datasource/remove_operational_context.js b/lib/app/helper_datasource/remove_operational_context.js new file mode 100644 index 00000000..5aba68d5 --- /dev/null +++ b/lib/app/helper_datasource/remove_operational_context.js @@ -0,0 +1,10 @@ +/** + * The Remove OperationalContext class + * + * @constructor + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +const RemoveFromDatasource = Function.inherits('Alchemy.OperationalContext.SaveDocumentToDatasource', 'RemoveFromDatasource'); diff --git a/lib/app/helper_datasource/save_operational_context.js b/lib/app/helper_datasource/save_operational_context.js new file mode 100644 index 00000000..5a8fcc33 --- /dev/null +++ b/lib/app/helper_datasource/save_operational_context.js @@ -0,0 +1,194 @@ +/** + * The Save OperationalContext class + * + * @constructor + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +const SaveDocumentToDatasource = Function.inherits('Alchemy.OperationalContext.DatasourceOperationalContext', 'SaveDocumentToDatasource'); + +/** + * Set the converted data + * (The root data passed through all the `toDatasource` methods) + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Object} data + */ +SaveDocumentToDatasource.setContextProperty('converted_data'); + +/** + * Set the save options + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Object} options + */ +SaveDocumentToDatasource.setContextProperty('save_options'); + +/** + * Get a field-specific context instance + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @return {Alchemy.OperationalContext.SaveFieldToDatasource|null} + */ +SaveDocumentToDatasource.setMethod(function getFieldContext(field_name) { + + let field = this.getSchema().getField(field_name); + + if (!field) { + return; + } + + let context = new SaveFieldToDatasource(this); + context.setField(field); + + let field_value = this.getHolder()[field_name]; + context.setFieldValue(field_value); + + return context; +}); + +/** + * Get a read-to-app context. + * This is used for converting the saved data back to the app format. + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Object} data The datasource data + * + * @return {Alchemy.OperationalContext.ReadDocumentFromDatasource|null} + */ +SaveDocumentToDatasource.setMethod(function getReadFromDatasourceContext(data) { + + let context = new Classes.Alchemy.OperationalContext.ReadDocumentFromDatasource(); + context.setRootData(data); + context.setModel(this.getModel()); + context.setSchema(this.getSchema()); + context.setDatasource(this.getDatasource()); + + return context; +}); + +/** + * The field-specific SaveOperationalContext class + * + * @constructor + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +const SaveFieldToDatasource = Function.inherits('Alchemy.OperationalContext.SaveDocumentToDatasource', 'SaveFieldToDatasource'); + +/** + * Set the field + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Alchemy.Field} field + */ +SaveFieldToDatasource.setContextProperty('field'); + +/** + * Set the original field value. + * This might be a value wrapped in an object (for translations) + * or in an array (for arrayable fields) + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {*} field_value + */ +SaveFieldToDatasource.setContextProperty('field_value'); + +/** + * Set the final converted field value + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {*} datasource_value + */ +SaveFieldToDatasource.setContextProperty('datasource_value'); + +/** + * The current working field value + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {*} working_value + */ +SaveFieldToDatasource.setContextProperty('working_value', function getWorkingValue(value) { + + if (value === undefined) { + value = this.getFieldValue(); + } + + return value; +}); + +/** + * Get this with the given working value + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {*} value + * + * @return {Alchemy.OperationalContext.SaveFieldToDatasource} + */ +SaveFieldToDatasource.setMethod(function withWorkingValue(value, holder) { + + let context = this.createChild(); + context.setWorkingValue(value); + + if (holder) { + context.setHolder(holder); + } + + return context; +}); + +/** + * With subdata value + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {*} value + * + * @return {Alchemy.OperationalContext.SaveFieldToDatasource} + */ +SaveFieldToDatasource.setMethod(function withValueOfSubSchema(value, sub_schema) { + + // The value becomes the holder + let context = this.withWorkingValue(null, value); + + // The sub_schema becomes the schema + context.setSchema(sub_schema); + + // And the holder also becomes the working data + context.setWorkingData(value); + + return context; +}); \ No newline at end of file diff --git a/lib/app/helper_field/11-date_field.js b/lib/app/helper_field/11-date_field.js index cc4920da..18240626 100644 --- a/lib/app/helper_field/11-date_field.js +++ b/lib/app/helper_field/11-date_field.js @@ -59,15 +59,14 @@ DateField.setCastFunction(function cast(value) { * * @author Jelle De Loecker * @since 0.5.0 - * @version 0.5.0 + * @version 1.4.0 * - * @param {*} value The field's own value - * @param {Object} data The main record - * @param {Datasource} datasource The datasource instance + * @param {Alchemy.OperationalContext.SaveFieldToDatasource} context + * @param {*} value * - * @return {*} + * @return {Pledge<*>|*} */ -DateField.setMethod(function _toDatasource(value, data, datasource, callback) { +DateField.setMethod(function _toDatasource(context, value) { value = this.cast(value); @@ -93,9 +92,7 @@ DateField.setMethod(function _toDatasource(value, data, datasource, callback) { }; } - Blast.nextTick(function() { - callback(null, value); - }); + return value; }); /** @@ -103,20 +100,20 @@ DateField.setMethod(function _toDatasource(value, data, datasource, callback) { * * @author Jelle De Loecker * @since 1.1.0 - * @version 1.1.0 + * @version 1.4.0 * - * @param {Object} query The original query - * @param {Object} options The original query options - * @param {*} value The field value, as stored in the DB - * @param {Function} callback + * @param {Alchemy.OperationalContext.ReadFieldFromDatasource} context + * @param {*} value + * + * @return {Date} */ -DateField.setMethod(function _toApp(query, options, value, callback) { +DateField.setMethod(function _toApp(context, value) { if (this.options.second_format && value) { value *= this.options.second_format; } - return callback(null, this.cast(value, false)); + return this.cast(value, false); }); /** diff --git a/lib/app/helper_field/15-local_temporal_field.js b/lib/app/helper_field/15-local_temporal_field.js index 22e24abb..3dfbc14b 100644 --- a/lib/app/helper_field/15-local_temporal_field.js +++ b/lib/app/helper_field/15-local_temporal_field.js @@ -51,15 +51,14 @@ LocalTemporal.setMethod(function _castCondition(value, field_paths) { * * @author Jelle De Loecker * @since 1.3.20 - * @version 1.3.20 + * @version 1.4.0 * - * @param {*} value The field's own value - * @param {Object} data The main record - * @param {Datasource} datasource The datasource instance + * @param {Alchemy.OperationalContext.SaveFieldToDatasource} context + * @param {*} value * - * @return {*} + * @return {Develry.LocalDate} */ -LocalTemporal.setMethod(function _toDatasource(value, data, datasource, callback) { +LocalTemporal.setMethod(function _toDatasource(context, value) { value = this.cast(value); @@ -71,7 +70,7 @@ LocalTemporal.setMethod(function _toDatasource(value, data, datasource, callback value = this.datasource.convertBigIntForDatasource(value); } - Blast.nextTick(() => callback(null, value)); + return value; }); /** @@ -79,14 +78,14 @@ LocalTemporal.setMethod(function _toDatasource(value, data, datasource, callback * * @author Jelle De Loecker * @since 1.3.20 - * @version 1.3.20 + * @version 1.4.0 * - * @param {Object} query The original query - * @param {Object} options The original query options - * @param {*} value The field value, as stored in the DB - * @param {Function} callback + * @param {Alchemy.OperationalContext.ReadFieldFromDatasource} context + * @param {*} value + * + * @return {Develry.LocalDate} */ -LocalTemporal.setMethod(function _toApp(query, options, value, callback) { +LocalTemporal.setMethod(function _toApp(context, value) { if (value && typeof value == 'object') { if (typeof value.getUTCDate != 'function') { @@ -94,7 +93,5 @@ LocalTemporal.setMethod(function _toApp(query, options, value, callback) { } } - value = this.cast(value); - - callback(null, value); + return this.cast(value); }); \ No newline at end of file diff --git a/lib/app/helper_field/20-decimal_field.js b/lib/app/helper_field/20-decimal_field.js index 5684a283..16395789 100644 --- a/lib/app/helper_field/20-decimal_field.js +++ b/lib/app/helper_field/20-decimal_field.js @@ -66,18 +66,17 @@ DecimalField.setMethod(function _castCondition(value, field_paths) { * @since 1.3.20 * @version 1.3.20 * - * @param {Mixed} value The field's own value - * @param {Object} data The main record - * @param {Datasource} datasource The datasource instance + * @param {Alchemy.OperationalContext.SaveFieldToDatasource} context + * @param {*} value * - * @return {Mixed} + * @return {*} */ -DecimalField.setMethod(function _toDatasource(value, data, datasource, callback) { +DecimalField.setMethod(function _toDatasource(context, value) { value = this.cast(value); value = this.datasource.convertDecimalForDatasource(value); - Blast.nextTick(() => callback(null, value)); + return value; }); /** @@ -85,20 +84,18 @@ DecimalField.setMethod(function _toDatasource(value, data, datasource, callback) * * @author Jelle De Loecker * @since 1.3.20 - * @version 1.3.20 + * @version 1.4.0 * - * @param {Object} query The original query - * @param {Object} options The original query options - * @param {Mixed} value The field value, as stored in the DB - * @param {Function} callback + * @param {Alchemy.OperationalContext.ReadFieldFromDatasource} context + * @param {*} value + * + * @return {Develry.Decimal} */ -DecimalField.setMethod(function _toApp(query, options, value, callback) { +DecimalField.setMethod(function _toApp(context, value) { if (value && typeof value == 'object') { value = value.toString(); } - value = this.cast(value); - - callback(null, value); + return this.cast(value); }); \ No newline at end of file diff --git a/lib/app/helper_field/big_int_field.js b/lib/app/helper_field/big_int_field.js index 1ae90fe7..437eb4df 100644 --- a/lib/app/helper_field/big_int_field.js +++ b/lib/app/helper_field/big_int_field.js @@ -55,7 +55,7 @@ BigIntField.setCastFunction(function cast(value) { * @param {Mixed} value * @param {Array} field_paths The path to the field * - * @return {Mixed} + * @return {*} */ BigIntField.setMethod(function _castCondition(value, field_paths) { @@ -72,18 +72,17 @@ BigIntField.setMethod(function _castCondition(value, field_paths) { * @since 1.3.6 * @version 1.3.20 * - * @param {Mixed} value The field's own value - * @param {Object} data The main record - * @param {Datasource} datasource The datasource instance + * @param {Alchemy.OperationalContext.SaveFieldToDatasource} context + * @param {*} value * - * @return {Mixed} + * @return {BigInt} */ -BigIntField.setMethod(function _toDatasource(value, data, datasource, callback) { +BigIntField.setMethod(function _toDatasource(context, value) { value = this.cast(value); value = this.datasource.convertBigIntForDatasource(value); - callback(null, value); + return value; }); /** @@ -91,20 +90,18 @@ BigIntField.setMethod(function _toDatasource(value, data, datasource, callback) * * @author Jelle De Loecker * @since 1.3.6 - * @version 1.3.20 + * @version 1.4.0 * - * @param {Object} query The original query - * @param {Object} options The original query options - * @param {Mixed} value The field value, as stored in the DB - * @param {Function} callback + * @param {Alchemy.OperationalContext.ReadFieldFromDatasource} context + * @param {*} value + * + * @return {Pledge<*>|*} */ -BigIntField.setMethod(function _toApp(query, options, value, callback) { +BigIntField.setMethod(function _toApp(context, value) { if (value) { value = this.datasource.castToBigInt(value); } - value = this.cast(value); - - callback(null, value); + return this.cast(value); }); \ No newline at end of file diff --git a/lib/app/helper_field/datetime_field.js b/lib/app/helper_field/datetime_field.js index d3fa8144..fc859fb2 100644 --- a/lib/app/helper_field/datetime_field.js +++ b/lib/app/helper_field/datetime_field.js @@ -25,15 +25,14 @@ Datetime.setDatatype('datetime'); * * @author Jelle De Loecker * @since 0.5.0 - * @version 1.1.0 + * @version 1.4.0 * - * @param {Mixed} value The field's own value - * @param {Object} data The main record - * @param {Datasource} datasource The datasource instance + * @param {Alchemy.OperationalContext.SaveFieldToDatasource} context + * @param {*} value * - * @return {Mixed} + * @return {Date|Object} */ -Datetime.setMethod(function _toDatasource(value, data, datasource, callback) { +Datetime.setMethod(function _toDatasource(context, value) { value = this.cast(value); @@ -63,7 +62,5 @@ Datetime.setMethod(function _toDatasource(value, data, datasource, callback) { }; } - Blast.nextTick(function() { - callback(null, value); - }); + return value; }); \ No newline at end of file diff --git a/lib/app/helper_field/mixed_field.js b/lib/app/helper_field/mixed_field.js index 05d3b4a4..808885e3 100644 --- a/lib/app/helper_field/mixed_field.js +++ b/lib/app/helper_field/mixed_field.js @@ -32,15 +32,14 @@ MixedField.setSelfContained(true); * * @author Jelle De Loecker * @since 1.3.21 - * @version 1.3.21 + * @version 1.4.0 * - * @param {Mixed} value The field's own value - * @param {Object} data The main record - * @param {Datasource} datasource The datasource instance + * @param {Alchemy.OperationalContext.SaveFieldToDatasource} context + * @param {*} value * - * @return {Mixed} + * @return {Pledge<*>|*} */ -MixedField.setMethod(function _toDatasource(value, data, datasource, callback) { +MixedField.setMethod(function _toDatasource(context, value) { if (value && typeof value == 'object' && !Object.isPlainObject(value)) { if (!(value instanceof Date)) { @@ -48,7 +47,7 @@ MixedField.setMethod(function _toDatasource(value, data, datasource, callback) { } } - Blast.nextTick(callback, null, null, value); + return value; }); /** @@ -56,18 +55,18 @@ MixedField.setMethod(function _toDatasource(value, data, datasource, callback) { * * @author Jelle De Loecker * @since 1.3.21 - * @version 1.3.21 + * @version 1.4.0 + * + * @param {Alchemy.OperationalContext.ReadFieldFromDatasource} context + * @param {*} value * - * @param {Object} query The original query - * @param {Object} options The original query options - * @param {Mixed} value The field value, as stored in the DB - * @param {Function} callback + * @return {Pledge<*>|*} */ -MixedField.setMethod(function _toApp(query, options, value, callback) { +MixedField.setMethod(function _toApp(context, value) { if (value && typeof value == 'object' && typeof value.dry == 'string') { value = JSON.unDry(value); } - callback(null, value); + return value; }); \ No newline at end of file diff --git a/lib/app/helper_field/object_field.js b/lib/app/helper_field/object_field.js index 6e547ec8..887d8404 100644 --- a/lib/app/helper_field/object_field.js +++ b/lib/app/helper_field/object_field.js @@ -54,15 +54,14 @@ ObjectField.setCastFunction(function cast(value) { * * @author Jelle De Loecker * @since 0.5.0 - * @version 1.1.0 + * @version 1.4.0 * - * @param {Mixed} value The field's own value - * @param {Object} data The main record - * @param {Datasource} datasource The datasource instance + * @param {Alchemy.OperationalContext.SaveFieldToDatasource} context + * @param {*} value * - * @return {Mixed} + * @return {Pledge<*>|*} */ -ObjectField.setMethod(function _toDatasource(value, data, datasource, callback) { +ObjectField.setMethod(function _toDatasource(context, value) { if (this.options.store_as_string) { @@ -71,5 +70,5 @@ ObjectField.setMethod(function _toDatasource(value, data, datasource, callback) } } - Blast.nextTick(callback, null, null, value); + return value; }); \ No newline at end of file diff --git a/lib/app/helper_field/password_field.js b/lib/app/helper_field/password_field.js index bd54c2e3..33c7e518 100644 --- a/lib/app/helper_field/password_field.js +++ b/lib/app/helper_field/password_field.js @@ -21,19 +21,22 @@ const bcrypt = alchemy.use('bcrypt'), * * @author Jelle De Loecker * @since 0.4.0 - * @version 0.4.0 + * @version 1.4.0 * - * @param {string} value Value of field - * @param {Object} data The data object containing `value` - * @param {Datasource} datasource The destination datasource + * @param {Alchemy.OperationalContext.SaveFieldToDatasource} context + * @param {*} value + * + * @return {Pledge<*>|*} */ -Password.setMethod(function _toDatasource(value, data, datasource, callback) { +Password.setMethod(function _toDatasource(context, value) { if (regex.test(value)) { - return setImmediate(function alreadyHashedPassword() { - callback(null, value); - }); + return value; } - bcrypt.hash(value, 10, callback) + let pledge = new Pledge.Swift(); + + bcrypt.hash(value, 10, pledge.getResolverFunction()); + + return pledge; }); \ No newline at end of file diff --git a/lib/app/helper_field/schema_field.js b/lib/app/helper_field/schema_field.js index e7f58ec9..5fcc1873 100644 --- a/lib/app/helper_field/schema_field.js +++ b/lib/app/helper_field/schema_field.js @@ -293,7 +293,7 @@ SchemaField.setMethod(function getSubschemaFromModel(model_name, foreign_key, lo path ); - return Pledge.Swift.waterfall(remote_request, result => { + let pledge = Pledge.Swift.waterfall(remote_request, result => { if (!result) { return null; @@ -308,6 +308,8 @@ SchemaField.setMethod(function getSubschemaFromModel(model_name, foreign_key, lo return found_schema; }); + + return pledge; }); /** @@ -315,31 +317,28 @@ SchemaField.setMethod(function getSubschemaFromModel(model_name, foreign_key, lo * * @author Jelle De Loecker * @since 0.2.0 - * @version 1.3.16 + * @version 1.4.0 * - * @param {Object} value Value of field, an object in this case - * @param {Object} data The data object containing `value` - * @param {Datasource} datasource The destination datasource + * @param {Alchemy.OperationalContext.SaveFieldToDatasource} context + * @param {*} value * - * @return {Object} + * @return {Pledge<*>|*} */ -SchemaField.setMethod(function _toDatasource(value, holder, datasource, callback) { +SchemaField.setMethod(function _toDatasource(context, value) { if (!this.force_array_contents) { - return this._toDatasourceFromValue(value, holder, datasource, callback); + return this._toDatasourceFromValue(context, value); } - value = Array.cast(value); - - let tasks = []; - - for (let entry of value) { - tasks.push(next => { - this._toDatasourceFromValue(entry, holder, datasource, next); - }); + if (!Array.isArray(value)) { + value = Array.cast(value); + context.setWorkingValue(value); } - Function.parallel(tasks, callback); + // @TODO: What about the holder? + let tasks = value.map(entry => this._toDatasourceFromValue(context.withWorkingValue(entry), entry)); + + return Pledge.Swift.parallel(tasks); }); /** @@ -349,17 +348,16 @@ SchemaField.setMethod(function _toDatasource(value, holder, datasource, callback * @since 0.2.0 * @version 1.4.0 * - * @param {Object} value Value of field, an object in this case - * @param {Object} data The data object containing `value` - * @param {Datasource} datasource The destination datasource + * @param {Alchemy.OperationalContext.SaveFieldToDatasource} context + * @param {Object} value * - * @return {Object} + * @return {Pledge<*>|*} */ -SchemaField.setMethod(function _toDatasourceFromValue(value, holder, datasource, callback) { +SchemaField.setMethod(function _toDatasourceFromValue(context, value) { - let sub_schema, + let holder = context.getHolder(), record; - + if (this.schema.name) { record = { [this.schema.name] : holder, @@ -368,24 +366,11 @@ SchemaField.setMethod(function _toDatasourceFromValue(value, holder, datasource, record = holder; } - sub_schema = this.getSubschema(record); - - if (Pledge.isThenable(sub_schema)) { - let pledge = new Pledge.Swift(); - - Pledge.Swift.done(sub_schema, (err, sub_schema) => { - if (err) { - pledge.reject(err); - return callback(err); - } - - pledge.resolve(this._toDatasourceFromValueWithSubSchema(value, holder, record, sub_schema, datasource, callback)); - }); - - return pledge; - } else { - return this._toDatasourceFromValueWithSubSchema(value, holder, record, sub_schema, datasource, callback); - } + return Pledge.Swift.waterfall( + this.getSubschema(record), + sub_schema => context.withValueOfSubSchema(value, sub_schema), + new_context => this._toDatasourceFromValueWithSubSchema(new_context, value), + ); }); /** @@ -395,21 +380,24 @@ SchemaField.setMethod(function _toDatasourceFromValue(value, holder, datasource, * @since 0.2.0 * @version 1.4.0 * - * @param {Object} value Value of field, an object in this case - * @param {Object} data The data object containing `value` - * @param {Datasource} datasource The destination datasource + * @param {Alchemy.OperationalContext.SaveFieldToDatasource} context + * @param {Object} value * - * @return {Object} + * @return {Pledge<*>|*} */ -SchemaField.setMethod(function _toDatasourceFromValueWithSubSchema(value, holder, record, sub_schema, datasource, callback) { +SchemaField.setMethod(function _toDatasourceFromValueWithSubSchema(context, value) { + + let sub_schema = context.getSchema(), + datasource = context.getDatasource(); // If the sub schema has been found, return it now if (sub_schema) { - return Pledge.Swift.done(datasource.toDatasource(sub_schema, value), callback); + let result = datasource.toDatasource(context); + return result; } if (sub_schema === false) { - return callback(null, null); + return null; } // @WARNING: @@ -423,20 +411,25 @@ SchemaField.setMethod(function _toDatasourceFromValueWithSubSchema(value, holder if (model) { model = Model.get(model); + let pledge = new Pledge.Swift(); + model.find('first', {document: false, fields: [this.options.schema]}, function gotRecord(err, result) { if (err) { - return callback(err); + return pledge.reject(err); } if (!result.length) { log.warning('Subschema was not found for', that.name, 'in', model.name, 'model'); - return Pledge.Swift.done(datasource.toDatasource(null, value), callback); + return pledge.resolve(datasource.toDatasource(context)); } + let holder = context.getHolder(); + // Add the newly found data let temp = Object.assign({}, holder); - record = {}; + + let record = {}; record[that.schema.name] = temp; Object.assign(record, result[0]); @@ -447,21 +440,23 @@ SchemaField.setMethod(function _toDatasourceFromValueWithSubSchema(value, holder Pledge.Swift.done(sub_schema, (err, sub_schema) => { if (err) { - return callback(err); + return pledge.reject(err); } - Pledge.Swift.done(datasource.toDatasource(sub_schema, value), callback); + context.setSchema(sub_schema); + + pledge.resolve(datasource.toDatasource(context)); }); }); - return; + return pledge; } else { log.warn('Model not found for subschema', this.options.schema); } } log.warning('Model and subschema were not found for', that.path); - return Pledge.Swift.done(datasource.toDatasource(null, value), callback); + return Pledge.Swift.done(datasource.toDatasource(context), callback); }); /** @@ -471,40 +466,27 @@ SchemaField.setMethod(function _toDatasourceFromValueWithSubSchema(value, holder * @since 0.2.0 * @version 1.4.0 * - * @param {Mixed} value - * @param {Function} callback + * @param {Alchemy.OperationalContext.ReadFieldFromDatasource} context + * @param {*} value + * + * @return {Pledge<*>|*} */ -SchemaField.setMethod(function _toApp(query, options, value, callback) { +SchemaField.setMethod(function _toApp(context, value) { if (!this.force_array_contents) { - return this._toAppFromValue(query, options, value, callback); + return this._toAppFromValue(context, value); } value = Array.cast(value); - let tasks = []; + let tasks = value.map(entry => this._toAppFromValue(context.withWorkingValue(entry), entry)); - for (let entry of value) { - tasks.push(next => { - this._toAppFromValue(query, options, entry, next); - }); - } - - Pledge.Swift.all(tasks).done((err, result) => { - - if (err) { - return callback(err); - } - - try { - result = this.cast(result) - } catch (err) { - callback(err); - return; - } + let result = Pledge.Swift.waterfall( + Pledge.Swift.parallel(tasks), + result => this.cast(result) + ); - callback(null, result); - }); + return result; }); /** @@ -514,29 +496,14 @@ SchemaField.setMethod(function _toApp(query, options, value, callback) { * @since 0.2.0 * @version 1.4.0 * - * @param {Mixed} value - * @param {Function} callback + * @param {Alchemy.OperationalContext.ReadFieldFromDatasource} context + * @param {*} value + * + * @return {Pledge<*>|*} */ -SchemaField.setMethod(async function _toAppFromValue(query, options, value, callback) { +SchemaField.setMethod(function _toAppFromValue(context, value) { - let record; - - if (options.parent_value) { - record = options.parent_value; - } else if (options._root_data) { - - let root_data = options._root_data; - - if (root_data[this.schema.name]) { - root_data = root_data[this.schema.name]; - } - - record = root_data; - } else { - record = { - [this.name] : value, - }; - } + let record = context.getHolder(); if (this.schema.name) { record = { @@ -544,26 +511,13 @@ SchemaField.setMethod(async function _toAppFromValue(query, options, value, call }; } - let sub_schema = this.getSubschema(record); - - if (Pledge.isThenable(sub_schema)) { - - let pledge = new Pledge.Swift(); - - Pledge.Swift.done(sub_schema, (err, sub_schema) => { - - if (err) { - pledge.reject(err); - return callback(err); - } - - this._toAppFromValueWithSubSchema(query, options, value, sub_schema, record, callback); - }); + let result = Pledge.Swift.waterfall( + this.getSubschema(record), + sub_schema => context.withValueOfSubSchema(value, sub_schema), + new_context => this._toAppFromValueWithSubSchema(new_context, value) + ); - return pledge; - } else { - return this._toAppFromValueWithSubSchema(query, options, value, sub_schema, record, callback); - } + return result; }); /** @@ -573,22 +527,31 @@ SchemaField.setMethod(async function _toAppFromValue(query, options, value, call * @since 0.2.0 * @version 1.4.0 * - * @param {Mixed} value - * @param {Function} callback + * @param {Alchemy.OperationalContext.ReadFieldFromDatasource} context + * @param {*} value + * + * @return {Pledge<*>|*} */ -SchemaField.setMethod(async function _toAppFromValueWithSubSchema(query, options, value, sub_schema, record, callback) { +SchemaField.setMethod(function _toAppFromValueWithSubSchema(context, value) { + + let sub_schema = context.getSchema(); // Explicit false means there is no schema at the moment, // and the value can be ignored if (sub_schema === false) { - return callback(null, null); + return null; } + const datasource = context.getDatasource(); + let that = this, Dummy, item, name; + let options = context.getQueryOptions() || {}; + let criteria = context.getCriteria(); + // Don't get schema associated records if recursive is disabled let get_associations_recursive_level = options.recursive; @@ -608,85 +571,80 @@ SchemaField.setMethod(async function _toAppFromValueWithSubSchema(query, options // If the sub schema has been found, return it now if (sub_schema) { - let tasks = {}; - - Object.each(value, (field_value, field_name) => { - - let field = sub_schema.get(field_name); - if (field != null) { + value = Swift.map(value, (field_value, field_name) => { - let sub_options = { - _root_data: options._root_data, - parent_field_schema_name: this.name, - parent_value : value, - }; + let field_context = context.getFieldContext(field_name); - tasks[field_name] = (next) => { - field.toApp({}, sub_options, field_value, next); - }; + if (field_context) { + return datasource.valueToApp(field_context); } }); - value = await Function.parallel(4, tasks); } else { console.warn('Failed to find sub schema for', this.name, 'in', record); } - // Get associated records if the subschema has associations defined - if (get_associations_recursive_level && this.field_schema && !Object.isEmpty(this.field_schema.associations)) { + return Swift.waterfall( + value, + value => { + // Get associated records if the subschema has associations defined + if (get_associations_recursive_level && this.field_schema && !Object.isEmpty(this.field_schema.associations)) { - name = this.name + 'FieldModel'; - Dummy = alchemy.getModel('Model', false); + name = this.name + 'FieldModel'; + Dummy = alchemy.getModel('Model', false); - Dummy = new Dummy({ - root_model : this.root_model, - name : name - }); + Dummy = new Dummy({ + root_model : this.root_model, + name : name + }); - item = {}; + item = {}; - item[name] = value; + item[name] = value; - let sub_criteria = Dummy.find(); + let sub_criteria = Dummy.find(); - // Disable generating Document instances - // if the original find did the same - sub_criteria.setOption('document', options.document); - sub_criteria.setOption('original_query', query); - sub_criteria.setOption('_root_data', options._root_data); - sub_criteria.setOption('_parent_field', that); - sub_criteria.setOption('_parent_model', that.schema.model_name); - sub_criteria.setOption('recursive', get_associations_recursive_level); + // Disable generating Document instances + // if the original find did the same + sub_criteria.setOption('document', options.document); + sub_criteria.setOption('original_query', criteria); + sub_criteria.setOption('_root_data', options._root_data); + sub_criteria.setOption('_parent_field', that); + sub_criteria.setOption('_parent_model', that.schema.model_name); + sub_criteria.setOption('recursive', get_associations_recursive_level); - sub_criteria.setOption('associations', this.field_schema.associations); + sub_criteria.setOption('associations', this.field_schema.associations); - // @todo: inherit other original find options? + // @todo: inherit other original find options? - Dummy.addAssociatedDataToRecord(sub_criteria, item, function gotAssociatedData(err, result) { + let pledge = new Pledge.Swift(); - var key; + Dummy.addAssociatedDataToRecord(sub_criteria, item, function gotAssociatedData(err, result) { - if (err) { - return callback(err); - } + if (err) { + return pledge.reject(err); + } + + let key; - for (key in result) { - if (key == name) { - continue; + for (key in result) { + if (key == name) { + continue; + } + + value[key] = result[key]; } - value[key] = result[key]; - } + pledge.resolve(value); + }); - callback(null, value); + return pledge; } - ); - return; - } - - callback(null, this.castEntry(value)); + return this.castEntry(value); + } + ); }); /** diff --git a/lib/app/helper_field/settings_field.js b/lib/app/helper_field/settings_field.js index c0bcbfd9..3a8215b1 100644 --- a/lib/app/helper_field/settings_field.js +++ b/lib/app/helper_field/settings_field.js @@ -76,17 +76,17 @@ Settings.setCastFunction(function cast(value) { * @since 1.4.0 * @version 1.4.0 * - * @param {*} value The field's own value - * @param {Object} data The main record - * @param {Datasource} datasource The datasource instance + * @param {Alchemy.OperationalContext.SaveFieldToDatasource} context + * @param {*} value * - * @return {*} + * @return {Pledge<*>|*} */ -Settings.setMethod(function _toDatasource(value, data, datasource, callback) { +Settings.setMethod(function _toDatasource(context, value) { if (value) { value = value.toDatasourceArray(); + context = context.withWorkingValue(value); } - _toDatasource.super.call(this, value, data, datasource, callback); + _toDatasource.super.call(this, context, value); }); diff --git a/lib/app/helper_model/model.js b/lib/app/helper_model/model.js index 41831e45..9a780f78 100644 --- a/lib/app/helper_model/model.js +++ b/lib/app/helper_model/model.js @@ -86,13 +86,32 @@ Model.postInherit(function setModelName() { * @version 1.3.22 */ Model.mapEventToMethod({ + saving : 'beforeSave', saved : 'afterSave', finding : 'beforeFind', queried : 'afterQuery', associated : 'afterAssociated', + + // Was 'afterFind' in behaviours found : 'afterData', foundDocuments : 'afterFind', removed : 'afterRemove', + to_datasource : 'beforeToDatasource', + removing : 'beforeRemove', + + // New + beforeFind : 'beforeFind', + afterData : 'afterData', + afterFind : 'afterFind', + afterQuery : 'afterQuery', + beforeSave : 'beforeSave', + beforeToDatasource : 'beforeToDatasource', + afterAssociated : 'afterAssociated', + afterSave : 'afterSave', + beforeRemove : 'beforeRemove', + afterRemove : 'afterRemove', + beforeNormalize : 'beforeNormalize', + beforeValidate : 'beforeValidate', }); /** @@ -702,6 +721,71 @@ Model.setMethod(function findAll(conditions, options) { return this.find('all', conditions); }); +/** + * Issue a data event + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {string} event_name + * @param {Array} args + * + * @return {Pledge|null} + */ +Model.setMethod(function issueDataEvent(event_name, args, next) { + + let tasks = []; + + if (this.behaviours) { + + let method_name, + behaviour, + key; + + if (this.constructor.event_to_method_map) { + method_name = this.constructor.event_to_method_map.get(event_name); + } + + if (!method_name) { + method_name = 'on' + event_name.camelize(); + } + + for (key in this.behaviours) { + behaviour = this.behaviours[key]; + + if (typeof behaviour[method_name] == 'function') { + tasks.push(() => behaviour[method_name](...args)); + } + } + } + + if (!tasks.length) { + let pledge = new Swift(); + this.issueEvent(event_name, args, pledge.getResolverFunction()); + + if (next) { + pledge.done(next); + } + + return pledge; + } + + tasks.push(() => { + let pledge = new Swift(); + this.issueEvent(event_name, args, pledge.getResolverFunction()); + return pledge; + }); + + let result = Swift.waterfall(...tasks); + + if (next) { + Swift.done(result, next); + } + + return result; +}); + /** * Query the database * @@ -799,10 +883,15 @@ Model.setMethod(function find(type, criteria, callback) { next(); } }, function doBeforeEvent(next) { - that.issueEvent('finding', [criteria], next); + that.issueDataEvent('beforeFind', [criteria], next); }, function doQuery(next) { - that.datasource.read(that, criteria, function done(err, result) { + let context = new Classes.Alchemy.OperationalContext.ReadDocumentFromDatasource(); + context.setDatasource(that.datasource); + context.setModel(that); + context.setCriteria(criteria); + + Pledge.Swift.done(that.datasource.read(context), (err, result) => { if (err) { return next(err); @@ -815,7 +904,7 @@ Model.setMethod(function find(type, criteria, callback) { next(); }); }, function emitQueried(next) { - that.issueEvent('queried', [criteria, records], next); + that.issueDataEvent('afterQuery', [criteria, records], next); }, function doTranslations(next) { if (!that.translateItems) { @@ -843,7 +932,7 @@ Model.setMethod(function find(type, criteria, callback) { // Add the associated data, 8 records at a time Function.parallel(8, tasks, next); }, function emitAssociated(next) { - that.issueEvent('associated', [criteria, records], next); + that.issueDataEvent('afterAssociated', [criteria, records], next); }, function doAfterType(next) { if (!Type) { @@ -861,7 +950,7 @@ Model.setMethod(function find(type, criteria, callback) { }, function doAfterEvent(next) { - that.issueEvent('found', [criteria, records], function afterFound(err) { + that.issueDataEvent('afterData', [criteria, records], function afterFound(err) { if (err) { return next(err); @@ -870,7 +959,7 @@ Model.setMethod(function find(type, criteria, callback) { if (criteria.options.document !== false) { records = that.createDocumentList(records, criteria); records.available = available; - that.issueEvent('foundDocuments', [criteria, records], next); + that.issueDataEvent('afterFind', [criteria, records], next); } else { next(); } @@ -898,8 +987,6 @@ Model.setMethod(function find(type, criteria, callback) { Function.parallel(8, tasks, next); }, function done(err) { - var i; - if (err != null) { if (callback) { @@ -910,6 +997,8 @@ Model.setMethod(function find(type, criteria, callback) { } if (Blast.isBrowser && criteria.options.document !== false) { + let i; + for (i = 0; i < records.length; i++) { records[i].checkAndInformDatasource(); } @@ -1806,10 +1895,10 @@ Model.setMethod(function saveRecord(document, options, callback) { }); }, function doBeforeNormalize(next) { // @TODO: make "beforeSave" only use promises - that.callOrNext('beforeNormalize', [document, options], next); + that.issueDataEvent('beforeNormalize', [document, options], next); }, function emitSavingEvent(next) { // @TODO: Should this be able to stop the saving without throwing an error? - that.issueEvent('saving', [document, options, creating], next); + that.issueDataEvent('beforeSave', [document, options, creating], next); }, function doDatabase(next) { if (options.debug) { @@ -1902,7 +1991,7 @@ Model.setMethod(function compose(data, options) { * * @author Jelle De Loecker * @since 0.2.0 - * @version 1.3.21 + * @version 1.4.0 * * @param {Document} document * @param {Object} options @@ -1926,7 +2015,7 @@ Model.setMethod(function createRecord(document, options, callback) { next(); } }, function doBeforeValidate(next) { - that.callOrNext('beforeValidate', [document, options], next); + that.issueDataEvent('beforeValidate', [document, options], next); }, function validate(next) { if (options.validate === false || !that.schema) { @@ -1935,7 +2024,7 @@ Model.setMethod(function createRecord(document, options, callback) { Pledge.done(that.schema.validate(document), next); }, function doBeforeSave(next) { - that.callOrNext('beforeSave', [document, options], next); + that.issueDataEvent('beforeSave', [document, options], next); }, function done(err) { if (err) { @@ -1946,13 +2035,27 @@ Model.setMethod(function createRecord(document, options, callback) { return callback(new Error('Model "' + that.model_name + '" has no datasource')); } - that.datasource.create(that, data, options, function afterCreate(err, result) { + let context = new Classes.Alchemy.OperationalContext.SaveDocumentToDatasource(); + context.setDatasource(that.datasource); + context.setModel(that); + context.setRootData(data); + context.setSaveOptions(options); + + let create_promise; + + try { + create_promise = that.datasource.create(context); + } catch (err) { + return callback(err); + } + + Pledge.Swift.done(create_promise, function afterCreate(err, result) { if (err != null) { return callback(err); } - that.issueEvent('saved', [result, options, true], function afterSavedEvent() { + that.issueDataEvent('afterSave', [result, options, true], function afterSavedEvent() { callback(null, result); }); }); @@ -1987,7 +2090,7 @@ function createDocumentForSaving(model, original_input, data) { * * @author Jelle De Loecker * @since 0.2.0 - * @version 1.3.21 + * @version 1.4.0 * * @param {Document} document * @param {Object} options @@ -2012,7 +2115,7 @@ Model.setMethod(function updateRecord(document, options, callback) { next(); } }, function doBeforeValidate(next) { - that.callOrNext('beforeValidate', [document, options], next); + that.issueDataEvent('beforeValidate', [document, options], next); }, function validate(next) { if (options.validate === false || !that.schema) { @@ -2022,7 +2125,7 @@ Model.setMethod(function updateRecord(document, options, callback) { Pledge.done(that.schema.validate(document), next); }, function doBeforeSave(next) { - that.callOrNext('beforeSave', [document, options], next); + that.issueDataEvent('beforeSave', [document, options], next); }, function done(err) { if (err) { @@ -2033,13 +2136,32 @@ Model.setMethod(function updateRecord(document, options, callback) { return callback(new Error('Model "' + that.model_name + '" has no datasource')); } - that.datasource.update(that, data, options, function afterUpdate(err, result) { + let context = new Classes.Alchemy.OperationalContext.SaveDocumentToDatasource(); + context.setDatasource(that.datasource); + context.setModel(that); + context.setRootData(data); + context.setSaveOptions(options); + + let update_result; + + try { + update_result = that.datasource.update(context); + } catch (err) { + return callback(err); + } + + Swift.done(update_result, function afterUpdate(err, result) { if (err != null) { return callback(err); } - that.issueEvent('saved', [result, options, false], function afterSavedEvent() { + that.issueDataEvent('afterSave', [result, options, false], function afterSavedEvent(err) { + + if (err) { + return callback(err); + } + callback(null, result); }); }); diff --git a/lib/class/behaviour.js b/lib/class/behaviour.js index d2c2217a..9ddb1694 100644 --- a/lib/class/behaviour.js +++ b/lib/class/behaviour.js @@ -5,7 +5,7 @@ * * @author Jelle De Loecker * @since 0.0.1 - * @version 1.0.3 + * @version 1.4.0 * * @param {Model} model Model instance * @param {Object} options Behaviour options @@ -17,30 +17,6 @@ global.Behaviour = Function.inherits('Alchemy.Base', function Behaviour(model, o // Merge options this.options = Object.create(options); - - if (typeof this.beforeFind === 'function') { - model.on('finding', this.beforeFind, this); - } - - if (typeof this.afterFind === 'function') { - model.on('found', this.afterFind, this); - } - - if (typeof this.beforeSave === 'function') { - model.on('saving', this.beforeSave, this); - } - - if (typeof this.beforeToDatasource == 'function') { - model.on('to_datasource', this.beforeToDatasource, this); - } - - if (typeof this.afterSave === 'function') { - model.on('saved', this.afterSave, this); - } - - if (typeof this.beforeRemove === 'function') { - model.on('removing', this.beforeRemove, this); - } }); /** diff --git a/lib/class/datasource.js b/lib/class/datasource.js index c08fca28..673c9f22 100644 --- a/lib/class/datasource.js +++ b/lib/class/datasource.js @@ -254,43 +254,29 @@ Datasource.setMethod(function getSchema(schema) { * @since 0.2.0 * @version 1.4.0 * - * @param {Schema|Model} schema - * @param {Object} data + * @param {Alchemy.OperationalContext.SaveDocumentToDatasource} context * * @return {Pledge|Object} */ -Datasource.setMethod(function toDatasource(schema, data) { +Datasource.setMethod(function toDatasource(context) { - // Make sure we get the schema - schema = this.getSchema(schema); - - if (Object.isPlainObject(schema)) { - throw new Error('The provided schema was a regular object'); - } + let schema = context.getSchema(), + data = context.getWorkingData(); if (!schema) { - if (alchemy.settings.debugging.debug) { - alchemy.distinctProblem('schema-not-found', 'Schema not found: not normalizing data'); - } - return data; } - let that = this, - tasks = {}; - - data = {...data}; + let tasks = {}; for (let field_name in data) { - let field = schema.get(field_name); - - if (field != null) { - let value = data[field_name]; + let field_context = context.getFieldContext(field_name); - tasks[field_name] = function doToDatasource(next) { - that.valueToDatasource(field, value, data, next); - }; + if (!field_context) { + continue; } + + tasks[field_name] = this.valueToDatasource(field_context); } return Pledge.Swift.parallel(tasks); @@ -303,14 +289,13 @@ Datasource.setMethod(function toDatasource(schema, data) { * @since 0.2.0 * @version 1.4.0 * - * @param {Schema|Model} schema - * @param {Object} query - * @param {Object} options - * @param {Object} data + * @param {Alchemy.OperationalContext.ReadDocumentFromDatasource} context * - * @return {Pledge<*>|*} + * @return {Pledge} */ -Datasource.setMethod(function toApp(schema, query, options, data) { +Datasource.setMethod(function toApp(context) { + + let data = context.getWorkingData(); if (!data) { throw new Error('Unable to convert data: no data given'); @@ -318,19 +303,13 @@ Datasource.setMethod(function toApp(schema, query, options, data) { let that = this; - schema = this.getSchema(schema); + let schema = context.getSchema(); if (schema == null) { alchemy.distinctProblem('schema-not-found-unnormalize', 'Schema not found: not un-normalizing data'); return data; } - options = {...options}; - - if (!options._root_data) { - options._root_data = data; - } - let tasks; if (data[schema.name]) { @@ -340,6 +319,8 @@ Datasource.setMethod(function toApp(schema, query, options, data) { let value = data[key], data_schema; + let sub_context = context.createChild(); + if (key == schema.name) { data_schema = schema; } else { @@ -357,25 +338,19 @@ Datasource.setMethod(function toApp(schema, query, options, data) { } data_schema = model.schema; + sub_context.setModel(model); } + sub_context.setSchema(data_schema); + tasks[key] = function addData(next) { // Associated data can return multiple items, so we need to unwind that if (Array.isArray(value)) { - let sub_tasks = [], - i; - - for (i = 0; i < value.length; i++) { - let row = value[i]; - sub_tasks.push(function doRow(next) { - Pledge.Swift.done(that.toApp(data_schema, query, options, row), next); - }); - } - + let sub_tasks = value.map(entry => that.toApp(sub_context.withDatasourceEntry(entry))); Function.parallel(false, 4, sub_tasks, next); } else { - Pledge.Swift.done(that.toApp(data_schema, query, options, value), next); + Pledge.Swift.done(that.toApp(sub_context.withDatasourceEntry(value)), next); } }; } @@ -383,7 +358,6 @@ Datasource.setMethod(function toApp(schema, query, options, data) { return Pledge.Swift.parallel(tasks); } - data = {...data}; tasks = {}; for (let entry of schema.getSortedItems()) { @@ -392,15 +366,11 @@ Datasource.setMethod(function toApp(schema, query, options, data) { field = entry.value, value = data[field_name]; - if (field.is_meta_field) { + if (field.is_meta_field || field == null) { continue; } - if (field != null) { - tasks[field_name] = function doToDatasource(next) { - that.valueToApp(field, query, options, value, next); - }; - } + tasks[field_name] = this.valueToApp(context.getFieldContext(field_name)); } if (Blast.isBrowser) { @@ -408,9 +378,7 @@ Datasource.setMethod(function toApp(schema, query, options, data) { if (key[0] == '_' && key[1] == '$') { // Certain extra data is stored on the browser as // properties starting with "_$" - tasks[key] = function addExtraneous(next) { - next(null, data[key]); - }; + tasks[key] = data[key]; } } } @@ -423,20 +391,15 @@ Datasource.setMethod(function toApp(schema, query, options, data) { * * @author Jelle De Loecker * @since 0.2.0 - * @version 0.2.0 + * @version 1.4.0 + * + * @param {Alchemy.OperationalContext.SaveFieldToDatasource} context + * + * @return {Pledge<*>|*} */ -Datasource.setMethod(function valueToDatasource(field, value, data, callback) { - - var that = this; - - field.toDatasource(value, data, this, function gotDatasourceValue(err, value) { - - if (err) { - return callback(err); - } - - that._valueToDatasource(field, value, data, callback); - }); +Datasource.setMethod(function valueToDatasource(context) { + const field = context.getField(); + return field.toDatasource(context); }); /** @@ -444,44 +407,15 @@ Datasource.setMethod(function valueToDatasource(field, value, data, callback) { * * @author Jelle De Loecker * @since 0.2.0 - * @version 0.2.0 - */ -Datasource.setMethod(function valueToApp(field, query, options, value, callback) { - - var that = this; - - field.toApp(query, options, value, function gotToAppValue(err, value) { - - if (err) { - return callback(err); - } - - that._valueToApp(field, query, options, value, callback); - }); -}); - -/** - * Prepare value to be stored in the database. - * Should be overridden by extended datasources. + * @version 1.4.0 * - * @author Jelle De Loecker - * @since 0.2.0 - * @version 1.1.0 - */ -Datasource.setMethod(function _valueToDatasource(field, value, data, callback) { - callback(null, value); -}); - -/** - * Prepare value to be returned to the app. - * Should be overridden by extended datasources. + * @param {Alchemy.OperationalContext.ReadFieldFromDatasource} context * - * @author Jelle De Loecker - * @since 0.2.0 - * @version 1.1.0 + * @return {Pledge<*>|*} */ -Datasource.setMethod(function _valueToApp(field, query, options, value, callback) { - callback(null, value); +Datasource.setMethod(function valueToApp(context) { + const field = context.getField(); + return field.toApp(context); }); /** @@ -491,16 +425,17 @@ Datasource.setMethod(function _valueToApp(field, query, options, value, callback * @since 0.2.0 * @version 1.4.0 * - * @param {Model} model - * @param {Criteria} criteria + * @param {Alchemy.OperationalContext.ReadDocumentFromDatasource} context + * + * @return {Pledge} */ -Datasource.setMethod(function read(model, criteria, callback) { +Datasource.setMethod(function read(context) { - var that = this, - pledge = new Pledge(), - hash; + let criteria = context.getCriteria(), + model = context.getModel(); - pledge.done(callback); + let that = this, + hash; // Look through the cache first if (this.queryCache && model.cache) { @@ -512,6 +447,8 @@ Datasource.setMethod(function read(model, criteria, callback) { let cached = model.cache.get(hash, true); if (cached) { + let pledge = new Pledge.Swift(); + cached.done(function gotCached(err, result) { if (err) { @@ -535,46 +472,32 @@ Datasource.setMethod(function read(model, criteria, callback) { let cache_pledge; if (hash && model.cache && this.queryCache) { - cache_pledge = new Pledge(); + cache_pledge = new Pledge.Swift(); + + // Store the pledge in the cache + model.cache.set(hash, cache_pledge); } model.emit('reading_datasource', criteria); - // Nothing in the cache, so do the actual reading - that._read(model, criteria, function afterRead(err, results, available) { - - var sub_pledge, - tasks, - i; + return Pledge.Swift.waterfall( + that._read(context), + _result => { model.emit('read_datasource', criteria); - if (err) { - return pledge.reject(err); - } + let {rows, available} = _result; if (criteria.options.return_raw_data) { criteria.setOption('document', false); - return handleResults(null, results); + } else { + rows = rows.map(row => that.toApp(context.withDatasourceEntry(row))); + rows = Pledge.Swift.parallel(rows); } - tasks = results.map(function eachEntry(entry) { - return function entryToApp(next) { - Pledge.Swift.done(that.toApp(model, criteria, {}, entry), next); - }; - }); - - sub_pledge = Function.parallel(tasks, handleResults); - - pledge._addProgressPledge(sub_pledge); - - function handleResults(err, app_results) { - - if (err) { - return pledge.reject(err); - } - - try { + return Pledge.Swift.waterfall( + rows, + app_results => { let result = { items : app_results, @@ -593,19 +516,10 @@ Datasource.setMethod(function read(model, criteria, callback) { cache_pledge.resolve(cloned); } - pledge.resolve(result); - } catch (err) { - pledge.reject(err); + return result; } - } + ); }); - - if (this.queryCache && model.cache && cache_pledge) { - // Store the pledge in the cache - model.cache.set(hash, cache_pledge); - } - - return pledge; }); /** @@ -615,41 +529,27 @@ Datasource.setMethod(function read(model, criteria, callback) { * @since 0.2.0 * @version 1.4.0 * - * @param {Model} model - * @param {Object} data - * @param {Object} options - * @param {Function} callback + * @param {Alchemy.OperationalContext.SaveToDatasource} context * * @return {Pledge} */ -Datasource.setMethod(function create(model, data, options, callback) { +Datasource.setMethod(function create(context) { - var that = this, - pledge; - - pledge = Function.series(false, function toDatasource(next) { + let result = Pledge.Swift.waterfall( // Convert the data into something the datasource will understand - Pledge.Swift.done(that.toDatasource(model, data), next); - }, function emitToDatasource(next, ds_data) { - model.emit('to_datasource', data, ds_data, options, true, function afterTDSevent(err, stopped) { - next(err, ds_data); - }); - }, function doCreate(next, ds_data) { - that._create(model, ds_data, options, next); - }, function gotUpdateResult(next, result) { - Pledge.Swift.done(that.toApp(model, null, options, result), next); - }, function done(err, result) { - - if (err) { - return; - } + () => this.toDatasource(context), - return result.last() || false; - }); + // Actually create the data + converted_data => this._create(context.setConvertedData(converted_data)), - pledge.done(callback); + // Convert the result back to something the app will understand + result => this.toApp(context.getReadFromDatasourceContext(result)), - return pledge; + // Return the last entry + result => result || false + ); + + return result; }); /** @@ -659,51 +559,35 @@ Datasource.setMethod(function create(model, data, options, callback) { * @since 0.2.0 * @version 1.4.0 * - * @param {Model} model - * @param {Object} data - * @param {Object} options - * @param {Function} callback + * @param {Alchemy.OperationalContext.SaveToDatasource} context * * @return {Pledge} */ -Datasource.setMethod(function update(model, data, options, callback) { +Datasource.setMethod(function update(context) { - var that = this, - pledge; + let data = context.getRootData(), + options = context.getSaveOptions(); if (options.set_updated !== false) { // Set the updated field data.updated = new Date(); } - pledge = Function.series(false, function toDatasource(next) { + let result = Swift.waterfall( // Convert the data into something the datasource will understand - Pledge.Swift.done(that.toDatasource(model, data), next); - }, function emitToDatasource(next, ds_data) { - model.emit('to_datasource', data, ds_data, options, false, function afterTDSevent(err, stopped) { - next(err, ds_data); - }); - }, function doUpdate(next, ds_data) { - that._update(model, ds_data, options, next); - }, function gotUpdateResult(next, result) { + this.toDatasource(context), - if (!result) { - return next(); - } - - Pledge.Swift.done(that.toApp(model, null, options, result), next); - }, function done(err, result) { + // Actually create the data + converted_data => this._update(context.setConvertedData(converted_data)), - if (err) { - return; - } + // Convert the result back to something the app will understand + result => this.toApp(context.getReadFromDatasourceContext(result)), - return result.last() || false; - }); + // Return the last entry + result => result || false + ); - pledge.done(callback); - - return pledge; + return result; }); /** @@ -711,10 +595,14 @@ Datasource.setMethod(function update(model, data, options, callback) { * * @author Jelle De Loecker * @since 0.2.0 - * @version 0.2.0 + * @version 1.4.0 + * + * @param {Alchemy.OperationalContext.RemoveFromDatasource} context + * + * @return {Pledge} */ -Datasource.setMethod(function remove(model, query, options, callback) { - this._remove(model, query, options, callback); +Datasource.setMethod(function remove(context) { + return this._remove(context); }); /** diff --git a/lib/class/field.js b/lib/class/field.js index 676c350f..3b2983ca 100644 --- a/lib/class/field.js +++ b/lib/class/field.js @@ -847,27 +847,25 @@ Field.setMethod(function _castCondition(value, field_paths) { * * @author Jelle De Loecker * @since 0.2.0 - * @version 0.2.0 + * @version 1.4.0 * - * @param {Mixed} value - * @param {Object} data - * @param {Datasource} datasource + * @param {Alchemy.OperationalContext.SaveFieldToDatasource} context * - * @return {Mixed} + * @return {*} */ -Field.setMethod(function toDatasource(value, data, datasource, callback) { +Field.setMethod(function toDatasource(context) { - var that = this; + let value = context.getFieldValue(); if (value == null) { - return callback(null, null); + return null; } if (this.is_translatable) { - this.translatableToDatasource(value, data, datasource, callback); - } else { - this.regularToDatasource(value, data, datasource, callback); + return this.translatableToDatasource(context, value); } + + return this.regularToDatasource(context, value); }); /** @@ -876,20 +874,15 @@ Field.setMethod(function toDatasource(value, data, datasource, callback) { * * @author Jelle De Loecker * @since 0.2.0 - * @version 1.1.0 + * @version 1.4.0 * - * @param {Mixed} value The field's own value - * @param {Object} data The main record - * @param {Datasource} datasource The datasource instance + * @param {Alchemy.OperationalContext.SaveFieldToDatasource} context + * @param {*} value * - * @return {Mixed} + * @return {Pledge<*>|*} */ -Field.setMethod(function _toDatasource(value, data, datasource, callback) { - value = this.cast(value, true); - - Blast.setImmediate(function() { - callback(null, value); - }); +Field.setMethod(function _toDatasource(context, value) { + return this.cast(value, true); }); /** @@ -897,33 +890,27 @@ Field.setMethod(function _toDatasource(value, data, datasource, callback) { * * @author Jelle De Loecker * @since 0.2.0 - * @version 1.3.1 + * @version 1.4.0 * - * @param {Object} values + * @param {Alchemy.OperationalContext.SaveFieldToDatasource} context + * @param {*} value + * + * @return {Pledge<*>|*} */ -Field.setMethod(function regularToDatasource(value, data, datasource, callback) { - - var that = this, - tasks; +Field.setMethod(function regularToDatasource(context, value) { // Non arrayable fields are easy, only need 1 call to _toDatasource if (!this.is_array) { - return this._toDatasource(value, data, datasource, callback); + return this._toDatasource(context, value); } if (!value) { - return callback(); + return value; } - // Arrayable fields need to process every value inside the array - tasks = value.map(function eachValue(entry) { - return function eachValueToDs(next) { - that._toDatasource(entry, data, datasource, next); - }; - }); + let tasks = value.map(entry => this._toDatasource(context.withWorkingValue(entry), entry)); - // Perform all the tasks in parallel - Function.parallel(tasks, callback); + return Pledge.Swift.parallel(tasks); }); /** @@ -931,22 +918,23 @@ Field.setMethod(function regularToDatasource(value, data, datasource, callback) * * @author Jelle De Loecker * @since 0.2.0 - * @version 0.2.0 + * @version 1.4.0 + * + * @param {Alchemy.OperationalContext.SaveFieldToDatasource} context + * @param {*} value * - * @param {Object} values + * @return {Pledge|Object} */ -Field.setMethod(function translatableToDatasource(value, data, datasource, callback) { +Field.setMethod(function translatableToDatasource(context, value) { - var that = this, - tasks = {}; + let tasks = {}; - Object.each(value, function eachValue(value, key) { - tasks[key] = function doTranslateTask(next) { - that.regularToDatasource(value, data, datasource, next); - }; - }); + for (let key in value) { + let sub_value = value[key]; + tasks[key] = this.regularToDatasource(context.withWorkingValue(sub_value), sub_value); + } - Function.parallel(tasks, callback); + return Pledge.Swift.parallel(tasks); }); /** @@ -956,16 +944,22 @@ Field.setMethod(function translatableToDatasource(value, data, datasource, callb * @since 0.2.0 * @version 1.4.0 * - * @param {Object} query The original query - * @param {Object} options The original query options - * @param {Mixed} value The field value, as stored in the DB - * @param {Function} callback + * @param {Alchemy.OperationalContext.ReadFieldFromDatasource} context + * + * @return {*} */ -Field.setMethod(function toApp(query, options, value, callback) { +Field.setMethod(function toApp(context) { + + let value = context.getFieldValue(); + + if (value == null) { + return null; + } + if (this.is_translatable) { - this.fromTranslatableToApp(query, options, value, callback); + return this.fromTranslatableToApp(context, value); } else { - this.fromRegularToApp(query, options, value, callback); + return this.fromRegularToApp(context, value); } }); @@ -974,33 +968,29 @@ Field.setMethod(function toApp(query, options, value, callback) { * * @author Jelle De Loecker * @since 0.2.0 - * @version 0.2.0 + * @version 1.4.0 * - * @param {Object} values + * @param {Alchemy.OperationalContext.ReadFieldFromDatasource} context + * @param {*} value + * + * @return {Pledge<*>|*} */ -Field.setMethod(function fromRegularToApp(query, options, value, callback) { +Field.setMethod(function fromRegularToApp(context, value) { // Non-arrayable fields need only 1 call to _toApp if (!this.is_array) { - return this._toApp(query, options, value, callback); + return this._toApp(context, value); } - const that = this; - // If it is an arrayable field, make sure the value is actually an array if (!Array.isArray(value)) { value = Array.cast(value); + context.setWorkingValue(value); } - // Call _toApp for every value inside the array - let tasks = value.map(function eachValue(entry) { - return function eachValueToApp(next) { - that._toApp(query, options, entry, next); - }; - }); + let tasks = value.map(entry => this._toApp(context.withWorkingValue(entry), entry)); - // Perform all the tasks in parallel - Function.parallel(tasks, callback); + return Pledge.Swift.parallel(tasks); }); /** @@ -1008,14 +998,14 @@ Field.setMethod(function fromRegularToApp(query, options, value, callback) { * * @author Jelle De Loecker * @since 0.2.0 - * @version 1.1.0 + * @version 1.4.0 + * + * @param {Alchemy.OperationalContext.ReadFieldFromDatasource} context + * @param {*} value * - * @param {Object} values + * @return {Pledge<*>|*} */ -Field.setMethod(function fromTranslatableToApp(query, options, value, callback) { - - var that = this, - tasks = {}; +Field.setMethod(function fromTranslatableToApp(context, value) { if (typeof value != 'object') { let temp = value, @@ -1029,17 +1019,17 @@ Field.setMethod(function fromTranslatableToApp(query, options, value, callback) prefix = '__'; } - value = {}; - value[prefix] = temp; + value = {[prefix]: temp}; } - Object.each(value, function eachValue(value, key) { - tasks[key] = function doTranslateTask(next) { - that.fromRegularToApp(query, options, value, next); - }; - }); + let tasks = {}; + + for (let key in value) { + let sub_value = value[key]; + tasks[key] = this.fromRegularToApp(context.withWorkingValue(sub_value), sub_value); + } - Function.parallel(tasks, callback); + return Pledge.Swift.parallel(tasks); }); /** @@ -1048,15 +1038,15 @@ Field.setMethod(function fromTranslatableToApp(query, options, value, callback) * * @author Jelle De Loecker * @since 0.2.0 - * @version 1.0.7 + * @version 1.4.0 + * + * @param {Alchemy.OperationalContext.ReadFieldFromDatasource} context + * @param {*} value * - * @param {Object} query The original query - * @param {Object} options The original query options - * @param {Mixed} value The field value, as stored in the DB - * @param {Function} callback + * @return {Pledge<*>|*} */ -Field.setMethod(function _toApp(query, options, value, callback) { - return callback(null, this.cast(value, false)); +Field.setMethod(function _toApp(context, value) { + return this.cast(value, false); }); /** @@ -1129,7 +1119,7 @@ Field.setMethod(function getValue(value) { * * @author Jelle De Loecker * @since 0.2.0 - * @version 1.1.5 + * @version 1.4.0 * * @param {Array} wrapped */ @@ -1165,7 +1155,7 @@ Field.setMethod(function _getValue(wrapped, done_translation) { result.push(target); } - } else { + } else if (wrapped?.length) { for (i = 0; i < wrapped.length; i++) { result.push(this._initialValueCast(wrapped[i])); } diff --git a/lib/class/model.js b/lib/class/model.js index 732ce893..ed6579f0 100644 --- a/lib/class/model.js +++ b/lib/class/model.js @@ -590,23 +590,24 @@ Model.setStatic(function getClientConfig() { * * @author Jelle De Loecker * @since 0.2.0 - * @version 0.2.0 - * - * @return {Document} + * @version 1.4.0 */ Model.setMethod(function initBehaviours() { - var behaviour, + let instances = {}, + behaviour, + count = 0, key; - this.behaviours = {}; - for (key in this.schema.behaviours) { behaviour = this.schema.behaviours[key]; - - this.behaviours[key] = new behaviour.constructor(this, behaviour.options); + instances[key] = new behaviour.constructor(this, behaviour.options); + count++; } + if (count) { + this.behaviours = instances; + } }); /** @@ -943,9 +944,9 @@ Model.setMethod(function saveRecord(document, options, callback) { next(); }); }, function doBeforeNormalize(next) { - that.callOrNext('beforeNormalize', [document, options], next); + that.issueDataEvent('beforeNormalize', [document, options], next); }, function emitSavingEvent(next) { - that.emit('saving', document, options, creating, function afterSavingEvent(err, stopped) { + that.issueDataEvent('beforeSave', [document, options, creating], function afterSavingEvent(err, stopped) { return next(err); }); }, function doDatabase(next) { @@ -970,10 +971,8 @@ Model.setMethod(function saveRecord(document, options, callback) { } }, function doAssociated(next) { - var tasks = [], - assoc, - entry, - key; + let tasks = [], + assoc; Object.each(document.$record, function eachEntry(entry, key) { @@ -1177,7 +1176,13 @@ Model.setMethod(function convertRecordToDatasourceFormat(record, options, callba // Normalize the data data = this.compose(data, options); - let pledge = Pledge.Swift.cast(this.datasource.toDatasource(this, data)); + let context = new Classes.Alchemy.OperationalContext.SaveDocumentToDatasource(); + context.setDatasource(this.datasource); + context.setModel(this); + context.setRootData(data); + context.setSaveOptions(options); + + let pledge = Swift.cast(this.datasource.toDatasource(context)); pledge.handleCallback(callback); @@ -1305,7 +1310,7 @@ Model.setMethod(async function executeMongoPipeline(pipeline) { * * @author Jelle De Loecker * @since 0.0.1 - * @version 1.3.22 + * @version 1.4.0 * * @param {string} id The object id * @param {Function} callback @@ -1364,14 +1369,19 @@ Model.setMethod(function remove(id, callback) { return; } - that.datasource.remove(that, query, {}, function afterRemove(err, result) { + let context = new Classes.Alchemy.OperationalContext.RemoveFromDatasource(); + context.setDatasource(that.datasource); + context.setModel(that); + context.setQuery(query); + + Swift.done(that.datasource.remove(context), function afterRemove(err, result) { if (err != null) { return pledge.reject(err); } if (has_remove_events) { - that.issueEvent('removed', [doc, result], () => pledge.resolve(result)); + that.issueDataEvent('afterRemove', [doc, result], () => pledge.resolve(result)); } else { pledge.resolve(result); } diff --git a/lib/scripts/create_shared_constants.js b/lib/scripts/create_shared_constants.js index 842203f9..867913bc 100644 --- a/lib/scripts/create_shared_constants.js +++ b/lib/scripts/create_shared_constants.js @@ -107,4 +107,14 @@ DEFINE_CLIENT('MutableFixedDecimal', Classes.Develry.MutableFixedDecimal); * @since 1.4.0 * @version 1.4.0 */ -DEFINE_CLIENT('Trail', Classes.Develry.Trail); \ No newline at end of file +DEFINE_CLIENT('Trail', Classes.Develry.Trail); + +/** + * The Swift class: + * The "Swift" version of the promise-like Pledge class + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +DEFINE_CLIENT('Swift', Classes.Pledge.Swift); \ No newline at end of file diff --git a/test/03-model.js b/test/03-model.js index a90498ae..a963bad5 100644 --- a/test/03-model.js +++ b/test/03-model.js @@ -221,6 +221,72 @@ describe('Model', function() { done(); }); }); + }); + + /** + * Saving data + */ + describe('#save(data, callback)', function() { + + it('should save the data and call back with a DocumentList', function(done) { + + Function.series(function doGriet(next) { + + var griet_data = { + firstname : 'Griet', + lastname : 'De Leener', + birthdate : new Date('1967-04-14'), + male : false + }; + + Model.get('Person').save(griet_data, function saved(err, list) { + + if (err) { + return next(err); + } + + assert.strictEqual(list.length, 1); + + let document = list[0]; + next(null, document); + }); + + }, function doJelle(next, griet) { + + data.parent_id = griet._id; + + Model.get('Person').save(data, function saved(err, list) { + + if (err) { + return next(err); + } + + assert.strictEqual(list.length, 1); + + let document = list[0]; + + testDocument(document, data); + + // Save the _id for next tests + _id = document._id; + + // Save this for later tests + global.person_doc = document; + + if (document.slug != 'jelle') { + return done(new Error('Expected the document to get the slug "jelle", but got: "' + document.slug + '"')); + } + + next(null, document); + }); + }, done); + }); + }); + + /** + * Adding more fields to the new Model + */ + describe('.addField(name, type, options) - With subschemes', function() { it('should be able to add schema as fields', function(next) { next = Function.regulate(next); @@ -340,7 +406,7 @@ describe('Model', function() { let comp = refetched.components?.[0]; if (!comp) { - throw new Error('The `components` field was not saved properly'); + throw new Error('The `components` field was not saved at all'); } let connections = comp.connections; @@ -1110,66 +1176,6 @@ describe('Model', function() { }); }); - /** - * Saving data - */ - describe('#save(data, callback)', function() { - - it('should save the data and call back with a DocumentList', function(done) { - - Function.series(function doGriet(next) { - - var griet_data = { - firstname : 'Griet', - lastname : 'De Leener', - birthdate : new Date('1967-04-14'), - male : false - }; - - Model.get('Person').save(griet_data, function saved(err, list) { - - if (err) { - return next(err); - } - - assert.strictEqual(list.length, 1); - - let document = list[0]; - next(null, document); - }); - - }, function doJelle(next, griet) { - - data.parent_id = griet._id; - - Model.get('Person').save(data, function saved(err, list) { - - if (err) { - return next(err); - } - - assert.strictEqual(list.length, 1); - - let document = list[0]; - - testDocument(document, data); - - // Save the _id for next tests - _id = document._id; - - // Save this for later tests - global.person_doc = document; - - if (document.slug != 'jelle') { - return done(new Error('Expected the document to get the slug "jelle", but got: "' + document.slug + '"')); - } - - next(null, document); - }); - }, done); - }); - }); - describe('#sort', function() { it('should be used when no sort parameter is given', async function() { @@ -1319,45 +1325,56 @@ describe('Model', function() { product = Model.get('Product'); - await product.ensureIds(list, async function done(err) { + let pledge = new Pledge(); + + product.ensureIds(list, async function done(err) { if (err) { - return done(err); + return pledge.reject(err); } - let prod; + try { + + let prod; + + prod = await product.findById('52efff000073570002000000'); + assert.strictEqual(prod.name, 'screen'); - prod = await product.findById('52efff000073570002000000'); - assert.strictEqual(prod.name, 'screen'); + prod = await product.findById('52efff000073570002000001'); + assert.strictEqual(prod.name, 'mouse'); - prod = await product.findById('52efff000073570002000001'); - assert.strictEqual(prod.name, 'mouse'); + prod = await product.findById('52efff000073570002000002'); + assert.strictEqual(prod.name, 'keyboard'); - prod = await product.findById('52efff000073570002000002'); - assert.strictEqual(prod.name, 'keyboard'); + let prods = await product.find('all'); + assert.strictEqual(prods.length, 3); - let prods = await product.find('all'); - assert.strictEqual(prods.length, 3); + let removed = await prod.remove(); - let removed = await prod.remove(); + assert.strictEqual(removed, true); - assert.strictEqual(removed, true); + prod = await product.findById('52efff000073570002000002'); + assert.strictEqual(prod, null); - prod = await product.findById('52efff000073570002000002'); - assert.strictEqual(prod, null); + prods = await product.find('all'); + assert.strictEqual(prods.length, 2, 'There should only be 3 products after one was removed'); - prods = await product.find('all'); - assert.strictEqual(prods.length, 2, 'There should only be 3 products after one was removed'); + // Ensure them again + await product.ensureIds(list); - // Ensure them again - await product.ensureIds(list); + prods = await product.find('all'); + assert.strictEqual(prods.length, 3, 'There should only be 3 products: only the missing one should have been added'); - prods = await product.find('all'); - assert.strictEqual(prods.length, 3, 'There should only be 3 products: only the missing one should have been added'); + prod = await product.findById('52efff000073570002000002'); + assert.strictEqual(prod.name, 'keyboard'); + } catch (err) { + return pledge.reject(err); + } - prod = await product.findById('52efff000073570002000002'); - assert.strictEqual(prod.name, 'keyboard'); + pledge.resolve(); }); + + return pledge; }); }); diff --git a/test/04-field.js b/test/04-field.js index 6e4cb7d8..b34219c2 100644 --- a/test/04-field.js +++ b/test/04-field.js @@ -872,7 +872,7 @@ describe('Field.Schema', function() { const FlobotonumField = Function.inherits('Alchemy.Field', 'Flobotonum'); FlobotonumField.setDatatype('object'); FlobotonumField.setSelfContained(true); - FlobotonumField.setMethod(function _toApp(query, options, value, callback) { + FlobotonumField.setMethod(function _toApp(context, value) { let result = { type : 'flobotonum', @@ -881,10 +881,11 @@ describe('Field.Schema', function() { to_app_counter : ++flobotonum_to_app_counter, }; - callback(null, result); + + return result; }); - FlobotonumField.setMethod(function _toDatasource(value, data, datasource, callback) { + FlobotonumField.setMethod(function _toDatasource(context, value) { let main_value = value.value; @@ -893,7 +894,7 @@ describe('Field.Schema', function() { to_ds_counter: ++flobotonum_to_ds_counter, }; - callback(null, value_wrapper); + return value_wrapper; }); let QuestComponents = alchemy.getClassGroup('all_quest_component'); @@ -992,7 +993,7 @@ describe('Field.Schema', function() { assert.strictEqual(first.settings?.flobotonum?.to_apped, true, 'The value should have been passed through `toApp`'); assert.strictEqual(first.settings?.flobotonum?.type, 'flobotonum', 'The value should have been passed through `toApp`'); assert.strictEqual(first.settings?.flobotonum?.value?.stuff, true, 'The inner flobotonum value should have been saved'); - assert.strictEqual(first.settings?.flobotonum?.to_app_counter, 2, 'This should have been the second `toApp` call for this field'); + assert.strictEqual(first.settings?.flobotonum?.to_app_counter, 1, 'This should have been the first `toApp` call for this field'); assert.strictEqual(first.settings?.decimal_duration?.toString(), '1.03'); assert.strictEqual(first.settings?.decimal_duration?.constructor?.name, 'FixedDecimal'); @@ -1002,7 +1003,7 @@ describe('Field.Schema', function() { assert.strictEqual(second.settings?.flobotonum?.to_apped, true, 'The value should have been passed through `toApp`'); assert.strictEqual(second.settings?.flobotonum?.type, 'flobotonum', 'The value should have been passed through `toApp`'); assert.strictEqual(second.settings?.flobotonum?.value?.morestuff, true, 'The inner flobotonum value should have been saved'); - assert.strictEqual(second.settings?.flobotonum?.to_app_counter, 3, 'This should have been the third `toApp` call for this field'); + assert.strictEqual(second.settings?.flobotonum?.to_app_counter, 2, 'This should have been the second `toApp` call for this field'); assert.strictEqual(second.settings?.decimal_duration?.toString(), '2.22'); assert.strictEqual(second.settings?.decimal_duration?.constructor?.name, 'FixedDecimal'); @@ -1011,7 +1012,7 @@ describe('Field.Schema', function() { assert.strictEqual(main.main_settings?.flobotonum?.to_apped, true, 'The value should have been passed through `toApp`'); assert.strictEqual(main.main_settings?.flobotonum?.type, 'flobotonum', 'The value should have been passed through `toApp`'); assert.strictEqual(main.main_settings?.flobotonum?.value?.mainstuff, true, 'The inner flobotonum value should have been saved'); - assert.strictEqual(main.main_settings?.flobotonum?.to_app_counter, 1, 'This should have been the first `toApp` call for this field'); + assert.strictEqual(main.main_settings?.flobotonum?.to_app_counter, 3, 'This should have been the third `toApp` call for this field'); assert.strictEqual(main.main_settings?.decimal_duration?.toString(), '3.13'); assert.strictEqual(main.main_settings?.decimal_duration?.constructor?.name, 'FixedDecimal'); @@ -1028,7 +1029,7 @@ describe('Field.Schema', function() { assert.strictEqual(main.main_settings?.flobotonum?.to_apped, true, 'The value should have been passed through `toApp`'); assert.strictEqual(main.main_settings?.flobotonum?.type, 'flobotonum', 'The value should have been passed through `toApp`'); assert.strictEqual(main.main_settings?.flobotonum?.value?.mainstuff, true, 'The inner flobotonum value should have been saved'); - assert.strictEqual(main.main_settings?.flobotonum?.to_app_counter, 4, 'This should have been the fourth `toApp` call for this field'); + assert.strictEqual(main.main_settings?.flobotonum?.to_app_counter, 6, 'This should have been the sixth `toApp` call for this field'); assert.strictEqual(main.main_settings?.decimal_duration?.toString(), '3.13'); assert.strictEqual(main.main_settings?.decimal_duration?.constructor?.name, 'FixedDecimal'); @@ -1037,14 +1038,14 @@ describe('Field.Schema', function() { assert.strictEqual(first.settings?.flobotonum?.to_apped, true, 'The value should have been passed through `toApp`'); assert.strictEqual(first.settings?.flobotonum?.type, 'flobotonum', 'The value should have been passed through `toApp`'); assert.strictEqual(first.settings?.flobotonum?.value?.stuff, true, 'The inner flobotonum value should have been saved'); - assert.strictEqual(first.settings?.flobotonum?.to_app_counter, 5, 'This should have been the fifth `toApp` call for this field'); + assert.strictEqual(first.settings?.flobotonum?.to_app_counter, 4, 'This should have been the fourth `toApp` call for this field'); assert.strictEqual(second.type, 'sleep'); assert.strictEqual(second.settings?.duration, 2); assert.strictEqual(second.settings?.flobotonum?.to_apped, true, 'The value should have been passed through `toApp`'); assert.strictEqual(second.settings?.flobotonum?.type, 'flobotonum', 'The value should have been passed through `toApp`'); assert.strictEqual(second.settings?.flobotonum?.value?.morestuff, true, 'The inner flobotonum value should have been saved'); - assert.strictEqual(second.settings?.flobotonum?.to_app_counter, 6, 'This should have been the sixth `toApp` call for this field'); + assert.strictEqual(second.settings?.flobotonum?.to_app_counter, 5, 'This should have been the fifth `toApp` call for this field'); }); it('should handle nested schemas with custom property names', async () => { diff --git a/test/06-document.js b/test/06-document.js index 0fd12006..346c0538 100644 --- a/test/06-document.js +++ b/test/06-document.js @@ -124,7 +124,7 @@ describe('Document', function() { assert.strictEqual(child.firstname, 'Jelle'); assert.strictEqual(child.lastname, 'De Loecker'); assert.strictEqual(child.male, null); - assert.deepStrictEqual(child.nicknames, []); + assert.deepStrictEqual(child.nicknames, null); let jelle = await Model.get('Person').findByValues({firstname: 'Jelle'});