Merge pull request #788 from strongloop/replace
Implementation of replaceOrCreate and replace
This commit is contained in:
commit
3b9d3d0212
|
@ -723,6 +723,64 @@ Memory.prototype.updateAttributes = function updateAttributes(model, id, data, o
|
|||
}
|
||||
};
|
||||
|
||||
Memory.prototype.replaceById = function(model, id, data, options, cb) {
|
||||
var self = this;
|
||||
if (!id) {
|
||||
var err = new Error('You must provide an id when replacing!');
|
||||
return process.nextTick(function() { cb(err); });
|
||||
}
|
||||
// Do not modify the data object passed in arguments
|
||||
data = Object.create(data);
|
||||
this.setIdValue(model, data, id);
|
||||
var cachedModels = this.collection(model);
|
||||
var modelData = cachedModels && this.collection(model)[id];
|
||||
if (!modelData) {
|
||||
var msg = 'Could not replace. Object with id ' + id + ' does not exist!';
|
||||
return process.nextTick(function() { cb(new Error(msg)); });
|
||||
}
|
||||
|
||||
var newModelData = {};
|
||||
for(var key in data) {
|
||||
var val = data[key];
|
||||
if(typeof val === 'function') {
|
||||
continue; // Skip methods
|
||||
}
|
||||
newModelData[key] = val;
|
||||
}
|
||||
|
||||
this.collection(model)[id] = serialize(newModelData);
|
||||
this.saveToFile(newModelData, function (err) {
|
||||
cb(err, self.fromDb(model, newModelData));
|
||||
});
|
||||
};
|
||||
|
||||
Memory.prototype.replaceOrCreate = function(model, data, options, callback) {
|
||||
var self = this;
|
||||
var idName = self.idNames(model)[0];
|
||||
var idValue = self.getIdValue(model, data);
|
||||
var filter = {where: {}};
|
||||
filter.where[idName] = idValue;
|
||||
var nodes = self._findAllSkippingIncludes(model, filter);
|
||||
var found = nodes[0];
|
||||
|
||||
if (!found) {
|
||||
// Calling _createSync to update the collection in a sync way and
|
||||
// to guarantee to create it in the same turn of even loop
|
||||
return self._createSync(model, data, function(err, id) {
|
||||
if (err) return process.nextTick(function() { cb(err); });
|
||||
self.saveToFile(id, function(err, id) {
|
||||
self.setIdValue(model, data, id);
|
||||
callback(err, self.fromDb(model, data), { isNewInstance: true });
|
||||
});
|
||||
});
|
||||
}
|
||||
var id = self.getIdValue(model, data);
|
||||
self.collection(model)[id] = serialize(data);
|
||||
self.saveToFile(data, function(err) {
|
||||
callback(err, self.fromDb(model, data), {isNewInstance: false});
|
||||
});
|
||||
};
|
||||
|
||||
Memory.prototype.transaction = function () {
|
||||
return new Memory(this);
|
||||
};
|
||||
|
|
354
lib/dao.js
354
lib/dao.js
|
@ -628,6 +628,188 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
|
|||
return cb.promise;
|
||||
};
|
||||
|
||||
/**
|
||||
* Replace or insert a model instance: replace exiting record if one is found, such that parameter `data.id` matches `id` of model instance;
|
||||
* otherwise, insert a new record.
|
||||
*
|
||||
* @param {Object} data The model instance data
|
||||
* @param {Object} [options] Options for replaceOrCreate
|
||||
* @param {Function} cb The callback function (optional).
|
||||
*/
|
||||
|
||||
DataAccessObject.replaceOrCreate = function replaceOrCreate(data, options, cb) {
|
||||
var connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
|
||||
if (connectionPromise) {
|
||||
return connectionPromise;
|
||||
}
|
||||
|
||||
if (cb === undefined) {
|
||||
if (typeof options === 'function') {
|
||||
// replaceOrCreta(data,cb)
|
||||
cb = options;
|
||||
options = {};
|
||||
}
|
||||
}
|
||||
|
||||
cb = cb || utils.createPromiseCallback();
|
||||
data = data || {};
|
||||
options = options || {};
|
||||
|
||||
assert(typeof data === 'object', '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 hookState = {};
|
||||
|
||||
var self = this;
|
||||
var Model = this;
|
||||
var connector = Model.getConnector();
|
||||
|
||||
var id = getIdValue(this, data);
|
||||
if (id === undefined || id === null) {
|
||||
return this.create(data, options, cb);
|
||||
}
|
||||
|
||||
var inst;
|
||||
if (data instanceof Model) {
|
||||
inst = data;
|
||||
} else {
|
||||
inst = new Model(data);
|
||||
}
|
||||
|
||||
var strict = inst.__strict;
|
||||
var context = {
|
||||
Model: Model,
|
||||
query: byIdQuery(Model, id),
|
||||
hookState: hookState,
|
||||
options: options
|
||||
};
|
||||
Model.notifyObserversOf('access', context, doReplaceOrCreate);
|
||||
|
||||
function doReplaceOrCreate(err, ctx) {
|
||||
if (err) return cb(err);
|
||||
|
||||
var isOriginalQuery = isWhereByGivenId(Model, ctx.query.where, id);
|
||||
var where = ctx.query.where;
|
||||
if (connector.replaceOrCreate && isOriginalQuery) {
|
||||
var context = {
|
||||
Model: Model,
|
||||
instance: inst,
|
||||
hookState: hookState,
|
||||
options: options
|
||||
};
|
||||
Model.notifyObserversOf('before save', context, function(err, ctx) {
|
||||
if (err) return cb(err);
|
||||
var update = inst.toObject(false);
|
||||
if (strict) {
|
||||
applyStrictCheck(Model, strict, update, inst, validateAndCallConnector);
|
||||
} else {
|
||||
validateAndCallConnector();
|
||||
}
|
||||
|
||||
function validateAndCallConnector(err){
|
||||
if (err) return cb(err);
|
||||
Model.applyProperties(update, inst);
|
||||
Model = Model.lookupModel(update);
|
||||
|
||||
var connector = self.getConnector();
|
||||
|
||||
if (options.validate === false) {
|
||||
return callConnector();
|
||||
}
|
||||
|
||||
// only when options.validate is not set, take model-setting into consideration
|
||||
if (options.validate === undefined && Model.settings.automaticValidation === false) {
|
||||
return callConnector();
|
||||
}
|
||||
|
||||
inst.isValid(function(valid) {
|
||||
if (!valid) return cb(new ValidationError(inst), inst);
|
||||
callConnector();
|
||||
}, update);
|
||||
|
||||
function callConnector() {
|
||||
update = removeUndefined(update);
|
||||
context = {
|
||||
Model: Model,
|
||||
where: where,
|
||||
data: update,
|
||||
currentInstance: inst,
|
||||
hookState: ctx.hookState,
|
||||
options: options
|
||||
};
|
||||
Model.notifyObserversOf('persist', context, function(err) {
|
||||
if (err) return done(err);
|
||||
connector.replaceOrCreate(Model.modelName, context.data, options, done);
|
||||
});
|
||||
}
|
||||
function done(err, data, info) {
|
||||
if (err) return cb(err);
|
||||
var context = {
|
||||
Model: Model,
|
||||
data: data,
|
||||
isNewInstance: info ? info.isNewInstance : undefined,
|
||||
hookState: ctx.hookState,
|
||||
options: options
|
||||
};
|
||||
Model.notifyObserversOf('loaded', context, function(err) {
|
||||
if (err) return cb(err);
|
||||
|
||||
var obj;
|
||||
if (data && !(data instanceof Model)) {
|
||||
inst._initProperties(data, { persisted: true });
|
||||
obj = inst;
|
||||
} else {
|
||||
obj = data;
|
||||
}
|
||||
if (err) {
|
||||
cb(err, obj);
|
||||
} else {
|
||||
var context = {
|
||||
Model: Model,
|
||||
instance: obj,
|
||||
isNewInstance: info ? info.isNewInstance : undefined,
|
||||
hookState: hookState,
|
||||
options: options
|
||||
};
|
||||
|
||||
Model.notifyObserversOf('after save', context, function(err) {
|
||||
if (!err) Model.emit('changed', inst);
|
||||
|
||||
cb(err, obj, info);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
var opts = {notify: false};
|
||||
if (ctx.options && ctx.options.transaction) {
|
||||
opts.transaction = ctx.options.transaction;
|
||||
}
|
||||
Model.findOne({where: ctx.query.where}, opts, function (err, found){
|
||||
if (err) return cb(err);
|
||||
if (!isOriginalQuery) {
|
||||
// The custom query returned from a hook may hide the fact that
|
||||
// there is already a model with `id` value `data[idName(Model)]`
|
||||
var pkName = idName(Model);
|
||||
delete data[pkName];
|
||||
if (found) id = found[pkName];
|
||||
}
|
||||
if (found) {
|
||||
self.replaceById(id, data, options, cb);
|
||||
} else {
|
||||
Model = self.lookupModel(data);
|
||||
var obj = new Model(data);
|
||||
obj.save(options, cb);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return cb.promise;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find one record that matches specified query criteria. Same as `find`, but limited to one record, and this function returns an
|
||||
* object, not a collection.
|
||||
|
@ -2406,6 +2588,178 @@ DataAccessObject.prototype.unsetAttribute = function unsetAttribute(name, nullif
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Replace set of attributes.
|
||||
* Performs validation before replacing.
|
||||
*
|
||||
* @trigger `validation`, `save` and `update` hooks
|
||||
* @param {Object} data Data to replace
|
||||
* @param {Object} [options] Options for replace
|
||||
* @param {Function} cb Callback function called with (err, instance)
|
||||
*/
|
||||
DataAccessObject.prototype.replaceAttributes = function(data, options, cb) {
|
||||
var Model = this.constructor;
|
||||
var id = getIdValue(this.constructor, this);
|
||||
return Model.replaceById(id, data, options, cb);
|
||||
};
|
||||
|
||||
DataAccessObject.replaceById = function(id, data, options, cb) {
|
||||
var connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
|
||||
if (connectionPromise) {
|
||||
return connectionPromise;
|
||||
}
|
||||
|
||||
if (cb === undefined) {
|
||||
if (typeof options === 'function') {
|
||||
cb = options;
|
||||
options = {};
|
||||
}
|
||||
}
|
||||
|
||||
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 = this.getConnector();
|
||||
assert(typeof connector.replaceById === 'function',
|
||||
'replaceById() must be implemented by the connector');
|
||||
|
||||
var pkName = idName(this);
|
||||
if (!data[pkName]) data[pkName] = id;
|
||||
|
||||
var Model = this;
|
||||
var inst = new Model(data);
|
||||
var enforced = {};
|
||||
this.applyProperties(enforced, inst);
|
||||
inst.setAttributes(enforced);
|
||||
Model = this.lookupModel(data); // data-specific
|
||||
if (Model !== inst.constructor) inst = new Model(data);
|
||||
var strict = inst.__strict;
|
||||
|
||||
if (isPKMissing(Model, cb))
|
||||
return cb.promise;
|
||||
|
||||
var model = Model.modelName;
|
||||
var hookState = {};
|
||||
|
||||
if (id !== data[pkName]) {
|
||||
var err = new Error('id property (' + pkName + ') ' +
|
||||
'cannot be updated from ' + inst[pkName] + ' to ' + data[pkName]);
|
||||
err.statusCode = 400;
|
||||
process.nextTick(function() { cb(err); });
|
||||
return cb.promise;
|
||||
}
|
||||
|
||||
var context = {
|
||||
Model: Model,
|
||||
instance: inst,
|
||||
isNewInstance: false,
|
||||
hookState: hookState,
|
||||
options: options
|
||||
};
|
||||
|
||||
Model.notifyObserversOf('before save', context, function(err, ctx) {
|
||||
if (err) return cb(err);
|
||||
|
||||
data = inst.toObject(false);
|
||||
|
||||
if (strict) {
|
||||
applyStrictCheck(Model, strict, data, inst, validateAndCallConnector);
|
||||
} else {
|
||||
validateAndCallConnector(null, data);
|
||||
}
|
||||
|
||||
function validateAndCallConnector(err, data) {
|
||||
if (err) return cb(err);
|
||||
data = removeUndefined(data);
|
||||
// update instance's properties
|
||||
inst.setAttributes(data);
|
||||
|
||||
var doValidate = true;
|
||||
if (options.validate === undefined) {
|
||||
if (Model.settings.automaticValidation !== undefined) {
|
||||
doValidate = Model.settings.automaticValidation;
|
||||
}
|
||||
} else {
|
||||
doValidate = options.validate;
|
||||
}
|
||||
|
||||
if (doValidate){
|
||||
inst.isValid(function (valid) {
|
||||
if (!valid) return cb(new ValidationError(inst), inst);
|
||||
|
||||
callConnector();
|
||||
}, data);
|
||||
} else {
|
||||
callConnector();
|
||||
}
|
||||
|
||||
function callConnector() {
|
||||
var idNames = Model.definition.idNames();
|
||||
var propKeys = Object.keys(Model.definition.properties);
|
||||
var nonIdsPropKeys = propKeys.filter(function(i) {return idNames.indexOf(i) < 0;});
|
||||
for (var i = 0; i < nonIdsPropKeys.length; i++) {
|
||||
var p = nonIdsPropKeys[i];
|
||||
inst[p] = null;
|
||||
}
|
||||
copyData(data, inst);
|
||||
var typedData = convertSubsetOfPropertiesByType(inst, data);
|
||||
context.data = typedData;
|
||||
|
||||
function replaceCallback(err, data) {
|
||||
if (err) return cb(err);
|
||||
|
||||
var ctx = {
|
||||
Model: Model,
|
||||
hookState: hookState,
|
||||
data: context.data,
|
||||
isNewInstance:false,
|
||||
options: options
|
||||
};
|
||||
Model.notifyObserversOf('loaded', ctx, function(err) {
|
||||
if (err) return cb(err);
|
||||
|
||||
inst.__persisted = true;
|
||||
inst.setAttributes(ctx.data);
|
||||
|
||||
var context = {
|
||||
Model: Model,
|
||||
instance: inst,
|
||||
isNewInstance: false,
|
||||
hookState: hookState,
|
||||
options: options
|
||||
};
|
||||
Model.notifyObserversOf('after save', context, function(err) {
|
||||
if (!err) Model.emit('changed', inst);
|
||||
|
||||
cb(err, inst);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var ctx = {
|
||||
Model: Model,
|
||||
where: byIdQuery(Model, id).where,
|
||||
data: context.data,
|
||||
isNewInstance:false,
|
||||
currentInstance: inst,
|
||||
hookState: hookState,
|
||||
options: options
|
||||
};
|
||||
Model.notifyObserversOf('persist', ctx, function(err) {
|
||||
connector.replaceById(model, id,
|
||||
inst.constructor._forDB(context.data), options, replaceCallback);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return cb.promise;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update set of attributes.
|
||||
* Performs validation before updating.
|
||||
|
|
|
@ -688,6 +688,311 @@ describe('manipulation', function () {
|
|||
});
|
||||
});
|
||||
|
||||
if (!getSchema().connector.replaceById) {
|
||||
describe.skip('replaceById - not implemented', function(){});
|
||||
} else {
|
||||
describe('replaceOrCreate', function() {
|
||||
var Post;
|
||||
var ds = getSchema();
|
||||
before(function() {
|
||||
Post = ds.define('Post', {
|
||||
title: { type: String, length: 255, index: true },
|
||||
content: { type: String },
|
||||
comments: [String]
|
||||
});
|
||||
});
|
||||
|
||||
it('works without options on create (promise variant)', function(done) {
|
||||
var post = {id: 123, title: 'a', content: 'AAA'};
|
||||
Post.replaceOrCreate(post)
|
||||
.then(function(p) {
|
||||
should.exist(p);
|
||||
p.should.be.instanceOf(Post);
|
||||
p.id.should.be.equal(post.id);
|
||||
p.should.not.have.property('_id');
|
||||
p.title.should.equal(post.title);
|
||||
p.content.should.equal(post.content);
|
||||
return Post.findById(p.id)
|
||||
.then(function (p) {
|
||||
p.id.should.equal(post.id);
|
||||
p.id.should.not.have.property('_id');
|
||||
p.title.should.equal(p.title);
|
||||
p.content.should.equal(p.content);
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('works with options on create (promise variant)', function(done) {
|
||||
var post = {id: 123, title: 'a', content: 'AAA'};
|
||||
Post.replaceOrCreate(post, {validate: false})
|
||||
.then(function(p) {
|
||||
should.exist(p);
|
||||
p.should.be.instanceOf(Post);
|
||||
p.id.should.be.equal(post.id);
|
||||
p.should.not.have.property('_id');
|
||||
p.title.should.equal(post.title);
|
||||
p.content.should.equal(post.content);
|
||||
return Post.findById(p.id)
|
||||
.then(function (p) {
|
||||
p.id.should.equal(post.id);
|
||||
p.id.should.not.have.property('_id');
|
||||
p.title.should.equal(p.title);
|
||||
p.content.should.equal(p.content);
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('works without options on update (promise variant)', function(done) {
|
||||
var post = {title: 'a', content: 'AAA', comments: ['Comment1']};
|
||||
Post.create(post)
|
||||
.then(function(created) {
|
||||
created = created.toObject();
|
||||
delete created.comments;
|
||||
delete created.content;
|
||||
created.title = 'b';
|
||||
return Post.replaceOrCreate(created)
|
||||
.then(function(p) {
|
||||
should.exist(p);
|
||||
p.should.be.instanceOf(Post);
|
||||
p.id.should.equal(created.id);
|
||||
p.should.not.have.property('_id');
|
||||
p.title.should.equal('b');
|
||||
p.should.not.have.property(p.content);
|
||||
p.should.not.have.property(p.comments);
|
||||
return Post.findById(created.id)
|
||||
.then(function (p) {
|
||||
p.should.not.have.property('_id');
|
||||
p.title.should.equal('b');
|
||||
p.should.have.property('content', undefined);
|
||||
p.should.have.property('comments', undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('works with options on update (promise variant)', function(done) {
|
||||
var post = {title: 'a', content: 'AAA', comments: ['Comment1']};
|
||||
Post.create(post)
|
||||
.then(function(created) {
|
||||
created = created.toObject();
|
||||
delete created.comments;
|
||||
delete created.content;
|
||||
created.title = 'b';
|
||||
return Post.replaceOrCreate(created, {validate: false})
|
||||
.then(function(p) {
|
||||
should.exist(p);
|
||||
p.should.be.instanceOf(Post);
|
||||
p.id.should.equal(created.id);
|
||||
p.should.not.have.property('_id');
|
||||
p.title.should.equal('b');
|
||||
p.should.not.have.property(p.content);
|
||||
p.should.not.have.property(p.comments);
|
||||
return Post.findById(created.id)
|
||||
.then(function (p) {
|
||||
p.should.not.have.property('_id');
|
||||
p.title.should.equal('b');
|
||||
p.should.have.property('content', undefined);
|
||||
p.should.have.property('comments', undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('works without options on update (callback variant)', function(done) {
|
||||
Post.create({title: 'a', content: 'AAA', comments: ['Comment1']},
|
||||
function(err, post) {
|
||||
if (err) return done(err);
|
||||
post = post.toObject();
|
||||
delete post.comments;
|
||||
delete post.content;
|
||||
post.title = 'b';
|
||||
Post.replaceOrCreate(post, function(err, p) {
|
||||
if (err) return done(err);
|
||||
p.id.should.equal(post.id);
|
||||
p.should.not.have.property('_id');
|
||||
p.title.should.equal('b');
|
||||
p.should.not.have.property(p.content);
|
||||
p.should.not.have.property(p.comments);
|
||||
Post.findById(post.id, function(err, p) {
|
||||
if (err) return done(err);
|
||||
p.id.should.eql(post.id);
|
||||
p.should.not.have.property('_id');
|
||||
p.title.should.equal('b');
|
||||
p.should.have.property('content', undefined);
|
||||
p.should.have.property('comments', undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('works with options on update (callback variant)', function(done) {
|
||||
Post.create({title: 'a', content: 'AAA', comments: ['Comment1']},
|
||||
{validate: false},
|
||||
function(err, post) {
|
||||
if (err) return done(err);
|
||||
post = post.toObject();
|
||||
delete post.comments;
|
||||
delete post.content;
|
||||
post.title = 'b';
|
||||
Post.replaceOrCreate(post, function(err, p) {
|
||||
if (err) return done(err);
|
||||
p.id.should.equal(post.id);
|
||||
p.should.not.have.property('_id');
|
||||
p.title.should.equal('b');
|
||||
p.should.not.have.property(p.content);
|
||||
p.should.not.have.property(p.comments);
|
||||
Post.findById(post.id, function(err, p) {
|
||||
if (err) return done(err);
|
||||
p.id.should.eql(post.id);
|
||||
p.should.not.have.property('_id');
|
||||
p.title.should.equal('b');
|
||||
p.should.have.property('content', undefined);
|
||||
p.should.have.property('comments', undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('works without options on create (callback variant)', function(done) {
|
||||
var post = {id: 123, title: 'a', content: 'AAA'};
|
||||
Post.replaceOrCreate(post, function(err, p) {
|
||||
if (err) return done(err);
|
||||
p.id.should.equal(post.id);
|
||||
p.should.not.have.property('_id');
|
||||
p.title.should.equal(post.title);
|
||||
p.content.should.equal(post.content);
|
||||
Post.findById(p.id, function(err, p) {
|
||||
if (err) return done(err);
|
||||
p.id.should.equal(post.id);
|
||||
p.should.not.have.property('_id');
|
||||
p.title.should.equal(post.title);
|
||||
p.content.should.equal(post.content);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('works with options on create (callback variant)', function(done) {
|
||||
var post = {id: 123, title: 'a', content: 'AAA'};
|
||||
Post.replaceOrCreate(post, {validate: false}, function (err, p) {
|
||||
if (err) return done(err);
|
||||
p.id.should.equal(post.id);
|
||||
p.should.not.have.property('_id');
|
||||
p.title.should.equal(post.title);
|
||||
p.content.should.equal(post.content);
|
||||
Post.findById(p.id, function(err, p) {
|
||||
if (err) return done(err);
|
||||
p.id.should.equal(post.id);
|
||||
p.should.not.have.property('_id');
|
||||
p.title.should.equal(post.title);
|
||||
p.content.should.equal(post.content);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!getSchema().connector.replaceById) {
|
||||
describe.skip('replaceAttributes/replaceById - not implemented', function(){});
|
||||
} else {
|
||||
describe('replaceAttributes', function() {
|
||||
var postInstance;
|
||||
var Post;
|
||||
var ds = getSchema();
|
||||
before(function () {
|
||||
Post = ds.define('Post', {
|
||||
title: {type: String, length: 255, index: true},
|
||||
content: {type: String},
|
||||
comments: [String]
|
||||
});
|
||||
});
|
||||
beforeEach(function (done) {
|
||||
Post.destroyAll(function () {
|
||||
Post.create({title: 'a', content: 'AAA'}, function (err, p) {
|
||||
if (err) return done(err);
|
||||
postInstance = p;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('works without options(promise variant)', function(done) {
|
||||
Post.findById(postInstance.id)
|
||||
.then(function(p){
|
||||
p.replaceAttributes({title: 'b'})
|
||||
.then(function(p) {
|
||||
should.exist(p);
|
||||
p.should.be.instanceOf(Post);
|
||||
p.title.should.equal('b');
|
||||
p.should.not.have.property('content', undefined);
|
||||
return Post.findById(postInstance.id)
|
||||
.then(function (p) {
|
||||
p.title.should.equal('b');
|
||||
p.should.have.property('content', undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('works with options(promise variant)', function(done) {
|
||||
Post.findById(postInstance.id)
|
||||
.then(function(p){
|
||||
p.replaceAttributes({title: 'b'}, {validate: false})
|
||||
.then(function(p) {
|
||||
should.exist(p);
|
||||
p.should.be.instanceOf(Post);
|
||||
p.title.should.equal('b');
|
||||
p.should.not.have.property('content', undefined);
|
||||
return Post.findById(postInstance.id)
|
||||
.then(function (p) {
|
||||
p.title.should.equal('b');
|
||||
p.should.have.property('content', undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('works without options(callback variant)', function(done) {
|
||||
Post.findById(postInstance.id, function(err, p) {
|
||||
if (err) return done(err);
|
||||
p.replaceAttributes({title: 'b'}, function(err, p) {
|
||||
if (err) return done(err);
|
||||
p.should.not.have.property('content', undefined);
|
||||
p.title.should.equal('b');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('works with options(callback variant)', function(done) {
|
||||
Post.findById(postInstance.id, function(err, p) {
|
||||
if (err) return done(err);
|
||||
p.replaceAttributes({title: 'b'}, {validate: false}, function(err, p) {
|
||||
if (err) return done(err);
|
||||
p.should.not.have.property('content', undefined);
|
||||
p.title.should.equal('b');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('findOrCreate', function() {
|
||||
it('should create a record with if new', function(done) {
|
||||
Person.findOrCreate({ name: 'Zed', gender: 'male' },
|
||||
|
|
|
@ -1374,6 +1374,252 @@ module.exports = function(dataSource, should) {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (!getSchema().connector.replaceById) {
|
||||
describe.skip('replaceById - not implemented', function(){});
|
||||
} else {
|
||||
describe('PersistedModel.prototype.replaceAttributes', function() {
|
||||
it('triggers hooks in the correct order', function(done) {
|
||||
monitorHookExecution();
|
||||
|
||||
existingInstance.replaceAttributes(
|
||||
{ name: 'replaced' },
|
||||
function(err, record, created) {
|
||||
if (err) return done(err);
|
||||
triggered.should.eql([
|
||||
'before save',
|
||||
'persist',
|
||||
'loaded',
|
||||
'after save'
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers `before save` hook', function(done) {
|
||||
TestModel.observe('before save', pushContextAndNext());
|
||||
|
||||
existingInstance.replaceAttributes({ name: 'changed' }, function(err) {
|
||||
if (err) return done(err);
|
||||
observedContexts.should.eql(aTestModelCtx({
|
||||
instance: {
|
||||
id: existingInstance.id,
|
||||
name: 'changed',
|
||||
extra: undefined,
|
||||
},
|
||||
isNewInstance: false
|
||||
}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('aborts when `before save` hook fails', function(done) {
|
||||
TestModel.observe('before save', nextWithError(expectedError));
|
||||
|
||||
existingInstance.replaceAttributes({ name: 'replaced' }, function(err) {
|
||||
[err].should.eql([expectedError]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('applies updates from `before save` hook', function(done) {
|
||||
TestModel.observe('before save', function(ctx, next) {
|
||||
ctx.instance.extra = 'extra data';
|
||||
ctx.instance.name = 'hooked name';
|
||||
next();
|
||||
});
|
||||
|
||||
existingInstance.replaceAttributes({ name: 'updated' }, function(err) {
|
||||
if (err) return done(err);
|
||||
TestModel.findById(existingInstance.id, function(err, instance) {
|
||||
if (err) return done(err);
|
||||
should.exists(instance);
|
||||
instance.toObject(true).should.eql({
|
||||
id: existingInstance.id,
|
||||
name: 'hooked name',
|
||||
extra: 'extra data'
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('validates model after `before save` hook', function(done) {
|
||||
TestModel.observe('before save', invalidateTestModel());
|
||||
|
||||
existingInstance.replaceAttributes({ name: 'updated' }, function(err) {
|
||||
(err || {}).should.be.instanceOf(ValidationError);
|
||||
(err.details.codes || {}).should.eql({ name: ['presence'] });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers `persist` hook', function(done) {
|
||||
TestModel.observe('persist', pushContextAndNext());
|
||||
existingInstance.replaceAttributes({ name: 'replacedName' }, function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
observedContexts.should.eql(aTestModelCtx({
|
||||
where: { id: existingInstance.id },
|
||||
data: {
|
||||
name: 'replacedName',
|
||||
id: existingInstance.id
|
||||
},
|
||||
currentInstance: {
|
||||
id: existingInstance.id,
|
||||
name: 'replacedName',
|
||||
extra: null
|
||||
},
|
||||
isNewInstance: false
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('applies delete from `persist` hook', function(done) {
|
||||
TestModel.observe('persist', pushContextAndNext(function(ctx){
|
||||
delete ctx.data.extra;
|
||||
}));
|
||||
|
||||
existingInstance.replaceAttributes({ name: 'changed' }, function(err, instance) {
|
||||
if (err) return done(err);
|
||||
instance.should.not.have.property('extra', 'hook data');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('applies updates from `persist` hook - for nested model instance', function(done) {
|
||||
var Address = dataSource.createModel('NestedAddress', {
|
||||
id: { type: String, id: true, default: 1 },
|
||||
city: { type: String, required: true },
|
||||
country: { type: String, required: true }
|
||||
});
|
||||
|
||||
var User = dataSource.createModel('UserWithAddress', {
|
||||
id: { type: String, id: true, default: uid() },
|
||||
name: { type: String, required: true },
|
||||
address: {type: Address, required: false},
|
||||
extra: {type: String}
|
||||
});
|
||||
|
||||
dataSource.automigrate(['UserWithAddress', 'NestedAddress'], function(err) {
|
||||
if (err) return done(err);
|
||||
User.create({name: 'Joe'}, function(err, instance) {
|
||||
if (err) return done(err);
|
||||
|
||||
var existingUser = instance;
|
||||
|
||||
User.observe('persist', pushContextAndNext(function(ctx) {
|
||||
should.exist(ctx.data.address)
|
||||
ctx.data.address.should.be.type('object');
|
||||
ctx.data.address.should.not.be.instanceOf(Address);
|
||||
|
||||
ctx.data.extra = 'hook data';
|
||||
}));
|
||||
|
||||
existingUser.replaceAttributes(
|
||||
{name: 'John', address: new Address({city: 'Springfield', country: 'USA'})},
|
||||
function(err, inst) {
|
||||
if (err) return done(err);
|
||||
|
||||
inst.should.have.property('extra', 'hook data');
|
||||
|
||||
User.findById(existingUser.id, function(err, dbInstance) {
|
||||
if (err) return done(err);
|
||||
dbInstance.toObject(true).should.eql({
|
||||
id: existingUser.id,
|
||||
name: 'John',
|
||||
address: {id: '1', city: 'Springfield', country: 'USA'},
|
||||
extra: 'hook data'
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers `loaded` hook', function(done) {
|
||||
TestModel.observe('loaded', pushContextAndNext());
|
||||
existingInstance.replaceAttributes({ name: 'changed' }, function(err, data) {
|
||||
if (err) return done(err);
|
||||
|
||||
observedContexts.should.eql(aTestModelCtx({
|
||||
data: {
|
||||
name: 'changed',
|
||||
id: data.id
|
||||
},
|
||||
isNewInstance : false
|
||||
}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('emits error when `loaded` hook fails', function(done) {
|
||||
TestModel.observe('loaded', nextWithError(expectedError));
|
||||
existingInstance.replaceAttributes(
|
||||
{ name: 'replaced' },
|
||||
function(err, instance) {
|
||||
[err].should.eql([expectedError]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('applies updates from `loaded` hook replaceAttributes', function(done) {
|
||||
TestModel.observe('loaded', pushContextAndNext(function(ctx){
|
||||
ctx.data.name = 'changed in hook';
|
||||
}));
|
||||
|
||||
existingInstance.replaceAttributes({ name: 'changed' }, function(err, instance) {
|
||||
if (err) return done(err);
|
||||
instance.should.have.property('name', 'changed in hook');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers `after save` hook', function(done) {
|
||||
TestModel.observe('after save', pushContextAndNext());
|
||||
|
||||
existingInstance.name = 'replaced';
|
||||
existingInstance.replaceAttributes({ name: 'replaced' }, function(err) {
|
||||
if (err) return done(err);
|
||||
observedContexts.should.eql(aTestModelCtx({
|
||||
instance: {
|
||||
id: existingInstance.id,
|
||||
name: 'replaced',
|
||||
extra: undefined
|
||||
},
|
||||
isNewInstance: false
|
||||
}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('aborts when `after save` hook fails', function(done) {
|
||||
TestModel.observe('after save', nextWithError(expectedError));
|
||||
|
||||
existingInstance.replaceAttributes({ name: 'replaced' }, function(err) {
|
||||
[err].should.eql([expectedError]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('applies updates from `after save` hook', function(done) {
|
||||
TestModel.observe('after save', function(ctx, next) {
|
||||
ctx.instance.should.be.instanceOf(TestModel);
|
||||
ctx.instance.extra = 'hook data';
|
||||
next();
|
||||
});
|
||||
|
||||
existingInstance.replaceAttributes({ name: 'updated' }, function(err, instance) {
|
||||
if (err) return done(err);
|
||||
instance.should.have.property('extra', 'hook data');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('PersistedModel.updateOrCreate', function() {
|
||||
it('triggers hooks in the correct order on create', function(done) {
|
||||
|
@ -1828,6 +2074,444 @@ module.exports = function(dataSource, should) {
|
|||
});
|
||||
});
|
||||
|
||||
if (!getSchema().connector.replaceById) {
|
||||
describe.skip('replaceById - not implemented', function(){});
|
||||
} else {
|
||||
describe('PersistedModel.replaceOrCreate', function() {
|
||||
it('triggers hooks in the correct order on create', function(done) {
|
||||
monitorHookExecution();
|
||||
|
||||
TestModel.replaceOrCreate(
|
||||
{ id: 'not-found', name: 'not found' },
|
||||
function(err, record, created) {
|
||||
if (err) return done(err);
|
||||
triggered.should.eql([
|
||||
'access',
|
||||
'before save',
|
||||
'persist',
|
||||
'loaded',
|
||||
'after save'
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers hooks in the correct order on replace', function(done) {
|
||||
monitorHookExecution();
|
||||
|
||||
TestModel.replaceOrCreate(
|
||||
{ id: existingInstance.id, name: 'new name' },
|
||||
function(err, record, created) {
|
||||
if (err) return done(err);
|
||||
if (dataSource.connector.replaceOrCreate) {
|
||||
triggered.should.eql([
|
||||
'access',
|
||||
'before save',
|
||||
'persist',
|
||||
'loaded',
|
||||
'after save'
|
||||
]);
|
||||
} else {
|
||||
// TODO: Please see loopback-datasource-juggler/issues#836
|
||||
//
|
||||
// loaded hook is triggered twice in non-atomic version:
|
||||
// 1) It gets triggered once by "find()" in this chain:
|
||||
// "replaceORCreate()->findOne()->find()",
|
||||
// which is a bug; Please see this ticket:
|
||||
// loopback-datasource-juggler/issues#836.
|
||||
// 2) It, also, gets triggered in "replaceAttributes()"
|
||||
// in this chain replaceORCreate()->replaceAttributes()
|
||||
triggered.should.eql([
|
||||
'access',
|
||||
'loaded',
|
||||
'before save',
|
||||
'persist',
|
||||
'loaded',
|
||||
'after save'
|
||||
]);
|
||||
};
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers `access` hook on create', function(done) {
|
||||
TestModel.observe('access', pushContextAndNext());
|
||||
|
||||
TestModel.replaceOrCreate(
|
||||
{ id: 'not-found', name: 'not found' },
|
||||
function(err, instance) {
|
||||
if (err) return done(err);
|
||||
observedContexts.should.eql(aTestModelCtx({ query: {
|
||||
where: { id: 'not-found' }
|
||||
}}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers `access` hook on replace', function(done) {
|
||||
TestModel.observe('access', pushContextAndNext());
|
||||
|
||||
TestModel.replaceOrCreate(
|
||||
{ id: existingInstance.id, name: 'new name' },
|
||||
function(err, instance) {
|
||||
if (err) return done(err);
|
||||
observedContexts.should.eql(aTestModelCtx({ query: {
|
||||
where: { id: existingInstance.id }
|
||||
}}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not trigger `access` on missing id', function(done) {
|
||||
TestModel.observe('access', pushContextAndNext());
|
||||
|
||||
TestModel.replaceOrCreate(
|
||||
{ name: 'new name' },
|
||||
function(err, instance) {
|
||||
if (err) return done(err);
|
||||
observedContexts.should.equal('hook not called');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('applies updates from `access` hook when found', function(done) {
|
||||
TestModel.observe('access', function(ctx, next) {
|
||||
ctx.query = { where: { id: { neq: existingInstance.id } } };
|
||||
next();
|
||||
});
|
||||
|
||||
TestModel.replaceOrCreate(
|
||||
{ id: existingInstance.id, name: 'new name' },
|
||||
function(err, instance) {
|
||||
if (err) return done(err);
|
||||
findTestModels({ fields: ['id', 'name' ] }, function(err, list) {
|
||||
if (err) return done(err);
|
||||
(list||[]).map(toObject).should.eql([
|
||||
{ id: existingInstance.id, name: existingInstance.name, extra: undefined },
|
||||
{ id: instance.id, name: 'new name', extra: undefined }
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('applies updates from `access` hook when not found', function(done) {
|
||||
TestModel.observe('access', function(ctx, next) {
|
||||
ctx.query = { where: { id: 'not-found' } };
|
||||
next();
|
||||
});
|
||||
|
||||
TestModel.replaceOrCreate(
|
||||
{ id: existingInstance.id, name: 'new name' },
|
||||
function(err, instance) {
|
||||
if (err) return done(err);
|
||||
findTestModels({ fields: ['id', 'name' ] }, function(err, list) {
|
||||
if (err) return done(err);
|
||||
(list||[]).map(toObject).should.eql([
|
||||
{ id: existingInstance.id, name: existingInstance.name, extra: undefined },
|
||||
{ id: list[1].id, name: 'second', extra: undefined },
|
||||
{ id: instance.id, name: 'new name', extra: undefined }
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers hooks only once', function(done) {
|
||||
TestModel.observe('access', pushNameAndNext('access'));
|
||||
TestModel.observe('before save', pushNameAndNext('before save'));
|
||||
|
||||
TestModel.observe('access', function(ctx, next) {
|
||||
ctx.query = { where: { id: { neq: existingInstance.id } } };
|
||||
next();
|
||||
});
|
||||
|
||||
TestModel.replaceOrCreate(
|
||||
{ id: 'ignored', name: 'new name' },
|
||||
function(err, instance) {
|
||||
if (err) return done(err);
|
||||
observersCalled.should.eql(['access', 'before save']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers `before save` hookon create', function(done) {
|
||||
TestModel.observe('before save', pushContextAndNext());
|
||||
TestModel.replaceOrCreate({id: existingInstance.id, name: 'new name'},
|
||||
function(err, instance) {
|
||||
if (err)
|
||||
return done(err);
|
||||
|
||||
var expectedContext = aTestModelCtx({
|
||||
instance: instance
|
||||
});
|
||||
|
||||
if (!dataSource.connector.replaceOrCreate) {
|
||||
expectedContext.isNewInstance = false;
|
||||
}
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers `before save` hook on replace', function(done) {
|
||||
TestModel.observe('before save', pushContextAndNext());
|
||||
TestModel.replaceOrCreate(
|
||||
{ id: existingInstance.id, name: 'replaced name' },
|
||||
function(err, instance) {
|
||||
if (err) return done(err);
|
||||
|
||||
var expectedContext = aTestModelCtx({
|
||||
instance: {
|
||||
id: existingInstance.id,
|
||||
name: 'replaced name',
|
||||
extra: undefined
|
||||
}
|
||||
});
|
||||
|
||||
if (!dataSource.connector.replaceOrCreate) {
|
||||
expectedContext.isNewInstance = false;
|
||||
}
|
||||
observedContexts.should.eql(expectedContext);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers `before save` hook on create', function(done) {
|
||||
TestModel.observe('before save', pushContextAndNext());
|
||||
|
||||
TestModel.replaceOrCreate(
|
||||
{ id: 'new-id', name: 'a name' },
|
||||
function(err, instance) {
|
||||
if (err) return done(err);
|
||||
|
||||
var expectedContext = aTestModelCtx({
|
||||
instance: {
|
||||
id: 'new-id',
|
||||
name: 'a name',
|
||||
extra: undefined
|
||||
}
|
||||
});
|
||||
|
||||
if (!dataSource.connector.replaceOrCreate) {
|
||||
expectedContext.isNewInstance = true;
|
||||
}
|
||||
observedContexts.should.eql(expectedContext);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('applies updates from `before save` hook on create', function(done) {
|
||||
TestModel.observe('before save', function(ctx, next) {
|
||||
ctx.instance.name = 'hooked';
|
||||
next();
|
||||
});
|
||||
|
||||
TestModel.replaceOrCreate(
|
||||
{ id: 'new-id', name: 'new name' },
|
||||
function(err, instance) {
|
||||
if (err) return done(err);
|
||||
instance.name.should.equal('hooked');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('validates model after `before save` hook on create', function(done) {
|
||||
TestModel.observe('before save', invalidateTestModel());
|
||||
|
||||
TestModel.replaceOrCreate(
|
||||
{ id: 'new-id', name: 'new name' },
|
||||
function(err, instance) {
|
||||
(err || {}).should.be.instanceOf(ValidationError);
|
||||
(err.details.codes || {}).should.eql({ name: ['presence'] });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers `persist` hook on create', function(done) {
|
||||
TestModel.observe('persist', pushContextAndNext());
|
||||
|
||||
TestModel.replaceOrCreate(
|
||||
{ id: 'new-id', name: 'a name' },
|
||||
function(err, instance) {
|
||||
if (err) return done(err);
|
||||
|
||||
var expectedContext = aTestModelCtx({
|
||||
currentInstance: {
|
||||
id: 'new-id',
|
||||
name: 'a name',
|
||||
extra: undefined
|
||||
}, data: {
|
||||
id: 'new-id',
|
||||
name: 'a name'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
if (dataSource.connector.replaceOrCreate) {
|
||||
expectedContext.where = { id: 'new-id' };
|
||||
} else {
|
||||
// non-atomic implementation does not provide ctx.where
|
||||
// because a new instance is being created, so there
|
||||
// are not records to match where filter.
|
||||
expectedContext.isNewInstance = true;
|
||||
}
|
||||
observedContexts.should.eql(expectedContext);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers `persist` hook on replace', function(done) {
|
||||
TestModel.observe('persist', pushContextAndNext());
|
||||
|
||||
TestModel.replaceOrCreate(
|
||||
{ id: existingInstance.id, name: 'replaced name' },
|
||||
function(err, instance) {
|
||||
if (err) return done(err);
|
||||
|
||||
var expected = {
|
||||
where: { id: existingInstance.id },
|
||||
data: {
|
||||
id: existingInstance.id,
|
||||
name: 'replaced name'
|
||||
},
|
||||
currentInstance: {
|
||||
id: existingInstance.id,
|
||||
name: 'replaced name',
|
||||
extra: undefined
|
||||
}
|
||||
};
|
||||
|
||||
var expectedContext = aTestModelCtx(expected);
|
||||
|
||||
var expectedContext;
|
||||
if (!dataSource.connector.replaceOrCreate) {
|
||||
expectedContext.isNewInstance = false;
|
||||
}
|
||||
|
||||
observedContexts.should.eql(expectedContext);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers `loaded` hook on create', function(done) {
|
||||
TestModel.observe('loaded', pushContextAndNext());
|
||||
|
||||
TestModel.replaceOrCreate(
|
||||
{ id: 'new-id', name: 'a name' },
|
||||
function(err, instance) {
|
||||
if (err) return done(err);
|
||||
observedContexts.should.eql(aTestModelCtx({
|
||||
data: {
|
||||
id: 'new-id',
|
||||
name: 'a name'
|
||||
},
|
||||
isNewInstance: true
|
||||
}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers `loaded` hook on replace', function(done) {
|
||||
TestModel.observe('loaded', pushContextAndNext());
|
||||
|
||||
TestModel.replaceOrCreate(
|
||||
{ id: existingInstance.id, name: 'replaced name' },
|
||||
function(err, instance) {
|
||||
if (err) return done(err);
|
||||
|
||||
if (dataSource.connector.replaceOrCreate) {
|
||||
observedContexts.should.eql(aTestModelCtx({
|
||||
data: {
|
||||
id: existingInstance.id,
|
||||
name: 'replaced name'
|
||||
},
|
||||
isNewInstance: false
|
||||
}));
|
||||
} else {
|
||||
// TODO: Please see loopback-datasource-juggler/issues#836
|
||||
//
|
||||
// loaded hook is triggered twice in non-atomic version:
|
||||
// 1) It gets triggered once by "find()" in this chain:
|
||||
// "replaceORCreate()->findOne()->find()",
|
||||
// which is a bug; Please see this ticket:
|
||||
// loopback-datasource-juggler/issues#836.
|
||||
// 2) It, also, gets triggered in "replaceAttributes()"
|
||||
// in this chain replaceORCreate()->replaceAttributes()
|
||||
observedContexts.should.eql([
|
||||
aTestModelCtx({
|
||||
data: {
|
||||
id: existingInstance.id,
|
||||
name: 'first'
|
||||
},
|
||||
isNewInstance: false,
|
||||
options: { notify: false }
|
||||
}),
|
||||
aTestModelCtx({
|
||||
data: {
|
||||
id: existingInstance.id,
|
||||
name: 'replaced name'
|
||||
},
|
||||
isNewInstance: false
|
||||
})
|
||||
]);
|
||||
}
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('emits error when `loaded` hook fails', function(done) {
|
||||
TestModel.observe('loaded', nextWithError(expectedError));
|
||||
TestModel.replaceOrCreate(
|
||||
{ id: 'new-id', name: 'a name' },
|
||||
function(err, instance) {
|
||||
[err].should.eql([expectedError]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers `after save` hook on replace', function(done) {
|
||||
TestModel.observe('after save', pushContextAndNext());
|
||||
|
||||
TestModel.replaceOrCreate(
|
||||
{ id: existingInstance.id, name: 'replaced name' },
|
||||
function(err, instance) {
|
||||
if (err) return done(err);
|
||||
observedContexts.should.eql(aTestModelCtx({
|
||||
instance: {
|
||||
id: existingInstance.id,
|
||||
name: 'replaced name',
|
||||
extra: undefined
|
||||
},
|
||||
isNewInstance: false
|
||||
}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers `after save` hook on create', function(done) {
|
||||
TestModel.observe('after save', pushContextAndNext());
|
||||
|
||||
TestModel.replaceOrCreate(
|
||||
{ id: 'new-id', name: 'a name' },
|
||||
function(err, instance) {
|
||||
if (err) return done(err);
|
||||
observedContexts.should.eql(aTestModelCtx({
|
||||
instance: {
|
||||
id: instance.id,
|
||||
name: 'a name',
|
||||
extra: undefined
|
||||
},
|
||||
isNewInstance: true
|
||||
}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('PersistedModel.deleteAll', function() {
|
||||
it('triggers `access` hook with query', function(done) {
|
||||
TestModel.observe('access', pushContextAndNext());
|
||||
|
|
Loading…
Reference in New Issue