-
Notifications
You must be signed in to change notification settings - Fork 367
Implementation of patchById #910
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| // Copyright IBM Corp. 2015,2016. All Rights Reserved. | ||
| // Node module: loopback-datasource-juggler | ||
| // This file is licensed under the MIT License. | ||
| // License text available at https://opensource.org/licenses/MIT | ||
|
|
||
| var ValidationError = require('./validations').ValidationError; | ||
| var assert = require('assert'); | ||
| var dao = require('./dao'); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cyclic dependencies are better to avoid. Please move the helper functions into a standalone file, e.g.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see we already have |
||
| var utils = require('./utils'); | ||
| var idEquals = utils.idEquals; | ||
| var removeUndefined = utils.removeUndefined; | ||
|
|
||
| module.exports = function patchById(id, data, options, cb) { | ||
| var connectionPromise = dao.stillConnecting(this.getDataSource(), this, arguments); | ||
| if (connectionPromise) { | ||
| return connectionPromise; | ||
| } | ||
| assert(arguments.length >= 2, 'At least two arguments are required'); | ||
|
|
||
| var Model = this; | ||
| var where = {}; | ||
| var strict = this.settings.strict; | ||
| // TODO: the methods which we have imported from dao (idName(), | ||
| // stillConnecting(), isWhereByGivenId(),...), may need to be | ||
| // moved to a more appropriate file (utils.js?) | ||
| var idPropertyName = dao.idName(Model); | ||
|
|
||
| where[idPropertyName] = id; | ||
| if (cb === undefined) { | ||
| if (typeof options === 'function') { | ||
| // patchById(id, data, cb) | ||
| cb = options; | ||
| options = {}; | ||
| } | ||
| } | ||
|
|
||
| data = data || {}; | ||
| cb = cb || utils.createPromiseCallback(); | ||
| options = options || {}; | ||
|
|
||
| assert((typeof data === 'object') && (data !== null), | ||
| 'The data argument must be an object'); | ||
| assert(typeof options === 'object', 'The options argument must be an object'); | ||
| assert(typeof cb === 'function', 'The cb argument must be a function'); | ||
|
|
||
| var connector = Model.getDataSource().connector; | ||
| assert(typeof connector.update === 'function', | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we call |
||
| 'update() must be implemented by the connector'); | ||
|
|
||
| // Make sure id cannot be changed | ||
| checkId(data[idPropertyName], id, idPropertyName, cb); | ||
|
|
||
| // Question[1]: | ||
| // Please see QUESTION-test[1] in`test\dao.suit\patch-by-id.suits.js` for failing test | ||
| // data = data.toObject(); | ||
| // data = removeUndefined(data); | ||
|
|
||
| var hookState = {}; | ||
| this.applyProperties(data); | ||
|
|
||
| var context = { | ||
| Model: Model, | ||
| where: where, | ||
| data: data, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we report |
||
| hookState: hookState, | ||
| options: options, | ||
| }; | ||
|
|
||
| Model.notifyObserversOf('before save', context, function(err, ctx) { | ||
| if (err) return cb(err); | ||
| var isOriginalQuery = dao.isWhereByGivenId(Model, ctx.where, id); | ||
|
|
||
| if (!isOriginalQuery) { | ||
| var err = new Error('ctx.where:' + JSON.stringify(ctx.where) + ' is not valid; the `' + | ||
| idPropertyName + '` original value is ' + id); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please improve the error message to mention that |
||
| err.statusCode = 403; | ||
| process.nextTick(function() { | ||
| cb(err); | ||
| }); | ||
| return cb.promise; | ||
| } | ||
|
|
||
| if (strict) { | ||
| var instInfo = { '__unknownProperties': [] }; | ||
| dao.applyStrictCheck(Model, strict, data, | ||
| instInfo, validateAndCallConnector); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't make sense to me, because AFAICT you are discarding
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I see you have a test case for this and I assume it passes. That means I do not fully understand what this piece of code is doing. Could you please explain a bit? |
||
| } else { | ||
| validateAndCallConnector(); | ||
| } | ||
|
|
||
| function validateAndCallConnector(err) { | ||
| if (err) return cb(err); | ||
| if (options.validate === false) { | ||
| return doUpdate(ctx.where, ctx.data); | ||
| } | ||
|
|
||
| // only when options.validate is not set, take model-setting into consideration | ||
| if (options.validate === undefined && Model.settings.automaticValidation === false) { | ||
| return doUpdate(ctx.where, ctx.data); | ||
| } | ||
|
|
||
| // Make sure id is not changed in the context of `before save` | ||
| checkId(ctx.data[idPropertyName], id, idPropertyName, cb); | ||
|
|
||
| dataObj = new Model(ctx.data); | ||
| dataObj.unsetAttribute(idPropertyName); | ||
| // validation required | ||
| dataObj.isValid(function(valid) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will not work in most cases. Consider a model with required "name" and optional "age" properties, and then call Ideally, we could run validators only on the properties affected by the patch operation. However, I think that's nontrivial and should be left out of scope of this pull request. OTOH, because we are skipping validation, calling Thoughts?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea I noticed that restriction too and unfortunately I don't think if there is any easy efficient alternative either... The only alternative I can think of is calling Initially the decision for |
||
| if (valid) { | ||
| doUpdate(ctx.where, ctx.data); | ||
| } else { | ||
| cb(new ValidationError(dataObj), dataObj); | ||
| } | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| function doUpdate(where, data) { | ||
| try { | ||
| data = removeUndefined(data); | ||
| data = Model._coerce(data); | ||
| } catch (err) { | ||
| return process.nextTick(function() { | ||
| cb(err); | ||
| }); | ||
| } | ||
|
|
||
| var ctx = { | ||
| Model: Model, | ||
| where: where, | ||
| data: data, | ||
| isNewInstance: false, | ||
| hookState: hookState, | ||
| options: options, | ||
| }; | ||
| Model.notifyObserversOf('persist', ctx, function(err) { | ||
| if (err) return cb (err); | ||
| if (connector.update.length === 5) { | ||
| connector.updateAttributes(Model.modelName, where[idPropertyName], ctx.data, options, patchCallback); | ||
| } else { | ||
| connector.updateAttributes(Model.modelName, where[idPropertyName], ctx.data, patchCallback); | ||
| } | ||
| }); | ||
|
|
||
| function patchCallback(err, info) { | ||
| if (err) return cb(err); | ||
| var ctx = { | ||
| Model: Model, | ||
| data: data, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are other DAO methods reporting |
||
| hookState: hookState, | ||
| options: options, | ||
| }; | ||
| Model.notifyObserversOf('loaded', ctx, function(err) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This does not make sense to me. The |
||
| if (err) return cb(err); | ||
| var context = { | ||
| Model: Model, | ||
| isNewInstance: false, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add |
||
| hookState: hookState, | ||
| options: options, | ||
| }; | ||
| Model.notifyObserversOf('after save', context, function(err) { | ||
| if (!err) Model.emit('changed', info); | ||
| // Just for consistency we return { count: 1} for all connectors | ||
| cb(err, { count: 1 }); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, this is not correct. What if the record-to-be-updated was deleted? I think we should report 0 or 1 depending on the result from the database. Unless the operation fails when the record no longer exists. Please add a unit-test to verify this edge case. |
||
| }); | ||
| }); | ||
| } | ||
| } | ||
| return cb.promise; | ||
| }; | ||
|
|
||
| var idValidationFailure; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I find this suspicious, isn't this a module-global singleton shared by all invocations of |
||
| function checkId(idData, id, idPropertyName, cb) { | ||
| if (!idValidationFailure && idData !== undefined && !idEquals(idData, id)) { | ||
| idValidationFailure = true; | ||
| var err = new Error('id property (' + idPropertyName + ') ' + | ||
| 'cannot be updated from ' + id + ' to ' + idData); | ||
| // return with 403 (forbidden) error status code | ||
| err.statusCode = 403; | ||
| process.nextTick(function() { | ||
| cb(err); | ||
| }); | ||
| return cb.promise; | ||
| } else { | ||
| return false; | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| var debug = require('debug')('test'); | ||
| var fs = require('fs'); | ||
| var path = require('path'); | ||
|
|
||
| module.exports = function(dataSource, should, connectorCapabilities) { | ||
| var operations = fs.readdirSync(__dirname); | ||
| operations = operations.filter(function(it) { | ||
| return it !== path.basename(__filename) && | ||
| !!require.extensions[path.extname(it).toLowerCase()]; | ||
| }); | ||
| for (var ix in operations) { | ||
| var name = operations[ix]; | ||
| var fullPath = require.resolve('./' + name); | ||
| debug('Loading test suite %s (%s)', name, fullPath); | ||
| require(fullPath).apply(this, arguments); | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes me wonder, can we put this code into a shared helper? module.exports = function(dataSource, should, connectorCapabilities) {
helpers.runAllSuitesInDirExceptFile(__dirname, __filename);
}; |
||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move the new file under a new folder, i.e.
dao/patch-by-id.js.