feat: add capability for insert multiple rows in single query
Signed-off-by: Samarpan Bhattacharya <this.is.samy@gmail.com>
This commit is contained in:
parent
cb20ae6575
commit
d29bec72a8
|
@ -47,6 +47,8 @@ function Memory(m, settings) {
|
||||||
|
|
||||||
util.inherits(Memory, Connector);
|
util.inherits(Memory, Connector);
|
||||||
|
|
||||||
|
Memory.prototype.multiInsertSupported = true;
|
||||||
|
|
||||||
Memory.prototype.getDefaultIdType = function() {
|
Memory.prototype.getDefaultIdType = function() {
|
||||||
return Number;
|
return Number;
|
||||||
};
|
};
|
||||||
|
@ -277,6 +279,29 @@ Memory.prototype.create = function create(model, data, options, callback) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Memory.prototype.createAll = function create(model, dataArray, options, callback) {
|
||||||
|
const returnArr = [];
|
||||||
|
async.eachSeries(
|
||||||
|
dataArray,
|
||||||
|
(data, cb) => {
|
||||||
|
this._createSync(model, data, (err, id) => {
|
||||||
|
if (err) {
|
||||||
|
return process.nextTick(function() {
|
||||||
|
cb(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const returnData = Object.assign({}, data);
|
||||||
|
this.setIdValue(model, returnData, id);
|
||||||
|
returnArr.push(returnData);
|
||||||
|
this.saveToFile(id, cb);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
callback(err, returnArr);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
Memory.prototype.updateOrCreate = function(model, data, options, callback) {
|
Memory.prototype.updateOrCreate = function(model, data, options, callback) {
|
||||||
const self = this;
|
const self = this;
|
||||||
this.exists(model, self.getIdValue(model, data), options, function(err, exists) {
|
this.exists(model, self.getIdValue(model, data), options, function(err, exists) {
|
||||||
|
|
236
lib/dao.js
236
lib/dao.js
|
@ -457,6 +457,242 @@ DataAccessObject.create = function(data, options, cb) {
|
||||||
return cb.promise;
|
return cb.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an instances of Model with given data array and save to the attached data source. Callback is optional.
|
||||||
|
* Example:
|
||||||
|
*```js
|
||||||
|
* User.createAll([{first: 'Joe', last: 'Bob'},{first: 'Tom', last: 'Cat'}], function(err, users) {
|
||||||
|
* console.log(users[0] instanceof User); // true
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
* Note: You must include a callback and use the created models provided in the callback if your code depends on your model being
|
||||||
|
* saved or having an ID.
|
||||||
|
*
|
||||||
|
* @param {Object} [dataArray] Optional data object with array of records
|
||||||
|
* @param {Object} [options] Options for create
|
||||||
|
* @param {Function} [cb] Callback function called with these arguments:
|
||||||
|
* - err (null or Error)
|
||||||
|
* - instance (null or Models)
|
||||||
|
*/
|
||||||
|
DataAccessObject.createAll = function(dataArray, options, cb) {
|
||||||
|
const connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
|
||||||
|
if (connectionPromise) {
|
||||||
|
return connectionPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Model = this;
|
||||||
|
const connector = Model.getConnector();
|
||||||
|
if (!connector.multiInsertSupported) {
|
||||||
|
// If multi insert is not supported, then, revert to create method
|
||||||
|
// Array is handled in create method already in legacy code
|
||||||
|
// This ensures backwards compatibility
|
||||||
|
return this.create(dataArray, options, cb);
|
||||||
|
}
|
||||||
|
assert(
|
||||||
|
typeof connector.createAll === 'function',
|
||||||
|
'createAll() must be implemented by the connector',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (options === undefined && cb === undefined) {
|
||||||
|
if (typeof dataArray === 'function') {
|
||||||
|
// create(cb)
|
||||||
|
cb = dataArray;
|
||||||
|
dataArray = [];
|
||||||
|
}
|
||||||
|
} else if (cb === undefined) {
|
||||||
|
if (typeof options === 'function') {
|
||||||
|
// create(data, cb);
|
||||||
|
cb = options;
|
||||||
|
options = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dataArray = dataArray || [];
|
||||||
|
options = options || {};
|
||||||
|
cb = cb || utils.createPromiseCallback();
|
||||||
|
|
||||||
|
assert(typeof dataArray === 'object' && dataArray.length,
|
||||||
|
'The data argument must be an array with length > 0');
|
||||||
|
assert(typeof options === 'object', 'The options argument must be an object');
|
||||||
|
assert(typeof cb === 'function', 'The cb argument must be a function');
|
||||||
|
|
||||||
|
const validationPromises = [];
|
||||||
|
for (let index = 0; index < dataArray.length; index++) {
|
||||||
|
const data = dataArray[index];
|
||||||
|
const hookState = {};
|
||||||
|
|
||||||
|
const enforced = {};
|
||||||
|
let obj;
|
||||||
|
|
||||||
|
try {
|
||||||
|
obj = new Model(data);
|
||||||
|
|
||||||
|
this.applyProperties(enforced, obj);
|
||||||
|
obj.setAttributes(enforced);
|
||||||
|
} catch (err) {
|
||||||
|
process.nextTick(function() {
|
||||||
|
cb(err);
|
||||||
|
});
|
||||||
|
return cb.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
Model = this.lookupModel(data); // data-specific
|
||||||
|
if (Model !== obj.constructor) obj = new Model(data);
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
Model: Model,
|
||||||
|
instance: obj,
|
||||||
|
isNewInstance: true,
|
||||||
|
hookState: hookState,
|
||||||
|
options: options,
|
||||||
|
};
|
||||||
|
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
Model.notifyObserversOf('before save', context, function(err) {
|
||||||
|
if (err) return reject({
|
||||||
|
error: err,
|
||||||
|
data: obj,
|
||||||
|
});
|
||||||
|
|
||||||
|
const d = obj.toObject(true);
|
||||||
|
|
||||||
|
// options has precedence on model-setting
|
||||||
|
if (options.validate === false) {
|
||||||
|
return resolve(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
// only when options.validate is not set, take model-setting into consideration
|
||||||
|
if (
|
||||||
|
options.validate === undefined &&
|
||||||
|
Model.settings.automaticValidation === false
|
||||||
|
) {
|
||||||
|
return resolve(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
// validation required
|
||||||
|
obj.isValid(
|
||||||
|
function(valid) {
|
||||||
|
if (valid) {
|
||||||
|
resolve(obj);
|
||||||
|
} else {
|
||||||
|
reject({
|
||||||
|
error: new ValidationError(obj),
|
||||||
|
data: obj,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
d,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
validationPromises.push(promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all(validationPromises).then((objArray) => {
|
||||||
|
const values = [];
|
||||||
|
const valMap = new Map();
|
||||||
|
objArray.forEach((obj) => {
|
||||||
|
const val = Model._sanitizeData(obj.toObject(true), options);
|
||||||
|
values.push(val);
|
||||||
|
valMap.set(obj, applyDefaultsOnWrites(val, Model.definition));
|
||||||
|
});
|
||||||
|
|
||||||
|
function createCallback(err, savedArray) {
|
||||||
|
if (err) {
|
||||||
|
return cb(err, objArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = values.map((val) => {
|
||||||
|
return {
|
||||||
|
Model: Model,
|
||||||
|
data: val,
|
||||||
|
isNewInstance: true,
|
||||||
|
hookState: {},
|
||||||
|
options: options,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
Model.notifyObserversOf('loaded', context, function(err) {
|
||||||
|
if (err) return cb(err);
|
||||||
|
|
||||||
|
const afterSavePromises = [];
|
||||||
|
savedArray.map((obj) => {
|
||||||
|
const dataModel = new Model(obj);
|
||||||
|
|
||||||
|
let afterSavePromise;
|
||||||
|
if (options.notify !== false) {
|
||||||
|
const context = {
|
||||||
|
Model: Model,
|
||||||
|
instance: dataModel,
|
||||||
|
isNewInstance: true,
|
||||||
|
hookState: {},
|
||||||
|
options: options,
|
||||||
|
};
|
||||||
|
|
||||||
|
afterSavePromise = new Promise((resolve, reject) => {
|
||||||
|
Model.notifyObserversOf('after save', context, function(err) {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(dataModel);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
afterSavePromises.push(afterSavePromise);
|
||||||
|
} else {
|
||||||
|
afterSavePromises.push(Promise.resolve(dataModel));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all(afterSavePromises).then(saved => {
|
||||||
|
cb(null, saved);
|
||||||
|
}).catch(err => {
|
||||||
|
cb(err, objArray);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
context = objArray.map(obj => {
|
||||||
|
return {
|
||||||
|
Model: Model,
|
||||||
|
data: valMap.get(obj),
|
||||||
|
isNewInstance: true,
|
||||||
|
currentInstance: obj,
|
||||||
|
hookState: {},
|
||||||
|
options: options,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const persistPromise = new Promise((resolve, reject) => {
|
||||||
|
Model.notifyObserversOf('persist', context, function(err, ctx) {
|
||||||
|
if (err) return reject(err);
|
||||||
|
|
||||||
|
const objDataArray = ctx
|
||||||
|
.map((obj) => {
|
||||||
|
return obj.currentInstance.constructor._forDB(obj.data);
|
||||||
|
})
|
||||||
|
.filter((objData) => !!objData);
|
||||||
|
resolve(objDataArray);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
persistPromise.then((objDataArray) => {
|
||||||
|
invokeConnectorMethod(
|
||||||
|
connector,
|
||||||
|
'createAll',
|
||||||
|
Model,
|
||||||
|
[objDataArray],
|
||||||
|
options,
|
||||||
|
createCallback,
|
||||||
|
);
|
||||||
|
}).catch((err) => {
|
||||||
|
err && cb(err);
|
||||||
|
});
|
||||||
|
}).catch((err) => {
|
||||||
|
err && cb(err.error, err.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
return cb.promise;
|
||||||
|
};
|
||||||
|
|
||||||
// Implementation of applyDefaultOnWrites property
|
// Implementation of applyDefaultOnWrites property
|
||||||
function applyDefaultsOnWrites(obj, modelDefinition) {
|
function applyDefaultsOnWrites(obj, modelDefinition) {
|
||||||
for (const key in modelDefinition.properties) {
|
for (const key in modelDefinition.properties) {
|
||||||
|
|
|
@ -31,8 +31,9 @@
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"build": "npm run build-ts-types",
|
"build": "npm run build-ts-types",
|
||||||
"build-ts-types": "tsc -p tsconfig.json --outDir dist",
|
"build-ts-types": "tsc -p tsconfig.json --outDir dist",
|
||||||
|
"pretest": "npm run build",
|
||||||
"test": "nyc mocha",
|
"test": "nyc mocha",
|
||||||
"posttest": "npm run tsc && npm run lint"
|
"posttest": "npm run lint"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^17.2.0",
|
"@commitlint/cli": "^17.2.0",
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -69,27 +69,6 @@ Object.defineProperty(module.exports, 'skip', {
|
||||||
value: skip,
|
value: skip,
|
||||||
});
|
});
|
||||||
|
|
||||||
function clearAndCreate(model, data, callback) {
|
|
||||||
const createdItems = [];
|
|
||||||
model.destroyAll(function() {
|
|
||||||
nextItem(null, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
let itemIndex = 0;
|
|
||||||
|
|
||||||
function nextItem(err, lastItem) {
|
|
||||||
if (lastItem !== null) {
|
|
||||||
createdItems.push(lastItem);
|
|
||||||
}
|
|
||||||
if (itemIndex >= data.length) {
|
|
||||||
callback(createdItems);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
model.create(data[itemIndex], nextItem);
|
|
||||||
itemIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint-disable mocha/handle-done-callback */
|
/* eslint-disable mocha/handle-done-callback */
|
||||||
function testOrm(dataSource) {
|
function testOrm(dataSource) {
|
||||||
const requestsAreCounted = dataSource.name !== 'mongodb';
|
const requestsAreCounted = dataSource.name !== 'mongodb';
|
||||||
|
@ -251,6 +230,45 @@ function testOrm(dataSource) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should save objects when createAll is invoked', function(test) {
|
||||||
|
const title = 'Initial title',
|
||||||
|
title2 = 'Hello world',
|
||||||
|
date = new Date();
|
||||||
|
|
||||||
|
Post.createAll(
|
||||||
|
[{
|
||||||
|
title: title,
|
||||||
|
date: date,
|
||||||
|
}],
|
||||||
|
function(err, objs) {
|
||||||
|
const obj = objs[0];
|
||||||
|
test.ok(obj.id, 'Object id should present');
|
||||||
|
test.equals(obj.title, title);
|
||||||
|
// test.equals(obj.date, date);
|
||||||
|
obj.title = title2;
|
||||||
|
test.ok(obj.propertyChanged('title'), 'Title changed');
|
||||||
|
obj.save(function(err, obj) {
|
||||||
|
test.equal(obj.title, title2);
|
||||||
|
test.ok(!obj.propertyChanged('title'));
|
||||||
|
|
||||||
|
const p = new Post({title: 1});
|
||||||
|
p.title = 2;
|
||||||
|
p.save(function(err, obj) {
|
||||||
|
test.ok(!p.propertyChanged('title'));
|
||||||
|
p.title = 3;
|
||||||
|
test.ok(p.propertyChanged('title'));
|
||||||
|
test.equal(p.title_was, 2);
|
||||||
|
p.save(function() {
|
||||||
|
test.equal(p.title_was, 3);
|
||||||
|
test.ok(!p.propertyChanged('title'));
|
||||||
|
test.done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should create object with initial data', function(test) {
|
it('should create object with initial data', function(test) {
|
||||||
const title = 'Initial title',
|
const title = 'Initial title',
|
||||||
date = new Date;
|
date = new Date;
|
||||||
|
|
|
@ -632,10 +632,13 @@ function seed(done) {
|
||||||
{id: 5, seq: 5, name: 'Stuart Sutcliffe', order: 3, vip: true},
|
{id: 5, seq: 5, name: 'Stuart Sutcliffe', order: 3, vip: true},
|
||||||
];
|
];
|
||||||
|
|
||||||
async.series([
|
async.series(
|
||||||
User.destroyAll.bind(User),
|
[
|
||||||
function(cb) {
|
User.destroyAll.bind(User),
|
||||||
async.each(beatles, User.create.bind(User), cb);
|
function(cb) {
|
||||||
},
|
User.createAll(beatles, cb);
|
||||||
], done);
|
},
|
||||||
|
],
|
||||||
|
done,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,8 +25,16 @@ ContextRecorder.prototype.recordAndNext = function(transformFm) {
|
||||||
transformFm(context);
|
transformFm(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
context = deepCloneToObject(context);
|
if (Array.isArray(context)) {
|
||||||
context.hookState.test = true;
|
context = context.map(ctx => {
|
||||||
|
const ctxCopy = deepCloneToObject(ctx);
|
||||||
|
ctxCopy.hookState.test = true;
|
||||||
|
return ctxCopy;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
context = deepCloneToObject(context);
|
||||||
|
context.hookState.test = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof self.records === 'string') {
|
if (typeof self.records === 'string') {
|
||||||
self.records = context;
|
self.records = context;
|
||||||
|
@ -37,7 +45,11 @@ ContextRecorder.prototype.recordAndNext = function(transformFm) {
|
||||||
self.records = [self.records];
|
self.records = [self.records];
|
||||||
}
|
}
|
||||||
|
|
||||||
self.records.push(context);
|
if (Array.isArray(context)) {
|
||||||
|
self.records.push(...context);
|
||||||
|
} else {
|
||||||
|
self.records.push(context);
|
||||||
|
}
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -671,6 +671,310 @@ module.exports = function(dataSource, should, connectorCapabilities) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('PersistedModel.createAll', function() {
|
||||||
|
it('triggers hooks in the correct order', function(done) {
|
||||||
|
monitorHookExecution();
|
||||||
|
|
||||||
|
TestModel.createAll(
|
||||||
|
[{name: '1'}, {name: '2'}],
|
||||||
|
function(err) {
|
||||||
|
if (err) return done(err);
|
||||||
|
|
||||||
|
hookMonitor.names.should.eql([
|
||||||
|
'before save',
|
||||||
|
'before save',
|
||||||
|
'persist',
|
||||||
|
'loaded',
|
||||||
|
'after save',
|
||||||
|
'after save',
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('aborts when `after save` fires when option to notify is false', function(done) {
|
||||||
|
monitorHookExecution();
|
||||||
|
|
||||||
|
TestModel.create(
|
||||||
|
[{name: '1'}, {name: '2'}],
|
||||||
|
{notify: false},
|
||||||
|
function(err) {
|
||||||
|
if (err) return done(err);
|
||||||
|
|
||||||
|
hookMonitor.names.should.not.containEql('after save');
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers `before save` hook for each item in the array', function(done) {
|
||||||
|
TestModel.observe('before save', ctxRecorder.recordAndNext());
|
||||||
|
|
||||||
|
TestModel.createAll([{name: '1'}, {name: '2'}], function(err, list) {
|
||||||
|
if (err) return done(err);
|
||||||
|
// Creation of multiple instances is executed in parallel
|
||||||
|
ctxRecorder.records.sort(function(c1, c2) {
|
||||||
|
return c1.instance.name - c2.instance.name;
|
||||||
|
});
|
||||||
|
ctxRecorder.records.should.eql([
|
||||||
|
aCtxForModel(TestModel, {
|
||||||
|
instance: {id: list[0].id, name: '1', extra: undefined},
|
||||||
|
isNewInstance: true,
|
||||||
|
}),
|
||||||
|
aCtxForModel(TestModel, {
|
||||||
|
instance: {id: list[1].id, name: '2', extra: undefined},
|
||||||
|
isNewInstance: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('aborts when `before save` hook fails', function(done) {
|
||||||
|
TestModel.observe('before save', nextWithError(expectedError));
|
||||||
|
|
||||||
|
TestModel.createAll([{name: '1'}, {name: '2'}], function(err) {
|
||||||
|
err.should.eql(expectedError);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies updates from `before save` hook to each item in the array', function(done) {
|
||||||
|
TestModel.observe('before save', function(ctx, next) {
|
||||||
|
ctx.instance.should.be.instanceOf(TestModel);
|
||||||
|
ctx.instance.extra = 'hook data';
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
TestModel.createAll(
|
||||||
|
[{id: uid.next(), name: 'a-name'}, {id: uid.next(), name: 'b-name'}],
|
||||||
|
function(err, instances) {
|
||||||
|
if (err) return done(err);
|
||||||
|
instances.forEach(instance => {
|
||||||
|
instance.should.have.property('extra', 'hook data');
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates model after `before save` hook', function(done) {
|
||||||
|
TestModel.observe('before save', invalidateTestModel());
|
||||||
|
|
||||||
|
TestModel.createAll([{name: 'created1'}, {name: 'created2'}], function(err) {
|
||||||
|
(err || {}).should.be.instanceOf(ValidationError);
|
||||||
|
(err.details.codes || {}).should.eql({name: ['presence']});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers `persist` hook', function(done) {
|
||||||
|
TestModel.observe('persist', ctxRecorder.recordAndNext());
|
||||||
|
|
||||||
|
TestModel.createAll(
|
||||||
|
[{id: 'new-id-1', name: 'a name'}, {id: 'new-id-2', name: 'b name'}],
|
||||||
|
function(err, instances) {
|
||||||
|
if (err) return done(err);
|
||||||
|
|
||||||
|
ctxRecorder.records.should.eql([
|
||||||
|
aCtxForModel(TestModel, {
|
||||||
|
data: {id: 'new-id-1', name: 'a name'},
|
||||||
|
isNewInstance: true,
|
||||||
|
currentInstance: {extra: null, id: 'new-id-1', name: 'a name'},
|
||||||
|
}),
|
||||||
|
aCtxForModel(TestModel, {
|
||||||
|
data: {id: 'new-id-2', name: 'b name'},
|
||||||
|
isNewInstance: true,
|
||||||
|
currentInstance: {extra: null, id: 'new-id-2', name: 'b name'},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies updates from `persist` hook', function(done) {
|
||||||
|
TestModel.observe(
|
||||||
|
'persist',
|
||||||
|
ctxRecorder.recordAndNext(function(ctxArr) {
|
||||||
|
// It's crucial to change `ctx.data` reference, not only data props
|
||||||
|
ctxArr.forEach(ctx => {
|
||||||
|
ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// By default, the instance passed to create callback is NOT updated
|
||||||
|
// with the changes made through persist/loaded hooks. To preserve
|
||||||
|
// backwards compatibility, we introduced a new setting updateOnLoad,
|
||||||
|
// which if set, will apply these changes to the model instance too.
|
||||||
|
TestModel.settings.updateOnLoad = true;
|
||||||
|
TestModel.createAll(
|
||||||
|
[{id: 'new-id', name: 'a name'}],
|
||||||
|
function(err, instances) {
|
||||||
|
if (err) return done(err);
|
||||||
|
|
||||||
|
instances.forEach(instance => {
|
||||||
|
instance.should.have.property('extra', 'hook data');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also query the database here to verify that, on `create`
|
||||||
|
// updates from `persist` hook are reflected into database
|
||||||
|
TestModel.findById('new-id', function(err, dbInstance) {
|
||||||
|
if (err) return done(err);
|
||||||
|
should.exists(dbInstance);
|
||||||
|
dbInstance.toObject(true).should.eql({
|
||||||
|
id: 'new-id',
|
||||||
|
name: 'a name',
|
||||||
|
extra: 'hook data',
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers `loaded` hook', function(done) {
|
||||||
|
TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
||||||
|
|
||||||
|
// By default, the instance passed to create callback is NOT updated
|
||||||
|
// with the changes made through persist/loaded hooks. To preserve
|
||||||
|
// backwards compatibility, we introduced a new setting updateOnLoad,
|
||||||
|
// which if set, will apply these changes to the model instance too.
|
||||||
|
TestModel.settings.updateOnLoad = true;
|
||||||
|
TestModel.createAll(
|
||||||
|
[
|
||||||
|
{id: 'new-id-1', name: 'a name'},
|
||||||
|
{id: 'new-id-2', name: 'b name'},
|
||||||
|
],
|
||||||
|
function(err) {
|
||||||
|
if (err) return done(err);
|
||||||
|
|
||||||
|
ctxRecorder.records.sort(function(c1, c2) {
|
||||||
|
return c1.data.name - c2.data.name;
|
||||||
|
});
|
||||||
|
ctxRecorder.records.should.eql([
|
||||||
|
aCtxForModel(TestModel, {
|
||||||
|
data: {id: 'new-id-1', name: 'a name'},
|
||||||
|
isNewInstance: true,
|
||||||
|
}),
|
||||||
|
aCtxForModel(TestModel, {
|
||||||
|
data: {id: 'new-id-2', name: 'b name'},
|
||||||
|
isNewInstance: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits error when `loaded` hook fails', function(done) {
|
||||||
|
TestModel.observe('loaded', nextWithError(expectedError));
|
||||||
|
TestModel.createAll(
|
||||||
|
[{id: 'new-id', name: 'a name'}],
|
||||||
|
function(err) {
|
||||||
|
err.should.eql(expectedError);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies updates from `loaded` hook', function(done) {
|
||||||
|
TestModel.observe(
|
||||||
|
'loaded',
|
||||||
|
ctxRecorder.recordAndNext(function(ctx) {
|
||||||
|
// It's crucial to change `ctx.data` reference, not only data props
|
||||||
|
ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// By default, the instance passed to create callback is NOT updated
|
||||||
|
// with the changes made through persist/loaded hooks. To preserve
|
||||||
|
// backwards compatibility, we introduced a new setting updateOnLoad,
|
||||||
|
// which if set, will apply these changes to the model instance too.
|
||||||
|
TestModel.settings.updateOnLoad = true;
|
||||||
|
TestModel.create(
|
||||||
|
[{id: 'new-id', name: 'a name'}],
|
||||||
|
function(err, instances) {
|
||||||
|
if (err) return done(err);
|
||||||
|
|
||||||
|
instances.forEach((instance) => {
|
||||||
|
instance.should.have.property('extra', 'hook data');
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers `after save` hook', function(done) {
|
||||||
|
TestModel.observe('after save', ctxRecorder.recordAndNext());
|
||||||
|
|
||||||
|
TestModel.createAll([{name: '1'}, {name: '2'}], function(err, list) {
|
||||||
|
if (err) return done(err);
|
||||||
|
|
||||||
|
ctxRecorder.records.sort(function(c1, c2) {
|
||||||
|
return c1.instance.name - c2.instance.name;
|
||||||
|
});
|
||||||
|
ctxRecorder.records.should.eql([
|
||||||
|
aCtxForModel(TestModel, {
|
||||||
|
instance: {id: list[0].id, name: '1', extra: undefined},
|
||||||
|
isNewInstance: true,
|
||||||
|
}),
|
||||||
|
aCtxForModel(TestModel, {
|
||||||
|
instance: {id: list[1].id, name: '2', extra: undefined},
|
||||||
|
isNewInstance: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('aborts when `after save` hook fails', function(done) {
|
||||||
|
TestModel.observe('after save', nextWithError(expectedError));
|
||||||
|
|
||||||
|
TestModel.createAll([{name: 'created'}], 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();
|
||||||
|
});
|
||||||
|
|
||||||
|
TestModel.createAll([
|
||||||
|
{name: 'a-name'},
|
||||||
|
{name: 'b-name'},
|
||||||
|
], function(err, instances) {
|
||||||
|
if (err) return done(err);
|
||||||
|
instances.forEach((instance) => {
|
||||||
|
instance.should.have.property('extra', 'hook data');
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('do not emit `after save` when before save fails for even one', function(done) {
|
||||||
|
TestModel.observe('before save', function(ctx, next) {
|
||||||
|
if (ctx.instance.name === 'fail') next(expectedError);
|
||||||
|
else next();
|
||||||
|
});
|
||||||
|
|
||||||
|
TestModel.observe('after save', ctxRecorder.recordAndNext());
|
||||||
|
|
||||||
|
TestModel.createAll([{name: 'ok'}, {name: 'fail'}], function(err, list) {
|
||||||
|
err.should.eql(expectedError);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('PersistedModel.findOrCreate', function() {
|
describe('PersistedModel.findOrCreate', function() {
|
||||||
it('triggers `access` hook', function(done) {
|
it('triggers `access` hook', function(done) {
|
||||||
TestModel.observe('access', ctxRecorder.recordAndNext());
|
TestModel.observe('access', ctxRecorder.recordAndNext());
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
// This file is licensed under the MIT License.
|
// This file is licensed under the MIT License.
|
||||||
// License text available at https://opensource.org/licenses/MIT
|
// License text available at https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
import {Callback, Options, PromiseOrVoid} from './common';
|
import { Callback, Options, PromiseOrVoid } from './common';
|
||||||
import {ModelBase, ModelData} from './model';
|
import { ModelBase, ModelData } from './model';
|
||||||
import {Filter, Where} from './query';
|
import { Filter, Where } from './query';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data object for persisted models
|
* Data object for persisted models
|
||||||
|
@ -47,6 +47,21 @@ export declare class PersistedModel extends ModelBase {
|
||||||
callback?: Callback<PersistedModel[]>,
|
callback?: Callback<PersistedModel[]>,
|
||||||
): PromiseOrVoid<PersistedModel[]>;
|
): PromiseOrVoid<PersistedModel[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an array of new instances of Model, and save to database in one DB query.
|
||||||
|
*
|
||||||
|
* @param {Object[]} [data] Optional data argument. An array of instances.
|
||||||
|
*
|
||||||
|
* @callback {Function} callback Callback function called with `cb(err, obj)` signature.
|
||||||
|
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
||||||
|
* @param {Object} models Model instances or null.
|
||||||
|
*/
|
||||||
|
static createAll(
|
||||||
|
data: PersistedData[],
|
||||||
|
options?: Options,
|
||||||
|
callback?: Callback<PersistedModel[]>,
|
||||||
|
): PromiseOrVoid<PersistedModel[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update or insert a model instance
|
* Update or insert a model instance
|
||||||
* @param {Object} data The model instance data to insert.
|
* @param {Object} data The model instance data to insert.
|
||||||
|
|
Loading…
Reference in New Issue