From 5974c6afdf73ecc4604ffcd686332c3f3d3d0838 Mon Sep 17 00:00:00 2001 From: crandmck Date: Fri, 8 Jan 2016 17:10:44 -0800 Subject: [PATCH 001/187] Pull in API doc fix from PR into master #1910 --- lib/persisted-model.js | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/persisted-model.js b/lib/persisted-model.js index b3bb62281..ab17ef16c 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -111,13 +111,29 @@ module.exports = function(registry) { }; /** - * Find one record matching the optional `where` filter. The same as `find`, but limited to one object. - * Returns an object, not collection. - * If not found, create the object using data provided as second argument. + * Finds one record matching the optional filter object. If not found, creates + * the object using the data provided as second argument. In this sense it is + * the same as `find`, but limited to one object. Returns an object, not + * collection. If you don't provide the filter object argument, it tries to + * locate an existing object that matches the `data` argument. * - * @param {Object} where Where clause, such as `{test: 'me'}` - *
see - * [Where filter](https://docs.strongloop.com/display/LB/Where+filter#Wherefilter-Whereclauseforothermethods). + * @options {Object} [filter] Optional Filter object; see below. + * @property {String|Object|Array} fields Identify fields to include in return result. + *
See [Fields filter](http://docs.strongloop.com/display/LB/Fields+filter). + * @property {String|Object|Array} include See PersistedModel.include documentation. + *
See [Include filter](http://docs.strongloop.com/display/LB/Include+filter). + * @property {Number} limit Maximum number of instances to return. + *
See [Limit filter](http://docs.strongloop.com/display/LB/Limit+filter). + * @property {String} order Sort order: either "ASC" for ascending or "DESC" for descending. + *
See [Order filter](http://docs.strongloop.com/display/LB/Order+filter). + * @property {Number} skip Number of results to skip. + *
See [Skip filter](http://docs.strongloop.com/display/LB/Skip+filter). + * @property {Object} where Where clause, like + * ``` + * {where: {key: val, key2: {gt: val2}, ...}} + * ``` + *
See + * [Where filter](https://docs.strongloop.com/display/LB/Where+filter#Wherefilter-Whereclauseforqueries). * @param {Object} data Data to insert if object matching the `where` filter is not found. * @callback {Function} callback Callback function called with `cb(err, instance)` arguments. Required. * @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object). From 8deec2e89aeea65dfbd8a9f43e2dc15a11faef15 Mon Sep 17 00:00:00 2001 From: Amir Jafarian Date: Thu, 17 Dec 2015 12:02:35 -0500 Subject: [PATCH 002/187] Checkpoint speedup --- common/models/checkpoint.js | 64 ++++++++++++++++---------------- lib/persisted-model.js | 7 +--- test/checkpoint.test.js | 74 ++++++++++++++++++++++++++++++++----- 3 files changed, 99 insertions(+), 46 deletions(-) diff --git a/common/models/checkpoint.js b/common/models/checkpoint.js index 304f83148..59cc33de0 100644 --- a/common/models/checkpoint.js +++ b/common/models/checkpoint.js @@ -27,43 +27,45 @@ module.exports = function(Checkpoint) { * Get the current checkpoint id * @callback {Function} callback * @param {Error} err - * @param {Number} checkpointId The current checkpoint id + * @param {Number} checkpoint The current checkpoint seq */ - Checkpoint.current = function(cb) { var Checkpoint = this; - this.find({ - limit: 1, - order: 'seq DESC' - }, function(err, checkpoints) { - if (err) return cb(err); - var checkpoint = checkpoints[0]; - if (checkpoint) { - cb(null, checkpoint.seq); - } else { - Checkpoint.create({ seq: 1 }, function(err, checkpoint) { - if (err) return cb(err); - cb(null, checkpoint.seq); - }); - } + Checkpoint._getSingleton(function(err, cp) { + cb(err, cp.seq); }); }; - Checkpoint.observe('before save', function(ctx, next) { - if (!ctx.instance) { - // Example: Checkpoint.updateAll() and Checkpoint.updateOrCreate() - return next(new Error('Checkpoint does not support partial updates.')); - } + Checkpoint._getSingleton = function(cb) { + var query = {limit: 1}; // match all instances, return only one + var initialData = {seq: 1}; + this.findOrCreate(query, initialData, cb); + }; - var model = ctx.instance; - if (!model.getId() && model.seq === undefined) { - model.constructor.current(function(err, seq) { - if (err) return next(err); - model.seq = seq + 1; - next(); + /** + * Increase the current checkpoint if it already exists otherwise initialize it + * @callback {Function} callback + * @param {Error} err + * @param {Object} checkpoint The current checkpoint + */ + Checkpoint.bumpLastSeq = function(cb) { + var Checkpoint = this; + Checkpoint._getSingleton(function(err, cp) { + if (err) return cb(err); + var originalSeq = cp.seq; + cp.seq++; + // Update the checkpoint but only if it was not changed under our hands + Checkpoint.updateAll({id: cp.id, seq: originalSeq}, {seq: cp.seq}, function(err, info) { + if (err) return cb(err); + // possible outcomes + // 1) seq was updated to seq+1 - exactly what we wanted! + // 2) somebody else already updated seq to seq+1 and our call was a no-op. + // That should be ok, checkpoints are time based, so we reuse the one created just now + // 3) seq was bumped more than once, so we will be using a value that is behind the latest seq. + // @bajtos is not entirely sure if this is ok, but since it wasn't handled by the current implementation either, + // he thinks we can keep it this way. + cb(null, cp); }); - } else { - next(); - } - }); + }); + }; }; diff --git a/lib/persisted-model.js b/lib/persisted-model.js index ab17ef16c..bcac8e18d 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -898,12 +898,7 @@ module.exports = function(registry) { PersistedModel.checkpoint = function(cb) { var Checkpoint = this.getChangeModel().getCheckpointModel(); - this.getSourceId(function(err, sourceId) { - if (err) return cb(err); - Checkpoint.create({ - sourceId: sourceId - }, cb); - }); + Checkpoint.bumpLastSeq(cb); }; /** diff --git a/test/checkpoint.test.js b/test/checkpoint.test.js index 98e248efb..c824d02ea 100644 --- a/test/checkpoint.test.js +++ b/test/checkpoint.test.js @@ -1,20 +1,22 @@ var async = require('async'); var loopback = require('../'); +var expect = require('chai').expect; -// create a unique Checkpoint model var Checkpoint = loopback.Checkpoint.extend('TestCheckpoint'); -var memory = loopback.createDataSource({ - connector: loopback.Memory -}); -Checkpoint.attachTo(memory); - describe('Checkpoint', function() { - describe('current()', function() { + describe('bumpLastSeq() and current()', function() { + beforeEach(function() { + var memory = loopback.createDataSource({ + connector: loopback.Memory + }); + Checkpoint.attachTo(memory); + }); + it('returns the highest `seq` value', function(done) { async.series([ - Checkpoint.create.bind(Checkpoint), - Checkpoint.create.bind(Checkpoint), + Checkpoint.bumpLastSeq.bind(Checkpoint), + Checkpoint.bumpLastSeq.bind(Checkpoint), function(next) { Checkpoint.current(function(err, seq) { if (err) next(err); @@ -24,5 +26,59 @@ describe('Checkpoint', function() { } ], done); }); + + it('Should be no race condition for current() when calling in parallel', function(done) { + async.parallel([ + function(next) { Checkpoint.current(next); }, + function(next) { Checkpoint.current(next); } + ], function(err, list) { + if (err) return done(err); + Checkpoint.find(function(err, data) { + if (err) return done(err); + expect(data).to.have.length(1); + done(); + }); + }); + }); + + it('Should be no race condition for bumpLastSeq() when calling in parallel', function(done) { + async.parallel([ + function(next) { Checkpoint.bumpLastSeq(next); }, + function(next) { Checkpoint.bumpLastSeq(next); } + ], function(err, list) { + if (err) return done(err); + Checkpoint.find(function(err, data) { + if (err) return done(err); + // The invariant "we have at most 1 checkpoint instance" is preserved + // even when multiple calls are made in parallel + expect(data).to.have.length(1); + // There is a race condition here, we could end up with both 2 or 3 as the "seq". + // The current implementation of the memory connector always yields 2 though. + expect(data[0].seq).to.equal(2); + // In this particular case, since the new last seq is always 2, both results + // should be 2. + expect(list.map(function(it) {return it.seq;})) + .to.eql([2, 2]); + done(); + }); + }); + }); + + it('Checkpoint.current() for non existing checkpoint should initialize checkpoint', function(done) { + Checkpoint.current(function(err, seq) { + expect(seq).to.equal(1); + done(err); + }); + }); + + it('bumpLastSeq() works when singleton instance does not exists yet', function(done) { + Checkpoint.bumpLastSeq(function(err, cp) { + // We expect `seq` to be 2 since `checkpoint` does not exist and + // `bumpLastSeq` for the first time not only initializes it to one, + // but also increments the initialized value by one. + expect(cp.seq).to.equal(2); + done(err); + }); + }); }); }); From 17bd1016910780cafbab463c669ea70fbd49d8ef Mon Sep 17 00:00:00 2001 From: Ryan Graham Date: Thu, 31 Dec 2015 16:52:31 -0800 Subject: [PATCH 003/187] ensure app is booted before integration tests --- test/access-control.integration.js | 6 ++++++ test/relations.integration.js | 6 ++++++ test/remoting.integration.js | 6 ++++++ test/user.integration.js | 6 ++++++ 4 files changed, 24 insertions(+) diff --git a/test/access-control.integration.js b/test/access-control.integration.js index 3125a6535..7e417bfe3 100644 --- a/test/access-control.integration.js +++ b/test/access-control.integration.js @@ -11,6 +11,12 @@ var CURRENT_USER = {email: 'current@test.test', password: 'test'}; var debug = require('debug')('loopback:test:access-control.integration'); describe('access control - integration', function() { + before(function(done) { + if (app.booting) { + return app.once('booted', done); + } + done(); + }); lt.beforeEach.withApp(app); diff --git a/test/relations.integration.js b/test/relations.integration.js index 70f1ae122..5257338b4 100644 --- a/test/relations.integration.js +++ b/test/relations.integration.js @@ -11,6 +11,12 @@ var debug = require('debug')('loopback:test:relations.integration'); var async = require('async'); describe('relations - integration', function() { + before(function(done) { + if (app.booting) { + return app.once('booted', done); + } + done(); + }); lt.beforeEach.withApp(app); diff --git a/test/remoting.integration.js b/test/remoting.integration.js index c3288d799..b2b92cd81 100644 --- a/test/remoting.integration.js +++ b/test/remoting.integration.js @@ -6,6 +6,12 @@ var app = require(path.join(SIMPLE_APP, 'server/server.js')); var assert = require('assert'); describe('remoting - integration', function() { + before(function(done) { + if (app.booting) { + return app.once('booted', done); + } + done(); + }); lt.beforeEach.withApp(app); lt.beforeEach.givenModel('store'); diff --git a/test/user.integration.js b/test/user.integration.js index b2a92537b..fd4a198bc 100644 --- a/test/user.integration.js +++ b/test/user.integration.js @@ -7,6 +7,12 @@ var app = require(path.join(SIMPLE_APP, 'server/server.js')); var expect = require('chai').expect; describe('users - integration', function() { + before(function(done) { + if (app.booting) { + return app.once('booted', done); + } + done(); + }); lt.beforeEach.withApp(app); From aff49ff63fe37203658df7bf10e7942128ca6406 Mon Sep 17 00:00:00 2001 From: Ryan Graham Date: Thu, 31 Dec 2015 16:53:40 -0800 Subject: [PATCH 004/187] test: fail on error instead of crash If the supertest request fails its basic assertions, there may not even be a body to perform checks against, so bail early when possible. --- test/relations.integration.js | 52 ++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/test/relations.integration.js b/test/relations.integration.js index 5257338b4..dbb2e2556 100644 --- a/test/relations.integration.js +++ b/test/relations.integration.js @@ -97,6 +97,7 @@ describe('relations - integration', function() { this.get(url) .query({'filter': {'include' : 'pictures'}}) .expect(200, function(err, res) { + if (err) return done(err); // console.log(res.body); expect(res.body.name).to.be.equal('Reader 1'); expect(res.body.pictures).to.be.eql([ @@ -112,6 +113,7 @@ describe('relations - integration', function() { this.get(url) .query({'filter': {'include' : 'imageable'}}) .expect(200, function(err, res) { + if (err) return done(err); // console.log(res.body); expect(res.body[0].name).to.be.equal('Picture 1'); expect(res.body[1].name).to.be.equal('Picture 2'); @@ -125,6 +127,7 @@ describe('relations - integration', function() { this.get(url) .query({'filter': {'include' : {'relation': 'imageable', 'scope': { 'include' : 'team'}}}}) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body[0].name).to.be.equal('Picture 1'); expect(res.body[1].name).to.be.equal('Picture 2'); expect(res.body[0].imageable.name).to.be.eql('Reader 1'); @@ -139,6 +142,7 @@ describe('relations - integration', function() { it('should invoke scoped methods remotely', function(done) { this.get('/api/stores/superStores') .expect(200, function(err, res) { + if (err) return done(err); expect(res.body).to.be.array; done(); }); @@ -375,7 +379,7 @@ describe('relations - integration', function() { self.url = root.relUrl; self.patient = root.patient; self.physician = root.physician; - done(); + done(err); }); }); @@ -414,7 +418,7 @@ describe('relations - integration', function() { self.url = root.relUrl; self.patient = root.patient; self.physician = root.physician; - done(); + done(err); }); }); @@ -459,7 +463,7 @@ describe('relations - integration', function() { self.url = root.relUrl; self.patient = root.patient; self.physician = root.physician; - done(); + done(err); }); }); @@ -479,7 +483,7 @@ describe('relations - integration', function() { '/patients/rel/' + '999'; self.patient = root.patient; self.physician = root.physician; - done(); + done(err); }); }); @@ -498,7 +502,7 @@ describe('relations - integration', function() { self.url = root.relUrl; self.patient = root.patient; self.physician = root.physician; - done(); + done(err); }); }); @@ -553,7 +557,7 @@ describe('relations - integration', function() { '/patients/' + root.patient.id; self.patient = root.patient; self.physician = root.physician; - done(); + done(err); }); }); @@ -574,7 +578,7 @@ describe('relations - integration', function() { '/patients/' + root.patient.id; self.patient = root.patient; self.physician = root.physician; - done(); + done(err); }); }); @@ -690,6 +694,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body).to.have.property('products'); expect(res.body.products).to.eql([ { @@ -709,6 +714,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body).to.have.property('products'); expect(res.body.products).to.eql([ { @@ -770,6 +776,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body.name).to.be.equal('Group 1'); expect(res.body.poster).to.be.eql( { url: 'http://image.url/' } @@ -783,6 +790,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body).to.be.eql( { url: 'http://image.url/' } ); @@ -806,6 +814,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body).to.be.eql( { url: 'http://changed.url/' } ); @@ -863,6 +872,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body.name).to.be.equal('List A'); expect(res.body.todoItems).to.be.eql([ { content: 'Todo 1', id: 1 }, @@ -877,6 +887,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body).to.be.eql([ { content: 'Todo 1', id: 1 }, { content: 'Todo 2', id: 2 } @@ -891,6 +902,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body).to.be.eql([ { content: 'Todo 2', id: 2 } ]); @@ -916,6 +928,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body).to.be.eql([ { content: 'Todo 1', id: 1 }, { content: 'Todo 2', id: 2 }, @@ -930,6 +943,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body).to.be.eql( { content: 'Todo 3', id: 3 } ); @@ -952,6 +966,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body).to.be.eql([ { content: 'Todo 1', id: 1 }, { content: 'Todo 3', id: 3 } @@ -963,6 +978,7 @@ describe('relations - integration', function() { it('returns a 404 response when embedded model is not found', function(done) { var url = '/api/todo-lists/' + this.todoList.id + '/items/2'; this.get(url).expect(404, function(err, res) { + if (err) return done(err); expect(res.body.error.status).to.be.equal(404); expect(res.body.error.message).to.be.equal('Unknown "todoItem" id "2".'); expect(res.body.error.code).to.be.equal('MODEL_NOT_FOUND'); @@ -1050,6 +1066,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body.ingredientIds).to.eql([test.ingredient1]); expect(res.body).to.not.have.property('ingredients'); done(); @@ -1075,6 +1092,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body).to.be.eql([ { name: 'Chocolate', id: test.ingredient1 }, { name: 'Sugar', id: test.ingredient2 }, @@ -1090,6 +1108,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body).to.be.eql([ { name: 'Chocolate', id: test.ingredient1 }, { name: 'Butter', id: test.ingredient3 } @@ -1105,6 +1124,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body).to.be.eql([ { name: 'Butter', id: test.ingredient3 } ]); @@ -1119,6 +1139,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body.ingredientIds).to.eql([ test.ingredient1, test.ingredient3 ]); @@ -1137,6 +1158,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body).to.be.eql( { name: 'Butter', id: test.ingredient3 } ); @@ -1152,6 +1174,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body.ingredientIds).to.eql(expected); expect(res.body).to.not.have.property('ingredients'); done(); @@ -1175,6 +1198,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body).to.be.eql([ { name: 'Chocolate', id: test.ingredient1 }, { name: 'Sugar', id: test.ingredient2 } @@ -1189,6 +1213,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body).to.be.eql([ { name: 'Chocolate', id: test.ingredient1 } ]); @@ -1216,6 +1241,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body).to.be.eql([ { name: 'Chocolate', id: test.ingredient1 }, { name: 'Sugar', id: test.ingredient2 } @@ -1241,6 +1267,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body).to.be.eql([ { name: 'Sugar', id: test.ingredient2 } ]); @@ -1254,6 +1281,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(res.body).to.be.eql([ { name: 'Chocolate', id: test.ingredient1 }, { name: 'Sugar', id: test.ingredient2 } @@ -1267,6 +1295,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { + if (err) return done(err); expect(err).to.not.exist; expect(res.body.name).to.equal('Photo 1'); done(); @@ -1401,6 +1430,7 @@ describe('relations - integration', function() { var test = this; this.get('/api/books/' + test.book.id + '/pages') .expect(200, function(err, res) { + if (err) return done(err); expect(res.body).to.be.an.array; expect(res.body).to.have.length(1); expect(res.body[0].name).to.equal('Page 1'); @@ -1412,6 +1442,7 @@ describe('relations - integration', function() { var test = this; this.get('/api/pages/' + test.page.id + '/notes/' + test.note.id) .expect(200, function(err, res) { + if (err) return done(err); expect(res.headers['x-before']).to.equal('before'); expect(res.headers['x-after']).to.equal('after'); expect(res.body).to.be.an.object; @@ -1424,6 +1455,7 @@ describe('relations - integration', function() { var test = this; this.get('/api/books/unknown/pages/' + test.page.id + '/notes') .expect(404, function(err, res) { + if (err) return done(err); expect(res.body.error).to.be.an.object; var expected = 'could not find a model with id unknown'; expect(res.body.error.message).to.equal(expected); @@ -1436,6 +1468,7 @@ describe('relations - integration', function() { var test = this; this.get('/api/images/' + test.image.id + '/book/pages') .end(function(err, res) { + if (err) return done(err); expect(res.body).to.be.an.array; expect(res.body).to.have.length(1); expect(res.body[0].name).to.equal('Page 1'); @@ -1447,6 +1480,7 @@ describe('relations - integration', function() { var test = this; this.get('/api/images/' + test.image.id + '/book/pages/' + test.page.id) .end(function(err, res) { + if (err) return done(err); expect(res.body).to.be.an.object; expect(res.body.name).to.equal('Page 1'); done(); @@ -1457,6 +1491,7 @@ describe('relations - integration', function() { var test = this; this.get('/api/books/' + test.book.id + '/pages/' + test.page.id + '/notes') .expect(200, function(err, res) { + if (err) return done(err); expect(res.body).to.be.an.array; expect(res.body).to.have.length(1); expect(res.body[0].text).to.equal('Page Note 1'); @@ -1468,6 +1503,7 @@ describe('relations - integration', function() { var test = this; this.get('/api/books/' + test.book.id + '/pages/' + test.page.id + '/notes/' + test.note.id) .expect(200, function(err, res) { + if (err) return done(err); expect(res.headers['x-before']).to.equal('before'); expect(res.headers['x-after']).to.equal('after'); expect(res.body).to.be.an.object; @@ -1480,6 +1516,7 @@ describe('relations - integration', function() { var test = this; this.get('/api/books/' + test.book.id + '/chapters/' + test.chapter.id + '/notes/' + test.cnote.id) .expect(200, function(err, res) { + if (err) return done(err); expect(res.headers['x-before']).to.empty; expect(res.headers['x-after']).to.empty; done(); @@ -1503,6 +1540,7 @@ describe('relations - integration', function() { var test = this; this.get('/api/books/' + test.book.id + '/pages/' + this.page.id + '/throws') .end(function(err, res) { + if (err) return done(err); expect(res.body).to.be.an('object'); expect(res.body.error).to.be.an('object'); expect(res.body.error.name).to.equal('Error'); From db0678baa68bf8ae9168e65fb3ab958d9f156bf1 Mon Sep 17 00:00:00 2001 From: Ryan Graham Date: Thu, 7 Jan 2016 13:29:27 -0800 Subject: [PATCH 005/187] test: use ephemeral port for e2e server --- Gruntfile.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index a7a755c81..3df919962 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -217,7 +217,10 @@ module.exports = function(grunt) { grunt.registerTask('e2e-server', function() { var done = this.async(); var app = require('./test/fixtures/e2e/app'); - app.listen(3000, done); + app.listen(0, function() { + process.env.PORT = this.address().port; + done(); + }); }); grunt.registerTask('e2e', ['e2e-server', 'karma:e2e']); From a0a1083564c84aaeb6ce7aa56808e6378e418e5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 26 Jan 2016 13:51:37 +0100 Subject: [PATCH 006/187] Hide verificationToken We should never be showing this publically. Adds unit test for hiding verification token. This is a back-port of pull request #1851 from gausie/patch-4 --- common/models/user.json | 2 +- test/user.test.js | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/common/models/user.json b/common/models/user.json index d70a89d3f..16545ab4b 100644 --- a/common/models/user.json +++ b/common/models/user.json @@ -32,7 +32,7 @@ "options": { "caseSensitiveEmail": true }, - "hidden": ["password"], + "hidden": ["password", "verificationToken"], "acls": [ { "principalType": "ROLE", diff --git a/test/user.test.js b/test/user.test.js index 8e2b14948..79f6547ee 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -1320,6 +1320,12 @@ describe('User', function() { }); }); + it('should hide verification tokens from user JSON', function(done) { + var user = new User({email: 'bar@bat.com', password: 'bar', verificationToken: 'a-token' }); + var data = user.toJSON(); + assert(!('verificationToken' in data)); + done(); + }); }); describe('User.confirm(options, fn)', function() { From 015e9cb80e9e7a537891219d8b50f9b94f9e0112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 26 Jan 2016 13:52:57 +0100 Subject: [PATCH 007/187] Correct JSDoc findOrCreate() callback in PersistedModel Update PersistedModel.findOrCreate() JSDoc to reflect the callback accepts an additional created boolean parameter. This is a back-port of pull request #1983 from noderat/patch-1 --- lib/persisted-model.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/persisted-model.js b/lib/persisted-model.js index bcac8e18d..5dceeec04 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -135,9 +135,10 @@ module.exports = function(registry) { *
See * [Where filter](https://docs.strongloop.com/display/LB/Where+filter#Wherefilter-Whereclauseforqueries). * @param {Object} data Data to insert if object matching the `where` filter is not found. - * @callback {Function} callback Callback function called with `cb(err, instance)` arguments. Required. + * @callback {Function} callback Callback function called with `cb(err, instance, created)` arguments. Required. * @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object). * @param {Object} instance Model instance matching the `where` filter, if found. + * @param {Boolean} created True if the instance matching the `where` filter was created. */ PersistedModel.findOrCreate = function findOrCreate(query, data, callback) { From 4753373f4fb03f820c0a66f96258fcfadb900cc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 4 Feb 2016 16:28:01 +0100 Subject: [PATCH 008/187] Travis: drop iojs, add v4.x and v5.x --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 168369451..e918e73fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,5 +3,6 @@ language: node_js node_js: - "0.10" - "0.12" - - "iojs" + - "4" + - "5" From 76ec49c96bd529715683ec2b6992f8a8f0429607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 4 Feb 2016 16:52:50 +0100 Subject: [PATCH 009/187] Fix race condition in error handler test --- test/error-handler.test.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/test/error-handler.test.js b/test/error-handler.test.js index d19abf47e..0494f2ef3 100644 --- a/test/error-handler.test.js +++ b/test/error-handler.test.js @@ -41,15 +41,23 @@ describe('loopback.errorHandler(options)', function() { //arrange var app = loopback(); app.use(loopback.urlNotFound()); - app.use(loopback.errorHandler({ includeStack: false, log: customLogger })); - //act - request(app).get('/url-does-not-exist').end(); + var errorLogged; + app.use(loopback.errorHandler({ + includeStack: false, + log: function customLogger(err, str, req) { + errorLogged = err; + } + })); - //assert - function customLogger(err, str, req) { - assert.ok(err.message === 'Cannot GET /url-does-not-exist'); + //act + request(app).get('/url-does-not-exist').end(function(err) { + if (err) return done(err); + //assert + expect(errorLogged) + .to.have.property('message', 'Cannot GET /url-does-not-exist'); done(); - } + }); + }); }); From 7a54da58708791a1bf92630a4b21ce7a270283cf Mon Sep 17 00:00:00 2001 From: Jue Hou Date: Wed, 27 Jan 2016 14:42:47 -0500 Subject: [PATCH 010/187] Promisify Model Change * Change.diff * Change.findOrCreateChange * Change.rectifyModelChanges * Change.prototype.currentRevision * Change.prototype.rectify --- common/models/change.js | 19 +++++-- test/change.test.js | 111 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 4 deletions(-) diff --git a/common/models/change.js b/common/models/change.js index be353a039..3b347ec5c 100644 --- a/common/models/change.js +++ b/common/models/change.js @@ -4,6 +4,7 @@ var PersistedModel = require('../../lib/loopback').PersistedModel; var loopback = require('../../lib/loopback'); +var utils = require('../../lib/utils'); var crypto = require('crypto'); var CJSON = {stringify: require('canonical-json')}; var async = require('async'); @@ -77,6 +78,8 @@ module.exports = function(Change) { var Change = this; var errors = []; + callback = callback || utils.createPromiseCallback(); + var tasks = modelIds.map(function(id) { return function(cb) { Change.findOrCreateChange(modelName, id, function(err, change) { @@ -111,6 +114,7 @@ module.exports = function(Change) { } callback(); }); + return callback.promise; }; /** @@ -138,6 +142,7 @@ module.exports = function(Change) { Change.findOrCreateChange = function(modelName, modelId, callback) { assert(loopback.findModel(modelName), modelName + ' does not exist'); + callback = callback || utils.createPromiseCallback(); var id = this.idForModel(modelName, modelId); var Change = this; @@ -155,6 +160,7 @@ module.exports = function(Change) { Change.updateOrCreate(ch, callback); } }); + return callback.promise; }; /** @@ -171,9 +177,7 @@ module.exports = function(Change) { change.debug('rectify change'); - cb = cb || function(err) { - if (err) throw new Error(err); - }; + cb = cb || utils.createPromiseCallback(); change.currentRevision(function(err, rev) { if (err) return cb(err); @@ -194,6 +198,7 @@ module.exports = function(Change) { } ); }); + return cb.promise; function doRectify(checkpoint, rev) { if (rev) { @@ -248,6 +253,7 @@ module.exports = function(Change) { */ Change.prototype.currentRevision = function(cb) { + cb = cb || utils.createPromiseCallback(); var model = this.getModelCtor(); var id = this.getModelId(); model.findById(id, function(err, inst) { @@ -258,6 +264,7 @@ module.exports = function(Change) { cb(null, null); } }); + return cb.promise; }; /** @@ -390,8 +397,11 @@ module.exports = function(Change) { */ Change.diff = function(modelName, since, remoteChanges, callback) { + callback = callback || utils.createPromiseCallback(); + if (!Array.isArray(remoteChanges) || remoteChanges.length === 0) { - return callback(null, {deltas: [], conflicts: []}); + callback(null, {deltas: [], conflicts: []}); + return callback.promise; } var remoteChangeIndex = {}; var modelIds = []; @@ -455,6 +465,7 @@ module.exports = function(Change) { conflicts: conflicts }); }); + return callback.promise; }; /** diff --git a/test/change.test.js b/test/change.test.js index 55d551c72..2f43c037f 100644 --- a/test/change.test.js +++ b/test/change.test.js @@ -82,6 +82,38 @@ describe('Change', function() { }); }); + describe('Change.rectifyModelChanges - promise variant', function() { + describe('using an existing untracked model', function() { + beforeEach(function(done) { + var test = this; + Change.rectifyModelChanges(this.modelName, [this.modelId]) + .then(function(trackedChanges) { + done(); + }) + .catch(done); + }); + + it('should create an entry', function(done) { + var test = this; + Change.find() + .then(function(trackedChanges) { + assert.equal(trackedChanges[0].modelId, test.modelId.toString()); + done(); + }) + .catch(done); + }); + + it('should only create one change', function(done) { + Change.count() + .then(function(count) { + assert.equal(count, 1); + done(); + }) + .catch(done); + }); + }); + }); + describe('Change.findOrCreateChange(modelName, modelId, callback)', function() { describe('when a change doesnt exist', function() { @@ -104,6 +136,27 @@ describe('Change', function() { }); }); + describe('when a change doesnt exist - promise variant', function() { + beforeEach(function(done) { + var test = this; + Change.findOrCreateChange(this.modelName, this.modelId) + .then(function(result) { + test.result = result; + done(); + }) + .catch(done); + }); + + it('should create an entry', function(done) { + var test = this; + Change.findById(this.result.id, function(err, change) { + if (err) return done(err); + assert.equal(change.id, test.result.id); + done(); + }); + }); + }); + describe('when a change does exist', function() { beforeEach(function(done) { var test = this; @@ -219,6 +272,28 @@ describe('Change', function() { }); }); + describe('change.rectify - promise variant', function() { + var change; + beforeEach(function(done) { + Change.findOrCreateChange(this.modelName, this.modelId) + .then(function(ch) { + change = ch; + done(); + }) + .catch(done); + }); + + it('should create a new change with the correct revision', function(done) { + var test = this; + change.rectify() + .then(function(ch) { + assert.equal(ch.rev, test.revisionForModel); + done(); + }) + .catch(done); + }); + }); + describe('change.currentRevision(callback)', function() { it('should get the correct revision', function(done) { var test = this; @@ -234,6 +309,23 @@ describe('Change', function() { }); }); + describe('change.currentRevision - promise variant', function() { + it('should get the correct revision', function(done) { + var test = this; + var change = new Change({ + modelName: this.modelName, + modelId: this.modelId + }); + + change.currentRevision() + .then(function(rev) { + assert.equal(rev, test.revisionForModel); + done(); + }) + .catch(done); + }); + }); + describe('Change.hash(str)', function() { // todo(ritch) test other hashing algorithms it('should hash the given string', function() { @@ -374,6 +466,25 @@ describe('Change', function() { }); }); + it('should return delta and conflict lists - promise variant', function(done) { + var remoteChanges = [ + // an update => should result in a delta + {rev: 'foo2', prev: 'foo', modelName: this.modelName, modelId: 9, checkpoint: 1}, + // no change => should not result in a delta / conflict + {rev: 'bar', prev: 'bar', modelName: this.modelName, modelId: 10, checkpoint: 1}, + // a conflict => should result in a conflict + {rev: 'bat2', prev: 'bat0', modelName: this.modelName, modelId: 11, checkpoint: 1}, + ]; + + Change.diff(this.modelName, 0, remoteChanges) + .then(function(diff) { + assert.equal(diff.deltas.length, 1); + assert.equal(diff.conflicts.length, 1); + done(); + }) + .catch(done); + }); + it('should set "prev" to local revision in non-conflicting delta', function(done) { var updateRecord = { rev: 'foo-new', From a0806eab89c2f62cba93e6579cd9b9c9a78ff4b2 Mon Sep 17 00:00:00 2001 From: Ryan Graham Date: Thu, 4 Feb 2016 08:21:15 -0800 Subject: [PATCH 011/187] test: remove errant console.log from test Using console.log like this can result in invalid xml when the xunit reporter is used. [Backport of pull request #2035] --- test/user.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/user.test.js b/test/user.test.js index 79f6547ee..f306332cb 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -1042,7 +1042,6 @@ describe('User', function() { user.verify(options) .then(function(result) { - console.log('here in then function'); assert(result.email); assert(result.email.response); assert(result.token); From e98ed99fe76621c82b5b4b3d690c51c726554caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 5 Feb 2016 09:21:25 +0100 Subject: [PATCH 012/187] Fix race condition in replication tests --- test/replication.test.js | 98 ++++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 44 deletions(-) diff --git a/test/replication.test.js b/test/replication.test.js index 2606aaf53..ecdffbf8d 100644 --- a/test/replication.test.js +++ b/test/replication.test.js @@ -67,40 +67,26 @@ describe('Replication / Change APIs', function() { }); it('should call rectifyAllChanges if no id is passed for rectifyOnDelete', function(done) { - SourceModel.rectifyChange = function() { - return done(new Error('Should not call rectifyChange')); - }; - SourceModel.rectifyAllChanges = function() { - return done(); - }; + var calls = mockSourceModelRectify(); SourceModel.destroyAll({name: 'John'}, function(err, data) { - if (err) - return done(err); + if (err) return done(err); + expect(calls).to.eql(['rectifyAllChanges']); + done(); }); }); it('should call rectifyAllChanges if no id is passed for rectifyOnSave', function(done) { - SourceModel.rectifyChange = function() { - return done(new Error('Should not call rectifyChange')); - }; - SourceModel.rectifyAllChanges = function() { - return done(); - }; + var calls = mockSourceModelRectify(); var newData = {'name': 'Janie'}; SourceModel.update({name: 'Jane'}, newData, function(err, data) { - if (err) - return done(err); + if (err) return done(err); + expect(calls).to.eql(['rectifyAllChanges']); + done(); }); }); it('rectifyOnDelete for Delete should call rectifyChange instead of rectifyAllChanges', function(done) { - TargetModel.rectifyChange = function() { - return done(); - }; - TargetModel.rectifyAllChanges = function() { - return done(new Error('Should not call rectifyAllChanges')); - }; - + var calls = mockTargetModelRectify(); async.waterfall([ function(callback) { SourceModel.destroyAll({name: 'John'}, callback); @@ -110,19 +96,14 @@ describe('Replication / Change APIs', function() { // replicate should call `rectifyOnSave` and then `rectifyChange` not `rectifyAllChanges` through `after save` operation } ], function(err, results) { - if (err) - return done(err); + if (err) return done(err); + expect(calls).to.eql(['rectifyChange']); + done(); }); }); it('rectifyOnSave for Update should call rectifyChange instead of rectifyAllChanges', function(done) { - TargetModel.rectifyChange = function() { - return done(); - }; - TargetModel.rectifyAllChanges = function() { - return done(new Error('Should not call rectifyAllChanges')); - }; - + var calls = mockTargetModelRectify(); var newData = {'name': 'Janie'}; async.waterfall([ function(callback) { @@ -131,20 +112,16 @@ describe('Replication / Change APIs', function() { function(data, callback) { SourceModel.replicate(TargetModel, callback); // replicate should call `rectifyOnSave` and then `rectifyChange` not `rectifyAllChanges` through `after save` operation - }], function(err, result) { - if (err) - return done(err); + } + ], function(err, result) { + if (err) return done(err); + expect(calls).to.eql(['rectifyChange']); + done(); }); }); it('rectifyOnSave for Create should call rectifyChange instead of rectifyAllChanges', function(done) { - TargetModel.rectifyChange = function() { - return done(); - }; - TargetModel.rectifyAllChanges = function() { - return done(new Error('Should not call rectifyAllChanges')); - }; - + var calls = mockTargetModelRectify(); var newData = [{name: 'Janie', surname: 'Doe'}]; async.waterfall([ function(callback) { @@ -155,10 +132,43 @@ describe('Replication / Change APIs', function() { // replicate should call `rectifyOnSave` and then `rectifyChange` not `rectifyAllChanges` through `after save` operation } ], function(err, result) { - if (err) - return done(err); + if (err) return done(err); + expect(calls).to.eql(['rectifyChange']); + done(); }); }); + + function mockSourceModelRectify() { + var calls = []; + + SourceModel.rectifyChange = function(id, cb) { + calls.push('rectifyChange'); + process.nextTick(cb); + }; + + SourceModel.rectifyAllChanges = function(cb) { + calls.push('rectifyAllChanges'); + process.nextTick(cb); + }; + + return calls; + } + + function mockTargetModelRectify() { + var calls = []; + + TargetModel.rectifyChange = function(id, cb) { + calls.push('rectifyChange'); + process.nextTick(cb); + }; + + TargetModel.rectifyAllChanges = function(cb) { + calls.push('rectifyAllChanges'); + process.nextTick(cb); + }; + + return calls; + } }); describe('Model.changes(since, filter, callback)', function() { From 70aec01e844f610aca6b029d62a1d2c3684ce111 Mon Sep 17 00:00:00 2001 From: Candy Date: Thu, 18 Feb 2016 10:38:54 -0500 Subject: [PATCH 013/187] Remove sl-blip from dependency --- package.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/package.json b/package.json index bccf39157..e5a08074d 100644 --- a/package.json +++ b/package.json @@ -102,8 +102,5 @@ "depd": "loopback-datasource-juggler/lib/browser.depd.js", "bcrypt": false }, - "license": "MIT", - "optionalDependencies": { - "sl-blip": "http://blip.strongloop.com/loopback@2.26.2" - } + "license": "MIT" } From 97f376c23934930a421ff8c81e620aa04eb7aeb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 19 Feb 2016 10:29:24 +0100 Subject: [PATCH 014/187] 2.27.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove sl-blip from dependency (Candy) * Fix race condition in replication tests (Miroslav Bajtoš) * test: remove errant console.log from test (Ryan Graham) * Promisify Model Change (Jue Hou) * Fix race condition in error handler test (Miroslav Bajtoš) * Travis: drop iojs, add v4.x and v5.x (Miroslav Bajtoš) * Correct JSDoc findOrCreate() callback in PersistedModel (Miroslav Bajtoš) * Hide verificationToken (Miroslav Bajtoš) * test: use ephemeral port for e2e server (Ryan Graham) * test: fail on error instead of crash (Ryan Graham) * ensure app is booted before integration tests (Ryan Graham) * Checkpoint speedup (Amir Jafarian) * Pull in API doc fix from PR into master #1910 (crandmck) --- CHANGES.md | 38 ++++++++++++++++++++++++++++++-------- package.json | 2 +- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ea87f5de4..914da0ae3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,33 @@ +2016-02-19, Version 2.27.0 +========================== + + * Remove sl-blip from dependency (Candy) + + * Fix race condition in replication tests (Miroslav Bajtoš) + + * test: remove errant console.log from test (Ryan Graham) + + * Promisify Model Change (Jue Hou) + + * Fix race condition in error handler test (Miroslav Bajtoš) + + * Travis: drop iojs, add v4.x and v5.x (Miroslav Bajtoš) + + * Correct JSDoc findOrCreate() callback in PersistedModel (Miroslav Bajtoš) + + * Hide verificationToken (Miroslav Bajtoš) + + * test: use ephemeral port for e2e server (Ryan Graham) + + * test: fail on error instead of crash (Ryan Graham) + + * ensure app is booted before integration tests (Ryan Graham) + + * Checkpoint speedup (Amir Jafarian) + + * Pull in API doc fix from PR into master #1910 (crandmck) + + 2015-12-22, Version 2.26.2 ========================== @@ -989,8 +1019,6 @@ 2014-07-15, Version 2.0.0-beta6 =============================== - * 2.0.0-beta6 (Miroslav Bajtoš) - * lib/application: publish Change models to REST API (Miroslav Bajtoš) * models/change: fix typo (Miroslav Bajtoš) @@ -1001,8 +1029,6 @@ 2014-07-03, Version 2.0.0-beta5 =============================== - * 2.0.0-beta5 (Miroslav Bajtoš) - * app: update `url` on `listening` event (Miroslav Bajtoš) * Fix "ReferenceError: loopback is not defined" in registry.memory(). (Guilherme Cirne) @@ -1021,8 +1047,6 @@ 2014-06-26, Version 2.0.0-beta4 =============================== - * 2.0.0-beta4 (Miroslav Bajtoš) - * package: upgrade juggler to 2.0.0-beta2 (Miroslav Bajtoš) * Fix loopback in PhantomJS, fix karma tests (Miroslav Bajtoš) @@ -1149,8 +1173,6 @@ 2014-05-28, Version 2.0.0-beta3 =============================== - * 2.0.0-beta3 (Miroslav Bajtoš) - * package.json: fix malformed json (Miroslav Bajtoš) * 2.0.0-beta2 (Ritchie Martori) diff --git a/package.json b/package.json index e5a08074d..b8f87ee89 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.26.2", + "version": "2.27.0", "description": "LoopBack: Open Source Framework for Node.js", "homepage": "http://loopback.io/", "keywords": [ From a4c643e8904afaf2b08fd8d2f9acd50efae2e93d Mon Sep 17 00:00:00 2001 From: Sam Roberts Date: Thu, 18 Feb 2016 20:36:07 -0800 Subject: [PATCH 015/187] application: correct spelling of "cannont" [back-port of pull request #2088] --- lib/application.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/application.js b/lib/application.js index bd3375539..c1932c20f 100644 --- a/lib/application.js +++ b/lib/application.js @@ -397,7 +397,7 @@ function dataSourcesFromConfig(name, config, connectorRegistry, registry) { var connectorPath; assert(typeof config === 'object', - 'cannont create data source without config object'); + 'can not create data source without config object'); if (typeof config.connector === 'string') { name = config.connector; From 50e3578992a81c939a974e313165d78718560593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 26 Feb 2016 14:20:07 +0100 Subject: [PATCH 016/187] Improve error message on connector init error [back-port of pull request #2105] --- lib/application.js | 20 +++++++++++++++----- test/app.test.js | 10 ++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/application.js b/lib/application.js index c1932c20f..dd5fdb730 100644 --- a/lib/application.js +++ b/lib/application.js @@ -219,11 +219,19 @@ app.models = function() { * @param {Object} config The data source config */ app.dataSource = function(name, config) { - var ds = dataSourcesFromConfig(name, config, this.connectors, this.registry); - this.dataSources[name] = - this.dataSources[classify(name)] = - this.dataSources[camelize(name)] = ds; - return ds; + try { + var ds = dataSourcesFromConfig(name, config, this.connectors, this.registry); + this.dataSources[name] = + this.dataSources[classify(name)] = + this.dataSources[camelize(name)] = ds; + return ds; + } catch (err) { + if (err.message) { + err.message = 'Cannot create data source ' + JSON.stringify(name) + + ': ' + err.message; + } + throw err; + } }; /** @@ -410,6 +418,8 @@ function dataSourcesFromConfig(name, config, connectorRegistry, registry) { config.connector = require(connectorPath); } } + if (!config.connector.name) + config.connector.name = name; } return registry.createDataSource(config); diff --git a/test/app.test.js b/test/app.test.js index 2701ef9d2..1144db2eb 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -732,6 +732,16 @@ describe('app', function() { app.dataSource('custom', { connector: 'custom' }); expect(app.dataSources.custom.name).to.equal(loopback.Memory.name); }); + + it('adds data source name to error messages', function() { + app.connector('throwing', { + initialize: function() { throw new Error('expected test error'); }, + }); + + expect(function() { + app.dataSource('bad-ds', { connector: 'throwing' }); + }).to.throw(/bad-ds.*throwing/); + }); }); describe.onServer('listen()', function() { From e4b275243fe11b2d4d756e9f8995fefa66fa8873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Kr=C3=B6ger?= Date: Mon, 29 Feb 2016 16:49:41 +0100 Subject: [PATCH 017/187] Allow built-in token middleware to run repeatedly Add two new options: - When `enableDoublecheck` is true, the middleware will run even if a previous middleware has already set `req.accessToken` (possibly to `null` for anonymous requests) - When `overwriteExistingToken` is true (and `enableDoublecheck` too), the middleware will overwrite `req.accessToken` set by a previous middleware instances. --- server/middleware/token.js | 20 ++++++++- test/access-token.test.js | 86 +++++++++++++++++++++++++++++++++++++- 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/server/middleware/token.js b/server/middleware/token.js index e80eb560b..146c75d17 100644 --- a/server/middleware/token.js +++ b/server/middleware/token.js @@ -62,6 +62,8 @@ function escapeRegExp(str) { * @property {Array} [headers] Array of header names. * @property {Array} [params] Array of param names. * @property {Boolean} [searchDefaultTokenKeys] Use the default search locations for Token in request + * @property {Boolean} [enableDoublecheck] Execute middleware although an instance mounted earlier in the chain didn't find a token + * @property {Boolean} [overwriteExistingToken] only has effect in combination with `enableDoublecheck`. If truthy, will allow to overwrite an existing accessToken. * @property {Function|String} [model] AccessToken model name or class to use. * @property {String} [currentUserLiteral] String literal for the current user. * @header loopback.token([options]) @@ -80,6 +82,9 @@ function token(options) { currentUserLiteral = escapeRegExp(currentUserLiteral); } + var enableDoublecheck = !!options.enableDoublecheck; + var overwriteExistingToken = !!options.overwriteExistingToken; + return function(req, res, next) { var app = req.app; var registry = app.registry; @@ -97,8 +102,19 @@ function token(options) { 'loopback.token() middleware requires a AccessToken model'); if (req.accessToken !== undefined) { - rewriteUserLiteral(req, currentUserLiteral); - return next(); + if (!enableDoublecheck) { + // req.accessToken is defined already (might also be "null" or "false") and enableDoublecheck + // has not been set --> skip searching for credentials + rewriteUserLiteral(req, currentUserLiteral); + return next(); + } + if (req.accessToken && req.accessToken.id && !overwriteExistingToken) { + // req.accessToken.id is defined, which means that some other middleware has identified a valid user. + // when overwriteExistingToken is not set to a truthy value, skip searching for credentials. + rewriteUserLiteral(req, currentUserLiteral); + return next(); + } + // continue normal operation (as if req.accessToken was undefined) } TokenModel.findForRequest(req, options, function(err, token) { req.accessToken = token || null; diff --git a/test/access-token.test.js b/test/access-token.test.js index 9e57cba2a..a2ddbdb09 100644 --- a/test/access-token.test.js +++ b/test/access-token.test.js @@ -65,7 +65,7 @@ describe('loopback.token(options)', function() { .end(done); }); - describe('populating req.toen from HTTP Basic Auth formatted authorization header', function() { + describe('populating req.token from HTTP Basic Auth formatted authorization header', function() { it('parses "standalone-token"', function(done) { var token = this.token.id; token = 'Basic ' + new Buffer(token).toString('base64'); @@ -199,6 +199,90 @@ describe('loopback.token(options)', function() { done(); }); }); + + describe('loading multiple instances of token middleware', function() { + it('should skip when req.token is already present and no further options are set', + function(done) { + var tokenStub = { id: 'stub id' }; + app.use(function(req, res, next) { + req.accessToken = tokenStub; + next(); + }); + app.use(loopback.token({ model: Token })); + app.get('/', function(req, res, next) { + res.send(req.accessToken); + }); + + request(app).get('/') + .set('Authorization', this.token.id) + .expect(200) + .end(function(err, res) { + if (err) return done(err); + expect(res.body).to.eql(tokenStub); + done(); + }); + }); + + it('should not overwrite valid existing token (has "id" property) ' + + ' when overwriteExistingToken is falsy', + function(done) { + var tokenStub = { id: 'stub id' }; + app.use(function(req, res, next) { + req.accessToken = tokenStub; + next(); + }); + app.use(loopback.token({ + model: Token, + enableDoublecheck: true, + })); + app.get('/', function(req, res, next) { + res.send(req.accessToken); + }); + + request(app).get('/') + .set('Authorization', this.token.id) + .expect(200) + .end(function(err, res) { + if (err) return done(err); + expect(res.body).to.eql(tokenStub); + done(); + }); + }); + + it('should overwrite existing token when enableDoublecheck ' + + 'and overwriteExistingToken options are truthy', + function(done) { + var token = this.token; + var tokenStub = { id: 'stub id' }; + + app.use(function(req, res, next) { + req.accessToken = tokenStub; + next(); + }); + app.use(loopback.token({ + model: Token, + enableDoublecheck: true, + overwriteExistingToken: true, + })); + app.get('/', function(req, res, next) { + res.send(req.accessToken); + }); + + request(app).get('/') + .set('Authorization', token.id) + .expect(200) + .end(function(err, res) { + if (err) return done(err); + expect(res.body).to.eql({ + id: token.id, + ttl: token.ttl, + userId: token.userId, + created: token.created.toJSON(), + }); + done(); + }); + }); + }); }); describe('AccessToken', function() { From 1ea1cd612a3a51cfeb73336c7b80d15097a66bb8 Mon Sep 17 00:00:00 2001 From: Tim Needham Date: Sun, 17 Jan 2016 14:42:38 +0000 Subject: [PATCH 018/187] Fix typo in Model.nestRemoting Prevent apps from crashing when using `Model.nestRemoting` without `{ hooks: false }` option. Note that it is not possible to reproduce this bug using our current Mocha test suite, because other tests modify the global state in such way that the bug no longer occurs. [back-port of #2245] --- lib/model.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/model.js b/lib/model.js index f3ec47b25..d6564bf49 100644 --- a/lib/model.js +++ b/lib/model.js @@ -806,8 +806,8 @@ module.exports = function(registry) { listenerTree.before = listenerTree.before || {}; listenerTree.after = listenerTree.after || {}; - var beforeListeners = remotes.listenerTree.before[toModelName] || {}; - var afterListeners = remotes.listenerTree.after[toModelName] || {}; + var beforeListeners = listenerTree.before[toModelName] || {}; + var afterListeners = listenerTree.after[toModelName] || {}; sharedClass.methods().forEach(function(method) { var delegateTo = method.rest && method.rest.delegateTo; From 2498c02f31469eb80f7edce60347b35f0b7a92e0 Mon Sep 17 00:00:00 2001 From: Supasate Choochaisri Date: Wed, 27 Apr 2016 18:15:24 +0700 Subject: [PATCH 019/187] Add new feature to emit a `remoteMethodDisabled` event when disabling a remote method. Signed-off-by: Supasate Choochaisri --- lib/application.js | 5 +++++ lib/model.js | 1 + package.json | 1 + test/app.test.js | 16 ++++++++++++++++ test/model.test.js | 18 ++++++++++++++++++ 5 files changed, 41 insertions(+) diff --git a/lib/application.js b/lib/application.js index dd5fdb730..c5485fdb1 100644 --- a/lib/application.js +++ b/lib/application.js @@ -153,6 +153,11 @@ app.model = function(Model, config) { this.emit('modelRemoted', Model.sharedClass); } + var self = this; + Model.on('remoteMethodDisabled', function(model, methodName) { + self.emit('remoteMethodDisabled', model, methodName); + }); + Model.shared = isPublic; Model.app = this; Model.emit('attached', this); diff --git a/lib/model.js b/lib/model.js index d6564bf49..685e84045 100644 --- a/lib/model.js +++ b/lib/model.js @@ -431,6 +431,7 @@ module.exports = function(registry) { Model.disableRemoteMethod = function(name, isStatic) { this.sharedClass.disableMethod(name, isStatic || false); + this.emit('remoteMethodDisabled', this.sharedClass, name); }; Model.belongsToRemoting = function(relationName, relation, define) { diff --git a/package.json b/package.json index b8f87ee89..01f16ec48 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "loopback-testing": "~1.1.0", "mocha": "^2.1.0", "sinon": "^1.13.0", + "sinon-chai": "^2.8.0", "strong-task-emitter": "^0.0.6", "supertest": "^0.15.0" }, diff --git a/test/app.test.js b/test/app.test.js index 1144db2eb..347d738b7 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -606,6 +606,22 @@ describe('app', function() { expect(remotedClass).to.eql(Color.sharedClass); }); + it('emits a `remoteMethodDisabled` event', function() { + var Color = PersistedModel.extend('color', { name: String }); + Color.shared = true; + var remoteMethodDisabledClass, disabledRemoteMethod; + app.on('remoteMethodDisabled', function(sharedClass, methodName) { + remoteMethodDisabledClass = sharedClass; + disabledRemoteMethod = methodName; + }); + app.model(Color); + app.models.Color.disableRemoteMethod('findOne'); + expect(remoteMethodDisabledClass).to.exist; + expect(remoteMethodDisabledClass).to.eql(Color.sharedClass); + expect(disabledRemoteMethod).to.exist; + expect(disabledRemoteMethod).to.eql('findOne'); + }); + it.onServer('updates REST API when a new model is added', function(done) { app.use(loopback.rest()); request(app).get('/colors').expect(404, function(err, res) { diff --git a/test/model.test.js b/test/model.test.js index b9a16f36d..37147f40a 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -1,9 +1,13 @@ var async = require('async'); +var chai = require('chai'); +var expect = chai.expect; var loopback = require('../'); var ACL = loopback.ACL; var Change = loopback.Change; var defineModelTestsWithDataSource = require('./util/model-tests'); var PersistedModel = loopback.PersistedModel; +var sinonChai = require('sinon-chai'); +chai.use(sinonChai); var describe = require('./util/describe'); @@ -618,6 +622,20 @@ describe.onServer('Remote Methods', function() { 'createChangeStream' ]); }); + + it('emits a `remoteMethodDisabled` event', function() { + var app = loopback(); + var model = PersistedModel.extend('TestModelForDisablingRemoteMethod'); + app.dataSource('db', { connector: 'memory' }); + app.model(model, { dataSource: 'db' }); + + var callbackSpy = require('sinon').spy(); + var TestModel = app.models.TestModelForDisablingRemoteMethod; + TestModel.on('remoteMethodDisabled', callbackSpy); + TestModel.disableRemoteMethod('findOne'); + + expect(callbackSpy).to.have.been.calledWith(TestModel.sharedClass, 'findOne'); + }); }); describe('Model.getApp(cb)', function() { From 6d738690c82b59476d992d5696a093cca1894aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 2 May 2016 13:00:09 +0200 Subject: [PATCH 020/187] 2.28.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add new feature to emit a `remoteMethodDisabled` event when disabling a remote method. (Supasate Choochaisri) * Fix typo in Model.nestRemoting (Tim Needham) * Allow built-in token middleware to run repeatedly (Benjamin Kröger) * Improve error message on connector init error (Miroslav Bajtoš) * application: correct spelling of "cannont" (Sam Roberts) --- CHANGES.md | 14 ++++++++++++++ package.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 914da0ae3..aa42725cf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,17 @@ +2016-05-02, Version 2.28.0 +========================== + + * Add new feature to emit a `remoteMethodDisabled` event when disabling a remote method. (Supasate Choochaisri) + + * Fix typo in Model.nestRemoting (Tim Needham) + + * Allow built-in token middleware to run repeatedly (Benjamin Kröger) + + * Improve error message on connector init error (Miroslav Bajtoš) + + * application: correct spelling of "cannont" (Sam Roberts) + + 2016-02-19, Version 2.27.0 ========================== diff --git a/package.json b/package.json index 01f16ec48..0ac007a69 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.27.0", + "version": "2.28.0", "description": "LoopBack: Open Source Framework for Node.js", "homepage": "http://loopback.io/", "keywords": [ From 845c59ecedbf34e0375bd21a7854a916bc56c145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 2 May 2016 13:48:12 +0200 Subject: [PATCH 021/187] test/user: use local registry Rework User tests to not depend on `app.autoAttach()` and global shared registry of Models. Instead, each tests creates a fresh app instance with a new in-memory datasource and a new set of Models. --- test/user.test.js | 98 +++++++++++++++++++++++++---------------------- 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/test/user.test.js b/test/user.test.js index f306332cb..23fca2ad5 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -1,12 +1,6 @@ require('./support'); var loopback = require('../'); -var User; -var AccessToken; -var MailConnector = require('../lib/connectors/mail'); - -var userMemory = loopback.createDataSource({ - connector: 'memory' -}); +var User, AccessToken; describe('User', function() { var validCredentialsEmail = 'foo@bar.com'; @@ -19,45 +13,59 @@ describe('User', function() { var invalidCredentials = {email: 'foo1@bar.com', password: 'invalid'}; var incompleteCredentials = {password: 'bar1'}; - var defaultApp; + // Create a local app variable to prevent clashes with the global + // variable shared by all tests. While this should not be necessary if + // the tests were written correctly, it turns out that's not the case :( + var app; + + beforeEach(function setupAppAndModels(done) { + // override the global app object provided by test/support.js + // and create a local one that does not share state with other tests + app = loopback({ localRegistry: true, loadBuiltinModels: true }); + app.dataSource('db', { connector: 'memory' }); + + // setup Email model, it's needed by User tests + app.dataSource('email', { + connector: loopback.Mail, + transports: [{ type: 'STUB' }], + }); + var Email = app.registry.getModel('Email'); + app.model(Email, { dataSource: 'email' }); + + // attach User and related models + User = app.registry.createModel('TestUser', {}, { + base: 'User', + http: { path: 'test-users' }, + }); + app.model(User, { dataSource: 'db' }); - beforeEach(function() { - // FIXME: [rfeng] Remove loopback.User.app so that remote hooks don't go - // to the wrong app instance - defaultApp = loopback.User.app; - loopback.User.app = null; - User = loopback.User.extend('TestUser', {}, {http: {path: 'test-users'}}); - AccessToken = loopback.AccessToken.extend('TestAccessToken'); - User.email = loopback.Email.extend('email'); - loopback.autoAttach(); + AccessToken = app.registry.getModel('AccessToken'); + app.model(AccessToken, { dataSource: 'db' }); + + User.email = Email; // Update the AccessToken relation to use the subclass of User AccessToken.belongsTo(User, {as: 'user', foreignKey: 'userId'}); User.hasMany(AccessToken, {as: 'accessTokens', foreignKey: 'userId'}); + // Speed up the password hashing algorithm + // for tests using the built-in User model + User.settings.saltWorkFactor = 4; + // allow many User.afterRemote's to be called User.setMaxListeners(0); - }); - - beforeEach(function(done) { - app.enableAuth(); - app.use(loopback.token({model: AccessToken})); + app.enableAuth({ dataSource: 'db' }); + app.use(loopback.token({ model: AccessToken })); app.use(loopback.rest()); app.model(User); User.create(validCredentials, function(err, user) { + if (err) return done(err); User.create(validCredentialsEmailVerified, done); }); }); - afterEach(function(done) { - loopback.User.app = defaultApp; - User.destroyAll(function(err) { - User.accessToken.destroyAll(done); - }); - }); - describe('User.create', function() { it('Create a new user', function(done) { User.create({email: 'f@b.com', password: 'bar'}, function(err, user) { @@ -694,12 +702,19 @@ describe('User', function() { var User; var AccessToken; - before(function() { - User = loopback.User.extend('RealmUser', {}, - {realmRequired: true, realmDelimiter: ':'}); - AccessToken = loopback.AccessToken.extend('RealmAccessToken'); + beforeEach(function() { + User = app.registry.createModel('RealmUser', {}, { + base: 'TestUser', + realmRequired: true, + realmDelimiter: ':', + }); - loopback.autoAttach(); + AccessToken = app.registry.createModel('RealmAccessToken', {}, { + base: 'AccessToken', + }); + + app.model(AccessToken, { dataSource: 'db' }); + app.model(User, { dataSource: 'db' }); // Update the AccessToken relation to use the subclass of User AccessToken.belongsTo(User, {as: 'user', foreignKey: 'userId'}); @@ -770,15 +785,6 @@ describe('User', function() { }); }); - afterEach(function(done) { - User.deleteAll({realm: 'realm1'}, function(err) { - if (err) { - return done(err); - } - User.deleteAll({realm: 'realm2'}, done); - }); - }); - it('rejects a user by without realm', function(done) { User.login(credentialWithoutRealm, function(err, accessToken) { assert(err); @@ -828,11 +834,11 @@ describe('User', function() { }); describe('User.login with realmRequired but no realmDelimiter', function() { - before(function() { + beforeEach(function() { User.settings.realmDelimiter = undefined; }); - after(function() { + afterEach(function() { User.settings.realmDelimiter = ':'; }); @@ -1534,7 +1540,7 @@ describe('User', function() { describe('ctor', function() { it('exports default Email model', function() { expect(User.email, 'User.email').to.be.a('function'); - expect(User.email.modelName, 'modelName').to.eql('email'); + expect(User.email.modelName, 'modelName').to.eql('Email'); }); it('exports default AccessToken model', function() { From cae9786f0eb20bb4e2bb14efad961ad251542df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 2 May 2016 14:30:11 +0200 Subject: [PATCH 022/187] Fix role.isOwner to support app-local registry --- common/models/role.js | 10 ++++------ test/role.test.js | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/common/models/role.js b/common/models/role.js index a176fec17..523e68982 100644 --- a/common/models/role.js +++ b/common/models/role.js @@ -147,12 +147,10 @@ module.exports = function(Role) { }); function isUserClass(modelClass) { - if (modelClass) { - return modelClass === loopback.User || - modelClass.prototype instanceof loopback.User; - } else { - return false; - } + if (!modelClass) return false; + var User = modelClass.modelBuilder.models.User; + if (!User) return false; + return modelClass == User || modelClass.prototype instanceof User; } /*! diff --git a/test/role.test.js b/test/role.test.js index 3053a0aff..8e5d1a129 100644 --- a/test/role.test.js +++ b/test/role.test.js @@ -407,4 +407,27 @@ describe('role model', function() { }); }); + describe('isOwner', function() { + it('supports app-local model registry', function(done) { + var app = loopback({ localRegistry: true, loadBuiltinModels: true }); + app.dataSource('db', { connector: 'memory' }); + // attach all auth-related models to 'db' datasource + app.enableAuth({ dataSource: 'db' }); + + var Role = app.models.Role; + var User = app.models.User; + + var u = app.registry.findModel('User'); + var credentials = { email: 'test@example.com', password: 'pass' }; + User.create(credentials, function(err, user) { + if (err) return done(err); + + Role.isOwner(User, user.id, user.id, function(err, result) { + if (err) return done(err); + expect(result, 'isOwner result').to.equal(true); + done(); + }); + }); + }); + }); }); From 53cd449c9c1b86f1147765a854d28f2b39be609c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 2 May 2016 14:53:21 +0200 Subject: [PATCH 023/187] test/rest.middleware: use local registry Rework tests in `test/rest.middleware.test.js` to not depend on `app.autoAttach()` and global shared registry of Models. Instead, each tests creates a fresh app instance with a new in-memory datasource and a new set of Models. --- test/rest.middleware.test.js | 78 +++++++++++++++++------------------- 1 file changed, 37 insertions(+), 41 deletions(-) diff --git a/test/rest.middleware.test.js b/test/rest.middleware.test.js index 5757c44b6..61b935644 100644 --- a/test/rest.middleware.test.js +++ b/test/rest.middleware.test.js @@ -1,11 +1,15 @@ var path = require('path'); describe('loopback.rest', function() { - var MyModel; + var app, MyModel; + beforeEach(function() { - var ds = app.dataSource('db', { connector: loopback.Memory }); - MyModel = ds.createModel('MyModel', {name: String}); - loopback.autoAttach(); + // override the global app object provided by test/support.js + // and create a local one that does not share state with other tests + app = loopback({ localRegistry: true, loadBuiltinModels: true }); + var db = app.dataSource('db', { connector: 'memory' }); + MyModel = app.registry.createModel('MyModel'); + MyModel.attachTo(db); }); it('works out-of-the-box', function(done) { @@ -101,7 +105,7 @@ describe('loopback.rest', function() { }); it('should honour `remoting.rest.supportedTypes`', function(done) { - var app = loopback(); + var app = loopback({ localRegistry: true }); // NOTE it is crucial to set `remoting` before creating any models var supportedTypes = ['json', 'application/javascript', 'text/javascript']; @@ -117,26 +121,24 @@ describe('loopback.rest', function() { }); it('allows models to provide a custom HTTP path', function(done) { - var ds = app.dataSource('db', { connector: loopback.Memory }); - var CustomModel = ds.createModel('CustomModel', + var CustomModel = app.registry.createModel('CustomModel', { name: String }, { http: { 'path': 'domain1/CustomModelPath' } }); - app.model(CustomModel); + app.model(CustomModel, { dataSource: 'db' }); app.use(loopback.rest()); request(app).get('/domain1/CustomModelPath').expect(200).end(done); }); it('should report 200 for url-encoded HTTP path', function(done) { - var ds = app.dataSource('db', { connector: loopback.Memory }); - var CustomModel = ds.createModel('CustomModel', + var CustomModel = app.registry.createModel('CustomModel', { name: String }, { http: { path: 'domain%20one/CustomModelPath' } }); - app.model(CustomModel); + app.model(CustomModel, { dataSource: 'db' }); app.use(loopback.rest()); request(app).get('/domain%20one/CustomModelPath').expect(200).end(done); @@ -144,12 +146,12 @@ describe('loopback.rest', function() { it('includes loopback.token when necessary', function(done) { givenUserModelWithAuth(); - app.enableAuth(); + app.enableAuth({ dataSource: 'db' }); app.use(loopback.rest()); givenLoggedInUser(function(err, token) { if (err) return done(err); - expect(token).instanceOf(app.models.accessToken); + expect(token).instanceOf(app.models.AccessToken); request(app).get('/users/' + token.userId) .set('Authorization', token.id) .expect(200) @@ -268,25 +270,25 @@ describe('loopback.rest', function() { it('should enable context using loopback.context', function(done) { app.use(loopback.context({ enableHttpContext: true })); - app.enableAuth(); + app.enableAuth({ dataSource: 'db' }); app.use(loopback.rest()); invokeGetToken(done); }); it('should enable context with loopback.rest', function(done) { - app.enableAuth(); - app.set('remoting', { context: { enableHttpContext: true } }); + app.enableAuth({ dataSource: 'db' }); + app.set('remoting', { context: { enableHttpContext: true }}); app.use(loopback.rest()); invokeGetToken(done); }); it('should support explicit context', function(done) { - app.enableAuth(); + app.enableAuth({ dataSource: 'db' }); app.use(loopback.context()); app.use(loopback.token( - { model: loopback.getModelByType(loopback.AccessToken) })); + { model: app.registry.getModelByType('AccessToken') })); app.use(function(req, res, next) { loopback.getCurrentContext().set('accessToken', req.accessToken); next(); @@ -321,32 +323,26 @@ describe('loopback.rest', function() { }); function givenUserModelWithAuth() { - // NOTE(bajtos) It is important to create a custom AccessToken model here, - // in order to overwrite the entry created by previous tests in - // the global model registry - app.model('accessToken', { - options: { - base: 'AccessToken' - }, - dataSource: 'db' - }); - return app.model('user', { - options: { - base: 'User', - relations: { - accessTokens: { - model: 'accessToken', - type: 'hasMany', - foreignKey: 'userId' - } - } - }, - dataSource: 'db' - }); + var AccessToken = app.registry.getModel('AccessToken'); + app.model(AccessToken, { dataSource: 'db' }); + var User = app.registry.getModel('User'); + app.model(User, { dataSource: 'db' }); + + // NOTE(bajtos) This is puzzling to me. The built-in User & AccessToken + // models should come with both relations already set up, i.e. the + // following two lines should not be neccessary. + // And it does behave that way when only tests in this file are run. + // However, when I run the full test suite (all files), the relations + // get broken. + AccessToken.belongsTo(User, { as: 'user', foreignKey: 'userId' }); + User.hasMany(AccessToken, { as: 'accessTokens', foreignKey: 'userId' }); + + return User; } + function givenLoggedInUser(cb, done) { var credentials = { email: 'user@example.com', password: 'pwd' }; - var User = app.models.user; + var User = app.models.User; User.create(credentials, function(err, user) { if (err) return done(err); From 6c59390754e776c9f831d06065aa0e7adbaa94b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 3 May 2016 10:19:45 +0200 Subject: [PATCH 024/187] Disable DEBUG output for eslint on Jenkins CI --- package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/package.json b/package.json index 0ac007a69..148d8007a 100644 --- a/package.json +++ b/package.json @@ -103,5 +103,10 @@ "depd": "loopback-datasource-juggler/lib/browser.depd.js", "bcrypt": false }, + "config": { + "ci": { + "debug": "*,-mocha:*,-eslint:*" + } + }, "license": "MIT" } From bd7f2b6db1daf61b144a97e534bbdfcc5b0ee075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 3 May 2016 15:55:37 +0200 Subject: [PATCH 025/187] travis: drop node@5, add node@6 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e918e73fb..d431fa429 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,5 +4,5 @@ node_js: - "0.10" - "0.12" - "4" - - "5" + - "6" From da2fb0ae15c2b436a26467cde94173f2339eb439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 3 May 2016 16:03:48 +0200 Subject: [PATCH 026/187] app: send port:0 instead of port:undefined Node v6 no longer supports port:undefined, this commit is fixing app.listen() to correctly send port:0 when no port is specified. --- lib/application.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/application.js b/lib/application.js index c5485fdb1..dad8f24f6 100644 --- a/lib/application.js +++ b/lib/application.js @@ -566,7 +566,11 @@ app.listen = function(cb) { (arguments.length == 1 && typeof arguments[0] == 'function'); if (useAppConfig) { - server.listen(this.get('port'), this.get('host'), cb); + var port = this.get('port'); + // NOTE(bajtos) port:undefined no longer works on node@6, + // we must pass port:0 explicitly + if (port === undefined) port = 0; + server.listen(port, this.get('host'), cb); } else { server.listen.apply(server, arguments); } From e2b1f78f1e801ac8f5dc7ed0a8bb594f39979207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 3 May 2016 16:43:45 +0200 Subject: [PATCH 027/187] Upgrade phantomjs to 2.x --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 148d8007a..2eb05c8b3 100644 --- a/package.json +++ b/package.json @@ -78,12 +78,13 @@ "karma-html2js-preprocessor": "^0.1.0", "karma-junit-reporter": "^0.2.2", "karma-mocha": "^0.1.10", - "karma-phantomjs-launcher": "^0.1.4", + "karma-phantomjs-launcher": "^1.0.0", "karma-script-launcher": "^0.1.0", "loopback-boot": "^2.7.0", "loopback-datasource-juggler": "^2.19.1", "loopback-testing": "~1.1.0", "mocha": "^2.1.0", + "phantomjs-prebuilt": "^2.1.7", "sinon": "^1.13.0", "sinon-chai": "^2.8.0", "strong-task-emitter": "^0.0.6", From 553889b378fc9b5f270b61f9fc3711b2607cdc29 Mon Sep 17 00:00:00 2001 From: Ryan Graham Date: Tue, 3 May 2016 17:09:47 -0700 Subject: [PATCH 028/187] relicense as MIT only --- LICENSE | 25 +++++++++++++++++++++++++ LICENSE.md | 9 --------- 2 files changed, 25 insertions(+), 9 deletions(-) create mode 100644 LICENSE delete mode 100644 LICENSE.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..a95641b20 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2013,2016. All Rights Reserved. +Node module: loopback +This project is licensed under the MIT License, full text below. + +-------- + +MIT license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 29d781523..000000000 --- a/LICENSE.md +++ /dev/null @@ -1,9 +0,0 @@ -Copyright (c) 2013-2015 StrongLoop, Inc and other contributors. - -loopback uses a dual license model. - -You may use this library under the terms of the [MIT License][], -or under the terms of the [StrongLoop Subscription Agreement][]. - -[MIT License]: http://opensource.org/licenses/MIT -[StrongLoop Subscription Agreement]: http://strongloop.com/license From 4d6f2e7ab704baed7a5c37396289485976d93bbb Mon Sep 17 00:00:00 2001 From: Ryan Graham Date: Tue, 3 May 2016 17:10:46 -0700 Subject: [PATCH 029/187] update/insert copyright notices --- Gruntfile.js | 5 +++++ browser/current-context.js | 5 +++++ common/models/access-token.js | 5 +++++ common/models/acl.js | 5 +++++ common/models/application.js | 5 +++++ common/models/change.js | 5 +++++ common/models/checkpoint.js | 5 +++++ common/models/email.js | 5 +++++ common/models/role-mapping.js | 5 +++++ common/models/role.js | 5 +++++ common/models/scope.js | 5 +++++ common/models/user.js | 5 +++++ example/client-server/client.js | 5 +++++ example/client-server/models.js | 5 +++++ example/client-server/server.js | 5 +++++ example/colors/app.js | 5 +++++ example/context/app.js | 5 +++++ example/mobile-models/app.js | 5 +++++ example/replication/app.js | 5 +++++ example/simple-data-source/app.js | 5 +++++ index.js | 5 +++++ lib/access-context.js | 5 +++++ lib/application.js | 5 +++++ lib/browser-express.js | 5 +++++ lib/builtin-models.js | 5 +++++ lib/connectors/base-connector.js | 5 +++++ lib/connectors/mail.js | 5 +++++ lib/connectors/memory.js | 5 +++++ lib/express-middleware.js | 5 +++++ lib/loopback.js | 5 +++++ lib/model.js | 5 +++++ lib/persisted-model.js | 5 +++++ lib/registry.js | 5 +++++ lib/runtime.js | 5 +++++ lib/server-app.js | 5 +++++ lib/utils.js | 5 +++++ server/current-context.js | 5 +++++ server/middleware/context.js | 5 +++++ server/middleware/error-handler.js | 5 +++++ server/middleware/favicon.js | 5 +++++ server/middleware/rest.js | 5 +++++ server/middleware/static.js | 5 +++++ server/middleware/status.js | 5 +++++ server/middleware/token.js | 5 +++++ server/middleware/url-not-found.js | 5 +++++ test/access-control.integration.js | 5 +++++ test/access-token.test.js | 5 +++++ test/acl.test.js | 5 +++++ test/app.test.js | 5 +++++ test/change-stream.test.js | 5 +++++ test/change.test.js | 5 +++++ test/checkpoint.test.js | 5 +++++ test/data-source.test.js | 5 +++++ test/e2e/remote-connector.e2e.js | 5 +++++ test/e2e/replication.e2e.js | 5 +++++ test/email.test.js | 5 +++++ test/error-handler.test.js | 5 +++++ test/fixtures/access-control/server/server.js | 5 +++++ test/fixtures/e2e/server/models.js | 5 +++++ test/fixtures/e2e/server/server.js | 5 +++++ .../shared-methods/both-configs-set/common/models/todo.js | 5 +++++ .../shared-methods/both-configs-set/server/server.js | 5 +++++ .../config-default-false/common/models/todo.js | 5 +++++ .../shared-methods/config-default-false/server/server.js | 5 +++++ .../shared-methods/config-default-true/common/models/todo.js | 5 +++++ .../shared-methods/config-default-true/server/server.js | 5 +++++ .../config-defined-false/common/models/todo.js | 5 +++++ .../shared-methods/config-defined-false/server/server.js | 5 +++++ .../shared-methods/config-defined-true/common/models/todo.js | 5 +++++ .../shared-methods/config-defined-true/server/server.js | 5 +++++ .../model-config-default-false/common/models/todo.js | 5 +++++ .../model-config-default-false/server/server.js | 5 +++++ .../model-config-default-true/common/models/todo.js | 5 +++++ .../model-config-default-true/server/server.js | 5 +++++ .../model-config-defined-false/common/models/todo.js | 5 +++++ .../model-config-defined-false/server/server.js | 5 +++++ .../model-config-defined-true/common/models/todo.js | 5 +++++ .../model-config-defined-true/server/server.js | 5 +++++ test/fixtures/simple-app/boot/foo.js | 5 +++++ test/fixtures/simple-app/common/models/bar.js | 5 +++++ test/fixtures/simple-integration-app/server/server.js | 5 +++++ test/fixtures/user-integration-app/server/server.js | 5 +++++ test/geo-point.test.js | 5 +++++ test/hidden-properties.test.js | 5 +++++ test/integration.test.js | 5 +++++ test/karma.conf.js | 5 +++++ test/loopback.test.js | 5 +++++ test/memory.test.js | 5 +++++ test/model.application.test.js | 5 +++++ test/model.test.js | 5 +++++ test/registries.test.js | 5 +++++ test/relations.integration.js | 5 +++++ test/remote-connector.test.js | 5 +++++ test/remoting-coercion.test.js | 5 +++++ test/remoting.integration.js | 5 +++++ test/replication.rest.test.js | 5 +++++ test/replication.test.js | 5 +++++ test/rest.middleware.test.js | 5 +++++ test/role.test.js | 5 +++++ test/support.js | 5 +++++ test/user.integration.js | 5 +++++ test/user.test.js | 5 +++++ test/util/describe.js | 5 +++++ test/util/it.js | 5 +++++ test/util/model-tests.js | 5 +++++ 105 files changed, 525 insertions(+) diff --git a/Gruntfile.js b/Gruntfile.js index 3df919962..756bede3a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*global module:false*/ module.exports = function(grunt) { diff --git a/browser/current-context.js b/browser/current-context.js index cdf1d8a28..97d4a1a70 100644 --- a/browser/current-context.js +++ b/browser/current-context.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + module.exports = function(loopback) { loopback.getCurrentContext = function() { return null; diff --git a/common/models/access-token.js b/common/models/access-token.js index 27cf5206d..750c21f8b 100644 --- a/common/models/access-token.js +++ b/common/models/access-token.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! * Module Dependencies. */ diff --git a/common/models/acl.js b/common/models/acl.js index 2a7306b17..d4823cec8 100644 --- a/common/models/acl.js +++ b/common/models/acl.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! Schema ACL options diff --git a/common/models/application.js b/common/models/application.js index 617798c15..3286410a5 100644 --- a/common/models/application.js +++ b/common/models/application.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var assert = require('assert'); var utils = require('../../lib/utils'); diff --git a/common/models/change.js b/common/models/change.js index 3b347ec5c..605caa23d 100644 --- a/common/models/change.js +++ b/common/models/change.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! * Module Dependencies. */ diff --git a/common/models/checkpoint.js b/common/models/checkpoint.js index 59cc33de0..b48cb1866 100644 --- a/common/models/checkpoint.js +++ b/common/models/checkpoint.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /** * Module Dependencies. */ diff --git a/common/models/email.js b/common/models/email.js index f73628027..6a6736dc6 100644 --- a/common/models/email.js +++ b/common/models/email.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /** * Email model. Extends LoopBack base [Model](#model-new-model). * @property {String} to Email addressee. Required. diff --git a/common/models/role-mapping.js b/common/models/role-mapping.js index ee728483c..53af71f9a 100644 --- a/common/models/role-mapping.js +++ b/common/models/role-mapping.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../lib/loopback'); /** diff --git a/common/models/role.js b/common/models/role.js index 523e68982..c86049b74 100644 --- a/common/models/role.js +++ b/common/models/role.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../lib/loopback'); var debug = require('debug')('loopback:security:role'); var assert = require('assert'); diff --git a/common/models/scope.js b/common/models/scope.js index 3c713b535..478124d2a 100644 --- a/common/models/scope.js +++ b/common/models/scope.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var assert = require('assert'); var loopback = require('../../lib/loopback'); diff --git a/common/models/user.js b/common/models/user.js index b91b3ca18..7466ca8e7 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! * Module Dependencies. */ diff --git a/example/client-server/client.js b/example/client-server/client.js index 436e266b8..48c098f5e 100644 --- a/example/client-server/client.js +++ b/example/client-server/client.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../'); var client = loopback(); var CartItem = require('./models').CartItem; diff --git a/example/client-server/models.js b/example/client-server/models.js index 34d5c8bac..ac1b14320 100644 --- a/example/client-server/models.js +++ b/example/client-server/models.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../'); var CartItem = exports.CartItem = loopback.PersistedModel.extend('CartItem', { diff --git a/example/client-server/server.js b/example/client-server/server.js index 7e466a563..52c738d3a 100644 --- a/example/client-server/server.js +++ b/example/client-server/server.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../'); var server = module.exports = loopback(); var CartItem = require('./models').CartItem; diff --git a/example/colors/app.js b/example/colors/app.js index e182f926b..3e57b373b 100644 --- a/example/colors/app.js +++ b/example/colors/app.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../'); var app = loopback(); diff --git a/example/context/app.js b/example/context/app.js index 12cedc078..fa35eacca 100644 --- a/example/context/app.js +++ b/example/context/app.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../'); var app = loopback(); diff --git a/example/mobile-models/app.js b/example/mobile-models/app.js index e7e3c582b..abdf34c15 100644 --- a/example/mobile-models/app.js +++ b/example/mobile-models/app.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var models = require('../../lib/models'); var loopback = require('../../'); diff --git a/example/replication/app.js b/example/replication/app.js index ab6e69870..32e32299a 100644 --- a/example/replication/app.js +++ b/example/replication/app.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../'); var app = loopback(); var db = app.dataSource('db', {connector: loopback.Memory}); diff --git a/example/simple-data-source/app.js b/example/simple-data-source/app.js index 3964df77b..234baeb05 100644 --- a/example/simple-data-source/app.js +++ b/example/simple-data-source/app.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../'); var app = loopback(); diff --git a/index.js b/index.js index 1512239a7..bd558efa0 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /** * loopback ~ public api */ diff --git a/lib/access-context.js b/lib/access-context.js index 75ec50165..af04bdbba 100644 --- a/lib/access-context.js +++ b/lib/access-context.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var assert = require('assert'); var loopback = require('./loopback'); var debug = require('debug')('loopback:security:access-context'); diff --git a/lib/application.js b/lib/application.js index c5485fdb1..ae8ebcf36 100644 --- a/lib/application.js +++ b/lib/application.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! * Module dependencies. */ diff --git a/lib/browser-express.js b/lib/browser-express.js index 2a7dbe912..3b4237202 100644 --- a/lib/browser-express.js +++ b/lib/browser-express.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var EventEmitter = require('events').EventEmitter; var util = require('util'); diff --git a/lib/builtin-models.js b/lib/builtin-models.js index 78bf63990..59f0ff655 100644 --- a/lib/builtin-models.js +++ b/lib/builtin-models.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + module.exports = function(registry) { // NOTE(bajtos) we must use static require() due to browserify limitations diff --git a/lib/connectors/base-connector.js b/lib/connectors/base-connector.js index c1e37b7ba..a11dcce31 100644 --- a/lib/connectors/base-connector.js +++ b/lib/connectors/base-connector.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /** * Expose `Connector`. */ diff --git a/lib/connectors/mail.js b/lib/connectors/mail.js index a36984c39..3271c145e 100644 --- a/lib/connectors/mail.js +++ b/lib/connectors/mail.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /** * Dependencies. */ diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index 6a34417cd..f62448f61 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /** * Expose `Memory`. */ diff --git a/lib/express-middleware.js b/lib/express-middleware.js index f058a74a6..cc7596ff7 100644 --- a/lib/express-middleware.js +++ b/lib/express-middleware.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var path = require('path'); var middlewares = exports; diff --git a/lib/loopback.js b/lib/loopback.js index fd770e20d..656db41c2 100644 --- a/lib/loopback.js +++ b/lib/loopback.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! * Module dependencies. */ diff --git a/lib/model.js b/lib/model.js index 685e84045..5ffd82fb5 100644 --- a/lib/model.js +++ b/lib/model.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! * Module Dependencies. */ diff --git a/lib/persisted-model.js b/lib/persisted-model.js index 5dceeec04..734992795 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! * Module Dependencies. */ diff --git a/lib/registry.js b/lib/registry.js index ade0e2e88..ccce6248f 100644 --- a/lib/registry.js +++ b/lib/registry.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var assert = require('assert'); var extend = require('util')._extend; var juggler = require('loopback-datasource-juggler'); diff --git a/lib/runtime.js b/lib/runtime.js index 7e791f5b1..8799447bb 100644 --- a/lib/runtime.js +++ b/lib/runtime.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /* * This is an internal file that should not be used outside of loopback. * All exported entities can be accessed via the `loopback` object. diff --git a/lib/server-app.js b/lib/server-app.js index 237a62540..290c9b5ac 100644 --- a/lib/server-app.js +++ b/lib/server-app.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var assert = require('assert'); var express = require('express'); var merge = require('util')._extend; diff --git a/lib/utils.js b/lib/utils.js index 306a1764a..555a18616 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + exports.createPromiseCallback = createPromiseCallback; function createPromiseCallback() { diff --git a/server/current-context.js b/server/current-context.js index 6b8304e22..4da00bf4c 100644 --- a/server/current-context.js +++ b/server/current-context.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var juggler = require('loopback-datasource-juggler'); var remoting = require('strong-remoting'); var cls = require('continuation-local-storage'); diff --git a/server/middleware/context.js b/server/middleware/context.js index 95352018f..73948bd76 100644 --- a/server/middleware/context.js +++ b/server/middleware/context.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../lib/loopback'); module.exports = context; diff --git a/server/middleware/error-handler.js b/server/middleware/error-handler.js index c549944bf..1d30ae289 100644 --- a/server/middleware/error-handler.js +++ b/server/middleware/error-handler.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var expressErrorHandler = require('errorhandler'); expressErrorHandler.title = 'Loopback'; diff --git a/server/middleware/favicon.js b/server/middleware/favicon.js index d2e1fa40d..b5cf10cec 100644 --- a/server/middleware/favicon.js +++ b/server/middleware/favicon.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /** * Serve the LoopBack favicon. * @header loopback.favicon() diff --git a/server/middleware/rest.js b/server/middleware/rest.js index 9c7e23a28..2ec5a200d 100644 --- a/server/middleware/rest.js +++ b/server/middleware/rest.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! * Module dependencies. */ diff --git a/server/middleware/static.js b/server/middleware/static.js index c01a538df..6f253dfa5 100644 --- a/server/middleware/static.js +++ b/server/middleware/static.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /** * Serve static assets of a LoopBack application. * diff --git a/server/middleware/status.js b/server/middleware/status.js index 3e9308115..f064a9d83 100644 --- a/server/middleware/status.js +++ b/server/middleware/status.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! * Export the middleware. */ diff --git a/server/middleware/token.js b/server/middleware/token.js index 146c75d17..b5038df23 100644 --- a/server/middleware/token.js +++ b/server/middleware/token.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! * Module dependencies. */ diff --git a/server/middleware/url-not-found.js b/server/middleware/url-not-found.js index dd696d79f..d204f1575 100644 --- a/server/middleware/url-not-found.js +++ b/server/middleware/url-not-found.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! * Export the middleware. * See discussion in Connect pull request #954 for more details diff --git a/test/access-control.integration.js b/test/access-control.integration.js index 7e417bfe3..eed48a227 100644 --- a/test/access-control.integration.js +++ b/test/access-control.integration.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*jshint -W030 */ var loopback = require('../'); diff --git a/test/access-token.test.js b/test/access-token.test.js index a2ddbdb09..b6f8bce06 100644 --- a/test/access-token.test.js +++ b/test/access-token.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../'); var extend = require('util')._extend; var Token = loopback.AccessToken.extend('MyToken'); diff --git a/test/acl.test.js b/test/acl.test.js index d8706eec3..4bb182cb0 100644 --- a/test/acl.test.js +++ b/test/acl.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var assert = require('assert'); var loopback = require('../index'); var Scope = loopback.Scope; diff --git a/test/app.test.js b/test/app.test.js index 347d738b7..8b0d0d52c 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*jshint -W030 */ var async = require('async'); diff --git a/test/change-stream.test.js b/test/change-stream.test.js index ab7405214..177e31904 100644 --- a/test/change-stream.test.js +++ b/test/change-stream.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + describe('PersistedModel.createChangeStream()', function() { describe('configured to source changes locally', function() { before(function() { diff --git a/test/change.test.js b/test/change.test.js index 2f43c037f..5adc87589 100644 --- a/test/change.test.js +++ b/test/change.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var async = require('async'); var expect = require('chai').expect; diff --git a/test/checkpoint.test.js b/test/checkpoint.test.js index c824d02ea..ca7272ea2 100644 --- a/test/checkpoint.test.js +++ b/test/checkpoint.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var async = require('async'); var loopback = require('../'); var expect = require('chai').expect; diff --git a/test/data-source.test.js b/test/data-source.test.js index 662c07184..19ce7a87e 100644 --- a/test/data-source.test.js +++ b/test/data-source.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + describe('DataSource', function() { var memory; diff --git a/test/e2e/remote-connector.e2e.js b/test/e2e/remote-connector.e2e.js index 1a3424718..6a49536c2 100644 --- a/test/e2e/remote-connector.e2e.js +++ b/test/e2e/remote-connector.e2e.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var path = require('path'); var loopback = require('../../'); var models = require('../fixtures/e2e/models'); diff --git a/test/e2e/replication.e2e.js b/test/e2e/replication.e2e.js index 24f6967e0..74671f66c 100644 --- a/test/e2e/replication.e2e.js +++ b/test/e2e/replication.e2e.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var path = require('path'); var loopback = require('../../'); var models = require('../fixtures/e2e/models'); diff --git a/test/email.test.js b/test/email.test.js index 018f543ca..a5be75314 100644 --- a/test/email.test.js +++ b/test/email.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../'); var MyEmail; var assert = require('assert'); diff --git a/test/error-handler.test.js b/test/error-handler.test.js index 0494f2ef3..f5a1a6629 100644 --- a/test/error-handler.test.js +++ b/test/error-handler.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../'); var app; var assert = require('assert'); diff --git a/test/fixtures/access-control/server/server.js b/test/fixtures/access-control/server/server.js index ef251648a..13c2b9100 100644 --- a/test/fixtures/access-control/server/server.js +++ b/test/fixtures/access-control/server/server.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../../..'); var boot = require('loopback-boot'); var app = module.exports = loopback(); diff --git a/test/fixtures/e2e/server/models.js b/test/fixtures/e2e/server/models.js index dc36ca030..4b658610c 100644 --- a/test/fixtures/e2e/server/models.js +++ b/test/fixtures/e2e/server/models.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../../../index'); var PersistedModel = loopback.PersistedModel; diff --git a/test/fixtures/e2e/server/server.js b/test/fixtures/e2e/server/server.js index bd8a9411d..7fd6b7452 100644 --- a/test/fixtures/e2e/server/server.js +++ b/test/fixtures/e2e/server/server.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../../../index'); var app = module.exports = loopback(); var models = require('./models'); diff --git a/test/fixtures/shared-methods/both-configs-set/common/models/todo.js b/test/fixtures/shared-methods/both-configs-set/common/models/todo.js index 43ab55fbb..5d5125b83 100644 --- a/test/fixtures/shared-methods/both-configs-set/common/models/todo.js +++ b/test/fixtures/shared-methods/both-configs-set/common/models/todo.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + module.exports = function(Todo) { }; diff --git a/test/fixtures/shared-methods/both-configs-set/server/server.js b/test/fixtures/shared-methods/both-configs-set/server/server.js index 7876752e9..0d3e6b52e 100644 --- a/test/fixtures/shared-methods/both-configs-set/server/server.js +++ b/test/fixtures/shared-methods/both-configs-set/server/server.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var boot = require('loopback-boot'); var loopback = require('../../../../../index'); diff --git a/test/fixtures/shared-methods/config-default-false/common/models/todo.js b/test/fixtures/shared-methods/config-default-false/common/models/todo.js index 43ab55fbb..5d5125b83 100644 --- a/test/fixtures/shared-methods/config-default-false/common/models/todo.js +++ b/test/fixtures/shared-methods/config-default-false/common/models/todo.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + module.exports = function(Todo) { }; diff --git a/test/fixtures/shared-methods/config-default-false/server/server.js b/test/fixtures/shared-methods/config-default-false/server/server.js index 7876752e9..0d3e6b52e 100644 --- a/test/fixtures/shared-methods/config-default-false/server/server.js +++ b/test/fixtures/shared-methods/config-default-false/server/server.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var boot = require('loopback-boot'); var loopback = require('../../../../../index'); diff --git a/test/fixtures/shared-methods/config-default-true/common/models/todo.js b/test/fixtures/shared-methods/config-default-true/common/models/todo.js index 43ab55fbb..5d5125b83 100644 --- a/test/fixtures/shared-methods/config-default-true/common/models/todo.js +++ b/test/fixtures/shared-methods/config-default-true/common/models/todo.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + module.exports = function(Todo) { }; diff --git a/test/fixtures/shared-methods/config-default-true/server/server.js b/test/fixtures/shared-methods/config-default-true/server/server.js index 7876752e9..0d3e6b52e 100644 --- a/test/fixtures/shared-methods/config-default-true/server/server.js +++ b/test/fixtures/shared-methods/config-default-true/server/server.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var boot = require('loopback-boot'); var loopback = require('../../../../../index'); diff --git a/test/fixtures/shared-methods/config-defined-false/common/models/todo.js b/test/fixtures/shared-methods/config-defined-false/common/models/todo.js index 43ab55fbb..5d5125b83 100644 --- a/test/fixtures/shared-methods/config-defined-false/common/models/todo.js +++ b/test/fixtures/shared-methods/config-defined-false/common/models/todo.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + module.exports = function(Todo) { }; diff --git a/test/fixtures/shared-methods/config-defined-false/server/server.js b/test/fixtures/shared-methods/config-defined-false/server/server.js index 7876752e9..0d3e6b52e 100644 --- a/test/fixtures/shared-methods/config-defined-false/server/server.js +++ b/test/fixtures/shared-methods/config-defined-false/server/server.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var boot = require('loopback-boot'); var loopback = require('../../../../../index'); diff --git a/test/fixtures/shared-methods/config-defined-true/common/models/todo.js b/test/fixtures/shared-methods/config-defined-true/common/models/todo.js index 43ab55fbb..5d5125b83 100644 --- a/test/fixtures/shared-methods/config-defined-true/common/models/todo.js +++ b/test/fixtures/shared-methods/config-defined-true/common/models/todo.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + module.exports = function(Todo) { }; diff --git a/test/fixtures/shared-methods/config-defined-true/server/server.js b/test/fixtures/shared-methods/config-defined-true/server/server.js index 7876752e9..0d3e6b52e 100644 --- a/test/fixtures/shared-methods/config-defined-true/server/server.js +++ b/test/fixtures/shared-methods/config-defined-true/server/server.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var boot = require('loopback-boot'); var loopback = require('../../../../../index'); diff --git a/test/fixtures/shared-methods/model-config-default-false/common/models/todo.js b/test/fixtures/shared-methods/model-config-default-false/common/models/todo.js index 43ab55fbb..5d5125b83 100644 --- a/test/fixtures/shared-methods/model-config-default-false/common/models/todo.js +++ b/test/fixtures/shared-methods/model-config-default-false/common/models/todo.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + module.exports = function(Todo) { }; diff --git a/test/fixtures/shared-methods/model-config-default-false/server/server.js b/test/fixtures/shared-methods/model-config-default-false/server/server.js index 7876752e9..0d3e6b52e 100644 --- a/test/fixtures/shared-methods/model-config-default-false/server/server.js +++ b/test/fixtures/shared-methods/model-config-default-false/server/server.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var boot = require('loopback-boot'); var loopback = require('../../../../../index'); diff --git a/test/fixtures/shared-methods/model-config-default-true/common/models/todo.js b/test/fixtures/shared-methods/model-config-default-true/common/models/todo.js index 43ab55fbb..5d5125b83 100644 --- a/test/fixtures/shared-methods/model-config-default-true/common/models/todo.js +++ b/test/fixtures/shared-methods/model-config-default-true/common/models/todo.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + module.exports = function(Todo) { }; diff --git a/test/fixtures/shared-methods/model-config-default-true/server/server.js b/test/fixtures/shared-methods/model-config-default-true/server/server.js index 7876752e9..0d3e6b52e 100644 --- a/test/fixtures/shared-methods/model-config-default-true/server/server.js +++ b/test/fixtures/shared-methods/model-config-default-true/server/server.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var boot = require('loopback-boot'); var loopback = require('../../../../../index'); diff --git a/test/fixtures/shared-methods/model-config-defined-false/common/models/todo.js b/test/fixtures/shared-methods/model-config-defined-false/common/models/todo.js index 43ab55fbb..5d5125b83 100644 --- a/test/fixtures/shared-methods/model-config-defined-false/common/models/todo.js +++ b/test/fixtures/shared-methods/model-config-defined-false/common/models/todo.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + module.exports = function(Todo) { }; diff --git a/test/fixtures/shared-methods/model-config-defined-false/server/server.js b/test/fixtures/shared-methods/model-config-defined-false/server/server.js index 7876752e9..0d3e6b52e 100644 --- a/test/fixtures/shared-methods/model-config-defined-false/server/server.js +++ b/test/fixtures/shared-methods/model-config-defined-false/server/server.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var boot = require('loopback-boot'); var loopback = require('../../../../../index'); diff --git a/test/fixtures/shared-methods/model-config-defined-true/common/models/todo.js b/test/fixtures/shared-methods/model-config-defined-true/common/models/todo.js index 43ab55fbb..5d5125b83 100644 --- a/test/fixtures/shared-methods/model-config-defined-true/common/models/todo.js +++ b/test/fixtures/shared-methods/model-config-defined-true/common/models/todo.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + module.exports = function(Todo) { }; diff --git a/test/fixtures/shared-methods/model-config-defined-true/server/server.js b/test/fixtures/shared-methods/model-config-defined-true/server/server.js index 7876752e9..0d3e6b52e 100644 --- a/test/fixtures/shared-methods/model-config-defined-true/server/server.js +++ b/test/fixtures/shared-methods/model-config-defined-true/server/server.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var boot = require('loopback-boot'); var loopback = require('../../../../../index'); diff --git a/test/fixtures/simple-app/boot/foo.js b/test/fixtures/simple-app/boot/foo.js index 7e7486341..3e779699b 100644 --- a/test/fixtures/simple-app/boot/foo.js +++ b/test/fixtures/simple-app/boot/foo.js @@ -1 +1,6 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + process.loadedFooJS = true; diff --git a/test/fixtures/simple-app/common/models/bar.js b/test/fixtures/simple-app/common/models/bar.js index 10a3d968f..2c4064c85 100644 --- a/test/fixtures/simple-app/common/models/bar.js +++ b/test/fixtures/simple-app/common/models/bar.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + module.exports = function(Bar) { process.loadedBarJS = true; }; diff --git a/test/fixtures/simple-integration-app/server/server.js b/test/fixtures/simple-integration-app/server/server.js index d3f1c09c8..7c92b9c2a 100644 --- a/test/fixtures/simple-integration-app/server/server.js +++ b/test/fixtures/simple-integration-app/server/server.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../../../index'); var boot = require('loopback-boot'); var app = module.exports = loopback(); diff --git a/test/fixtures/user-integration-app/server/server.js b/test/fixtures/user-integration-app/server/server.js index 1d3d6720f..bbcb7fcc3 100644 --- a/test/fixtures/user-integration-app/server/server.js +++ b/test/fixtures/user-integration-app/server/server.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../../../index'); var boot = require('loopback-boot'); var app = module.exports = loopback(); diff --git a/test/geo-point.test.js b/test/geo-point.test.js index 9372caaea..8fb84b3e6 100644 --- a/test/geo-point.test.js +++ b/test/geo-point.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + describe('GeoPoint', function() { describe('geoPoint.distanceTo(geoPoint, options)', function() { it('Get the distance to another `GeoPoint`', function() { diff --git a/test/hidden-properties.test.js b/test/hidden-properties.test.js index c5e80e9ae..c00f538f5 100644 --- a/test/hidden-properties.test.js +++ b/test/hidden-properties.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../'); describe('hidden properties', function() { diff --git a/test/integration.test.js b/test/integration.test.js index 7aae5dc0b..27a7317cc 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var net = require('net'); describe('loopback application', function() { it('pauses request stream during authentication', function(done) { diff --git a/test/karma.conf.js b/test/karma.conf.js index f77923674..96a4d4e6d 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + // Karma configuration // http://karma-runner.github.io/0.12/config/configuration-file.html diff --git a/test/loopback.test.js b/test/loopback.test.js index 63d61d140..5531e8951 100644 --- a/test/loopback.test.js +++ b/test/loopback.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var it = require('./util/it'); var describe = require('./util/describe'); var Domain = require('domain'); diff --git a/test/memory.test.js b/test/memory.test.js index 7d9feb70e..4fe2f2a7e 100644 --- a/test/memory.test.js +++ b/test/memory.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + describe('Memory Connector', function() { it('Create a model using the memory connector', function(done) { // use the built in memory function diff --git a/test/model.application.test.js b/test/model.application.test.js index 96d8ab756..088172627 100644 --- a/test/model.application.test.js +++ b/test/model.application.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require(('../')); var assert = require('assert'); var Application = loopback.Application; diff --git a/test/model.test.js b/test/model.test.js index 37147f40a..46ec17eea 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var async = require('async'); var chai = require('chai'); var expect = chai.expect; diff --git a/test/registries.test.js b/test/registries.test.js index 8f44ac426..28804d043 100644 --- a/test/registries.test.js +++ b/test/registries.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + describe('Registry', function() { describe('one per app', function() { it('should allow two apps to reuse the same model name', function(done) { diff --git a/test/relations.integration.js b/test/relations.integration.js index dbb2e2556..5a1068f41 100644 --- a/test/relations.integration.js +++ b/test/relations.integration.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*jshint -W030 */ var loopback = require('../'); diff --git a/test/remote-connector.test.js b/test/remote-connector.test.js index e267c2e86..62a499f76 100644 --- a/test/remote-connector.test.js +++ b/test/remote-connector.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../'); var defineModelTestsWithDataSource = require('./util/model-tests'); diff --git a/test/remoting-coercion.test.js b/test/remoting-coercion.test.js index 6dab48e08..ee7aa51ef 100644 --- a/test/remoting-coercion.test.js +++ b/test/remoting-coercion.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../'); var request = require('supertest'); diff --git a/test/remoting.integration.js b/test/remoting.integration.js index b2b92cd81..02c255384 100644 --- a/test/remoting.integration.js +++ b/test/remoting.integration.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../'); var lt = require('loopback-testing'); var path = require('path'); diff --git a/test/replication.rest.test.js b/test/replication.rest.test.js index 6316457f0..68a79595e 100644 --- a/test/replication.rest.test.js +++ b/test/replication.rest.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var async = require('async'); var debug = require('debug')('test'); var extend = require('util')._extend; diff --git a/test/replication.test.js b/test/replication.test.js index ecdffbf8d..df6ca7874 100644 --- a/test/replication.test.js +++ b/test/replication.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var assert = require('assert'); var async = require('async'); var loopback = require('../'); diff --git a/test/rest.middleware.test.js b/test/rest.middleware.test.js index 61b935644..614209049 100644 --- a/test/rest.middleware.test.js +++ b/test/rest.middleware.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var path = require('path'); describe('loopback.rest', function() { diff --git a/test/role.test.js b/test/role.test.js index 8e5d1a129..7ac313bb7 100644 --- a/test/role.test.js +++ b/test/role.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var assert = require('assert'); var sinon = require('sinon'); var loopback = require('../index'); diff --git a/test/support.js b/test/support.js index 9e2d791f0..915f38dc4 100644 --- a/test/support.js +++ b/test/support.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /** * loopback test setup and support. */ diff --git a/test/user.integration.js b/test/user.integration.js index fd4a198bc..b3a82cd56 100644 --- a/test/user.integration.js +++ b/test/user.integration.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*jshint -W030 */ var loopback = require('../'); var lt = require('loopback-testing'); diff --git a/test/user.test.js b/test/user.test.js index 23fca2ad5..e4839c6e2 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + require('./support'); var loopback = require('../'); var User, AccessToken; diff --git a/test/util/describe.js b/test/util/describe.js index db7121131..ebcc3555c 100644 --- a/test/util/describe.js +++ b/test/util/describe.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../'); module.exports = describe; diff --git a/test/util/it.js b/test/util/it.js index f1b004e24..e3316dffe 100644 --- a/test/util/it.js +++ b/test/util/it.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../'); module.exports = it; diff --git a/test/util/model-tests.js b/test/util/model-tests.js index cd89f307b..d5509ca38 100644 --- a/test/util/model-tests.js +++ b/test/util/model-tests.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var async = require('async'); var describe = require('./describe'); var loopback = require('../../'); From 4798b2f8c9b2416e435f8b478bd8b8008df28b26 Mon Sep 17 00:00:00 2001 From: Supasate Choochaisri Date: Fri, 29 Apr 2016 15:50:11 +0700 Subject: [PATCH 030/187] Add feature to not allow duplicate role name - Also fix jshint error in backported test --- common/models/role.js | 2 ++ test/role.test.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/common/models/role.js b/common/models/role.js index c86049b74..b8c1242d5 100644 --- a/common/models/role.js +++ b/common/models/role.js @@ -445,4 +445,6 @@ module.exports = function(Role) { if (callback) callback(err, roles); }); }; + + Role.validatesUniquenessOf('name', { message: 'already exists' }); }; diff --git a/test/role.test.js b/test/role.test.js index 7ac313bb7..d11f4b376 100644 --- a/test/role.test.js +++ b/test/role.test.js @@ -93,6 +93,22 @@ describe('role model', function() { }); + it('should not allow duplicate role name', function(done) { + Role.create({ name: 'userRole' }, function(err, role) { + if (err) return done(err); + + Role.create({ name: 'userRole' }, function(err, role) { + expect(err).to.exist; //jshint ignore:line + expect(err).to.have.property('name', 'ValidationError'); + expect(err).to.have.deep.property('details.codes.name'); + expect(err.details.codes.name).to.contain('uniqueness'); + expect(err).to.have.property('statusCode', 422); + + done(); + }); + }); + }); + it('should automatically generate role id', function() { User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function(err, user) { @@ -233,6 +249,7 @@ describe('role model', function() { password: 'jpass' }, function(err, u) { if (err) return done(err); + user = u; User.create({ username: 'mary', @@ -240,15 +257,18 @@ describe('role model', function() { password: 'mpass' }, function(err, u) { if (err) return done(err); + Application.create({ name: 'demo' }, function(err, a) { if (err) return done(err); + app = a; Role.create({ name: 'admin' }, function(err, r) { if (err) return done(err); + role = r; var principals = [ { @@ -272,7 +292,9 @@ describe('role model', function() { it('should resolve user by id', function(done) { ACL.resolvePrincipal(ACL.USER, user.id, function(err, u) { if (err) return done(err); + expect(u.id).to.eql(user.id); + done(); }); }); @@ -280,7 +302,9 @@ describe('role model', function() { it('should resolve user by username', function(done) { ACL.resolvePrincipal(ACL.USER, user.username, function(err, u) { if (err) return done(err); + expect(u.username).to.eql(user.username); + done(); }); }); @@ -288,7 +312,9 @@ describe('role model', function() { it('should resolve user by email', function(done) { ACL.resolvePrincipal(ACL.USER, user.email, function(err, u) { if (err) return done(err); + expect(u.email).to.eql(user.email); + done(); }); }); @@ -296,7 +322,9 @@ describe('role model', function() { it('should resolve app by id', function(done) { ACL.resolvePrincipal(ACL.APP, app.id, function(err, a) { if (err) return done(err); + expect(a.id).to.eql(app.id); + done(); }); }); @@ -304,7 +332,9 @@ describe('role model', function() { it('should resolve app by name', function(done) { ACL.resolvePrincipal(ACL.APP, app.name, function(err, a) { if (err) return done(err); + expect(a.name).to.eql(app.name); + done(); }); }); @@ -312,7 +342,9 @@ describe('role model', function() { it('should report isMappedToRole by user.username', function(done) { ACL.isMappedToRole(ACL.USER, user.username, 'admin', function(err, flag) { if (err) return done(err); + expect(flag).to.eql(true); + done(); }); }); @@ -320,7 +352,9 @@ describe('role model', function() { it('should report isMappedToRole by user.email', function(done) { ACL.isMappedToRole(ACL.USER, user.email, 'admin', function(err, flag) { if (err) return done(err); + expect(flag).to.eql(true); + done(); }); }); @@ -329,7 +363,9 @@ describe('role model', function() { function(done) { ACL.isMappedToRole(ACL.USER, 'mary', 'admin', function(err, flag) { if (err) return done(err); + expect(flag).to.eql(false); + done(); }); }); @@ -337,7 +373,9 @@ describe('role model', function() { it('should report isMappedToRole by app.name', function(done) { ACL.isMappedToRole(ACL.APP, app.name, 'admin', function(err, flag) { if (err) return done(err); + expect(flag).to.eql(true); + done(); }); }); @@ -345,7 +383,9 @@ describe('role model', function() { it('should report isMappedToRole by app.name', function(done) { ACL.isMappedToRole(ACL.APP, app.name, 'admin', function(err, flag) { if (err) return done(err); + expect(flag).to.eql(true); + done(); }); }); @@ -383,6 +423,7 @@ describe('role model', function() { role[pluralName](function(err, models) { assert(!err); assert.equal(models.length, 1); + if (++runs === mappings.length) { done(); } @@ -404,6 +445,7 @@ describe('role model', function() { assert.equal(users.length, 1); assert.equal(users[0].id, user.id); assert(User.find.calledWith(query)); + done(); }); }); From e89fbd7ce835d1bfae17bd9afff2115d61a82b08 Mon Sep 17 00:00:00 2001 From: Supasate Choochaisri Date: Tue, 3 May 2016 09:45:21 +0700 Subject: [PATCH 031/187] Clean up by removing unnecessary comments Signed-off-by: Supasate Choochaisri --- test/role.test.js | 37 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/test/role.test.js b/test/role.test.js index d11f4b376..e2de4f928 100644 --- a/test/role.test.js +++ b/test/role.test.js @@ -15,7 +15,6 @@ var async = require('async'); var expect = require('chai').expect; function checkResult(err, result) { - // console.log(err, result); assert(!err); } @@ -65,11 +64,10 @@ describe('role model', function() { }); it('should define role/user relations', function() { - - User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function(err, user) { - // console.log('User: ', user.id); - Role.create({name: 'userRole'}, function(err, role) { - role.principals.create({principalType: RoleMapping.USER, principalId: user.id}, function(err, p) { + User.create({ name: 'Raymond', email: 'x@y.com', password: 'foobar' }, function(err, user) { + Role.create({ name: 'userRole' }, function(err, role) { + role.principals.create({ principalType: RoleMapping.USER, principalId: user.id }, + function(err, p) { Role.find(function(err, roles) { assert(!err); assert.equal(roles.length, 1); @@ -77,7 +75,6 @@ describe('role model', function() { }); role.principals(function(err, principals) { assert(!err); - // console.log(principals); assert.equal(principals.length, 1); assert.equal(principals[0].principalType, RoleMapping.USER); assert.equal(principals[0].principalId, user.id); @@ -110,10 +107,8 @@ describe('role model', function() { }); it('should automatically generate role id', function() { - - User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function(err, user) { - // console.log('User: ', user.id); - Role.create({name: 'userRole'}, function(err, role) { + User.create({ name: 'Raymond', email: 'x@y.com', password: 'foobar' }, function(err, user) { + Role.create({ name: 'userRole' }, function(err, role) { assert(role.id); role.principals.create({principalType: RoleMapping.USER, principalId: user.id}, function(err, p) { assert(p.id); @@ -125,7 +120,6 @@ describe('role model', function() { }); role.principals(function(err, principals) { assert(!err); - // console.log(principals); assert.equal(principals.length, 1); assert.equal(principals[0].principalType, RoleMapping.USER); assert.equal(principals[0].principalId, user.id); @@ -142,13 +136,12 @@ describe('role model', function() { }); it('should support getRoles() and isInRole()', function() { - User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function(err, user) { - // console.log('User: ', user.id); - Role.create({name: 'userRole'}, function(err, role) { - role.principals.create({principalType: RoleMapping.USER, principalId: user.id}, function(err, p) { - // Role.find(console.log); - // role.principals(console.log); - Role.isInRole('userRole', {principalType: RoleMapping.USER, principalId: user.id}, function(err, exists) { + User.create({ name: 'Raymond', email: 'x@y.com', password: 'foobar' }, function(err, user) { + Role.create({ name: 'userRole' }, function(err, role) { + role.principals.create({ principalType: RoleMapping.USER, principalId: user.id }, + function(err, p) { + Role.isInRole('userRole', { principalType: RoleMapping.USER, principalId: user.id }, + function(err, exists) { assert(!err && exists === true); }); @@ -225,9 +218,9 @@ describe('role model', function() { assert(!err && yes); }); - // console.log('User: ', user.id); - Album.create({name: 'Album 1', userId: user.id}, function(err, album1) { - Role.isInRole(Role.OWNER, {principalType: ACL.USER, principalId: user.id, model: Album, id: album1.id}, function(err, yes) { + Album.create({ name: 'Album 1', userId: user.id }, function(err, album1) { + var role = { principalType: ACL.USER, principalId: user.id, model: Album, id: album1.id }; + Role.isInRole(Role.OWNER, role, function(err, yes) { assert(!err && yes); }); Album.create({name: 'Album 2'}, function(err, album2) { From 25ade96d27ad96dedfc21b4313f6c7eb2477f406 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Fri, 6 May 2016 13:50:01 -0700 Subject: [PATCH 032/187] Backport separate error checking and done logic --- test/access-control.integration.js | 2 + test/access-token.test.js | 36 ++++- test/acl.test.js | 3 + test/app.test.js | 61 ++++++++ test/change-stream.test.js | 3 + test/change.test.js | 39 ++++++ test/checkpoint.test.js | 9 ++ test/e2e/remote-connector.e2e.js | 4 + test/e2e/replication.e2e.js | 4 +- test/email.test.js | 2 + test/error-handler.test.js | 3 + test/hidden-properties.test.js | 4 + test/integration.test.js | 2 + test/loopback.test.js | 4 + test/memory.test.js | 1 + test/model.application.test.js | 26 +++- test/model.test.js | 39 +++++- test/registries.test.js | 1 + test/relations.integration.js | 167 +++++++++++++++++++--- test/remote-connector.test.js | 4 + test/remoting-coercion.test.js | 2 + test/remoting.integration.js | 2 + test/replication.rest.test.js | 50 +++++++ test/replication.test.js | 108 +++++++++++++- test/rest.middleware.test.js | 31 ++++- test/role.test.js | 2 + test/user.integration.js | 43 +++--- test/user.test.js | 217 +++++++++++++++++------------ test/util/model-tests.js | 14 +- 29 files changed, 737 insertions(+), 146 deletions(-) diff --git a/test/access-control.integration.js b/test/access-control.integration.js index eed48a227..b32152573 100644 --- a/test/access-control.integration.js +++ b/test/access-control.integration.js @@ -137,6 +137,7 @@ describe('access control - integration', function() { var userCounter; function newUserData() { userCounter = userCounter ? ++userCounter : 1; + return { email: 'new-' + userCounter + '@test.test', password: 'test' @@ -214,6 +215,7 @@ describe('access control - integration', function() { balance: 100 }, function(err, act) { self.url = '/api/accounts/' + act.id; + done(); }); diff --git a/test/access-token.test.js b/test/access-token.test.js index b6f8bce06..573cae5a4 100644 --- a/test/access-token.test.js +++ b/test/access-token.test.js @@ -149,7 +149,8 @@ describe('loopback.token(options)', function() { .set('authorization', id) .end(function(err, res) { assert(!err); - assert.deepEqual(res.body, {userId: userId}); + assert.deepEqual(res.body, { userId: userId }); + done(); }); }); @@ -164,7 +165,8 @@ describe('loopback.token(options)', function() { .set('authorization', id) .end(function(err, res) { assert(!err); - assert.deepEqual(res.body, {userId: userId, state: 1}); + assert.deepEqual(res.body, { userId: userId, state: 1 }); + done(); }); }); @@ -179,7 +181,8 @@ describe('loopback.token(options)', function() { .set('authorization', id) .end(function(err, res) { assert(!err); - assert.deepEqual(res.body, {userId: userId, state: 1}); + assert.deepEqual(res.body, { userId: userId, state: 1 }); + done(); }); }); @@ -188,6 +191,7 @@ describe('loopback.token(options)', function() { var tokenStub = { id: 'stub id' }; app.use(function(req, res, next) { req.accessToken = tokenStub; + next(); }); app.use(loopback.token({ model: Token })); @@ -200,7 +204,9 @@ describe('loopback.token(options)', function() { .expect(200) .end(function(err, res) { if (err) return done(err); + expect(res.body).to.eql(tokenStub); + done(); }); }); @@ -211,6 +217,7 @@ describe('loopback.token(options)', function() { var tokenStub = { id: 'stub id' }; app.use(function(req, res, next) { req.accessToken = tokenStub; + next(); }); app.use(loopback.token({ model: Token })); @@ -223,7 +230,9 @@ describe('loopback.token(options)', function() { .expect(200) .end(function(err, res) { if (err) return done(err); + expect(res.body).to.eql(tokenStub); + done(); }); }); @@ -234,6 +243,7 @@ describe('loopback.token(options)', function() { var tokenStub = { id: 'stub id' }; app.use(function(req, res, next) { req.accessToken = tokenStub; + next(); }); app.use(loopback.token({ @@ -249,7 +259,9 @@ describe('loopback.token(options)', function() { .expect(200) .end(function(err, res) { if (err) return done(err); + expect(res.body).to.eql(tokenStub); + done(); }); }); @@ -262,6 +274,7 @@ describe('loopback.token(options)', function() { app.use(function(req, res, next) { req.accessToken = tokenStub; + next(); }); app.use(loopback.token({ @@ -278,12 +291,14 @@ describe('loopback.token(options)', function() { .expect(200) .end(function(err, res) { if (err) return done(err); + expect(res.body).to.eql({ id: token.id, ttl: token.ttl, userId: token.userId, created: token.created.toJSON(), }); + done(); }); }); @@ -306,6 +321,7 @@ describe('AccessToken', function() { it('should be validateable', function(done) { this.token.validate(function(err, isValid) { assert(isValid); + done(); }); }); @@ -321,7 +337,9 @@ describe('AccessToken', function() { Token.findForRequest(req, function(err, token) { if (err) return done(err); + expect(token.id).to.eql(expectedTokenId); + done(); }); }); @@ -355,9 +373,11 @@ describe('app.enableAuth()', function() { if (err) { return done(err); } + var errorResponse = res.body.error; assert(errorResponse); assert.equal(errorResponse.code, 'AUTHORIZATION_REQUIRED'); + done(); }); }); @@ -371,9 +391,11 @@ describe('app.enableAuth()', function() { if (err) { return done(err); } + var errorResponse = res.body.error; assert(errorResponse); assert.equal(errorResponse.code, 'ACCESS_DENIED'); + done(); }); }); @@ -387,9 +409,11 @@ describe('app.enableAuth()', function() { if (err) { return done(err); } + var errorResponse = res.body.error; assert(errorResponse); assert.equal(errorResponse.code, 'MODEL_NOT_FOUND'); + done(); }); }); @@ -403,9 +427,11 @@ describe('app.enableAuth()', function() { if (err) { return done(err); } + var errorResponse = res.body.error; assert(errorResponse); assert.equal(errorResponse.code, 'AUTHORIZATION_REQUIRED'); + done(); }); }); @@ -436,7 +462,9 @@ describe('app.enableAuth()', function() { .expect('Content-Type', /json/) .end(function(err, res) { if (err) return done(err); + expect(res.body.token.id).to.eql(token.id); + done(); }); }); @@ -446,7 +474,9 @@ function createTestingToken(done) { var test = this; Token.create({userId: '123'}, function(err, token) { if (err) return done(err); + test.token = token; + done(); }); } diff --git a/test/acl.test.js b/test/acl.test.js index 4bb182cb0..3022ebe6b 100644 --- a/test/acl.test.js +++ b/test/acl.test.js @@ -387,7 +387,9 @@ describe('access check', function() { MyTestModel.beforeRemote('find', function(ctx, next) { // ensure this is called after checkAccess if (!checkAccessCalled) return done(new Error('incorrect order')); + beforeHookCalled = true; + next(); }); @@ -396,6 +398,7 @@ describe('access check', function() { .end(function(err, result) { assert(beforeHookCalled, 'the before hook should be called'); assert(checkAccessCalled, 'checkAccess should have been called'); + done(); }); }); diff --git a/test/app.test.js b/test/app.test.js index 8b0d0d52c..fd8d03735 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -39,10 +39,12 @@ describe('app', function() { executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(steps).to.eql([ 'initial', 'session', 'auth', 'parse', 'main', 'routes', 'files', 'final' ]); + done(); }); }); @@ -53,7 +55,9 @@ describe('app', function() { executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(steps).to.eql(['first', 'second']); + done(); }); }); @@ -65,7 +69,9 @@ describe('app', function() { executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(steps).to.eql(['routes:before', 'main', 'routes:after']); + done(); }); }); @@ -85,7 +91,9 @@ describe('app', function() { expect(found).have.property('phase', 'routes:before'); executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(steps).to.eql(['my-handler', 'extra-handler']); + done(); }); }); @@ -103,7 +111,9 @@ describe('app', function() { expect(found).have.property('phase', 'routes:before'); executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(steps).to.eql(['my-handler']); + done(); }); }); @@ -121,7 +131,9 @@ describe('app', function() { expect(found).have.property('phase', 'routes:before'); executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(steps).to.eql(['my-handler']); + done(); }); }); @@ -131,6 +143,7 @@ describe('app', function() { app.middleware('initial', function(req, res, next) { steps.push('initial'); + next(expectedError); }); @@ -138,12 +151,15 @@ describe('app', function() { app.use(function errorHandler(err, req, res, next) { expect(err).to.equal(expectedError); steps.push('error'); + next(); }); executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(steps).to.eql(['initial', 'error']); + done(); }); }); @@ -157,6 +173,7 @@ describe('app', function() { executeMiddlewareHandlers(app, function(err) { expect(err).to.equal(expectedError); + done(); }); }); @@ -175,12 +192,15 @@ describe('app', function() { app.middleware('initial', function(err, req, res, next) { handledError = err; + next(); }); executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(handledError).to.equal(expectedError); + done(); }); }); @@ -193,7 +213,9 @@ describe('app', function() { function(url, next) { executeMiddlewareHandlers(app, url, next); }, function(err) { if (err) return done(err); + expect(steps).to.eql(['/scope', '/scope/item']); + done(); }); }); @@ -206,7 +228,9 @@ describe('app', function() { function(url, next) { executeMiddlewareHandlers(app, url, next); }, function(err) { if (err) return done(err); + expect(steps).to.eql(['/a', '/b']); + done(); }); }); @@ -219,7 +243,9 @@ describe('app', function() { function(url, next) { executeMiddlewareHandlers(app, url, next); }, function(err) { if (err) return done(err); + expect(steps).to.eql(['/a', '/b', '/scope']); + done(); }); }); @@ -227,12 +253,15 @@ describe('app', function() { it('sets req.url to a sub-path', function(done) { app.middleware('initial', ['/scope'], function(req, res, next) { steps.push(req.url); + next(); }); executeMiddlewareHandlers(app, '/scope/id', function(err) { if (err) return done(err); + expect(steps).to.eql(['/id']); + done(); }); }); @@ -244,11 +273,13 @@ describe('app', function() { app.middleware('initial', function(rq, rs, next) { req = rq; res = rs; + next(); }); executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(getObjectAndPrototypeKeys(req), 'request').to.include.members([ 'accepts', 'get', @@ -278,12 +309,15 @@ describe('app', function() { var reqProps; app.middleware('initial', function(req, res, next) { reqProps = { baseUrl: req.baseUrl, originalUrl: req.originalUrl }; + next(); }); executeMiddlewareHandlers(app, '/test/url', function(err) { if (err) return done(err); + expect(reqProps).to.eql({ baseUrl: '', originalUrl: '/test/url' }); + done(); }); }); @@ -295,7 +329,9 @@ describe('app', function() { executeMiddlewareHandlers(app, '/test', function(err) { if (err) return done(err); + expect(steps).to.eql(['route', 'files']); + done(); }); }); @@ -315,7 +351,9 @@ describe('app', function() { executeMiddlewareHandlers(app, function(err) { if (err) return done; + expect(steps).to.eql(numbers); + done(); }); }); @@ -329,6 +367,7 @@ describe('app', function() { mountpath: req.app.mountpath, parent: req.app.parent }; + next(); }); subapp.on('mount', function() { mountWasEmitted = true; }); @@ -337,11 +376,13 @@ describe('app', function() { executeMiddlewareHandlers(app, '/mountpath/test', function(err) { if (err) return done(err); + expect(mountWasEmitted, 'mountWasEmitted').to.be.true; expect(data).to.eql({ mountpath: '/mountpath', parent: app }); + done(); }); }); @@ -355,25 +396,30 @@ describe('app', function() { subapp.use(function verifyTestAssumptions(req, res, next) { expect(req.__proto__).to.not.equal(expected.req); expect(res.__proto__).to.not.equal(expected.res); + next(); }); app.middleware('initial', function saveOriginalValues(req, res, next) { expected.req = req.__proto__; expected.res = res.__proto__; + next(); }); app.middleware('routes', subapp); app.middleware('final', function saveActualValues(req, res, next) { actual.req = req.__proto__; actual.res = res.__proto__; + next(); }); executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(actual.req, 'req').to.equal(expected.req); expect(actual.res, 'res').to.equal(expected.res); + done(); }); }); @@ -388,6 +434,7 @@ describe('app', function() { function pathSavingHandler() { return function(req, res, next) { steps.push(req.originalUrl); + next(); }; } @@ -411,6 +458,7 @@ describe('app', function() { var args = Array.prototype.slice.apply(arguments); return function(req, res, next) { steps.push(args); + next(); }; }; @@ -461,12 +509,14 @@ describe('app', function() { executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(steps).to.eql([ ['before'], [expectedConfig], ['after', 2], [{x: 1}] ]); + done(); }); }); @@ -477,6 +527,7 @@ describe('app', function() { function factory() { return function(req, res, next) { steps.push(req.originalUrl); + next(); }; }, @@ -490,7 +541,9 @@ describe('app', function() { function(url, next) { executeMiddlewareHandlers(app, url, next); }, function(err) { if (err) return done(err); + expect(steps).to.eql(['/a', '/b', '/scope']); + done(); }); }); @@ -547,13 +600,16 @@ describe('app', function() { names.forEach(function(it) { app.middleware(it, function(req, res, next) { steps.push(it); + next(); }); }); executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(steps).to.eql(names); + done(); }); } @@ -786,6 +842,7 @@ describe('app', function() { app.listen(function() { expect(app.get('port'), 'port').to.not.equal(0); + done(); }); }); @@ -799,6 +856,7 @@ describe('app', function() { var host = process.platform === 'win32' ? 'localhost' : app.get('host'); var expectedUrl = 'http://' + host + ':' + app.get('port') + '/'; expect(app.get('url'), 'url').to.equal(expectedUrl); + done(); }); }); @@ -809,6 +867,7 @@ describe('app', function() { app.listen(0, '127.0.0.1', function() { expect(app.get('port'), 'port').to.not.equal(0).and.not.equal(1); expect(this.address().address).to.equal('127.0.0.1'); + done(); }); }); @@ -819,6 +878,7 @@ describe('app', function() { app.set('port', 1); app.listen(0).on('listening', function() { expect(app.get('port'), 'port') .to.not.equal(0).and.not.equal(1); + done(); }); } @@ -833,6 +893,7 @@ describe('app', function() { app.listen() .on('listening', function() { expect(this.address().address).to.equal('127.0.0.1'); + done(); }); }); diff --git a/test/change-stream.test.js b/test/change-stream.test.js index 177e31904..cbd24ca23 100644 --- a/test/change-stream.test.js +++ b/test/change-stream.test.js @@ -22,6 +22,7 @@ describe('PersistedModel.createChangeStream()', function() { changes.on('data', function(change) { expect(change.type).to.equal('create'); changes.destroy(); + done(); }); @@ -36,6 +37,7 @@ describe('PersistedModel.createChangeStream()', function() { changes.on('data', function(change) { expect(change.type).to.equal('update'); changes.destroy(); + done(); }); newScore.updateAttributes({ @@ -52,6 +54,7 @@ describe('PersistedModel.createChangeStream()', function() { changes.on('data', function(change) { expect(change.type).to.equal('remove'); changes.destroy(); + done(); }); diff --git a/test/change.test.js b/test/change.test.js index 5adc87589..220d8055f 100644 --- a/test/change.test.js +++ b/test/change.test.js @@ -33,9 +33,11 @@ describe('Change', function() { }; TestModel.create(test.data, function(err, model) { if (err) return done(err); + test.model = model; test.modelId = model.id; test.revisionForModel = Change.revisionForInst(model); + done(); }); }); @@ -66,6 +68,7 @@ describe('Change', function() { var test = this; Change.rectifyModelChanges(this.modelName, [this.modelId], function(err, trackedChanges) { if (err) return done(err); + done(); }); }); @@ -74,6 +77,7 @@ describe('Change', function() { var test = this; Change.find(function(err, trackedChanges) { assert.equal(trackedChanges[0].modelId, test.modelId.toString()); + done(); }); }); @@ -81,6 +85,7 @@ describe('Change', function() { it('should only create one change', function(done) { Change.count(function(err, count) { assert.equal(count, 1); + done(); }); }); @@ -103,6 +108,7 @@ describe('Change', function() { Change.find() .then(function(trackedChanges) { assert.equal(trackedChanges[0].modelId, test.modelId.toString()); + done(); }) .catch(done); @@ -112,6 +118,7 @@ describe('Change', function() { Change.count() .then(function(count) { assert.equal(count, 1); + done(); }) .catch(done); @@ -126,7 +133,9 @@ describe('Change', function() { var test = this; Change.findOrCreateChange(this.modelName, this.modelId, function(err, result) { if (err) return done(err); + test.result = result; + done(); }); }); @@ -135,7 +144,9 @@ describe('Change', function() { var test = this; Change.findById(this.result.id, function(err, change) { if (err) return done(err); + assert.equal(change.id, test.result.id); + done(); }); }); @@ -147,6 +158,7 @@ describe('Change', function() { Change.findOrCreateChange(this.modelName, this.modelId) .then(function(result) { test.result = result; + done(); }) .catch(done); @@ -156,7 +168,9 @@ describe('Change', function() { var test = this; Change.findById(this.result.id, function(err, change) { if (err) return done(err); + assert.equal(change.id, test.result.id); + done(); }); }); @@ -170,6 +184,7 @@ describe('Change', function() { modelId: test.modelId }, function(err, change) { test.existingChange = change; + done(); }); }); @@ -178,7 +193,9 @@ describe('Change', function() { var test = this; Change.findOrCreateChange(this.modelName, this.modelId, function(err, result) { if (err) return done(err); + test.result = result; + done(); }); }); @@ -186,6 +203,7 @@ describe('Change', function() { it('should find the entry', function(done) { var test = this; assert.equal(test.existingChange.id, test.result.id); + done(); }); }); @@ -201,6 +219,7 @@ describe('Change', function() { }, function(err, ch) { change = ch; + done(err); }); }); @@ -209,6 +228,7 @@ describe('Change', function() { var test = this; change.rectify(function(err, ch) { assert.equal(ch.rev, test.revisionForModel); + done(); }); }); @@ -232,6 +252,7 @@ describe('Change', function() { expect(change.type(), 'type').to.equal('update'); expect(change.prev, 'prev').to.equal(originalRev); expect(change.rev, 'rev').to.equal(test.revisionForModel); + next(); } ], done); @@ -243,7 +264,9 @@ describe('Change', function() { function checkpoint(next) { TestModel.checkpoint(function(err, inst) { if (err) return next(err); + cp = inst.seq; + next(); }); } @@ -254,6 +277,7 @@ describe('Change', function() { model.name += 'updated'; model.save(function(err) { test.revisionForModel = Change.revisionForInst(model); + next(err); }); } @@ -269,8 +293,10 @@ describe('Change', function() { change.rectify(function(err, c) { if (err) return done(err); + expect(c.rev, 'rev').to.equal(originalRev); // sanity check expect(c.checkpoint, 'checkpoint').to.equal(originalCheckpoint); + done(); }); }); @@ -283,6 +309,7 @@ describe('Change', function() { Change.findOrCreateChange(this.modelName, this.modelId) .then(function(ch) { change = ch; + done(); }) .catch(done); @@ -293,6 +320,7 @@ describe('Change', function() { change.rectify() .then(function(ch) { assert.equal(ch.rev, test.revisionForModel); + done(); }) .catch(done); @@ -309,6 +337,7 @@ describe('Change', function() { change.currentRevision(function(err, rev) { assert.equal(rev, test.revisionForModel); + done(); }); }); @@ -325,6 +354,7 @@ describe('Change', function() { change.currentRevision() .then(function(rev) { assert.equal(rev, test.revisionForModel); + done(); }) .catch(done); @@ -465,8 +495,10 @@ describe('Change', function() { Change.diff(this.modelName, 0, remoteChanges, function(err, diff) { if (err) return done(err); + assert.equal(diff.deltas.length, 1); assert.equal(diff.conflicts.length, 1); + done(); }); }); @@ -485,6 +517,7 @@ describe('Change', function() { .then(function(diff) { assert.equal(diff.deltas.length, 1); assert.equal(diff.conflicts.length, 1); + done(); }) .catch(done); @@ -500,6 +533,7 @@ describe('Change', function() { }; Change.diff(this.modelName, 0, [updateRecord], function(err, diff) { if (err) return done(err); + expect(diff.conflicts, 'conflicts').to.have.length(0); expect(diff.deltas, 'deltas').to.have.length(1); var actual = diff.deltas[0].toObject(); @@ -511,6 +545,7 @@ describe('Change', function() { prev: 'foo', // this is the current local revision rev: 'foo-new', }); + done(); }); }); @@ -527,6 +562,7 @@ describe('Change', function() { // with rev=foo CP=1 Change.diff(this.modelName, 2, [updateRecord], function(err, diff) { if (err) return done(err); + expect(diff.conflicts, 'conflicts').to.have.length(0); expect(diff.deltas, 'deltas').to.have.length(1); var actual = diff.deltas[0].toObject(); @@ -538,6 +574,7 @@ describe('Change', function() { prev: 'foo', // this is the current local revision rev: 'foo-new', }); + done(); }); }); @@ -553,6 +590,7 @@ describe('Change', function() { Change.diff(this.modelName, 0, [updateRecord], function(err, diff) { if (err) return done(err); + expect(diff.conflicts).to.have.length(0); expect(diff.deltas).to.have.length(1); var actual = diff.deltas[0].toObject(); @@ -564,6 +602,7 @@ describe('Change', function() { prev: null, // this is the current local revision rev: 'new-rev', }); + done(); }); }); diff --git a/test/checkpoint.test.js b/test/checkpoint.test.js index ca7272ea2..b5b9093ae 100644 --- a/test/checkpoint.test.js +++ b/test/checkpoint.test.js @@ -25,7 +25,9 @@ describe('Checkpoint', function() { function(next) { Checkpoint.current(function(err, seq) { if (err) next(err); + expect(seq).to.equal(3); + next(); }); } @@ -38,9 +40,12 @@ describe('Checkpoint', function() { function(next) { Checkpoint.current(next); } ], function(err, list) { if (err) return done(err); + Checkpoint.find(function(err, data) { if (err) return done(err); + expect(data).to.have.length(1); + done(); }); }); @@ -52,6 +57,7 @@ describe('Checkpoint', function() { function(next) { Checkpoint.bumpLastSeq(next); } ], function(err, list) { if (err) return done(err); + Checkpoint.find(function(err, data) { if (err) return done(err); // The invariant "we have at most 1 checkpoint instance" is preserved @@ -64,6 +70,7 @@ describe('Checkpoint', function() { // should be 2. expect(list.map(function(it) {return it.seq;})) .to.eql([2, 2]); + done(); }); }); @@ -72,6 +79,7 @@ describe('Checkpoint', function() { it('Checkpoint.current() for non existing checkpoint should initialize checkpoint', function(done) { Checkpoint.current(function(err, seq) { expect(seq).to.equal(1); + done(err); }); }); @@ -82,6 +90,7 @@ describe('Checkpoint', function() { // `bumpLastSeq` for the first time not only initializes it to one, // but also increments the initialized value by one. expect(cp.seq).to.equal(2); + done(err); }); }); diff --git a/test/e2e/remote-connector.e2e.js b/test/e2e/remote-connector.e2e.js index 6a49536c2..e24436929 100644 --- a/test/e2e/remote-connector.e2e.js +++ b/test/e2e/remote-connector.e2e.js @@ -24,7 +24,9 @@ describe('RemoteConnector', function() { foo: 'bar' }, function(err, inst) { if (err) return done(err); + assert(inst.id); + done(); }); }); @@ -35,7 +37,9 @@ describe('RemoteConnector', function() { }); m.save(function(err, data) { if (err) return done(err); + assert(data.foo === 'bar'); + done(); }); }); diff --git a/test/e2e/replication.e2e.js b/test/e2e/replication.e2e.js index 74671f66c..ad7d57363 100644 --- a/test/e2e/replication.e2e.js +++ b/test/e2e/replication.e2e.js @@ -32,8 +32,10 @@ describe('Replication', function() { }, function(err, created) { LocalTestModel.replicate(0, TestModel, function() { if (err) return done(err); - TestModel.findOne({n: RANDOM}, function(err, found) { + + TestModel.findOne({ n: RANDOM }, function(err, found) { assert.equal(created.id, found.id); + done(); }); }); diff --git a/test/email.test.js b/test/email.test.js index a5be75314..be12a3a63 100644 --- a/test/email.test.js +++ b/test/email.test.js @@ -66,6 +66,7 @@ describe('Email and SMTP', function() { assert(mail.response); assert(mail.envelope); assert(mail.messageId); + done(err); }); }); @@ -83,6 +84,7 @@ describe('Email and SMTP', function() { assert(mail.response); assert(mail.envelope); assert(mail.messageId); + done(err); }); }); diff --git a/test/error-handler.test.js b/test/error-handler.test.js index f5a1a6629..273481240 100644 --- a/test/error-handler.test.js +++ b/test/error-handler.test.js @@ -22,6 +22,7 @@ describe('loopback.errorHandler(options)', function() { .get('/url-does-not-exist') .end(function(err, res) { assert.ok(res.error.text.match(/
  •    at raiseUrlNotFoundError/)); + done(); }); }); @@ -38,6 +39,7 @@ describe('loopback.errorHandler(options)', function() { .get('/url-does-not-exist') .end(function(err, res) { assert.ok(res.error.text.match(/
      <\/ul>/)); + done(); }); }); @@ -61,6 +63,7 @@ describe('loopback.errorHandler(options)', function() { //assert expect(errorLogged) .to.have.property('message', 'Cannot GET /url-does-not-exist'); + done(); }); diff --git a/test/hidden-properties.test.js b/test/hidden-properties.test.js index c00f538f5..6e3f72064 100644 --- a/test/hidden-properties.test.js +++ b/test/hidden-properties.test.js @@ -46,8 +46,10 @@ describe('hidden properties', function() { .expect(200) .end(function(err, res) { if (err) return done(err); + var product = res.body[0]; assert.equal(product.secret, undefined); + done(); }); }); @@ -60,9 +62,11 @@ describe('hidden properties', function() { .expect(200) .end(function(err, res) { if (err) return done(err); + var category = res.body[0]; var product = category.products[0]; assert.equal(product.secret, undefined); + done(); }); }); diff --git a/test/integration.test.js b/test/integration.test.js index 27a7317cc..074bd2625 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -21,7 +21,9 @@ describe('loopback application', function() { 'X', function(err, res) { if (err) return done(err); + expect(res).to.match(/\nX$/); + done(); }); }); diff --git a/test/loopback.test.js b/test/loopback.test.js index 5531e8951..b3aea149f 100644 --- a/test/loopback.test.js +++ b/test/loopback.test.js @@ -611,6 +611,7 @@ describe('loopback', function() { }else { ctxx.result.data = 'context not available'; } + next(); }); @@ -618,7 +619,9 @@ describe('loopback', function() { .get('/TestModels/test') .end(function(err, res) { if (err) return done(err); + expect(res.body.data).to.equal('a value stored in context'); + done(); }); }); @@ -632,6 +635,7 @@ describe('loopback', function() { var ctx = loopback.getCurrentContext(); expect(ctx).is.an('object'); expect(ctx.get('test-key')).to.equal('test-value'); + done(); }); }); diff --git a/test/memory.test.js b/test/memory.test.js index 4fe2f2a7e..9a223be5f 100644 --- a/test/memory.test.js +++ b/test/memory.test.js @@ -33,6 +33,7 @@ describe('Memory Connector', function() { function count() { Product.count(function(err, count) { assert.equal(count, 3); + done(); }); } diff --git a/test/model.application.test.js b/test/model.application.test.js index 088172627..2f6e17ca6 100644 --- a/test/model.application.test.js +++ b/test/model.application.test.js @@ -17,6 +17,7 @@ describe('Application', function() { assert.equal(app.owner, 'rfeng'); assert.equal(app.name, 'MyTestApp'); assert.equal(app.description, 'My test application'); + done(err, result); }); }); @@ -29,6 +30,7 @@ describe('Application', function() { assert.equal(app.owner, 'rfeng'); assert.equal(app.name, 'MyTestApp'); assert.equal(app.description, 'My test application'); + done(); }) .catch(function(err) { @@ -52,6 +54,7 @@ describe('Application', function() { assert(app.created); assert(app.modified); assert.equal(typeof app.id, 'string'); + done(err, result); }); }); @@ -102,6 +105,7 @@ describe('Application', function() { serverApiKey: 'serverKey' } }); + done(err, result); }); }); @@ -121,6 +125,7 @@ describe('Application', function() { assert(app.created); assert(app.modified); registeredApp = app; + done(err, result); }); }); @@ -146,6 +151,7 @@ describe('Application', function() { assert(app.created); assert(app.modified); registeredApp = app; + done(err, result); }); }); @@ -172,6 +178,7 @@ describe('Application', function() { assert(app.created); assert(app.modified); registeredApp = app; + done(); }) .catch(function(err) { @@ -185,6 +192,7 @@ describe('Application', function() { assert(app.id); assert(app.id === registeredApp.id); registeredApp = app; + done(err, result); }); }); @@ -196,6 +204,7 @@ describe('Application', function() { assert(app.id); assert(app.id === registeredApp.id); registeredApp = app; + done(); }) .catch(function(err) { @@ -208,6 +217,7 @@ describe('Application', function() { function(err, result) { assert.equal(result.application.id, registeredApp.id); assert.equal(result.keyType, 'clientKey'); + done(err, result); }); }); @@ -216,10 +226,11 @@ describe('Application', function() { function(done) { Application.authenticate(registeredApp.id, registeredApp.clientKey) .then(function(result) { - assert.equal(result.application.id, registeredApp.id); - assert.equal(result.keyType, 'clientKey'); - done(); - }) + assert.equal(result.application.id, registeredApp.id); + assert.equal(result.keyType, 'clientKey'); + + done(); + }) .catch(function(err) { done(err); }); @@ -230,6 +241,7 @@ describe('Application', function() { function(err, result) { assert.equal(result.application.id, registeredApp.id); assert.equal(result.keyType, 'javaScriptKey'); + done(err, result); }); }); @@ -239,6 +251,7 @@ describe('Application', function() { function(err, result) { assert.equal(result.application.id, registeredApp.id); assert.equal(result.keyType, 'restApiKey'); + done(err, result); }); }); @@ -248,6 +261,7 @@ describe('Application', function() { function(err, result) { assert.equal(result.application.id, registeredApp.id); assert.equal(result.keyType, 'masterKey'); + done(err, result); }); }); @@ -257,6 +271,7 @@ describe('Application', function() { function(err, result) { assert.equal(result.application.id, registeredApp.id); assert.equal(result.keyType, 'windowsKey'); + done(err, result); }); }); @@ -265,6 +280,7 @@ describe('Application', function() { Application.authenticate(registeredApp.id, 'invalid-key', function(err, result) { assert(!result); + done(err, result); }); }); @@ -273,6 +289,7 @@ describe('Application', function() { Application.authenticate(registeredApp.id, 'invalid-key') .then(function(result) { assert(!result); + done(); }) .catch(function(err) { @@ -309,6 +326,7 @@ describe('Application subclass', function() { Application.findById(app.id, function(err, myApp) { assert(!err); assert(myApp === null); + done(err, myApp); }); }); diff --git a/test/model.test.js b/test/model.test.js index 46ec17eea..a02690011 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -148,6 +148,7 @@ describe.onServer('Remote Methods', function() { User.destroyAll(function() { User.count(function(err, count) { assert.equal(count, 0); + done(); }); }); @@ -164,7 +165,9 @@ describe.onServer('Remote Methods', function() { .expect(200) .end(function(err, res) { if (err) return done(err); + assert.equal(res.body, 123); + done(); }); }); @@ -174,12 +177,12 @@ describe.onServer('Remote Methods', function() { .get('/users/not-found') .expect(404) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + var errorResponse = res.body.error; assert(errorResponse); assert.equal(errorResponse.code, 'MODEL_NOT_FOUND'); + done(); }); }); @@ -192,6 +195,7 @@ describe.onServer('Remote Methods', function() { .expect(200) .end(function(err, res) { if (err) return done(err); + var userId = res.body.id; assert(userId); request(app) @@ -200,8 +204,10 @@ describe.onServer('Remote Methods', function() { .expect(200) .end(function(err, res) { if (err) return done(err); + assert.equal(res.body.first, 'x', 'first should be x'); assert(res.body.last === undefined, 'last should not be present'); + done(); }); }); @@ -215,6 +221,7 @@ describe.onServer('Remote Methods', function() { .expect(200) .end(function(err, res) { if (err) return done(err); + var userId = res.body.id; assert(userId); request(app) @@ -224,6 +231,7 @@ describe.onServer('Remote Methods', function() { .expect(200) .end(function(err, res) { if (err) return done(err); + var post = res.body; request(app) .get('/users/' + userId + '?filter[include]=posts') @@ -231,9 +239,11 @@ describe.onServer('Remote Methods', function() { .expect(200) .end(function(err, res) { if (err) return done(err); + assert.equal(res.body.first, 'x', 'first should be x'); assert.equal(res.body.last, 'y', 'last should be y'); assert.deepEqual(post, res.body.posts[0]); + done(); }); }); @@ -248,6 +258,7 @@ describe.onServer('Remote Methods', function() { User.beforeRemote('create', function(ctx, user, next) { hookCalled = true; + next(); }); @@ -259,7 +270,9 @@ describe.onServer('Remote Methods', function() { .expect(200) .end(function(err, res) { if (err) return done(err); + assert(hookCalled, 'hook wasnt called'); + done(); }); }); @@ -273,11 +286,13 @@ describe.onServer('Remote Methods', function() { User.beforeRemote('create', function(ctx, user, next) { assert(!afterCalled); beforeCalled = true; + next(); }); User.afterRemote('create', function(ctx, user, next) { assert(beforeCalled); afterCalled = true; + next(); }); @@ -289,8 +304,10 @@ describe.onServer('Remote Methods', function() { .expect(200) .end(function(err, res) { if (err) return done(err); + assert(beforeCalled, 'before hook was not called'); assert(afterCalled, 'after hook was not called'); + done(); }); }); @@ -301,14 +318,17 @@ describe.onServer('Remote Methods', function() { var actualError = 'hook not called'; User.afterRemoteError('login', function(ctx, next) { actualError = ctx.error; + next(); }); request(app).get('/users/sign-in?username=bob&password=123') .end(function(err, res) { if (err) return done(err); + expect(actualError) .to.have.property('message', 'bad username and password!'); + done(); }); }); @@ -327,6 +347,7 @@ describe.onServer('Remote Methods', function() { assert(ctx.res); assert(ctx.res.write); assert(ctx.res.end); + next(); }); @@ -338,7 +359,9 @@ describe.onServer('Remote Methods', function() { .expect(200) .end(function(err, res) { if (err) return done(err); + assert(hookCalled); + done(); }); }); @@ -356,6 +379,7 @@ describe.onServer('Remote Methods', function() { assert(ctx.res); assert(ctx.res.write); assert(ctx.res.end); + next(); }); @@ -367,7 +391,9 @@ describe.onServer('Remote Methods', function() { .expect(200) .end(function(err, res) { if (err) return done(err); + assert(hookCalled); + done(); }); }); @@ -392,6 +418,7 @@ describe.onServer('Remote Methods', function() { book.chapters({where: {title: 'Chapter 1'}}, function(err, chapters) { assert.equal(chapters.length, 1); assert.equal(chapters[0].title, 'Chapter 1'); + done(); }); }); @@ -537,6 +564,7 @@ describe.onServer('Remote Methods', function() { it('Get the Source Id', function(done) { User.getSourceId(function(err, id) { assert.equal('memory-user', id); + done(); }); }); @@ -556,6 +584,7 @@ describe.onServer('Remote Methods', function() { if (err) return done(err); assert.equal(result, current + 1); + done(); }); @@ -655,7 +684,9 @@ describe.onServer('Remote Methods', function() { app.model(TestModel, { dataSource: 'db' }); TestModel.getApp(function(err, a) { if (err) return done(err); + expect(a).to.equal(app); + done(); }); // fails on time-out when not implemented correctly @@ -664,7 +695,9 @@ describe.onServer('Remote Methods', function() { it('calls the callback after attached', function(done) { TestModel.getApp(function(err, a) { if (err) return done(err); + expect(a).to.equal(app); + done(); }); app.model(TestModel, { dataSource: 'db' }); diff --git a/test/registries.test.js b/test/registries.test.js index 28804d043..2f9cda87f 100644 --- a/test/registries.test.js +++ b/test/registries.test.js @@ -43,6 +43,7 @@ describe('Registry', function() { expect(bars.map(function(f) { return f.parent; })).to.eql(['bar']); + done(); }); }); diff --git a/test/relations.integration.js b/test/relations.integration.js index 5a1068f41..e853513e8 100644 --- a/test/relations.integration.js +++ b/test/relations.integration.js @@ -79,10 +79,12 @@ describe('relations - integration', function() { app.models.Team.create({ name: 'Team 1' }, function(err, team) { if (err) return done(err); + test.team = team; app.models.Reader.create({ name: 'Reader 1' }, function(err, reader) { if (err) return done(err); + test.reader = reader; reader.pictures.create({ name: 'Picture 1' }); reader.pictures.create({ name: 'Picture 2' }); @@ -103,12 +105,13 @@ describe('relations - integration', function() { .query({'filter': {'include' : 'pictures'}}) .expect(200, function(err, res) { if (err) return done(err); - // console.log(res.body); + expect(res.body.name).to.be.equal('Reader 1'); expect(res.body.pictures).to.be.eql([ { name: 'Picture 1', id: 1, imageableId: 1, imageableType: 'Reader'}, { name: 'Picture 2', id: 2, imageableId: 1, imageableType: 'Reader'}, ]); + done(); }); }); @@ -119,10 +122,11 @@ describe('relations - integration', function() { .query({'filter': {'include' : 'imageable'}}) .expect(200, function(err, res) { if (err) return done(err); - // console.log(res.body); + expect(res.body[0].name).to.be.equal('Picture 1'); expect(res.body[1].name).to.be.equal('Picture 2'); - expect(res.body[0].imageable).to.be.eql({ name: 'Reader 1', id: 1, teamId: 1}); + expect(res.body[0].imageable).to.be.eql({ name: 'Reader 1', id: 1, teamId: 1 }); + done(); }); }); @@ -133,10 +137,12 @@ describe('relations - integration', function() { .query({'filter': {'include' : {'relation': 'imageable', 'scope': { 'include' : 'team'}}}}) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body[0].name).to.be.equal('Picture 1'); expect(res.body[1].name).to.be.equal('Picture 2'); expect(res.body[0].imageable.name).to.be.eql('Reader 1'); - expect(res.body[0].imageable.team).to.be.eql({ name: 'Team 1', id: 1}); + expect(res.body[0].imageable.team).to.be.eql({ name: 'Team 1', id: 1 }); + done(); }); }); @@ -148,7 +154,9 @@ describe('relations - integration', function() { this.get('/api/stores/superStores') .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.be.array; + done(); }); }); @@ -199,8 +207,10 @@ describe('relations - integration', function() { this.http.send(this.newWidget); this.http.end(function(err) { if (err) return done(err); + this.req = this.http.req; this.res = this.http.res; + done(); }.bind(this)); }); @@ -226,7 +236,9 @@ describe('relations - integration', function() { storeId: this.store.id }, function(err, count) { if (err) return done(err); + assert.equal(count, 2); + done(); }); }); @@ -241,6 +253,7 @@ describe('relations - integration', function() { }, function(err, widget) { self.widget = widget; self.url = '/api/stores/' + self.store.id + '/widgets/' + widget.id; + done(); }); }); @@ -314,6 +327,7 @@ describe('relations - integration', function() { }, function(err, widget) { self.widget = widget; self.url = '/api/widgets/' + self.widget.id + '/store'; + done(); }); }); @@ -348,6 +362,7 @@ describe('relations - integration', function() { name: 'ph1' }, function(err, physician) { root.physician = physician; + done(); }); }, @@ -360,6 +375,7 @@ describe('relations - integration', function() { root.patient = patient; root.relUrl = '/api/physicians/' + root.physician.id + '/patients/rel/' + root.patient.id; + done(); }); } : function(done) { @@ -369,6 +385,7 @@ describe('relations - integration', function() { root.patient = patient; root.relUrl = '/api/physicians/' + root.physician.id + '/patients/rel/' + root.patient.id; + done(); }); }], function(err, done) { @@ -384,6 +401,7 @@ describe('relations - integration', function() { self.url = root.relUrl; self.patient = root.patient; self.physician = root.physician; + done(err); }); }); @@ -400,6 +418,7 @@ describe('relations - integration', function() { app.models.appointment.find(function(err, apps) { assert.equal(apps.length, 1); assert.equal(apps[0].patientId, self.patient.id); + done(); }); }); @@ -409,6 +428,7 @@ describe('relations - integration', function() { self.physician.patients(function(err, patients) { assert.equal(patients.length, 1); assert.equal(patients[0].id, self.patient.id); + done(); }); }); @@ -423,6 +443,7 @@ describe('relations - integration', function() { self.url = root.relUrl; self.patient = root.patient; self.physician = root.physician; + done(err); }); }); @@ -445,6 +466,7 @@ describe('relations - integration', function() { assert.equal(apps[0].patientId, self.patient.id); assert.equal(apps[0].physicianId, self.physician.id); assert.equal(apps[0].date.getTime(), NOW); + done(); }); }); @@ -454,6 +476,7 @@ describe('relations - integration', function() { self.physician.patients(function(err, patients) { assert.equal(patients.length, 1); assert.equal(patients[0].id, self.patient.id); + done(); }); }); @@ -468,6 +491,7 @@ describe('relations - integration', function() { self.url = root.relUrl; self.patient = root.patient; self.physician = root.physician; + done(err); }); }); @@ -488,6 +512,7 @@ describe('relations - integration', function() { '/patients/rel/' + '999'; self.patient = root.patient; self.physician = root.physician; + done(err); }); }); @@ -507,6 +532,7 @@ describe('relations - integration', function() { self.url = root.relUrl; self.patient = root.patient; self.physician = root.physician; + done(err); }); }); @@ -516,6 +542,7 @@ describe('relations - integration', function() { app.models.appointment.find(function(err, apps) { assert.equal(apps.length, 1); assert.equal(apps[0].patientId, self.patient.id); + done(); }); }); @@ -525,6 +552,7 @@ describe('relations - integration', function() { self.physician.patients(function(err, patients) { assert.equal(patients.length, 1); assert.equal(patients[0].id, self.patient.id); + done(); }); }); @@ -538,6 +566,7 @@ describe('relations - integration', function() { var self = this; app.models.appointment.find(function(err, apps) { assert.equal(apps.length, 0); + done(); }); }); @@ -547,6 +576,7 @@ describe('relations - integration', function() { // Need to refresh the cache self.physician.patients(true, function(err, patients) { assert.equal(patients.length, 0); + done(); }); }); @@ -562,6 +592,7 @@ describe('relations - integration', function() { '/patients/' + root.patient.id; self.patient = root.patient; self.physician = root.physician; + done(err); }); }); @@ -583,6 +614,7 @@ describe('relations - integration', function() { '/patients/' + root.patient.id; self.patient = root.patient; self.physician = root.physician; + done(err); }); }); @@ -596,6 +628,7 @@ describe('relations - integration', function() { var self = this; app.models.appointment.find(function(err, apps) { assert.equal(apps.length, 0); + done(); }); }); @@ -605,6 +638,7 @@ describe('relations - integration', function() { // Need to refresh the cache self.physician.patients(true, function(err, patients) { assert.equal(patients.length, 0); + done(); }); }); @@ -613,6 +647,7 @@ describe('relations - integration', function() { var self = this; app.models.patient.find(function(err, patients) { assert.equal(patients.length, 0); + done(); }); }); @@ -644,7 +679,9 @@ describe('relations - integration', function() { name: 'a-product' }, function(err, product) { if (err) return done(err); + test.product = product; + done(); }); }); @@ -653,6 +690,7 @@ describe('relations - integration', function() { app.models.category.create({ name: 'another-category' }, function(err, cat) { if (err) return done(err); + cat.products.create({ name: 'another-product' }, done); }); }); @@ -667,12 +705,14 @@ describe('relations - integration', function() { this.get('/api/products?filter[where][categoryId]=' + this.category.id) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.eql([ { id: expectedProduct.id, name: expectedProduct.name } ]); + done(); }); }); @@ -682,12 +722,14 @@ describe('relations - integration', function() { this.get('/api/categories/' + this.category.id + '/products') .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.eql([ { id: expectedProduct.id, name: expectedProduct.name } ]); + done(); }); }); @@ -700,6 +742,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.have.property('products'); expect(res.body.products).to.eql([ { @@ -707,6 +750,7 @@ describe('relations - integration', function() { name: expectedProduct.name } ]); + done(); }); }); @@ -720,6 +764,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.have.property('products'); expect(res.body.products).to.eql([ { @@ -727,6 +772,7 @@ describe('relations - integration', function() { name: expectedProduct.name } ]); + done(); }); }); @@ -754,7 +800,9 @@ describe('relations - integration', function() { app.models.group.create({ name: 'Group 1' }, function(err, group) { if (err) return done(err); + test.group = group; + done(); }); }); @@ -772,6 +820,7 @@ describe('relations - integration', function() { expect(res.body).to.be.eql( { url: 'http://image.url/' } ); + done(); }); }); @@ -782,10 +831,12 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body.name).to.be.equal('Group 1'); expect(res.body.poster).to.be.eql( { url: 'http://image.url/' } ); + done(); }); }); @@ -796,9 +847,11 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.be.eql( { url: 'http://image.url/' } ); + done(); }); }); @@ -810,6 +863,7 @@ describe('relations - integration', function() { .send({ url: 'http://changed.url/' }) .expect(200, function(err, res) { expect(res.body.url).to.be.equal('http://changed.url/'); + done(); }); }); @@ -820,9 +874,11 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.be.eql( { url: 'http://changed.url/' } ); + done(); }); }); @@ -861,6 +917,7 @@ describe('relations - integration', function() { app.models.todoList.create({ name: 'List A' }, function(err, list) { if (err) return done(err); + test.todoList = list; list.items.build({ content: 'Todo 1' }); list.items.build({ content: 'Todo 2' }); @@ -878,11 +935,13 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body.name).to.be.equal('List A'); expect(res.body.todoItems).to.be.eql([ { content: 'Todo 1', id: 1 }, { content: 'Todo 2', id: 2 } ]); + done(); }); }); @@ -893,10 +952,12 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.be.eql([ { content: 'Todo 1', id: 1 }, { content: 'Todo 2', id: 2 } ]); + done(); }); }); @@ -908,9 +969,11 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.be.eql([ { content: 'Todo 2', id: 2 } ]); + done(); }); }); @@ -924,6 +987,7 @@ describe('relations - integration', function() { .send({ content: 'Todo 3' }) .expect(200, function(err, res) { expect(res.body).to.be.eql(expected); + done(); }); }); @@ -934,11 +998,13 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.be.eql([ { content: 'Todo 1', id: 1 }, { content: 'Todo 2', id: 2 }, { content: 'Todo 3', id: 3 } ]); + done(); }); }); @@ -949,9 +1015,11 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.be.eql( { content: 'Todo 3', id: 3 } ); + done(); }); }); @@ -972,10 +1040,12 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.be.eql([ { content: 'Todo 1', id: 1 }, { content: 'Todo 3', id: 3 } ]); + done(); }); }); @@ -984,9 +1054,11 @@ describe('relations - integration', function() { var url = '/api/todo-lists/' + this.todoList.id + '/items/2'; this.get(url).expect(404, function(err, res) { if (err) return done(err); + expect(res.body.error.status).to.be.equal(404); expect(res.body.error.message).to.be.equal('Unknown "todoItem" id "2".'); expect(res.body.error.code).to.be.equal('MODEL_NOT_FOUND'); + done(); }); }); @@ -1038,6 +1110,7 @@ describe('relations - integration', function() { app.models.recipe.create({ name: 'Recipe' }, function(err, recipe) { if (err) return done(err); + test.recipe = recipe; recipe.ingredients.create({ name: 'Chocolate' }, @@ -1052,6 +1125,7 @@ describe('relations - integration', function() { var test = this; app.models.ingredient.create({ name: 'Sugar' }, function(err, ing) { test.ingredient2 = ing.id; + done(); }); }); @@ -1072,8 +1146,10 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body.ingredientIds).to.eql([test.ingredient1]); expect(res.body).to.not.have.property('ingredients'); + done(); }); }); @@ -1087,6 +1163,7 @@ describe('relations - integration', function() { .expect(200, function(err, res) { expect(res.body.name).to.be.eql('Butter'); test.ingredient3 = res.body.id; + done(); }); }); @@ -1098,11 +1175,13 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.be.eql([ { name: 'Chocolate', id: test.ingredient1 }, { name: 'Sugar', id: test.ingredient2 }, { name: 'Butter', id: test.ingredient3 } ]); + done(); }); }); @@ -1114,10 +1193,12 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.be.eql([ { name: 'Chocolate', id: test.ingredient1 }, { name: 'Butter', id: test.ingredient3 } ]); + done(); }); }); @@ -1130,9 +1211,11 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.be.eql([ { name: 'Butter', id: test.ingredient3 } ]); + done(); }); }); @@ -1145,6 +1228,7 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body.ingredientIds).to.eql([ test.ingredient1, test.ingredient3 ]); @@ -1152,6 +1236,7 @@ describe('relations - integration', function() { { name: 'Chocolate', id: test.ingredient1 }, { name: 'Butter', id: test.ingredient3 } ]); + done(); }); }); @@ -1164,9 +1249,11 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.be.eql( { name: 'Butter', id: test.ingredient3 } ); + done(); }); }); @@ -1180,8 +1267,10 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body.ingredientIds).to.eql(expected); expect(res.body).to.not.have.property('ingredients'); + done(); }); }); @@ -1204,10 +1293,12 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.be.eql([ { name: 'Chocolate', id: test.ingredient1 }, { name: 'Sugar', id: test.ingredient2 } ]); + done(); }); }); @@ -1219,9 +1310,11 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.be.eql([ { name: 'Chocolate', id: test.ingredient1 } ]); + done(); }); }); @@ -1236,6 +1329,7 @@ describe('relations - integration', function() { expect(res.body).to.be.eql( { name: 'Sugar', id: test.ingredient2 } ); + done(); }); }); @@ -1247,10 +1341,12 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.be.eql([ { name: 'Chocolate', id: test.ingredient1 }, { name: 'Sugar', id: test.ingredient2 } ]); + done(); }); }); @@ -1273,9 +1369,11 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.be.eql([ { name: 'Sugar', id: test.ingredient2 } ]); + done(); }); }); @@ -1287,10 +1385,12 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.be.eql([ { name: 'Chocolate', id: test.ingredient1 }, { name: 'Sugar', id: test.ingredient2 } ]); + done(); }); }); @@ -1301,8 +1401,10 @@ describe('relations - integration', function() { this.get(url) .expect(200, function(err, res) { if (err) return done(err); + expect(err).to.not.exist; expect(res.body.name).to.equal('Photo 1'); + done(); }); }); @@ -1379,11 +1481,13 @@ describe('relations - integration', function() { Page.beforeRemote('prototype.__findById__notes', function(ctx, result, next) { ctx.res.set('x-before', 'before'); + next(); }); Page.afterRemote('prototype.__findById__notes', function(ctx, result, next) { ctx.res.set('x-after', 'after'); + next(); }); @@ -1394,14 +1498,17 @@ describe('relations - integration', function() { app.models.Book.create({ name: 'Book 1' }, function(err, book) { if (err) return done(err); + test.book = book; book.pages.create({ name: 'Page 1' }, function(err, page) { if (err) return done(err); + test.page = page; page.notes.create({ text: 'Page Note 1' }, function(err, note) { test.note = note; + done(); }); }); @@ -1413,9 +1520,11 @@ describe('relations - integration', function() { test.book.chapters.create({ name: 'Chapter 1' }, function(err, chapter) { if (err) return done(err); + test.chapter = chapter; chapter.notes.create({ text: 'Chapter Note 1' }, function(err, note) { test.cnote = note; + done(); }); }); @@ -1426,7 +1535,9 @@ describe('relations - integration', function() { app.models.Image.create({ name: 'Cover 1', book: test.book }, function(err, image) { if (err) return done(err); + test.image = image; + done(); }); }); @@ -1436,9 +1547,11 @@ describe('relations - integration', function() { this.get('/api/books/' + test.book.id + '/pages') .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.be.an.array; expect(res.body).to.have.length(1); expect(res.body[0].name).to.equal('Page 1'); + done(); }); }); @@ -1448,10 +1561,12 @@ describe('relations - integration', function() { this.get('/api/pages/' + test.page.id + '/notes/' + test.note.id) .expect(200, function(err, res) { if (err) return done(err); + expect(res.headers['x-before']).to.equal('before'); expect(res.headers['x-after']).to.equal('after'); expect(res.body).to.be.an.object; expect(res.body.text).to.equal('Page Note 1'); + done(); }); }); @@ -1461,10 +1576,12 @@ describe('relations - integration', function() { this.get('/api/books/unknown/pages/' + test.page.id + '/notes') .expect(404, function(err, res) { if (err) return done(err); + expect(res.body.error).to.be.an.object; var expected = 'could not find a model with id unknown'; expect(res.body.error.message).to.equal(expected); expect(res.body.error.code).to.be.equal('MODEL_NOT_FOUND'); + done(); }); }); @@ -1474,9 +1591,11 @@ describe('relations - integration', function() { this.get('/api/images/' + test.image.id + '/book/pages') .end(function(err, res) { if (err) return done(err); + expect(res.body).to.be.an.array; expect(res.body).to.have.length(1); expect(res.body[0].name).to.equal('Page 1'); + done(); }); }); @@ -1486,8 +1605,10 @@ describe('relations - integration', function() { this.get('/api/images/' + test.image.id + '/book/pages/' + test.page.id) .end(function(err, res) { if (err) return done(err); + expect(res.body).to.be.an.object; expect(res.body.name).to.equal('Page 1'); + done(); }); }); @@ -1497,9 +1618,11 @@ describe('relations - integration', function() { this.get('/api/books/' + test.book.id + '/pages/' + test.page.id + '/notes') .expect(200, function(err, res) { if (err) return done(err); + expect(res.body).to.be.an.array; expect(res.body).to.have.length(1); expect(res.body[0].text).to.equal('Page Note 1'); + done(); }); }); @@ -1509,10 +1632,12 @@ describe('relations - integration', function() { this.get('/api/books/' + test.book.id + '/pages/' + test.page.id + '/notes/' + test.note.id) .expect(200, function(err, res) { if (err) return done(err); + expect(res.headers['x-before']).to.equal('before'); expect(res.headers['x-after']).to.equal('after'); expect(res.body).to.be.an.object; expect(res.body.text).to.equal('Page Note 1'); + done(); }); }); @@ -1522,8 +1647,10 @@ describe('relations - integration', function() { this.get('/api/books/' + test.book.id + '/chapters/' + test.chapter.id + '/notes/' + test.cnote.id) .expect(200, function(err, res) { if (err) return done(err); + expect(res.headers['x-before']).to.empty; expect(res.headers['x-after']).to.empty; + done(); }); }); @@ -1535,6 +1662,7 @@ describe('relations - integration', function() { http.forEach(function(opt) { // destroyAll has been shared but missing http property if (opt.path === undefined) return; + expect(opt.path, method.stringName).to.match(/^\/.*/); }); }); @@ -1546,11 +1674,13 @@ describe('relations - integration', function() { this.get('/api/books/' + test.book.id + '/pages/' + this.page.id + '/throws') .end(function(err, res) { if (err) return done(err); + expect(res.body).to.be.an('object'); expect(res.body.error).to.be.an('object'); expect(res.body.error.name).to.equal('Error'); expect(res.body.error.status).to.equal(500); expect(res.body.error.message).to.equal('This should not crash the app'); + done(); }); }); @@ -1562,10 +1692,10 @@ describe('relations - integration', function() { before(function createCustomer(done) { var test = this; app.models.customer.create({ name: 'John' }, function(err, c) { - if (err) { - return done(err); - } + if (err) return done(err); + cust = c; + done(); }); }); @@ -1573,9 +1703,8 @@ describe('relations - integration', function() { after(function(done) { var self = this; this.app.models.customer.destroyAll(function(err) { - if (err) { - return done(err); - } + if (err) return done(err); + self.app.models.profile.destroyAll(done); }); }); @@ -1586,11 +1715,11 @@ describe('relations - integration', function() { this.post(url) .send({points: 10}) .expect(200, function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + expect(res.body.points).to.be.eql(10); expect(res.body.customerId).to.be.eql(cust.id); + done(); }); }); @@ -1599,11 +1728,11 @@ describe('relations - integration', function() { var url = '/api/customers/' + cust.id + '/profile'; this.get(url) .expect(200, function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + expect(res.body.points).to.be.eql(10); expect(res.body.customerId).to.be.eql(cust.id); + done(); }); }); @@ -1622,11 +1751,11 @@ describe('relations - integration', function() { this.put(url) .send({points: 100}) .expect(200, function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + expect(res.body.points).to.be.eql(100); expect(res.body.customerId).to.be.eql(cust.id); + done(); }); }); diff --git a/test/remote-connector.test.js b/test/remote-connector.test.js index 62a499f76..701e170ba 100644 --- a/test/remote-connector.test.js +++ b/test/remote-connector.test.js @@ -21,6 +21,7 @@ describe('RemoteConnector', function() { port: remoteApp.get('port'), connector: loopback.Remote }); + done(); }); }, @@ -48,6 +49,7 @@ describe('RemoteConnector', function() { port: remoteApp.get('port'), connector: loopback.Remote }); + done(); }); }); @@ -70,8 +72,10 @@ describe('RemoteConnector', function() { var m = new RemoteModel({foo: 'bar'}); m.save(function(err, inst) { if (err) return done(err); + assert(inst instanceof RemoteModel); assert(calledServerCreate); + done(); }); }); diff --git a/test/remoting-coercion.test.js b/test/remoting-coercion.test.js index ee7aa51ef..bc6bd7950 100644 --- a/test/remoting-coercion.test.js +++ b/test/remoting-coercion.test.js @@ -32,7 +32,9 @@ describe('remoting coercion', function() { }) .end(function(err) { if (err) return done(err); + assert(called); + done(); }); }); diff --git a/test/remoting.integration.js b/test/remoting.integration.js index 02c255384..5154bb473 100644 --- a/test/remoting.integration.js +++ b/test/remoting.integration.js @@ -52,6 +52,7 @@ describe('remoting - integration', function() { this.req = this.http.req; this.res = this.http.res; assert.equal(this.res.statusCode, 200); + done(); }.bind(this)); }); @@ -72,6 +73,7 @@ describe('remoting - integration', function() { this.res = this.http.res; // Request is rejected with 413 assert.equal(this.res.statusCode, 413); + done(); }.bind(this)); }); diff --git a/test/replication.rest.test.js b/test/replication.rest.test.js index 68a79595e..eb963e7b7 100644 --- a/test/replication.rest.test.js +++ b/test/replication.rest.test.js @@ -114,11 +114,14 @@ describe('Replication over REST', function() { it('allows pull from server', function(done) { RemoteCar.replicate(LocalCar, function(err, conflicts, cps) { if (err) return done(err); + if (conflicts.length) return done(conflictError(conflicts)); LocalCar.find(function(err, list) { if (err) return done(err); + expect(list.map(carToString)).to.include.members(serverCars); + done(); }); }); @@ -137,11 +140,14 @@ describe('Replication over REST', function() { it('allows pull from server', function(done) { RemoteCar.replicate(LocalCar, function(err, conflicts, cps) { if (err) return done(err); + if (conflicts.length) return done(conflictError(conflicts)); LocalCar.find(function(err, list) { if (err) return done(err); + expect(list.map(carToString)).to.include.members(serverCars); + done(); }); }); @@ -150,11 +156,14 @@ describe('Replication over REST', function() { it('allows push to the server', function(done) { LocalCar.replicate(RemoteCar, function(err, conflicts, cps) { if (err) return done(err); + if (conflicts.length) return done(conflictError(conflicts)); ServerCar.find(function(err, list) { if (err) return done(err); + expect(list.map(carToString)).to.include.members(clientCars); + done(); }); }); @@ -224,6 +233,7 @@ describe('Replication over REST', function() { it('allows reverse resolve() on the client', function(done) { RemoteCar.replicate(LocalCar, function(err, conflicts) { if (err) return done(err); + expect(conflicts, 'conflicts').to.have.length(1); // By default, conflicts are always resolved by modifying @@ -238,7 +248,9 @@ describe('Replication over REST', function() { RemoteCar.replicate(LocalCar, function(err, conflicts) { if (err) return done(err); + if (conflicts.length) return done(conflictError(conflicts)); + done(); }); }); @@ -248,6 +260,7 @@ describe('Replication over REST', function() { it('rejects resolve() on the server', function(done) { RemoteCar.replicate(LocalCar, function(err, conflicts) { if (err) return done(err); + expect(conflicts, 'conflicts').to.have.length(1); conflicts[0].resolveUsingSource(expectHttpError(401, done)); }); @@ -262,13 +275,17 @@ describe('Replication over REST', function() { it('allows resolve() on the client', function(done) { LocalCar.replicate(RemoteCar, function(err, conflicts) { if (err) return done(err); + expect(conflicts).to.have.length(1); conflicts[0].resolveUsingSource(function(err) { if (err) return done(err); + LocalCar.replicate(RemoteCar, function(err, conflicts) { if (err) return done(err); + if (conflicts.length) return done(conflictError(conflicts)); + done(); }); }); @@ -278,13 +295,17 @@ describe('Replication over REST', function() { it('allows resolve() on the server', function(done) { RemoteCar.replicate(LocalCar, function(err, conflicts) { if (err) return done(err); + expect(conflicts).to.have.length(1); conflicts[0].resolveUsingSource(function(err) { if (err) return done(err); + RemoteCar.replicate(LocalCar, function(err, conflicts) { if (err) return done(err); + if (conflicts.length) return done(conflictError(conflicts)); + done(); }); }); @@ -298,10 +319,13 @@ describe('Replication over REST', function() { setAccessToken(aliceToken); RemoteUser.replicate(LocalUser, function(err, conflicts, cps) { if (err) return done(err); + if (conflicts.length) return done(conflictError(conflicts)); + LocalUser.find(function(err, users) { var userNames = users.map(function(u) { return u.username; }); expect(userNames).to.eql([ALICE.username]); + done(); }); }); @@ -315,7 +339,9 @@ describe('Replication over REST', function() { setAccessToken(aliceToken); LocalUser.replicate(RemoteUser, function(err, conflicts) { if (err) return next(err); + if (conflicts.length) return next(conflictError(conflicts)); + next(); }); }, @@ -323,8 +349,10 @@ describe('Replication over REST', function() { function verify(next) { RemoteUser.findById(aliceId, function(err, found) { if (err) return next(err); + expect(found.toObject()) .to.have.property('fullname', 'Alice Smith'); + next(); }); } @@ -340,7 +368,9 @@ describe('Replication over REST', function() { LocalUser.replicate(RemoteUser, function(err, conflicts) { if (!err) return next(new Error('Replicate should have failed.')); + expect(err).to.have.property('statusCode', 401); // or 403? + next(); }); }, @@ -348,8 +378,10 @@ describe('Replication over REST', function() { function verify(next) { ServerUser.findById(aliceId, function(err, found) { if (err) return next(err); + expect(found.toObject()) .to.not.have.property('fullname'); + next(); }); } @@ -461,6 +493,7 @@ describe('Replication over REST', function() { serverApp.use(function(req, res, next) { debug(req.method + ' ' + req.path); + next(); }); serverApp.use(loopback.token({ model: ServerToken })); @@ -472,6 +505,7 @@ describe('Replication over REST', function() { serverApp.listen(function() { serverUrl = serverApp.get('url').replace(/\/+$/, ''); request = supertest(serverUrl); + done(); }); } @@ -527,18 +561,22 @@ describe('Replication over REST', function() { function(next) { ServerUser.create([ALICE, PETER, EMERY], function(err, created) { if (err) return next(err); + aliceId = created[0].id; peterId = created[1].id; + next(); }); }, function(next) { ServerUser.login(ALICE, function(err, token) { if (err) return next(err); + aliceToken = token.id; ServerUser.login(PETER, function(err, token) { if (err) return next(err); + peterToken = token.id; ServerUser.login(EMERY, function(err, token) { @@ -557,7 +595,9 @@ describe('Replication over REST', function() { ], function(err, cars) { if (err) return next(err); + serverCars = cars.map(carToString); + next(); }); } @@ -574,7 +614,9 @@ describe('Replication over REST', function() { [{ maker: 'Local', model: 'Custom' }], function(err, cars) { if (err) return next(err); + clientCars = cars.map(carToString); + next(); }); }, @@ -584,9 +626,12 @@ describe('Replication over REST', function() { function seedConflict(done) { LocalCar.replicate(ServerCar, function(err, conflicts) { if (err) return done(err); + if (conflicts.length) return done(conflictError(conflicts)); + ServerCar.replicate(LocalCar, function(err, conflicts) { if (err) return done(err); + if (conflicts.length) return done(conflictError(conflicts)); // Hard-coded, see the seed data above @@ -595,6 +640,7 @@ describe('Replication over REST', function() { new LocalCar({ id: conflictedCarId }) .updateAttributes({ model: 'Client' }, function(err, c) { if (err) return done(err); + new ServerCar({ id: conflictedCarId }) .updateAttributes({ model: 'Server' }, done); }); @@ -612,7 +658,9 @@ describe('Replication over REST', function() { function expectHttpError(code, done) { return function(err) { if (!err) return done(new Error('The method should have failed.')); + expect(err).to.have.property('statusCode', code); + done(); }; } @@ -620,7 +668,9 @@ describe('Replication over REST', function() { function replicateServerToLocal(next) { ServerUser.replicate(LocalUser, function(err, conflicts) { if (err) return next(err); + if (conflicts.length) return next(conflictError(conflicts)); + next(); }); } diff --git a/test/replication.test.js b/test/replication.test.js index df6ca7874..83a88eb98 100644 --- a/test/replication.test.js +++ b/test/replication.test.js @@ -51,6 +51,7 @@ describe('Replication / Change APIs', function() { this.createInitalData = function(cb) { SourceModel.create({name: 'foo'}, function(err, inst) { if (err) return cb(err); + test.model = inst; SourceModel.replicate(TargetModel, cb); }); @@ -75,7 +76,9 @@ describe('Replication / Change APIs', function() { var calls = mockSourceModelRectify(); SourceModel.destroyAll({name: 'John'}, function(err, data) { if (err) return done(err); + expect(calls).to.eql(['rectifyAllChanges']); + done(); }); }); @@ -85,7 +88,9 @@ describe('Replication / Change APIs', function() { var newData = {'name': 'Janie'}; SourceModel.update({name: 'Jane'}, newData, function(err, data) { if (err) return done(err); + expect(calls).to.eql(['rectifyAllChanges']); + done(); }); }); @@ -102,7 +107,9 @@ describe('Replication / Change APIs', function() { } ], function(err, results) { if (err) return done(err); + expect(calls).to.eql(['rectifyChange']); + done(); }); }); @@ -120,7 +127,9 @@ describe('Replication / Change APIs', function() { } ], function(err, result) { if (err) return done(err); + expect(calls).to.eql(['rectifyChange']); + done(); }); }); @@ -138,7 +147,9 @@ describe('Replication / Change APIs', function() { } ], function(err, result) { if (err) return done(err); + expect(calls).to.eql(['rectifyChange']); + done(); }); }); @@ -181,9 +192,11 @@ describe('Replication / Change APIs', function() { var test = this; this.SourceModel.create({name: 'foo'}, function(err) { if (err) return done(err); + setTimeout(function() { test.SourceModel.changes(test.startingCheckpoint, {}, function(err, changes) { assert.equal(changes.length, 1); + done(); }); }, 1); @@ -197,8 +210,9 @@ describe('Replication / Change APIs', function() { if (err) return done(err); SourceModel.changes(FUTURE_CHECKPOINT, {}, function(err, changes) { if (err) return done(err); - /*jshint -W030 */ - expect(changes).to.be.empty; + + expect(changes).to.be.empty; //jshint ignore:line + done(); }); }); @@ -217,6 +231,7 @@ describe('Replication / Change APIs', function() { function(cb) { sourceModel.find(function(err, result) { if (err) return cb(err); + sourceData = result; cb(); }); @@ -224,6 +239,7 @@ describe('Replication / Change APIs', function() { function(cb) { targetModel.find(function(err, result) { if (err) return cb(err); + targetData = result; cb(); }); @@ -232,6 +248,7 @@ describe('Replication / Change APIs', function() { if (err) return done(err); assert.deepEqual(sourceData, targetData); + done(); }); } @@ -242,6 +259,7 @@ describe('Replication / Change APIs', function() { this.SourceModel.create({name: 'foo'}, function(err) { if (err) return done(err); + test.SourceModel.replicate(test.startingCheckpoint, test.TargetModel, options, function(err, conflicts) { if (err) return done(err); @@ -258,6 +276,7 @@ describe('Replication / Change APIs', function() { this.SourceModel.create({name: 'foo'}, function(err) { if (err) return done(err); + test.SourceModel.replicate(test.startingCheckpoint, test.TargetModel, options) .then(function(conflicts) { @@ -292,6 +311,7 @@ describe('Replication / Change APIs', function() { if (err) return done(err); // '1' should be skipped by replication expect(getIds(list)).to.eql(['2']); + next(); }); } @@ -324,6 +344,7 @@ describe('Replication / Change APIs', function() { if (err) return done(err); // '1' should be skipped by replication expect(getIds(list)).to.eql(['2']); + next(); }); } @@ -339,7 +360,9 @@ describe('Replication / Change APIs', function() { SourceModel.replicate(10, TargetModel, function(err) { if (err) return done(err); + expect(diffSince).to.eql([10]); + done(); }); }); @@ -354,6 +377,7 @@ describe('Replication / Change APIs', function() { SourceModel.replicate(10, TargetModel, {}) .then(function() { expect(diffSince).to.eql([10]); + done(); }) .catch(function(err) { @@ -371,8 +395,10 @@ describe('Replication / Change APIs', function() { var since = { source: 1, target: 2 }; SourceModel.replicate(since, TargetModel, function(err) { if (err) return done(err); + expect(sourceSince).to.eql([1]); expect(targetSince).to.eql([2]); + done(); }); }); @@ -389,6 +415,7 @@ describe('Replication / Change APIs', function() { .then(function() { expect(sourceSince).to.eql([1]); expect(targetSince).to.eql([2]); + done(); }) .catch(function(err) { @@ -411,7 +438,9 @@ describe('Replication / Change APIs', function() { function getLastCp(next) { SourceModel.currentCheckpoint(function(err, cp) { if (err) return done(err); + lastCp = cp; + next(); }); }, @@ -426,6 +455,7 @@ describe('Replication / Change APIs', function() { TargetModel.find(function(err, list) { expect(getIds(list), 'target ids after first sync') .to.include.members(['init']); + next(); }); }); @@ -436,6 +466,7 @@ describe('Replication / Change APIs', function() { function verify(next) { TargetModel.find(function(err, list) { expect(getIds(list), 'target ids').to.eql(['init', 'racer']); + next(); }); } @@ -455,11 +486,13 @@ describe('Replication / Change APIs', function() { TargetModel, function(err, conflicts, newCheckpoints) { if (err) return cb(err); + expect(conflicts, 'conflicts').to.eql([]); expect(newCheckpoints, 'currentCheckpoints').to.eql({ source: sourceCp + 1, target: targetCp + 1 }); + cb(); }); } @@ -468,7 +501,9 @@ describe('Replication / Change APIs', function() { function bumpSourceCheckpoint(cb) { SourceModel.checkpoint(function(err, inst) { if (err) return cb(err); + sourceCp = inst.seq; + cb(); }); } @@ -476,7 +511,9 @@ describe('Replication / Change APIs', function() { function bumpTargetCheckpoint(cb) { TargetModel.checkpoint(function(err, inst) { if (err) return cb(err); + targetCp = inst.seq; + cb(); }); } @@ -491,11 +528,14 @@ describe('Replication / Change APIs', function() { function verify(next) { TargetModel.currentCheckpoint(function(err, cp) { if (err) return next(err); + TargetModel.getChangeModel().find( { where: { checkpoint: { gte: cp } } }, function(err, changes) { if (err) return done(err); + expect(changes).to.have.length(0); + done(); }); }); @@ -659,6 +699,7 @@ describe('Replication / Change APIs', function() { cb); } }); + next(); }, replicateExpectingSuccess(), @@ -694,10 +735,13 @@ describe('Replication / Change APIs', function() { } ], function(err) { if (err) return done(err); + SourceModel.replicate(TargetModel, function(err, conflicts) { if (err) return done(err); + test.conflicts = conflicts; test.conflict = conflicts[0]; + done(); }); }); @@ -710,6 +754,7 @@ describe('Replication / Change APIs', function() { it('type should be UPDATE', function(done) { this.conflict.type(function(err, type) { assert.equal(type, Change.UPDATE); + done(); }); }); @@ -721,6 +766,7 @@ describe('Replication / Change APIs', function() { assert.equal(test.model.getId(), sourceChange.getModelId()); assert.equal(sourceChange.type(), Change.UPDATE); assert.equal(targetChange.type(), Change.UPDATE); + done(); }); }); @@ -735,6 +781,7 @@ describe('Replication / Change APIs', function() { id: test.model.id, name: 'target update' }); + done(); }); }); @@ -753,6 +800,7 @@ describe('Replication / Change APIs', function() { function(cb) { SourceModel.findOne(function(err, inst) { if (err) return cb(err); + test.model = inst; inst.remove(cb); }); @@ -760,16 +808,20 @@ describe('Replication / Change APIs', function() { function(cb) { TargetModel.findOne(function(err, inst) { if (err) return cb(err); + inst.name = 'target update'; inst.save(cb); }); } ], function(err) { if (err) return done(err); + SourceModel.replicate(TargetModel, function(err, conflicts) { if (err) return done(err); + test.conflicts = conflicts; test.conflict = conflicts[0]; + done(); }); }); @@ -782,6 +834,7 @@ describe('Replication / Change APIs', function() { it('type should be DELETE', function(done) { this.conflict.type(function(err, type) { assert.equal(type, Change.DELETE); + done(); }); }); @@ -793,6 +846,7 @@ describe('Replication / Change APIs', function() { assert.equal(test.model.getId(), sourceChange.getModelId()); assert.equal(sourceChange.type(), Change.DELETE); assert.equal(targetChange.type(), Change.UPDATE); + done(); }); }); @@ -804,6 +858,7 @@ describe('Replication / Change APIs', function() { id: test.model.id, name: 'target update' }); + done(); }); }); @@ -830,15 +885,19 @@ describe('Replication / Change APIs', function() { function(cb) { TargetModel.findOne(function(err, inst) { if (err) return cb(err); + inst.remove(cb); }); } ], function(err) { if (err) return done(err); + SourceModel.replicate(TargetModel, function(err, conflicts) { if (err) return done(err); + test.conflicts = conflicts; test.conflict = conflicts[0]; + done(); }); }); @@ -851,6 +910,7 @@ describe('Replication / Change APIs', function() { it('type should be DELETE', function(done) { this.conflict.type(function(err, type) { assert.equal(type, Change.DELETE); + done(); }); }); @@ -862,6 +922,7 @@ describe('Replication / Change APIs', function() { assert.equal(test.model.getId(), sourceChange.getModelId()); assert.equal(sourceChange.type(), Change.UPDATE); assert.equal(targetChange.type(), Change.DELETE); + done(); }); }); @@ -873,6 +934,7 @@ describe('Replication / Change APIs', function() { id: test.model.id, name: 'source update' }); + done(); }); }); @@ -891,6 +953,7 @@ describe('Replication / Change APIs', function() { function(cb) { SourceModel.findOne(function(err, inst) { if (err) return cb(err); + test.model = inst; inst.remove(cb); }); @@ -898,15 +961,19 @@ describe('Replication / Change APIs', function() { function(cb) { TargetModel.findOne(function(err, inst) { if (err) return cb(err); + inst.remove(cb); }); } ], function(err) { if (err) return done(err); + SourceModel.replicate(TargetModel, function(err, conflicts) { if (err) return done(err); + test.conflicts = conflicts; test.conflict = conflicts[0]; + done(); }); }); @@ -922,6 +989,7 @@ describe('Replication / Change APIs', function() { it('detects "create"', function(done) { SourceModel.create({}, function(err, inst) { if (err) return done(err); + assertChangeRecordedForId(inst.id, done); }); }); @@ -929,10 +997,12 @@ describe('Replication / Change APIs', function() { it('detects "updateOrCreate"', function(done) { givenReplicatedInstance(function(err, created) { if (err) return done(err); + var data = created.toObject(); created.name = 'updated'; SourceModel.updateOrCreate(created, function(err, inst) { if (err) return done(err); + assertChangeRecordedForId(inst.id, done); }); }); @@ -946,6 +1016,7 @@ describe('Replication / Change APIs', function() { this.all(model, query, function(err, list) { if (err || (list && list[0])) return callback(err, list && list[0], false); + this.create(model, data, function(err) { callback(err, data, true); }); @@ -955,6 +1026,7 @@ describe('Replication / Change APIs', function() { this.all(model, query, {}, function(err, list) { if (err || (list && list[0])) return callback(err, list && list[0], false); + this.create(model, data, {}, function(err) { callback(err, data, true); }); @@ -967,6 +1039,7 @@ describe('Replication / Change APIs', function() { { name: 'created' }, function(err, inst) { if (err) return done(err); + assertChangeRecordedForId(inst.id, done); }); }); @@ -974,6 +1047,7 @@ describe('Replication / Change APIs', function() { it('detects "deleteById"', function(done) { givenReplicatedInstance(function(err, inst) { if (err) return done(err); + SourceModel.deleteById(inst.id, function(err) { assertChangeRecordedForId(inst.id, done); }); @@ -983,8 +1057,10 @@ describe('Replication / Change APIs', function() { it('detects "deleteAll"', function(done) { givenReplicatedInstance(function(err, inst) { if (err) return done(err); + SourceModel.deleteAll({ name: inst.name }, function(err) { if (err) return done(err); + assertChangeRecordedForId(inst.id, done); }); }); @@ -993,11 +1069,13 @@ describe('Replication / Change APIs', function() { it('detects "updateAll"', function(done) { givenReplicatedInstance(function(err, inst) { if (err) return done(err); + SourceModel.updateAll( { name: inst.name }, { name: 'updated' }, function(err) { if (err) return done(err); + assertChangeRecordedForId(inst.id, done); }); }); @@ -1006,9 +1084,11 @@ describe('Replication / Change APIs', function() { it('detects "prototype.save"', function(done) { givenReplicatedInstance(function(err, inst) { if (err) return done(err); + inst.name = 'updated'; inst.save(function(err) { if (err) return done(err); + assertChangeRecordedForId(inst.id, done); }); }); @@ -1017,8 +1097,10 @@ describe('Replication / Change APIs', function() { it('detects "prototype.updateAttributes"', function(done) { givenReplicatedInstance(function(err, inst) { if (err) return done(err); + inst.updateAttributes({ name: 'updated' }, function(err) { if (err) return done(err); + assertChangeRecordedForId(inst.id, done); }); }); @@ -1027,6 +1109,7 @@ describe('Replication / Change APIs', function() { it('detects "prototype.delete"', function(done) { givenReplicatedInstance(function(err, inst) { if (err) return done(err); + inst.delete(function(err) { assertChangeRecordedForId(inst.id, done); }); @@ -1036,8 +1119,10 @@ describe('Replication / Change APIs', function() { function givenReplicatedInstance(cb) { SourceModel.create({ name: 'a-name' }, function(err, inst) { if (err) return cb(err); + SourceModel.checkpoint(function(err) { if (err) return cb(err); + cb(null, inst); }); }); @@ -1047,8 +1132,10 @@ describe('Replication / Change APIs', function() { SourceModel.getChangeModel().getCheckpointModel() .current(function(err, cp) { if (err) return cb(err); + SourceModel.changes(cp - 1, {}, function(err, pendingChanges) { if (err) return cb(err); + expect(pendingChanges, 'list of changes').to.have.length(1); var change = pendingChanges[0].toObject(); expect(change).to.have.property('checkpoint', cp); // sanity check @@ -1056,6 +1143,7 @@ describe('Replication / Change APIs', function() { // NOTE(bajtos) Change.modelId is always String // regardless of the type of the changed model's id property expect(change).to.have.property('modelId', '' + id); + cb(); }); }); @@ -1071,6 +1159,7 @@ describe('Replication / Change APIs', function() { SourceModel.create({ id: 'test-instance' }, function(err, result) { sourceInstance = result; sourceInstanceId = result.id; + next(err); }); }, @@ -1109,7 +1198,9 @@ describe('Replication / Change APIs', function() { function verifyTargetModelWasDeleted(next) { TargetModel.find(function(err, list) { if (err) return next(err); + expect(getIds(list)).to.not.contain(sourceInstance.id); + next(); }); } @@ -1376,6 +1467,7 @@ describe('Replication / Change APIs', function() { return function updateInstanceB(next) { ClientB.findById(sourceInstanceId, function(err, instance) { if (err) return next(err); + instance.name = name; instance.save(next); }); @@ -1408,6 +1500,7 @@ describe('Replication / Change APIs', function() { debug('delete source instance', value); sourceInstance.remove(function(err) { sourceInstance = null; + next(err); }); }; @@ -1415,11 +1508,14 @@ describe('Replication / Change APIs', function() { function verifySourceWasReplicated(target) { if (!target) target = TargetModel; + return function verify(next) { target.findById(sourceInstanceId, function(err, targetInstance) { if (err) return next(err); + expect(targetInstance && targetInstance.toObject()) .to.eql(sourceInstance && sourceInstance.toObject()); + next(); }); }; @@ -1445,9 +1541,11 @@ describe('Replication / Change APIs', function() { source.replicate(since, target, function(err, conflicts, cps) { if (err) return next(err); + if (conflicts.length === 0) { _since[sinceIx] = cps; } + next(err, conflicts, cps); }); } @@ -1465,10 +1563,12 @@ describe('Replication / Change APIs', function() { return function doReplicate(next) { replicate(source, target, since, function(err, conflicts, cps) { if (err) return next(err); + if (conflicts.length) { return next(new Error('Unexpected conflicts\n' + conflicts.map(JSON.stringify).join('\n'))); } + next(); }); }; @@ -1482,6 +1582,7 @@ describe('Replication / Change APIs', function() { var self = this; fn(function(err) { if (err) return cb(err); + bulkUpdate.call(self, data, cb); }); @@ -1494,11 +1595,14 @@ describe('Replication / Change APIs', function() { return function verify(next) { source.findById(id, function(err, expected) { if (err) return next(err); + target.findById(id, function(err, actual) { if (err) return next(err); + expect(actual && actual.toObject()) .to.eql(expected && expected.toObject()); debug('replicated instance: %j', actual); + next(); }); }); diff --git a/test/rest.middleware.test.js b/test/rest.middleware.test.js index 614209049..2e372f0ea 100644 --- a/test/rest.middleware.test.js +++ b/test/rest.middleware.test.js @@ -34,6 +34,7 @@ describe('loopback.rest', function() { .del('/mymodels/' + inst.id) .expect(200, function(err, res) { expect(res.body.count).to.equal(1); + done(); }); }); @@ -45,12 +46,12 @@ describe('loopback.rest', function() { request(app).get('/mymodels/1') .expect(404) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + var errorResponse = res.body.error; assert(errorResponse); assert.equal(errorResponse.code, 'MODEL_NOT_FOUND'); + done(); }); }); @@ -70,7 +71,9 @@ describe('loopback.rest', function() { .expect(200) .end(function(err, res) { if (err) return done(err); - expect(res.body).to.eql({exists: false}); + + expect(res.body).to.eql({ exists: false }); + done(); }); }); @@ -103,7 +106,9 @@ describe('loopback.rest', function() { .expect(200) .end(function(err, res) { if (err) return done(err); - expect(res.body).to.eql({exists: true}); + + expect(res.body).to.eql({ exists: true }); + done(); }); }); @@ -177,12 +182,15 @@ describe('loopback.rest', function() { app.use(loopback.rest()); givenLoggedInUser(function(err, token) { if (err) return done(err); + request(app).get('/users/getToken') .set('Authorization', token.id) .expect(200) .end(function(err, res) { if (err) return done(err); + expect(res.body.id).to.equal(null); + done(); }); }, done); @@ -194,7 +202,9 @@ describe('loopback.rest', function() { .expect(200) .end(function(err, res) { if (err) return done(err); + expect(res.body).to.eql([]); + done(); }); }); @@ -205,7 +215,9 @@ describe('loopback.rest', function() { .expect(200) .end(function(err, res) { if (err) return done(err); + expect(res.body).to.eql({}); + done(); }); }); @@ -262,12 +274,15 @@ describe('loopback.rest', function() { function invokeGetToken(done) { givenLoggedInUser(function(err, token) { if (err) return done(err); + request(app).get('/users/getToken') .set('Authorization', token.id) .expect(200) .end(function(err, res) { if (err) return done(err); + expect(res.body.id).to.equal(token.id); + done(); }); }); @@ -296,6 +311,7 @@ describe('loopback.rest', function() { { model: app.registry.getModelByType('AccessToken') })); app.use(function(req, res, next) { loopback.getCurrentContext().set('accessToken', req.accessToken); + next(); }); app.use(loopback.rest()); @@ -351,6 +367,7 @@ describe('loopback.rest', function() { User.create(credentials, function(err, user) { if (err) return done(err); + User.login(credentials, cb); }); } @@ -399,7 +416,9 @@ describe('loopback.rest', function() { .expect(200) .end(function(err, res) { if (err) return done(err); + expect(res.body.count).to.equal(3); + done(); }); }); @@ -445,7 +464,9 @@ describe('loopback.rest', function() { .expect(200) .end(function(err, res) { if (err) return done(err); + expect(res.body.count).to.equal(3); + done(); }); }); diff --git a/test/role.test.js b/test/role.test.js index e2de4f928..0c98fbcea 100644 --- a/test/role.test.js +++ b/test/role.test.js @@ -464,7 +464,9 @@ describe('role model', function() { Role.isOwner(User, user.id, user.id, function(err, result) { if (err) return done(err); + expect(result, 'isOwner result').to.equal(true); + done(); }); }); diff --git a/test/user.integration.js b/test/user.integration.js index b3a82cd56..753df249b 100644 --- a/test/user.integration.js +++ b/test/user.integration.js @@ -30,10 +30,13 @@ describe('users - integration', function() { app.models.AccessToken.belongsTo(app.models.User); app.models.User.destroyAll(function(err) { if (err) return done(err); + app.models.post.destroyAll(function(err) { if (err) return done(err); + app.models.blog.destroyAll(function(err) { if (err) return done(err); + done(); }); }); @@ -49,8 +52,10 @@ describe('users - integration', function() { .send({username: 'x', email: 'x@y.com', password: 'x'}) .expect(200, function(err, res) { if (err) return done(err); + expect(res.body.id).to.exist; userId = res.body.id; + done(); }); }); @@ -61,11 +66,11 @@ describe('users - integration', function() { this.post(url) .send({username: 'x', email: 'x@y.com', password: 'x'}) .expect(200, function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + expect(res.body.id).to.exist; accessToken = res.body.id; + done(); }); }); @@ -75,12 +80,12 @@ describe('users - integration', function() { this.post(url) .send({title: 'T1', content: 'C1'}) .expect(200, function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + expect(res.body.title).to.be.eql('T1'); expect(res.body.content).to.be.eql('C1'); expect(res.body.userId).to.be.eql(userId); + done(); }); }); @@ -91,13 +96,13 @@ describe('users - integration', function() { var url = '/api/posts?filter={"include":{"user":"accessTokens"}}'; this.get(url) .expect(200, function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + expect(res.body).to.have.property('length', 1); var post = res.body[0]; expect(post.user).to.have.property('username', 'x'); expect(post.user).to.not.have.property('accessTokens'); + done(); }); }); @@ -113,11 +118,11 @@ describe('users - integration', function() { this.post(url) .send({username: 'x', email: 'x@y.com', password: 'x'}) .expect(200, function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + expect(res.body.id).to.exist; userId = res.body.id; + done(); }); }); @@ -128,11 +133,11 @@ describe('users - integration', function() { this.post(url) .send({username: 'x', email: 'x@y.com', password: 'x'}) .expect(200, function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + expect(res.body.id).to.exist; accessToken = res.body.id; + done(); }); }); @@ -146,9 +151,11 @@ describe('users - integration', function() { console.error(err); return done(err); } + expect(res.body.title).to.be.eql('T1'); expect(res.body.content).to.be.eql('C1'); expect(res.body.userId).to.be.eql(userId); + done(); }); }); @@ -157,13 +164,13 @@ describe('users - integration', function() { var url = '/api/blogs?filter={"include":{"user":"accessTokens"}}'; this.get(url) .expect(200, function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + expect(res.body).to.have.property('length', 1); var blog = res.body[0]; expect(blog.user).to.have.property('username', 'x'); expect(blog.user).to.not.have.property('accessTokens'); + done(); }); }); diff --git a/test/user.test.js b/test/user.test.js index e4839c6e2..28e9650cc 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -67,6 +67,7 @@ describe('User', function() { User.create(validCredentials, function(err, user) { if (err) return done(err); + User.create(validCredentialsEmailVerified, done); }); }); @@ -77,6 +78,7 @@ describe('User', function() { assert(!err); assert(user.id); assert(user.email); + done(); }); }); @@ -85,8 +87,10 @@ describe('User', function() { User.settings.caseSensitiveEmail = false; User.create({email: 'F@b.com', password: 'bar'}, function(err, user) { if (err) return done(err); + assert(user.id); assert.equal(user.email, user.email.toLowerCase()); + done(); }); }); @@ -94,9 +98,11 @@ describe('User', function() { it('Create a new user (email case-sensitive)', function(done) { User.create({email: 'F@b.com', password: 'bar'}, function(err, user) { if (err) return done(err); + assert(user.id); assert(user.email); assert.notEqual(user.email, user.email.toLowerCase()); + done(); }); }); @@ -110,8 +116,9 @@ describe('User', function() { User.findById(user.id, function(err, user) { assert(user.id); assert(user.email); - assert.deepEqual(user.credentials, {cert: 'xxxxx', key: '111'}); - assert.deepEqual(user.challenges, {x: 'X', a: 1}); + assert.deepEqual(user.credentials, { cert: 'xxxxx', key: '111' }); + assert.deepEqual(user.challenges, { x: 'X', a: 1 }); + done(); }); }); @@ -138,6 +145,7 @@ describe('User', function() { User.create({email: 'c@d.com'}, function(err) { assert(err); + done(); }); }); @@ -145,6 +153,7 @@ describe('User', function() { it('Requires a valid email', function(done) { User.create({email: 'foo@', password: '123'}, function(err) { assert(err); + done(); }); }); @@ -153,6 +162,7 @@ describe('User', function() { User.create({email: 'a@b.com', password: 'foobar'}, function() { User.create({email: 'a@b.com', password: 'batbaz'}, function(err) { assert(err, 'should error because the email is not unique!'); + done(); }); }); @@ -162,8 +172,10 @@ describe('User', function() { User.settings.caseSensitiveEmail = false; User.create({email: 'A@b.com', password: 'foobar'}, function(err) { if (err) return done(err); - User.create({email: 'a@b.com', password: 'batbaz'}, function(err) { + + User.create({ email: 'a@b.com', password: 'batbaz' }, function(err) { assert(err, 'should error because the email is not unique!'); + done(); }); }); @@ -173,7 +185,9 @@ describe('User', function() { User.create({email: 'A@b.com', password: 'foobar'}, function(err, user1) { User.create({email: 'a@b.com', password: 'batbaz'}, function(err, user2) { if (err) return done(err); + assert.notEqual(user1.email, user2.email); + done(); }); }); @@ -183,6 +197,7 @@ describe('User', function() { User.create({email: 'a@b.com', username: 'abc', password: 'foobar'}, function() { User.create({email: 'b@b.com', username: 'abc', password: 'batbaz'}, function(err) { assert(err, 'should error because the username is not unique!'); + done(); }); }); @@ -194,6 +209,7 @@ describe('User', function() { assert(!accessToken, 'should not create a accessToken without a valid password'); assert(err, 'should not login without a password'); assert.equal(err.code, 'LOGIN_FAILED'); + done(); }); }); @@ -258,10 +274,10 @@ describe('User', function() { .expect(200) .send(validCredentialsEmailVerifiedOverREST) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + assert(!res.body.emailVerified); + done(); }); }); @@ -271,6 +287,7 @@ describe('User', function() { it('Should not throw an error if the query does not contain {where: }', function(done) { User.find({}, function(err) { if (err) done(err); + done(); }); }); @@ -279,8 +296,10 @@ describe('User', function() { User.settings.caseSensitiveEmail = false; User.find({where:{email: validMixedCaseEmailCredentials.email}}, function(err, result) { if (err) done(err); + assert(result[0], 'The query did not find the user'); assert.equal(result[0].email, validCredentialsEmail); + done(); }); }); @@ -303,6 +322,7 @@ describe('User', function() { assert(accessToken.userId); assert(accessToken.id); assert.equal(accessToken.id.length, 64); + done(); }); }); @@ -310,6 +330,7 @@ describe('User', function() { it('Try to login with invalid email case', function(done) { User.login(validMixedCaseEmailCredentials, function(err, accessToken) { assert(err); + done(); }); }); @@ -336,6 +357,7 @@ describe('User', function() { assert(accessToken.id); assert.equal(accessToken.ttl, 120); assert.equal(accessToken.id.length, 64); + done(); }); }); @@ -354,6 +376,7 @@ describe('User', function() { assert(accessToken.id); assert.equal(accessToken.ttl, 120); assert.equal(accessToken.id.length, 64); + done(); }) .catch(function(err) { @@ -384,6 +407,7 @@ describe('User', function() { assert.equal(accessToken.id.length, 64); // Restore create access token User.prototype.createAccessToken = createToken; + done(); }); }); @@ -414,6 +438,7 @@ describe('User', function() { assert.equal(accessToken.scopes, 'default'); // Restore create access token User.prototype.createAccessToken = createToken; + done(); }); }); @@ -425,6 +450,7 @@ describe('User', function() { assert(err); assert.equal(err.code, 'LOGIN_FAILED'); assert(!accessToken); + done(); }); }); @@ -433,11 +459,13 @@ describe('User', function() { User.login(invalidCredentials) .then(function(accessToken) { assert(!accessToken); + done(); }) .catch(function(err) { assert(err); assert.equal(err.code, 'LOGIN_FAILED'); + done(); }); }); @@ -446,6 +474,7 @@ describe('User', function() { User.login(incompleteCredentials, function(err, accessToken) { assert(err); assert.equal(err.code, 'USERNAME_EMAIL_REQUIRED'); + done(); }); }); @@ -454,11 +483,13 @@ describe('User', function() { User.login(incompleteCredentials) .then(function(accessToken) { assert(!accessToken); + done(); }) .catch(function(err) { assert(err); assert.equal(err.code, 'USERNAME_EMAIL_REQUIRED'); + done(); }); }); @@ -470,9 +501,8 @@ describe('User', function() { .expect(200) .send(validCredentials) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + var accessToken = res.body; assert(accessToken.userId); @@ -491,11 +521,11 @@ describe('User', function() { .expect(401) .send(invalidCredentials) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + var errorResponse = res.body.error; assert.equal(errorResponse.code, 'LOGIN_FAILED'); + done(); }); }); @@ -507,11 +537,11 @@ describe('User', function() { .expect(400) .send(incompleteCredentials) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + var errorResponse = res.body.error; assert.equal(errorResponse.code, 'USERNAME_EMAIL_REQUIRED'); + done(); }); }); @@ -524,11 +554,11 @@ describe('User', function() { .expect(400) .send(JSON.stringify(validCredentials)) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + var errorResponse = res.body.error; assert.equal(errorResponse.code, 'USERNAME_EMAIL_REQUIRED'); + done(); }); }); @@ -540,13 +570,13 @@ describe('User', function() { .expect(200) .expect('Content-Type', /json/) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + var token = res.body; expect(token.user, 'body.user').to.not.equal(undefined); expect(token.user, 'body.user') .to.have.property('email', validCredentials.email); + done(); }); }); @@ -558,13 +588,13 @@ describe('User', function() { .expect(200) .expect('Content-Type', /json/) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + var token = res.body; expect(token.user, 'body.user').to.not.equal(undefined); expect(token.user, 'body.user') .to.have.property('email', validCredentials.email); + done(); }); }); @@ -591,6 +621,7 @@ describe('User', function() { // error message should be "login failed" and not "login failed as the email has not been verified" assert(err && !/verified/.test(err.message), ('expecting "login failed" error message, received: "' + err.message + '"')); assert.equal(err.code, 'LOGIN_FAILED'); + done(); }); }); @@ -605,6 +636,7 @@ describe('User', function() { // error message should be "login failed" and not "login failed as the email has not been verified" assert(err && !/verified/.test(err.message), ('expecting "login failed" error message, received: "' + err.message + '"')); assert.equal(err.code, 'LOGIN_FAILED'); + done(); }); }); @@ -613,6 +645,7 @@ describe('User', function() { User.login(validCredentials, function(err, accessToken) { assert(err); assert.equal(err.code, 'LOGIN_FAILED_EMAIL_NOT_VERIFIED'); + done(); }); }); @@ -625,6 +658,7 @@ describe('User', function() { .catch(function(err) { assert(err); assert.equal(err.code, 'LOGIN_FAILED_EMAIL_NOT_VERIFIED'); + done(); }); }); @@ -632,6 +666,7 @@ describe('User', function() { it('Login a user by with email verification', function(done) { User.login(validCredentialsEmailVerified, function(err, accessToken) { assertGoodToken(accessToken); + done(); }); }); @@ -640,6 +675,7 @@ describe('User', function() { User.login(validCredentialsEmailVerified) .then(function(accessToken) { assertGoodToken(accessToken); + done(); }) .catch(function(err) { @@ -654,9 +690,8 @@ describe('User', function() { .expect(200) .send(validCredentialsEmailVerified) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + var accessToken = res.body; assertGoodToken(accessToken); @@ -673,14 +708,14 @@ describe('User', function() { .expect(401) .send({ email: validCredentialsEmail }) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + // strongloop/loopback#931 // error message should be "login failed" and not "login failed as the email has not been verified" var errorResponse = res.body.error; assert(errorResponse && !/verified/.test(errorResponse.message), ('expecting "login failed" error message, received: "' + errorResponse.message + '"')); assert.equal(errorResponse.code, 'LOGIN_FAILED'); + done(); }); }); @@ -692,11 +727,11 @@ describe('User', function() { .expect(401) .send(validCredentials) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + var errorResponse = res.body.error; assert.equal(errorResponse.code, 'LOGIN_FAILED_EMAIL_NOT_VERIFIED'); + done(); }); }); @@ -782,9 +817,8 @@ describe('User', function() { var user1; beforeEach(function(done) { User.create(realm1User, function(err, u) { - if (err) { - return done(err); - } + if (err) return done(err); + user1 = u; User.create(realm2User, done); }); @@ -794,6 +828,7 @@ describe('User', function() { User.login(credentialWithoutRealm, function(err, accessToken) { assert(err); assert.equal(err.code, 'REALM_REQUIRED'); + done(); }); }); @@ -802,6 +837,7 @@ describe('User', function() { User.login(credentialWithBadRealm, function(err, accessToken) { assert(err); assert.equal(err.code, 'LOGIN_FAILED'); + done(); }); }); @@ -810,6 +846,7 @@ describe('User', function() { User.login(credentialWithBadPass, function(err, accessToken) { assert(err); assert.equal(err.code, 'LOGIN_FAILED'); + done(); }); }); @@ -818,6 +855,7 @@ describe('User', function() { User.login(credentialWithRealm, function(err, accessToken) { assertGoodToken(accessToken); assert.equal(accessToken.userId, user1.id); + done(); }); }); @@ -826,6 +864,7 @@ describe('User', function() { User.login(credentialRealmInUsername, function(err, accessToken) { assertGoodToken(accessToken); assert.equal(accessToken.userId, user1.id); + done(); }); }); @@ -834,6 +873,7 @@ describe('User', function() { User.login(credentialRealmInEmail, function(err, accessToken) { assertGoodToken(accessToken); assert.equal(accessToken.userId, user1.id); + done(); }); }); @@ -851,6 +891,7 @@ describe('User', function() { User.login(credentialWithRealm, function(err, accessToken) { assertGoodToken(accessToken); assert.equal(accessToken.userId, user1.id); + done(); }); }); @@ -860,6 +901,7 @@ describe('User', function() { User.login(credentialRealmInEmail, function(err, accessToken) { assert(err); assert.equal(err.code, 'REALM_REQUIRED'); + done(); }); }); @@ -904,9 +946,8 @@ describe('User', function() { .expect(200) .send({email: 'foo@bar.com', password: 'bar'}) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + var accessToken = res.body; assert(accessToken.userId); @@ -929,12 +970,11 @@ describe('User', function() { assert(token); return function(err) { - if (err) { - return done(err); - } + if (err) return done(err); AccessToken.findById(token, function(err, accessToken) { assert(!accessToken, 'accessToken should not exist after logging out'); + done(err); }); }; @@ -946,6 +986,7 @@ describe('User', function() { var u = new User({username: 'foo', password: 'bar'}); u.hasPassword('bar', function(err, isMatch) { assert(isMatch, 'password doesnt match'); + done(); }); }); @@ -955,6 +996,7 @@ describe('User', function() { u.hasPassword('bar') .then(function(isMatch) { assert(isMatch, 'password doesnt match'); + done(); }) .catch(function(err) { @@ -969,6 +1011,7 @@ describe('User', function() { User.findById(user.id, function(err, uu) { uu.hasPassword('b', function(err, isMatch) { assert(isMatch); + done(); }); }); @@ -988,6 +1031,7 @@ describe('User', function() { User.findById(user.id, function(err, uu) { uu.hasPassword('baz2', function(err, isMatch) { assert(isMatch); + done(); }); }); @@ -1022,6 +1066,7 @@ describe('User', function() { var msg = result.email.response.toString('utf-8'); assert(~msg.indexOf('/api/test-users/confirm')); assert(~msg.indexOf('To: bar@bat.com')); + done(); }); }); @@ -1032,9 +1077,7 @@ describe('User', function() { .expect(200) .send({email: 'bar@bat.com', password: 'bar'}) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); }); }); @@ -1059,6 +1102,7 @@ describe('User', function() { var msg = result.email.response.toString('utf-8'); assert(~msg.indexOf('/api/test-users/confirm')); assert(~msg.indexOf('To: bar@bat.com')); + done(); }) .catch(function(err) { @@ -1072,9 +1116,7 @@ describe('User', function() { .expect('Content-Type', /json/) .expect(200) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); }); }); @@ -1095,6 +1137,7 @@ describe('User', function() { user.verify(options, function(err, result) { assert(result.email); assert.equal(result.email.messageId, 'custom-header-value'); + done(); }); }); @@ -1105,9 +1148,7 @@ describe('User', function() { .expect(200) .send({email: 'bar@bat.com', password: 'bar'}) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); }); }); @@ -1141,6 +1182,7 @@ describe('User', function() { assert.equal(result.token, 'token-123456'); var msg = result.email.response.toString('utf-8'); assert(~msg.indexOf('token-123456')); + done(); }); }); @@ -1151,9 +1193,7 @@ describe('User', function() { .expect(200) .send({email: 'bar@bat.com', password: 'bar'}) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); }); }); @@ -1180,6 +1220,7 @@ describe('User', function() { assert(err); assert.equal(err.message, 'Fake error'); assert.equal(result, undefined); + done(); }); }); @@ -1190,9 +1231,7 @@ describe('User', function() { .expect(200) .send({email: 'bar@bat.com', password: 'bar'}) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); }); }); @@ -1214,6 +1253,7 @@ describe('User', function() { user.verify(options, function(err, result) { var msg = result.email.response.toString('utf-8'); assert(~msg.indexOf('http://myapp.org:3000/')); + done(); }); }); @@ -1224,9 +1264,7 @@ describe('User', function() { .expect(200) .send({email: 'bar@bat.com', password: 'bar'}) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); }); }); @@ -1247,6 +1285,7 @@ describe('User', function() { user.verify(options, function(err, result) { var msg = result.email.response.toString('utf-8'); assert(~msg.indexOf('http://myapp.org/')); + done(); }); }); @@ -1257,9 +1296,7 @@ describe('User', function() { .expect(200) .send({email: 'bar@bat.com', password: 'bar'}) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); }); }); @@ -1280,6 +1317,7 @@ describe('User', function() { user.verify(options, function(err, result) { var msg = result.email.response.toString('utf-8'); assert(~msg.indexOf('https://myapp.org:3000/')); + done(); }); }); @@ -1290,9 +1328,7 @@ describe('User', function() { .expect(200) .send({email: 'bar@bat.com', password: 'bar'}) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); }); }); @@ -1313,6 +1349,7 @@ describe('User', function() { user.verify(options, function(err, result) { var msg = result.email.response.toString('utf-8'); assert(~msg.indexOf('https://myapp.org/')); + done(); }); }); @@ -1323,9 +1360,7 @@ describe('User', function() { .expect(200) .send({email: 'bar@bat.com', password: 'bar'}) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); }); }); }); @@ -1334,6 +1369,7 @@ describe('User', function() { var user = new User({email: 'bar@bat.com', password: 'bar', verificationToken: 'a-token' }); var data = user.toJSON(); assert(!('verificationToken' in data)); + done(); }); }); @@ -1355,9 +1391,8 @@ describe('User', function() { }; user.verify(options, function(err, result) { - if (err) { - return done(err); - } + if (err) return done(err); + testFunc(result, done); }); }); @@ -1368,9 +1403,7 @@ describe('User', function() { .expect(302) .send({email: 'bar@bat.com', password: 'bar'}) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); }); } @@ -1382,9 +1415,8 @@ describe('User', function() { '&redirect=' + encodeURIComponent(options.redirect)) .expect(302) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + done(); }); }, done); @@ -1420,12 +1452,12 @@ describe('User', function() { '&redirect=' + encodeURIComponent(options.redirect)) .expect(404) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + var errorResponse = res.body.error; assert(errorResponse); assert.equal(errorResponse.code, 'USER_NOT_FOUND'); + done(); }); }, done); @@ -1439,12 +1471,12 @@ describe('User', function() { '&redirect=' + encodeURIComponent(options.redirect)) .expect(400) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + var errorResponse = res.body.error; assert(errorResponse); assert.equal(errorResponse.code, 'INVALID_TOKEN'); + done(); }); }, done); @@ -1460,6 +1492,7 @@ describe('User', function() { User.resetPassword({ }, function(err) { assert(err); assert.equal(err.code, 'EMAIL_REQUIRED'); + done(); }); }); @@ -1472,6 +1505,7 @@ describe('User', function() { .catch(function(err) { assert(err); assert.equal(err.code, 'EMAIL_REQUIRED'); + done(); }); }); @@ -1481,6 +1515,7 @@ describe('User', function() { assert(err); assert.equal(err.code, 'EMAIL_NOT_FOUND'); assert.equal(err.statusCode, 404); + done(); }); }); @@ -1502,7 +1537,9 @@ describe('User', function() { assert(calledBack); info.accessToken.user(function(err, user) { if (err) return done(err); + assert.equal(user.email, email); + done(); }); }); @@ -1515,12 +1552,12 @@ describe('User', function() { .expect(400) .send({ }) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + var errorResponse = res.body.error; assert(errorResponse); assert.equal(errorResponse.code, 'EMAIL_REQUIRED'); + done(); }); }); @@ -1532,10 +1569,10 @@ describe('User', function() { .expect(204) .send({ email: email }) .end(function(err, res) { - if (err) { - return done(err); - } + if (err) return done(err); + assert.deepEqual(res.body, { }); + done(); }); }); diff --git a/test/util/model-tests.js b/test/util/model-tests.js index d5509ca38..d554c2d48 100644 --- a/test/util/model-tests.js +++ b/test/util/model-tests.js @@ -130,6 +130,7 @@ module.exports = function defineModelTestsWithDataSource(options) { user.isValid(function(valid) { assert(valid === false); assert(user.errors.age, 'model should have age error'); + done(); }); }); @@ -139,6 +140,7 @@ module.exports = function defineModelTestsWithDataSource(options) { it('Create an instance of Model with given data and save to the attached data source', function(done) { User.create({first: 'Joe', last: 'Bob'}, function(err, user) { assert(user instanceof User); + done(); }); }); @@ -151,6 +153,7 @@ module.exports = function defineModelTestsWithDataSource(options) { assert(user.id); assert(!err); assert(!user.errors); + done(); }); }); @@ -170,6 +173,7 @@ module.exports = function defineModelTestsWithDataSource(options) { assert.equal(updatedUser.first, 'updatedFirst'); assert.equal(updatedUser.last, 'updatedLast'); assert.equal(updatedUser.age, 100); + done(); }); }); @@ -185,6 +189,7 @@ module.exports = function defineModelTestsWithDataSource(options) { User.upsert({first: 'bob', id: 7}, function(err, updatedUser) { assert(!err); assert.equal(updatedUser.first, 'bob'); + done(); }); }); @@ -196,12 +201,16 @@ module.exports = function defineModelTestsWithDataSource(options) { User.create({first: 'joe', last: 'bob'}, function(err, user) { User.findById(user.id, function(err, foundUser) { if (err) return done(err); + assert.equal(user.id, foundUser.id); User.deleteById(foundUser.id, function(err) { if (err) return done(err); - User.find({ where: { id: user.id } }, function(err, found) { + + User.find({ where: { id: user.id }}, function(err, found) { if (err) return done(err); + assert.equal(found.length, 0); + done(); }); }); @@ -216,6 +225,7 @@ module.exports = function defineModelTestsWithDataSource(options) { User.deleteById(user.id, function(err) { User.findById(user.id, function(err, notFound) { assert.equal(notFound, null); + done(); }); }); @@ -230,6 +240,7 @@ module.exports = function defineModelTestsWithDataSource(options) { assert.equal(user.id, 23); assert.equal(user.first, 'michael'); assert.equal(user.last, 'jordan'); + done(); }); }); @@ -247,6 +258,7 @@ module.exports = function defineModelTestsWithDataSource(options) { .on('done', function() { User.count({age: {gt: 99}}, function(err, count) { assert.equal(count, 2); + done(); }); }); From 78688037114ac4e61fb1011791d4d0547c90a9c5 Mon Sep 17 00:00:00 2001 From: Rik Date: Sun, 8 May 2016 13:10:56 +0200 Subject: [PATCH 033/187] Update user.js allow to change all {href} instances in user.verify() mail into generated url instead of just one --- common/models/user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/models/user.js b/common/models/user.js index 7466ca8e7..4263e354e 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -420,7 +420,7 @@ module.exports = function(User) { options.text = options.text || 'Please verify your email by opening this link in a web browser:\n\t{href}'; - options.text = options.text.replace('{href}', options.verifyHref); + options.text = options.text.replace(/\{href\}/g, options.verifyHref); options.to = options.to || user.email; From 8fef4845f8f40deb5b2349a2a36ed3f1c50be555 Mon Sep 17 00:00:00 2001 From: juehou Date: Tue, 26 Apr 2016 00:35:54 -0400 Subject: [PATCH 034/187] Resolver support return promise --- common/models/role.js | 14 +++++++++++--- test/role.test.js | 20 ++++++++++++++++++-- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/common/models/role.js b/common/models/role.js index b8c1242d5..c4e7e8b3e 100644 --- a/common/models/role.js +++ b/common/models/role.js @@ -128,8 +128,9 @@ module.exports = function(Role) { /** * Add custom handler for roles. * @param {String} role Name of role. - * @param {Function} resolver Function that determines if a principal is in the specified role. - * Signature must be `function(role, context, callback)` + * @param {Function} resolver Function that determines + * if a principal is in the specified role. + * Should provide a callback or return a promise. */ Role.registerResolver = function(role, resolver) { if (!Role.resolvers) { @@ -295,7 +296,14 @@ module.exports = function(Role) { var resolver = Role.resolvers[role]; if (resolver) { debug('Custom resolver found for role %s', role); - resolver(role, context, callback); + + var promise = resolver(role, context, callback); + if (promise && typeof promise.then === 'function') { + promise.then( + function(result) { callback(null, result); }, + callback + ); + } return; } diff --git a/test/role.test.js b/test/role.test.js index 0c98fbcea..50a5e0321 100644 --- a/test/role.test.js +++ b/test/role.test.js @@ -13,6 +13,7 @@ var Application = loopback.Application; var ACL = loopback.ACL; var async = require('async'); var expect = require('chai').expect; +var Promise = require('bluebird'); function checkResult(err, result) { assert(!err); @@ -181,6 +182,13 @@ describe('role model', function() { }); it('should support owner role resolver', function() { + Role.registerResolver('returnPromise', function(role, context) { + return new Promise(function(resolve) { + process.nextTick(function() { + resolve(true); + }); + }); + }); var Album = ds.createModel('Album', { name: String, @@ -196,10 +204,18 @@ describe('role model', function() { }); User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function(err, user) { - Role.isInRole(Role.AUTHENTICATED, {principalType: ACL.USER, principalId: user.id}, function(err, yes) { + Role.isInRole('returnPromise', { principalType: ACL.USER, principalId: user.id }, + function(err, yes) { assert(!err && yes); }); - Role.isInRole(Role.AUTHENTICATED, {principalType: ACL.USER, principalId: null}, function(err, yes) { + + Role.isInRole(Role.AUTHENTICATED, { principalType: ACL.USER, principalId: user.id }, + function(err, yes) { + assert(!err && yes); + }); + + Role.isInRole(Role.AUTHENTICATED, { principalType: ACL.USER, principalId: null }, + function(err, yes) { assert(!err && !yes); }); From 6097fbc00543777705a0f071e9f2b7fe78f116bb Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 11 May 2016 22:46:27 -0700 Subject: [PATCH 035/187] Fix JSCS unsupported rule error Replace 'validateJSDoc' rule with 'jsDoc'. 'validateJSDoc' was deprecated in v1.7.0. In related news, JSCS was recently deprecated in favor of ESlint so .jscrc can be removed once features have been rolled over. --- .jscsrc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.jscsrc b/.jscsrc index 8deeea1cf..0362337e0 100644 --- a/.jscsrc +++ b/.jscsrc @@ -15,9 +15,7 @@ "allowComments": true, "allowRegex": true }, - "validateJSDoc": { - "checkParamNames": false, - "checkRedundantParams": false, + "jsDoc": { "requireParamTypes": true } } From 25fe4970e6a8ee18d8aceed48bfcbd9f8bdff17c Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Tue, 10 May 2016 15:23:38 -0700 Subject: [PATCH 036/187] Remove env.json and strong-pm dir [back-port of https://github.com/strongloop/loopback/pull/2327] --- .strong-pm/env.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .strong-pm/env.json diff --git a/.strong-pm/env.json b/.strong-pm/env.json deleted file mode 100644 index e69de29bb..000000000 From 75da4c778442636642d0354e9f9354d976984bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 31 May 2016 18:48:47 +0200 Subject: [PATCH 037/187] Deprecate getters for express 3.x middleware In LoopBack 3.0, we are removing these getters, see #2394. --- lib/express-middleware.js | 21 +++++++++++++-------- server/middleware/favicon.js | 8 +++++++- test/access-token.test.js | 3 ++- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/lib/express-middleware.js b/lib/express-middleware.js index cc7596ff7..98fae0408 100644 --- a/lib/express-middleware.js +++ b/lib/express-middleware.js @@ -4,6 +4,7 @@ // License text available at https://opensource.org/licenses/MIT var path = require('path'); +var deprecated = require('depd')('loopback'); var middlewares = exports; @@ -29,12 +30,10 @@ var middlewareModules = { 'cookieParser': 'cookie-parser', 'cookieSession': 'cookie-session', 'csrf': 'csurf', - 'errorHandler': 'errorhandler', 'session': 'express-session', 'methodOverride': 'method-override', 'logger': 'morgan', 'responseTime': 'response-time', - 'favicon': 'serve-favicon', 'directory': 'serve-index', // 'static': 'serve-static', 'vhost': 'vhost' @@ -44,14 +43,20 @@ middlewares.bodyParser = safeRequire('body-parser'); middlewares.json = middlewares.bodyParser && middlewares.bodyParser.json; middlewares.urlencoded = middlewares.bodyParser && middlewares.bodyParser.urlencoded; +['bodyParser', 'json', 'urlencoded'].forEach(function(name) { + if (!middlewares[name]) return; + middlewares[name] = deprecated.function( + middlewares[name], + deprecationMessage(name, 'body-parser')); +}); + for (var m in middlewareModules) { var moduleName = middlewareModules[m]; middlewares[m] = safeRequire(moduleName) || createMiddlewareNotInstalled(m, moduleName); + deprecated.property(middlewares, m, deprecationMessage(m, moduleName)); } -// serve-favicon requires a path -var favicon = middlewares.favicon; -middlewares.favicon = function(icon, options) { - icon = icon || path.join(__dirname, '../favicon.ico'); - return favicon(icon, options); -}; +function deprecationMessage(accessor, moduleName) { + return 'loopback.' + accessor + ' is deprecated. ' + + 'Use `require(\'' + moduleName + '\');` instead.'; +} diff --git a/server/middleware/favicon.js b/server/middleware/favicon.js index b5cf10cec..48ffac4ff 100644 --- a/server/middleware/favicon.js +++ b/server/middleware/favicon.js @@ -3,8 +3,14 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +var favicon = require('serve-favicon'); +var path = require('path'); + /** * Serve the LoopBack favicon. * @header loopback.favicon() */ -module.exports = require('../../lib/express-middleware').favicon; +module.exports = function(icon, options) { + icon = icon || path.join(__dirname, '../../favicon.ico'); + return favicon(icon, options); +}; diff --git a/test/access-token.test.js b/test/access-token.test.js index 573cae5a4..53b0996e9 100644 --- a/test/access-token.test.js +++ b/test/access-token.test.js @@ -3,6 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +var cookieParser = require('cookie-parser'); var loopback = require('../'); var extend = require('util')._extend; var Token = loopback.AccessToken.extend('MyToken'); @@ -500,7 +501,7 @@ function createTestApp(testToken, settings, done) { var app = loopback(); - app.use(loopback.cookieParser('secret')); + app.use(cookieParser('secret')); app.use(loopback.token(tokenSettings)); app.get('/token', function(req, res) { res.cookie('authorization', testToken.id, {signed: true}); From 05f8774ed698f76849647f2c2c40f518af358a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 31 May 2016 18:58:14 +0200 Subject: [PATCH 038/187] jscsrc: remove jsDoc rule The rule is no longer supported. --- .jscsrc | 3 --- 1 file changed, 3 deletions(-) diff --git a/.jscsrc b/.jscsrc index 0362337e0..ba97a76df 100644 --- a/.jscsrc +++ b/.jscsrc @@ -14,8 +14,5 @@ "value": 150, "allowComments": true, "allowRegex": true - }, - "jsDoc": { - "requireParamTypes": true } } From b013e66883fc264135b0623e24dd8c0b162e82a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 1 Jun 2016 09:26:45 +0200 Subject: [PATCH 039/187] test: increase timeouts on CI --- test/rest.middleware.test.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/rest.middleware.test.js b/test/rest.middleware.test.js index 2e372f0ea..f2a97d021 100644 --- a/test/rest.middleware.test.js +++ b/test/rest.middleware.test.js @@ -17,6 +17,9 @@ describe('loopback.rest', function() { MyModel.attachTo(db); }); + if (process.env.CI) + this.timeout(3000); + it('works out-of-the-box', function(done) { app.model(MyModel); app.use(loopback.rest()); From a1c98a858995ec9689203ceee09373980ee58699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 7 Jun 2016 17:07:07 +0200 Subject: [PATCH 040/187] 2.29.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: increase timeouts on CI (Miroslav Bajtoš) * jscsrc: remove jsDoc rule (Miroslav Bajtoš) * Deprecate getters for express 3.x middleware (Miroslav Bajtoš) * Remove env.json and strong-pm dir (Ritchie Martori) * Fix JSCS unsupported rule error (Jason) * Resolver support return promise (juehou) * Update user.js (Rik) * Backport separate error checking and done logic (Simon Ho) * Clean up by removing unnecessary comments (Supasate Choochaisri) * Add feature to not allow duplicate role name (Supasate Choochaisri) * update/insert copyright notices (Ryan Graham) * relicense as MIT only (Ryan Graham) * Upgrade phantomjs to 2.x (Miroslav Bajtoš) * app: send port:0 instead of port:undefined (Miroslav Bajtoš) * travis: drop node@5, add node@6 (Miroslav Bajtoš) * Disable DEBUG output for eslint on Jenkins CI (Miroslav Bajtoš) * test/rest.middleware: use local registry (Miroslav Bajtoš) * Fix role.isOwner to support app-local registry (Miroslav Bajtoš) * test/user: use local registry (Miroslav Bajtoš) --- CHANGES.md | 42 ++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index aa42725cf..c7f37cf10 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,45 @@ +2016-06-07, Version 2.29.0 +========================== + + * test: increase timeouts on CI (Miroslav Bajtoš) + + * jscsrc: remove jsDoc rule (Miroslav Bajtoš) + + * Deprecate getters for express 3.x middleware (Miroslav Bajtoš) + + * Remove env.json and strong-pm dir (Ritchie Martori) + + * Fix JSCS unsupported rule error (Jason) + + * Resolver support return promise (juehou) + + * Update user.js (Rik) + + * Backport separate error checking and done logic (Simon Ho) + + * Clean up by removing unnecessary comments (Supasate Choochaisri) + + * Add feature to not allow duplicate role name (Supasate Choochaisri) + + * update/insert copyright notices (Ryan Graham) + + * relicense as MIT only (Ryan Graham) + + * Upgrade phantomjs to 2.x (Miroslav Bajtoš) + + * app: send port:0 instead of port:undefined (Miroslav Bajtoš) + + * travis: drop node@5, add node@6 (Miroslav Bajtoš) + + * Disable DEBUG output for eslint on Jenkins CI (Miroslav Bajtoš) + + * test/rest.middleware: use local registry (Miroslav Bajtoš) + + * Fix role.isOwner to support app-local registry (Miroslav Bajtoš) + + * test/user: use local registry (Miroslav Bajtoš) + + 2016-05-02, Version 2.28.0 ========================== diff --git a/package.json b/package.json index 2eb05c8b3..9e33ec3fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.28.0", + "version": "2.29.0", "description": "LoopBack: Open Source Framework for Node.js", "homepage": "http://loopback.io/", "keywords": [ From 7e051a7549c9bfaf7cd49bfa5591f3a5635db81b Mon Sep 17 00:00:00 2001 From: Benjamin Kroeger Date: Wed, 13 Apr 2016 16:34:41 +0200 Subject: [PATCH 041/187] add missing unit tests for #2108 Subsequent token middleware tries to read `token.id` when `enableDoublecheck: true`. That caused a "Cannot read property `id` of `null`" error when the first middleware didn't actually find a valid accessToken. [back-port of #2227] --- test/access-token.test.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/access-token.test.js b/test/access-token.test.js index 53b0996e9..4b6ebc5d4 100644 --- a/test/access-token.test.js +++ b/test/access-token.test.js @@ -267,6 +267,40 @@ describe('loopback.token(options)', function() { }); }); + it('should overwrite invalid existing token (is !== undefined and has no "id" property) ' + + ' when enableDoubkecheck is true', + function(done) { + var token = this.token; + + app.use(function(req, res, next) { + req.accessToken = null; + next(); + }); + + app.use(loopback.token({ + model: Token, + enableDoublecheck: true, + })); + + app.get('/', function(req, res, next) { + res.send(req.accessToken); + }); + + request(app).get('/') + .set('Authorization', token.id) + .expect(200) + .end(function(err, res) { + if (err) return done(err); + expect(res.body).to.eql({ + id: token.id, + ttl: token.ttl, + userId: token.userId, + created: token.created.toJSON(), + }); + done(); + }); + }); + it('should overwrite existing token when enableDoublecheck ' + 'and overwriteExistingToken options are truthy', function(done) { From 4480cd92ab71aada01fc6e76d7310959755737ea Mon Sep 17 00:00:00 2001 From: Loay Date: Thu, 16 Jun 2016 02:20:33 -0400 Subject: [PATCH 042/187] Fix verificationToken bug #2440 --- common/models/user.js | 2 +- test/user.test.js | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/common/models/user.js b/common/models/user.js index 4263e354e..d6c958ab9 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -474,7 +474,7 @@ module.exports = function(User) { fn(err); } else { if (user && user.verificationToken === token) { - user.verificationToken = undefined; + user.verificationToken = null; user.emailVerified = true; user.save(function(err) { if (err) { diff --git a/test/user.test.js b/test/user.test.js index 28e9650cc..2fd77870c 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -1422,6 +1422,21 @@ describe('User', function() { }, done); }); + it('sets verificationToken to null after confirmation', function(done) { + testConfirm(function(result, done) { + User.confirm(result.uid, result.token, false, function(err) { + if (err) return done(err); + + // Verify by loading user data stored in the datasource + User.findById(result.uid, function(err, user) { + if (err) return done(err); + expect(user).to.have.property('verificationToken', null); + done(); + }); + }); + }, done); + }); + it('Should report 302 when redirect url is set', function(done) { testConfirm(function(result, done) { request(app) From 8fe77b2a062bf0de945f9b899f9e71af6d23546f Mon Sep 17 00:00:00 2001 From: Jue Hou Date: Mon, 11 Jan 2016 14:28:10 -0500 Subject: [PATCH 043/187] Fix description for User.prototype.hasPassword --- common/models/user.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/models/user.js b/common/models/user.js index d6c958ab9..09bdcb74c 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -306,7 +306,9 @@ module.exports = function(User) { * Compare the given `password` with the users hashed password. * * @param {String} password The plain text password - * @returns {Boolean} + * @callback {Function} callback Callback function + * @param {Error} err Error object + * @param {Boolean} isMatch Returns true if the given `password` matches record */ User.prototype.hasPassword = function(plain, fn) { From 8bed218a74e4e0e4200cdbe1864de80d26ed7f80 Mon Sep 17 00:00:00 2001 From: Samuel Reed Date: Tue, 5 Jul 2016 10:47:30 -0500 Subject: [PATCH 044/187] Support 'alias' in mail transport config. Useful if you need to set up multiple transports of the same type. --- lib/connectors/mail.js | 8 +++++--- test/email.test.js | 8 ++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/connectors/mail.js b/lib/connectors/mail.js index 3271c145e..8f98f4e74 100644 --- a/lib/connectors/mail.js +++ b/lib/connectors/mail.js @@ -59,10 +59,11 @@ MailConnector.prototype.DataAccessObject = Mailer; * Example: * * Email.setupTransport({ - * type: 'SMTP', + * type: "SMTP", * host: "smtp.gmail.com", // hostname * secureConnection: true, // use SSL * port: 465, // port for secure SMTP + * alias: "gmail", // optional alias for use with 'transport' option when sending * auth: { * user: "gmail.user@gmail.com", * pass: "userpass" @@ -88,7 +89,7 @@ MailConnector.prototype.setupTransport = function(setting) { transport = mailer.createTransport(transportModule(setting)); } - connector.transportsIndex[setting.type] = transport; + connector.transportsIndex[setting.alias || setting.type] = transport; connector.transports.push(transport); }; @@ -127,7 +128,8 @@ MailConnector.prototype.defaultTransport = function() { * to: "bar@blurdybloop.com, baz@blurdybloop.com", // list of receivers * subject: "Hello ✔", // Subject line * text: "Hello world ✔", // plaintext body - * html: "Hello world ✔" // html body + * html: "Hello world ✔", // html body + * transport: "gmail", // See 'alias' option above in setupTransport * } * * See https://github.com/andris9/Nodemailer for other supported options. diff --git a/test/email.test.js b/test/email.test.js index be12a3a63..c1303d8db 100644 --- a/test/email.test.js +++ b/test/email.test.js @@ -38,6 +38,14 @@ describe('Email connector', function() { assert(connector.transportForName('smtp')); }); + it('should set up a aliased transport for SMTP' , function() { + var connector = new MailConnector({transport: + {type: 'smtp', service: 'ses-us-east-1', alias: 'ses-smtp'} + }); + + assert(connector.transportForName('ses-smtp')); + }); + }); describe('Email and SMTP', function() { From e7bf538a2c4c5cccdb38ac6aa2423c6fd2836c29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 12 Jul 2016 16:53:57 +0200 Subject: [PATCH 045/187] 2.29.1 * Fix description for User.prototype.hasPassword (Jue Hou) * Fix verificationToken bug #2440 (Loay) * add missing unit tests for #2108 (Benjamin Kroeger) --- CHANGES.md | 10 ++++++++++ package.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index c7f37cf10..42456aaba 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,13 @@ +2016-07-12, Version 2.29.1 +========================== + + * Fix description for User.prototype.hasPassword (Jue Hou) + + * Fix verificationToken bug #2440 (Loay) + + * add missing unit tests for #2108 (Benjamin Kroeger) + + 2016-06-07, Version 2.29.0 ========================== diff --git a/package.json b/package.json index 9e33ec3fc..f65d44985 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.29.0", + "version": "2.29.1", "description": "LoopBack: Open Source Framework for Node.js", "homepage": "http://loopback.io/", "keywords": [ From 619372e51e1fce6360411feed3c478ac804ed113 Mon Sep 17 00:00:00 2001 From: Loay Date: Thu, 7 Jul 2016 11:30:57 -0400 Subject: [PATCH 046/187] Backport/Fix security issue 580 --- common/models/user.js | 14 +++++++ test/user.test.js | 87 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/common/models/user.js b/common/models/user.js index 09bdcb74c..1a5e6e8c6 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -302,6 +302,20 @@ module.exports = function(User) { return fn.promise; }; + User.observe('before delete', function(ctx, next) { + var AccessToken = ctx.Model.relations.accessTokens.modelTo; + var pkName = ctx.Model.definition.idName() || 'id'; + ctx.Model.find({ where: ctx.where, fields: [pkName] }, function(err, list) { + if (err) return next(err); + + var ids = list.map(function(u) { return u[pkName]; }); + ctx.where = {}; + ctx.where[pkName] = { inq: ids }; + + AccessToken.destroyAll({ userId: { inq: ids }}, next); + }); + }); + /** * Compare the given `password` with the users hashed password. * diff --git a/test/user.test.js b/test/user.test.js index 2fd77870c..ade96e4af 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -6,6 +6,7 @@ require('./support'); var loopback = require('../'); var User, AccessToken; +var async = require('async'); describe('User', function() { var validCredentialsEmail = 'foo@bar.com'; @@ -227,6 +228,92 @@ describe('User', function() { assert(u2.password === u1.password); }); + it('invalidates the user\'s accessToken when the user is deleted By id', function(done) { + var usersId; + async.series([ + function(next) { + User.create({ email: 'b@c.com', password: 'bar' }, function(err, user) { + usersId = user.id; + next(err); + }); + }, + function(next) { + User.login({ email: 'b@c.com', password: 'bar' }, function(err, accessToken) { + if (err) return next(err); + assert(accessToken.userId); + next(); + }); + }, + function(next) { + User.deleteById(usersId, function(err) { + next(err); + }); + }, + function(next) { + User.findById(usersId, function(err, userFound) { + if (err) return next(err); + expect(userFound).to.equal(null); + AccessToken.find({ where: { userId: usersId }}, function(err, tokens) { + if (err) return next(err); + expect(tokens.length).to.equal(0); + next(); + }); + }); + }, + ], function(err) { + if (err) return done(err); + done(); + }); + }); + + it('invalidates the user\'s accessToken when the user is deleted all', function(done) { + var usersId, accessTokenId; + async.series([ + function(next) { + User.create([{ name: 'myname', email: 'b@c.com', password: 'bar' }, + { name: 'myname', email: 'd@c.com', password: 'bar' }], function(err, user) { + usersId = user.id; + next(err); + }); + }, + function(next) { + User.login({ email: 'b@c.com', password: 'bar' }, function(err, accessToken) { + accessTokenId = accessToken.userId; + if (err) return next (err); + assert(accessTokenId); + next(); + }); + }, + function(next) { + User.login({ email: 'd@c.com', password: 'bar' }, function(err, accessToken) { + accessTokenId = accessToken.userId; + if (err) return next (err); + assert(accessTokenId); + next(); + }); + }, + function(next) { + User.deleteAll({ name: 'myname' }, function(err, user) { + next(err); + }); + }, + function(next) { + User.find({ where: { name: 'myname' }}, function(err, userFound) { + if (err) return next (err); + expect(userFound.length).to.equal(0); + AccessToken.find({ where: { userId: usersId }}, function(err, tokens) { + if (err) return next(err); + expect(tokens.length).to.equal(0); + next(); + }); + }); + }, + ], function(err) { + if (err) return done(err); + done(); + }); + }); + describe('custom password hash', function() { var defaultHashPassword; var defaultValidatePassword; From a8f30af49d8f4c4983b217ec179372e8107c1e8a Mon Sep 17 00:00:00 2001 From: Loay Date: Mon, 25 Jul 2016 17:22:51 -0400 Subject: [PATCH 047/187] Fix test case error --- test/user.test.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/test/user.test.js b/test/user.test.js index ade96e4af..c37bc5a1b 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -267,19 +267,24 @@ describe('User', function() { }); it('invalidates the user\'s accessToken when the user is deleted all', function(done) { - var usersId, accessTokenId; + var userIds = []; + var accessTokenId; async.series([ function(next) { - User.create([{ name: 'myname', email: 'b@c.com', password: 'bar' }, - { name: 'myname', email: 'd@c.com', password: 'bar' }], function(err, user) { - usersId = user.id; + User.create([ + { name: 'myname', email: 'b@c.com', password: 'bar' }, + { name: 'myname', email: 'd@c.com', password: 'bar' }, + ], function(err, users) { + userIds = users.map(function(u) { + return u.id; + }); next(err); }); }, function(next) { User.login({ email: 'b@c.com', password: 'bar' }, function(err, accessToken) { accessTokenId = accessToken.userId; - if (err) return next (err); + if (err) return next(err); assert(accessTokenId); next(); }); @@ -287,7 +292,7 @@ describe('User', function() { function(next) { User.login({ email: 'd@c.com', password: 'bar' }, function(err, accessToken) { accessTokenId = accessToken.userId; - if (err) return next (err); + if (err) return next(err); assert(accessTokenId); next(); }); @@ -299,9 +304,9 @@ describe('User', function() { }, function(next) { User.find({ where: { name: 'myname' }}, function(err, userFound) { - if (err) return next (err); + if (err) return next(err); expect(userFound.length).to.equal(0); - AccessToken.find({ where: { userId: usersId }}, function(err, tokens) { + AccessToken.find({ where: { userId: { inq: userIds }}}, function(err, tokens) { if (err) return next(err); expect(tokens.length).to.equal(0); next(); From 895629632f970814fee38261701ca21de456558b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 26 Jul 2016 14:10:13 +0200 Subject: [PATCH 048/187] test: use local registry in test fixtures Use local registry in test fixtures to prevent collision in globally shared models. Fix issues discoverd in auth implementation where the global registry was used instead of the correct local one. --- common/models/acl.js | 4 +- common/models/change.js | 2 +- test/access-token.test.js | 3 ++ .../access-control/server/model-config.json | 3 +- test/fixtures/access-control/server/server.js | 2 +- test/fixtures/e2e/server/server.js | 2 +- .../simple-app/server/model-config.json | 3 +- .../server/model-config.json | 3 +- .../simple-integration-app/server/server.js | 2 +- .../common/models/post.json | 4 +- .../server/datasources.json | 2 +- .../server/model-config.json | 7 ++-- .../user-integration-app/server/server.js | 2 +- test/model.test.js | 39 ++++++++----------- test/user.integration.js | 8 +--- 15 files changed, 41 insertions(+), 45 deletions(-) diff --git a/common/models/acl.js b/common/models/acl.js index d4823cec8..6bde460c0 100644 --- a/common/models/acl.js +++ b/common/models/acl.js @@ -268,7 +268,7 @@ module.exports = function(ACL) { * @return {Object[]} An array of ACLs */ ACL.getStaticACLs = function getStaticACLs(model, property) { - var modelClass = loopback.findModel(model); + var modelClass = this.registry.findModel(model); var staticACLs = []; if (modelClass && modelClass.settings.acls) { modelClass.settings.acls.forEach(function(acl) { @@ -360,7 +360,7 @@ module.exports = function(ACL) { acls = acls.concat(dynACLs); resolved = self.resolvePermission(acls, req); if (resolved && resolved.permission === ACL.DEFAULT) { - var modelClass = loopback.findModel(model); + var modelClass = self.registry.findModel(model); resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW; } if (callback) callback(null, resolved); diff --git a/common/models/change.js b/common/models/change.js index 605caa23d..ad19a7542 100644 --- a/common/models/change.js +++ b/common/models/change.js @@ -146,7 +146,7 @@ module.exports = function(Change) { */ Change.findOrCreateChange = function(modelName, modelId, callback) { - assert(loopback.findModel(modelName), modelName + ' does not exist'); + assert(this.registry.findModel(modelName), modelName + ' does not exist'); callback = callback || utils.createPromiseCallback(); var id = this.idForModel(modelName, modelId); var Change = this; diff --git a/test/access-token.test.js b/test/access-token.test.js index 4b6ebc5d4..c0b9a4367 100644 --- a/test/access-token.test.js +++ b/test/access-token.test.js @@ -397,6 +397,9 @@ describe('AccessToken', function() { }); describe('app.enableAuth()', function() { + beforeEach(function setupAuthWithModels() { + app.enableAuth({ dataSource: ds }); + }); beforeEach(createTestingToken); it('prevents remote call with 401 status on denied ACL', function(done) { diff --git a/test/fixtures/access-control/server/model-config.json b/test/fixtures/access-control/server/model-config.json index 2a48ed23c..ddb886fcc 100644 --- a/test/fixtures/access-control/server/model-config.json +++ b/test/fixtures/access-control/server/model-config.json @@ -2,7 +2,8 @@ "_meta": { "sources": [ "../common/models", - "./models" + "./models", + "../../../../common/models" ] }, "ACL": { diff --git a/test/fixtures/access-control/server/server.js b/test/fixtures/access-control/server/server.js index 13c2b9100..ba596a361 100644 --- a/test/fixtures/access-control/server/server.js +++ b/test/fixtures/access-control/server/server.js @@ -5,7 +5,7 @@ var loopback = require('../../../..'); var boot = require('loopback-boot'); -var app = module.exports = loopback(); +var app = module.exports = loopback({ localRegistry: true }); boot(app, __dirname); diff --git a/test/fixtures/e2e/server/server.js b/test/fixtures/e2e/server/server.js index 7fd6b7452..c353834fb 100644 --- a/test/fixtures/e2e/server/server.js +++ b/test/fixtures/e2e/server/server.js @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT var loopback = require('../../../../index'); -var app = module.exports = loopback(); +var app = module.exports = loopback({ localRegistry: true }); var models = require('./models'); var TestModel = models.TestModel; diff --git a/test/fixtures/simple-app/server/model-config.json b/test/fixtures/simple-app/server/model-config.json index 08448d73a..86602c000 100644 --- a/test/fixtures/simple-app/server/model-config.json +++ b/test/fixtures/simple-app/server/model-config.json @@ -2,7 +2,8 @@ "_meta": { "sources": [ "../common/models", - "./models" + "./models", + "../../../../common/models" ] }, "User": { diff --git a/test/fixtures/simple-integration-app/server/model-config.json b/test/fixtures/simple-integration-app/server/model-config.json index f5d34f287..ed73e7ca6 100644 --- a/test/fixtures/simple-integration-app/server/model-config.json +++ b/test/fixtures/simple-integration-app/server/model-config.json @@ -2,7 +2,8 @@ "_meta": { "sources": [ "../common/models", - "./models" + "./models", + "../../../../common/models" ] }, "ACL": { diff --git a/test/fixtures/simple-integration-app/server/server.js b/test/fixtures/simple-integration-app/server/server.js index 7c92b9c2a..95bc89610 100644 --- a/test/fixtures/simple-integration-app/server/server.js +++ b/test/fixtures/simple-integration-app/server/server.js @@ -5,7 +5,7 @@ var loopback = require('../../../../index'); var boot = require('loopback-boot'); -var app = module.exports = loopback(); +var app = module.exports = loopback({ localRegistry: true }); boot(app, __dirname); var apiPath = '/api'; diff --git a/test/fixtures/user-integration-app/common/models/post.json b/test/fixtures/user-integration-app/common/models/post.json index e6990891f..dd6d41f45 100644 --- a/test/fixtures/user-integration-app/common/models/post.json +++ b/test/fixtures/user-integration-app/common/models/post.json @@ -1,5 +1,5 @@ { - "name": "post", + "name": "Post", "base": "PersistedModel", "properties": { "title": { @@ -18,4 +18,4 @@ "model": "User" } } -} \ No newline at end of file +} diff --git a/test/fixtures/user-integration-app/server/datasources.json b/test/fixtures/user-integration-app/server/datasources.json index b024f6f31..9ebcf84f7 100644 --- a/test/fixtures/user-integration-app/server/datasources.json +++ b/test/fixtures/user-integration-app/server/datasources.json @@ -8,4 +8,4 @@ {"type": "STUB"} ] } -} \ No newline at end of file +} diff --git a/test/fixtures/user-integration-app/server/model-config.json b/test/fixtures/user-integration-app/server/model-config.json index b81818289..172ba6f86 100644 --- a/test/fixtures/user-integration-app/server/model-config.json +++ b/test/fixtures/user-integration-app/server/model-config.json @@ -2,7 +2,8 @@ "_meta": { "sources": [ "../common/models", - "./models" + "./models", + "../../../../common/models" ] }, "Email": { @@ -14,7 +15,7 @@ "public": true, "relations": { "posts": { - "model": "post", + "model": "Post", "type": "hasMany", "foreignKey": "userId" } @@ -51,7 +52,7 @@ "dataSource": "db", "public": true }, - "post": { + "Post": { "dataSource": "db", "public": true } diff --git a/test/fixtures/user-integration-app/server/server.js b/test/fixtures/user-integration-app/server/server.js index bbcb7fcc3..bfd7e1afc 100644 --- a/test/fixtures/user-integration-app/server/server.js +++ b/test/fixtures/user-integration-app/server/server.js @@ -5,7 +5,7 @@ var loopback = require('../../../../index'); var boot = require('loopback-boot'); -var app = module.exports = loopback(); +var app = module.exports = loopback({ localRegistry: true }); app.enableAuth(); boot(app, __dirname); app.use(loopback.token({model: app.models.AccessToken})); diff --git a/test/model.test.js b/test/model.test.js index a02690011..27e89ca5b 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -8,7 +8,6 @@ var chai = require('chai'); var expect = chai.expect; var loopback = require('../'); var ACL = loopback.ACL; -var Change = loopback.Change; var defineModelTestsWithDataSource = require('./util/model-tests'); var PersistedModel = loopback.PersistedModel; var sinonChai = require('sinon-chai'); @@ -80,7 +79,10 @@ describe.onServer('Remote Methods', function() { var app; beforeEach(function() { - User = PersistedModel.extend('user', { + app = loopback({ localRegistry: true, loadBuiltinModels: true }); + app.set('remoting', { errorHandler: { debug: true, log: false }}); + + User = app.registry.createModel('user', { id: { id: true, type: String, defaultFn: 'guid' }, 'first': String, 'last': String, @@ -93,7 +95,7 @@ describe.onServer('Remote Methods', function() { trackChanges: true }); - Post = PersistedModel.extend('post', { + Post = app.registry.createModel('post', { id: { id: true, type: String, defaultFn: 'guid' }, title: String, content: String @@ -101,12 +103,10 @@ describe.onServer('Remote Methods', function() { trackChanges: true }); - dataSource = loopback.createDataSource({ - connector: loopback.Memory - }); + dataSource = app.dataSource('db', { connector: 'memory' }); - User.attachTo(dataSource); - Post.attachTo(dataSource); + app.model(User, { dataSource: 'db' }); + app.model(Post, { dataSource: 'db' }); User.hasMany(Post); @@ -118,21 +118,16 @@ describe.onServer('Remote Methods', function() { } }; - loopback.remoteMethod( - User.login, - { - accepts: [ - {arg: 'username', type: 'string', required: true}, - {arg: 'password', type: 'string', required: true} - ], - returns: {arg: 'sessionId', type: 'any', root: true}, - http: {path: '/sign-in', verb: 'get'} - } - ); + User.remoteMethod('login', { + accepts: [ + { arg: 'username', type: 'string', required: true }, + { arg: 'password', type: 'string', required: true }, + ], + returns: { arg: 'sessionId', type: 'any', root: true }, + http: { path: '/sign-in', verb: 'get' }, + }); - app = loopback(); app.use(loopback.rest()); - app.model(User); }); describe('Model.destroyAll(callback)', function() { @@ -556,7 +551,7 @@ describe.onServer('Remote Methods', function() { it('Get the Change Model', function() { var UserChange = User.getChangeModel(); var change = new UserChange(); - assert(change instanceof Change); + assert(change instanceof app.registry.getModel('Change')); }); }); diff --git a/test/user.integration.js b/test/user.integration.js index 753df249b..46f597c58 100644 --- a/test/user.integration.js +++ b/test/user.integration.js @@ -22,16 +22,10 @@ describe('users - integration', function() { lt.beforeEach.withApp(app); before(function(done) { - // HACK: [rfeng] We have to reset the relations as they are polluted by - // other tests - app.models.User.hasMany(app.models.post); - app.models.User.hasMany(app.models.AccessToken, - {options: {disableInclude: true}}); - app.models.AccessToken.belongsTo(app.models.User); app.models.User.destroyAll(function(err) { if (err) return done(err); - app.models.post.destroyAll(function(err) { + app.models.Post.destroyAll(function(err) { if (err) return done(err); app.models.blog.destroyAll(function(err) { From 2ab599fdd1bb5c1706e176fa0126ae682864597a Mon Sep 17 00:00:00 2001 From: Amir Jafarian Date: Thu, 28 Jul 2016 11:29:25 -0400 Subject: [PATCH 049/187] Avoid calling deprecated methds *Avoid calling deprecated `getHttpMethod` and `getFullPath` --- test/remoting.integration.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/remoting.integration.js b/test/remoting.integration.js index 5154bb473..f047f7b3e 100644 --- a/test/remoting.integration.js +++ b/test/remoting.integration.js @@ -99,9 +99,9 @@ describe('remoting - integration', function() { ')', formatReturns(m), ' ', - m.getHttpMethod(), + m.getEndpoints()[0].verb, ' ', - m.getFullPath() + m.getEndpoints()[0].fullPath ].join(''); } From fa8ac8d324c65b0d1441a817b34c2458276b43c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 29 Jul 2016 16:49:29 +0200 Subject: [PATCH 050/187] Backport of #2565 --- test/remote-connector.test.js | 23 +++++++++++++++++------ test/util/model-tests.js | 3 ++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/test/remote-connector.test.js b/test/remote-connector.test.js index 701e170ba..88023ddd9 100644 --- a/test/remote-connector.test.js +++ b/test/remote-connector.test.js @@ -25,14 +25,25 @@ describe('RemoteConnector', function() { done(); }); }, + + // We are defining the model attached to the remote connector datasource, + // therefore change tracking must be disabled, only the remote API for + // replication should be present + trackChanges: false, + enableRemoteReplication: true, + onDefine: function(Model) { - var RemoteModel = Model.extend('Remote' + Model.modelName, {}, - { plural: Model.pluralModelName }); - RemoteModel.attachTo(loopback.createDataSource({ - connector: loopback.Memory + var ServerModel = Model.extend('Server' + Model.modelName, {}, { + plural: Model.pluralModelName, + // This is the model running on the server & attached to a real + // datasource, that's the place where to keep track of changes + trackChanges: true, + }); + ServerModel.attachTo(loopback.createDataSource({ + connector: loopback.Memory, })); - remoteApp.model(RemoteModel); - } + remoteApp.model(ServerModel); + }, }); beforeEach(function(done) { diff --git a/test/util/model-tests.js b/test/util/model-tests.js index d554c2d48..cac6ffca1 100644 --- a/test/util/model-tests.js +++ b/test/util/model-tests.js @@ -51,7 +51,8 @@ module.exports = function defineModelTestsWithDataSource(options) { 'domain': String, 'email': String }, { - trackChanges: true + trackChanges: options.trackChanges !== false, + enableRemoteReplication: options.enableRemoteReplication, }); User.attachTo(dataSource); From 7f5f8d6df51ff874f9ed6f024f48977d300561a0 Mon Sep 17 00:00:00 2001 From: jannyHou Date: Fri, 29 Jul 2016 14:41:43 -0400 Subject: [PATCH 051/187] Increase timeout --- test/rest.middleware.test.js | 1 + test/user.test.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/test/rest.middleware.test.js b/test/rest.middleware.test.js index f2a97d021..80c9774c1 100644 --- a/test/rest.middleware.test.js +++ b/test/rest.middleware.test.js @@ -6,6 +6,7 @@ var path = require('path'); describe('loopback.rest', function() { + this.timeout(10000); var app, MyModel; beforeEach(function() { diff --git a/test/user.test.js b/test/user.test.js index c37bc5a1b..3fc2ccda8 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -9,6 +9,8 @@ var User, AccessToken; var async = require('async'); describe('User', function() { + this.timeout(10000); + var validCredentialsEmail = 'foo@bar.com'; var validCredentials = {email: validCredentialsEmail, password: 'bar'}; var validCredentialsEmailVerified = {email: 'foo1@bar.com', password: 'bar1', emailVerified: true}; From fea3b781a091ac3bd07479a19c490914b0bb7a17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 2 Aug 2016 10:59:21 +0200 Subject: [PATCH 052/187] Update dependencies to their latest versions --- .jscsrc | 2 ++ common/models/user.js | 3 +- lib/persisted-model.js | 2 +- package.json | 50 ++++++++++++++++++---------------- test/access-token.test.js | 4 +-- test/error-handler.test.js | 7 +++-- test/hidden-properties.test.js | 18 ++++++------ test/karma.conf.js | 6 ++-- test/relations.integration.js | 6 ++-- test/user.test.js | 6 ++-- 10 files changed, 54 insertions(+), 50 deletions(-) diff --git a/.jscsrc b/.jscsrc index ba97a76df..b2569dc0c 100644 --- a/.jscsrc +++ b/.jscsrc @@ -10,6 +10,8 @@ ], "disallowMultipleVarDecl": "exceptUndefined", "disallowSpacesInsideObjectBrackets": null, + "jsDoc": false, + "requireDotNotation": false, "maximumLineLength": { "value": 150, "allowComments": true, diff --git a/common/models/user.js b/common/models/user.js index 1a5e6e8c6..4d9292a22 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -672,8 +672,7 @@ module.exports = function(User) { return tokenID; }, description: 'Do not supply this argument, it is automatically extracted ' + 'from request headers.' - } - ], + }], http: {verb: 'all'} } ); diff --git a/lib/persisted-model.js b/lib/persisted-model.js index 734992795..a7b8315d4 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -702,7 +702,7 @@ module.exports = function(registry) { accepts: [ {arg: 'since', type: 'number', description: 'Find deltas since this checkpoint'}, {arg: 'remoteChanges', type: 'array', description: 'an array of change objects', - http: {source: 'body'}} + http: {source: 'body'}} ], returns: {arg: 'result', type: 'object', root: true}, http: {verb: 'post', path: '/diff'} diff --git a/package.json b/package.json index f65d44985..bef100d67 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "test": "grunt mocha-and-karma" }, "dependencies": { - "async": "^0.9.0", + "async": "^2.0.1", "bcryptjs": "^2.1.0", "body-parser": "^1.12.0", "canonical-json": "0.0.4", @@ -46,8 +46,8 @@ "inflection": "^1.6.0", "loopback-connector-remote": "^1.0.3", "loopback-phase": "^1.2.0", - "nodemailer": "^1.3.1", - "nodemailer-stub-transport": "^0.1.5", + "nodemailer": "^2.5.0", + "nodemailer-stub-transport": "^1.0.0", "serve-favicon": "^2.2.0", "stable": "^0.1.5", "strong-remoting": "^2.21.0", @@ -58,37 +58,39 @@ "loopback-datasource-juggler": "^2.19.0" }, "devDependencies": { - "bluebird": "^2.9.9", - "browserify": "^10.0.0", - "chai": "^2.1.1", + "bluebird": "^3.4.1", + "browserify": "^13.1.0", + "chai": "^3.5.0", "es5-shim": "^4.1.0", - "grunt": "^0.4.5", - "grunt-browserify": "^3.5.0", - "grunt-cli": "^0.1.13", - "grunt-contrib-jshint": "^0.11.0", - "grunt-contrib-uglify": "^0.9.1", - "grunt-contrib-watch": "^0.6.1", - "grunt-jscs": "^1.5.0", - "grunt-karma": "^0.10.1", + "eslint-config-loopback": "^1.0.0", + "grunt": "^1.0.1", + "grunt-browserify": "^5.0.0", + "grunt-cli": "^1.2.0", + "grunt-contrib-jshint": "^1.0.0", + "grunt-contrib-uglify": "^2.0.0", + "grunt-contrib-watch": "^1.0.0", + "grunt-jscs": "^3.0.1", + "grunt-karma": "^2.0.0", "grunt-mocha-test": "^0.12.7", - "karma": "^0.12.31", - "karma-browserify": "^4.0.0", - "karma-chrome-launcher": "^0.1.7", - "karma-firefox-launcher": "^0.1.4", - "karma-html2js-preprocessor": "^0.1.0", - "karma-junit-reporter": "^0.2.2", - "karma-mocha": "^0.1.10", + "karma": "^1.1.2", + "karma-browserify": "^4.4.2", + "karma-chrome-launcher": "^1.0.1", + "karma-firefox-launcher": "^1.0.0", + "karma-html2js-preprocessor": "^1.0.0", + "karma-junit-reporter": "^1.0.0", + "karma-mocha": "^1.1.1", "karma-phantomjs-launcher": "^1.0.0", - "karma-script-launcher": "^0.1.0", + "karma-script-launcher": "^1.0.0", "loopback-boot": "^2.7.0", "loopback-datasource-juggler": "^2.19.1", "loopback-testing": "~1.1.0", - "mocha": "^2.1.0", + "mocha": "^3.0.0", "phantomjs-prebuilt": "^2.1.7", "sinon": "^1.13.0", "sinon-chai": "^2.8.0", + "strong-error-handler": "^1.0.1", "strong-task-emitter": "^0.0.6", - "supertest": "^0.15.0" + "supertest": "^2.0.0" }, "repository": { "type": "git", diff --git a/test/access-token.test.js b/test/access-token.test.js index c0b9a4367..690b1953d 100644 --- a/test/access-token.test.js +++ b/test/access-token.test.js @@ -421,7 +421,7 @@ describe('app.enableAuth()', function() { }); it('prevent remote call with app setting status on denied ACL', function(done) { - createTestAppAndRequest(this.token, {app:{aclErrorStatus:403}}, done) + createTestAppAndRequest(this.token, {app: {aclErrorStatus: 403}}, done) .del('/tests/123') .expect(403) .set('authorization', this.token.id) @@ -439,7 +439,7 @@ describe('app.enableAuth()', function() { }); it('prevent remote call with app setting status on denied ACL', function(done) { - createTestAppAndRequest(this.token, {model:{aclErrorStatus:404}}, done) + createTestAppAndRequest(this.token, {model: {aclErrorStatus: 404}}, done) .del('/tests/123') .expect(404) .set('authorization', this.token.id) diff --git a/test/error-handler.test.js b/test/error-handler.test.js index 273481240..d522c5e6c 100644 --- a/test/error-handler.test.js +++ b/test/error-handler.test.js @@ -5,7 +5,7 @@ var loopback = require('../'); var app; -var assert = require('assert'); +var expect = require('chai').expect; var request = require('supertest'); describe('loopback.errorHandler(options)', function() { @@ -21,7 +21,8 @@ describe('loopback.errorHandler(options)', function() { request(app) .get('/url-does-not-exist') .end(function(err, res) { - assert.ok(res.error.text.match(/
      •    at raiseUrlNotFoundError/)); + expect(res.error.text).to.match( + /
        • (  )+at raiseUrlNotFoundError/); done(); }); @@ -38,7 +39,7 @@ describe('loopback.errorHandler(options)', function() { request(app) .get('/url-does-not-exist') .end(function(err, res) { - assert.ok(res.error.text.match(/
            <\/ul>/)); + expect(res.error.text).to.match(/
              <\/ul>/); done(); }); diff --git a/test/hidden-properties.test.js b/test/hidden-properties.test.js index 6e3f72064..8059ddf5b 100644 --- a/test/hidden-properties.test.js +++ b/test/hidden-properties.test.js @@ -41,17 +41,17 @@ describe('hidden properties', function() { it('should hide a property remotely', function(done) { request(this.app) - .get('/products') - .expect('Content-Type', /json/) - .expect(200) - .end(function(err, res) { - if (err) return done(err); + .get('/products') + .expect('Content-Type', /json/) + .expect(200) + .end(function(err, res) { + if (err) return done(err); - var product = res.body[0]; - assert.equal(product.secret, undefined); + var product = res.body[0]; + assert.equal(product.secret, undefined); - done(); - }); + done(); + }); }); it('should hide a property of nested models', function(done) { diff --git a/test/karma.conf.js b/test/karma.conf.js index 96a4d4e6d..9c1110258 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -73,9 +73,9 @@ module.exports = function(config) { captureTimeout: 60000, // to avoid DISCONNECTED messages - browserDisconnectTimeout : 10000, // default 2000 - browserDisconnectTolerance : 1, // default 0 - browserNoActivityTimeout : 60000, //default 10000 + browserDisconnectTimeout: 10000, // default 2000 + browserDisconnectTolerance: 1, // default 0 + browserNoActivityTimeout: 60000, //default 10000 // Continuous Integration mode // if true, it capture browsers, run tests and exit diff --git a/test/relations.integration.js b/test/relations.integration.js index e853513e8..ba4ea15ed 100644 --- a/test/relations.integration.js +++ b/test/relations.integration.js @@ -102,7 +102,7 @@ describe('relations - integration', function() { it('includes the related child model', function(done) { var url = '/api/readers/' + this.reader.id; this.get(url) - .query({'filter': {'include' : 'pictures'}}) + .query({'filter': {'include': 'pictures'}}) .expect(200, function(err, res) { if (err) return done(err); @@ -119,7 +119,7 @@ describe('relations - integration', function() { it('includes the related parent model', function(done) { var url = '/api/pictures'; this.get(url) - .query({'filter': {'include' : 'imageable'}}) + .query({'filter': {'include': 'imageable'}}) .expect(200, function(err, res) { if (err) return done(err); @@ -134,7 +134,7 @@ describe('relations - integration', function() { it('includes related models scoped to the related parent model', function(done) { var url = '/api/pictures'; this.get(url) - .query({'filter': {'include' : {'relation': 'imageable', 'scope': { 'include' : 'team'}}}}) + .query({'filter': {'include': {'relation': 'imageable', 'scope': { 'include': 'team'}}}}) .expect(200, function(err, res) { if (err) return done(err); diff --git a/test/user.test.js b/test/user.test.js index 3fc2ccda8..cfb7232e8 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -388,7 +388,7 @@ describe('User', function() { it('Should be able to find lowercase email with mixed-case email query', function(done) { User.settings.caseSensitiveEmail = false; - User.find({where:{email: validMixedCaseEmailCredentials.email}}, function(err, result) { + User.find({where: {email: validMixedCaseEmailCredentials.email}}, function(err, result) { if (err) done(err); assert(result[0], 'The query did not find the user'); @@ -1225,7 +1225,7 @@ describe('User', function() { redirect: '/', protocol: ctx.req.protocol, host: ctx.req.get('host'), - headers: {'message-id':'custom-header-value'} + headers: {'message-id': 'custom-header-value'} }; user.verify(options, function(err, result) { @@ -1680,7 +1680,7 @@ describe('User', function() { .end(function(err, res) { if (err) return done(err); - assert.deepEqual(res.body, { }); + assert.deepEqual(res.body, ''); done(); }); From ed953a4c6fb0671fabbe00e3719e329e3ccabf82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 3 Aug 2016 15:55:29 +0200 Subject: [PATCH 053/187] test: fix broken Role tests Rework the test suite to always report errors and correctly signal when async tests are done. This should prevent spurious test failures on CI servers that are difficult to troubleshoot, because the error is reported for different test case. --- test/role.test.js | 513 +++++++++++++++++++++++++++++++--------------- 1 file changed, 344 insertions(+), 169 deletions(-) diff --git a/test/role.test.js b/test/role.test.js index 50a5e0321..971ed6222 100644 --- a/test/role.test.js +++ b/test/role.test.js @@ -6,11 +6,6 @@ var assert = require('assert'); var sinon = require('sinon'); var loopback = require('../index'); -var Role = loopback.Role; -var RoleMapping = loopback.RoleMapping; -var User = loopback.User; -var Application = loopback.Application; -var ACL = loopback.ACL; var async = require('async'); var expect = require('chai').expect; var Promise = require('bluebird'); @@ -20,17 +15,29 @@ function checkResult(err, result) { } describe('role model', function() { - var ds; + var app, Role, RoleMapping, User, Application, ACL; beforeEach(function() { - ds = loopback.createDataSource({connector: 'memory'}); - // Re-attach the models so that they can have isolated store to avoid + // Use local app registry to ensure models are isolated to avoid // pollutions from other tests - ACL.attachTo(ds); - User.attachTo(ds); - Role.attachTo(ds); - RoleMapping.attachTo(ds); - Application.attachTo(ds); + app = loopback({ localRegistry: true, loadBuiltinModels: true }); + app.dataSource('db', { connector: 'memory' }); + + ACL = app.registry.getModel('ACL'); + app.model(ACL, { dataSource: 'db' }); + + User = app.registry.getModel('User'); + app.model(User, { dataSource: 'db' }); + + Role = app.registry.getModel('Role'); + app.model(Role, { dataSource: 'db' }); + + RoleMapping = app.registry.getModel('RoleMapping'); + app.model(RoleMapping, { dataSource: 'db' }); + + Application = app.registry.getModel('Application'); + app.model(Application, { dataSource: 'db' }); + ACL.roleModel = Role; ACL.roleMappingModel = RoleMapping; ACL.userModel = User; @@ -40,55 +47,91 @@ describe('role model', function() { Role.applicationModel = Application; }); - it('should define role/role relations', function() { - Role.create({name: 'user'}, function(err, userRole) { - Role.create({name: 'admin'}, function(err, adminRole) { - userRole.principals.create({principalType: RoleMapping.ROLE, principalId: adminRole.id}, function(err, mapping) { - Role.find(function(err, roles) { - assert.equal(roles.length, 2); - }); - RoleMapping.find(function(err, mappings) { - assert.equal(mappings.length, 1); - assert.equal(mappings[0].principalType, RoleMapping.ROLE); - assert.equal(mappings[0].principalId, adminRole.id); - }); - userRole.principals(function(err, principals) { - assert.equal(principals.length, 1); - }); - userRole.roles(function(err, roles) { - assert.equal(roles.length, 1); + it('should define role/role relations', function(done) { + Role.create({ name: 'user' }, function(err, userRole) { + if (err) return done(err); + Role.create({ name: 'admin' }, function(err, adminRole) { + if (err) return done(err); + userRole.principals.create( + { principalType: RoleMapping.ROLE, principalId: adminRole.id }, + function(err, mapping) { + if (err) return done(err); + + async.parallel([ + function(next) { + Role.find(function(err, roles) { + if (err) return next(err); + assert.equal(roles.length, 2); + next(); + }); + }, + function(next) { + RoleMapping.find(function(err, mappings) { + if (err) return next(err); + assert.equal(mappings.length, 1); + assert.equal(mappings[0].principalType, RoleMapping.ROLE); + assert.equal(mappings[0].principalId, adminRole.id); + next(); + }); + }, + function(next) { + userRole.principals(function(err, principals) { + if (err) return next(err); + assert.equal(principals.length, 1); + next(); + }); + }, + function(next) { + userRole.roles(function(err, roles) { + if (err) return next(err); + assert.equal(roles.length, 1); + next(); + }); + }, + ], done); }); - }); }); }); - }); - it('should define role/user relations', function() { + it('should define role/user relations', function(done) { User.create({ name: 'Raymond', email: 'x@y.com', password: 'foobar' }, function(err, user) { + if (err) return done(err); Role.create({ name: 'userRole' }, function(err, role) { + if (err) return done(err); role.principals.create({ principalType: RoleMapping.USER, principalId: user.id }, function(err, p) { - Role.find(function(err, roles) { - assert(!err); - assert.equal(roles.length, 1); - assert.equal(roles[0].name, 'userRole'); - }); - role.principals(function(err, principals) { - assert(!err); - assert.equal(principals.length, 1); - assert.equal(principals[0].principalType, RoleMapping.USER); - assert.equal(principals[0].principalId, user.id); - }); - role.users(function(err, users) { - assert(!err); - assert.equal(users.length, 1); - assert.equal(users[0].id, user.id); - }); + if (err) return done(err); + async.parallel([ + function(next) { + Role.find(function(err, roles) { + if (err) return next(err); + assert.equal(roles.length, 1); + assert.equal(roles[0].name, 'userRole'); + next(); + }); + }, + function(next) { + role.principals(function(err, principals) { + if (err) return next(err); + assert.equal(principals.length, 1); + assert.equal(principals[0].principalType, RoleMapping.USER); + assert.equal(principals[0].principalId, user.id); + next(); + }); + }, + function(next) { + role.users(function(err, users) { + if (err) return next(err); + assert.equal(users.length, 1); + assert.equal(users[0].id, user.id); + next(); + }); + }, + ], done); }); }); }); - }); it('should not allow duplicate role name', function(done) { @@ -107,81 +150,146 @@ describe('role model', function() { }); }); - it('should automatically generate role id', function() { + it('should automatically generate role id', function(done) { User.create({ name: 'Raymond', email: 'x@y.com', password: 'foobar' }, function(err, user) { + if (err) return done(err); Role.create({ name: 'userRole' }, function(err, role) { + if (err) return done(err); assert(role.id); - role.principals.create({principalType: RoleMapping.USER, principalId: user.id}, function(err, p) { + role.principals.create({ principalType: RoleMapping.USER, principalId: user.id }, + function(err, p) { + if (err) return done(err); assert(p.id); assert.equal(p.roleId, role.id); - Role.find(function(err, roles) { - assert(!err); - assert.equal(roles.length, 1); - assert.equal(roles[0].name, 'userRole'); - }); - role.principals(function(err, principals) { - assert(!err); - assert.equal(principals.length, 1); - assert.equal(principals[0].principalType, RoleMapping.USER); - assert.equal(principals[0].principalId, user.id); - }); - role.users(function(err, users) { - assert(!err); - assert.equal(users.length, 1); - assert.equal(users[0].id, user.id); - }); + async.parallel([ + function(next) { + Role.find(function(err, roles) { + if (err) return next(err); + assert.equal(roles.length, 1); + assert.equal(roles[0].name, 'userRole'); + next(); + }); + }, + function(next) { + role.principals(function(err, principals) { + if (err) return next(err); + assert.equal(principals.length, 1); + assert.equal(principals[0].principalType, RoleMapping.USER); + assert.equal(principals[0].principalId, user.id); + next(); + }); + }, + function(next) { + role.users(function(err, users) { + if (err) return next(err); + assert.equal(users.length, 1); + assert.equal(users[0].id, user.id); + }); + next(); + }, + ], done); }); }); }); - }); - it('should support getRoles() and isInRole()', function() { + it('should support getRoles() and isInRole()', function(done) { User.create({ name: 'Raymond', email: 'x@y.com', password: 'foobar' }, function(err, user) { + if (err) return done(err); Role.create({ name: 'userRole' }, function(err, role) { + if (err) return done(err); role.principals.create({ principalType: RoleMapping.USER, principalId: user.id }, function(err, p) { - Role.isInRole('userRole', { principalType: RoleMapping.USER, principalId: user.id }, - function(err, exists) { - assert(!err && exists === true); - }); - - Role.isInRole('userRole', {principalType: RoleMapping.APP, principalId: user.id}, function(err, exists) { - assert(!err && exists === false); - }); - - Role.isInRole('userRole', {principalType: RoleMapping.USER, principalId: 100}, function(err, exists) { - assert(!err && exists === false); - }); - - Role.getRoles({principalType: RoleMapping.USER, principalId: user.id}, function(err, roles) { - assert.equal(roles.length, 3); // everyone, authenticated, userRole - assert(roles.indexOf(role.id) >= 0); - assert(roles.indexOf(Role.EVERYONE) >= 0); - assert(roles.indexOf(Role.AUTHENTICATED) >= 0); - }); - Role.getRoles({principalType: RoleMapping.APP, principalId: user.id}, function(err, roles) { - assert.equal(roles.length, 2); - assert(roles.indexOf(Role.EVERYONE) >= 0); - assert(roles.indexOf(Role.AUTHENTICATED) >= 0); - }); - Role.getRoles({principalType: RoleMapping.USER, principalId: 100}, function(err, roles) { - assert.equal(roles.length, 2); - assert(roles.indexOf(Role.EVERYONE) >= 0); - assert(roles.indexOf(Role.AUTHENTICATED) >= 0); - }); - Role.getRoles({principalType: RoleMapping.USER, principalId: null}, function(err, roles) { - assert.equal(roles.length, 2); - assert(roles.indexOf(Role.EVERYONE) >= 0); - assert(roles.indexOf(Role.UNAUTHENTICATED) >= 0); - }); + if (err) return done(err); + async.series([ + function(next) { + Role.isInRole( + 'userRole', + { principalType: RoleMapping.USER, principalId: user.id }, + function(err, inRole) { + if (err) return next(err); + // NOTE(bajtos) Apparently isRole is not a boolean, + // but the matchin role object instead + assert(!!inRole); + next(); + }); + }, + function(next) { + Role.isInRole( + 'userRole', + { principalType: RoleMapping.APP, principalId: user.id }, + function(err, inRole) { + if (err) return next(err); + assert(!inRole); + next(); + }); + }, + function(next) { + Role.isInRole( + 'userRole', + { principalType: RoleMapping.USER, principalId: 100 }, + function(err, inRole) { + if (err) return next(err); + assert(!inRole); + next(); + }); + }, + function(next) { + Role.getRoles( + { principalType: RoleMapping.USER, principalId: user.id }, + function(err, roles) { + if (err) return next(err); + expect(roles).to.eql([ + Role.AUTHENTICATED, + Role.EVERYONE, + role.id, + ]); + next(); + }); + }, + function(next) { + Role.getRoles( + { principalType: RoleMapping.APP, principalId: user.id }, + function(err, roles) { + if (err) return next(err); + expect(roles).to.eql([ + Role.AUTHENTICATED, + Role.EVERYONE, + ]); + next(); + }); + }, + function(next) { + Role.getRoles( + { principalType: RoleMapping.USER, principalId: 100 }, + function(err, roles) { + if (err) return next(err); + expect(roles).to.eql([ + Role.AUTHENTICATED, + Role.EVERYONE, + ]); + next(); + }); + }, + function(next) { + Role.getRoles( + { principalType: RoleMapping.USER, principalId: null }, + function(err, roles) { + if (err) return next(err); + expect(roles).to.eql([ + Role.UNAUTHENTICATED, + Role.EVERYONE, + ]); + next(); + }); + }, + ], done); }); }); }); - }); - it('should support owner role resolver', function() { + it('should support owner role resolver', function(done) { Role.registerResolver('returnPromise', function(role, context) { return new Promise(function(resolve) { process.nextTick(function() { @@ -190,61 +298,120 @@ describe('role model', function() { }); }); - var Album = ds.createModel('Album', { + var Album = app.registry.createModel('Album', { name: String, - userId: Number + userId: Number, }, { relations: { user: { type: 'belongsTo', model: 'User', - foreignKey: 'userId' - } - } + foreignKey: 'userId', + }, + }, }); + app.model(Album, { dataSource: 'db' }); - User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function(err, user) { - Role.isInRole('returnPromise', { principalType: ACL.USER, principalId: user.id }, - function(err, yes) { - assert(!err && yes); - }); - - Role.isInRole(Role.AUTHENTICATED, { principalType: ACL.USER, principalId: user.id }, - function(err, yes) { - assert(!err && yes); - }); - - Role.isInRole(Role.AUTHENTICATED, { principalType: ACL.USER, principalId: null }, - function(err, yes) { - assert(!err && !yes); - }); - - Role.isInRole(Role.UNAUTHENTICATED, {principalType: ACL.USER, principalId: user.id}, function(err, yes) { - assert(!err && !yes); - }); - Role.isInRole(Role.UNAUTHENTICATED, {principalType: ACL.USER, principalId: null}, function(err, yes) { - assert(!err && yes); - }); - - Role.isInRole(Role.EVERYONE, {principalType: ACL.USER, principalId: user.id}, function(err, yes) { - assert(!err && yes); - }); - - Role.isInRole(Role.EVERYONE, {principalType: ACL.USER, principalId: null}, function(err, yes) { - assert(!err && yes); - }); - - Album.create({ name: 'Album 1', userId: user.id }, function(err, album1) { - var role = { principalType: ACL.USER, principalId: user.id, model: Album, id: album1.id }; - Role.isInRole(Role.OWNER, role, function(err, yes) { - assert(!err && yes); - }); - Album.create({name: 'Album 2'}, function(err, album2) { - Role.isInRole(Role.OWNER, {principalType: ACL.USER, principalId: user.id, model: Album, id: album2.id}, function(err, yes) { - assert(!err && !yes); + User.create({ name: 'Raymond', email: 'x@y.com', password: 'foobar' }, function(err, user) { + if (err) return done(err); + async.parallel([ + function(next) { + Role.isInRole( + 'returnPromise', + { principalType: ACL.USER, principalId: user.id }, + function(err, yes) { + if (err) return next(err); + assert(yes); + next(); + }); + }, + function(next) { + Role.isInRole( + Role.AUTHENTICATED, + { principalType: ACL.USER, principalId: user.id }, + function(err, yes) { + if (err) next(err); + assert(yes); + next(); + }); + }, + function(next) { + Role.isInRole( + Role.AUTHENTICATED, + { principalType: ACL.USER, principalId: null }, + function(err, yes) { + if (err) next(err); + assert(!yes); + next(); + }); + }, + function(next) { + Role.isInRole( + Role.UNAUTHENTICATED, + { principalType: ACL.USER, principalId: user.id }, + function(err, yes) { + if (err) return next(err); + assert(!yes); + next(); + }); + }, + function(next) { + Role.isInRole( + Role.UNAUTHENTICATED, + { principalType: ACL.USER, principalId: null }, + function(err, yes) { + if (err) return next(err); + assert(yes); + next(); + }); + }, + function(next) { + Role.isInRole( + Role.EVERYONE, + { principalType: ACL.USER, principalId: user.id }, + function(err, yes) { + if (err) return next(err); + assert(yes); + next(); + }); + }, + function(next) { + Role.isInRole( + Role.EVERYONE, + { principalType: ACL.USER, principalId: null }, + function(err, yes) { + if (err) return next(err); + assert(yes); + next(); + }); + }, + function(next) { + Album.create({ name: 'Album 1', userId: user.id }, function(err, album1) { + if (err) return done(err); + var role = { + principalType: ACL.USER, principalId: user.id, + model: Album, id: album1.id, + }; + Role.isInRole(Role.OWNER, role, function(err, yes) { + if (err) return next(err); + assert(yes); + + Album.create({ name: 'Album 2' }, function(err, album2) { + if (err) return next(err); + role = { + principalType: ACL.USER, principalId: user.id, + model: Album, id: album2.id, + }; + Role.isInRole(Role.OWNER, role, function(err, yes) { + if (err) return next(err); + assert(!yes); + next(); + }); + }); + }); }); - }); - }); + }, + ], done); }); }); @@ -255,7 +422,7 @@ describe('role model', function() { User.create({ username: 'john', email: 'john@gmail.com', - password: 'jpass' + password: 'jpass', }, function(err, u) { if (err) return done(err); @@ -263,18 +430,18 @@ describe('role model', function() { User.create({ username: 'mary', email: 'mary@gmail.com', - password: 'mpass' + password: 'mpass', }, function(err, u) { if (err) return done(err); Application.create({ - name: 'demo' + name: 'demo', }, function(err, a) { if (err) return done(err); app = a; Role.create({ - name: 'admin' + name: 'admin', }, function(err, r) { if (err) return done(err); @@ -282,12 +449,12 @@ describe('role model', function() { var principals = [ { principalType: ACL.USER, - principalId: user.id + principalId: user.id, }, { principalType: ACL.APP, - principalId: app.id - } + principalId: app.id, + }, ]; async.each(principals, function(p, done) { role.principals.create(p, done); @@ -398,7 +565,6 @@ describe('role model', function() { done(); }); }); - }); describe('listByPrincipalType', function() { @@ -425,12 +591,17 @@ describe('role model', function() { mappings.forEach(function(principalType) { var Model = principalTypesToModels[principalType]; - Model.create({name:'test', email:'x@y.com', password: 'foobar'}, function(err, model) { - Role.create({name:'testRole'}, function(err, role) { - role.principals.create({principalType: principalType, principalId: model.id}, function(err, p) { + Model.create({ name: 'test', email: 'x@y.com', password: 'foobar' }, function(err, model) { + if (err) return done(err); + var uniqueRoleName = 'testRoleFor' + principalType; + Role.create({ name: uniqueRoleName }, function(err, role) { + if (err) return done(err); + role.principals.create({ principalType: principalType, principalId: model.id }, + function(err, p) { + if (err) return done(err); var pluralName = Model.pluralModelName.toLowerCase(); role[pluralName](function(err, models) { - assert(!err); + if (err) return done(err); assert.equal(models.length, 1); if (++runs === mappings.length) { @@ -444,13 +615,17 @@ describe('role model', function() { }); it('should apply query', function(done) { - User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function(err, user) { - Role.create({name: 'userRole'}, function(err, role) { - role.principals.create({principalType: RoleMapping.USER, principalId: user.id}, function(err, p) { - var query = {fields:['id', 'name']}; + User.create({ name: 'Raymond', email: 'x@y.com', password: 'foobar' }, function(err, user) { + if (err) return done(err); + Role.create({ name: 'userRole' }, function(err, role) { + if (err) return done(err); + role.principals.create({ principalType: RoleMapping.USER, principalId: user.id }, + function(err, p) { + if (err) return done(err); + var query = { fields: ['id', 'name'] }; sandbox.spy(User, 'find'); role.users(query, function(err, users) { - assert(!err); + if (err) return done(err); assert.equal(users.length, 1); assert.equal(users[0].id, user.id); assert(User.find.calledWith(query)); From fc5f16d833f67a9fdc8be4e69fb9402f0792256f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 4 Aug 2016 11:00:00 +0200 Subject: [PATCH 054/187] test: make status test more robust Rework assertions to report helpful messages on failure. Increase the "elapsed" limit from 100ms to 300ms to support our slow CI machines. --- test/app.test.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/test/app.test.js b/test/app.test.js index fd8d03735..d0f0aca60 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -14,6 +14,7 @@ var loopback = require('../'); var PersistedModel = loopback.PersistedModel; var describe = require('./util/describe'); +var expect = require('chai').expect; var it = require('./util/it'); describe('app', function() { @@ -945,18 +946,14 @@ describe('app', function() { .end(function(err, res) { if (err) return done(err); - assert.equal(typeof res.body, 'object'); - assert(res.body.started); - // The number can be 0 - assert(res.body.uptime !== undefined); + expect(res.body).to.be.an('object'); + expect(res.body).to.have.property('started'); + expect(res.body.uptime, 'uptime').to.be.gte(0); var elapsed = Date.now() - Number(new Date(res.body.started)); - // elapsed should be a positive number... - assert(elapsed >= 0); - - // less than 100 milliseconds - assert(elapsed < 100); + // elapsed should be a small positive number... + expect(elapsed, 'elapsed').to.be.within(0, 300); done(); }); From 593fd6e0422bb58ce0f8dce1732c0111ffb95fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 4 Aug 2016 13:32:47 +0200 Subject: [PATCH 055/187] test: increate timeout in Role test --- test/role.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/role.test.js b/test/role.test.js index 971ed6222..83b82f331 100644 --- a/test/role.test.js +++ b/test/role.test.js @@ -419,6 +419,7 @@ describe('role model', function() { var user, app, role; beforeEach(function(done) { + this.timeout(5000); User.create({ username: 'john', email: 'john@gmail.com', From 2eec008e0e3e018191029e93993d083af10f901b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 4 Aug 2016 14:41:33 +0200 Subject: [PATCH 056/187] test: fix "socket hang up" error in app.test Rework the test to always wait for the client request to finish before calling the test done. --- test/app.test.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test/app.test.js b/test/app.test.js index d0f0aca60..fd2abfd89 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -1046,8 +1046,18 @@ describe('app', function() { }); function executeMiddlewareHandlers(app, urlPath, callback) { + var handlerError; var server = http.createServer(function(req, res) { - app.handle(req, res, callback); + app.handle(req, res, function(err) { + if (err) { + handlerError = err; + res.statusCode = err.status || err.statusCode || 500; + res.end(err.stack || err); + } else { + res.statusCode = 204; + res.end(); + } + }); }); if (callback === undefined && typeof urlPath === 'function') { @@ -1058,6 +1068,6 @@ function executeMiddlewareHandlers(app, urlPath, callback) { request(server) .get(urlPath) .end(function(err) { - if (err) return callback(err); + callback(handlerError || err); }); } From 0eff26199c1ef3505f13458286cb94862a0fdba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 5 Aug 2016 11:33:57 +0200 Subject: [PATCH 057/187] test: fix timeout in rest.middleware.test --- test/rest.middleware.test.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/rest.middleware.test.js b/test/rest.middleware.test.js index 80c9774c1..58fd91ab3 100644 --- a/test/rest.middleware.test.js +++ b/test/rest.middleware.test.js @@ -18,9 +18,6 @@ describe('loopback.rest', function() { MyModel.attachTo(db); }); - if (process.env.CI) - this.timeout(3000); - it('works out-of-the-box', function(done) { app.model(MyModel); app.use(loopback.rest()); From 3767940472ef5b2b169c975743ae50c5f7f05e77 Mon Sep 17 00:00:00 2001 From: Candy Date: Tue, 7 Jun 2016 10:48:28 -0400 Subject: [PATCH 058/187] Backport of #2407 --- browser/current-context.js | 4 +- common/models/access-token.js | 4 +- common/models/acl.js | 4 +- common/models/change.js | 4 +- common/models/email.js | 6 +- common/models/user.js | 67 +++++++++-------- example/client-server/client.js | 6 +- example/colors/app.js | 4 +- example/context/app.js | 6 +- example/mobile-models/app.js | 15 ++-- example/simple-data-source/app.js | 4 +- index.js | 3 + intl/en/messages.json | 115 ++++++++++++++++++++++++++++++ lib/application.js | 26 +++---- lib/connectors/mail.js | 18 ++--- lib/model.js | 89 +++++++++++++---------- lib/persisted-model.js | 92 ++++++++++++------------ lib/registry.js | 29 ++++---- lib/server-app.js | 4 +- package.json | 1 + 20 files changed, 336 insertions(+), 165 deletions(-) create mode 100644 intl/en/messages.json diff --git a/browser/current-context.js b/browser/current-context.js index 97d4a1a70..9963a528b 100644 --- a/browser/current-context.js +++ b/browser/current-context.js @@ -3,6 +3,8 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +var g = require('strong-globalize')(); + module.exports = function(loopback) { loopback.getCurrentContext = function() { return null; @@ -10,6 +12,6 @@ module.exports = function(loopback) { loopback.runInContext = loopback.createContext = function() { - throw new Error('Current context is not supported in the browser.'); + throw new Error(g.f('Current context is not supported in the browser.')); }; }; diff --git a/common/models/access-token.js b/common/models/access-token.js index 750c21f8b..ae75051f3 100644 --- a/common/models/access-token.js +++ b/common/models/access-token.js @@ -7,6 +7,8 @@ * Module Dependencies. */ +var g = require('strong-globalize')(); + var loopback = require('../../lib/loopback'); var assert = require('assert'); var uid = require('uid2'); @@ -112,7 +114,7 @@ module.exports = function(AccessToken) { } else if (isValid) { cb(null, token); } else { - var e = new Error('Invalid Access Token'); + var e = new Error(g.f('Invalid Access Token')); e.status = e.statusCode = 401; e.code = 'INVALID_TOKEN'; cb(e); diff --git a/common/models/acl.js b/common/models/acl.js index 6bde460c0..e9340d76b 100644 --- a/common/models/acl.js +++ b/common/models/acl.js @@ -36,6 +36,8 @@ */ +var g = require('strong-globalize')(); + var loopback = require('../../lib/loopback'); var async = require('async'); var assert = require('assert'); @@ -535,7 +537,7 @@ module.exports = function(ACL) { break; default: process.nextTick(function() { - var err = new Error('Invalid principal type: ' + type); + var err = new Error(g.f('Invalid principal type: %s', type)); err.statusCode = 400; cb(err); }); diff --git a/common/models/change.js b/common/models/change.js index ad19a7542..cf47082dc 100644 --- a/common/models/change.js +++ b/common/models/change.js @@ -7,6 +7,8 @@ * Module Dependencies. */ +var g = require('strong-globalize')(); + var PersistedModel = require('../../lib/loopback').PersistedModel; var loopback = require('../../lib/loopback'); var utils = require('../../lib/utils'); @@ -112,7 +114,7 @@ module.exports = function(Change) { }) .join('\n'); - var msg = 'Cannot rectify ' + modelName + ' changes:\n' + desc; + var msg = g.f('Cannot rectify %s changes:\n%s', modelName, desc); err = new Error(msg); err.details = { errors: errors }; return callback(err); diff --git a/common/models/email.js b/common/models/email.js index 6a6736dc6..8ead34aa6 100644 --- a/common/models/email.js +++ b/common/models/email.js @@ -15,6 +15,8 @@ * @inherits {Model} */ +var g = require('strong-globalize')(); + module.exports = function(Email) { /** @@ -44,13 +46,13 @@ module.exports = function(Email) { */ Email.send = function() { - throw new Error('You must connect the Email Model to a Mail connector'); + throw new Error(g.f('You must connect the {{Email}} Model to a {{Mail}} connector')); }; /** * A shortcut for Email.send(this). */ Email.prototype.send = function() { - throw new Error('You must connect the Email Model to a Mail connector'); + throw new Error(g.f('You must connect the {{Email}} Model to a {{Mail}} connector')); }; }; diff --git a/common/models/user.js b/common/models/user.js index 4d9292a22..90a17c561 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -7,6 +7,8 @@ * Module Dependencies. */ +var g = require('strong-globalize')(); + var loopback = require('../../lib/loopback'); var utils = require('../../lib/utils'); var path = require('path'); @@ -205,14 +207,14 @@ module.exports = function(User) { realmDelimiter); if (realmRequired && !query.realm) { - var err1 = new Error('realm is required'); + var err1 = new Error(g.f('{{realm}} is required')); err1.statusCode = 400; err1.code = 'REALM_REQUIRED'; fn(err1); return fn.promise; } if (!query.email && !query.username) { - var err2 = new Error('username or email is required'); + var err2 = new Error(g.f('{{username}} or {{email}} is required')); err2.statusCode = 400; err2.code = 'USERNAME_EMAIL_REQUIRED'; fn(err2); @@ -220,7 +222,7 @@ module.exports = function(User) { } self.findOne({where: query}, function(err, user) { - var defaultError = new Error('login failed'); + var defaultError = new Error(g.f('login failed')); defaultError.statusCode = 401; defaultError.code = 'LOGIN_FAILED'; @@ -250,7 +252,7 @@ module.exports = function(User) { if (self.settings.emailVerificationRequired && !user.emailVerified) { // Fail to log in if email verification is not done yet debug('User email has not been verified'); - err = new Error('login failed as the email has not been verified'); + err = new Error(g.f('login failed as the email has not been verified')); err.statusCode = 401; err.code = 'LOGIN_FAILED_EMAIL_NOT_VERIFIED'; fn(err); @@ -296,7 +298,7 @@ module.exports = function(User) { } else if (accessToken) { accessToken.destroy(fn); } else { - fn(new Error('could not find accessToken')); + fn(new Error(g.f('could not find {{accessToken}}'))); } }); return fn.promise; @@ -438,15 +440,21 @@ module.exports = function(User) { options.text = options.text.replace(/\{href\}/g, options.verifyHref); + options.text = g.f(options.text); + options.to = options.to || user.email; options.subject = options.subject || 'Thanks for Registering'; + options.subject = g.f(options.subject); + options.headers = options.headers || {}; var template = loopback.template(options.template); options.html = template(options); + options.html = g.f(options.html); + Email.send(options, function(err, email) { if (err) { fn(err); @@ -501,11 +509,11 @@ module.exports = function(User) { }); } else { if (user) { - err = new Error('Invalid token: ' + token); + err = new Error(g.f('Invalid token: %s', token)); err.statusCode = 400; err.code = 'INVALID_TOKEN'; } else { - err = new Error('User not found: ' + uid); + err = new Error(g.f('User not found: %s', uid)); err.statusCode = 404; err.code = 'USER_NOT_FOUND'; } @@ -533,7 +541,7 @@ module.exports = function(User) { options = options || {}; if (typeof options.email !== 'string') { - var err = new Error('Email is required'); + var err = new Error(g.f('Email is required')); err.statusCode = 400; err.code = 'EMAIL_REQUIRED'; cb(err); @@ -545,7 +553,7 @@ module.exports = function(User) { return cb(err); } if (!user) { - err = new Error('Email not found'); + err = new Error(g.f('Email not found')); err.statusCode = 404; err.code = 'EMAIL_NOT_FOUND'; return cb(err); @@ -581,7 +589,7 @@ module.exports = function(User) { if (typeof plain === 'string' && plain) { return true; } - var err = new Error('Invalid password: ' + plain); + var err = new Error(g.f('Invalid password: %s', plain)); err.statusCode = 422; throw err; }; @@ -640,20 +648,21 @@ module.exports = function(User) { UserModel.remoteMethod( 'login', { - description: 'Login a user with username/email and password.', + description: g.f('Login a user with username/email and password.'), accepts: [ {arg: 'credentials', type: 'object', required: true, http: {source: 'body'}}, - {arg: 'include', type: ['string'], http: {source: 'query' }, - description: 'Related objects to include in the response. ' + - 'See the description of return value for more details.'} + {arg: 'include', type: ['string'], http: {source: 'query'}, + description: g.f('Related objects to include in the response. ' + + 'See the description of return value for more details.') }, ], returns: { arg: 'accessToken', type: 'object', root: true, description: - 'The response body contains properties of the AccessToken created on login.\n' + + g.f('The response body contains properties of the {{AccessToken}} created on login.\n' + 'Depending on the value of `include` parameter, the body may contain ' + 'additional properties:\n\n' + - ' - `user` - `{User}` - Data of the currently logged in user. (`include=user`)\n\n' + ' - `user` - `U+007BUserU+007D` - Data of the currently logged in user. ' + + '{{(`include=user`)}}\n\n'), }, http: {verb: 'post'} } @@ -662,17 +671,17 @@ module.exports = function(User) { UserModel.remoteMethod( 'logout', { - description: 'Logout a user with access token.', + description: g.f('Logout a user with access token.'), accepts: [ {arg: 'access_token', type: 'string', required: true, http: function(ctx) { - var req = ctx && ctx.req; - var accessToken = req && req.accessToken; - var tokenID = accessToken && accessToken.id; - - return tokenID; - }, description: 'Do not supply this argument, it is automatically extracted ' + - 'from request headers.' - }], + var req = ctx && ctx.req; + var accessToken = req && req.accessToken; + var tokenID = accessToken && accessToken.id; + return tokenID; + }, description: g.f('Do not supply this argument, it is automatically extracted ' + + 'from request headers.'), + }, + ], http: {verb: 'all'} } ); @@ -680,7 +689,7 @@ module.exports = function(User) { UserModel.remoteMethod( 'confirm', { - description: 'Confirm a user registration with email verification token.', + description: g.f('Confirm a user registration with email verification token.'), accepts: [ {arg: 'uid', type: 'string', required: true}, {arg: 'token', type: 'string', required: true}, @@ -693,7 +702,7 @@ module.exports = function(User) { UserModel.remoteMethod( 'resetPassword', { - description: 'Reset password for a user with email.', + description: g.f('Reset password for a user with email.'), accepts: [ {arg: 'options', type: 'object', required: true, http: {source: 'body'}} ], @@ -704,7 +713,7 @@ module.exports = function(User) { UserModel.afterRemote('confirm', function(ctx, inst, next) { if (ctx.args.redirect !== undefined) { if (!ctx.res) { - return next(new Error('The transport does not support HTTP redirects.')); + return next(new Error(g.f('The transport does not support HTTP redirects.'))); } ctx.res.location(ctx.args.redirect); ctx.res.status(302); @@ -722,7 +731,7 @@ module.exports = function(User) { // email validation regex var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'}); + UserModel.validatesFormatOf('email', {with: re, message: g.f('Must provide a valid email')}); // FIXME: We need to add support for uniqueness of composite keys in juggler if (!(UserModel.settings.realmRequired || UserModel.settings.realmDelimiter)) { diff --git a/example/client-server/client.js b/example/client-server/client.js index 48c098f5e..63cd4e2e9 100644 --- a/example/client-server/client.js +++ b/example/client-server/client.js @@ -3,6 +3,8 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +var g = require('strong-globalize')(); + var loopback = require('../../'); var client = loopback(); var CartItem = require('./models').CartItem; @@ -16,10 +18,10 @@ CartItem.attachTo(remote); // call the remote method CartItem.sum(1, function(err, total) { - console.log('result:', err || total); + g.log('result:%s', err || total); }); // call a built in remote method CartItem.find(function(err, items) { - console.log(items); + g.log(items); }); diff --git a/example/colors/app.js b/example/colors/app.js index 3e57b373b..b7a3038c2 100644 --- a/example/colors/app.js +++ b/example/colors/app.js @@ -3,6 +3,8 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +var g = require('strong-globalize')(); + var loopback = require('../../'); var app = loopback(); @@ -22,4 +24,4 @@ Color.create({name: 'blue'}); app.listen(3000); -console.log('a list of colors is available at http://localhost:3000/colors'); +g.log('a list of colors is available at {{http://localhost:3000/colors}}'); diff --git a/example/context/app.js b/example/context/app.js index fa35eacca..8cf43e7e7 100644 --- a/example/context/app.js +++ b/example/context/app.js @@ -3,6 +3,8 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +var g = require('strong-globalize')(); + var loopback = require('../../'); var app = loopback(); @@ -22,7 +24,7 @@ var Color = loopback.createModel('color', { 'name': String }); Color.beforeRemote('**', function (ctx, unused, next) { // Inside LoopBack code, you can read the property from the context var ns = loopback.getCurrentContext(); - console.log('Request to host', ns && ns.get('host')); + g.log('Request to host %s', ns && ns.get('host')); next(); }); @@ -30,5 +32,5 @@ app.dataSource('db', { connector: 'memory' }); app.model(Color, { dataSource: 'db' }); app.listen(3000, function() { - console.log('A list of colors is available at http://localhost:3000/colors'); + g.log('A list of colors is available at {{http://localhost:3000/colors}}'); }); diff --git a/example/mobile-models/app.js b/example/mobile-models/app.js index abdf34c15..fe5f4765d 100644 --- a/example/mobile-models/app.js +++ b/example/mobile-models/app.js @@ -3,6 +3,8 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +var g = require('strong-globalize')(); + var models = require('../../lib/models'); var loopback = require('../../'); @@ -37,14 +39,15 @@ var data = {pushSettings: [ ]} Application.create(data, function(err, data) { - console.log('Created: ', data.toObject()); + g.log('Created: %s', data.toObject()); }); -Application.register('rfeng', 'MyApp', {description: 'My first mobile application'}, function (err, result) { - console.log(result.toObject()); +Application.register('rfeng', 'MyApp', { description: g.f('My first mobile application') }, + function(err, result) { + console.log(result.toObject()); - result.resetKeys(function (err, result) { - console.log(result.toObject()); - }); + result.resetKeys(function(err, result) { + console.log(result.toObject()); + }); }); diff --git a/example/simple-data-source/app.js b/example/simple-data-source/app.js index 234baeb05..3074d7b9e 100644 --- a/example/simple-data-source/app.js +++ b/example/simple-data-source/app.js @@ -3,6 +3,8 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +var g = require('strong-globalize')(); + var loopback = require('../../'); var app = loopback(); @@ -25,4 +27,4 @@ Color.all(function () { app.listen(3000); -console.log('a list of colors is available at http://localhost:3000/colors'); +g.log('a list of colors is available at {{http://localhost:3000/colors}}'); diff --git a/index.js b/index.js index bd558efa0..ac439167f 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,9 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +var SG = require('strong-globalize'); +SG.SetRootDir(__dirname, {autonomousMsgLoading: 'all'}); + /** * loopback ~ public api */ diff --git a/intl/en/messages.json b/intl/en/messages.json new file mode 100644 index 000000000..10e29a3ac --- /dev/null +++ b/intl/en/messages.json @@ -0,0 +1,115 @@ +{ + "3b46d3a780fd6ae5f95ade489a0efffe": "Current context is not supported in the browser.", + "7e0fca41d098607e1c9aa353c67e0fa1": "Invalid Access Token", + "320c482401afa1207c04343ab162e803": "Invalid principal type: {0}", + "c2b5d51f007178170ca3952d59640ca4": "Cannot rectify {0} changes:\n{1}", + "5858e63efaa0e4ad86b61c0459ea32fa": "You must connect the {{Email}} Model to a {{Mail}} connector", + "0caffe1d763c8cca6a61814abe33b776": "Email is required", + "1b2a6076dccbe91a56f1672eb3b8598c": "The response body contains properties of the {{AccessToken}} created on login.\nDepending on the value of `include` parameter, the body may contain additional properties:\n\n - `user` - `U+007BUserU+007D` - Data of the currently logged in user. {{(`include=user`)}}\n\n", + "2362ba55796c733e337af169922327a2": "Related objects to include in the response. See the description of return value for more details.", + "306999d39387d87b2638199ff0bed8ad": "Reset password for a user with email.", + "3aae63bb7e8e046641767571c1591441": "login failed as the email has not been verified", + "3caaa84fc103d6d5612173ae6d43b245": "Invalid token: {0}", + "42e3fa18945255412ebc6561e2c6f1dc": "Confirm a user registration with email verification token.", + "430b6326d7ebf6600a39f614ef516bc8": "Do not supply this argument, it is automatically extracted from request headers.", + "44a6c8b1ded4ed653d19ddeaaf89a606": "Email not found", + "5e81ad3847a290dc650b47618b9cbc7e": "login failed", + "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "Login a user with username/email and password.", + "8608c28f5e6df0008266e3c497836176": "Logout a user with access token.", + "860d1a0b8bd340411fb32baa72867989": "The transport does not support HTTP redirects.", + "895b1f941d026870b3cc8e6af087c197": "{{username}} or {{email}} is required", + "a50d10fc6e0959b220e085454c40381e": "User not found: {0}", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} is required", + "c34fa20eea0091747fcc9eda204b8d37": "could not find {{accessToken}}", + "c68a93f0a9524fed4ff64372fc90c55f": "Must provide a valid email", + "f58cdc481540cd1f69a4aa4da2e37981": "Invalid password: {0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "result:{0}", + "10e01c895dc0b2fecc385f9f462f1ca6": "a list of colors is available at {{http://localhost:3000/colors}}", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "Request to host {0}", + "a40684f5a9f546115258b76938d1de37": "A list of colors is available at {{http://localhost:3000/colors}}", + "1e85f822b547a75d7d385048030e4ecb": "Created: {0}", + "7d5e7ed0efaedf3f55f380caae0df8b8": "My first mobile application", + "04bd8af876f001ceaf443aad6a9002f9": "Authentication requires model {0} to be defined.", + "095afbf2f1f0e5be678f5dac5c54e717": "Access Denied", + "2d3071e3b18681c80a090dc0efbdb349": "could not find {0} with id {1}", + "316e5b82c203cf3de31a449ee07d0650": "Expected boolean, got {0}", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Cannot create data source {0}: {1}", + "7e287fc885d9fdcf42da3a12f38572c1": "Authorization Required", + "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} was removed, use the new module {{loopback-boot}} instead", + "1d7833c3ca2f05fdad8fad7537531c40": "\t SUBJECT:{0}", + "275f22ab95671f095640ca99194b7635": "\t FROM:{0}", + "3d63008ccfb2af1db2142e8cc2716ace": "Warning: No email transport specified for sending email. Setup a transport to send mail messages.", + "4a4f04a4e480fc5d4ee73b84d9a4b904": "Sending Mail:", + "63a091ced88001ab6acb58f61ec041c5": "\t TEXT:{0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t TO:{0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORT:{0}", + "0da38687fed24275c1547e815914a8e3": "Delete a related item by id for {0}.", + "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "Criteria to match model instances", + "22fe62fa8d595b72c62208beddaa2a56": "Update a related item by id for {0}.", + "528325f3cbf1b0ab9a08447515daac9a": "Update {0} of this model.", + "543d19bad5e47ee1e9eb8af688e857b4": "Foreign key for {0}.", + "598ff0255ffd1d1b71e8de55dbe2c034": "Check the existence of {0} relation to an item by id.", + "5a36cc6ba0cc27c754f6c5ed6015ea3c": "Remove the {0} relation to an item by id.", + "5fa3afb425819ebde958043e598cb664": "could not find a model with {{id}} {0}", + "61e5deebaf44d68f4e6a508f30cc31a3": "Relation `{0}` does not exist for model `{1}`", + "651f0b3cbba001635152ec3d3d954d0a": "Find a related item by id for {0}.", + "7bc7b301ad9c4fc873029d57fb9740fe": "Queries {0} of {1}.", + "7c837b88fd0e509bd3fc722d7ddf0711": "Foreign key for {0}", + "830cb6c862f8f364e9064cea0026f701": "Fetches hasOne relation {0}.", + "855ecd4a64885ba272d782435f72a4d4": "Unknown \"{0}\" id \"{1}\".", + "86254879d01a60826a851066987703f2": "Add a related item by id for {0}.", + "8ae418c605b6a45f2651be9b1677c180": "Invalid remote method: `{0}`", + "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Fetches belongsTo relation {0}.", + "c0057a569ff9d3b509bac61a4b2f605d": "Deletes all {0} of this model.", + "cd0412f2f33a4a2a316acc834f3f21a6": "must specify an {{id}} or {{data}}", + "d6f43b266533b04d442bdb3955622592": "Creates a new instance in {0} of this model.", + "da13d3cdf21330557254670dddd8c5c7": "Counts {0} of {1}.", + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "Unknown \"{0}\" {{id}} \"{1}\".", + "f66ae3cf379b2fce28575a3282defe1a": "Deletes {0} of this model.", + "03f79fa268fe199de2ce4345515431c1": "No change record found for {0} with id {1}", + "0f1c71f74b040bfbe8d384a414e31f03": "Get a set of deltas and conflicts since the given checkpoint.", + "15254dec061d023d6c030083a0cef50f": "Create a new instance of the model and persist it into the data source.", + "16a11368d55b85a209fc6aea69ee5f7a": "Delete all matching records.", + "1bc1d489ddf347af47af3d9b1fc7cc15": "Run multiple updates at once. Note: this is not atomic.", + "1bc7d8283c9abda512692925c8d8e3c0": "Get the current checkpoint.", + "1caa7cc61266e7aef7db7d2f0e27ac3e": "Update the properties of the most recent change record kept for this instance.", + "2a7df74fe6e8462e617b79d5fbb536ea": "Get the most recent change record for this instance.", + "2a9684b3d5b3b67af74bac74eb1b0843": "Find all instances of the model matched by filter from the data source.", + "2e50838caf0c927735eb15d12866bdd7": "Get the changes to a model since a given checkpoint.Provide a filter object to reduce the number of results returned.", + "4203ab415ec66a78d3164345439ba76e": "Cannot call {0}.{1}(). The {2} method has not been setup. The {{PersistedModel}} has not been correctly attached to a {{DataSource}}!", + "51ea9b6245bb5e672b236d640ca3b048": "An object of Change property name/value pairs", + "55ddedd4c501348f82cb89db02ec85c1": "An object of model property name/value pairs", + "5aaa76c72ae1689fd3cf62589784a4ba": "Update attributes for a model instance and persist it into the data source.", + "5f659bbc15e6e2b249fa33b3879b5f69": "Find a model instance by {{id}} from the data source.", + "62e8b0a733417978bab22c8dacf5d7e6": "Cannot apply bulk updates, the connector does not correctly report the number of updated records.", + "6329e0ac1de3c250ebb1df5afd5a2a7b": "The number of instances updated", + "6bc376432cd9972cf991aad3de371e78": "Missing data for change: {0}", + "79295ac04822d2e9702f0dd1d0240336": "Update instances of the model matched by {{where}} from the data source.", + "7f2fde7f0f860ead224b11ba8d75aa1c": "Create an update list from a delta list.", + "89b57e764c2267129294b07589dbfdc2": "Delete a model instance by {{id}} from the data source.", + "8bab6720ecc58ec6412358c858a53484": "Bulk update failed, the connector has modified unexpected number of records: {0}", + "a98b6cc6547706b5c6bffde0ed5fd55c": "Find first instance of the model matched by filter from the data source.", + "c46d4aba1f14809c16730faa46933495": "Filter defining fields and include", + "c65600640f206f585d300b4bcb699d95": "Create a checkpoint.", + "cf64c7afc74d3a8120abcd028f98c770": "Update an existing model instance or insert a new one into the data source.", + "dcb6261868ff0a7b928aa215b07d068c": "Create a change stream.", + "e43e320a435ec1fa07648c1da0d558a7": "Check whether a model instance exists in the data source.", + "e92aa25b6b864e3454b65a7c422bd114": "Bulk update failed, the connector has deleted unexpected number of records: {0}", + "ea63d226b6968e328bdf6876010786b5": "Cannot apply bulk updates, the connector does not correctly report the number of deleted records.", + "f1d4ac54357cc0932f385d56814ba7e4": "Conflict", + "f37d94653793e33f4203e45b4a1cf106": "Count instances of the model matched by where from the data source.", + "0731d0109e46c21a4e34af3346ed4856": "This behaviour may change in the next major version.", + "2e110abee2c95bcfc2dafd48be7e2095": "Cannot configure {0}: {{config.dataSource}} must be an instance of {{DataSource}}", + "308e1d484516a33df788f873e65faaff": "Model `{0}` is extending deprecated `DataModel. Use `PersistedModel` instead.", + "3438fab56cc7ab92dfd88f0497e523e0": "The relations property of `{0}` configuration must be an object", + "4cac5f051ae431321673e04045d37772": "Model `{0}` is extending an unknown model `{1}`. Using `PersistedModel` as the base.", + "734a7bebb65e10899935126ba63dd51f": "The options property of `{0}` configuration must be an object", + "779467f467862836e19f494a37d6ab77": "The acls property of `{0}` configuration must be an array of objects", + "80a32e80cbed65eba2103201a7c94710": "Model not found: {0}", + "83cbdc2560ba9f09155ccfc63e08f1a1": "Property `{0}` cannot be reconfigured for `{1}`", + "97795efe0c3eb7f35ce8cf8cfe70682b": "The configuration of `{0}` is missing {{`dataSource`}} property.\nUse `null` or `false` to mark models not attached to any data source.", + "a80038252430df2754884bf3c845c4cf": "Remoting metadata for \"{0}.{1}\" is missing \"isStatic\" flag, the method is registered as an instance method.", + "dc568bee32deb0f6eaf63e73b20e8ceb": "Ignoring non-object \"methods\" setting of \"{0}\".", + "3aecb24fa8bdd3f79d168761ca8a6729": "Unknown {{middleware}} phase {0}" +} diff --git a/lib/application.js b/lib/application.js index 1f7b5c64a..f5df78713 100644 --- a/lib/application.js +++ b/lib/application.js @@ -7,6 +7,8 @@ * Module dependencies. */ +var g = require('strong-globalize')(); + var DataSource = require('loopback-datasource-juggler').DataSource; var Registry = require('./registry'); var assert = require('assert'); @@ -237,8 +239,8 @@ app.dataSource = function(name, config) { return ds; } catch (err) { if (err.message) { - err.message = 'Cannot create data source ' + JSON.stringify(name) + - ': ' + err.message; + err.message = g.f('Cannot create data source %s: %s', + JSON.stringify(name), err.message); } throw err; } @@ -322,7 +324,7 @@ app.enableAuth = function(options) { var Model = app.registry.findModel(m); if (!Model) { throw new Error( - 'Authentication requires model ' + m + ' to be defined.'); + g.f('Authentication requires model %s to be defined.', m)); } if (m.dataSource || m.app) return; @@ -378,17 +380,17 @@ app.enableAuth = function(options) { var messages = { 403: { - message: 'Access Denied', - code: 'ACCESS_DENIED' + message: g.f('Access Denied'), + code: 'ACCESS_DENIED', }, 404: { - message: ('could not find ' + modelName + ' with id ' + modelId), - code: 'MODEL_NOT_FOUND' + message: (g.f('could not find %s with id %s', modelName, modelId)), + code: 'MODEL_NOT_FOUND', }, 401: { - message: 'Authorization Required', - code: 'AUTHORIZATION_REQUIRED' - } + message: g.f('Authorization Required'), + code: 'AUTHORIZATION_REQUIRED', + }, }; var e = new Error(messages[errStatusCode].message || messages[403].message); @@ -408,7 +410,7 @@ app.enableAuth = function(options) { app.boot = function(options) { throw new Error( - '`app.boot` was removed, use the new module loopback-boot instead'); + g.f('{{`app.boot`}} was removed, use the new module {{loopback-boot}} instead')); }; function dataSourcesFromConfig(name, config, connectorRegistry, registry) { @@ -486,7 +488,7 @@ function setSharedMethodSharedProperties(model, app, modelConfigs) { var settingValue = settings[setting]; var settingValueType = typeof settingValue; if (settingValueType !== 'boolean') - throw new TypeError('Expected boolean, got ' + settingValueType); + throw new TypeError(g.f('Expected boolean, got %s', settingValueType)); }); // set sharedMethod.shared using the merged settings diff --git a/lib/connectors/mail.js b/lib/connectors/mail.js index 3271c145e..439e29c29 100644 --- a/lib/connectors/mail.js +++ b/lib/connectors/mail.js @@ -7,6 +7,8 @@ * Dependencies. */ +var g = require('strong-globalize')(); + var mailer = require('nodemailer'); var assert = require('assert'); var debug = require('debug')('loopback:connector:mail'); @@ -149,22 +151,22 @@ Mailer.send = function(options, fn) { } if (debug.enabled || settings && settings.debug) { - console.log('Sending Mail:'); + g.log('Sending Mail:'); if (options.transport) { - console.log('\t TRANSPORT:', options.transport); + console.log(g.f('\t TRANSPORT:%s', options.transport)); } - console.log('\t TO:', options.to); - console.log('\t FROM:', options.from); - console.log('\t SUBJECT:', options.subject); - console.log('\t TEXT:', options.text); - console.log('\t HTML:', options.html); + g.log('\t TO:%s', options.to); + g.log('\t FROM:%s', options.from); + g.log('\t SUBJECT:%s', options.subject); + g.log('\t TEXT:%s', options.text); + g.log('\t HTML:%s', options.html); } if (transport) { assert(transport.sendMail, 'You must supply an Email.settings.transports containing a valid transport'); transport.sendMail(options, fn); } else { - console.warn('Warning: No email transport specified for sending email.' + + g.warn('Warning: No email transport specified for sending email.' + ' Setup a transport to send mail messages.'); process.nextTick(function() { fn(null, options); diff --git a/lib/model.js b/lib/model.js index 5ffd82fb5..3b5fe4c4b 100644 --- a/lib/model.js +++ b/lib/model.js @@ -6,6 +6,9 @@ /*! * Module Dependencies. */ + +var g = require('strong-globalize')(); + var assert = require('assert'); var RemoteObjects = require('strong-remoting'); var SharedClass = require('strong-remoting').SharedClass; @@ -168,14 +171,14 @@ module.exports = function(registry) { } else if (model) { fn(null, model); } else { - err = new Error('could not find a model with id ' + id); + err = new Error(g.f('could not find a model with {{id}} %s', id)); err.statusCode = 404; err.code = 'MODEL_NOT_FOUND'; fn(err); } }); } else { - fn(new Error('must specify an id or data')); + fn(new Error(g.f('must specify an {{id}} or {{data}}'))); } }; @@ -449,8 +452,8 @@ module.exports = function(registry) { http: {verb: 'get', path: '/' + pathName}, accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}}, accessType: 'READ', - description: 'Fetches belongsTo relation ' + relationName + '.', - returns: {arg: relationName, type: modelName, root: true} + description: g.f('Fetches belongsTo relation %s.', relationName), + returns: {arg: relationName, type: modelName, root: true}, }, fn); }; @@ -458,7 +461,7 @@ module.exports = function(registry) { if (ctx.result !== null) return cb(); var fk = ctx.getArgByName('fk'); - var msg = 'Unknown "' + toModelName + '" id "' + fk + '".'; + var msg = g.f('Unknown "%s" id "%s".', toModelName, fk); var error = new Error(msg); error.statusCode = error.status = 404; error.code = 'MODEL_NOT_FOUND'; @@ -473,7 +476,7 @@ module.exports = function(registry) { isStatic: false, http: {verb: 'get', path: '/' + pathName}, accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}}, - description: 'Fetches hasOne relation ' + relationName + '.', + description: g.f('Fetches hasOne relation %s.', relationName), accessType: 'READ', returns: {arg: relationName, type: relation.modelTo.modelName, root: true}, rest: {after: convertNullToNotFoundError.bind(null, toModelName)} @@ -483,7 +486,7 @@ module.exports = function(registry) { isStatic: false, http: {verb: 'post', path: '/' + pathName}, accepts: {arg: 'data', type: toModelName, http: {source: 'body'}}, - description: 'Creates a new instance in ' + relationName + ' of this model.', + description: g.f('Creates a new instance in %s of this model.', relationName), accessType: 'WRITE', returns: {arg: 'data', type: toModelName, root: true} }); @@ -492,7 +495,7 @@ module.exports = function(registry) { isStatic: false, http: {verb: 'put', path: '/' + pathName}, accepts: {arg: 'data', type: toModelName, http: {source: 'body'}}, - description: 'Update ' + relationName + ' of this model.', + description: g.f('Update %s of this model.', relationName), accessType: 'WRITE', returns: {arg: 'data', type: toModelName, root: true} }); @@ -500,8 +503,8 @@ module.exports = function(registry) { define('__destroy__' + relationName, { isStatic: false, http: {verb: 'delete', path: '/' + pathName}, - description: 'Deletes ' + relationName + ' of this model.', - accessType: 'WRITE' + description: g.f('Deletes %s of this model.', relationName), + accessType: 'WRITE', }); }; @@ -514,9 +517,10 @@ module.exports = function(registry) { isStatic: false, http: {verb: 'get', path: '/' + pathName + '/:fk'}, accepts: {arg: 'fk', type: 'any', - description: 'Foreign key for ' + relationName, required: true, + description: g.f('Foreign key for %s', relationName), + required: true, http: {source: 'path'}}, - description: 'Find a related item by id for ' + relationName + '.', + description: g.f('Find a related item by id for %s.', relationName), accessType: 'READ', returns: {arg: 'result', type: toModelName, root: true}, rest: {after: convertNullToNotFoundError.bind(null, toModelName)} @@ -526,10 +530,11 @@ module.exports = function(registry) { define('__destroyById__' + relationName, { isStatic: false, http: {verb: 'delete', path: '/' + pathName + '/:fk'}, - accepts: {arg: 'fk', type: 'any', - description: 'Foreign key for ' + relationName, required: true, + accepts: { arg: 'fk', type: 'any', + description: g.f('Foreign key for %s', relationName), + required: true, http: {source: 'path'}}, - description: 'Delete a related item by id for ' + relationName + '.', + description: g.f('Delete a related item by id for %s.', relationName), accessType: 'WRITE', returns: [] }, destroyByIdFunc); @@ -540,11 +545,12 @@ module.exports = function(registry) { http: {verb: 'put', path: '/' + pathName + '/:fk'}, accepts: [ {arg: 'fk', type: 'any', - description: 'Foreign key for ' + relationName, required: true, - http: {source: 'path'}}, - {arg: 'data', type: toModelName, http: {source: 'body'}} + description: g.f('Foreign key for %s', relationName), + required: true, + http: { source: 'path' }}, + {arg: 'data', type: toModelName, http: {source: 'body'}}, ], - description: 'Update a related item by id for ' + relationName + '.', + description: g.f('Update a related item by id for %s.', relationName), accessType: 'WRITE', returns: {arg: 'result', type: toModelName, root: true} }, updateByIdFunc); @@ -562,10 +568,11 @@ module.exports = function(registry) { define('__link__' + relationName, { isStatic: false, http: {verb: 'put', path: '/' + pathName + '/rel/:fk'}, - accepts: [{arg: 'fk', type: 'any', - description: 'Foreign key for ' + relationName, required: true, + accepts: [{ arg: 'fk', type: 'any', + description: g.f('Foreign key for %s', relationName), + required: true, http: {source: 'path'}}].concat(accepts), - description: 'Add a related item by id for ' + relationName + '.', + description: g.f('Add a related item by id for %s.', relationName), accessType: 'WRITE', returns: {arg: relationName, type: modelThrough.modelName, root: true} }, addFunc); @@ -575,9 +582,10 @@ module.exports = function(registry) { isStatic: false, http: {verb: 'delete', path: '/' + pathName + '/rel/:fk'}, accepts: {arg: 'fk', type: 'any', - description: 'Foreign key for ' + relationName, required: true, + description: g.f('Foreign key for %s', relationName), + required: true, http: {source: 'path'}}, - description: 'Remove the ' + relationName + ' relation to an item by id.', + description: g.f('Remove the %s relation to an item by id.', relationName), accessType: 'WRITE', returns: [] }, removeFunc); @@ -589,9 +597,10 @@ module.exports = function(registry) { isStatic: false, http: {verb: 'head', path: '/' + pathName + '/rel/:fk'}, accepts: {arg: 'fk', type: 'any', - description: 'Foreign key for ' + relationName, required: true, + description: g.f('Foreign key for %s', relationName), + required: true, http: {source: 'path'}}, - description: 'Check the existence of ' + relationName + ' relation to an item by id.', + description: g.f('Check the existence of %s relation to an item by id.', relationName), accessType: 'READ', returns: {arg: 'exists', type: 'boolean', root: true}, rest: { @@ -600,7 +609,7 @@ module.exports = function(registry) { if (ctx.result === false) { var modelName = ctx.method.sharedClass.name; var id = ctx.getArgByName('id'); - var msg = 'Unknown "' + modelName + '" id "' + id + '".'; + var msg = g.f('Unknown "%s" {{id}} "%s".', modelName, id); var error = new Error(msg); error.statusCode = error.status = 404; error.code = 'MODEL_NOT_FOUND'; @@ -634,7 +643,7 @@ module.exports = function(registry) { isStatic: isStatic, http: {verb: 'get', path: '/' + pathName}, accepts: {arg: 'filter', type: 'object'}, - description: 'Queries ' + scopeName + ' of ' + this.modelName + '.', + description: g.f('Queries %s of %s.', scopeName, this.modelName), accessType: 'READ', returns: {arg: scopeName, type: [toModelName], root: true} }); @@ -643,7 +652,7 @@ module.exports = function(registry) { isStatic: isStatic, http: {verb: 'post', path: '/' + pathName}, accepts: {arg: 'data', type: toModelName, http: {source: 'body'}}, - description: 'Creates a new instance in ' + scopeName + ' of this model.', + description: g.f('Creates a new instance in %s of this model.', scopeName), accessType: 'WRITE', returns: {arg: 'data', type: toModelName, root: true} }); @@ -651,15 +660,16 @@ module.exports = function(registry) { define('__delete__' + scopeName, { isStatic: isStatic, http: {verb: 'delete', path: '/' + pathName}, - description: 'Deletes all ' + scopeName + ' of this model.', - accessType: 'WRITE' + description: g.f('Deletes all %s of this model.', scopeName), + accessType: 'WRITE', }); define('__count__' + scopeName, { isStatic: isStatic, http: {verb: 'get', path: '/' + pathName + '/count'}, - accepts: {arg: 'where', type: 'object', description: 'Criteria to match model instances'}, - description: 'Counts ' + scopeName + ' of ' + this.modelName + '.', + accepts: {arg: 'where', type: 'object', + description: g.f('Criteria to match model instances')}, + description: g.f('Counts %s of %s.', scopeName, this.modelName), accessType: 'READ', returns: {arg: 'count', type: 'number'} }); @@ -708,9 +718,9 @@ module.exports = function(registry) { acceptArgs = [ { arg: paramName, type: 'any', http: { source: 'path' }, - description: 'Foreign key for ' + relation.name + '.', - required: true - } + description: g.f('Foreign key for %s.', relation.name), + required: true, + }, ]; } else { httpPath = pathName; @@ -738,12 +748,12 @@ module.exports = function(registry) { var getterFn = relation.modelFrom.prototype[getterName]; if (typeof getterFn !== 'function') { - throw new Error('Invalid remote method: `' + getterName + '`'); + throw new Error(g.f('Invalid remote method: `%s`', getterName)); } var nestedFn = relation.modelTo.prototype[method.name]; if (typeof nestedFn !== 'function') { - throw new Error('Invalid remote method: `' + method.name + '`'); + throw new Error(g.f('Invalid remote method: `%s`', method.name)); } var opts = {}; @@ -836,7 +846,8 @@ module.exports = function(registry) { }); } else { - throw new Error('Relation `' + relationName + '` does not exist for model `' + this.modelName + '`'); + var msg = g.f('Relation `%s` does not exist for model `%s`', relationName, this.modelName); + throw new Error(msg); } }; diff --git a/lib/persisted-model.js b/lib/persisted-model.js index a7b8315d4..4ecfa05f4 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -7,6 +7,7 @@ * Module Dependencies. */ +var g = require('strong-globalize')(); var runtime = require('./runtime'); var assert = require('assert'); var async = require('async'); @@ -65,9 +66,10 @@ module.exports = function(registry) { function throwNotAttached(modelName, methodName) { throw new Error( - 'Cannot call ' + modelName + '.' + methodName + '().' + - ' The ' + methodName + ' method has not been setup.' + - ' The PersistedModel has not been correctly attached to a DataSource!' + g.f('Cannot call %s.%s().' + + ' The %s method has not been setup.' + + ' The {{PersistedModel}} has not been correctly attached to a {{DataSource}}!', + modelName, methodName, methodName) ); } @@ -82,7 +84,7 @@ module.exports = function(registry) { var modelName = ctx.method.sharedClass.name; var id = ctx.getArgByName('id'); - var msg = 'Unknown "' + modelName + '" id "' + id + '".'; + var msg = g.f('Unknown "%s" {{id}} "%s".', modelName, id); var error = new Error(msg); error.statusCode = error.status = 404; error.code = 'MODEL_NOT_FOUND'; @@ -558,7 +560,7 @@ module.exports = function(registry) { } setRemoting(PersistedModel, 'create', { - description: 'Create a new instance of the model and persist it into the data source.', + description: g.f('Create a new instance of the model and persist it into the data source.'), accessType: 'WRITE', accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}}, returns: {arg: 'data', type: typeName, root: true}, @@ -567,7 +569,8 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'upsert', { aliases: ['updateOrCreate'], - description: 'Update an existing model instance or insert a new one into the data source.', + description: g.f('Update an existing model instance or insert a new one ' + + 'into the data source.'), accessType: 'WRITE', accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}}, returns: {arg: 'data', type: typeName, root: true}, @@ -575,7 +578,7 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'exists', { - description: 'Check whether a model instance exists in the data source.', + description: g.f('Check whether a model instance exists in the data source.'), accessType: 'READ', accepts: {arg: 'id', type: 'any', description: 'Model id', required: true}, returns: {arg: 'exists', type: 'boolean'}, @@ -606,13 +609,13 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'findById', { - description: 'Find a model instance by id from the data source.', + description: g.f('Find a model instance by {{id}} from the data source.'), accessType: 'READ', accepts: [ { arg: 'id', type: 'any', description: 'Model id', required: true, http: {source: 'path'}}, { arg: 'filter', type: 'object', - description: 'Filter defining fields and include'} + description: g.f('Filter defining fields and include') }, ], returns: {arg: 'data', type: typeName, root: true}, http: {verb: 'get', path: '/:id'}, @@ -620,7 +623,7 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'find', { - description: 'Find all instances of the model matched by filter from the data source.', + description: g.f('Find all instances of the model matched by filter from the data source.'), accessType: 'READ', accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, include, order, offset, and limit'}, returns: {arg: 'data', type: [typeName], root: true}, @@ -628,7 +631,7 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'findOne', { - description: 'Find first instance of the model matched by filter from the data source.', + description: g.f('Find first instance of the model matched by filter from the data source.'), accessType: 'READ', accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, include, order, offset, and limit'}, returns: {arg: 'data', type: typeName, root: true}, @@ -637,7 +640,7 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'destroyAll', { - description: 'Delete all matching records.', + description: g.f('Delete all matching records.'), accessType: 'WRITE', accepts: {arg: 'where', type: 'object', description: 'filter.where object'}, returns: { @@ -652,17 +655,17 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'updateAll', { aliases: ['update'], - description: 'Update instances of the model matched by where from the data source.', + description: g.f('Update instances of the model matched by {{where}} from the data source.'), accessType: 'WRITE', accepts: [ - {arg: 'where', type: 'object', http: {source: 'query'}, - description: 'Criteria to match model instances'}, + {arg: 'where', type: 'object', http: { source: 'query'}, + description: g.f('Criteria to match model instances')}, {arg: 'data', type: 'object', http: {source: 'body'}, - description: 'An object of model property name/value pairs'}, + description: g.f('An object of model property name/value pairs')}, ], returns: { arg: 'count', - description: 'The number of instances updated', + description: g.f('The number of instances updated'), type: 'object', root: true }, @@ -671,7 +674,7 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'deleteById', { aliases: ['destroyById', 'removeById'], - description: 'Delete a model instance by id from the data source.', + description: g.f('Delete a model instance by {{id}} from the data source.'), accessType: 'WRITE', accepts: {arg: 'id', type: 'any', description: 'Model id', required: true, http: {source: 'path'}}, @@ -680,7 +683,7 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'count', { - description: 'Count instances of the model matched by where from the data source.', + description: g.f('Count instances of the model matched by where from the data source.'), accessType: 'READ', accepts: {arg: 'where', type: 'object', description: 'Criteria to match model instances'}, returns: {arg: 'count', type: 'number'}, @@ -688,7 +691,8 @@ module.exports = function(registry) { }); setRemoting(PersistedModel.prototype, 'updateAttributes', { - description: 'Update attributes for a model instance and persist it into the data source.', + description: g.f('Update attributes for a model instance and persist it into ' + + 'the data source.'), accessType: 'WRITE', accepts: {arg: 'data', type: 'object', http: {source: 'body'}, description: 'An object of model property name/value pairs'}, returns: {arg: 'data', type: typeName, root: true}, @@ -697,7 +701,7 @@ module.exports = function(registry) { if (options.trackChanges || options.enableRemoteReplication) { setRemoting(PersistedModel, 'diff', { - description: 'Get a set of deltas and conflicts since the given checkpoint.', + description: g.f('Get a set of deltas and conflicts since the given checkpoint.'), accessType: 'READ', accepts: [ {arg: 'since', type: 'number', description: 'Find deltas since this checkpoint'}, @@ -709,8 +713,8 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'changes', { - description: 'Get the changes to a model since a given checkpoint.' + - 'Provide a filter object to reduce the number of results returned.', + description: g.f('Get the changes to a model since a given checkpoint.' + + 'Provide a filter object to reduce the number of results returned.'), accessType: 'READ', accepts: [ {arg: 'since', type: 'number', description: 'Only return changes since this checkpoint'}, @@ -721,7 +725,7 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'checkpoint', { - description: 'Create a checkpoint.', + description: g.f('Create a checkpoint.'), // The replication algorithm needs to create a source checkpoint, // even though it is otherwise not making any source changes. // We need to allow this method for users that don't have full @@ -732,14 +736,14 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'currentCheckpoint', { - description: 'Get the current checkpoint.', + description: g.f('Get the current checkpoint.'), accessType: 'READ', returns: {arg: 'checkpoint', type: 'object', root: true}, http: {verb: 'get', path: '/checkpoint'} }); setRemoting(PersistedModel, 'createUpdates', { - description: 'Create an update list from a delta list.', + description: g.f('Create an update list from a delta list.'), // This operation is read-only, it does not change any local data. // It is called by the replication algorithm to compile a list // of changes to apply on the target. @@ -750,14 +754,14 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'bulkUpdate', { - description: 'Run multiple updates at once. Note: this is not atomic.', + description: g.f('Run multiple updates at once. Note: this is not atomic.'), accessType: 'WRITE', accepts: {arg: 'updates', type: 'array'}, http: {verb: 'post', path: '/bulk-update'} }); setRemoting(PersistedModel, 'findLastChange', { - description: 'Get the most recent change record for this instance.', + description: g.f('Get the most recent change record for this instance.'), accessType: 'READ', accepts: { arg: 'id', type: 'any', required: true, http: { source: 'path' }, @@ -769,8 +773,8 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'updateLastChange', { description: [ - 'Update the properties of the most recent change record', - 'kept for this instance.' + g.f('Update the properties of the most recent change record ' + + 'kept for this instance.'), ], accessType: 'WRITE', accepts: [ @@ -780,7 +784,7 @@ module.exports = function(registry) { }, { arg: 'data', type: 'object', http: {source: 'body'}, - description: 'An object of Change property name/value pairs' + description: g.f('An object of Change property name/value pairs'), }, ], returns: { arg: 'result', type: this.Change.modelName, root: true }, @@ -806,7 +810,7 @@ module.exports = function(registry) { } setRemoting(PersistedModel, 'createChangeStream', { - description: 'Create a change stream.', + description: g.f('Create a change stream.'), accessType: 'READ', http: [ {verb: 'post', path: '/change-stream'}, @@ -1148,7 +1152,7 @@ module.exports = function(registry) { if (err) return cb(err); if (!inst) { return cb && - cb(new Error('Missing data for change: ' + change.modelId)); + cb(new Error(g.f('Missing data for change: %s', change.modelId))); } if (inst.toObject) { update.data = inst.toObject(); @@ -1216,7 +1220,7 @@ module.exports = function(registry) { async.parallel(tasks, function(err) { if (err) return callback(err); if (conflicts.length) { - err = new Error('Conflict'); + err = new Error(g.f('Conflict')); err.statusCode = 409; err.details = { conflicts: conflicts }; return callback(err); @@ -1280,16 +1284,16 @@ module.exports = function(registry) { case undefined: case null: return cb(new Error( - 'Cannot apply bulk updates, ' + + g.f('Cannot apply bulk updates, ' + 'the connector does not correctly report ' + - 'the number of updated records.')); + 'the number of updated records.'))); default: debug('%s.updateAll modified unexpected number of instances: %j', Model.modelName, count); return cb(new Error( - 'Bulk update failed, the connector has modified unexpected ' + - 'number of records: ' + JSON.stringify(count))); + g.f('Bulk update failed, the connector has modified unexpected ' + + 'number of records: %s', JSON.stringify(count)))); } }); } @@ -1362,16 +1366,16 @@ module.exports = function(registry) { case undefined: case null: return cb(new Error( - 'Cannot apply bulk updates, ' + + g.f('Cannot apply bulk updates, ' + 'the connector does not correctly report ' + - 'the number of deleted records.')); + 'the number of deleted records.'))); default: debug('%s.deleteAll modified unexpected number of instances: %j', Model.modelName, count); return cb(new Error( - 'Bulk update failed, the connector has deleted unexpected ' + - 'number of records: ' + JSON.stringify(count))); + g.f('Bulk update failed, the connector has deleted unexpected ' + + 'number of records: %s', JSON.stringify(count)))); } }); } @@ -1586,8 +1590,8 @@ module.exports = function(registry) { this.findLastChange(id, function(err, inst) { if (err) return cb(err); if (!inst) { - err = new Error('No change record found for ' + - self.modelName + ' with id ' + id); + err = new Error(g.f('No change record found for %s with id %s', + self.modelName, id)); err.statusCode = 404; return cb(err); } diff --git a/lib/registry.js b/lib/registry.js index ccce6248f..c3d976210 100644 --- a/lib/registry.js +++ b/lib/registry.js @@ -3,6 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +var g = require('strong-globalize')(); var assert = require('assert'); var extend = require('util')._extend; var juggler = require('loopback-datasource-juggler'); @@ -113,11 +114,11 @@ Registry.prototype.createModel = function(name, properties, options) { if (BaseModel === undefined) { if (baseName === 'DataModel') { - console.warn('Model `%s` is extending deprecated `DataModel. ' + + g.warn('Model `%s` is extending deprecated `DataModel. ' + 'Use `PersistedModel` instead.', name); BaseModel = this.getModel('PersistedModel'); } else { - console.warn('Model `%s` is extending an unknown model `%s`. ' + + g.warn('Model `%s` is extending an unknown model `%s`. ' + 'Using `PersistedModel` as the base.', name, baseName); } } @@ -197,7 +198,7 @@ Registry.prototype.configureModel = function(ModelCtor, config) { relations[key] = extend(relations[key] || {}, config.relations[key]); }); } else if (config.relations != null) { - console.warn('The relations property of `%s` configuration ' + + g.warn('The relations property of `%s` configuration ' + 'must be an object', modelName); } @@ -208,7 +209,7 @@ Registry.prototype.configureModel = function(ModelCtor, config) { addACL(acls, acl); }); } else if (config.acls != null) { - console.warn('The acls property of `%s` configuration ' + + g.warn('The acls property of `%s` configuration ' + 'must be an array of objects', modelName); } @@ -225,12 +226,12 @@ Registry.prototype.configureModel = function(ModelCtor, config) { if (!(p in excludedProperties)) { settings[p] = config.options[p]; } else { - console.warn('Property `%s` cannot be reconfigured for `%s`', + g.warn('Property `%s` cannot be reconfigured for `%s`', p, modelName); } } } else if (config.options != null) { - console.warn('The options property of `%s` configuration ' + + g.warn('The options property of `%s` configuration ' + 'must be an object', modelName); } @@ -238,8 +239,8 @@ Registry.prototype.configureModel = function(ModelCtor, config) { // configuration, so that the datasource picks up updated relations if (config.dataSource) { assert(config.dataSource instanceof DataSource, - 'Cannot configure ' + ModelCtor.modelName + - ': config.dataSource must be an instance of DataSource'); + g.f('Cannot configure %s: {{config.dataSource}} must be an instance ' + + 'of {{DataSource}}', ModelCtor.modelName)); ModelCtor.attachTo(config.dataSource); debug('Attached model `%s` to dataSource `%s`', modelName, config.dataSource.name); @@ -249,8 +250,8 @@ Registry.prototype.configureModel = function(ModelCtor, config) { } else { debug('Model `%s` is not attached to any DataSource, possibly by a mistake.', modelName); - console.warn( - 'The configuration of `%s` is missing `dataSource` property.\n' + + g.warn( + 'The configuration of `%s` is missing {{`dataSource`}} property.\n' + 'Use `null` or `false` to mark models not attached to any data source.', modelName); } @@ -262,7 +263,7 @@ Registry.prototype.configureModel = function(ModelCtor, config) { Registry.prototype._defineRemoteMethods = function(ModelCtor, methods) { if (!methods) return; if (typeof methods !== 'object') { - console.warn('Ignoring non-object "methods" setting of "%s".', + g.warn('Ignoring non-object "methods" setting of "%s".', ModelCtor.modelName); return; } @@ -270,11 +271,11 @@ Registry.prototype._defineRemoteMethods = function(ModelCtor, methods) { Object.keys(methods).forEach(function(key) { var meta = methods[key]; if (typeof meta.isStatic !== 'boolean') { - console.warn('Remoting metadata for "%s.%s" is missing "isStatic" ' + + g.warn('Remoting metadata for "%s.%s" is missing "isStatic" ' + 'flag, the method is registered as an instance method.', ModelCtor.modelName, key); - console.warn('This behaviour may change in the next major version.'); + g.warn('This behaviour may change in the next major version.'); } ModelCtor.remoteMethod(key, meta); }); @@ -306,7 +307,7 @@ Registry.prototype.getModel = function(modelName) { var model = this.findModel(modelName); if (model) return model; - throw new Error('Model not found: ' + modelName); + throw new Error(g.f('Model not found: %s', modelName)); }; /** diff --git a/lib/server-app.js b/lib/server-app.js index 290c9b5ac..f6b7b1257 100644 --- a/lib/server-app.js +++ b/lib/server-app.js @@ -3,6 +3,8 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +var g = require('strong-globalize')(); + var assert = require('assert'); var express = require('express'); var merge = require('util')._extend; @@ -188,7 +190,7 @@ proto.middleware = function(name, paths, handler) { } if (this._requestHandlingPhases.indexOf(name) === -1) - throw new Error('Unknown middleware phase ' + name); + throw new Error(g.f('Unknown {{middleware}} phase %s', name)); debug('use %s %s %s', fullPhaseName, paths, handlerName); diff --git a/package.json b/package.json index bef100d67..c4534e18a 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "nodemailer-stub-transport": "^1.0.0", "serve-favicon": "^2.2.0", "stable": "^0.1.5", + "strong-globalize": "^2.6.2", "strong-remoting": "^2.21.0", "uid2": "0.0.3", "underscore.string": "^3.0.3" From 81318e603da65ed337cf2a3816801d4238538b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 8 Aug 2016 15:12:56 +0200 Subject: [PATCH 059/187] test: increase timeout to prevent CI failures [back-port of #2591] --- test/remote-connector.test.js | 2 ++ test/replication.rest.test.js | 2 ++ test/replication.test.js | 2 ++ test/role.test.js | 3 ++- 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/test/remote-connector.test.js b/test/remote-connector.test.js index 88023ddd9..dd058da60 100644 --- a/test/remote-connector.test.js +++ b/test/remote-connector.test.js @@ -7,6 +7,8 @@ var loopback = require('../'); var defineModelTestsWithDataSource = require('./util/model-tests'); describe('RemoteConnector', function() { + this.timeout(10000); + var remoteApp; var remote; diff --git a/test/replication.rest.test.js b/test/replication.rest.test.js index eb963e7b7..9c436b2f4 100644 --- a/test/replication.rest.test.js +++ b/test/replication.rest.test.js @@ -11,6 +11,8 @@ var expect = require('chai').expect; var supertest = require('supertest'); describe('Replication over REST', function() { + this.timeout(10000); + var ALICE = { id: 'a', username: 'alice', email: 'a@t.io', password: 'p' }; var PETER = { id: 'p', username: 'peter', email: 'p@t.io', password: 'p' }; var EMERY = { id: 'e', username: 'emery', email: 'e@t.io', password: 'p' }; diff --git a/test/replication.test.js b/test/replication.test.js index 83a88eb98..13e8b0d4a 100644 --- a/test/replication.test.js +++ b/test/replication.test.js @@ -13,6 +13,8 @@ var expect = require('chai').expect; var debug = require('debug')('test'); describe('Replication / Change APIs', function() { + this.timeout(10000); + var dataSource, SourceModel, TargetModel; var useSinceFilter; var tid = 0; // per-test unique id used e.g. to build unique model names diff --git a/test/role.test.js b/test/role.test.js index 83b82f331..1b1112f24 100644 --- a/test/role.test.js +++ b/test/role.test.js @@ -15,6 +15,8 @@ function checkResult(err, result) { } describe('role model', function() { + this.timeout(10000); + var app, Role, RoleMapping, User, Application, ACL; beforeEach(function() { @@ -419,7 +421,6 @@ describe('role model', function() { var user, app, role; beforeEach(function(done) { - this.timeout(5000); User.create({ username: 'john', email: 'john@gmail.com', From ca28e7ff9e46f8992f945d4198e68628e4946f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 28 Jul 2016 16:36:23 +0200 Subject: [PATCH 060/187] Deprecate current-context API Deprecate all current-context APIs in favour of loopback-context-cls. --- Gruntfile.js | 4 - browser/current-context.js | 17 ----- lib/current-context.js | 77 +++++++++++++++++++ lib/loopback.js | 2 +- package.json | 3 +- server/current-context.js | 143 ----------------------------------- server/middleware/context.js | 61 +++------------ test/access-token.test.js | 3 +- 8 files changed, 90 insertions(+), 220 deletions(-) delete mode 100644 browser/current-context.js create mode 100644 lib/current-context.js delete mode 100644 server/current-context.js diff --git a/Gruntfile.js b/Gruntfile.js index 756bede3a..25b0a880b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -44,9 +44,6 @@ module.exports = function(grunt) { common: { src: ['common/**/*.js'] }, - browser: { - src: ['browser/**/*.js'] - }, server: { src: ['server/**/*.js'] }, @@ -59,7 +56,6 @@ module.exports = function(grunt) { lib: ['lib/**/*.js'], common: ['common/**/*.js'], server: ['server/**/*.js'], - browser: ['browser/**/*.js'], test: ['test/**/*.js'] }, watch: { diff --git a/browser/current-context.js b/browser/current-context.js deleted file mode 100644 index 9963a528b..000000000 --- a/browser/current-context.js +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright IBM Corp. 2015,2016. All Rights Reserved. -// Node module: loopback -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -var g = require('strong-globalize')(); - -module.exports = function(loopback) { - loopback.getCurrentContext = function() { - return null; - }; - - loopback.runInContext = - loopback.createContext = function() { - throw new Error(g.f('Current context is not supported in the browser.')); - }; -}; diff --git a/lib/current-context.js b/lib/current-context.js new file mode 100644 index 000000000..b654bdc90 --- /dev/null +++ b/lib/current-context.js @@ -0,0 +1,77 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +var juggler = require('loopback-datasource-juggler'); +var remoting = require('strong-remoting'); +var LoopBackContext = require('loopback-context'); +var deprecated = require('depd')('loopback'); + +module.exports = function(loopback) { + + /** + * Get the current context object. The context is preserved + * across async calls, it behaves like a thread-local storage. + * + * @returns {ChainedContext} The context object or null. + */ + loopback.getCurrentContext = function() { + // NOTE(bajtos) LoopBackContext.getCurrentContext is overriden whenever + // the context changes, therefore we cannot simply assign + // LoopBackContext.getCurrentContext() to loopback.getCurrentContext() + deprecated('loopback.getCurrentContext() is deprecated. See ' + + 'https://docs.strongloop.com/display/APIC/Using%20current%20context' + + 'for more details.'); + return LoopBackContext.getCurrentContext(); + }; + + juggler.getCurrentContext = + remoting.getCurrentContext = loopback.getCurrentContext; + + /** + * Run the given function in such way that + * `loopback.getCurrentContext` returns the + * provided context object. + * + * **NOTE** + * + * The method is supported on the server only, it does not work + * in the browser at the moment. + * + * @param {Function} fn The function to run, it will receive arguments + * (currentContext, currentDomain). + * @param {ChainedContext} context An optional context object. + * When no value is provided, then the default global context is used. + */ + loopback.runInContext = function(fn, ctx) { + deprecated('loopback.runInContext() is deprecated. See ' + + 'https://docs.strongloop.com/display/APIC/Using%20current%20context' + + 'for more details.'); + return LoopBackContext.runInContext(fn, ctx); + }; + + /** + * Create a new LoopBackContext instance that can be used + * for `loopback.runInContext`. + * + * **NOTES** + * + * At the moment, `loopback.getCurrentContext` supports + * a single global context instance only. If you call `createContext()` + * multiple times, `getCurrentContext` will return the last context + * created. + * + * The method is supported on the server only, it does not work + * in the browser at the moment. + * + * @param {String} scopeName An optional scope name. + * @return {ChainedContext} The new context object. + */ + loopback.createContext = function(scopeName) { + deprecated('loopback.createContext() is deprecated. See ' + + 'https://docs.strongloop.com/display/APIC/Using%20current%20context' + + 'for more details.'); + return LoopBackContext.createContext(scopeName); + }; +}; diff --git a/lib/loopback.js b/lib/loopback.js index 656db41c2..3b3555393 100644 --- a/lib/loopback.js +++ b/lib/loopback.js @@ -222,7 +222,7 @@ loopback.template = function(file) { }); }; -require('../server/current-context')(loopback); +require('../lib/current-context')(loopback); /** * Create a named vanilla JavaScript class constructor with an attached diff --git a/package.json b/package.json index c4534e18a..f17cf8c01 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "bcryptjs": "^2.1.0", "body-parser": "^1.12.0", "canonical-json": "0.0.4", - "continuation-local-storage": "^3.1.3", "cookie-parser": "^1.3.4", "debug": "^2.1.2", "depd": "^1.0.0", @@ -45,6 +44,7 @@ "express": "^4.12.2", "inflection": "^1.6.0", "loopback-connector-remote": "^1.0.3", + "loopback-context": "^1.0.0", "loopback-phase": "^1.2.0", "nodemailer": "^2.5.0", "nodemailer-stub-transport": "^1.0.0", @@ -100,7 +100,6 @@ "browser": { "express": "./lib/browser-express.js", "./lib/server-app.js": "./lib/browser-express.js", - "./server/current-context.js": "./browser/current-context.js", "connect": false, "nodemailer": false, "supertest": false, diff --git a/server/current-context.js b/server/current-context.js deleted file mode 100644 index 4da00bf4c..000000000 --- a/server/current-context.js +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright IBM Corp. 2015,2016. All Rights Reserved. -// Node module: loopback -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -var juggler = require('loopback-datasource-juggler'); -var remoting = require('strong-remoting'); -var cls = require('continuation-local-storage'); -var domain = require('domain'); - -module.exports = function(loopback) { - - /** - * Get the current context object. The context is preserved - * across async calls, it behaves like a thread-local storage. - * - * @returns {ChainedContext} The context object or null. - */ - loopback.getCurrentContext = function() { - // A placeholder method, see loopback.createContext() for the real version - return null; - }; - - /** - * Run the given function in such way that - * `loopback.getCurrentContext` returns the - * provided context object. - * - * **NOTE** - * - * The method is supported on the server only, it does not work - * in the browser at the moment. - * - * @param {Function} fn The function to run, it will receive arguments - * (currentContext, currentDomain). - * @param {ChainedContext} context An optional context object. - * When no value is provided, then the default global context is used. - */ - loopback.runInContext = function(fn, context) { - var currentDomain = domain.create(); - currentDomain.oldBind = currentDomain.bind; - currentDomain.bind = function(callback, context) { - return currentDomain.oldBind(ns.bind(callback, context), context); - }; - - var ns = context || loopback.createContext('loopback'); - - currentDomain.run(function() { - ns.run(function executeInContext(context) { - fn(ns, currentDomain); - }); - }); - }; - - /** - * Create a new LoopBackContext instance that can be used - * for `loopback.runInContext`. - * - * **NOTES** - * - * At the moment, `loopback.getCurrentContext` supports - * a single global context instance only. If you call `createContext()` - * multiple times, `getCurrentContext` will return the last context - * created. - * - * The method is supported on the server only, it does not work - * in the browser at the moment. - * - * @param {String} scopeName An optional scope name. - * @return {ChainedContext} The new context object. - */ - loopback.createContext = function(scopeName) { - // Make the namespace globally visible via the process.context property - process.context = process.context || {}; - var ns = process.context[scopeName]; - if (!ns) { - ns = cls.createNamespace(scopeName); - process.context[scopeName] = ns; - // Set up loopback.getCurrentContext() - loopback.getCurrentContext = function() { - return ns && ns.active ? ns : null; - }; - - chain(juggler); - chain(remoting); - } - return ns; - }; - - /** - * Create a chained context - * @param {Object} child The child context - * @param {Object} parent The parent context - * @private - * @constructor - */ - function ChainedContext(child, parent) { - this.child = child; - this.parent = parent; - } - - /** - * Get the value by name from the context. If it doesn't exist in the child - * context, try the parent one - * @param {String} name Name of the context property - * @returns {*} Value of the context property - * @private - */ - ChainedContext.prototype.get = function(name) { - var val = this.child && this.child.get(name); - if (val === undefined) { - return this.parent && this.parent.get(name); - } - }; - - ChainedContext.prototype.set = function(name, val) { - if (this.child) { - return this.child.set(name, val); - } else { - return this.parent && this.parent.set(name, val); - } - }; - - ChainedContext.prototype.reset = function(name, val) { - if (this.child) { - return this.child.reset(name, val); - } else { - return this.parent && this.parent.reset(name, val); - } - }; - - function chain(child) { - if (typeof child.getCurrentContext === 'function') { - var childContext = new ChainedContext(child.getCurrentContext(), - loopback.getCurrentContext()); - child.getCurrentContext = function() { - return childContext; - }; - } else { - child.getCurrentContext = loopback.getCurrentContext; - } - } -}; diff --git a/server/middleware/context.js b/server/middleware/context.js index 73948bd76..1bbb87cdd 100644 --- a/server/middleware/context.js +++ b/server/middleware/context.js @@ -3,55 +3,12 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -var loopback = require('../../lib/loopback'); - -module.exports = context; - -var name = 'loopback'; - -/** - * Context middleware. - * ```js - * var app = loopback(); - * app.use(loopback.context(options); - * app.use(loopback.rest()); - * app.listen(); - * ``` - * @options {Object} [options] Options for context - * @property {String} name Context scope name. - * @property {Boolean} enableHttpContext Whether HTTP context is enabled. Default is false. - * @header loopback.context([options]) - */ - -function context(options) { - options = options || {}; - var scope = options.name || name; - var enableHttpContext = options.enableHttpContext || false; - var ns = loopback.createContext(scope); - - // Return the middleware - return function contextHandler(req, res, next) { - if (req.loopbackContext) { - return next(); - } - - loopback.runInContext(function processRequestInContext(ns, domain) { - req.loopbackContext = ns; - - // Bind req/res event emitters to the given namespace - ns.bindEmitter(req); - ns.bindEmitter(res); - - // Add req/res event emitters to the current domain - domain.add(req); - domain.add(res); - - // Run the code in the context of the namespace - if (enableHttpContext) { - // Set up the transport context - ns.set('http', {req: req, res: res}); - } - next(); - }); - }; -} +var deprecated = require('depd')('loopback'); +var perRequestContext = require('loopback-context').perRequest; + +module.exports = function() { + deprecated('loopback#context middleware is deprecated. See ' + + 'https://docs.strongloop.com/display/APIC/Using%20current%20context' + + 'for more details.'); + return perRequestContext.apply(this, arguments); +}; diff --git a/test/access-token.test.js b/test/access-token.test.js index 690b1953d..aec2b86a1 100644 --- a/test/access-token.test.js +++ b/test/access-token.test.js @@ -477,7 +477,8 @@ describe('app.enableAuth()', function() { it('stores token in the context', function(done) { var TestModel = loopback.createModel('TestModel', { base: 'Model' }); TestModel.getToken = function(cb) { - cb(null, loopback.getCurrentContext().get('accessToken') || null); + var ctx = loopback.getCurrentContext(); + cb(null, ctx && ctx.get('accessToken') || null); }; TestModel.remoteMethod('getToken', { returns: { arg: 'token', type: 'object' }, From b08a1cfba3248e094fa1aa7143c8224690fd067b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 10 Aug 2016 12:40:38 +0200 Subject: [PATCH 061/187] Globalize current-context deprecation messages --- lib/current-context.js | 19 ++++++++++--------- server/middleware/context.js | 7 ++++--- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/current-context.js b/lib/current-context.js index b654bdc90..185b7b16e 100644 --- a/lib/current-context.js +++ b/lib/current-context.js @@ -7,6 +7,7 @@ var juggler = require('loopback-datasource-juggler'); var remoting = require('strong-remoting'); var LoopBackContext = require('loopback-context'); var deprecated = require('depd')('loopback'); +var g = require('strong-globalize')(); module.exports = function(loopback) { @@ -20,9 +21,9 @@ module.exports = function(loopback) { // NOTE(bajtos) LoopBackContext.getCurrentContext is overriden whenever // the context changes, therefore we cannot simply assign // LoopBackContext.getCurrentContext() to loopback.getCurrentContext() - deprecated('loopback.getCurrentContext() is deprecated. See ' + - 'https://docs.strongloop.com/display/APIC/Using%20current%20context' + - 'for more details.'); + deprecated(g.f('%s is deprecated. See %s for more details.', + 'loopback.getCurrentContext()', + 'https://docs.strongloop.com/display/APIC/Using%20current%20context')); return LoopBackContext.getCurrentContext(); }; @@ -45,9 +46,9 @@ module.exports = function(loopback) { * When no value is provided, then the default global context is used. */ loopback.runInContext = function(fn, ctx) { - deprecated('loopback.runInContext() is deprecated. See ' + - 'https://docs.strongloop.com/display/APIC/Using%20current%20context' + - 'for more details.'); + deprecated(g.f('%s is deprecated. See %s for more details.', + 'loopback.runInContext()', + 'https://docs.strongloop.com/display/APIC/Using%20current%20context')); return LoopBackContext.runInContext(fn, ctx); }; @@ -69,9 +70,9 @@ module.exports = function(loopback) { * @return {ChainedContext} The new context object. */ loopback.createContext = function(scopeName) { - deprecated('loopback.createContext() is deprecated. See ' + - 'https://docs.strongloop.com/display/APIC/Using%20current%20context' + - 'for more details.'); + deprecated(g.f('%s is deprecated. See %s for more details.', + 'loopback.createContext()', + 'https://docs.strongloop.com/display/APIC/Using%20current%20context')); return LoopBackContext.createContext(scopeName); }; }; diff --git a/server/middleware/context.js b/server/middleware/context.js index 1bbb87cdd..4c5fe7b0b 100644 --- a/server/middleware/context.js +++ b/server/middleware/context.js @@ -4,11 +4,12 @@ // License text available at https://opensource.org/licenses/MIT var deprecated = require('depd')('loopback'); +var g = require('strong-globalize')(); var perRequestContext = require('loopback-context').perRequest; module.exports = function() { - deprecated('loopback#context middleware is deprecated. See ' + - 'https://docs.strongloop.com/display/APIC/Using%20current%20context' + - 'for more details.'); + deprecated(g.f('%s middleware is deprecated. See %s for more details.', + 'loopback#context', + 'https://docs.strongloop.com/display/APIC/Using%20current%20context')); return perRequestContext.apply(this, arguments); }; From 99dc1f954198c751cf28a861f495c1125d213eeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 8 Aug 2016 17:24:17 +0200 Subject: [PATCH 062/187] common: add KeyValueModel --- common/models/key-value-model.js | 76 ++++++++++++++++++++ common/models/key-value-model.json | 4 ++ lib/builtin-models.js | 4 ++ test/key-value-model.test.js | 110 +++++++++++++++++++++++++++++ test/loopback.test.js | 1 + 5 files changed, 195 insertions(+) create mode 100644 common/models/key-value-model.js create mode 100644 common/models/key-value-model.json create mode 100644 test/key-value-model.test.js diff --git a/common/models/key-value-model.js b/common/models/key-value-model.js new file mode 100644 index 000000000..200465ca1 --- /dev/null +++ b/common/models/key-value-model.js @@ -0,0 +1,76 @@ +var g = require('strong-globalize')(); + +module.exports = function(KeyValueModel) { + // TODO add api docs + KeyValueModel.get = function(key, options, callback) { + throwNotAttached(this.modelName, 'get'); + }; + + // TODO add api docs + KeyValueModel.set = function(key, value, options, callback) { + throwNotAttached(this.modelName, 'set'); + }; + + // TODO add api docs + KeyValueModel.expire = function(key, ttl, options, callback) { + throwNotAttached(this.modelName, 'expire'); + }; + + KeyValueModel.setup = function() { + KeyValueModel.base.setup.apply(this, arguments); + + this.remoteMethod('get', { + accepts: { + arg: 'key', type: 'string', required: true, + http: { source: 'path' }, + }, + returns: { arg: 'value', type: 'any', root: true }, + http: { path: '/:key', verb: 'get' }, + rest: { after: convertNullToNotFoundError }, + }); + + this.remoteMethod('set', { + accepts: [ + { arg: 'key', type: 'string', required: true, + http: { source: 'path' }}, + { arg: 'value', type: 'any', required: true, + http: { source: 'body' }}, + { arg: 'ttl', type: 'number', + http: { source: 'query' }, + description: 'time to live in milliseconds' }, + ], + http: { path: '/:key', verb: 'put' }, + }); + + this.remoteMethod('expire', { + accepts: [ + { arg: 'key', type: 'string', required: true, + http: { source: 'path' }}, + { arg: 'ttl', type: 'number', required: true, + http: { source: 'form' }}, + ], + http: { path: '/:key/expire', verb: 'put' }, + }); + }; +}; + +function throwNotAttached(modelName, methodName) { + throw new Error(g.f( + 'Cannot call %s.%s(). ' + + 'The %s method has not been setup. ' + + 'The {{KeyValueModel}} has not been correctly attached ' + + 'to a {{DataSource}}!', + modelName, methodName, methodName)); +} + +function convertNullToNotFoundError(ctx, cb) { + if (ctx.result !== null) return cb(); + + var modelName = ctx.method.sharedClass.name; + var id = ctx.getArgByName('id'); + var msg = g.f('Unknown "%s" {{key}} "%s".', modelName, id); + var error = new Error(msg); + error.statusCode = error.status = 404; + error.code = 'KEY_NOT_FOUND'; + cb(error); +} diff --git a/common/models/key-value-model.json b/common/models/key-value-model.json new file mode 100644 index 000000000..72884e879 --- /dev/null +++ b/common/models/key-value-model.json @@ -0,0 +1,4 @@ +{ + "name": "KeyValueModel", + "base": "Model" +} diff --git a/lib/builtin-models.js b/lib/builtin-models.js index 59f0ff655..dc23b409f 100644 --- a/lib/builtin-models.js +++ b/lib/builtin-models.js @@ -6,6 +6,10 @@ module.exports = function(registry) { // NOTE(bajtos) we must use static require() due to browserify limitations + registry.KeyValueModel = createModel( + require('../common/models/key-value-model.json'), + require('../common/models/key-value-model.js')); + registry.Email = createModel( require('../common/models/email.json'), require('../common/models/email.js')); diff --git a/test/key-value-model.test.js b/test/key-value-model.test.js new file mode 100644 index 000000000..e81ed63c7 --- /dev/null +++ b/test/key-value-model.test.js @@ -0,0 +1,110 @@ +var expect = require('chai').expect; +var http = require('http'); +var loopback = require('..'); +var supertest = require('supertest'); + +var AN_OBJECT_VALUE = { name: 'an-object' }; + +describe('KeyValueModel', function() { + var request, app, CacheItem; + beforeEach(setupAppAndCacheItem); + + describe('REST API', function() { + before(setupSharedHttpServer); + + it('provides "get(key)" at "GET /key"', function(done) { + CacheItem.set('get-key', AN_OBJECT_VALUE); + request.get('/CacheItems/get-key') + .end(function(err, res) { + if (err) return done(err); + expect(res.body).to.eql(AN_OBJECT_VALUE); + done(); + }); + }); + + it('returns 404 when getting a key that does not exist', function(done) { + request.get('/CacheItems/key-does-not-exist') + .expect(404, done); + }); + + it('provides "set(key)" at "PUT /key"', function(done) { + request.put('/CacheItems/set-key') + .send(AN_OBJECT_VALUE) + .expect(204) + .end(function(err, res) { + if (err) return done(err); + CacheItem.get('set-key', function(err, value) { + if (err) return done(err); + expect(value).to.eql(AN_OBJECT_VALUE); + done(); + }); + }); + }); + + it('provides "set(key, ttl)" at "PUT /key?ttl={num}"', function(done) { + request.put('/CacheItems/set-key-ttl?ttl=10') + .send(AN_OBJECT_VALUE) + .end(function(err, res) { + if (err) return done(err); + setTimeout(function() { + CacheItem.get('set-key-ttl', function(err, value) { + if (err) return done(err); + expect(value).to.equal(null); + done(); + }); + }, 20); + }); + }); + + it('provides "expire(key, ttl)" at "PUT /key/expire"', + function(done) { + CacheItem.set('expire-key', AN_OBJECT_VALUE, function(err) { + if (err) return done(err); + request.put('/CacheItems/expire-key/expire') + .send({ ttl: 10 }) + .end(function(err, res) { + if (err) return done(err); + setTimeout(function() { + CacheItem.get('set-key-ttl', function(err, value) { + if (err) return done(err); + expect(value).to.equal(null); + done(); + }); + }, 20); + }); + }); + }); + + it('returns 404 when expiring a key that does not exist', function(done) { + request.put('/CacheItems/key-does-not-exist/expire') + .send({ ttl: 10 }) + .expect(404, done); + }); + }); + + function setupAppAndCacheItem() { + app = loopback({ localRegistry: true, loadBuiltinModels: true }); + app.use(loopback.rest()); + + CacheItem = app.registry.createModel({ + name: 'CacheItem', + base: 'KeyValueModel', + }); + + app.dataSource('kv', { connector: 'kv-memory' }); + app.model(CacheItem, { dataSource: 'kv' }); + } + + var _server, _requestHandler; // eslint-disable-line one-var + function setupSharedHttpServer(done) { + _server = http.createServer(function(req, res) { + app(req, res); + }); + _server.listen(0, '127.0.0.1') + .once('listening', function() { + request = supertest('http://127.0.0.1/' + this.address().port); + done(); + }) + .once('error', function(err) { done(err); }); + } +}); diff --git a/test/loopback.test.js b/test/loopback.test.js index b3aea149f..3f833b0a1 100644 --- a/test/loopback.test.js +++ b/test/loopback.test.js @@ -45,6 +45,7 @@ describe('loopback', function() { 'DataSource', 'Email', 'GeoPoint', + 'KeyValueModel', 'Mail', 'Memory', 'Model', From 5978cb49194d22e02fe52e42448957f109c169ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 10 Aug 2016 15:26:14 +0200 Subject: [PATCH 063/187] Fix token middleware to not trigger CLS init Rework the token middleware to access current context via `req.loopbackContext` instead of `loopback.getCurentContext()`. That way the CLS/AsyncListener machinery is configured only in applications that are using current context. --- server/middleware/token.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/middleware/token.js b/server/middleware/token.js index b5038df23..21e3b5cab 100644 --- a/server/middleware/token.js +++ b/server/middleware/token.js @@ -124,7 +124,7 @@ function token(options) { TokenModel.findForRequest(req, options, function(err, token) { req.accessToken = token || null; rewriteUserLiteral(req, currentUserLiteral); - var ctx = loopback.getCurrentContext(); + var ctx = req.loopbackContext; if (ctx) ctx.set('accessToken', token); next(err); }); From 1dab10da3cb07b73103bcd3542b436650aba6927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 12 Aug 2016 13:33:14 +0200 Subject: [PATCH 064/187] Revert globalization of assert() messages --- lib/registry.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/registry.js b/lib/registry.js index c3d976210..f114e37c0 100644 --- a/lib/registry.js +++ b/lib/registry.js @@ -239,8 +239,8 @@ Registry.prototype.configureModel = function(ModelCtor, config) { // configuration, so that the datasource picks up updated relations if (config.dataSource) { assert(config.dataSource instanceof DataSource, - g.f('Cannot configure %s: {{config.dataSource}} must be an instance ' + - 'of {{DataSource}}', ModelCtor.modelName)); + 'Cannot configure ' + ModelCtor.modelName + + ': config.dataSource must be an instance of DataSource'); ModelCtor.attachTo(config.dataSource); debug('Attached model `%s` to dataSource `%s`', modelName, config.dataSource.name); From 3b88753c8e67b9f50df347bbf80f3c2e534e04da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 12 Aug 2016 13:39:47 +0200 Subject: [PATCH 065/187] Cache remoting descriptions to speed up tests Calling strong-globalize is relatively expensive (about 300 microseconds for each call). When using per-app local registry in unit-tests, each app creation requires about 500 calls of `g.t` just for remoting metadata, i.e. about 150ms. In this commit, we introduce `g.s` that caches the results from strong-globalize to speed up creation of remoting metadata. --- lib/persisted-model.js | 62 +++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/lib/persisted-model.js b/lib/persisted-model.js index 4ecfa05f4..537448b52 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -16,6 +16,18 @@ var debug = require('debug')('loopback:persisted-model'); var PassThrough = require('stream').PassThrough; var utils = require('./utils'); +// workaround for low performance of strong-globalize +// see https://github.com/strongloop/strong-globalize/issues/66 +var stringCache = Object.create(null); +g.s = function(str) { + assert.equal(1, arguments.length, 'g.s() does not support parameters'); + if (str in stringCache) + return stringCache[str]; + var result = g.t(str); + stringCache[str] = result; + return result; +}; + module.exports = function(registry) { var Model = registry.getModel('Model'); @@ -560,7 +572,7 @@ module.exports = function(registry) { } setRemoting(PersistedModel, 'create', { - description: g.f('Create a new instance of the model and persist it into the data source.'), + description: g.s('Create a new instance of the model and persist it into the data source.'), accessType: 'WRITE', accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}}, returns: {arg: 'data', type: typeName, root: true}, @@ -569,7 +581,7 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'upsert', { aliases: ['updateOrCreate'], - description: g.f('Update an existing model instance or insert a new one ' + + description: g.s('Update an existing model instance or insert a new one ' + 'into the data source.'), accessType: 'WRITE', accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}}, @@ -578,7 +590,7 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'exists', { - description: g.f('Check whether a model instance exists in the data source.'), + description: g.s('Check whether a model instance exists in the data source.'), accessType: 'READ', accepts: {arg: 'id', type: 'any', description: 'Model id', required: true}, returns: {arg: 'exists', type: 'boolean'}, @@ -609,13 +621,13 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'findById', { - description: g.f('Find a model instance by {{id}} from the data source.'), + description: g.s('Find a model instance by {{id}} from the data source.'), accessType: 'READ', accepts: [ { arg: 'id', type: 'any', description: 'Model id', required: true, http: {source: 'path'}}, { arg: 'filter', type: 'object', - description: g.f('Filter defining fields and include') }, + description: g.s('Filter defining fields and include') }, ], returns: {arg: 'data', type: typeName, root: true}, http: {verb: 'get', path: '/:id'}, @@ -623,7 +635,7 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'find', { - description: g.f('Find all instances of the model matched by filter from the data source.'), + description: g.s('Find all instances of the model matched by filter from the data source.'), accessType: 'READ', accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, include, order, offset, and limit'}, returns: {arg: 'data', type: [typeName], root: true}, @@ -631,7 +643,7 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'findOne', { - description: g.f('Find first instance of the model matched by filter from the data source.'), + description: g.s('Find first instance of the model matched by filter from the data source.'), accessType: 'READ', accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, include, order, offset, and limit'}, returns: {arg: 'data', type: typeName, root: true}, @@ -640,7 +652,7 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'destroyAll', { - description: g.f('Delete all matching records.'), + description: g.s('Delete all matching records.'), accessType: 'WRITE', accepts: {arg: 'where', type: 'object', description: 'filter.where object'}, returns: { @@ -655,17 +667,17 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'updateAll', { aliases: ['update'], - description: g.f('Update instances of the model matched by {{where}} from the data source.'), + description: g.s('Update instances of the model matched by {{where}} from the data source.'), accessType: 'WRITE', accepts: [ {arg: 'where', type: 'object', http: { source: 'query'}, - description: g.f('Criteria to match model instances')}, + description: g.s('Criteria to match model instances')}, {arg: 'data', type: 'object', http: {source: 'body'}, - description: g.f('An object of model property name/value pairs')}, + description: g.s('An object of model property name/value pairs')}, ], returns: { arg: 'count', - description: g.f('The number of instances updated'), + description: g.s('The number of instances updated'), type: 'object', root: true }, @@ -674,7 +686,7 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'deleteById', { aliases: ['destroyById', 'removeById'], - description: g.f('Delete a model instance by {{id}} from the data source.'), + description: g.s('Delete a model instance by {{id}} from the data source.'), accessType: 'WRITE', accepts: {arg: 'id', type: 'any', description: 'Model id', required: true, http: {source: 'path'}}, @@ -683,7 +695,7 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'count', { - description: g.f('Count instances of the model matched by where from the data source.'), + description: g.s('Count instances of the model matched by where from the data source.'), accessType: 'READ', accepts: {arg: 'where', type: 'object', description: 'Criteria to match model instances'}, returns: {arg: 'count', type: 'number'}, @@ -691,7 +703,7 @@ module.exports = function(registry) { }); setRemoting(PersistedModel.prototype, 'updateAttributes', { - description: g.f('Update attributes for a model instance and persist it into ' + + description: g.s('Update attributes for a model instance and persist it into ' + 'the data source.'), accessType: 'WRITE', accepts: {arg: 'data', type: 'object', http: {source: 'body'}, description: 'An object of model property name/value pairs'}, @@ -701,7 +713,7 @@ module.exports = function(registry) { if (options.trackChanges || options.enableRemoteReplication) { setRemoting(PersistedModel, 'diff', { - description: g.f('Get a set of deltas and conflicts since the given checkpoint.'), + description: g.s('Get a set of deltas and conflicts since the given checkpoint.'), accessType: 'READ', accepts: [ {arg: 'since', type: 'number', description: 'Find deltas since this checkpoint'}, @@ -713,7 +725,7 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'changes', { - description: g.f('Get the changes to a model since a given checkpoint.' + + description: g.s('Get the changes to a model since a given checkpoint.' + 'Provide a filter object to reduce the number of results returned.'), accessType: 'READ', accepts: [ @@ -725,7 +737,7 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'checkpoint', { - description: g.f('Create a checkpoint.'), + description: g.s('Create a checkpoint.'), // The replication algorithm needs to create a source checkpoint, // even though it is otherwise not making any source changes. // We need to allow this method for users that don't have full @@ -736,14 +748,14 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'currentCheckpoint', { - description: g.f('Get the current checkpoint.'), + description: g.s('Get the current checkpoint.'), accessType: 'READ', returns: {arg: 'checkpoint', type: 'object', root: true}, http: {verb: 'get', path: '/checkpoint'} }); setRemoting(PersistedModel, 'createUpdates', { - description: g.f('Create an update list from a delta list.'), + description: g.s('Create an update list from a delta list.'), // This operation is read-only, it does not change any local data. // It is called by the replication algorithm to compile a list // of changes to apply on the target. @@ -754,14 +766,14 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'bulkUpdate', { - description: g.f('Run multiple updates at once. Note: this is not atomic.'), + description: g.s('Run multiple updates at once. Note: this is not atomic.'), accessType: 'WRITE', accepts: {arg: 'updates', type: 'array'}, http: {verb: 'post', path: '/bulk-update'} }); setRemoting(PersistedModel, 'findLastChange', { - description: g.f('Get the most recent change record for this instance.'), + description: g.s('Get the most recent change record for this instance.'), accessType: 'READ', accepts: { arg: 'id', type: 'any', required: true, http: { source: 'path' }, @@ -773,7 +785,7 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'updateLastChange', { description: [ - g.f('Update the properties of the most recent change record ' + + g.s('Update the properties of the most recent change record ' + 'kept for this instance.'), ], accessType: 'WRITE', @@ -784,7 +796,7 @@ module.exports = function(registry) { }, { arg: 'data', type: 'object', http: {source: 'body'}, - description: g.f('An object of Change property name/value pairs'), + description: g.s('An object of Change property name/value pairs'), }, ], returns: { arg: 'result', type: this.Change.modelName, root: true }, @@ -810,7 +822,7 @@ module.exports = function(registry) { } setRemoting(PersistedModel, 'createChangeStream', { - description: g.f('Create a change stream.'), + description: g.s('Create a change stream.'), accessType: 'READ', http: [ {verb: 'post', path: '/change-stream'}, From d8aa6bdf0000278863434db298df149e5af3fcaa Mon Sep 17 00:00:00 2001 From: Loay Date: Wed, 3 Aug 2016 19:01:33 -0400 Subject: [PATCH 066/187] Add bcrypt validation https://github.com/strongloop/loopback/pull/2580 --- common/models/user.js | 24 +++++++++---- index.js | 2 +- test/user.test.js | 79 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 7 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index 90a17c561..b857e38e5 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -14,7 +14,7 @@ var utils = require('../../lib/utils'); var path = require('path'); var SALT_WORK_FACTOR = 10; var crypto = require('crypto'); - +var MAX_PASSWORD_LENGTH = 72; var bcrypt; try { // Try the native module first @@ -538,7 +538,6 @@ module.exports = function(User) { cb = cb || utils.createPromiseCallback(); var UserModel = this; var ttl = UserModel.settings.resetPasswordTokenTTL || DEFAULT_RESET_PW_TTL; - options = options || {}; if (typeof options.email !== 'string') { var err = new Error(g.f('Email is required')); @@ -548,7 +547,14 @@ module.exports = function(User) { return cb.promise; } - UserModel.findOne({ where: {email: options.email} }, function(err, user) { + try { + if (options.password) { + UserModel.validatePassword(options.password); + } + } catch (err) { + return cb(err); + } + UserModel.findOne({ where: { email: options.email }}, function(err, user) { if (err) { return cb(err); } @@ -586,14 +592,20 @@ module.exports = function(User) { }; User.validatePassword = function(plain) { - if (typeof plain === 'string' && plain) { + var err; + if (plain && typeof plain === 'string' && plain.length <= MAX_PASSWORD_LENGTH) { return true; } - var err = new Error(g.f('Invalid password: %s', plain)); + if (plain.length > MAX_PASSWORD_LENGTH) { + err = new Error(g.f('Password too long: %s', plain)); + err.code = 'PASSWORD_TOO_LONG'; + } else { + err = new Error(g.f('Invalid password: %s', plain)); + err.code = 'INVALID_PASSWORD'; + } err.statusCode = 422; throw err; }; - /*! * Setup an extended user model. */ diff --git a/index.js b/index.js index ac439167f..0f02a1c1e 100644 --- a/index.js +++ b/index.js @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT var SG = require('strong-globalize'); -SG.SetRootDir(__dirname, {autonomousMsgLoading: 'all'}); +SG.SetRootDir(__dirname, { autonomousMsgLoading: 'all' }); /** * loopback ~ public api diff --git a/test/user.test.js b/test/user.test.js index cfb7232e8..0ac361bfe 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -377,6 +377,68 @@ describe('User', function() { }); }); + describe('Password length validation', function() { + var pass72Char = new Array(70).join('a') + '012'; + var pass73Char = pass72Char + '3'; + var passTooLong = pass72Char + 'WXYZ1234'; + + it('rejects passwords longer than 72 characters', function(done) { + try { + User.create({ email: 'b@c.com', password: pass73Char }, function(err) { + if (err) return done(err); + done(new Error('User.create() should have thrown an error.')); + }); + } catch (e) { + expect(e).to.match(/Password too long/); + done(); + } + }); + + it('rejects a new user with password longer than 72 characters', function(done) { + try { + var u = new User({ username: 'foo', password: pass73Char }); + assert(false, 'Error should have been thrown'); + } catch (e) { + expect(e).to.match(/Password too long/); + done(); + } + }); + + it('accepts passwords that are exactly 72 characters long', function(done) { + User.create({ email: 'b@c.com', password: pass72Char }, function(err, user) { + if (err) return done(err); + User.findById(user.id, function(err, userFound) { + if (err) return done(err); + assert(userFound); + done(); + }); + }); + }); + + it('allows login with password exactly 72 characters long', function(done) { + User.create({ email: 'b@c.com', password: pass72Char }, function(err) { + if (err) return done(err); + User.login({ email: 'b@c.com', password: pass72Char }, function(err, accessToken) { + if (err) return done(err); + assertGoodToken(accessToken); + assert(accessToken.id); + done(); + }); + }); + }); + + it('rejects password reset when password is more than 72 chars', function(done) { + User.create({ email: 'b@c.com', password: pass72Char }, function(err) { + if (err) return done(err); + User.resetPassword({ email: 'b@c.com', password: pass73Char }, function(err) { + assert(err); + expect(err).to.match(/Password too long/); + done(); + }); + }); + }); + }); + describe('Access-hook for queries with email NOT case-sensitive', function() { it('Should not throw an error if the query does not contain {where: }', function(done) { User.find({}, function(err) { @@ -692,6 +754,23 @@ describe('User', function() { done(); }); }); + + it('allows login with password too long but created in old LB version', + function(done) { + var bcrypt = require('bcryptjs'); + var longPassword = new Array(80).join('a'); + var oldHash = bcrypt.hashSync(longPassword, bcrypt.genSaltSync(1)); + + User.create({ email: 'b@c.com', password: oldHash }, function(err) { + if (err) return done(err); + User.login({ email: 'b@c.com', password: longPassword }, function(err, accessToken) { + if (err) return done(err); + assert(accessToken.id); + // we are logged in, the test passed + done(); + }); + }); + }); }); function assertGoodToken(accessToken) { From e562137807ffd7bb8ab3d91af9c27fa35c7b61f5 Mon Sep 17 00:00:00 2001 From: Amir Jafarian Date: Mon, 9 May 2016 15:24:03 -0400 Subject: [PATCH 067/187] Expose `Replace*` methods *Re-mapping `updateAttributes` endpoint to use `PATCH` and `PUT`(configurable) verb *Exposing `replaceById` and `replaceOrCreate` via `POST` and `PUT`(configurable) verb --- common/models/user.json | 6 + lib/persisted-model.js | 135 +++++++++-- test/access-control.integration.js | 126 +++++++++-- .../access-control/common/models/account.json | 6 +- .../models/accountWithReplaceOnPUTfalse.json | 44 ++++ .../access-control/common/models/user.json | 3 +- .../access-control/server/model-config.json | 6 +- .../common/models/store-replacing.json | 19 ++ .../common/models/store-updating.json | 19 ++ .../server/model-config.json | 8 + test/model.test.js | 17 +- test/remoting.integration.js | 212 ++++++++++++------ 12 files changed, 487 insertions(+), 114 deletions(-) create mode 100644 test/fixtures/access-control/common/models/accountWithReplaceOnPUTfalse.json create mode 100644 test/fixtures/simple-integration-app/common/models/store-replacing.json create mode 100644 test/fixtures/simple-integration-app/common/models/store-updating.json diff --git a/common/models/user.json b/common/models/user.json index 16545ab4b..a3f3973fc 100644 --- a/common/models/user.json +++ b/common/models/user.json @@ -75,6 +75,12 @@ "permission": "ALLOW", "property": "updateAttributes" }, + { + "principalType": "ROLE", + "principalId": "$owner", + "permission": "ALLOW", + "property": "replaceById" + }, { "principalType": "ROLE", "principalId": "$everyone", diff --git a/lib/persisted-model.js b/lib/persisted-model.js index 537448b52..534a41e01 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -125,10 +125,27 @@ module.exports = function(registry) { * @param {Object} model Updated model instance. */ - PersistedModel.upsert = PersistedModel.updateOrCreate = function upsert(data, callback) { + PersistedModel.upsert = PersistedModel.updateOrCreate = PersistedModel.patchOrCreate = + function upsert(data, callback) { throwNotAttached(this.modelName, 'upsert'); }; + /** + * Replace or insert a model instance; replace existing 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. + * @options {Object} [options] Options for replaceOrCreate + * @property {Boolean} validate Perform validation before saving. Default is true. + * @callback {Function} callback Callback function called with `cb(err, obj)` signature. + * @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object). + * @param {Object} model Replaced model instance. + */ + + PersistedModel.replaceOrCreate = function replaceOrCreate(data, callback) { + throwNotAttached(this.modelName, 'replaceOrCreate'); + }; + /** * Finds one record matching the optional filter object. If not found, creates * the object using the data provided as second argument. In this sense it is @@ -492,10 +509,45 @@ module.exports = function(registry) { * @param {Object} instance Updated instance. */ - PersistedModel.prototype.updateAttributes = function updateAttributes(data, cb) { + PersistedModel.prototype.updateAttributes = PersistedModel.prototype.patchAttributes = + function updateAttributes(data, cb) { throwNotAttached(this.modelName, 'updateAttributes'); }; + /** + * Replace attributes for a model instance and persist it into the datasource. + * Performs validation before replacing. + * + * @param {Object} data Data to replace. + * @options {Object} [options] Options for replace + * @property {Boolean} validate Perform validation before saving. Default is true. + * @callback {Function} callback Callback function called with `(err, instance)` arguments. + * @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object). + * @param {Object} instance Replaced instance. + */ + + PersistedModel.prototype.replaceAttributes = function replaceAttributes(data, cb) { + throwNotAttached(this.modelName, 'replaceAttributes'); + }; + + /** + * Replace attributes for a model instance whose id is the first input + * argument and persist it into the datasource. + * Performs validation before replacing. + * + * @param {*} id The ID value of model instance to replace. + * @param {Object} data Data to replace. + * @options {Object} [options] Options for replace + * @property {Boolean} validate Perform validation before saving. Default is true. + * @callback {Function} callback Callback function called with `(err, instance)` arguments. + * @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object). + * @param {Object} instance Replaced instance. + */ + + PersistedModel.replaceById = function replaceById(id, data, cb) { + throwNotAttached(this.modelName, 'replaceById'); + }; + /** * Reload object from persistence. Requires `id` member of `object` to be able to call `find`. * @callback {Function} callback Callback function called with `(err, instance)` arguments. Required. @@ -564,6 +616,9 @@ module.exports = function(registry) { var typeName = PersistedModel.modelName; var options = PersistedModel.settings; + // This is just for LB 2.x + options.replaceOnPUT = options.replaceOnPUT === true; + function setRemoting(scope, name, options) { var fn = scope[name]; fn._delegate = true; @@ -579,15 +634,35 @@ module.exports = function(registry) { http: {verb: 'post', path: '/'} }); - setRemoting(PersistedModel, 'upsert', { - aliases: ['updateOrCreate'], - description: g.s('Update an existing model instance or insert a new one ' + - 'into the data source.'), + var upsertOptions = { + aliases: ['patchOrCreate', 'updateOrCreate'], + description: g.s('Patch an existing model instance or insert a new one into the data source.'), accessType: 'WRITE', - accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}}, - returns: {arg: 'data', type: typeName, root: true}, - http: {verb: 'put', path: '/'} - }); + accepts: { arg: 'data', type: 'object', http: { source: 'body' }, description: + 'Model instance data' }, + returns: { arg: 'data', type: typeName, root: true }, + http: [{ verb: 'patch', path: '/' }], + }; + + if (!options.replaceOnPUT) { + upsertOptions.http.push({ verb: 'put', path: '/' }); + } + setRemoting(PersistedModel, 'upsert', upsertOptions); + + var replaceOrCreateOptions = { + description: 'Replace an existing model instance or insert a new one into the data source.', + accessType: 'WRITE', + accepts: { arg: 'data', type: 'object', http: { source: 'body' }, description: + 'Model instance data' }, + returns: { arg: 'data', type: typeName, root: true }, + http: [{ verb: 'post', path: '/replaceOrCreate' }], + }; + + if (options.replaceOnPUT) { + replaceOrCreateOptions.http.push({ verb: 'put', path: '/' }); + } + + setRemoting(PersistedModel, 'replaceOrCreate', replaceOrCreateOptions); setRemoting(PersistedModel, 'exists', { description: g.s('Check whether a model instance exists in the data source.'), @@ -634,6 +709,25 @@ module.exports = function(registry) { rest: {after: convertNullToNotFoundError} }); + var replaceByIdOptions = { + description: 'Replace attributes for a model instance and persist it into the data source.', + accessType: 'WRITE', + accepts: [ + { arg: 'id', type: 'any', description: 'Model id', required: true, + http: { source: 'path' }}, + { arg: 'data', type: 'object', http: { source: 'body' }, description: + 'Model instance data' }, + ], + returns: { arg: 'data', type: typeName, root: true }, + http: [{ verb: 'post', path: '/:id/replace' }], + }; + + if (options.replaceOnPUT) { + replaceByIdOptions.http.push({ verb: 'put', path: '/:id' }); + } + + setRemoting(PersistedModel, 'replaceById', replaceByIdOptions); + setRemoting(PersistedModel, 'find', { description: g.s('Find all instances of the model matched by filter from the data source.'), accessType: 'READ', @@ -702,14 +796,21 @@ module.exports = function(registry) { http: {verb: 'get', path: '/count'} }); - setRemoting(PersistedModel.prototype, 'updateAttributes', { - description: g.s('Update attributes for a model instance and persist it into ' + - 'the data source.'), + var updateAttributesOptions = { + aliases: ['patchAttributes'], + description: g.s('Patch attributes for a model instance and persist it into the data source.'), accessType: 'WRITE', - accepts: {arg: 'data', type: 'object', http: {source: 'body'}, description: 'An object of model property name/value pairs'}, - returns: {arg: 'data', type: typeName, root: true}, - http: {verb: 'put', path: '/'} - }); + + accepts: { arg: 'data', type: 'object', http: { source: 'body' }, description: 'An object of model property name/value pairs' }, + returns: { arg: 'data', type: typeName, root: true }, + http: [{ verb: 'patch', path: '/' }], + }; + + setRemoting(PersistedModel.prototype, 'updateAttributes', updateAttributesOptions); + + if (!options.replaceOnPUT) { + updateAttributesOptions.http.push({ verb: 'put', path: '/' }); + } if (options.trackChanges || options.enableRemoteReplication) { setRemoting(PersistedModel, 'diff', { diff --git a/test/access-control.integration.js b/test/access-control.integration.js index b32152573..3fe2e9c57 100644 --- a/test/access-control.integration.js +++ b/test/access-control.integration.js @@ -121,9 +121,15 @@ describe('access control - integration', function() { assert.equal(user.password, undefined); }); }); + + // user has replaceOnPUT = false; so then both PUT and PATCH should be allowed for update lt.describe.whenCalledRemotely('PUT', '/api/users/:id', function() { lt.it.shouldBeAllowed(); }); + + lt.describe.whenCalledRemotely('PATCH', '/api/users/:id', function() { + lt.it.shouldBeAllowed(); + }); }); lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForUser); @@ -173,7 +179,7 @@ describe('access control - integration', function() { } }); - describe('/accounts', function() { + describe('/accounts with replaceOnPUT true', function() { var count = 0; before(function() { var roleModel = loopback.getModelByType(loopback.Role); @@ -187,48 +193,135 @@ describe('access control - integration', function() { }); }); - lt.beforeEach.givenModel('account'); + lt.beforeEach.givenModel('accountWithReplaceOnPUTtrue'); - lt.it.shouldBeDeniedWhenCalledAnonymously('GET', '/api/accounts'); - lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', '/api/accounts'); - lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', '/api/accounts'); + lt.it.shouldBeDeniedWhenCalledAnonymously('GET', '/api/accounts-replacing'); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', '/api/accounts-replacing'); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', '/api/accounts-replacing'); lt.it.shouldBeDeniedWhenCalledAnonymously('GET', urlForAccount); lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForAccount); lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', urlForAccount); - lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/accounts'); - lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/accounts'); - lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/accounts'); + lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/accounts-replacing'); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/accounts-replacing'); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/accounts-replacing'); + + lt.it.shouldBeDeniedWhenCalledAnonymously('POST', urlForReplaceAccountPOST); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', urlForReplaceAccountPOST); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', urlForReplaceAccountPOST); lt.it.shouldBeDeniedWhenCalledAnonymously('PUT', urlForAccount); lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForAccount); lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PUT', urlForAccount); + lt.it.shouldBeDeniedWhenCalledAnonymously('PATCH', urlForAccount); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('PATCH', urlForAccount); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PATCH', urlForAccount); + lt.describe.whenLoggedInAsUser(CURRENT_USER, function() { + var actId; beforeEach(function(done) { var self = this; - // Create an account under the given user - app.models.account.create({ + app.models.accountWithReplaceOnPUTtrue.create({ userId: self.user.id, balance: 100 }, function(err, act) { - self.url = '/api/accounts/' + act.id; + actId = act.id; + self.url = '/api/accounts-replacing/' + actId; + done(); + }); + }); + lt.describe.whenCalledRemotely('PATCH', '/api/accounts-replacing/:id', function() { + lt.it.shouldBeAllowed(); + }); + lt.describe.whenCalledRemotely('PUT', '/api/accounts-replacing/:id', function() { + lt.it.shouldBeAllowed(); + }); + lt.describe.whenCalledRemotely('GET', '/api/accounts-replacing/:id', function() { + lt.it.shouldBeAllowed(); + }); + lt.describe.whenCalledRemotely('DELETE', '/api/accounts-replacing/:id', function() { + lt.it.shouldBeDenied(); + }); + describe('replace on POST verb', function() { + beforeEach(function(done) { + this.url = '/api/accounts-replacing/' + actId + '/replace'; done(); }); + lt.describe.whenCalledRemotely('POST', '/api/accounts-replacing/:id/replace', function() { + lt.it.shouldBeAllowed(); + }); + }); + }); + lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForAccount); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForAccount); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForAccount); + + function urlForAccount() { + return '/api/accounts-replacing/' + this.accountWithReplaceOnPUTtrue.id; + } + function urlForReplaceAccountPOST() { + return '/api/accounts-replacing/' + this.accountWithReplaceOnPUTtrue.id + '/replace'; + } + }); + + describe('/accounts with replaceOnPUT false', function() { + lt.beforeEach.givenModel('accountWithReplaceOnPUTfalse'); + lt.it.shouldBeDeniedWhenCalledAnonymously('POST', urlForReplaceAccountPOST); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', urlForReplaceAccountPOST); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', urlForReplaceAccountPOST); + + lt.it.shouldBeDeniedWhenCalledAnonymously('PUT', urlForAccount); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForAccount); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PUT', urlForAccount); + + lt.it.shouldBeDeniedWhenCalledAnonymously('PATCH', urlForAccount); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('PATCH', urlForAccount); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PATCH', urlForAccount); + + lt.describe.whenLoggedInAsUser(CURRENT_USER, function() { + var actId; + beforeEach(function(done) { + var self = this; + // Create an account under the given user + app.models.accountWithReplaceOnPUTfalse.create({ + userId: self.user.id, + balance: 100, + }, function(err, act) { + actId = act.id; + self.url = '/api/accounts-updating/' + actId; + done(); + }); }); - lt.describe.whenCalledRemotely('PUT', '/api/accounts/:id', function() { + + lt.describe.whenCalledRemotely('PATCH', '/api/accounts-updating/:id', function() { + lt.it.shouldBeAllowed(); + }); + + lt.describe.whenCalledRemotely('PUT', '/api/accounts-updating/:id', function() { + lt.it.shouldBeAllowed(); }); - lt.describe.whenCalledRemotely('GET', '/api/accounts/:id', function() { + lt.describe.whenCalledRemotely('GET', '/api/accounts-updating/:id', function() { lt.it.shouldBeAllowed(); }); - lt.describe.whenCalledRemotely('DELETE', '/api/accounts/:id', function() { + lt.describe.whenCalledRemotely('DELETE', '/api/accounts-updating/:id', function() { lt.it.shouldBeDenied(); }); + + describe('replace on POST verb', function() { + beforeEach(function(done) { + this.url = '/api/accounts-updating/' + actId + '/replace'; + done(); + }); + lt.describe.whenCalledRemotely('POST', '/api/accounts-updating/:id/replace', function() { + lt.it.shouldBeAllowed(); + }); + }); }); lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForAccount); @@ -236,7 +329,10 @@ describe('access control - integration', function() { lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForAccount); function urlForAccount() { - return '/api/accounts/' + this.account.id; + return '/api/accounts-updating/' + this.accountWithReplaceOnPUTfalse.id; + } + function urlForReplaceAccountPOST() { + return '/api/accounts-updating/' + this.accountWithReplaceOnPUTfalse.id + '/replace'; } }); diff --git a/test/fixtures/access-control/common/models/account.json b/test/fixtures/access-control/common/models/account.json index 4607adfc0..e0c08bfd4 100644 --- a/test/fixtures/access-control/common/models/account.json +++ b/test/fixtures/access-control/common/models/account.json @@ -1,5 +1,6 @@ { - "name": "account", + "name": "accountWithReplaceOnPUTtrue", + "plural": "accounts-replacing", "relations": { "transactions": { "model": "transaction", @@ -38,5 +39,6 @@ "principalId": "$dummy" } ], - "properties": {} + "properties": {}, + "replaceOnPUT": true } \ No newline at end of file diff --git a/test/fixtures/access-control/common/models/accountWithReplaceOnPUTfalse.json b/test/fixtures/access-control/common/models/accountWithReplaceOnPUTfalse.json new file mode 100644 index 000000000..a54f9cc28 --- /dev/null +++ b/test/fixtures/access-control/common/models/accountWithReplaceOnPUTfalse.json @@ -0,0 +1,44 @@ +{ + "name": "accountWithReplaceOnPUTfalse", + "plural": "accounts-updating", + "relations": { + "transactions": { + "model": "transaction", + "type": "hasMany" + }, + "user": { + "model": "user", + "type": "belongsTo", + "foreignKey": "userId" + } + }, + "acls": [ + { + "accessType": "*", + "permission": "DENY", + "principalType": "ROLE", + "principalId": "$everyone" + }, + { + "accessType": "*", + "permission": "ALLOW", + "principalType": "ROLE", + "principalId": "$owner" + }, + { + "permission": "DENY", + "principalType": "ROLE", + "principalId": "$owner", + "property": "deleteById" + }, + { + "accessType": "*", + "permission": "DENY", + "property": "find", + "principalType": "ROLE", + "principalId": "$dummy" + } + ], + "properties": {}, + "replaceOnPUT": false +} \ No newline at end of file diff --git a/test/fixtures/access-control/common/models/user.json b/test/fixtures/access-control/common/models/user.json index 7ecfb3735..ef769cbfa 100644 --- a/test/fixtures/access-control/common/models/user.json +++ b/test/fixtures/access-control/common/models/user.json @@ -19,5 +19,6 @@ "principalType": "ROLE", "principalId": "$everyone" } - ] + ], + "replaceOnPUT": false } \ No newline at end of file diff --git a/test/fixtures/access-control/server/model-config.json b/test/fixtures/access-control/server/model-config.json index ddb886fcc..62816359b 100644 --- a/test/fixtures/access-control/server/model-config.json +++ b/test/fixtures/access-control/server/model-config.json @@ -34,7 +34,11 @@ "public": true, "dataSource": "db" }, - "account": { + "accountWithReplaceOnPUTtrue": { + "public": true, + "dataSource": "db" + }, + "accountWithReplaceOnPUTfalse": { "public": true, "dataSource": "db" }, diff --git a/test/fixtures/simple-integration-app/common/models/store-replacing.json b/test/fixtures/simple-integration-app/common/models/store-replacing.json new file mode 100644 index 000000000..171665263 --- /dev/null +++ b/test/fixtures/simple-integration-app/common/models/store-replacing.json @@ -0,0 +1,19 @@ +{ + "name": "storeWithReplaceOnPUTtrue", + "plural": "stores-replacing", + "properties": {}, + "scopes": { + "superStores": { + "where": { + "size": "super" + } + } + }, + "relations": { + "widgets": { + "model": "widget", + "type": "hasMany" + } + }, + "replaceOnPUT": true +} \ No newline at end of file diff --git a/test/fixtures/simple-integration-app/common/models/store-updating.json b/test/fixtures/simple-integration-app/common/models/store-updating.json new file mode 100644 index 000000000..c876336c8 --- /dev/null +++ b/test/fixtures/simple-integration-app/common/models/store-updating.json @@ -0,0 +1,19 @@ +{ + "name": "storeWithReplaceOnPUTfalse", + "plural": "stores-updating", + "properties": {}, + "scopes": { + "superStores": { + "where": { + "size": "super" + } + } + }, + "relations": { + "widgets": { + "model": "widget", + "type": "hasMany" + } + }, + "replaceOnPUT": false +} \ No newline at end of file diff --git a/test/fixtures/simple-integration-app/server/model-config.json b/test/fixtures/simple-integration-app/server/model-config.json index ed73e7ca6..1eb761597 100644 --- a/test/fixtures/simple-integration-app/server/model-config.json +++ b/test/fixtures/simple-integration-app/server/model-config.json @@ -38,6 +38,14 @@ "public": true, "dataSource": "db" }, + "storeWithReplaceOnPUTfalse": { + "public": true, + "dataSource": "db" + }, + "storeWithReplaceOnPUTtrue": { + "public": true, + "dataSource": "db" + }, "physician": { "dataSource": "db", "public": true diff --git a/test/model.test.js b/test/model.test.js index 27e89ca5b..ef7dd9c26 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -627,9 +627,14 @@ describe.onServer('Remote Methods', function() { var methodNames = []; metadata.methods.forEach(function(method) { methodNames.push(method.name); - methodNames = methodNames.concat(method.sharedMethod.aliases || []); + var aliases = method.sharedMethod.aliases; + if (method.name.indexOf('prototype.') === 0) { + aliases = aliases.map(function(alias) { + return 'prototype.' + alias; + }); + } + methodNames = methodNames.concat(aliases || []); }); - expect(methodNames).to.have.members([ // NOTE(bajtos) These three methods are disabled by default // Because all tests share the same global registry model @@ -637,9 +642,11 @@ describe.onServer('Remote Methods', function() { // this test was seeing this method (with all aliases) as public // 'destroyAll', 'deleteAll', 'remove', 'create', - 'upsert', 'updateOrCreate', + 'upsert', 'updateOrCreate', 'patchOrCreate', 'exists', 'findById', + 'replaceById', + 'replaceOrCreate', 'find', 'findOne', 'updateAll', 'update', @@ -647,8 +654,8 @@ describe.onServer('Remote Methods', function() { 'destroyById', 'removeById', 'count', - 'prototype.updateAttributes', - 'createChangeStream' + 'prototype.patchAttributes', 'prototype.updateAttributes', + 'createChangeStream', ]); }); diff --git a/test/remoting.integration.js b/test/remoting.integration.js index f047f7b3e..59b3ca07c 100644 --- a/test/remoting.integration.js +++ b/test/remoting.integration.js @@ -80,61 +80,28 @@ describe('remoting - integration', function() { }); describe('Model shared classes', function() { - function formatReturns(m) { - var returns = m.returns; - if (!returns || returns.length === 0) { - return ''; - } - var type = returns[0].type; - return type ? ':' + type : ''; - } - - function formatMethod(m) { - return [ - m.name, - '(', - m.accepts.map(function(a) { - return a.arg + ':' + a.type; - }).join(','), - ')', - formatReturns(m), - ' ', - m.getEndpoints()[0].verb, - ' ', - m.getEndpoints()[0].fullPath - ].join(''); - } - - function findClass(name) { - return app.handler('rest').adapter - .getClasses() - .filter(function(c) { - return c.name === name; - })[0]; - } - - it('has expected remote methods', function() { + it('has expected remote methods with default model.settings.replaceOnPUT' + + 'set to false (2.x)', + function() { var storeClass = findClass('store'); - var methods = storeClass.methods - .filter(function(m) { - return m.name.indexOf('__') === -1; - }) - .map(function(m) { - return formatMethod(m); - }); + var methods = getFormattedMethodsExcludingRelations(storeClass.methods); var expectedMethods = [ 'create(data:object):store POST /stores', + 'upsert(data:object):store PATCH /stores', 'upsert(data:object):store PUT /stores', + 'replaceOrCreate(data:object):store POST /stores/replaceOrCreate', 'exists(id:any):boolean GET /stores/:id/exists', 'findById(id:any,filter:object):store GET /stores/:id', + 'prototype.updateAttributes(data:object):store PUT /stores/:id', + 'replaceById(id:any,data:object):store POST /stores/:id/replace', 'find(filter:object):store GET /stores', 'findOne(filter:object):store GET /stores/findOne', 'updateAll(where:object,data:object):object POST /stores/update', 'deleteById(id:any):object DELETE /stores/:id', 'count(where:object):number GET /stores/count', - 'prototype.updateAttributes(data:object):store PUT /stores/:id', - 'createChangeStream(options:object):ReadableStream POST /stores/change-stream' + 'prototype.updateAttributes(data:object):store PATCH /stores/:id', + 'createChangeStream(options:object):ReadableStream POST /stores/change-stream', ]; // The list of methods is from docs: @@ -144,13 +111,7 @@ describe('remoting - integration', function() { it('has expected remote methods for scopes', function() { var storeClass = findClass('store'); - var methods = storeClass.methods - .filter(function(m) { - return m.name.indexOf('__') === 0; - }) - .map(function(m) { - return formatMethod(m); - }); + var methods = getFormattedScopeMethods(storeClass.methods); var expectedMethods = [ '__get__superStores(filter:object):store GET /stores/superStores', @@ -166,13 +127,7 @@ describe('remoting - integration', function() { function() { var widgetClass = findClass('widget'); - var methods = widgetClass.methods - .filter(function(m) { - return m.name.indexOf('prototype.__') === 0; - }) - .map(function(m) { - return formatMethod(m); - }); + var methods = getFormattedPrototypeMethods(widgetClass.methods); var expectedMethods = [ 'prototype.__get__store(refresh:boolean):store ' + @@ -185,13 +140,7 @@ describe('remoting - integration', function() { function() { var physicianClass = findClass('store'); - var methods = physicianClass.methods - .filter(function(m) { - return m.name.indexOf('prototype.__') === 0; - }) - .map(function(m) { - return formatMethod(m); - }); + var methods = getFormattedPrototypeMethods(physicianClass.methods); var expectedMethods = [ 'prototype.__findById__widgets(fk:any):widget ' + @@ -214,15 +163,8 @@ describe('remoting - integration', function() { it('should have correct signatures for hasMany-through methods', function() { // jscs:disable validateIndentation - - var physicianClass = findClass('physician'); - var methods = physicianClass.methods - .filter(function(m) { - return m.name.indexOf('prototype.__') === 0; - }) - .map(function(m) { - return formatMethod(m); - }); + var physicianClass = findClass('physician'); + var methods = getFormattedPrototypeMethods(physicianClass.methods); var expectedMethods = [ 'prototype.__findById__patients(fk:any):patient ' + @@ -251,3 +193,127 @@ describe('remoting - integration', function() { }); }); + +describe('With model.settings.replaceOnPUT false', function() { + lt.beforeEach.withApp(app); + lt.beforeEach.givenModel('storeWithReplaceOnPUTfalse'); + afterEach(function(done) { + this.app.models.storeWithReplaceOnPUTfalse.destroyAll(done); + }); + + it('should have expected remote methods', + function() { + var storeClass = findClass('storeWithReplaceOnPUTfalse'); + var methods = getFormattedMethodsExcludingRelations(storeClass.methods); + + var expectedMethods = [ + 'upsert(data:object):storeWithReplaceOnPUTfalse PATCH /stores-updating', + 'upsert(data:object):storeWithReplaceOnPUTfalse PUT /stores-updating', + 'replaceOrCreate(data:object):storeWithReplaceOnPUTfalse POST /stores-updating/replaceOrCreate', + 'replaceById(id:any,data:object):storeWithReplaceOnPUTfalse POST /stores-updating/:id/replace', + 'prototype.updateAttributes(data:object):storeWithReplaceOnPUTfalse PATCH /stores-updating/:id', + 'prototype.updateAttributes(data:object):storeWithReplaceOnPUTfalse PUT /stores-updating/:id', + ]; + + expect(methods).to.include.members(expectedMethods); + }); +}); + +describe('With model.settings.replaceOnPUT true', function() { + lt.beforeEach.withApp(app); + lt.beforeEach.givenModel('storeWithReplaceOnPUTtrue'); + afterEach(function(done) { + this.app.models.storeWithReplaceOnPUTtrue.destroyAll(done); + }); + + it('should have expected remote methods', + function() { + var storeClass = findClass('storeWithReplaceOnPUTtrue'); + var methods = getFormattedMethodsExcludingRelations(storeClass.methods); + + var expectedMethods = [ + 'upsert(data:object):storeWithReplaceOnPUTtrue PATCH /stores-replacing', + 'replaceOrCreate(data:object):storeWithReplaceOnPUTtrue POST /stores-replacing/replaceOrCreate', + 'replaceOrCreate(data:object):storeWithReplaceOnPUTtrue PUT /stores-replacing', + 'replaceById(id:any,data:object):storeWithReplaceOnPUTtrue POST /stores-replacing/:id/replace', + 'replaceById(id:any,data:object):storeWithReplaceOnPUTtrue PUT /stores-replacing/:id', + 'prototype.updateAttributes(data:object):storeWithReplaceOnPUTtrue PATCH /stores-replacing/:id', + ]; + + expect(methods).to.include.members(expectedMethods); + }); +}); + +function formatReturns(m) { + var returns = m.returns; + if (!returns || returns.length === 0) { + return ''; + } + var type = returns[0].type; + return type ? ':' + type : ''; +} + +function formatMethod(m) { + var arr = []; + var endpoints = m.getEndpoints(); + for (var i = 0; i < endpoints.length; i++) { + arr.push([ + m.name, + '(', + m.accepts.map(function(a) { + return a.arg + ':' + a.type; + }).join(','), + ')', + formatReturns(m), + ' ', + endpoints[i].verb, + ' ', + endpoints[i].fullPath, + ].join('')); + } + return arr; +} + +function findClass(name) { + return app.handler('rest').adapter + .getClasses() + .filter(function(c) { + return c.name === name; + })[0]; +} + +function getFormattedMethodsExcludingRelations(methods) { + return methods.filter(function(m) { + return m.name.indexOf('__') === -1; + }) + .map(function(m) { + return formatMethod(m); + }) + .reduce(function(p, c) { + return p.concat(c); + }); +} + +function getFormattedScopeMethods(methods) { + return methods.filter(function(m) { + return m.name.indexOf('__') === 0; + }) + .map(function(m) { + return formatMethod(m); + }) + .reduce(function(p, c) { + return p.concat(c); + }); +} + +function getFormattedPrototypeMethods(methods) { + return methods.filter(function(m) { + return m.name.indexOf('prototype.__') === 0; + }) + .map(function(m) { + return formatMethod(m); + }) + .reduce(function(p, c) { + return p.concat(c); + }); +} From 7932d75c447f0ed4a916582192de34ff74a23bcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 15 Aug 2016 10:57:02 +0200 Subject: [PATCH 068/187] Revert globalization of Swagger descriptions --- common/models/user.js | 16 +++++----- lib/model.js | 47 ++++++++++++++-------------- lib/persisted-model.js | 69 +++++++++++++++++------------------------- 3 files changed, 60 insertions(+), 72 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index b857e38e5..210df6db1 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -660,12 +660,12 @@ module.exports = function(User) { UserModel.remoteMethod( 'login', { - description: g.f('Login a user with username/email and password.'), + description: 'Login a user with username/email and password.', accepts: [ {arg: 'credentials', type: 'object', required: true, http: {source: 'body'}}, {arg: 'include', type: ['string'], http: {source: 'query'}, - description: g.f('Related objects to include in the response. ' + - 'See the description of return value for more details.') }, + description: 'Related objects to include in the response. ' + + 'See the description of return value for more details.' }, ], returns: { arg: 'accessToken', type: 'object', root: true, @@ -683,15 +683,15 @@ module.exports = function(User) { UserModel.remoteMethod( 'logout', { - description: g.f('Logout a user with access token.'), + description: 'Logout a user with access token.', accepts: [ {arg: 'access_token', type: 'string', required: true, http: function(ctx) { var req = ctx && ctx.req; var accessToken = req && req.accessToken; var tokenID = accessToken && accessToken.id; return tokenID; - }, description: g.f('Do not supply this argument, it is automatically extracted ' + - 'from request headers.'), + }, description: 'Do not supply this argument, it is automatically extracted ' + + 'from request headers.', }, ], http: {verb: 'all'} @@ -701,7 +701,7 @@ module.exports = function(User) { UserModel.remoteMethod( 'confirm', { - description: g.f('Confirm a user registration with email verification token.'), + description: 'Confirm a user registration with email verification token.', accepts: [ {arg: 'uid', type: 'string', required: true}, {arg: 'token', type: 'string', required: true}, @@ -714,7 +714,7 @@ module.exports = function(User) { UserModel.remoteMethod( 'resetPassword', { - description: g.f('Reset password for a user with email.'), + description: 'Reset password for a user with email.', accepts: [ {arg: 'options', type: 'object', required: true, http: {source: 'body'}} ], diff --git a/lib/model.js b/lib/model.js index 3b5fe4c4b..5651c0e7a 100644 --- a/lib/model.js +++ b/lib/model.js @@ -13,6 +13,7 @@ var assert = require('assert'); var RemoteObjects = require('strong-remoting'); var SharedClass = require('strong-remoting').SharedClass; var extend = require('util')._extend; +var format = require('util').format; module.exports = function(registry) { @@ -452,7 +453,7 @@ module.exports = function(registry) { http: {verb: 'get', path: '/' + pathName}, accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}}, accessType: 'READ', - description: g.f('Fetches belongsTo relation %s.', relationName), + description: format('Fetches belongsTo relation %s.', relationName), returns: {arg: relationName, type: modelName, root: true}, }, fn); }; @@ -476,7 +477,7 @@ module.exports = function(registry) { isStatic: false, http: {verb: 'get', path: '/' + pathName}, accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}}, - description: g.f('Fetches hasOne relation %s.', relationName), + description: format('Fetches hasOne relation %s.', relationName), accessType: 'READ', returns: {arg: relationName, type: relation.modelTo.modelName, root: true}, rest: {after: convertNullToNotFoundError.bind(null, toModelName)} @@ -486,7 +487,7 @@ module.exports = function(registry) { isStatic: false, http: {verb: 'post', path: '/' + pathName}, accepts: {arg: 'data', type: toModelName, http: {source: 'body'}}, - description: g.f('Creates a new instance in %s of this model.', relationName), + description: format('Creates a new instance in %s of this model.', relationName), accessType: 'WRITE', returns: {arg: 'data', type: toModelName, root: true} }); @@ -495,7 +496,7 @@ module.exports = function(registry) { isStatic: false, http: {verb: 'put', path: '/' + pathName}, accepts: {arg: 'data', type: toModelName, http: {source: 'body'}}, - description: g.f('Update %s of this model.', relationName), + description: format('Update %s of this model.', relationName), accessType: 'WRITE', returns: {arg: 'data', type: toModelName, root: true} }); @@ -503,7 +504,7 @@ module.exports = function(registry) { define('__destroy__' + relationName, { isStatic: false, http: {verb: 'delete', path: '/' + pathName}, - description: g.f('Deletes %s of this model.', relationName), + description: format('Deletes %s of this model.', relationName), accessType: 'WRITE', }); }; @@ -517,10 +518,10 @@ module.exports = function(registry) { isStatic: false, http: {verb: 'get', path: '/' + pathName + '/:fk'}, accepts: {arg: 'fk', type: 'any', - description: g.f('Foreign key for %s', relationName), + description: format('Foreign key for %s', relationName), required: true, http: {source: 'path'}}, - description: g.f('Find a related item by id for %s.', relationName), + description: format('Find a related item by id for %s.', relationName), accessType: 'READ', returns: {arg: 'result', type: toModelName, root: true}, rest: {after: convertNullToNotFoundError.bind(null, toModelName)} @@ -531,10 +532,10 @@ module.exports = function(registry) { isStatic: false, http: {verb: 'delete', path: '/' + pathName + '/:fk'}, accepts: { arg: 'fk', type: 'any', - description: g.f('Foreign key for %s', relationName), + description: format('Foreign key for %s', relationName), required: true, http: {source: 'path'}}, - description: g.f('Delete a related item by id for %s.', relationName), + description: format('Delete a related item by id for %s.', relationName), accessType: 'WRITE', returns: [] }, destroyByIdFunc); @@ -545,12 +546,12 @@ module.exports = function(registry) { http: {verb: 'put', path: '/' + pathName + '/:fk'}, accepts: [ {arg: 'fk', type: 'any', - description: g.f('Foreign key for %s', relationName), + description: format('Foreign key for %s', relationName), required: true, http: { source: 'path' }}, {arg: 'data', type: toModelName, http: {source: 'body'}}, ], - description: g.f('Update a related item by id for %s.', relationName), + description: format('Update a related item by id for %s.', relationName), accessType: 'WRITE', returns: {arg: 'result', type: toModelName, root: true} }, updateByIdFunc); @@ -569,10 +570,10 @@ module.exports = function(registry) { isStatic: false, http: {verb: 'put', path: '/' + pathName + '/rel/:fk'}, accepts: [{ arg: 'fk', type: 'any', - description: g.f('Foreign key for %s', relationName), + description: format('Foreign key for %s', relationName), required: true, http: {source: 'path'}}].concat(accepts), - description: g.f('Add a related item by id for %s.', relationName), + description: format('Add a related item by id for %s.', relationName), accessType: 'WRITE', returns: {arg: relationName, type: modelThrough.modelName, root: true} }, addFunc); @@ -582,10 +583,10 @@ module.exports = function(registry) { isStatic: false, http: {verb: 'delete', path: '/' + pathName + '/rel/:fk'}, accepts: {arg: 'fk', type: 'any', - description: g.f('Foreign key for %s', relationName), + description: format('Foreign key for %s', relationName), required: true, http: {source: 'path'}}, - description: g.f('Remove the %s relation to an item by id.', relationName), + description: format('Remove the %s relation to an item by id.', relationName), accessType: 'WRITE', returns: [] }, removeFunc); @@ -597,10 +598,10 @@ module.exports = function(registry) { isStatic: false, http: {verb: 'head', path: '/' + pathName + '/rel/:fk'}, accepts: {arg: 'fk', type: 'any', - description: g.f('Foreign key for %s', relationName), + description: format('Foreign key for %s', relationName), required: true, http: {source: 'path'}}, - description: g.f('Check the existence of %s relation to an item by id.', relationName), + description: format('Check the existence of %s relation to an item by id.', relationName), accessType: 'READ', returns: {arg: 'exists', type: 'boolean', root: true}, rest: { @@ -643,7 +644,7 @@ module.exports = function(registry) { isStatic: isStatic, http: {verb: 'get', path: '/' + pathName}, accepts: {arg: 'filter', type: 'object'}, - description: g.f('Queries %s of %s.', scopeName, this.modelName), + description: format('Queries %s of %s.', scopeName, this.modelName), accessType: 'READ', returns: {arg: scopeName, type: [toModelName], root: true} }); @@ -652,7 +653,7 @@ module.exports = function(registry) { isStatic: isStatic, http: {verb: 'post', path: '/' + pathName}, accepts: {arg: 'data', type: toModelName, http: {source: 'body'}}, - description: g.f('Creates a new instance in %s of this model.', scopeName), + description: format('Creates a new instance in %s of this model.', scopeName), accessType: 'WRITE', returns: {arg: 'data', type: toModelName, root: true} }); @@ -660,7 +661,7 @@ module.exports = function(registry) { define('__delete__' + scopeName, { isStatic: isStatic, http: {verb: 'delete', path: '/' + pathName}, - description: g.f('Deletes all %s of this model.', scopeName), + description: format('Deletes all %s of this model.', scopeName), accessType: 'WRITE', }); @@ -668,8 +669,8 @@ module.exports = function(registry) { isStatic: isStatic, http: {verb: 'get', path: '/' + pathName + '/count'}, accepts: {arg: 'where', type: 'object', - description: g.f('Criteria to match model instances')}, - description: g.f('Counts %s of %s.', scopeName, this.modelName), + description: 'Criteria to match model instances'}, + description: format('Counts %s of %s.', scopeName, this.modelName), accessType: 'READ', returns: {arg: 'count', type: 'number'} }); @@ -718,7 +719,7 @@ module.exports = function(registry) { acceptArgs = [ { arg: paramName, type: 'any', http: { source: 'path' }, - description: g.f('Foreign key for %s.', relation.name), + description: format('Foreign key for %s.', relation.name), required: true, }, ]; diff --git a/lib/persisted-model.js b/lib/persisted-model.js index 534a41e01..d3e9a6fe5 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -16,18 +16,6 @@ var debug = require('debug')('loopback:persisted-model'); var PassThrough = require('stream').PassThrough; var utils = require('./utils'); -// workaround for low performance of strong-globalize -// see https://github.com/strongloop/strong-globalize/issues/66 -var stringCache = Object.create(null); -g.s = function(str) { - assert.equal(1, arguments.length, 'g.s() does not support parameters'); - if (str in stringCache) - return stringCache[str]; - var result = g.t(str); - stringCache[str] = result; - return result; -}; - module.exports = function(registry) { var Model = registry.getModel('Model'); @@ -627,7 +615,7 @@ module.exports = function(registry) { } setRemoting(PersistedModel, 'create', { - description: g.s('Create a new instance of the model and persist it into the data source.'), + description: 'Create a new instance of the model and persist it into the data source.', accessType: 'WRITE', accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}}, returns: {arg: 'data', type: typeName, root: true}, @@ -636,7 +624,7 @@ module.exports = function(registry) { var upsertOptions = { aliases: ['patchOrCreate', 'updateOrCreate'], - description: g.s('Patch an existing model instance or insert a new one into the data source.'), + description: 'Patch an existing model instance or insert a new one into the data source.', accessType: 'WRITE', accepts: { arg: 'data', type: 'object', http: { source: 'body' }, description: 'Model instance data' }, @@ -665,7 +653,7 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'replaceOrCreate', replaceOrCreateOptions); setRemoting(PersistedModel, 'exists', { - description: g.s('Check whether a model instance exists in the data source.'), + description: 'Check whether a model instance exists in the data source.', accessType: 'READ', accepts: {arg: 'id', type: 'any', description: 'Model id', required: true}, returns: {arg: 'exists', type: 'boolean'}, @@ -696,13 +684,13 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'findById', { - description: g.s('Find a model instance by {{id}} from the data source.'), + description: 'Find a model instance by {{id}} from the data source.', accessType: 'READ', accepts: [ { arg: 'id', type: 'any', description: 'Model id', required: true, http: {source: 'path'}}, { arg: 'filter', type: 'object', - description: g.s('Filter defining fields and include') }, + description: 'Filter defining fields and include' }, ], returns: {arg: 'data', type: typeName, root: true}, http: {verb: 'get', path: '/:id'}, @@ -729,7 +717,7 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'replaceById', replaceByIdOptions); setRemoting(PersistedModel, 'find', { - description: g.s('Find all instances of the model matched by filter from the data source.'), + description: 'Find all instances of the model matched by filter from the data source.', accessType: 'READ', accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, include, order, offset, and limit'}, returns: {arg: 'data', type: [typeName], root: true}, @@ -737,7 +725,7 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'findOne', { - description: g.s('Find first instance of the model matched by filter from the data source.'), + description: 'Find first instance of the model matched by filter from the data source.', accessType: 'READ', accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, include, order, offset, and limit'}, returns: {arg: 'data', type: typeName, root: true}, @@ -746,7 +734,7 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'destroyAll', { - description: g.s('Delete all matching records.'), + description: 'Delete all matching records.', accessType: 'WRITE', accepts: {arg: 'where', type: 'object', description: 'filter.where object'}, returns: { @@ -761,17 +749,17 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'updateAll', { aliases: ['update'], - description: g.s('Update instances of the model matched by {{where}} from the data source.'), + description: 'Update instances of the model matched by {{where}} from the data source.', accessType: 'WRITE', accepts: [ {arg: 'where', type: 'object', http: { source: 'query'}, - description: g.s('Criteria to match model instances')}, + description: 'Criteria to match model instances'}, {arg: 'data', type: 'object', http: {source: 'body'}, - description: g.s('An object of model property name/value pairs')}, + description: 'An object of model property name/value pairs'}, ], returns: { arg: 'count', - description: g.s('The number of instances updated'), + description: 'The number of instances updated', type: 'object', root: true }, @@ -780,7 +768,7 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'deleteById', { aliases: ['destroyById', 'removeById'], - description: g.s('Delete a model instance by {{id}} from the data source.'), + description: 'Delete a model instance by {{id}} from the data source.', accessType: 'WRITE', accepts: {arg: 'id', type: 'any', description: 'Model id', required: true, http: {source: 'path'}}, @@ -789,7 +777,7 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'count', { - description: g.s('Count instances of the model matched by where from the data source.'), + description: 'Count instances of the model matched by where from the data source.', accessType: 'READ', accepts: {arg: 'where', type: 'object', description: 'Criteria to match model instances'}, returns: {arg: 'count', type: 'number'}, @@ -798,7 +786,7 @@ module.exports = function(registry) { var updateAttributesOptions = { aliases: ['patchAttributes'], - description: g.s('Patch attributes for a model instance and persist it into the data source.'), + description: 'Patch attributes for a model instance and persist it into the data source.', accessType: 'WRITE', accepts: { arg: 'data', type: 'object', http: { source: 'body' }, description: 'An object of model property name/value pairs' }, @@ -814,7 +802,7 @@ module.exports = function(registry) { if (options.trackChanges || options.enableRemoteReplication) { setRemoting(PersistedModel, 'diff', { - description: g.s('Get a set of deltas and conflicts since the given checkpoint.'), + description: 'Get a set of deltas and conflicts since the given checkpoint.', accessType: 'READ', accepts: [ {arg: 'since', type: 'number', description: 'Find deltas since this checkpoint'}, @@ -826,8 +814,8 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'changes', { - description: g.s('Get the changes to a model since a given checkpoint.' + - 'Provide a filter object to reduce the number of results returned.'), + description: 'Get the changes to a model since a given checkpoint.' + + 'Provide a filter object to reduce the number of results returned.', accessType: 'READ', accepts: [ {arg: 'since', type: 'number', description: 'Only return changes since this checkpoint'}, @@ -838,7 +826,7 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'checkpoint', { - description: g.s('Create a checkpoint.'), + description: 'Create a checkpoint.', // The replication algorithm needs to create a source checkpoint, // even though it is otherwise not making any source changes. // We need to allow this method for users that don't have full @@ -849,14 +837,14 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'currentCheckpoint', { - description: g.s('Get the current checkpoint.'), + description: 'Get the current checkpoint.', accessType: 'READ', returns: {arg: 'checkpoint', type: 'object', root: true}, http: {verb: 'get', path: '/checkpoint'} }); setRemoting(PersistedModel, 'createUpdates', { - description: g.s('Create an update list from a delta list.'), + description: 'Create an update list from a delta list.', // This operation is read-only, it does not change any local data. // It is called by the replication algorithm to compile a list // of changes to apply on the target. @@ -867,14 +855,14 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'bulkUpdate', { - description: g.s('Run multiple updates at once. Note: this is not atomic.'), + description: 'Run multiple updates at once. Note: this is not atomic.', accessType: 'WRITE', accepts: {arg: 'updates', type: 'array'}, http: {verb: 'post', path: '/bulk-update'} }); setRemoting(PersistedModel, 'findLastChange', { - description: g.s('Get the most recent change record for this instance.'), + description: 'Get the most recent change record for this instance.', accessType: 'READ', accepts: { arg: 'id', type: 'any', required: true, http: { source: 'path' }, @@ -885,10 +873,9 @@ module.exports = function(registry) { }); setRemoting(PersistedModel, 'updateLastChange', { - description: [ - g.s('Update the properties of the most recent change record ' + - 'kept for this instance.'), - ], + description: + 'Update the properties of the most recent change record ' + + 'kept for this instance.', accessType: 'WRITE', accepts: [ { @@ -897,7 +884,7 @@ module.exports = function(registry) { }, { arg: 'data', type: 'object', http: {source: 'body'}, - description: g.s('An object of Change property name/value pairs'), + description: 'An object of Change property name/value pairs', }, ], returns: { arg: 'result', type: this.Change.modelName, root: true }, @@ -923,7 +910,7 @@ module.exports = function(registry) { } setRemoting(PersistedModel, 'createChangeStream', { - description: g.s('Create a change stream.'), + description: 'Create a change stream.', accessType: 'READ', http: [ {verb: 'post', path: '/change-stream'}, From 7f0219183857b547df6e79db9a5578b013e4633f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 16 Aug 2016 16:15:01 +0200 Subject: [PATCH 069/187] 2.30.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert globalization of Swagger descriptions (Miroslav Bajtoš) * Expose `Replace*` methods (Amir Jafarian) * Add bcrypt validation (Loay) * Cache remoting descriptions to speed up tests (Miroslav Bajtoš) * Revert globalization of assert() messages (Miroslav Bajtoš) * Fix token middleware to not trigger CLS init (Miroslav Bajtoš) * common: add KeyValueModel (Miroslav Bajtoš) * Globalize current-context deprecation messages (Miroslav Bajtoš) * Deprecate current-context API (Miroslav Bajtoš) * test: increase timeout to prevent CI failures (Miroslav Bajtoš) * Backport of #2407 (Candy) * test: fix timeout in rest.middleware.test (Miroslav Bajtoš) * test: fix "socket hang up" error in app.test (Miroslav Bajtoš) * test: increate timeout in Role test (Miroslav Bajtoš) * test: make status test more robust (Miroslav Bajtoš) * test: fix broken Role tests (Miroslav Bajtoš) * Update dependencies to their latest versions (Miroslav Bajtoš) * Increase timeout (jannyHou) * Backport of #2565 (Miroslav Bajtoš) * Avoid calling deprecated methds (Amir Jafarian) * test: use local registry in test fixtures (Miroslav Bajtoš) * Fix test case error (Loay) * Backport/Fix security issue 580 (Loay) --- CHANGES.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 42456aaba..01be461e7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,53 @@ +2016-08-16, Version 2.30.0 +========================== + + * Revert globalization of Swagger descriptions (Miroslav Bajtoš) + + * Expose `Replace*` methods (Amir Jafarian) + + * Add bcrypt validation (Loay) + + * Cache remoting descriptions to speed up tests (Miroslav Bajtoš) + + * Revert globalization of assert() messages (Miroslav Bajtoš) + + * Fix token middleware to not trigger CLS init (Miroslav Bajtoš) + + * common: add KeyValueModel (Miroslav Bajtoš) + + * Globalize current-context deprecation messages (Miroslav Bajtoš) + + * Deprecate current-context API (Miroslav Bajtoš) + + * test: increase timeout to prevent CI failures (Miroslav Bajtoš) + + * Backport of #2407 (Candy) + + * test: fix timeout in rest.middleware.test (Miroslav Bajtoš) + + * test: fix "socket hang up" error in app.test (Miroslav Bajtoš) + + * test: increate timeout in Role test (Miroslav Bajtoš) + + * test: make status test more robust (Miroslav Bajtoš) + + * test: fix broken Role tests (Miroslav Bajtoš) + + * Update dependencies to their latest versions (Miroslav Bajtoš) + + * Increase timeout (jannyHou) + + * Backport of #2565 (Miroslav Bajtoš) + + * Avoid calling deprecated methds (Amir Jafarian) + + * test: use local registry in test fixtures (Miroslav Bajtoš) + + * Fix test case error (Loay) + + * Backport/Fix security issue 580 (Loay) + + 2016-07-12, Version 2.29.1 ========================== diff --git a/package.json b/package.json index f17cf8c01..6e8686e22 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.29.1", + "version": "2.30.0", "description": "LoopBack: Open Source Framework for Node.js", "homepage": "http://loopback.io/", "keywords": [ From ba2fe0ee052e3e22f6f6e45366a4feb5168e7bc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20F=C3=BCrstenberg?= Date: Tue, 16 Aug 2016 13:51:42 +0200 Subject: [PATCH 070/187] Fix token middleware crash Fix token middleware to check if `req.loopbackContext` is active. The context is not active for example when express-session calls setImmediate which breaks CLS. --- package.json | 1 + server/middleware/token.js | 2 +- test/access-token.test.js | 25 +++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 6e8686e22..78b12e50f 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "chai": "^3.5.0", "es5-shim": "^4.1.0", "eslint-config-loopback": "^1.0.0", + "express-session": "^1.14.0", "grunt": "^1.0.1", "grunt-browserify": "^5.0.0", "grunt-cli": "^1.2.0", diff --git a/server/middleware/token.js b/server/middleware/token.js index 21e3b5cab..2ceb701d4 100644 --- a/server/middleware/token.js +++ b/server/middleware/token.js @@ -125,7 +125,7 @@ function token(options) { req.accessToken = token || null; rewriteUserLiteral(req, currentUserLiteral); var ctx = req.loopbackContext; - if (ctx) ctx.set('accessToken', token); + if (ctx && ctx.active) ctx.set('accessToken', token); next(err); }); }; diff --git a/test/access-token.test.js b/test/access-token.test.js index aec2b86a1..917994da0 100644 --- a/test/access-token.test.js +++ b/test/access-token.test.js @@ -6,6 +6,8 @@ var cookieParser = require('cookie-parser'); var loopback = require('../'); var extend = require('util')._extend; +var session = require('express-session'); + var Token = loopback.AccessToken.extend('MyToken'); var ds = loopback.createDataSource({connector: loopback.Memory}); Token.attachTo(ds); @@ -507,6 +509,29 @@ describe('app.enableAuth()', function() { done(); }); }); + + // See https://github.com/strongloop/loopback-context/issues/6 + it('checks whether context is active', function(done) { + var app = loopback(); + + app.enableAuth(); + app.use(loopback.context()); + app.use(session({ + secret: 'kitty', + saveUninitialized: true, + resave: true + })); + app.use(loopback.token({ model: Token })); + app.get('/', function(req, res) { res.send('OK'); }); + app.use(loopback.rest()); + + request(app) + .get('/') + .set('authorization', this.token.id) + .set('cookie', 'connect.sid=s%3AFTyno9_MbGTJuOwdh9bxsYCVxlhlulTZ.PZvp85jzLXZBCBkhCsSfuUjhij%2Fb0B1K2RYZdxSQU0c') + .expect(200, 'OK') + .end(done); + }); }); function createTestingToken(done) { From 56fa9829f7dcaf8cf68958963f9b26cf3097ee45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 17 Aug 2016 16:26:40 +0200 Subject: [PATCH 071/187] 2.31.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix token middleware crash (Carl Fürstenberg) * Support 'alias' in mail transport config. (Samuel Reed) --- CHANGES.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 01be461e7..0dde50558 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,11 @@ +2016-08-17, Version 2.31.0 +========================== + + * Fix token middleware crash (Carl Fürstenberg) + + * Support 'alias' in mail transport config. (Samuel Reed) + + 2016-08-16, Version 2.30.0 ========================== diff --git a/package.json b/package.json index 78b12e50f..83465fd57 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.30.0", + "version": "2.31.0", "description": "LoopBack: Open Source Framework for Node.js", "homepage": "http://loopback.io/", "keywords": [ From b221af7cf66a0b3110d61c0b6052b1ab6bc5ad99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 16 Aug 2016 14:56:15 +0200 Subject: [PATCH 072/187] KeyValueModel: add API for listing keys - Expose "keys()" at "GET /keys" - Add a dummy implementation for "iterateKeys" to serve a useful error message when the model is not attached correctly. --- common/models/key-value-model.js | 19 +++++++++++++++++++ test/key-value-model.test.js | 14 ++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/common/models/key-value-model.js b/common/models/key-value-model.js index 200465ca1..c7fbbbe3f 100644 --- a/common/models/key-value-model.js +++ b/common/models/key-value-model.js @@ -16,6 +16,16 @@ module.exports = function(KeyValueModel) { throwNotAttached(this.modelName, 'expire'); }; + // TODO add api docs + KeyValueModel.keys = function(filter, options, callback) { + throwNotAttached(this.modelName, 'keys'); + }; + + // TODO add api docs + KeyValueModel.iterateKeys = function(filter, options) { + throwNotAttached(this.modelName, 'iterateKeys'); + }; + KeyValueModel.setup = function() { KeyValueModel.base.setup.apply(this, arguments); @@ -51,6 +61,15 @@ module.exports = function(KeyValueModel) { ], http: { path: '/:key/expire', verb: 'put' }, }); + + this.remoteMethod('keys', { + accepts: { + arg: 'filter', type: 'object', required: false, + http: { source: 'query' }, + }, + returns: { arg: 'keys', type: ['string'], root: true }, + http: { path: '/keys', verb: 'get' }, + }); }; }; diff --git a/test/key-value-model.test.js b/test/key-value-model.test.js index e81ed63c7..6ca2e1e23 100644 --- a/test/key-value-model.test.js +++ b/test/key-value-model.test.js @@ -80,6 +80,20 @@ describe('KeyValueModel', function() { .send({ ttl: 10 }) .expect(404, done); }); + + it('provides "keys(filter)" at "GET /keys"', function(done) { + CacheItem.set('list-key', AN_OBJECT_VALUE, function(err) { + if (err) return done(err); + request.get('/CacheItems/keys') + .send(AN_OBJECT_VALUE) + .end(function(err, res) { + if (err) return done(err); + if (res.body.error) return done(res.body.error); + expect(res.body).to.eql(['list-key']); + done(); + }); + }); + }); }); function setupAppAndCacheItem() { From c538aa764da5ba2e9d46f1995104721e2b2d7a2d Mon Sep 17 00:00:00 2001 From: Benjamin Kroeger Date: Tue, 16 Aug 2016 17:02:34 +0200 Subject: [PATCH 073/187] resolve related models from correct registry Also modify setup of test servers when ACL was used, force the app to `loadBuiltinModels` with localRegistry. --- common/models/acl.js | 12 ++++---- test/access-control.integration.js | 29 +++++++++++++++++++ .../access-control/common/models/bank.json | 6 ++++ test/fixtures/access-control/server/server.js | 5 +++- .../user-integration-app/server/server.js | 6 +++- 5 files changed, 50 insertions(+), 8 deletions(-) diff --git a/common/models/acl.js b/common/models/acl.js index e9340d76b..19d5241cc 100644 --- a/common/models/acl.js +++ b/common/models/acl.js @@ -394,7 +394,8 @@ module.exports = function(ACL) { */ ACL.checkAccessForContext = function(context, callback) { - var registry = this.registry; + this.resolveRelatedModels(); + var roleModel = this.roleModel; if (!(context instanceof AccessContext)) { context = new AccessContext(context); @@ -420,7 +421,6 @@ module.exports = function(ACL) { var staticACLs = this.getStaticACLs(model.modelName, property); var self = this; - var roleModel = registry.getModelByType(Role); this.find({where: {model: model.modelName, property: propertyQuery, accessType: accessTypeQuery}}, function(err, acls) { if (err) { @@ -507,10 +507,10 @@ module.exports = function(ACL) { ACL.resolveRelatedModels = function() { if (!this.roleModel) { var reg = this.registry; - this.roleModel = reg.getModelByType(loopback.Role); - this.roleMappingModel = reg.getModelByType(loopback.RoleMapping); - this.userModel = reg.getModelByType(loopback.User); - this.applicationModel = reg.getModelByType(loopback.Application); + this.roleModel = reg.getModelByType('Role'); + this.roleMappingModel = reg.getModelByType('RoleMapping'); + this.userModel = reg.getModelByType('User'); + this.applicationModel = reg.getModelByType('Application'); } }; diff --git a/test/access-control.integration.js b/test/access-control.integration.js index 3fe2e9c57..b2fe33116 100644 --- a/test/access-control.integration.js +++ b/test/access-control.integration.js @@ -152,6 +152,34 @@ describe('access control - integration', function() { }); describe('/banks', function() { + var SPECIAL_USER = { email: 'special@test.test', password: 'test' }; + + // define dynamic role that would only grant access when the authenticated user's email is equal to + // SPECIAL_USER's email + + before(function() { + var roleModel = app.registry.getModel('Role'); + var userModel = app.registry.getModel('user'); + + roleModel.registerResolver('$dynamic-role', function(role, context, callback) { + if (!(context && context.accessToken && context.accessToken.userId)) { + return process.nextTick(function() { + callback && callback(null, false); + }); + } + var accessToken = context.accessToken; + userModel.findById(accessToken.userId, function(err, user) { + if (err) { + return callback(err, false); + } + if (user && user.email === SPECIAL_USER.email) { + return callback(null, true); + } + return callback(null, false); + }); + }); + }); + lt.beforeEach.givenModel('bank'); lt.it.shouldBeAllowedWhenCalledAnonymously('GET', '/api/banks'); @@ -173,6 +201,7 @@ describe('access control - integration', function() { lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForBank); lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForBank); lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForBank); + lt.it.shouldBeAllowedWhenCalledByUser(SPECIAL_USER, 'DELETE', urlForBank); function urlForBank() { return '/api/banks/' + this.bank.id; diff --git a/test/fixtures/access-control/common/models/bank.json b/test/fixtures/access-control/common/models/bank.json index a06555b89..f31c78764 100644 --- a/test/fixtures/access-control/common/models/bank.json +++ b/test/fixtures/access-control/common/models/bank.json @@ -22,6 +22,12 @@ "permission": "ALLOW", "principalType": "ROLE", "principalId": "$everyone" + }, + { + "accessType": "WRITE", + "permission": "ALLOW", + "principalType": "ROLE", + "principalId": "$dynamic-role" } ], "properties": {} diff --git a/test/fixtures/access-control/server/server.js b/test/fixtures/access-control/server/server.js index ba596a361..988fb5c39 100644 --- a/test/fixtures/access-control/server/server.js +++ b/test/fixtures/access-control/server/server.js @@ -5,7 +5,10 @@ var loopback = require('../../../..'); var boot = require('loopback-boot'); -var app = module.exports = loopback({ localRegistry: true }); +var app = module.exports = loopback({ + localRegistry: true, + loadBuiltinModels: true +}); boot(app, __dirname); diff --git a/test/fixtures/user-integration-app/server/server.js b/test/fixtures/user-integration-app/server/server.js index bfd7e1afc..4f636e7a3 100644 --- a/test/fixtures/user-integration-app/server/server.js +++ b/test/fixtures/user-integration-app/server/server.js @@ -5,7 +5,11 @@ var loopback = require('../../../../index'); var boot = require('loopback-boot'); -var app = module.exports = loopback({ localRegistry: true }); +var app = module.exports = loopback({ + localRegistry: true, + loadBuiltinModels: true +}); + app.enableAuth(); boot(app, __dirname); app.use(loopback.token({model: app.models.AccessToken})); From ecd881a0f3bf77348b051bafec4742b5eb85a223 Mon Sep 17 00:00:00 2001 From: Benjamin Kroeger Date: Tue, 16 Aug 2016 17:03:21 +0200 Subject: [PATCH 074/187] streamline use if `self` --- common/models/acl.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/common/models/acl.js b/common/models/acl.js index 19d5241cc..96cd79297 100644 --- a/common/models/acl.js +++ b/common/models/acl.js @@ -394,8 +394,9 @@ module.exports = function(ACL) { */ ACL.checkAccessForContext = function(context, callback) { - this.resolveRelatedModels(); - var roleModel = this.roleModel; + var self = this; + self.resolveRelatedModels(); + var roleModel = self.roleModel; if (!(context instanceof AccessContext)) { context = new AccessContext(context); @@ -418,10 +419,9 @@ module.exports = function(ACL) { var req = new AccessRequest(modelName, property, accessType, ACL.DEFAULT, methodNames); var effectiveACLs = []; - var staticACLs = this.getStaticACLs(model.modelName, property); + var staticACLs = self.getStaticACLs(model.modelName, property); - var self = this; - this.find({where: {model: model.modelName, property: propertyQuery, + self.find({where: {model: model.modelName, property: propertyQuery, accessType: accessTypeQuery}}, function(err, acls) { if (err) { if (callback) callback(err); From 55eb8d72e6ae3420067f150cbd33448f90bc7c0b Mon Sep 17 00:00:00 2001 From: Amir Jafarian Date: Wed, 24 Aug 2016 16:19:06 -0400 Subject: [PATCH 075/187] Reorder PATCH Vs PUT endpoints * Reorder PATCH Vs PUT endpoints for update* methods * Backport of #2670 --- lib/persisted-model.js | 4 ++-- test/remoting.integration.js | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/persisted-model.js b/lib/persisted-model.js index d3e9a6fe5..fe2d7c5d5 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -633,7 +633,7 @@ module.exports = function(registry) { }; if (!options.replaceOnPUT) { - upsertOptions.http.push({ verb: 'put', path: '/' }); + upsertOptions.http.unshift({ verb: 'put', path: '/' }); } setRemoting(PersistedModel, 'upsert', upsertOptions); @@ -797,7 +797,7 @@ module.exports = function(registry) { setRemoting(PersistedModel.prototype, 'updateAttributes', updateAttributesOptions); if (!options.replaceOnPUT) { - updateAttributesOptions.http.push({ verb: 'put', path: '/' }); + updateAttributesOptions.http.unshift({ verb: 'put', path: '/' }); } if (options.trackChanges || options.enableRemoteReplication) { diff --git a/test/remoting.integration.js b/test/remoting.integration.js index 59b3ca07c..c0c4cd316 100644 --- a/test/remoting.integration.js +++ b/test/remoting.integration.js @@ -207,15 +207,25 @@ describe('With model.settings.replaceOnPUT false', function() { var methods = getFormattedMethodsExcludingRelations(storeClass.methods); var expectedMethods = [ - 'upsert(data:object):storeWithReplaceOnPUTfalse PATCH /stores-updating', + 'create(data:object):storeWithReplaceOnPUTfalse POST /stores-updating', 'upsert(data:object):storeWithReplaceOnPUTfalse PUT /stores-updating', + 'upsert(data:object):storeWithReplaceOnPUTfalse PATCH /stores-updating', 'replaceOrCreate(data:object):storeWithReplaceOnPUTfalse POST /stores-updating/replaceOrCreate', + 'exists(id:any):boolean GET /stores-updating/:id/exists', + 'exists(id:any):boolean HEAD /stores-updating/:id', + 'findById(id:any,filter:object):storeWithReplaceOnPUTfalse GET /stores-updating/:id', 'replaceById(id:any,data:object):storeWithReplaceOnPUTfalse POST /stores-updating/:id/replace', - 'prototype.updateAttributes(data:object):storeWithReplaceOnPUTfalse PATCH /stores-updating/:id', + 'find(filter:object):storeWithReplaceOnPUTfalse GET /stores-updating', + 'findOne(filter:object):storeWithReplaceOnPUTfalse GET /stores-updating/findOne', + 'updateAll(where:object,data:object):object POST /stores-updating/update', + 'deleteById(id:any):object DELETE /stores-updating/:id', + 'count(where:object):number GET /stores-updating/count', 'prototype.updateAttributes(data:object):storeWithReplaceOnPUTfalse PUT /stores-updating/:id', + 'prototype.updateAttributes(data:object):storeWithReplaceOnPUTfalse PATCH /stores-updating/:id', + 'createChangeStream(options:object):ReadableStream POST /stores-updating/change-stream', + 'createChangeStream(options:object):ReadableStream GET /stores-updating/change-stream', ]; - - expect(methods).to.include.members(expectedMethods); + expect(methods).to.eql(expectedMethods); }); }); From 6e71a52e9026377b1b634327aed94ecaad11a90d Mon Sep 17 00:00:00 2001 From: Subramanian Krishnan Date: Fri, 26 Aug 2016 16:23:38 +0530 Subject: [PATCH 076/187] Make the app instance available to connectors --- lib/application.js | 1 + test/app.test.js | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/lib/application.js b/lib/application.js index f5df78713..6f6cb6826 100644 --- a/lib/application.js +++ b/lib/application.js @@ -236,6 +236,7 @@ app.dataSource = function(name, config) { this.dataSources[name] = this.dataSources[classify(name)] = this.dataSources[camelize(name)] = ds; + ds.app = this; return ds; } catch (err) { if (err.message) { diff --git a/test/app.test.js b/test/app.test.js index fd2abfd89..ab1a09232 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -820,6 +820,12 @@ describe('app', function() { app.dataSource('bad-ds', { connector: 'throwing' }); }).to.throw(/bad-ds.*throwing/); }); + + it('adds app reference to the data source object', function() { + app.dataSource('ds', { connector: 'memory' }); + expect(app.datasources.ds.app).to.not.equal(undefined); + expect(app.datasources.ds.app).to.equal(app); + }); }); describe.onServer('listen()', function() { From 069d3e8f2f33d4db50a0cecad6d060fb2684c778 Mon Sep 17 00:00:00 2001 From: Candy Date: Mon, 29 Aug 2016 10:10:39 -0400 Subject: [PATCH 077/187] Apply g.f to literal strings Backport #2684 --- common/models/user.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index 210df6db1..89017c453 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -436,25 +436,20 @@ module.exports = function(User) { function sendEmail(user) { options.verifyHref += '&token=' + user.verificationToken; - options.text = options.text || 'Please verify your email by opening this link in a web browser:\n\t{href}'; + options.text = options.text || g.f('Please verify your email by opening ' + + 'this link in a web browser:\n\t%s', options.verifyHref); options.text = options.text.replace(/\{href\}/g, options.verifyHref); - options.text = g.f(options.text); - options.to = options.to || user.email; - options.subject = options.subject || 'Thanks for Registering'; - - options.subject = g.f(options.subject); + options.subject = options.subject || g.f('Thanks for Registering'); options.headers = options.headers || {}; var template = loopback.template(options.template); options.html = template(options); - options.html = g.f(options.html); - Email.send(options, function(err, email) { if (err) { fn(err); From 07a04b71da1a87e3bb5e58b6bbf41d88256a56e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 31 Aug 2016 15:21:39 +0200 Subject: [PATCH 078/187] app.enableAuth: correctly detect attached models Fix a typo in "app.enableAuth" that caused the method to not detect the situation when e.g. the built-in User model is already attached to a datasource. --- lib/application.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/application.js b/lib/application.js index f5df78713..61015f3b2 100644 --- a/lib/application.js +++ b/lib/application.js @@ -327,8 +327,10 @@ app.enableAuth = function(options) { g.f('Authentication requires model %s to be defined.', m)); } - if (m.dataSource || m.app) return; + if (Model.dataSource || Model.app) return; + // Find descendants of Model that are attached, + // for example "Customer" extending "User" model for (var name in appModels) { var candidate = appModels[name]; var isSubclass = candidate.prototype instanceof Model; From bc10d68c541839ca75ef8db2612dc96320bef59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 31 Aug 2016 15:23:48 +0200 Subject: [PATCH 079/187] test/user: don't attach User model twice --- test/user.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/user.test.js b/test/user.test.js index 0ac361bfe..c638f41b0 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -66,7 +66,6 @@ describe('User', function() { app.enableAuth({ dataSource: 'db' }); app.use(loopback.token({ model: AccessToken })); app.use(loopback.rest()); - app.model(User); User.create(validCredentials, function(err, user) { if (err) return done(err); From eb434124394b8f48ac3190ce32d7ba319b292e1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 5 Sep 2016 15:06:18 +0200 Subject: [PATCH 080/187] 2.32.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test/user: don't attach User model twice (Miroslav Bajtoš) * app.enableAuth: correctly detect attached models (Miroslav Bajtoš) * Apply g.f to literal strings (Candy) * Make the app instance available to connectors (Subramanian Krishnan) * Reorder PATCH Vs PUT endpoints (Amir Jafarian) * streamline use if `self` (Benjamin Kroeger) * resolve related models from correct registry (Benjamin Kroeger) * KeyValueModel: add API for listing keys (Miroslav Bajtoš) --- CHANGES.md | 20 ++++++++++++++++++++ package.json | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 0dde50558..d1b2bc464 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,23 @@ +2016-09-05, Version 2.32.0 +========================== + + * test/user: don't attach User model twice (Miroslav Bajtoš) + + * app.enableAuth: correctly detect attached models (Miroslav Bajtoš) + + * Apply g.f to literal strings (Candy) + + * Make the app instance available to connectors (Subramanian Krishnan) + + * Reorder PATCH Vs PUT endpoints (Amir Jafarian) + + * streamline use if `self` (Benjamin Kroeger) + + * resolve related models from correct registry (Benjamin Kroeger) + + * KeyValueModel: add API for listing keys (Miroslav Bajtoš) + + 2016-08-17, Version 2.31.0 ========================== diff --git a/package.json b/package.json index 83465fd57..ab76a659d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.31.0", + "version": "2.32.0", "description": "LoopBack: Open Source Framework for Node.js", "homepage": "http://loopback.io/", "keywords": [ From 381222bf7a9f82c39696829e92d10571263b73b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 6 Sep 2016 12:50:03 +0200 Subject: [PATCH 081/187] Rework email validation to use isemail Drop hand-crafted RegExp in favour of a 3rd-party module that supports RFC5321, RFC5322 and other relevant standards. --- common/models/user.js | 19 +++++++++++++++---- package.json | 1 + test/user.test.js | 17 ++++++++++++----- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index 89017c453..ea35f6b13 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -9,6 +9,7 @@ var g = require('strong-globalize')(); +var isEmail = require('isemail'); var loopback = require('../../lib/loopback'); var utils = require('../../lib/utils'); var path = require('path'); @@ -735,10 +736,9 @@ module.exports = function(User) { assert(loopback.AccessToken, 'AccessToken model must be defined before User model'); UserModel.accessToken = loopback.AccessToken; - // email validation regex - var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - - UserModel.validatesFormatOf('email', {with: re, message: g.f('Must provide a valid email')}); + UserModel.validate('email', emailValidator, { + message: g.f('Must provide a valid email') + }); // FIXME: We need to add support for uniqueness of composite keys in juggler if (!(UserModel.settings.realmRequired || UserModel.settings.realmDelimiter)) { @@ -756,3 +756,14 @@ module.exports = function(User) { User.setup(); }; + +function emailValidator(err, done) { + var value = this.email; + if (value == null) + return; + if (typeof value !== 'string') + return err('string'); + if (value === '') return; + if (!isEmail(value)) + return err('email'); +} diff --git a/package.json b/package.json index ab76a659d..e9eed0604 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "errorhandler": "^1.3.4", "express": "^4.12.2", "inflection": "^1.6.0", + "isemail": "^1.2.0", "loopback-connector-remote": "^1.0.3", "loopback-context": "^1.0.0", "loopback-phase": "^1.2.0", diff --git a/test/user.test.js b/test/user.test.js index c638f41b0..c22f8039c 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -132,10 +132,7 @@ describe('User', function() { assert.equal(err.name, 'ValidationError'); assert.equal(err.statusCode, 422); assert.equal(err.details.context, User.modelName); - assert.deepEqual(err.details.codes.email, [ - 'presence', - 'format.null' - ]); + assert.deepEqual(err.details.codes.email, ['presence']); done(); }); @@ -155,11 +152,21 @@ describe('User', function() { it('Requires a valid email', function(done) { User.create({email: 'foo@', password: '123'}, function(err) { assert(err); - + assert.equal(err.name, 'ValidationError'); + assert.equal(err.statusCode, 422); + assert.equal(err.details.context, User.modelName); + assert.deepEqual(err.details.codes.email, ['custom.email']); done(); }); }); + it('allows TLD domains in email', function() { + return User.create({ + email: 'local@com', + password: '123' + }); + }); + it('Requires a unique email', function(done) { User.create({email: 'a@b.com', password: 'foobar'}, function() { User.create({email: 'a@b.com', password: 'batbaz'}, function(err) { From fcfdb73bdbf6dbee6cdaaf173aac4fcbe5c345e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 2 Sep 2016 14:40:11 +0200 Subject: [PATCH 082/187] Fix remoting metadata for "data" arguments Fix the definition of "data" argument to { type: 'object', model: modelName, ... } That way strong-remoting passed the request body directly to the model method (does not create a new model instance), but the swagger will still provide correct schema for these arguments. This fixes a bug where upsert in relation methods was adding default property values to request payload. --- lib/model.js | 10 ++-- lib/persisted-model.js | 18 +++--- .../common/models/widget.json | 9 ++- test/relations.integration.js | 26 +++++++++ test/remoting.integration.js | 58 +++++++++---------- 5 files changed, 78 insertions(+), 43 deletions(-) diff --git a/lib/model.js b/lib/model.js index 5651c0e7a..3930a9092 100644 --- a/lib/model.js +++ b/lib/model.js @@ -486,7 +486,7 @@ module.exports = function(registry) { define('__create__' + relationName, { isStatic: false, http: {verb: 'post', path: '/' + pathName}, - accepts: {arg: 'data', type: toModelName, http: {source: 'body'}}, + accepts: {arg: 'data', type: 'object', model: toModelName, http: {source: 'body'}}, description: format('Creates a new instance in %s of this model.', relationName), accessType: 'WRITE', returns: {arg: 'data', type: toModelName, root: true} @@ -495,7 +495,7 @@ module.exports = function(registry) { define('__update__' + relationName, { isStatic: false, http: {verb: 'put', path: '/' + pathName}, - accepts: {arg: 'data', type: toModelName, http: {source: 'body'}}, + accepts: {arg: 'data', type: 'object', model: toModelName, http: {source: 'body'}}, description: format('Update %s of this model.', relationName), accessType: 'WRITE', returns: {arg: 'data', type: toModelName, root: true} @@ -549,7 +549,7 @@ module.exports = function(registry) { description: format('Foreign key for %s', relationName), required: true, http: { source: 'path' }}, - {arg: 'data', type: toModelName, http: {source: 'body'}}, + {arg: 'data', type: 'object', model: toModelName, http: {source: 'body'}}, ], description: format('Update a related item by id for %s.', relationName), accessType: 'WRITE', @@ -562,7 +562,7 @@ module.exports = function(registry) { var accepts = []; if (relation.type === 'hasMany' && relation.modelThrough) { // Restrict: only hasManyThrough relation can have additional properties - accepts.push({arg: 'data', type: modelThrough.modelName, http: {source: 'body'}}); + accepts.push({arg: 'data', type: 'object', model: modelThrough.modelName, http: {source: 'body'}}); } var addFunc = this.prototype['__link__' + relationName]; @@ -652,7 +652,7 @@ module.exports = function(registry) { define('__create__' + scopeName, { isStatic: isStatic, http: {verb: 'post', path: '/' + pathName}, - accepts: {arg: 'data', type: toModelName, http: {source: 'body'}}, + accepts: {arg: 'data', type: 'object', model: toModelName, http: {source: 'body'}}, description: format('Creates a new instance in %s of this model.', scopeName), accessType: 'WRITE', returns: {arg: 'data', type: toModelName, root: true} diff --git a/lib/persisted-model.js b/lib/persisted-model.js index fe2d7c5d5..a1946dd12 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -617,7 +617,7 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'create', { description: 'Create a new instance of the model and persist it into the data source.', accessType: 'WRITE', - accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}}, + accepts: {arg: 'data', type: 'object', model: typeName, description: 'Model instance data', http: {source: 'body'}}, returns: {arg: 'data', type: typeName, root: true}, http: {verb: 'post', path: '/'} }); @@ -626,7 +626,7 @@ module.exports = function(registry) { aliases: ['patchOrCreate', 'updateOrCreate'], description: 'Patch an existing model instance or insert a new one into the data source.', accessType: 'WRITE', - accepts: { arg: 'data', type: 'object', http: { source: 'body' }, description: + accepts: { arg: 'data', type: 'object', model: typeName, http: { source: 'body' }, description: 'Model instance data' }, returns: { arg: 'data', type: typeName, root: true }, http: [{ verb: 'patch', path: '/' }], @@ -640,7 +640,7 @@ module.exports = function(registry) { var replaceOrCreateOptions = { description: 'Replace an existing model instance or insert a new one into the data source.', accessType: 'WRITE', - accepts: { arg: 'data', type: 'object', http: { source: 'body' }, description: + accepts: { arg: 'data', type: 'object', model: typeName, http: { source: 'body' }, description: 'Model instance data' }, returns: { arg: 'data', type: typeName, root: true }, http: [{ verb: 'post', path: '/replaceOrCreate' }], @@ -703,7 +703,7 @@ module.exports = function(registry) { accepts: [ { arg: 'id', type: 'any', description: 'Model id', required: true, http: { source: 'path' }}, - { arg: 'data', type: 'object', http: { source: 'body' }, description: + { arg: 'data', type: 'object', model: typeName, http: { source: 'body' }, description: 'Model instance data' }, ], returns: { arg: 'data', type: typeName, root: true }, @@ -754,7 +754,7 @@ module.exports = function(registry) { accepts: [ {arg: 'where', type: 'object', http: { source: 'query'}, description: 'Criteria to match model instances'}, - {arg: 'data', type: 'object', http: {source: 'body'}, + {arg: 'data', type: 'object', model: typeName, http: {source: 'body'}, description: 'An object of model property name/value pairs'}, ], returns: { @@ -789,7 +789,11 @@ module.exports = function(registry) { description: 'Patch attributes for a model instance and persist it into the data source.', accessType: 'WRITE', - accepts: { arg: 'data', type: 'object', http: { source: 'body' }, description: 'An object of model property name/value pairs' }, + accepts: { + arg: 'data', type: 'object', model: typeName, + http: { source: 'body' }, + description: 'An object of model property name/value pairs' + }, returns: { arg: 'data', type: typeName, root: true }, http: [{ verb: 'patch', path: '/' }], }; @@ -883,7 +887,7 @@ module.exports = function(registry) { description: 'Model id' }, { - arg: 'data', type: 'object', http: {source: 'body'}, + arg: 'data', type: 'object', model: typeName, http: {source: 'body'}, description: 'An object of Change property name/value pairs', }, ], diff --git a/test/fixtures/simple-integration-app/common/models/widget.json b/test/fixtures/simple-integration-app/common/models/widget.json index 018c0e1da..f9eaa1947 100644 --- a/test/fixtures/simple-integration-app/common/models/widget.json +++ b/test/fixtures/simple-integration-app/common/models/widget.json @@ -1,10 +1,15 @@ { "name": "widget", - "properties": {}, + "properties": { + "name": { + "type": "string", + "default": "DefaultWidgetName" + } + }, "relations": { "store": { "model": "store", "type": "belongsTo" } } -} \ No newline at end of file +} diff --git a/test/relations.integration.js b/test/relations.integration.js index ba4ea15ed..9871edae5 100644 --- a/test/relations.integration.js +++ b/test/relations.integration.js @@ -243,6 +243,32 @@ describe('relations - integration', function() { }); }); }); + + describe('PUT /api/store/:id/widgets/:fk', function() { + beforeEach(function(done) { + var self = this; + this.store.widgets.create({ + name: this.widgetName + }, function(err, widget) { + self.widget = widget; + self.url = '/api/stores/' + self.store.id + '/widgets/' + widget.id; + done(); + }); + }); + it('does not add default properties to request body', function(done) { + var self = this; + self.request.put(self.url) + .send({ active: true }) + .end(function(err) { + if (err) return done(err); + app.models.Widget.findById(self.widget.id, function(err, w) { + if (err) return done(err); + expect(w.name).to.equal(self.widgetName); + done(); + }); + }); + }); + }); }); describe('/stores/:id/widgets/:fk - 200', function() { diff --git a/test/remoting.integration.js b/test/remoting.integration.js index c0c4cd316..3319e87a9 100644 --- a/test/remoting.integration.js +++ b/test/remoting.integration.js @@ -87,20 +87,20 @@ describe('remoting - integration', function() { var methods = getFormattedMethodsExcludingRelations(storeClass.methods); var expectedMethods = [ - 'create(data:object):store POST /stores', - 'upsert(data:object):store PATCH /stores', - 'upsert(data:object):store PUT /stores', - 'replaceOrCreate(data:object):store POST /stores/replaceOrCreate', + 'create(data:object:store):store POST /stores', + 'upsert(data:object:store):store PATCH /stores', + 'upsert(data:object:store):store PUT /stores', + 'replaceOrCreate(data:object:store):store POST /stores/replaceOrCreate', 'exists(id:any):boolean GET /stores/:id/exists', 'findById(id:any,filter:object):store GET /stores/:id', - 'prototype.updateAttributes(data:object):store PUT /stores/:id', - 'replaceById(id:any,data:object):store POST /stores/:id/replace', + 'prototype.updateAttributes(data:object:store):store PUT /stores/:id', + 'replaceById(id:any,data:object:store):store POST /stores/:id/replace', 'find(filter:object):store GET /stores', 'findOne(filter:object):store GET /stores/findOne', - 'updateAll(where:object,data:object):object POST /stores/update', + 'updateAll(where:object,data:object:store):object POST /stores/update', 'deleteById(id:any):object DELETE /stores/:id', 'count(where:object):number GET /stores/count', - 'prototype.updateAttributes(data:object):store PATCH /stores/:id', + 'prototype.updateAttributes(data:object:store):store PATCH /stores/:id', 'createChangeStream(options:object):ReadableStream POST /stores/change-stream', ]; @@ -115,7 +115,7 @@ describe('remoting - integration', function() { var expectedMethods = [ '__get__superStores(filter:object):store GET /stores/superStores', - '__create__superStores(data:store):store POST /stores/superStores', + '__create__superStores(data:object:store):store POST /stores/superStores', '__delete__superStores() DELETE /stores/superStores', '__count__superStores(where:object):number GET /stores/superStores/count' ]; @@ -147,11 +147,11 @@ describe('remoting - integration', function() { 'GET /stores/:id/widgets/:fk', 'prototype.__destroyById__widgets(fk:any) ' + 'DELETE /stores/:id/widgets/:fk', - 'prototype.__updateById__widgets(fk:any,data:widget):widget ' + + 'prototype.__updateById__widgets(fk:any,data:object:widget):widget ' + 'PUT /stores/:id/widgets/:fk', 'prototype.__get__widgets(filter:object):widget ' + 'GET /stores/:id/widgets', - 'prototype.__create__widgets(data:widget):widget ' + + 'prototype.__create__widgets(data:object:widget):widget ' + 'POST /stores/:id/widgets', 'prototype.__delete__widgets() ' + 'DELETE /stores/:id/widgets', @@ -171,9 +171,9 @@ describe('remoting - integration', function() { 'GET /physicians/:id/patients/:fk', 'prototype.__destroyById__patients(fk:any) ' + 'DELETE /physicians/:id/patients/:fk', - 'prototype.__updateById__patients(fk:any,data:patient):patient ' + + 'prototype.__updateById__patients(fk:any,data:object:patient):patient ' + 'PUT /physicians/:id/patients/:fk', - 'prototype.__link__patients(fk:any,data:appointment):appointment ' + + 'prototype.__link__patients(fk:any,data:object:appointment):appointment ' + 'PUT /physicians/:id/patients/rel/:fk', 'prototype.__unlink__patients(fk:any) ' + 'DELETE /physicians/:id/patients/rel/:fk', @@ -181,7 +181,7 @@ describe('remoting - integration', function() { 'HEAD /physicians/:id/patients/rel/:fk', 'prototype.__get__patients(filter:object):patient ' + 'GET /physicians/:id/patients', - 'prototype.__create__patients(data:patient):patient ' + + 'prototype.__create__patients(data:object:patient):patient ' + 'POST /physicians/:id/patients', 'prototype.__delete__patients() ' + 'DELETE /physicians/:id/patients', @@ -207,21 +207,21 @@ describe('With model.settings.replaceOnPUT false', function() { var methods = getFormattedMethodsExcludingRelations(storeClass.methods); var expectedMethods = [ - 'create(data:object):storeWithReplaceOnPUTfalse POST /stores-updating', - 'upsert(data:object):storeWithReplaceOnPUTfalse PUT /stores-updating', - 'upsert(data:object):storeWithReplaceOnPUTfalse PATCH /stores-updating', - 'replaceOrCreate(data:object):storeWithReplaceOnPUTfalse POST /stores-updating/replaceOrCreate', + 'create(data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse POST /stores-updating', + 'upsert(data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse PUT /stores-updating', + 'upsert(data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse PATCH /stores-updating', + 'replaceOrCreate(data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse POST /stores-updating/replaceOrCreate', 'exists(id:any):boolean GET /stores-updating/:id/exists', 'exists(id:any):boolean HEAD /stores-updating/:id', 'findById(id:any,filter:object):storeWithReplaceOnPUTfalse GET /stores-updating/:id', - 'replaceById(id:any,data:object):storeWithReplaceOnPUTfalse POST /stores-updating/:id/replace', + 'replaceById(id:any,data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse POST /stores-updating/:id/replace', 'find(filter:object):storeWithReplaceOnPUTfalse GET /stores-updating', 'findOne(filter:object):storeWithReplaceOnPUTfalse GET /stores-updating/findOne', - 'updateAll(where:object,data:object):object POST /stores-updating/update', + 'updateAll(where:object,data:object:storeWithReplaceOnPUTfalse):object POST /stores-updating/update', 'deleteById(id:any):object DELETE /stores-updating/:id', 'count(where:object):number GET /stores-updating/count', - 'prototype.updateAttributes(data:object):storeWithReplaceOnPUTfalse PUT /stores-updating/:id', - 'prototype.updateAttributes(data:object):storeWithReplaceOnPUTfalse PATCH /stores-updating/:id', + 'prototype.updateAttributes(data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse PUT /stores-updating/:id', + 'prototype.updateAttributes(data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse PATCH /stores-updating/:id', 'createChangeStream(options:object):ReadableStream POST /stores-updating/change-stream', 'createChangeStream(options:object):ReadableStream GET /stores-updating/change-stream', ]; @@ -242,12 +242,12 @@ describe('With model.settings.replaceOnPUT true', function() { var methods = getFormattedMethodsExcludingRelations(storeClass.methods); var expectedMethods = [ - 'upsert(data:object):storeWithReplaceOnPUTtrue PATCH /stores-replacing', - 'replaceOrCreate(data:object):storeWithReplaceOnPUTtrue POST /stores-replacing/replaceOrCreate', - 'replaceOrCreate(data:object):storeWithReplaceOnPUTtrue PUT /stores-replacing', - 'replaceById(id:any,data:object):storeWithReplaceOnPUTtrue POST /stores-replacing/:id/replace', - 'replaceById(id:any,data:object):storeWithReplaceOnPUTtrue PUT /stores-replacing/:id', - 'prototype.updateAttributes(data:object):storeWithReplaceOnPUTtrue PATCH /stores-replacing/:id', + 'upsert(data:object:storeWithReplaceOnPUTtrue):storeWithReplaceOnPUTtrue PATCH /stores-replacing', + 'replaceOrCreate(data:object:storeWithReplaceOnPUTtrue):storeWithReplaceOnPUTtrue POST /stores-replacing/replaceOrCreate', + 'replaceOrCreate(data:object:storeWithReplaceOnPUTtrue):storeWithReplaceOnPUTtrue PUT /stores-replacing', + 'replaceById(id:any,data:object:storeWithReplaceOnPUTtrue):storeWithReplaceOnPUTtrue POST /stores-replacing/:id/replace', + 'replaceById(id:any,data:object:storeWithReplaceOnPUTtrue):storeWithReplaceOnPUTtrue PUT /stores-replacing/:id', + 'prototype.updateAttributes(data:object:storeWithReplaceOnPUTtrue):storeWithReplaceOnPUTtrue PATCH /stores-replacing/:id', ]; expect(methods).to.include.members(expectedMethods); @@ -271,7 +271,7 @@ function formatMethod(m) { m.name, '(', m.accepts.map(function(a) { - return a.arg + ':' + a.type; + return a.arg + ':' + a.type + (a.model ? ':' + a.model : ''); }).join(','), ')', formatReturns(m), From 4c013deaae37bd8b59e15f555aff09e41488f6bd Mon Sep 17 00:00:00 2001 From: Sonali Samantaray Date: Fri, 2 Sep 2016 18:10:47 +0530 Subject: [PATCH 083/187] Expose upsertWithWhere Backport of #2539 --- lib/model.js | 2 ++ lib/persisted-model.js | 37 ++++++++++++++++++++++++++++ test/access-control.integration.js | 8 ++++++ test/data-source.test.js | 2 ++ test/model.test.js | 39 ++++++++++++++++++++++++++++++ test/remoting.integration.js | 9 +++++++ test/replication.test.js | 13 ++++++++++ 7 files changed, 110 insertions(+) diff --git a/lib/model.js b/lib/model.js index 3930a9092..61fe5c3fa 100644 --- a/lib/model.js +++ b/lib/model.js @@ -366,6 +366,8 @@ module.exports = function(registry) { return ACL.WRITE; case 'updateOrCreate': return ACL.WRITE; + case 'upsertWithWhere': + return ACL.WRITE; case 'upsert': return ACL.WRITE; case 'exists': diff --git a/lib/persisted-model.js b/lib/persisted-model.js index a1946dd12..e2b1968d1 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -118,6 +118,28 @@ module.exports = function(registry) { throwNotAttached(this.modelName, 'upsert'); }; + /** + * Update or insert a model instance based on the search criteria. + * If there is a single instance retrieved, update the retrieved model. + * Creates a new model if no model instances were found. + * Returns an error if multiple instances are found. + * * @param {Object} [where] `where` filter, like + * ``` + * { key: val, key2: {gt: 'val2'}, ...} + * ``` + *
              see + * [Where filter](https://docs.strongloop.com/display/LB/Where+filter#Wherefilter-Whereclauseforothermethods). + * @param {Object} data The model instance data to insert. + * @callback {Function} callback Callback function called with `cb(err, obj)` signature. + * @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object). + * @param {Object} model Updated model instance. + */ + + PersistedModel.upsertWithWhere = + PersistedModel.patchOrCreateWithWhere = function upsertWithWhere(where, data, callback) { + throwNotAttached(this.modelName, 'upsertWithWhere'); + }; + /** * Replace or insert a model instance; replace existing record if one is found, * such that parameter `data.id` matches `id` of model instance; otherwise, @@ -652,6 +674,21 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'replaceOrCreate', replaceOrCreateOptions); + setRemoting(PersistedModel, 'upsertWithWhere', { + aliases: ['patchOrCreateWithWhere'], + description: 'Update an existing model instance or insert a new one into ' + + 'the data source based on the where criteria.', + accessType: 'WRITE', + accepts: [ + { arg: 'where', type: 'object', http: { source: 'query' }, + description: 'Criteria to match model instances' }, + { arg: 'data', type: 'object', http: { source: 'body' }, + description: 'An object of model property name/value pairs' }, + ], + returns: { arg: 'data', type: typeName, root: true }, + http: { verb: 'post', path: '/upsertWithWhere' }, + }); + setRemoting(PersistedModel, 'exists', { description: 'Check whether a model instance exists in the data source.', accessType: 'READ', diff --git a/test/access-control.integration.js b/test/access-control.integration.js index b2fe33116..b9ce7b376 100644 --- a/test/access-control.integration.js +++ b/test/access-control.integration.js @@ -132,6 +132,10 @@ describe('access control - integration', function() { }); }); + lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/users/upsertWithWhere'); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/users/upsertWithWhere'); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/users/upsertWithWhere'); + lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForUser); lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForUser); lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForUser); @@ -203,6 +207,10 @@ describe('access control - integration', function() { lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForBank); lt.it.shouldBeAllowedWhenCalledByUser(SPECIAL_USER, 'DELETE', urlForBank); + lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/banks/upsertWithWhere'); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/banks/upsertWithWhere'); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/banks/upsertWithWhere'); + function urlForBank() { return '/api/banks/' + this.bank.id; } diff --git a/test/data-source.test.js b/test/data-source.test.js index 19ce7a87e..a3ec55ef0 100644 --- a/test/data-source.test.js +++ b/test/data-source.test.js @@ -22,6 +22,7 @@ describe('DataSource', function() { assert.isFunc(Color, 'findOne'); assert.isFunc(Color, 'create'); assert.isFunc(Color, 'updateOrCreate'); + assert.isFunc(Color, 'upsertWithWhere'); assert.isFunc(Color, 'upsert'); assert.isFunc(Color, 'findOrCreate'); assert.isFunc(Color, 'exists'); @@ -83,6 +84,7 @@ describe('DataSource', function() { existsAndShared('_forDB', false); existsAndShared('create', true); existsAndShared('updateOrCreate', true); + existsAndShared('upsertWithWhere', true); existsAndShared('upsert', true); existsAndShared('findOrCreate', false); existsAndShared('exists', true); diff --git a/test/model.test.js b/test/model.test.js index ef7dd9c26..dadfb728f 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -152,6 +152,43 @@ describe.onServer('Remote Methods', function() { }); }); + describe('Model.upsertWithWhere(where, data, callback)', function() { + it('Updates when a Model instance is retreived from data source', function(done) { + var taskEmitter = new TaskEmitter(); + taskEmitter + .task(User, 'create', { first: 'jill', second: 'pill' }) + .task(User, 'create', { first: 'bob', second: 'sob' }) + .on('done', function() { + User.upsertWithWhere({ second: 'pill' }, { second: 'jones' }, function(err, user) { + if (err) return done(err); + var id = user.id; + User.findById(id, function(err, user) { + if (err) return done(err); + assert.equal(user.second, 'jones'); + done(); + }); + }); + }); + }); + + it('Creates when no Model instance is retreived from data source', function(done) { + var taskEmitter = new TaskEmitter(); + taskEmitter + .task(User, 'create', { first: 'simon', second: 'somers' }) + .on('done', function() { + User.upsertWithWhere({ first: 'somers' }, { first: 'Simon' }, function(err, user) { + if (err) return done(err); + var id = user.id; + User.findById(id, function(err, user) { + if (err) return done(err); + assert.equal(user.first, 'Simon'); + done(); + }); + }); + }); + }); + }); + describe('Example Remote Method', function() { it('Call the method using HTTP / REST', function(done) { request(app) @@ -523,6 +560,7 @@ describe.onServer('Remote Methods', function() { describe('Model.checkAccessTypeForMethod(remoteMethod)', function() { shouldReturn('create', ACL.WRITE); shouldReturn('updateOrCreate', ACL.WRITE); + shouldReturn('upsertWithWhere', ACL.WRITE); shouldReturn('upsert', ACL.WRITE); shouldReturn('exists', ACL.READ); shouldReturn('findById', ACL.READ); @@ -643,6 +681,7 @@ describe.onServer('Remote Methods', function() { // 'destroyAll', 'deleteAll', 'remove', 'create', 'upsert', 'updateOrCreate', 'patchOrCreate', + 'upsertWithWhere', 'patchOrCreateWithWhere', 'exists', 'findById', 'replaceById', diff --git a/test/remoting.integration.js b/test/remoting.integration.js index 3319e87a9..5d1dbc72e 100644 --- a/test/remoting.integration.js +++ b/test/remoting.integration.js @@ -192,6 +192,14 @@ describe('remoting - integration', function() { }); }); + it('has upsertWithWhere remote method', function() { + var storeClass = findClass('store'); + var methods = getFormattedMethodsExcludingRelations(storeClass.methods); + var expectedMethods = [ + 'upsertWithWhere(where:object,data:object):store POST /stores/upsertWithWhere', + ]; + expect(methods).to.include.members(expectedMethods); + }); }); describe('With model.settings.replaceOnPUT false', function() { @@ -211,6 +219,7 @@ describe('With model.settings.replaceOnPUT false', function() { 'upsert(data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse PUT /stores-updating', 'upsert(data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse PATCH /stores-updating', 'replaceOrCreate(data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse POST /stores-updating/replaceOrCreate', + 'upsertWithWhere(where:object,data:object):storeWithReplaceOnPUTfalse POST /stores-updating/upsertWithWhere', 'exists(id:any):boolean GET /stores-updating/:id/exists', 'exists(id:any):boolean HEAD /stores-updating/:id', 'findById(id:any,filter:object):storeWithReplaceOnPUTfalse GET /stores-updating/:id', diff --git a/test/replication.test.js b/test/replication.test.js index 13e8b0d4a..3a9585b02 100644 --- a/test/replication.test.js +++ b/test/replication.test.js @@ -1010,6 +1010,19 @@ describe('Replication / Change APIs', function() { }); }); + it('detects "upsertWithWhere"', function(done) { + givenReplicatedInstance(function(err, inst) { + if (err) return done(err); + SourceModel.upsertWithWhere( + { name: inst.name }, + { name: 'updated' }, + function(err) { + if (err) return done(err); + assertChangeRecordedForId(inst.id, done); + }); + }); + }); + it('detects "findOrCreate"', function(done) { // make sure we bypass find+create and call the connector directly SourceModel.dataSource.connector.findOrCreate = From 4d6f2da5785908ac34d481a680caf5008563e752 Mon Sep 17 00:00:00 2001 From: Amir Jafarian Date: Wed, 7 Sep 2016 13:23:53 -0400 Subject: [PATCH 084/187] Fix data argument for upsertWithWhere * Related PR: #2727 --- lib/persisted-model.js | 2 +- test/remoting.integration.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/persisted-model.js b/lib/persisted-model.js index e2b1968d1..3e4530e6f 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -682,7 +682,7 @@ module.exports = function(registry) { accepts: [ { arg: 'where', type: 'object', http: { source: 'query' }, description: 'Criteria to match model instances' }, - { arg: 'data', type: 'object', http: { source: 'body' }, + { arg: 'data', type: 'object', model: typeName, http: { source: 'body' }, description: 'An object of model property name/value pairs' }, ], returns: { arg: 'data', type: typeName, root: true }, diff --git a/test/remoting.integration.js b/test/remoting.integration.js index 5d1dbc72e..41ec366e5 100644 --- a/test/remoting.integration.js +++ b/test/remoting.integration.js @@ -196,7 +196,7 @@ describe('remoting - integration', function() { var storeClass = findClass('store'); var methods = getFormattedMethodsExcludingRelations(storeClass.methods); var expectedMethods = [ - 'upsertWithWhere(where:object,data:object):store POST /stores/upsertWithWhere', + 'upsertWithWhere(where:object,data:object:store):store POST /stores/upsertWithWhere', ]; expect(methods).to.include.members(expectedMethods); }); @@ -219,7 +219,7 @@ describe('With model.settings.replaceOnPUT false', function() { 'upsert(data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse PUT /stores-updating', 'upsert(data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse PATCH /stores-updating', 'replaceOrCreate(data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse POST /stores-updating/replaceOrCreate', - 'upsertWithWhere(where:object,data:object):storeWithReplaceOnPUTfalse POST /stores-updating/upsertWithWhere', + 'upsertWithWhere(where:object,data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse POST /stores-updating/upsertWithWhere', 'exists(id:any):boolean GET /stores-updating/:id/exists', 'exists(id:any):boolean HEAD /stores-updating/:id', 'findById(id:any,filter:object):storeWithReplaceOnPUTfalse GET /stores-updating/:id', From 8f642b593cc43b97afb05cff54da32018c223d05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 9 Sep 2016 10:25:18 +0200 Subject: [PATCH 085/187] 2.33.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix data argument for upsertWithWhere (Amir Jafarian) * Expose upsertWithWhere (Sonali Samantaray) * Fix remoting metadata for "data" arguments (Miroslav Bajtoš) * Rework email validation to use isemail (Miroslav Bajtoš) --- CHANGES.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index d1b2bc464..e38394aaf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,15 @@ +2016-09-09, Version 2.33.0 +========================== + + * Fix data argument for upsertWithWhere (Amir Jafarian) + + * Expose upsertWithWhere (Sonali Samantaray) + + * Fix remoting metadata for "data" arguments (Miroslav Bajtoš) + + * Rework email validation to use isemail (Miroslav Bajtoš) + + 2016-09-05, Version 2.32.0 ========================== diff --git a/package.json b/package.json index e9eed0604..b1ee9d16a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.32.0", + "version": "2.33.0", "description": "LoopBack: Open Source Framework for Node.js", "homepage": "http://loopback.io/", "keywords": [ From 7d1f31cfb4f64190f9fd6182f6ee90b4449901ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 12 Sep 2016 11:27:35 +0200 Subject: [PATCH 086/187] 2.34.0 --- CHANGES.md | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index e38394aaf..566a2596d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,5 @@ + + 2016-09-09, Version 2.33.0 ========================== diff --git a/package.json b/package.json index b1ee9d16a..32c3f4e1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.33.0", + "version": "2.34.0", "description": "LoopBack: Open Source Framework for Node.js", "homepage": "http://loopback.io/", "keywords": [ From c4214024bee00f5058cd897ab335ee38d5cdf789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 12 Sep 2016 13:21:07 +0200 Subject: [PATCH 087/187] Upgrade loopback-testing to the latest ^1.4 --- package.json | 2 +- test/access-control.integration.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 32c3f4e1d..f0fd5c5b4 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "karma-script-launcher": "^1.0.0", "loopback-boot": "^2.7.0", "loopback-datasource-juggler": "^2.19.1", - "loopback-testing": "~1.1.0", + "loopback-testing": "^1.4.0", "mocha": "^3.0.0", "phantomjs-prebuilt": "^2.1.7", "sinon": "^1.13.0", diff --git a/test/access-control.integration.js b/test/access-control.integration.js index b9ce7b376..23bcbf5c3 100644 --- a/test/access-control.integration.js +++ b/test/access-control.integration.js @@ -24,6 +24,7 @@ describe('access control - integration', function() { }); lt.beforeEach.withApp(app); + lt.beforeEach.withUserModel('user'); /* describe('accessToken', function() { @@ -105,7 +106,7 @@ describe('access control - integration', function() { lt.describe.whenLoggedInAsUser(CURRENT_USER, function() { beforeEach(function() { - this.url = '/api/users/' + this.user.id + '?ok'; + this.url = '/api/users/' + this.loggedInAccessToken.userId + '?ok'; }); lt.describe.whenCalledRemotely('DELETE', '/api/users/:id', function() { lt.it.shouldBeAllowed(); @@ -262,7 +263,7 @@ describe('access control - integration', function() { var self = this; // Create an account under the given user app.models.accountWithReplaceOnPUTtrue.create({ - userId: self.user.id, + userId: self.loggedInAccessToken.userId, balance: 100 }, function(err, act) { actId = act.id; @@ -326,7 +327,7 @@ describe('access control - integration', function() { var self = this; // Create an account under the given user app.models.accountWithReplaceOnPUTfalse.create({ - userId: self.user.id, + userId: self.loggedInAccessToken.userId, balance: 100, }, function(err, act) { actId = act.id; From 3df5b2814cd67c00e547a09d6d47aa83ac608311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 6 Sep 2016 13:55:54 +0200 Subject: [PATCH 088/187] Fix double-slash in confirmation URL Fix the code building the URL used in the email-verification email to prevent double-slash in the URL when e.g. restApiRoot is '/'. Before: http://example.com//users/confirm?... Now: http://example.com/users/confirm?... --- common/models/user.js | 20 +++++++++++++++++--- test/user.test.js | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index ea35f6b13..e90980cde 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -401,14 +401,18 @@ module.exports = function(User) { (options.protocol === 'https' && options.port == '443') ) ? '' : ':' + options.port; + var urlPath = joinUrlPath( + options.restApiRoot, + userModel.http.path, + userModel.sharedClass.find('confirm', true).http.path + ); + options.verifyHref = options.verifyHref || options.protocol + '://' + options.host + displayPort + - options.restApiRoot + - userModel.http.path + - userModel.sharedClass.find('confirm', true).http.path + + urlPath + '?uid=' + options.user.id + '&redirect=' + @@ -767,3 +771,13 @@ function emailValidator(err, done) { if (!isEmail(value)) return err('email'); } + +function joinUrlPath(args) { + var result = arguments[0]; + for (var ix = 1; ix < arguments.length; ix++) { + var next = arguments[ix]; + result += result[result.length - 1] === '/' && next[0] === '/' ? + next.slice(1) : next; + } + return result; +} diff --git a/test/user.test.js b/test/user.test.js index c22f8039c..55d893275 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -1551,6 +1551,41 @@ describe('User', function() { done(); }); + + it('should squash "//" when restApiRoot is "/"', function(done) { + var emailBody; + User.afterRemote('create', function(ctx, user, next) { + assert(user, 'afterRemote should include result'); + + var options = { + type: 'email', + to: user.email, + from: 'noreply@myapp.org', + redirect: '/', + host: 'myapp.org', + port: 3000, + restApiRoot: '/', + }; + + user.verify(options, function(err, result) { + if (err) return next(err); + emailBody = result.email.response.toString('utf-8'); + next(); + }); + }); + + request(app) + .post('/test-users') + .expect('Content-Type', /json/) + .expect(200) + .send({email: 'user@example.com', password: 'pass'}) + .end(function(err, res) { + if (err) return done(err); + expect(emailBody) + .to.contain('http://myapp.org:3000/test-users/confirm'); + done(); + }); + }); }); describe('User.confirm(options, fn)', function() { From ec8250cf58fbba5a639c1e00740db860ffa7cd5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 13 Sep 2016 09:08:28 +0200 Subject: [PATCH 089/187] 2.34.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix double-slash in confirmation URL (Miroslav Bajtoš) --- CHANGES.md | 9 +++++++++ package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 566a2596d..50d67eb0d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +2016-09-13, Version 2.34.1 +========================== + + * Fix double-slash in confirmation URL (Miroslav Bajtoš) + + +2016-09-12, Version 2.34.0 +========================== + 2016-09-09, Version 2.33.0 diff --git a/package.json b/package.json index 32c3f4e1d..31dd61b69 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.34.0", + "version": "2.34.1", "description": "LoopBack: Open Source Framework for Node.js", "homepage": "http://loopback.io/", "keywords": [ From fa310d58820bc04427d3501c5e1b41fdc7a3a9f1 Mon Sep 17 00:00:00 2001 From: Loay Date: Tue, 30 Aug 2016 15:09:11 -0400 Subject: [PATCH 090/187] Invalidate sessions after email change --- common/models/user.js | 28 ++++ test/user.test.js | 313 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 341 insertions(+) diff --git a/common/models/user.js b/common/models/user.js index e90980cde..5c482c77b 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -657,6 +657,34 @@ module.exports = function(User) { next(); }); + // Delete old sessions once email is updated + UserModel.observe('before save', function beforeEmailUpdate(ctx, next) { + if (ctx.isNewInstance) return next(); + if (!ctx.where && !ctx.instance) return next(); + var where = ctx.where || { id: ctx.instance.id }; + ctx.Model.find({ where: where }, function(err, userInstances) { + if (err) return next(err); + ctx.hookState.originalUserData = userInstances.map(function(u) { + return { id: u.id, email: u.email }; + }); + next(); + }); + }); + + UserModel.observe('after save', function afterEmailUpdate(ctx, next) { + if (!ctx.Model.relations.accessTokens) return next(); + var AccessToken = ctx.Model.relations.accessTokens.modelTo; + var newEmail = (ctx.instance || ctx.data).email; + if (!ctx.hookState.originalUserData) return next(); + var idsToExpire = ctx.hookState.originalUserData.filter(function(u) { + return u.email !== newEmail; + }).map(function(u) { + return u.id; + }); + if (!idsToExpire.length) return next(); + AccessToken.deleteAll({ userId: { inq: idsToExpire }}, next); + }); + UserModel.remoteMethod( 'login', { diff --git a/test/user.test.js b/test/user.test.js index 55d893275..35fed6307 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -41,9 +41,12 @@ describe('User', function() { app.model(Email, { dataSource: 'email' }); // attach User and related models + // forceId is set to false for the purpose of updating the same affected user within the + // `Email Update` test cases. User = app.registry.createModel('TestUser', {}, { base: 'User', http: { path: 'test-users' }, + forceId: false, }); app.model(User, { dataSource: 'db' }); @@ -1808,6 +1811,316 @@ describe('User', function() { }); }); + describe('Email Update', function() { + describe('User changing email property', function() { + var user, originalUserToken1, originalUserToken2, newUserCreated; + var currentEmailCredentials = { email: 'original@example.com', password: 'bar' }; + var updatedEmailCredentials = { email: 'updated@example.com', password: 'bar' }; + var newUserCred = { email: 'newuser@example.com', password: 'newpass' }; + + beforeEach('create user then login', function createAndLogin(done) { + async.series([ + function createUserWithOriginalEmail(next) { + User.create(currentEmailCredentials, function(err, userCreated) { + if (err) return next(err); + user = userCreated; + next(); + }); + }, + function firstLoginWithOriginalEmail(next) { + User.login(currentEmailCredentials, function(err, accessToken1) { + if (err) return next(err); + assert(accessToken1.userId); + originalUserToken1 = accessToken1.id; + next(); + }); + }, + function secondLoginWithOriginalEmail(next) { + User.login(currentEmailCredentials, function(err, accessToken2) { + if (err) return next(err); + assert(accessToken2.userId); + originalUserToken2 = accessToken2.id; + next(); + }); + }, + ], done); + }); + + it('invalidates sessions when email is changed using `updateAttributes`', function(done) { + user.updateAttributes( + { email: updatedEmailCredentials.email }, + function(err, userInstance) { + if (err) return done(err); + assertNoAccessTokens(done); + }); + }); + + it('invalidates sessions when email is changed using `replaceAttributes`', function(done) { + user.replaceAttributes(updatedEmailCredentials, function(err, userInstance) { + if (err) return done(err); + assertNoAccessTokens(done); + }); + }); + + it('invalidates sessions when email is changed using `updateOrCreate`', function(done) { + User.updateOrCreate({ + id: user.id, + email: updatedEmailCredentials.email, + password: updatedEmailCredentials.password, + }, function(err, userInstance) { + if (err) return done(err); + assertNoAccessTokens(done); + }); + }); + + it('invalidates sessions when the email is changed using `replaceById`', function(done) { + User.replaceById(user.id, updatedEmailCredentials, function(err, userInstance) { + if (err) return done(err); + assertNoAccessTokens(done); + }); + }); + + it('invalidates sessions when the email is changed using `replaceOrCreate`', function(done) { + User.replaceOrCreate({ + id: user.id, + email: updatedEmailCredentials.email, + password: updatedEmailCredentials.password, + }, function(err, userInstance) { + if (err) return done(err); + assertNoAccessTokens(done); + }); + }); + + it('keeps sessions AS IS if firstName is added using `updateAttributes`', function(done) { + user.updateAttributes({ 'firstName': 'Janny' }, function(err, userInstance) { + if (err) return done(err); + assertUntouchedTokens(done); + }); + }); + + it('keeps sessions AS IS if firstName is added using `replaceAttributes`', function(done) { + user.replaceAttributes({ + email: currentEmailCredentials.email, + password: currentEmailCredentials.password, + firstName: 'Candy', + }, function(err, userInstance) { + if (err) return done(err); + assertUntouchedTokens(done); + }); + }); + + it('keeps sessions AS IS if firstName is added using `updateOrCreate`', function(done) { + User.updateOrCreate({ + id: user.id, + firstName: 'Loay', + email: currentEmailCredentials.email, + password: currentEmailCredentials.password, + }, function(err, userInstance) { + if (err) return done(err); + assertUntouchedTokens(done); + }); + }); + + it('keeps sessions AS IS if firstName is added using `replaceById`', function(done) { + User.replaceById( + user.id, + { + firstName: 'Miroslav', + email: currentEmailCredentials.email, + password: currentEmailCredentials.password, + }, function(err, userInstance) { + if (err) return done(err); + assertUntouchedTokens(done); + }); + }); + + it('keeps sessions AS IS if a new user is created using `create`', function(done) { + async.series([ + function(next) { + User.create(newUserCred, function(err, newUserInstance) { + if (err) return done(err); + newUserCreated = newUserInstance; + next(); + }); + }, + function(next) { + User.login(newUserCred, function(err, newAccessToken) { + if (err) return done(err); + assert(newAccessToken.id); + assertPreservedToken(next); + }); + }, + ], done); + }); + + it('keeps sessions AS IS if a new user is created using `updateOrCreate`', function(done) { + async.series([ + function(next) { + User.create(newUserCred, function(err, newUserInstance2) { + if (err) return done(err); + newUserCreated = newUserInstance2; + next(); + }); + }, + function(next) { + User.login(newUserCred, function(err, newAccessToken2) { + if (err) return done(err); + assert(newAccessToken2.id); + assertPreservedToken(next); + }); + }, + ], done); + }); + + function assertPreservedToken(done) { + AccessToken.find({ where: { userId: user.id }}, function(err, tokens) { + if (err) return done(err); + expect(tokens.length).to.equal(2); + expect([tokens[0].id, tokens[1].id]).to.have.members([originalUserToken1, + originalUserToken2]); + done(); + }); + } + + function assertNoAccessTokens(done) { + AccessToken.find({ where: { userId: user.id }}, function(err, tokens) { + if (err) return done(err); + expect(tokens.length).to.equal(0); + done(); + }); + } + + function assertUntouchedTokens(done) { + AccessToken.find({ where: { userId: user.id }}, function(err, tokens) { + if (err) return done(err); + expect(tokens.length).to.equal(2); + done(); + }); + } + }); + + describe('User not changing email property', function() { + var user1, user2, user3; + it('preserves other users\' sessions if their email is untouched', function(done) { + async.series([ + function(next) { + User.create({ email: 'user1@example.com', password: 'u1pass' }, function(err, u1) { + if (err) return done(err); + User.create({ email: 'user2@example.com', password: 'u2pass' }, function(err, u2) { + if (err) return done(err); + User.create({ email: 'user3@example.com', password: 'u3pass' }, function(err, u3) { + if (err) return done(err); + user1 = u1; + user2 = u2; + user3 = u3; + next(); + }); + }); + }); + }, + function(next) { + User.login( + { email: 'user1@example.com', password: 'u1pass' }, + function(err, accessToken1) { + if (err) return next(err); + User.login( + { email: 'user2@example.com', password: 'u2pass' }, + function(err, accessToken2) { + if (err) return next(err); + User.login({ email: 'user3@example.com', password: 'u3pass' }, + function(err, accessToken3) { + if (err) return next(err); + next(); + }); + }); + }); + }, + function(next) { + user2.updateAttribute('email', 'user2Update@b.com', function(err, userInstance) { + if (err) return next(err); + assert.equal(userInstance.email, 'user2Update@b.com'); + next(); + }); + }, + function(next) { + AccessToken.find({ where: { userId: user1.id }}, function(err, tokens1) { + if (err) return next(err); + AccessToken.find({ where: { userId: user2.id }}, function(err, tokens2) { + if (err) return next(err); + AccessToken.find({ where: { userId: user3.id }}, function(err, tokens3) { + if (err) return next(err); + + expect(tokens1.length).to.equal(1); + expect(tokens2.length).to.equal(0); + expect(tokens3.length).to.equal(1); + next(); + }); + }); + }); + }, + ], done); + }); + }); + + it('invalidates sessions after using updateAll', function(done) { + var userSpecial, userNormal; + async.series([ + function createSpecialUser(next) { + User.create( + { email: 'special@example.com', password: 'pass1', name: 'Special' }, + function(err, specialInstance) { + if (err) return next(err); + userSpecial = specialInstance; + next(); + }); + }, + function createNormaluser(next) { + User.create( + { email: 'normal@example.com', password: 'pass2' }, + function(err, normalInstance) { + if (err) return next(err); + userNormal = normalInstance; + next(); + }); + }, + function loginSpecialUser(next) { + User.login({ email: 'special@example.com', password: 'pass1' }, function(err, ats) { + if (err) return next(err); + next(); + }); + }, + function loginNormalUser(next) { + User.login({ email: 'normal@example.com', password: 'pass2' }, function(err, atn) { + if (err) return next(err); + next(); + }); + }, + function updateSpecialUser(next) { + User.updateAll( + { name: 'Special' }, + { email: 'superspecial@example.com' }, function(err, info) { + if (err) return next(err); + next(); + }); + }, + function verifyTokensOfSpecialUser(next) { + AccessToken.find({ where: { userId: userSpecial.id }}, function(err, tokens1) { + if (err) return done(err); + expect(tokens1.length).to.equal(0); + next(); + }); + }, + function verifyTokensOfNormalUser(next) { + AccessToken.find({ userId: userNormal.userId }, function(err, tokens2) { + if (err) return done(err); + expect(tokens2.length).to.equal(1); + next(); + }); + }, + ], done); + }); + }); + describe('ctor', function() { it('exports default Email model', function() { expect(User.email, 'User.email').to.be.a('function'); From f7f448d5694d645114db96daefc38b847a3ddbef Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Mon, 19 Sep 2016 15:39:01 -0700 Subject: [PATCH 091/187] Add docs for KeyValue model Backport of #2743 --- common/models/key-value-model.js | 142 +++++++++++++++++++++++++++++-- docs.json | 1 + 2 files changed, 138 insertions(+), 5 deletions(-) diff --git a/common/models/key-value-model.js b/common/models/key-value-model.js index c7fbbbe3f..2c0aeed78 100644 --- a/common/models/key-value-model.js +++ b/common/models/key-value-model.js @@ -1,31 +1,163 @@ var g = require('strong-globalize')(); +/** + * Data model for key-value databases. + * + * @class KeyValueModel + * @inherits {Model} + */ + module.exports = function(KeyValueModel) { - // TODO add api docs + /** + * Return the value associated with a given key. + * + * @param {String} key Key to use when searching the database. + * @options {Object} options + * @callback {Function} callback + * @param {Error} err Error object. + * @param {Any} result Value associated with the given key. + * @promise + * + * @header KeyValueModel.get(key, cb) + */ KeyValueModel.get = function(key, options, callback) { throwNotAttached(this.modelName, 'get'); }; - // TODO add api docs + /** + * Persist a value and associate it with the given key. + * + * @param {String} key Key to associate with the given value. + * @param {Any} value Value to persist. + * @options {Number|Object} options Optional settings for the key-value + * pair. If a Number is provided, it is set as the TTL (time to live) in ms + * (milliseconds) for the key-value pair. + * @property {Number} ttl TTL for the key-value pair in ms. + * @callback {Function} callback + * @param {Error} err Error object. + * @promise + * + * @header KeyValueModel.set(key, value, cb) + */ KeyValueModel.set = function(key, value, options, callback) { throwNotAttached(this.modelName, 'set'); }; - // TODO add api docs + /** + * Set the TTL (time to live) in ms (milliseconds) for a given key. TTL is the + * remaining time before a key-value pair is discarded from the database. + * + * @param {String} key Key to use when searching the database. + * @param {Number} ttl TTL in ms to set for the key. + * @options {Object} options + * @callback {Function} callback + * @param {Error} err Error object. + * @promise + * + * @header KeyValueModel.expire(key, ttl, cb) + */ KeyValueModel.expire = function(key, ttl, options, callback) { throwNotAttached(this.modelName, 'expire'); }; - // TODO add api docs + /** + * Return the TTL (time to live) for a given key. TTL is the remaining time + * before a key-value pair is discarded from the database. + * + * @param {String} key Key to use when searching the database. + * @options {Object} options + * @callback {Function} callback + * @param {Error} error + * @param {Number} ttl Expiration time for the key-value pair. `undefined` if + * TTL was not initially set. + * @promise + * + * @header KeyValueModel.ttl(key, cb) + */ + KeyValueModel.ttl = function(key, options, callback) { + throwNotAttached(this.modelName, 'ttl'); + }; + + /** + * Return all keys in the database. + * + * **WARNING**: This method is not suitable for large data sets as all + * key-values pairs are loaded into memory at once. For large data sets, + * use `iterateKeys()` instead. + * + * @param {Object} filter An optional filter object with the following + * @param {String} filter.match Glob string used to filter returned + * keys (i.e. `userid.*`). All connectors are required to support `*` and + * `?`, but may also support additional special characters specific to the + * database. + * @param {Object} options + * @callback {Function} callback + * @promise + * + * @header KeyValueModel.keys(filter, cb) + */ KeyValueModel.keys = function(filter, options, callback) { throwNotAttached(this.modelName, 'keys'); }; - // TODO add api docs + /** + * Asynchronously iterate all keys in the database. Similar to `.keys()` but + * instead allows for iteration over large data sets without having to load + * everything into memory at once. + * + * Callback example: + * ```js + * // Given a model named `Color` with two keys `red` and `blue` + * var iterator = Color.iterateKeys(); + * it.next(function(err, key) { + * // key contains `red` + * it.next(function(err, key) { + * // key contains `blue` + * }); + * }); + * ``` + * + * Promise example: + * ```js + * // Given a model named `Color` with two keys `red` and `blue` + * var iterator = Color.iterateKeys(); + * Promise.resolve().then(function() { + * return it.next(); + * }) + * .then(function(key) { + * // key contains `red` + * return it.next(); + * }); + * .then(function(key) { + * // key contains `blue` + * }); + * ``` + * + * @param {Object} filter An optional filter object with the following + * @param {String} filter.match Glob string to use to filter returned + * keys (i.e. `userid.*`). All connectors are required to support `*` and + * `?`. They may also support additional special characters that are + * specific to the backing database. + * @param {Object} options + * @returns {AsyncIterator} An Object implementing `next(cb) -> Promise` + * function that can be used to iterate all keys. + * + * @header KeyValueModel.iterateKeys(filter) + */ KeyValueModel.iterateKeys = function(filter, options) { throwNotAttached(this.modelName, 'iterateKeys'); }; + /*! + * Set up remoting metadata for this model. + * + * **Notes**: + * - The method is called automatically by `Model.extend` and/or + * `app.registry.createModel` + * - In general, base models use call this to ensure remote methods are + * inherited correctly, see bug at + * https://github.com/strongloop/loopback/issues/2350 + */ KeyValueModel.setup = function() { KeyValueModel.base.setup.apply(this, arguments); diff --git a/docs.json b/docs.json index c99680b27..29bb0008b 100644 --- a/docs.json +++ b/docs.json @@ -24,6 +24,7 @@ "common/models/application.js", "common/models/change.js", "common/models/email.js", + "common/models/key-value-model.js", "common/models/role.js", "common/models/role-mapping.js", "common/models/scope.js", From 59eeb998035fc63e8489d5ed8c1ed0b8a1ac98d9 Mon Sep 17 00:00:00 2001 From: Loay Date: Wed, 24 Aug 2016 16:30:58 -0400 Subject: [PATCH 092/187] Allow resetPassword if email is verified --- common/models/user.js | 9 ++++++++- test/user.test.js | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/common/models/user.js b/common/models/user.js index 5c482c77b..1295e6313 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -566,7 +566,14 @@ module.exports = function(User) { } // create a short lived access token for temp login to change password // TODO(ritch) - eventually this should only allow password change - user.accessTokens.create({ttl: ttl}, function(err, accessToken) { + if (UserModel.settings.emailVerificationRequired && !user.emailVerified) { + err = new Error(g.f('Email has not been verified')); + err.statusCode = 401; + err.code = 'RESET_FAILED_EMAIL_NOT_VERIFIED'; + return cb(err); + } + + user.accessTokens.create({ ttl: ttl }, function(err, accessToken) { if (err) { return cb(err); } diff --git a/test/user.test.js b/test/user.test.js index 35fed6307..b17abc5f4 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -2121,6 +2121,43 @@ describe('User', function() { }); }); + describe('password reset with/without email verification', function() { + it('allows resetPassword by email if email verification is required and done', + function(done) { + User.settings.emailVerificationRequired = true; + var email = validCredentialsEmailVerified.email; + + User.resetPassword({ email: email }, function(err, info) { + if (err) return done(err); + done(); + }); + }); + + it('disallows resetPassword by email if email verification is required and not done', + function(done) { + User.settings.emailVerificationRequired = true; + var email = validCredentialsEmail; + + User.resetPassword({ email: email }, function(err) { + assert(err); + assert.equal(err.code, 'RESET_FAILED_EMAIL_NOT_VERIFIED'); + assert.equal(err.statusCode, 401); + done(); + }); + }); + + it('allows resetPassword by email if email verification is not required', + function(done) { + User.settings.emailVerificationRequired = false; + var email = validCredentialsEmail; + + User.resetPassword({ email: email }, function(err) { + if (err) return done(err); + done(); + }); + }); + }); + describe('ctor', function() { it('exports default Email model', function() { expect(User.email, 'User.email').to.be.a('function'); From 74063ab559984b3fec60f63d77cd99ac258ae1bc Mon Sep 17 00:00:00 2001 From: Candy Date: Tue, 20 Sep 2016 14:15:19 -0400 Subject: [PATCH 093/187] Add translation files for 2.x --- intl/de/messages.json | 116 +++++++++++++++++++++++++++++++++++++ intl/es/messages.json | 116 +++++++++++++++++++++++++++++++++++++ intl/fr/messages.json | 116 +++++++++++++++++++++++++++++++++++++ intl/it/messages.json | 116 +++++++++++++++++++++++++++++++++++++ intl/ja/messages.json | 116 +++++++++++++++++++++++++++++++++++++ intl/ko/messages.json | 116 +++++++++++++++++++++++++++++++++++++ intl/nl/messages.json | 116 +++++++++++++++++++++++++++++++++++++ intl/pt/messages.json | 116 +++++++++++++++++++++++++++++++++++++ intl/tr/messages.json | 116 +++++++++++++++++++++++++++++++++++++ intl/zh-Hans/messages.json | 116 +++++++++++++++++++++++++++++++++++++ intl/zh-Hant/messages.json | 116 +++++++++++++++++++++++++++++++++++++ 11 files changed, 1276 insertions(+) create mode 100644 intl/de/messages.json create mode 100644 intl/es/messages.json create mode 100644 intl/fr/messages.json create mode 100644 intl/it/messages.json create mode 100644 intl/ja/messages.json create mode 100644 intl/ko/messages.json create mode 100644 intl/nl/messages.json create mode 100644 intl/pt/messages.json create mode 100644 intl/tr/messages.json create mode 100644 intl/zh-Hans/messages.json create mode 100644 intl/zh-Hant/messages.json diff --git a/intl/de/messages.json b/intl/de/messages.json new file mode 100644 index 000000000..f80f10e81 --- /dev/null +++ b/intl/de/messages.json @@ -0,0 +1,116 @@ +{ + "3b46d3a780fd6ae5f95ade489a0efffe": "Der aktuelle Kontext wird im Browser nicht unterstützt.", + "7e0fca41d098607e1c9aa353c67e0fa1": "Ungültiges Zugriffstoken", + "320c482401afa1207c04343ab162e803": "Ungültiger Prinzipaltyp: {0}", + "c2b5d51f007178170ca3952d59640ca4": "{0} Änderungen können nicht behoben werden:\n{1}", + "5858e63efaa0e4ad86b61c0459ea32fa": "Sie müssen das {{Email}}-Modell mit einem {{Mail}}-Konnektor verbinden", + "0caffe1d763c8cca6a61814abe33b776": "E-Mail ist erforderlich", + "1b2a6076dccbe91a56f1672eb3b8598c": "Der Antworthauptteil enthält Eigenschaften des bei der Anmeldung erstellten {{AccessToken}}.\nAbhängig vom Wert des Parameters 'include' kann der Hauptteil zusätzliche Eigenschaften enthalten:\n\n - user - U+007BUserU+007D - Daten des derzeit angemeldeten Benutzers. {{(`include=user`)}}\n\n", + "2362ba55796c733e337af169922327a2": "In die Antwort einzuschließende zugehörige Objekte. Weitere Details enthält die Beschreibung des Rückgabewerts.", + "306999d39387d87b2638199ff0bed8ad": "Kennwort zurücksetzen für einen Benutzer mit E-Mail.", + "3aae63bb7e8e046641767571c1591441": "Anmeldung fehlgeschlagen, da die E-Mail-Adresse nicht bestätigt wurde", + "3caaa84fc103d6d5612173ae6d43b245": "Ungültiges Token: {0}", + "42e3fa18945255412ebc6561e2c6f1dc": "Bestätigen Sie eine Benutzerregistrierung mit E-Mail-Bestätigungs-Token.", + "430b6326d7ebf6600a39f614ef516bc8": "Geben Sie dieses Argument nicht an, es wird automatisch aus Anforderungsheaders extrahiert.", + "44a6c8b1ded4ed653d19ddeaaf89a606": "E-Mail nicht gefunden", + "5e81ad3847a290dc650b47618b9cbc7e": "Anmeldung fehlgeschlagen", + "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "Benutzer mit Benutzername/E-Mail und Kennwort anmelden.", + "8608c28f5e6df0008266e3c497836176": "Benutzer mit Zugriffstoken abmelden.", + "860d1a0b8bd340411fb32baa72867989": "Die Transportmethode unterstützt keine HTTP-Umleitungen.", + "895b1f941d026870b3cc8e6af087c197": "{{username}} oder {{email}} ist erforderlich", + "a50d10fc6e0959b220e085454c40381e": "Benutzer nicht gefunden: {0}", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} ist erforderlich", + "c34fa20eea0091747fcc9eda204b8d37": "{{accessToken}} konnte nicht gefunden werden", + "c68a93f0a9524fed4ff64372fc90c55f": "Eine gültige E-Mail-Adresse muss angegeben werden", + "f58cdc481540cd1f69a4aa4da2e37981": "Ungültiges Kennwort: {0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "Ergebnis:{0}", + "10e01c895dc0b2fecc385f9f462f1ca6": "eine Liste mit Farben ist verfügbar unter {{http://localhost:3000/colors}}", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "Anforderung an Host {0}", + "a40684f5a9f546115258b76938d1de37": "Eine Liste mit Farben ist verfügbar unter {{http://localhost:3000/colors}}", + "1e85f822b547a75d7d385048030e4ecb": "Erstellt: {0}", + "7d5e7ed0efaedf3f55f380caae0df8b8": "Meine erste mobile Anwendung", + "04bd8af876f001ceaf443aad6a9002f9": "Für die Authentifizierung muss Modell {0} definiert sein.", + "095afbf2f1f0e5be678f5dac5c54e717": "Zugriff verweigert", + "2d3071e3b18681c80a090dc0efbdb349": "{0} mit ID {1} konnte nicht gefunden werden", + "316e5b82c203cf3de31a449ee07d0650": "Erwartet wurde boolescher Wert, {0} empfangen", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Kann Datenquelle {0} nicht erstellen: {1}", + "7e287fc885d9fdcf42da3a12f38572c1": "Berechtigung erforderlich", + "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} wurde entfernt, verwenden Sie stattdessen das neue Modul {{loopback-boot}}", + "1d7833c3ca2f05fdad8fad7537531c40": "\t BETREFF:{0}", + "275f22ab95671f095640ca99194b7635": "\t VON:{0}", + "3d63008ccfb2af1db2142e8cc2716ace": "Warnung: Keine E-Mail-Transportmethode für das Senden von E-Mails angegeben. Richten Sie eine Transportmethode für das Senden von E-Mails ein.", + "4a4f04a4e480fc5d4ee73b84d9a4b904": "E-Mail senden:", + "63a091ced88001ab6acb58f61ec041c5": "\t TEXT:{0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t AN:{0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORTMETHODE:{0}", + "0da38687fed24275c1547e815914a8e3": "Zugehöriges Element nach ID für {0} löschen.", + "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "Kriterien für den Abgleich von Modellinstanzen", + "22fe62fa8d595b72c62208beddaa2a56": "Zugehöriges Element nach ID für {0} aktualisieren.", + "528325f3cbf1b0ab9a08447515daac9a": "{0} von diesem Modell aktualisieren.", + "543d19bad5e47ee1e9eb8af688e857b4": "Fremdschlüssel für {0}.", + "598ff0255ffd1d1b71e8de55dbe2c034": "Vorhandensein von {0}-Beziehung zu einem Element nach ID prüfen.", + "5a36cc6ba0cc27c754f6c5ed6015ea3c": "{0}-Beziehung zu einem Element nach ID entfernen.", + "5fa3afb425819ebde958043e598cb664": "Modell mit {{id}} {0} konnte nicht gefunden werden", + "61e5deebaf44d68f4e6a508f30cc31a3": "Beziehung '{0} ist für Modell {1} nicht vorhanden", + "651f0b3cbba001635152ec3d3d954d0a": "Zugehöriges Element nach ID für {0} suchen.", + "7bc7b301ad9c4fc873029d57fb9740fe": "Abfrage von {0} von {1}.", + "7c837b88fd0e509bd3fc722d7ddf0711": "Fremdschlüssel für {0}", + "830cb6c862f8f364e9064cea0026f701": "Ruft hasOne-Beziehung {0} ab.", + "855ecd4a64885ba272d782435f72a4d4": "\"{0}\" unbekannt, ID \"{1}\".", + "86254879d01a60826a851066987703f2": "Zugehöriges Element nach ID für {0} hinzufügen.", + "8ae418c605b6a45f2651be9b1677c180": "Ungültige Remote-Methode: '{0}'", + "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Ruft belongsTo-Beziehung {0} ab.", + "c0057a569ff9d3b509bac61a4b2f605d": "Löscht alle {0} von diesem Modell.", + "cd0412f2f33a4a2a316acc834f3f21a6": "muss {{id}} oder {{data}} angeben", + "d6f43b266533b04d442bdb3955622592": "Erstellt eine neue Instanz in {0} von diesem Modell.", + "da13d3cdf21330557254670dddd8c5c7": "Zählt {0} von {1}.", + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" unbekannt, {{id}} \"{1}\".", + "f66ae3cf379b2fce28575a3282defe1a": "Löscht {0} von diesem Modell.", + "03f79fa268fe199de2ce4345515431c1": "Kein Änderungssatz gefunden für {0} mit ID {1}", + "0f1c71f74b040bfbe8d384a414e31f03": "Rufen Sie einen Satz an Deltas und Konflikten seit dem angegebenen Prüfpunkt ab.", + "15254dec061d023d6c030083a0cef50f": "Erstellen Sie eine neue Instanz des Modells und definieren Sie es für die Datenquelle als persistent.", + "16a11368d55b85a209fc6aea69ee5f7a": "Löschen Sie alle übereinstimmenden Datensätze.", + "1bc1d489ddf347af47af3d9b1fc7cc15": "Führen Sie mehrere Aktualisierungen gleichzeitig aus. Anmerkung: Dies ist nicht atomar.", + "1bc7d8283c9abda512692925c8d8e3c0": "Rufen Sie den aktuellen Prüfpunkt ab.", + "1caa7cc61266e7aef7db7d2f0e27ac3e": "Aktualisieren Sie die Eigenschaften der neuesten Änderungssätze für diese Instanz.", + "2a7df74fe6e8462e617b79d5fbb536ea": "Rufen Sie den neuesten Änderungssatz für diese Instanz ab.", + "2a9684b3d5b3b67af74bac74eb1b0843": "Suchen Sie alle Instanzen des Modells aus der Datenquelle, die laut Filter übereinstimmen.", + "2e50838caf0c927735eb15d12866bdd7": "Rufen Sie die Änderungen an einem Modell seit einem angegebenen Prüfpunkt ab. Geben Sie ein Filterobjekt zum Reduzieren der Anzahl an zurückgegebenen Ergebnissen an.", + "4203ab415ec66a78d3164345439ba76e": "{0}.{1}() kann nicht aufgerufen werden. Die Methode {2} wurde nicht konfiguriert. {{PersistedModel}} wurde nicht ordnungsgemäß an eine {{DataSource}} angehängt!", + "51ea9b6245bb5e672b236d640ca3b048": "Ein Objekt aus Paaren aus Änderungseigenschaftsnamen und -werten", + "55ddedd4c501348f82cb89db02ec85c1": "Ein Objekt aus Paaren aus Modelleigenschaftsnamen und -werten", + "5aaa76c72ae1689fd3cf62589784a4ba": "Aktualisieren Sie Attribute für eine Modellinstanz und definieren Sie sie für die Datenquelle als persistent.", + "5f659bbc15e6e2b249fa33b3879b5f69": "Suchen Sie eine Modellinstanz nach {{id}} aus der Datenquelle.", + "62e8b0a733417978bab22c8dacf5d7e6": "Massenaktualisierungen können nicht angewendet werden, der Konnektor meldet die Anzahl aktualisierter Datensätze nicht richtig.", + "6329e0ac1de3c250ebb1df5afd5a2a7b": "Die Anzahl der aktualisierten Instanzen", + "6bc376432cd9972cf991aad3de371e78": "Fehlende Daten für Änderung: {0}", + "79295ac04822d2e9702f0dd1d0240336": "Aktualisieren Sie die Instanzen des Modells aus der Datenquelle, die laut {{where}} übereinstimmen.", + "7f2fde7f0f860ead224b11ba8d75aa1c": "Erstellen Sie eine Aktualisierungsliste aus einer Deltaliste.", + "89b57e764c2267129294b07589dbfdc2": "Löschen Sie eine Modellinstanz nach {{id}} aus der Datenquelle.", + "8bab6720ecc58ec6412358c858a53484": "Massenaktualisierung fehlgeschlagen, der Konnektor hat eine unerwartete Anzahl an Datensätzen geändert: {0}", + "a98b6cc6547706b5c6bffde0ed5fd55c": "Suchen Sie die erste Instanz des Modells aus der Datenquelle, die laut Filter übereinstimmen.", + "c46d4aba1f14809c16730faa46933495": "Definierende Felder filtern und einschließen", + "c65600640f206f585d300b4bcb699d95": "Erstellen Sie einen Prüfpunkt.", + "cf64c7afc74d3a8120abcd028f98c770": "Aktualisieren Sie eine vorhandene Modellinstanz oder fügen Sie eine neue in die Datenquelle ein.", + "dcb6261868ff0a7b928aa215b07d068c": "Erstellen Sie einen Änderungsdatenstrom.", + "e43e320a435ec1fa07648c1da0d558a7": "Überprüfen Sie, ob in der neuen Datenquelle eine Modellinstanz vorhanden ist.", + "e92aa25b6b864e3454b65a7c422bd114": "Massenaktualisierung fehlgeschlagen, der Konnektor hat eine unerwartete Anzahl an Datensätzen gelöscht : {0}", + "ea63d226b6968e328bdf6876010786b5": "Massenaktualisierungen können nicht angewendet werden, der Konnektor meldet die Anzahl gelöschter Datensätze nicht richtig.", + "f1d4ac54357cc0932f385d56814ba7e4": "Konflikt", + "f37d94653793e33f4203e45b4a1cf106": "Zählen Sie die Instanzen des Modells aus der Datenquelle, die laut Position (where) übereinstimmen.", + "0731d0109e46c21a4e34af3346ed4856": "Dieses Verhalten kann sich in der nächsten Hauptversion ändern.", + "2e110abee2c95bcfc2dafd48be7e2095": "{0} kann nicht konfiguriert werden: {{config.dataSource}} muss eine Instanz von {{DataSource}} sein", + "308e1d484516a33df788f873e65faaff": "Modell '{0}' bietet veraltetes 'DataModel' an. Verwenden Sie stattdessen 'PersistedModel'.", + "3438fab56cc7ab92dfd88f0497e523e0": "Die relations-Eigenschaft der Konfiguration '{0}' muss ein Objekt sein", + "4cac5f051ae431321673e04045d37772": "Modell '{0}' bietet das unbekannte Modell '{1}' an. 'PersistedModel' wird als Basis verwendet.", + "734a7bebb65e10899935126ba63dd51f": "Die options-Eigenschaft der Konfiguration '{0}' muss ein Objekt sein", + "779467f467862836e19f494a37d6ab77": "Die acls-Eigenschaft der Konfiguration '{0}' muss eine Reihe von Objekten sein", + "80a32e80cbed65eba2103201a7c94710": "Modell nicht gefunden: {0}", + "83cbdc2560ba9f09155ccfc63e08f1a1": "Eigenschaft '{0}' kann für {1} nicht rekonfiguriert werden", + "97795efe0c3eb7f35ce8cf8cfe70682b": "Der Konfiguration von {0} fehlt die {{`dataSource`}}-Eigenschaft.\nVerwenden Sie 'null' oder 'false', um Modelle zu kennzeichnen, die mit keiner Datenquelle verbunden sind.", + "a80038252430df2754884bf3c845c4cf": "Den Remote-Anbindungs-Metadaten für \"{0}.{1}\" fehlt das Flag \"isStatic\"; die Methode ist als Instanzdefinitionsmethode registriert.", + "dc568bee32deb0f6eaf63e73b20e8ceb": "Nicht-Objekt-Einstellung \"{0}\" von \"methods\" wird ignoriert.", + "3aecb24fa8bdd3f79d168761ca8a6729": "Unbekannte {{middleware}}-Phase {0}" +} + diff --git a/intl/es/messages.json b/intl/es/messages.json new file mode 100644 index 000000000..e91c6c4f1 --- /dev/null +++ b/intl/es/messages.json @@ -0,0 +1,116 @@ +{ + "3b46d3a780fd6ae5f95ade489a0efffe": "El contexto actual no está soportado en el navegador.", + "7e0fca41d098607e1c9aa353c67e0fa1": "Señal de acceso no válida ", + "320c482401afa1207c04343ab162e803": "Tipo de principal no válido: {0}", + "c2b5d51f007178170ca3952d59640ca4": "No se pueden rectificar los cambios de {0}:\n{1}", + "5858e63efaa0e4ad86b61c0459ea32fa": "Debe conectar el modelo de {{Email}} a un conector de {{Mail}}", + "0caffe1d763c8cca6a61814abe33b776": "Es necesario el correo electrónico", + "1b2a6076dccbe91a56f1672eb3b8598c": "El cuerpo de respuesta contiene propiedades de la {{AccessToken}} creada durante el inicio de la sesión. \nDependiendo del valor del parámetro `include`, el cuerpo puede contener propiedades adicionales:\n\n - `user` - `U+007BUserU+007D` - Datos del usuario conectado actualmente. {{(`include=user`)}}\n\n", + "2362ba55796c733e337af169922327a2": "Objetos relacionados a incluir en la respuesta. Consulte la descripción del valor de retorno para obtener más detalles.", + "306999d39387d87b2638199ff0bed8ad": "Restablecer contraseña para un usuario con correo electrónico.", + "3aae63bb7e8e046641767571c1591441": "el inicio de sesión ha fallado porque el correo electrónico no ha sido verificado", + "3caaa84fc103d6d5612173ae6d43b245": "La señal no es válida: {0} ", + "42e3fa18945255412ebc6561e2c6f1dc": "Confirmar un registro de usuario con la señal de verificación de correo electrónico.", + "430b6326d7ebf6600a39f614ef516bc8": "No proporcione este argumento, se extrae automáticamente de las cabeceras de solicitud.", + "44a6c8b1ded4ed653d19ddeaaf89a606": "Correo electrónico no encontrado", + "5e81ad3847a290dc650b47618b9cbc7e": "el inicio de sesión ha fallado", + "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "Iniciar una sesión de usuario con nombre de usuario/correo electrónico y contraseña.", + "8608c28f5e6df0008266e3c497836176": "Finalizar una sesión de usuario con señal de acceso.", + "860d1a0b8bd340411fb32baa72867989": "El transporte no admite redirecciones HTTP.", + "895b1f941d026870b3cc8e6af087c197": "{{username}} o {{email}} es obligatorio", + "a50d10fc6e0959b220e085454c40381e": "No se ha encontrado el usuario: {0}", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} es obligatorio", + "c34fa20eea0091747fcc9eda204b8d37": "no se ha encontrado {{accessToken}}", + "c68a93f0a9524fed4ff64372fc90c55f": "Debe proporcionar un correo electrónico válido", + "f58cdc481540cd1f69a4aa4da2e37981": "Contraseña no válida: {0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "resultado:{0}", + "10e01c895dc0b2fecc385f9f462f1ca6": "una lista de colores está disponible en {{http://localhost:3000/colors}}", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "Solicitud al host {0}", + "a40684f5a9f546115258b76938d1de37": "Una lista de colores está disponible en {{http://localhost:3000/colors}}", + "1e85f822b547a75d7d385048030e4ecb": "Creado: {0}", + "7d5e7ed0efaedf3f55f380caae0df8b8": "Mi primera aplicación móvil", + "04bd8af876f001ceaf443aad6a9002f9": "La autenticación requiere la definición del modelo {0}.", + "095afbf2f1f0e5be678f5dac5c54e717": "Acceso denegado ", + "2d3071e3b18681c80a090dc0efbdb349": "no se ha encontrado {0} con el ID {1}", + "316e5b82c203cf3de31a449ee07d0650": "Se esperaba un booleano, se ha obtenido {0}", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "No se puede crear el origen de datos {0}: {1}", + "7e287fc885d9fdcf42da3a12f38572c1": "Autorización necesaria", + "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} se ha eliminado, utilice el nuevo módulo {{loopback-boot}} en su lugar", + "1d7833c3ca2f05fdad8fad7537531c40": "\t ASUNTO:{0}", + "275f22ab95671f095640ca99194b7635": "\t DESDE:{0}", + "3d63008ccfb2af1db2142e8cc2716ace": "Aviso: No se ha especificado ningún transporte de correo electrónico para enviar correo electrónico. Configure un transporte para enviar mensajes de correo.", + "4a4f04a4e480fc5d4ee73b84d9a4b904": "Enviando correo:", + "63a091ced88001ab6acb58f61ec041c5": "\t TEXTO:{0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t A:{0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORTE:{0}", + "0da38687fed24275c1547e815914a8e3": "Suprimir un elemento relacionado por id para {0}.", + "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "Criterios de coincidencia de instancias de modelo", + "22fe62fa8d595b72c62208beddaa2a56": "Actualizar un elemento relacionado por id para {0}.", + "528325f3cbf1b0ab9a08447515daac9a": "Actualizar {0} de este modelo.", + "543d19bad5e47ee1e9eb8af688e857b4": "Clave foránea para {0}.", + "598ff0255ffd1d1b71e8de55dbe2c034": "Comprobar la existencia de la relación {0} con un elemento por id.", + "5a36cc6ba0cc27c754f6c5ed6015ea3c": "Eliminar la relación {0} con un elemento por id.", + "5fa3afb425819ebde958043e598cb664": "no se ha encontrado un modelo con {{id}} {0}", + "61e5deebaf44d68f4e6a508f30cc31a3": "La relación `{0}` no existe para el modelo `{1}`", + "651f0b3cbba001635152ec3d3d954d0a": "Buscar un elemento relacionado por id para {0}.", + "7bc7b301ad9c4fc873029d57fb9740fe": "{0} consultas de {1}.", + "7c837b88fd0e509bd3fc722d7ddf0711": "Clave foránea para {0}", + "830cb6c862f8f364e9064cea0026f701": "Capta la relación hasOne {0}.", + "855ecd4a64885ba272d782435f72a4d4": "Id de \"{0}\" desconocido \"{1}\".", + "86254879d01a60826a851066987703f2": "Añadir un elemento relacionado por id para {0}.", + "8ae418c605b6a45f2651be9b1677c180": "Método remoto no válido: `{0}`", + "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Capta la relación belongsTo {0}.", + "c0057a569ff9d3b509bac61a4b2f605d": "Suprime todos los {0} de este modelo.", + "cd0412f2f33a4a2a316acc834f3f21a6": "debe especificar un {{id}} o {{data}}", + "d6f43b266533b04d442bdb3955622592": "Crea una nueva instancia en {0} de este modelo.", + "da13d3cdf21330557254670dddd8c5c7": "Recuentos {0} de {1}.", + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "{{id}} de \"{0}\" desconocido \"{1}\".", + "f66ae3cf379b2fce28575a3282defe1a": "Suprime {0} de este modelo.", + "03f79fa268fe199de2ce4345515431c1": "No se ha encontrado ningún registro de cambio para {0} con el id {1}", + "0f1c71f74b040bfbe8d384a414e31f03": "Obtener un conjunto de deltas y conflictos desde el punto de comprobación especificado.", + "15254dec061d023d6c030083a0cef50f": "Crear una nueva instancia del modelo y hacerla persistente en el origen de datos.", + "16a11368d55b85a209fc6aea69ee5f7a": "Suprimir todos los registros coincidentes.", + "1bc1d489ddf347af47af3d9b1fc7cc15": "Ejecutar varias actualizaciones a la vez. Nota: no es atómico.", + "1bc7d8283c9abda512692925c8d8e3c0": "Obtener el punto de comprobación actual.", + "1caa7cc61266e7aef7db7d2f0e27ac3e": "Actualizar las propiedades del registro de cambio más reciente conservado para esta instancia.", + "2a7df74fe6e8462e617b79d5fbb536ea": "Obtener el registro de cambio más reciente para esta instancia.", + "2a9684b3d5b3b67af74bac74eb1b0843": "Buscar todas las instancias del modelo coincidentes por filtro del origen de datos.", + "2e50838caf0c927735eb15d12866bdd7": "Obtener los cambios de un modelo desde un punto de comprobación determinado. Proporcione un objeto de filtro para reducir el número de resultados devueltos.", + "4203ab415ec66a78d3164345439ba76e": "No se puede llamar a {0}.{1}(). El método {2} no se ha configurado. {{PersistedModel}} no se ha conectado correctamente a un {{DataSource}}.", + "51ea9b6245bb5e672b236d640ca3b048": "Un objeto de pares de nombre/valor de propiedad de cambio", + "55ddedd4c501348f82cb89db02ec85c1": "Un objeto de pares de nombre/valor de propiedad de modelo", + "5aaa76c72ae1689fd3cf62589784a4ba": "Actualizar atributos para una instancia de modelo y hacerla persistente en el origen de datos.", + "5f659bbc15e6e2b249fa33b3879b5f69": "Buscar una instancia de modelo por {{id}} del origen de datos.", + "62e8b0a733417978bab22c8dacf5d7e6": "No pueden aplicarse actualizaciones masivas, el conector no notifica correctamente el número de registros actualizados.", + "6329e0ac1de3c250ebb1df5afd5a2a7b": "El número de instancias actualizadas", + "6bc376432cd9972cf991aad3de371e78": "Faltan datos para el cambio: {0}", + "79295ac04822d2e9702f0dd1d0240336": "Actualizar instancias del modelo comparadas por {{where}} del origen de datos.", + "7f2fde7f0f860ead224b11ba8d75aa1c": "Crear una lista de actualización desde una lista delta.", + "89b57e764c2267129294b07589dbfdc2": "Suprimir una instancia de modelo por {{id}} del origen de datos.", + "8bab6720ecc58ec6412358c858a53484": "La actualización masiva ha fallado, el conector ha modificado un número de registros inesperado: {0}", + "a98b6cc6547706b5c6bffde0ed5fd55c": "Buscar primera instancia del modelo comparado por filtro del origen de datos.", + "c46d4aba1f14809c16730faa46933495": "Filtrar definiendo campos e incluir", + "c65600640f206f585d300b4bcb699d95": "Crear un punto de comprobación.", + "cf64c7afc74d3a8120abcd028f98c770": "Actualizar una instancia de modelo existente o insertar una nueva en el origen de datos.", + "dcb6261868ff0a7b928aa215b07d068c": "Crear una corriente de cambio.", + "e43e320a435ec1fa07648c1da0d558a7": "Comprobar si una instancia de modelo existe en el origen de datos.", + "e92aa25b6b864e3454b65a7c422bd114": "La actualización masiva ha fallado, el conector ha suprimido un número de registros inesperado: {0}", + "ea63d226b6968e328bdf6876010786b5": "No pueden aplicarse actualizaciones masivas, el conector no notifica correctamente el número de registros suprimidos.", + "f1d4ac54357cc0932f385d56814ba7e4": "Conflicto ", + "f37d94653793e33f4203e45b4a1cf106": "Recuento de instancias del modelo comparadas por where desde el origen de datos.", + "0731d0109e46c21a4e34af3346ed4856": "Este comportamiento puede cambiar en la próxima versión principal. ", + "2e110abee2c95bcfc2dafd48be7e2095": "No se puede configurar {0}: {{config.dataSource}} debe ser una instancia de {{DataSource}}", + "308e1d484516a33df788f873e65faaff": "El modelo `{0}` está ampliando `DataModel` en desuso. Utilice `PersistedModel` en su lugar. ", + "3438fab56cc7ab92dfd88f0497e523e0": "La configuración de la propiedad relations de `{0}` debe ser un objeto", + "4cac5f051ae431321673e04045d37772": "El modelo `{0}` está ampliando un modelo desconocido `{1}`. Se utiliza `PersistedModel` como base.", + "734a7bebb65e10899935126ba63dd51f": "La configuración de la propiedad de options de `{0}` debe ser un objeto", + "779467f467862836e19f494a37d6ab77": "La configuración de la propiedad acls de `{0}` debe ser una matriz de objetos", + "80a32e80cbed65eba2103201a7c94710": "No se ha encontrado el modelo: {0} ", + "83cbdc2560ba9f09155ccfc63e08f1a1": "La propiedad `{0}` no puede reconfigurarse para `{1}`", + "97795efe0c3eb7f35ce8cf8cfe70682b": "En la configuración de `{0}` falta la propiedad {{`dataSource`}}.\nUtilice `null` o `false` para marcar los modelos no conectados a ningún origen de datos.", + "a80038252430df2754884bf3c845c4cf": "En los metadatos de interacción remota para \"{0}.{1}\" falta el indicador \"isStatic\", el método está registrado como método de instancia. ", + "dc568bee32deb0f6eaf63e73b20e8ceb": "Se ignora el valor \"methods\" no de objeto de \"{0}\".", + "3aecb24fa8bdd3f79d168761ca8a6729": "Fase de {{middleware}} desconocida {0}" +} + diff --git a/intl/fr/messages.json b/intl/fr/messages.json new file mode 100644 index 000000000..3b087758a --- /dev/null +++ b/intl/fr/messages.json @@ -0,0 +1,116 @@ +{ + "3b46d3a780fd6ae5f95ade489a0efffe": "Le contexte en cours n'est pas pris en charge dans le navigateur.", + "7e0fca41d098607e1c9aa353c67e0fa1": "Jeton d'accès non valide", + "320c482401afa1207c04343ab162e803": "Type de principal non valide : {0}", + "c2b5d51f007178170ca3952d59640ca4": "Impossible de rectifier les modifications {0} :\n{1}", + "5858e63efaa0e4ad86b61c0459ea32fa": "Vous devez connecter le modèle {{Email}} à un connecteur {{Mail}}", + "0caffe1d763c8cca6a61814abe33b776": "L'adresse électronique est obligatoire", + "1b2a6076dccbe91a56f1672eb3b8598c": "Le corps de réponse contient les propriétés de {{AccessToken}} créées lors de la connexion.\nEn fonction de la valeur du paramètre `include`, le corps peut contenir des propriétés supplémentaires :\n\n - `user` - `U+007BUserU+007D` - Données de l'utilisateur connecté. {{(`include=user`)}}\n\n", + "2362ba55796c733e337af169922327a2": "Objets associés à inclure dans la réponse. Pour plus de détails, voir la description de la valeur de retour.", + "306999d39387d87b2638199ff0bed8ad": "Réinitialisez le mot de passe pour un utilisateur avec une adresse électronique.", + "3aae63bb7e8e046641767571c1591441": "la connexion a échoué car l'adresse électronique n'a pas été vérifiée", + "3caaa84fc103d6d5612173ae6d43b245": "Jeton non valide : {0}", + "42e3fa18945255412ebc6561e2c6f1dc": "Confirmez un enregistrement d'utilisateur avec jeton de vérification d'adresse électronique.", + "430b6326d7ebf6600a39f614ef516bc8": "Ne fournissez pas cet argument ; il est extrait automatiquement des en-têtes de la demande.", + "44a6c8b1ded4ed653d19ddeaaf89a606": "Adresse électronique introuvable", + "5e81ad3847a290dc650b47618b9cbc7e": "échec de la connexion", + "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "Connectez un utilisateur avec nom d'utilisateur/adresse électronique et mot de passe.", + "8608c28f5e6df0008266e3c497836176": "Déconnectez un utilisateur avec jeton d'accès.", + "860d1a0b8bd340411fb32baa72867989": "Le transport ne prend pas en charge les réacheminements HTTP.", + "895b1f941d026870b3cc8e6af087c197": "{{username}} ou {{email}} est obligatoire", + "a50d10fc6e0959b220e085454c40381e": "Utilisateur introuvable : {0}", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} est obligatoire", + "c34fa20eea0091747fcc9eda204b8d37": "impossible de trouver {{accessToken}}", + "c68a93f0a9524fed4ff64372fc90c55f": "Obligation de fournir une adresse électronique valide", + "f58cdc481540cd1f69a4aa4da2e37981": "Mot de passe non valide : {0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "résultat :{0}", + "10e01c895dc0b2fecc385f9f462f1ca6": "une liste de couleurs est disponible sur {{http://localhost:3000/colors}}", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "Demande à l'hôte {0}", + "a40684f5a9f546115258b76938d1de37": "Une liste de couleurs est disponible sur {{http://localhost:3000/colors}}", + "1e85f822b547a75d7d385048030e4ecb": "Création de : {0}", + "7d5e7ed0efaedf3f55f380caae0df8b8": "Ma première application mobile", + "04bd8af876f001ceaf443aad6a9002f9": "L'authentification exige que le modèle {0} soit défini.", + "095afbf2f1f0e5be678f5dac5c54e717": "Accès refusé", + "2d3071e3b18681c80a090dc0efbdb349": "impossible de trouver {0} avec l'id {1}", + "316e5b82c203cf3de31a449ee07d0650": "Valeur booléenne attendue, {0} obtenu", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Impossible de créer la source de données {0} : {1}", + "7e287fc885d9fdcf42da3a12f38572c1": "Autorisation requise", + "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} a été supprimé ; utilisez à la place le nouveau module {{loopback-boot}}", + "1d7833c3ca2f05fdad8fad7537531c40": "\t SUJET :{0}", + "275f22ab95671f095640ca99194b7635": "\t DE :{0}", + "3d63008ccfb2af1db2142e8cc2716ace": "Avertissement : Aucun transport de courrier électronique n'est spécifié pour l'envoi d'un message électronique. Configurez un transport pour envoyer des messages électroniques.", + "4a4f04a4e480fc5d4ee73b84d9a4b904": "Envoi d'un message électronique :", + "63a091ced88001ab6acb58f61ec041c5": "\t TEXTE :{0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML :{0}", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t A :{0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORT :{0}", + "0da38687fed24275c1547e815914a8e3": "Supprimez un élément lié par id pour {0}.", + "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "Critères de mise en concordance des instances de modèle", + "22fe62fa8d595b72c62208beddaa2a56": "Mettez à jour un élément lié par id pour {0}.", + "528325f3cbf1b0ab9a08447515daac9a": "Mettez à jour {0} de ce modèle.", + "543d19bad5e47ee1e9eb8af688e857b4": "Clé externe pour {0}.", + "598ff0255ffd1d1b71e8de55dbe2c034": "Vérifiez l'existence de la relation {0} à un élément par id.", + "5a36cc6ba0cc27c754f6c5ed6015ea3c": "Supprimez la relation {0} à un élément par id.", + "5fa3afb425819ebde958043e598cb664": "impossible de trouver un modèle avec {{id}} {0}", + "61e5deebaf44d68f4e6a508f30cc31a3": "La relation `{0}` n'existe pas pour le modèle `{1}`", + "651f0b3cbba001635152ec3d3d954d0a": "Recherchez un élément lié par id pour {0}.", + "7bc7b301ad9c4fc873029d57fb9740fe": "Demandes {0} de {1}.", + "7c837b88fd0e509bd3fc722d7ddf0711": "Clé externe pour {0}", + "830cb6c862f8f364e9064cea0026f701": "Extrait la relation hasOne {0}.", + "855ecd4a64885ba272d782435f72a4d4": "ID \"{0}\" inconnu \"{1}\".", + "86254879d01a60826a851066987703f2": "Ajoutez un élément lié par id pour {0}.", + "8ae418c605b6a45f2651be9b1677c180": "Méthode distante non valide : `{0}`", + "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Extrait la relation belongsTo {0}.", + "c0057a569ff9d3b509bac61a4b2f605d": "Supprime tous les {0} de ce modèle.", + "cd0412f2f33a4a2a316acc834f3f21a6": "obligation de spécifier {{id}} ou {{data}}", + "d6f43b266533b04d442bdb3955622592": "Crée une instance dans {0} de ce modèle.", + "da13d3cdf21330557254670dddd8c5c7": "Compte {0} de {1}.", + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" {{id}} \"{1}\" inconnu.", + "f66ae3cf379b2fce28575a3282defe1a": "Supprime {0} de ce modèle.", + "03f79fa268fe199de2ce4345515431c1": "Aucun enregistrement de changement trouvé pour {0} avec l'id {1}", + "0f1c71f74b040bfbe8d384a414e31f03": "Obtenez un ensemble d'écarts et de conflits depuis le point de contrôle donné.", + "15254dec061d023d6c030083a0cef50f": "Créez une instance du modèle et rendez-la persistante dans la source de données.", + "16a11368d55b85a209fc6aea69ee5f7a": "Supprimez tous les enregistrements correspondants.", + "1bc1d489ddf347af47af3d9b1fc7cc15": "Exécutez plusieurs mises à jour simultanément. Remarque : ce n'est pas atomique.", + "1bc7d8283c9abda512692925c8d8e3c0": "Obtenez le point de contrôle en cours.", + "1caa7cc61266e7aef7db7d2f0e27ac3e": "Mettez à jour les propriétés de l'enregistrement d'un changement le plus récent conservé pour cette instance.", + "2a7df74fe6e8462e617b79d5fbb536ea": "Obtenez l'enregistrement d'un changement le plus récent pour cette instance.", + "2a9684b3d5b3b67af74bac74eb1b0843": "Recherchez toutes les instances du modèle correspondant au filtre à partir de la source de données.", + "2e50838caf0c927735eb15d12866bdd7": "Obtenez les changements apportés à un modèle depuis un point de contrôle donné. Fournissez un objet de filtre pour réduire le nombre de résultats renvoyés.", + "4203ab415ec66a78d3164345439ba76e": "Impossible d'appeler {0}.{1}(). La méthode {2} n'a pas été configurée. {{PersistedModel}} n'a pas été associé correctement à {{DataSource}} !", + "51ea9b6245bb5e672b236d640ca3b048": "Objet des paires nom-valeur de la propriété change", + "55ddedd4c501348f82cb89db02ec85c1": "Objet des paires nom-valeur de la propriété model", + "5aaa76c72ae1689fd3cf62589784a4ba": "Mettez à jour les attributs pour une instance de modèle et rendez-la persistante dans la source de données.", + "5f659bbc15e6e2b249fa33b3879b5f69": "Recherchez une instance de modèle par {{id}} à partir de la source de données.", + "62e8b0a733417978bab22c8dacf5d7e6": "Impossible d'appliquer des mises à jour en bloc ; le connecteur ne signale pas correctement le nombre d'enregistrements mis à jour.", + "6329e0ac1de3c250ebb1df5afd5a2a7b": "Nombre d'instances mises à jour", + "6bc376432cd9972cf991aad3de371e78": "Données manquantes pour le changement : {0}", + "79295ac04822d2e9702f0dd1d0240336": "Mettez à jour les instances du modèle correspondant à {{where}} à partir de la source de données.", + "7f2fde7f0f860ead224b11ba8d75aa1c": "Créez une liste de mises à jour à partir d'une liste d'écarts.", + "89b57e764c2267129294b07589dbfdc2": "Supprimez une instance de modèle par {{id}} à partir de la source de données.", + "8bab6720ecc58ec6412358c858a53484": "La mise à jour en bloc a échoué ; le connecteur a modifié un nombre inattendu d'enregistrements : {0}", + "a98b6cc6547706b5c6bffde0ed5fd55c": "Recherchez la première instance du modèle correspondant au filtre à partir de la source de données.", + "c46d4aba1f14809c16730faa46933495": "Filtrer en définissant fields et include", + "c65600640f206f585d300b4bcb699d95": "Créez un point de contrôle.", + "cf64c7afc74d3a8120abcd028f98c770": "Mettez à jour une instance de modèle existante ou insérez-en une nouvelle dans la source de données.", + "dcb6261868ff0a7b928aa215b07d068c": "Créez un flux de changements.", + "e43e320a435ec1fa07648c1da0d558a7": "Vérifiez si une instance de modèle existe dans la source de données.", + "e92aa25b6b864e3454b65a7c422bd114": "La mise à jour en bloc a échoué ; le connecteur a supprimé un nombre inattendu d'enregistrements : {0}", + "ea63d226b6968e328bdf6876010786b5": "Impossible d'appliquer des mises à jour en bloc ; le connecteur ne signale pas correctement le nombre d'enregistrements supprimés.", + "f1d4ac54357cc0932f385d56814ba7e4": "Conflit", + "f37d94653793e33f4203e45b4a1cf106": "Dénombrez les instances du modèle correspondant à where à partir de la source de données.", + "0731d0109e46c21a4e34af3346ed4856": "Ce comportement peut changer dans la version principale suivante.", + "2e110abee2c95bcfc2dafd48be7e2095": "Impossible de configurer {0} : {{config.dataSource}} doit être une instance de {{DataSource}}", + "308e1d484516a33df788f873e65faaff": "Le modèle `{0}` étend le `DataModel obsolète. Utilisez à la place `PersistedModel`.", + "3438fab56cc7ab92dfd88f0497e523e0": "La propriété relations de la configuration `{0}` doit être un objet", + "4cac5f051ae431321673e04045d37772": "Le modèle `{0}` étend un modèle inconnu `{1}`. Utilisation de `PersistedModel` comme base.", + "734a7bebb65e10899935126ba63dd51f": "La propriété options de la configuration `{0}` doit être un objet", + "779467f467862836e19f494a37d6ab77": "La propriété acls de la configuration `{0}` doit être un tableau d'objets", + "80a32e80cbed65eba2103201a7c94710": "Modèle introuvable : {0}", + "83cbdc2560ba9f09155ccfc63e08f1a1": "La propriété `{0}` ne peut pas être reconfigurée pour `{1}`", + "97795efe0c3eb7f35ce8cf8cfe70682b": "La propriété {{`dataSource`}} est manquante dans la configuration de `{0}`.\nUtilisez `null` ou `false` pour marquer les modèles non associés à une source de données.", + "a80038252430df2754884bf3c845c4cf": "Métadonnées remoting pour \"{0}.{1}\" ne comporte pas l'indicateur \"isStatic\" ; la méthode est enregistrée en tant que méthode instance.", + "dc568bee32deb0f6eaf63e73b20e8ceb": "Le paramètre \"methods\" non objet de \"{0}\" est ignoré.", + "3aecb24fa8bdd3f79d168761ca8a6729": "Phase {{middleware}} inconnue {0}" +} + diff --git a/intl/it/messages.json b/intl/it/messages.json new file mode 100644 index 000000000..db2a718b6 --- /dev/null +++ b/intl/it/messages.json @@ -0,0 +1,116 @@ +{ + "3b46d3a780fd6ae5f95ade489a0efffe": "Il contesto corrente non è supportato nel browser.", + "7e0fca41d098607e1c9aa353c67e0fa1": "Token di accesso non valido", + "320c482401afa1207c04343ab162e803": "Tipo principal non valido: {0}", + "c2b5d51f007178170ca3952d59640ca4": "Impossibile correggere {0} modifiche:\n{1}", + "5858e63efaa0e4ad86b61c0459ea32fa": "È necessario collegare il modello {{Email}} ad un connettore {{Mail}}", + "0caffe1d763c8cca6a61814abe33b776": "L'email è obbligatoria", + "1b2a6076dccbe91a56f1672eb3b8598c": "Il corpo della risposta contiene proprietà del {{AccessToken}} creato all'accesso. \nIn base al valore del parametro `include`, il corpo può contenere ulteriori proprietà:\n\n - `user` - `U+007BUserU+007D` - Dati dell'utente attualmente collegato.. {{(`include=user`)}}\n\n", + "2362ba55796c733e337af169922327a2": "Oggetti correlati da includere nella risposta. Per ulteriori dettagli, consultare la descrizione del valore di restituzione.", + "306999d39387d87b2638199ff0bed8ad": "Reimpostare la password per un utente con email.", + "3aae63bb7e8e046641767571c1591441": "login non riuscito perché l'email non è stata verificata", + "3caaa84fc103d6d5612173ae6d43b245": "Token non valido: {0}", + "42e3fa18945255412ebc6561e2c6f1dc": "Confermare una registrazione utente con token di verifica email.", + "430b6326d7ebf6600a39f614ef516bc8": "Non fornire questo argomento, viene automaticamente estratto dalle intestazioni richiesta.", + "44a6c8b1ded4ed653d19ddeaaf89a606": "Email non trovata", + "5e81ad3847a290dc650b47618b9cbc7e": "login non riuscito", + "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "Eseguire il login di un utente con nome utente/email e password.", + "8608c28f5e6df0008266e3c497836176": "Scollegare un utente con token di accesso.", + "860d1a0b8bd340411fb32baa72867989": "Il trasporto non supporta i reindirizzamenti HTTP.", + "895b1f941d026870b3cc8e6af087c197": "Sono richiesti {{username}} o {{email}}", + "a50d10fc6e0959b220e085454c40381e": "Utente non trovato: {0}", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} è obbligatorio", + "c34fa20eea0091747fcc9eda204b8d37": "impossibile trovare {{accessToken}}", + "c68a93f0a9524fed4ff64372fc90c55f": "È necessario fornire una email valida", + "f58cdc481540cd1f69a4aa4da2e37981": "Password non valida: {0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "risultato:{0}", + "10e01c895dc0b2fecc385f9f462f1ca6": "un elenco dei colori è disponibile all'indirizzo {{http://localhost:3000/colors}}", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "Richiesta all'host {0}", + "a40684f5a9f546115258b76938d1de37": "Un elenco dei colori è disponibile all'indirizzo {{http://localhost:3000/colors}}", + "1e85f822b547a75d7d385048030e4ecb": "Creato: {0}", + "7d5e7ed0efaedf3f55f380caae0df8b8": "Prima applicazione mobile personale", + "04bd8af876f001ceaf443aad6a9002f9": "L'autenticazione richiede che sia definito il modello {0}.", + "095afbf2f1f0e5be678f5dac5c54e717": "Accesso negato", + "2d3071e3b18681c80a090dc0efbdb349": "impossibile trovare {0} con id {1}", + "316e5b82c203cf3de31a449ee07d0650": "Previsto valore booleano, ricevuto {0}", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Impossibile creare l'origine dati {0}: {1}", + "7e287fc885d9fdcf42da3a12f38572c1": "Autorizzazione richiesta", + "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} è stato rimosso, utilizzare il nuovo modulo {{loopback-boot}}", + "1d7833c3ca2f05fdad8fad7537531c40": "\t OGGETTO:{0}", + "275f22ab95671f095640ca99194b7635": "\t DA:{0}", + "3d63008ccfb2af1db2142e8cc2716ace": "Avvertenza: nessun trasporto email specificato per l'invio della email. Configurare un trasporto per inviare messaggi email.", + "4a4f04a4e480fc5d4ee73b84d9a4b904": "Invio email:", + "63a091ced88001ab6acb58f61ec041c5": "\t TESTO:{0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t A:{0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRASPORTO:{0}", + "0da38687fed24275c1547e815914a8e3": "Eliminare un elemento correlato in base all'ID per {0}.", + "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "Criteri per la corrispondenza delle istanze del modello", + "22fe62fa8d595b72c62208beddaa2a56": "Aggiornare un elemento correlato in base all'ID per {0}.", + "528325f3cbf1b0ab9a08447515daac9a": "Aggiornare {0} di questo modello.", + "543d19bad5e47ee1e9eb8af688e857b4": "Chiave esterna per {0}.", + "598ff0255ffd1d1b71e8de55dbe2c034": "Verificare l'esistenza della relazione {0} ad un elemento in base all'ID.", + "5a36cc6ba0cc27c754f6c5ed6015ea3c": "Rimuovere la relazione {0} ad un elemento in base all'ID.", + "5fa3afb425819ebde958043e598cb664": "impossibile trovare un modello con {{id}} {0}", + "61e5deebaf44d68f4e6a508f30cc31a3": "La relazione `{0}` non esiste per il modello `{1}`", + "651f0b3cbba001635152ec3d3d954d0a": "Trovare un elemento correlato in base all'ID per {0}.", + "7bc7b301ad9c4fc873029d57fb9740fe": "Query {0} di {1}.", + "7c837b88fd0e509bd3fc722d7ddf0711": "Chiave esterna per {0}", + "830cb6c862f8f364e9064cea0026f701": "Recupera la relazione hasOne {0}.", + "855ecd4a64885ba272d782435f72a4d4": "ID sconosciuto \"{0}\" \"{1}\".", + "86254879d01a60826a851066987703f2": "Aggiungere un elemento correlato in base all'ID per {0}.", + "8ae418c605b6a45f2651be9b1677c180": "Metodo remoto non valido: `{0}`", + "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Recupera la relazione belongsTo {0}.", + "c0057a569ff9d3b509bac61a4b2f605d": "Elimina tutti i {0} di questo modello.", + "cd0412f2f33a4a2a316acc834f3f21a6": "è necessario specificare {{id}} o {{data}}", + "d6f43b266533b04d442bdb3955622592": "Crea una nuova istanza di questo modello in {0}.", + "da13d3cdf21330557254670dddd8c5c7": "{0} di {1}.", + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "{{id}} \"{0}\" sconosciuto \"{1}\".", + "f66ae3cf379b2fce28575a3282defe1a": "Elimina {0} di questo modello.", + "03f79fa268fe199de2ce4345515431c1": "Nessun record di modifica trovato per {0} con id {1}", + "0f1c71f74b040bfbe8d384a414e31f03": "Acquisire una serie di delta e conflitti a partire dal checkpoint fornito.", + "15254dec061d023d6c030083a0cef50f": "Creare una nuova istanza del modello e renderla permanente nell'origine dati.", + "16a11368d55b85a209fc6aea69ee5f7a": "Eliminare tutti i record corrispondenti.", + "1bc1d489ddf347af47af3d9b1fc7cc15": "Eseguire più aggiornamenti contemporaneamente. Nota: questa operazione non è atomica.", + "1bc7d8283c9abda512692925c8d8e3c0": "Acquisire il checkpoint corrente.", + "1caa7cc61266e7aef7db7d2f0e27ac3e": "Aggiornare le proprietà del record di modifiche più recente conservato per questa istanza.", + "2a7df74fe6e8462e617b79d5fbb536ea": "Acquisire il record di modifiche più recente per questa istanza.", + "2a9684b3d5b3b67af74bac74eb1b0843": "Trovare tutte le istanze del modello corrispondenti in base al filtro dall'origine dati.", + "2e50838caf0c927735eb15d12866bdd7": "Acquisire le modifiche ad un modello a partire da un determinato checkpoint. Fornire un oggetto filtro per ridurre il numero di risultati restituiti.", + "4203ab415ec66a78d3164345439ba76e": "Impossibile richiamare {0}.{1}(). Il metodo {2} non è stato configurato. {{PersistedModel}} non è stato correttamente collegato ad una {{DataSource}}!", + "51ea9b6245bb5e672b236d640ca3b048": "Un oggetto della coppia nome/valore della proprietà di modifica", + "55ddedd4c501348f82cb89db02ec85c1": "In oggetto della coppia nome/valore della proprietà del modello", + "5aaa76c72ae1689fd3cf62589784a4ba": "Aggiornare gli attributi per un'istanza del modello e renderli permanenti nell'origine dati.", + "5f659bbc15e6e2b249fa33b3879b5f69": "Trovare un'istanza del modello in base a {{id}} dall'origine dati.", + "62e8b0a733417978bab22c8dacf5d7e6": "Impossibile applicare gli aggiornamenti in massa, il connettore non indica correttamente il numero di record aggiornati.", + "6329e0ac1de3c250ebb1df5afd5a2a7b": "Il numero di istanze aggiornate", + "6bc376432cd9972cf991aad3de371e78": "Dati mancanti per la modifica: {0}", + "79295ac04822d2e9702f0dd1d0240336": "Aggiornare le istanze del modello corrispondenti in base a {{where}} dall'origine dati.", + "7f2fde7f0f860ead224b11ba8d75aa1c": "Creare un elenco di aggiornamento da un elenco delta.", + "89b57e764c2267129294b07589dbfdc2": "Eliminare un'istanza del modello in base a {{id}} dall'origine dati.", + "8bab6720ecc58ec6412358c858a53484": "Aggiornamento in massa non riuscito, il connettore ha modificato un numero non previsto di record: {0}", + "a98b6cc6547706b5c6bffde0ed5fd55c": "Trovare la prima istanza del modello corrispondente in base al filtro dall'origine dati.", + "c46d4aba1f14809c16730faa46933495": "Filtro che definisce campi ed include", + "c65600640f206f585d300b4bcb699d95": "Creare un checkpoint.", + "cf64c7afc74d3a8120abcd028f98c770": "Aggiornare un'istanza del modello esistente oppure inserire una nuova istanza nell'origine dati.", + "dcb6261868ff0a7b928aa215b07d068c": "Creare un flusso di modifica.", + "e43e320a435ec1fa07648c1da0d558a7": "Verificare se nell'origine dati esiste un'istanza del modello.", + "e92aa25b6b864e3454b65a7c422bd114": "Aggiornamento in massa non riuscito, il connettore ha eliminato un numero non previsto di record: {0}", + "ea63d226b6968e328bdf6876010786b5": "Impossibile applicare gli aggiornamenti in massa, il connettore non indica correttamente il numero di record eliminati.", + "f1d4ac54357cc0932f385d56814ba7e4": "Conflitto", + "f37d94653793e33f4203e45b4a1cf106": "Conteggiare le istanze del modello corrispondenti in base a where dall'origine dati.", + "0731d0109e46c21a4e34af3346ed4856": "Questo funzionamento può essere modificato nella versione principale successiva.", + "2e110abee2c95bcfc2dafd48be7e2095": "Impossibile configurare {0}: {{config.dataSource}} deve essere un'istanza di {{DataSource}}", + "308e1d484516a33df788f873e65faaff": "Il modello `{0}` estende il modello `DataModel obsoleto. Utilizzare `PersistedModel`.", + "3438fab56cc7ab92dfd88f0497e523e0": "La proprietà relations della configurazione `{0}` deve essere un oggetto", + "4cac5f051ae431321673e04045d37772": "Il modello `{0}` estende un modello sconosciuto `{1}`. Viene utilizzato `PersistedModel` come base.", + "734a7bebb65e10899935126ba63dd51f": "La proprietà options della configurazione `{0}` deve essere un oggetto", + "779467f467862836e19f494a37d6ab77": "La proprietà acls della configurazione `{0}` deve essere un array di oggetti", + "80a32e80cbed65eba2103201a7c94710": "Modello non trovato: {0}", + "83cbdc2560ba9f09155ccfc63e08f1a1": "Impossibile riconfigurare la proprietà `{0}` per `{1}`", + "97795efe0c3eb7f35ce8cf8cfe70682b": "La configurazione di `{0}` non contiene la proprietà {{`dataSource`}}.\nUtilizzare `null` o `false` per contrassegnare i modelli non collegati ad alcuna origine dati.", + "a80038252430df2754884bf3c845c4cf": "Metadati della comunicazione in remoto per \"{0}.{1}\" non presenta l'indicatore \"isStatic\", il metodo è registrato come metodo dell'istanza.", + "dc568bee32deb0f6eaf63e73b20e8ceb": "L'impostazione \"methods\" non oggetto di \"{0}\" viene ignorata.", + "3aecb24fa8bdd3f79d168761ca8a6729": "Fase {{middleware}} sconosciuta {0}" +} + diff --git a/intl/ja/messages.json b/intl/ja/messages.json new file mode 100644 index 000000000..8f5ae814a --- /dev/null +++ b/intl/ja/messages.json @@ -0,0 +1,116 @@ +{ + "3b46d3a780fd6ae5f95ade489a0efffe": "現行コンテキストはブラウザーではサポートされていません。", + "7e0fca41d098607e1c9aa353c67e0fa1": "無効なアクセス・トークン", + "320c482401afa1207c04343ab162e803": "無効なプリンシパル・タイプ: {0}", + "c2b5d51f007178170ca3952d59640ca4": "{0} 件の変更を修正できません:\n{1}", + "5858e63efaa0e4ad86b61c0459ea32fa": "{{Email}} モデルを {{Mail}} コネクターに接続する必要があります", + "0caffe1d763c8cca6a61814abe33b776": "E メールが必要です", + "1b2a6076dccbe91a56f1672eb3b8598c": "応答の本文には、ログイン時に作成された {{AccessToken}} のプロパティーが含まれます。\n「include」パラメーターの値によっては、本文に追加プロパティーが含まれる場合があります:\n\n - 「user」 - 「U+007BUserU+007D」 - 現在ログインしているユーザーのデータ。 {{(`include=user`)}}\n\n", + "2362ba55796c733e337af169922327a2": "応答に含める関連オブジェクト。詳細については、戻り値の説明を参照してください。", + "306999d39387d87b2638199ff0bed8ad": "E メールを使用してユーザーのパスワードをリセットします。", + "3aae63bb7e8e046641767571c1591441": "E メールが検証されていないためログインに失敗しました", + "3caaa84fc103d6d5612173ae6d43b245": "無効なトークン: {0}", + "42e3fa18945255412ebc6561e2c6f1dc": "E メール検証トークンを使用してユーザー登録を確認します。", + "430b6326d7ebf6600a39f614ef516bc8": "この引数は指定しないでください。要求ヘッダーから自動的に抽出されます。", + "44a6c8b1ded4ed653d19ddeaaf89a606": "E メールが見つかりません", + "5e81ad3847a290dc650b47618b9cbc7e": "ログインに失敗しました", + "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "ユーザー名/E メールおよびパスワードを使用してユーザーをログインさせます。", + "8608c28f5e6df0008266e3c497836176": "アクセス・トークンを使用してユーザーをログアウトさせます。", + "860d1a0b8bd340411fb32baa72867989": "トランスポートは HTTP リダイレクトをサポートしていません。", + "895b1f941d026870b3cc8e6af087c197": "{{username}} または {{email}} が必要です", + "a50d10fc6e0959b220e085454c40381e": "ユーザーが見つかりません: {0}", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} が必要です", + "c34fa20eea0091747fcc9eda204b8d37": "{{accessToken}} が見つかりませんでした", + "c68a93f0a9524fed4ff64372fc90c55f": "有効な E メールを指定する必要があります", + "f58cdc481540cd1f69a4aa4da2e37981": "無効なパスワード: {0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "結果: {0}", + "10e01c895dc0b2fecc385f9f462f1ca6": "色のリストを {{http://localhost:3000/colors}} で参照できます", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "ホスト {0} に対する要求", + "a40684f5a9f546115258b76938d1de37": "色のリストを {{http://localhost:3000/colors}} で参照できます", + "1e85f822b547a75d7d385048030e4ecb": "作成: {0}", + "7d5e7ed0efaedf3f55f380caae0df8b8": "自分の最初のモバイル・アプリケーション", + "04bd8af876f001ceaf443aad6a9002f9": "認証ではモデル {0} を定義する必要があります。", + "095afbf2f1f0e5be678f5dac5c54e717": "アクセスが拒否されました", + "2d3071e3b18681c80a090dc0efbdb349": "ID が {1} である {0} は見つかりませんでした", + "316e5b82c203cf3de31a449ee07d0650": "ブール値が必要ですが、{0} を受け取りました", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "データ・ソース {0} を作成できません: {1}", + "7e287fc885d9fdcf42da3a12f38572c1": "許可が必要です", + "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} は削除されました。代わりに新規モジュール {{loopback-boot}} を使用してください", + "1d7833c3ca2f05fdad8fad7537531c40": "\t 件名:{0}", + "275f22ab95671f095640ca99194b7635": "\t 送信元:{0}", + "3d63008ccfb2af1db2142e8cc2716ace": "警告: E メールを送信するための E メール・トランスポートが指定されていません。メール・メッセージを送信するためのトランスポートをセットアップしてください。", + "4a4f04a4e480fc5d4ee73b84d9a4b904": "メールの送信:", + "63a091ced88001ab6acb58f61ec041c5": "\t テキスト:{0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t 宛先:{0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t トランスポート:{0}", + "0da38687fed24275c1547e815914a8e3": "{0} の ID によって関連項目を削除します。", + "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "モデル・インスタンスと突き合わせる基準", + "22fe62fa8d595b72c62208beddaa2a56": "{0} の ID によって関連項目を更新します。", + "528325f3cbf1b0ab9a08447515daac9a": "このモデルの {0} を更新します。", + "543d19bad5e47ee1e9eb8af688e857b4": "{0} の外部キー。", + "598ff0255ffd1d1b71e8de55dbe2c034": "ID によって項目に対する {0} 関係の存在を検査します。", + "5a36cc6ba0cc27c754f6c5ed6015ea3c": "ID によって項目に対する {0} 関係を削除します。", + "5fa3afb425819ebde958043e598cb664": "{{id}} {0} のモデルは見つかりませんでした", + "61e5deebaf44d68f4e6a508f30cc31a3": "モデル `{1}` の関係 `{0}` は存在しません", + "651f0b3cbba001635152ec3d3d954d0a": "{0} の ID によって関連項目を検索します。", + "7bc7b301ad9c4fc873029d57fb9740fe": "{1} の {0} を照会します。", + "7c837b88fd0e509bd3fc722d7ddf0711": "{0} の外部キー", + "830cb6c862f8f364e9064cea0026f701": "hasOne 関係 {0} をフェッチします。", + "855ecd4a64885ba272d782435f72a4d4": "不明な \"{0}\" ID \"{1}\"。", + "86254879d01a60826a851066987703f2": "{0} の ID によって関連項目を追加します。", + "8ae418c605b6a45f2651be9b1677c180": "無効なリモート・メソッド: `{0}`", + "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "belongsTo 関係 {0} をフェッチします。", + "c0057a569ff9d3b509bac61a4b2f605d": "このモデルのすべての {0} を削除します。", + "cd0412f2f33a4a2a316acc834f3f21a6": "{{id}} または {{data}} を指定する必要があります", + "d6f43b266533b04d442bdb3955622592": "このモデルの {0} の新規インスタンスを作成します。", + "da13d3cdf21330557254670dddd8c5c7": "{1} の {0} をカウントします。", + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "不明な \"{0}\" {{id}} \"{1}\"。", + "f66ae3cf379b2fce28575a3282defe1a": "このモデルの {0} を削除します。", + "03f79fa268fe199de2ce4345515431c1": "ID が {1} である {0} の変更レコードは見つかりませんでした", + "0f1c71f74b040bfbe8d384a414e31f03": "指定のチェックポイント以降の差分および競合のセットを取得します。", + "15254dec061d023d6c030083a0cef50f": "モデルの新規インスタンスを作成し、データ・ソースに永続化します。", + "16a11368d55b85a209fc6aea69ee5f7a": "一致するレコードをすべて削除します。", + "1bc1d489ddf347af47af3d9b1fc7cc15": "一度に複数の更新を実行します。注: これはアトミックではありません。", + "1bc7d8283c9abda512692925c8d8e3c0": "現在のチェックポイントを取得します。", + "1caa7cc61266e7aef7db7d2f0e27ac3e": "このインスタンスに対して保持されている最新の変更レコードのプロパティーを更新します。", + "2a7df74fe6e8462e617b79d5fbb536ea": "このインスタンスの最新の変更レコードを取得します。", + "2a9684b3d5b3b67af74bac74eb1b0843": "データ・ソースからフィルターと一致するモデルのすべてのインスタンスを検索します。", + "2e50838caf0c927735eb15d12866bdd7": "指定のチェックポイント以降のモデルに対する変更を取得します。フィルター・オブジェクトを指定して、返される結果の数を減らします。", + "4203ab415ec66a78d3164345439ba76e": "{0}.{1}() を呼び出せません。{2} メソッドがセットアップされていません。{{PersistedModel}} が {{DataSource}} に正しく付加されていません。", + "51ea9b6245bb5e672b236d640ca3b048": "変更プロパティー名/値のペアのオブジェクト", + "55ddedd4c501348f82cb89db02ec85c1": "モデル・プロパティー名/値のペアのオブジェクト", + "5aaa76c72ae1689fd3cf62589784a4ba": "モデル・インスタンスの属性を更新し、データ・ソースに永続化します。", + "5f659bbc15e6e2b249fa33b3879b5f69": "データ・ソースから {{id}} によってモデル・インスタンスを検索します。", + "62e8b0a733417978bab22c8dacf5d7e6": "一括更新を適用できません。更新レコードの数がコネクターによって正しく報告されません。", + "6329e0ac1de3c250ebb1df5afd5a2a7b": "更新されたインスタンスの数", + "6bc376432cd9972cf991aad3de371e78": "変更の欠落データ: {0}", + "79295ac04822d2e9702f0dd1d0240336": "データ・ソースから {{where}} と一致するモデルのインスタンスを更新します。", + "7f2fde7f0f860ead224b11ba8d75aa1c": "差分リストから更新リストを作成します。", + "89b57e764c2267129294b07589dbfdc2": "データ・ソースから {{id}} によってモデル・インスタンスを削除します。", + "8bab6720ecc58ec6412358c858a53484": "一括更新に失敗しました。コネクターは、予期しない数のレコードを変更しました: {0}", + "a98b6cc6547706b5c6bffde0ed5fd55c": "データ・ソースからフィルターと一致するモデルの最初のインスタンスを検索します。", + "c46d4aba1f14809c16730faa46933495": "フィルター定義フィールドおよび include", + "c65600640f206f585d300b4bcb699d95": "チェックポイントを作成します。 ", + "cf64c7afc74d3a8120abcd028f98c770": "既存のモデル・インスタンスを更新するか、新規のモデル・インスタンスをデータ・ソースに挿入します。", + "dcb6261868ff0a7b928aa215b07d068c": "変更ストリームを作成します。", + "e43e320a435ec1fa07648c1da0d558a7": "モデル・インスタンスがデータ・ソースに存在するかどうか検査します。", + "e92aa25b6b864e3454b65a7c422bd114": "一括更新に失敗しました。コネクターは、予期しない数のレコードを削除しました: {0}", + "ea63d226b6968e328bdf6876010786b5": "一括更新を適用できません。削除レコードの数がコネクターによって正しく報告されません。", + "f1d4ac54357cc0932f385d56814ba7e4": "競合", + "f37d94653793e33f4203e45b4a1cf106": "データ・ソースから where と一致するモデルのインスタンスをカウントします。", + "0731d0109e46c21a4e34af3346ed4856": "この動作は、次のメジャー・バージョンで変わる可能性があります。", + "2e110abee2c95bcfc2dafd48be7e2095": "{0} を構成できません: {{config.dataSource}} は {{DataSource}} のインスタンスでなければなりません。 ", + "308e1d484516a33df788f873e65faaff": "モデル `{0}` は、非推奨の「DataModel」を拡張しています。代わりに「PersistedModel」を使用してください。", + "3438fab56cc7ab92dfd88f0497e523e0": "`{0}` 構成の relations プロパティーはオブジェクトでなければなりません", + "4cac5f051ae431321673e04045d37772": "モデル `{0}` は、不明なモデル `{1}` を拡張しています。ベースとして「PersistedModel」を使用します。", + "734a7bebb65e10899935126ba63dd51f": "`{0}` 構成の options プロパティーはオブジェクトでなければなりません", + "779467f467862836e19f494a37d6ab77": "`{0}` 構成の acls プロパティーはオブジェクトの配列でなければなりません", + "80a32e80cbed65eba2103201a7c94710": "モデルが見つかりません: {0}", + "83cbdc2560ba9f09155ccfc63e08f1a1": "プロパティー`{0}` を `{1}` に対して再構成できません", + "97795efe0c3eb7f35ce8cf8cfe70682b": "`{0}` の構成に {{`dataSource`}} プロパティーが欠落しています。\n「null」または「false」を使用して、データ・ソースに付加されないモデルにマークを付けます。", + "a80038252430df2754884bf3c845c4cf": "\"{0}.{1}\" のリモート・メタデータに \"isStatic\" フラグが欠落しています。このメソッドはインスタンス・メソッドとして登録されています。", + "dc568bee32deb0f6eaf63e73b20e8ceb": "\"{0}\" の非オブジェクト \"methods\" 設定を無視します。", + "3aecb24fa8bdd3f79d168761ca8a6729": "不明な {{middleware}} のフェーズ {0}" +} + diff --git a/intl/ko/messages.json b/intl/ko/messages.json new file mode 100644 index 000000000..a1a057b7b --- /dev/null +++ b/intl/ko/messages.json @@ -0,0 +1,116 @@ +{ + "3b46d3a780fd6ae5f95ade489a0efffe": "현재 컨텍스트가 브라우저에서 지원되지 않습니다. ", + "7e0fca41d098607e1c9aa353c67e0fa1": "올바르지 않은 액세스 토큰", + "320c482401afa1207c04343ab162e803": "올바르지 않은 프린시펄 유형: {0}", + "c2b5d51f007178170ca3952d59640ca4": "{0} 변경사항을 교정할 수 없음:\n{1}", + "5858e63efaa0e4ad86b61c0459ea32fa": "{{Email}} 모델을 {{Mail}} 커넥터에 연결해야 합니다. ", + "0caffe1d763c8cca6a61814abe33b776": "이메일은 필수입니다.", + "1b2a6076dccbe91a56f1672eb3b8598c": "응답 본문에 로그인 시 작성한 {{AccessToken}} 특성이 포함됩니다. \n`include` 매개변수 값에 따라 본문에 추가 특성이 포함될 수 있습니다. \n\n - `user` - `U+007BUserU+007D` - 현재 로그인된 사용자의 데이터. {{(`include=user`)}}\n\n", + "2362ba55796c733e337af169922327a2": "응답에 포함시킬 관련 오브젝트입니다. 자세한 정보는 리턴 값의 설명을 참조하십시오. ", + "306999d39387d87b2638199ff0bed8ad": "이메일을 사용하여 사용자 비밀번호를 재설정하십시오. ", + "3aae63bb7e8e046641767571c1591441": "이메일이 확인되지 않아서 로그인에 실패했습니다. ", + "3caaa84fc103d6d5612173ae6d43b245": "올바르지 않은 토큰: {0}", + "42e3fa18945255412ebc6561e2c6f1dc": "이메일 확인 토큰을 사용하여 사용자 등록을 확인하십시오. ", + "430b6326d7ebf6600a39f614ef516bc8": "이 인수를 제공하지 마십시오. 이는 요청 헤더에서 자동으로 추출됩니다. ", + "44a6c8b1ded4ed653d19ddeaaf89a606": "이메일을 찾을 수 없음", + "5e81ad3847a290dc650b47618b9cbc7e": "로그인 실패", + "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "사용자 이름/이메일 및 비밀번호를 가진 사용자로 로그인하십시오. ", + "8608c28f5e6df0008266e3c497836176": "액세스 토큰을 가진 사용자로 로그아웃하십시오. ", + "860d1a0b8bd340411fb32baa72867989": "전송에서 HTTP 경로 재지원을 지원하지 않습니다.", + "895b1f941d026870b3cc8e6af087c197": "{{username}} 또는 {{email}}은(는) 필수입니다.", + "a50d10fc6e0959b220e085454c40381e": "사용자를 찾을 수 없음: {0}", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}}은(는) 필수입니다.", + "c34fa20eea0091747fcc9eda204b8d37": "{{accessToken}}을(를) 찾을 수 없음", + "c68a93f0a9524fed4ff64372fc90c55f": "올바른 이메일을 제공해야 함", + "f58cdc481540cd1f69a4aa4da2e37981": "올바르지 않은 비밀번호: {0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "결과: {0}", + "10e01c895dc0b2fecc385f9f462f1ca6": "색상 목록은 {{http://localhost:3000/colors}}에 있음", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "호스트 {0}에 요청", + "a40684f5a9f546115258b76938d1de37": "색상 목록은 {{http://localhost:3000/colors}}에 있음", + "1e85f822b547a75d7d385048030e4ecb": "작성 날짜: {0}", + "7d5e7ed0efaedf3f55f380caae0df8b8": "내 첫 번째 모바일 애플리케이션", + "04bd8af876f001ceaf443aad6a9002f9": "인증을 위해 {0} 모델이 정의되어야 함", + "095afbf2f1f0e5be678f5dac5c54e717": "액세스 거부", + "2d3071e3b18681c80a090dc0efbdb349": "ID {1}(으)로 {0}을(를) 찾을 수 없음 ", + "316e5b82c203cf3de31a449ee07d0650": "예상 부울, 실제 {0}", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "데이터 소스 {0}을(를) 작성할 수 없음: {1}", + "7e287fc885d9fdcf42da3a12f38572c1": "권한 필수", + "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}}이(가) 제거되었습니다. 대신 새 모듈 {{loopback-boot}}을(를) 사용하십시오. ", + "1d7833c3ca2f05fdad8fad7537531c40": "\t 제목:{0}", + "275f22ab95671f095640ca99194b7635": "\t 발신인:{0}", + "3d63008ccfb2af1db2142e8cc2716ace": "경고: 이메일 발송을 위해 이메일 전송이 지정되지 않았습니다. 메일 메시지를 보내려면 전송을 설정하십시오. ", + "4a4f04a4e480fc5d4ee73b84d9a4b904": "메일 발송 중:", + "63a091ced88001ab6acb58f61ec041c5": "\t 텍스트:{0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t 수신인:{0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t 전송:{0}", + "0da38687fed24275c1547e815914a8e3": "{0}에 대해 ID별 관련 항목을 삭제하십시오.", + "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "모델 인스턴스에 일치하는 기준", + "22fe62fa8d595b72c62208beddaa2a56": "{0}에 대해 ID별 관련 항목을 업데이트하십시오.", + "528325f3cbf1b0ab9a08447515daac9a": "이 모델의 {0}을(를) 업데이트하십시오.", + "543d19bad5e47ee1e9eb8af688e857b4": "{0}의 외부 키.", + "598ff0255ffd1d1b71e8de55dbe2c034": "ID별 항목에 대해 {0} 관계가 있는지 확인하십시오.", + "5a36cc6ba0cc27c754f6c5ed6015ea3c": "ID별 항목에 대한 {0} 관계를 제거하십시오.", + "5fa3afb425819ebde958043e598cb664": "{{id}} {0}인 모델을 찾을 수 없음", + "61e5deebaf44d68f4e6a508f30cc31a3": "모델 `{1}`에 대해 관계 `{0}`이(가) 없습니다. ", + "651f0b3cbba001635152ec3d3d954d0a": "{0}에 대해 ID별 관련 항목을 찾으십시오.", + "7bc7b301ad9c4fc873029d57fb9740fe": "{0} / {1} 조회.", + "7c837b88fd0e509bd3fc722d7ddf0711": "{0}의 외부 키", + "830cb6c862f8f364e9064cea0026f701": "페치에 하나의 관계 {0}이(가) 있습니다.", + "855ecd4a64885ba272d782435f72a4d4": "알 수 없는 \"{0}\" ID \"{1}\".", + "86254879d01a60826a851066987703f2": "{0}에 대해 ID별 관련 항목을 추가하십시오. ", + "8ae418c605b6a45f2651be9b1677c180": "올바르지 않은 원격 메소드: `{0}`", + "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "페치가 관계 {0}에 속합니다.", + "c0057a569ff9d3b509bac61a4b2f605d": "이 모델의 모든 {0}을(를) 삭제하십시오.", + "cd0412f2f33a4a2a316acc834f3f21a6": "{{id}} 또는 {{data}}을(를) 지정해야 함", + "d6f43b266533b04d442bdb3955622592": "이 모델의 {0}에서 새 인스턴스를 작성합니다. ", + "da13d3cdf21330557254670dddd8c5c7": "{0} / {1} 계수.", + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "알 수 없는 \"{0}\" {{id}} \"{1}\".", + "f66ae3cf379b2fce28575a3282defe1a": "이 모델의 {0}을(를) 삭제합니다.", + "03f79fa268fe199de2ce4345515431c1": "ID가 {1}인 {0}에 대한 변경 레코드를 찾을 수 없음", + "0f1c71f74b040bfbe8d384a414e31f03": "주어진 체크포인트 이후의 델타와 충돌 세트를 확보하십시오.", + "15254dec061d023d6c030083a0cef50f": "모델의 새 인스턴스를 작성하고 이를 데이터 소스로 지속시킵니다.", + "16a11368d55b85a209fc6aea69ee5f7a": "일치하는 모든 레코드를 삭제하십시오.", + "1bc1d489ddf347af47af3d9b1fc7cc15": "여러 업데이트를 한 번에 실행하십시오. 참고: 이는 Atomic 업데이트가 아닙니다. ", + "1bc7d8283c9abda512692925c8d8e3c0": "현재 체크포인트를 확보하십시오.", + "1caa7cc61266e7aef7db7d2f0e27ac3e": "이 인스턴스에 대해 보존된 최근의 변경 레코드 특성을 업데이트하십시오.", + "2a7df74fe6e8462e617b79d5fbb536ea": "이 인스턴스에 대한 최근의 변경 레코드를 확보하십시오.", + "2a9684b3d5b3b67af74bac74eb1b0843": "데이터 소스에서 필터링하여 일치하는 모든 모델 인스턴스를 찾으십시오.", + "2e50838caf0c927735eb15d12866bdd7": "주어진 체크포인트 이후에 모델에 대한 변경사항을 확보하십시오. 리턴되는 결과 수를 줄이려면 필터 오브젝트를 제공하십시오.", + "4203ab415ec66a78d3164345439ba76e": "{0}.{1}()을(를) 호출할 수 없습니다. {2} 메소드가 설정되지 않았습니다. {{PersistedModel}}이(가) {{DataSource}}에 재대로 첨부되지 않았습니다!", + "51ea9b6245bb5e672b236d640ca3b048": "변경 특성 이름/값 쌍의 오브젝트", + "55ddedd4c501348f82cb89db02ec85c1": "모델 특성 이름/값 쌍의 오브젝트", + "5aaa76c72ae1689fd3cf62589784a4ba": "모델 인스턴스의 속성을 업데이트하고 이를 데이터 소스로 지속시킵니다.", + "5f659bbc15e6e2b249fa33b3879b5f69": "데이터 소스에서 {{id}}(으)로 모델 인스턴스를 찾으십시오.", + "62e8b0a733417978bab22c8dacf5d7e6": "벌크 업데이트를 적용할 수 없습니다. 커넥터가 업데이트된 레코드 수를 제대로 보고하지 않습니다. ", + "6329e0ac1de3c250ebb1df5afd5a2a7b": "업데이트된 인스턴스 수", + "6bc376432cd9972cf991aad3de371e78": "변경을 위한 데이터 누락: {0}", + "79295ac04822d2e9702f0dd1d0240336": "데이터 소스에서 {{where}}에 일치하는 모델 인스턴스를 업데이트하십시오. ", + "7f2fde7f0f860ead224b11ba8d75aa1c": "델타 목록에서 업데이트 목록을 작성하십시오. ", + "89b57e764c2267129294b07589dbfdc2": "데이터 소스에서 {{id}}(으)로 모델 인스턴스를 삭제하십시오.", + "8bab6720ecc58ec6412358c858a53484": "벌크 업데이트에 실패했습니다. 커넥터가 예상치 못한 수의 레코드를 수정했습니다. {0}", + "a98b6cc6547706b5c6bffde0ed5fd55c": "데이터 소스에서 필터링하여 일치하는 첫 번째 모델 인스턴스를 찾으십시오.", + "c46d4aba1f14809c16730faa46933495": "정의 필드를 필터링하여 포함", + "c65600640f206f585d300b4bcb699d95": "체크포인트를 작성하십시오. ", + "cf64c7afc74d3a8120abcd028f98c770": "기존 모델 인스턴스를 업데이트하거나 새 인스턴스를 데이터 소스에 삽입하십시오.", + "dcb6261868ff0a7b928aa215b07d068c": "변경 스트림을 작성하십시오.", + "e43e320a435ec1fa07648c1da0d558a7": "모델 인스턴스가 데이터 소스에 있는지 확인하십시오.", + "e92aa25b6b864e3454b65a7c422bd114": "벌크 업데이트에 실패했습니다. 커넥터가 예상치 못한 수의 레코드를 삭제했습니다. {0}", + "ea63d226b6968e328bdf6876010786b5": "벌크 업데이트를 적용할 수 없습니다. 커넥터가 삭제된 레코드 수를 제대로 보고하지 않습니다. ", + "f1d4ac54357cc0932f385d56814ba7e4": "충돌", + "f37d94653793e33f4203e45b4a1cf106": "데이터 소스에서 where에 일치하는 모델 인스턴스 수를 세십시오.", + "0731d0109e46c21a4e34af3346ed4856": "이 동작은 다음 주요 버전에서 변경될 수 있습니다.", + "2e110abee2c95bcfc2dafd48be7e2095": "{0}을(를) 구성할 수 없음: {{config.dataSource}}이(가) {{DataSource}}의 인스턴스여야 함", + "308e1d484516a33df788f873e65faaff": "모델 `{0}`은(는) 더 이상 사용되지 않는 `DataModel`의 확장입니다. 대신 `PersistedModel`을 사용하십시오.", + "3438fab56cc7ab92dfd88f0497e523e0": "`{0}` 구성의 관계 특성은 오브젝트여야 함", + "4cac5f051ae431321673e04045d37772": "모델 `{0}`은(는) 알 수 없는 모델 `{1}`의 확장입니다. `PersistedModel`을 기본으로 사용하십시오.", + "734a7bebb65e10899935126ba63dd51f": "`{0}` 구성의 옵션 특성은 오브젝트여야 함", + "779467f467862836e19f494a37d6ab77": "`{0}` 구성의 acls 특성은 오브젝트 배열이어야 함", + "80a32e80cbed65eba2103201a7c94710": "모델을 찾을 수 없음: {0}", + "83cbdc2560ba9f09155ccfc63e08f1a1": "`{1}`에 대해 `{0}` 특성을 다시 구성할 수 없음", + "97795efe0c3eb7f35ce8cf8cfe70682b": "`{0}`의 구성에 {{`dataSource`}} 특성이 누락되었습니다.\n데이터 소스에 첨부되지 않은 모델을 표시하려면 `null` 또는 `false`를 사용하십시오.", + "a80038252430df2754884bf3c845c4cf": "\"{0}.{1}\"에 대한 원격 메타데이터에 \"isStatic\" 플래그가 누락되었습니다. 이 메소드는 인스턴스 메소드로 등록되어 있습니다.", + "dc568bee32deb0f6eaf63e73b20e8ceb": "\"{0}\"의 비오브젝트 \"methods\" 설정 무시", + "3aecb24fa8bdd3f79d168761ca8a6729": "알 수 없는 {{middleware}} 단계 {0}" +} + diff --git a/intl/nl/messages.json b/intl/nl/messages.json new file mode 100644 index 000000000..4a168cbfc --- /dev/null +++ b/intl/nl/messages.json @@ -0,0 +1,116 @@ +{ + "3b46d3a780fd6ae5f95ade489a0efffe": "Huidige context wordt niet ondersteund in de browser.", + "7e0fca41d098607e1c9aa353c67e0fa1": "Ongeldig toegangstoken", + "320c482401afa1207c04343ab162e803": "Ongeldig type principal: {0}", + "c2b5d51f007178170ca3952d59640ca4": "Wijzigingen van {0} kunnen niet worden hersteld:\n{1}", + "5858e63efaa0e4ad86b61c0459ea32fa": "U moet verbinding maken tussen het model {{Email}} en een {{Mail}}-connector", + "0caffe1d763c8cca6a61814abe33b776": "E-mail is vereist", + "1b2a6076dccbe91a56f1672eb3b8598c": "De lopende tekst van het antwoord bevat eigenschappen van het {{AccessToken}} dat is gemaakt bij aanmelding.\nAfhankelijk van de waarde van de parameter 'include' kan de lopende tekst aanvullende eigenschappen bevatten:\n\n - 'user' - 'U+007BUserU+007D' - Gegevens van de aangemelde gebruiker. {{(`include=user`)}}\n\n", + "2362ba55796c733e337af169922327a2": "Gerelateerde objecten die moeten worden opgenomen in de respons. Zie de beschrijving van de retourwaarde voor meer informatie.", + "306999d39387d87b2638199ff0bed8ad": "Wachtwoord resetten voor een gebruiker met e-mail.", + "3aae63bb7e8e046641767571c1591441": "Aanmelding mislukt omdat e-mail niet is gecontroleerd", + "3caaa84fc103d6d5612173ae6d43b245": "Ongeldig token: {0}", + "42e3fa18945255412ebc6561e2c6f1dc": "Bevestig een gebruikersregistratie met e-mailverificatietoken.", + "430b6326d7ebf6600a39f614ef516bc8": "Geef dit argument niet op, het wordt automatisch geëxtraheerd uit opdrachtheaders.", + "44a6c8b1ded4ed653d19ddeaaf89a606": "E-mail is niet gevonden", + "5e81ad3847a290dc650b47618b9cbc7e": "Aanmelden is mislukt", + "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "Meld u aan als gebruiker met gebruikersnaam/e-mailadres en wachtwoord.", + "8608c28f5e6df0008266e3c497836176": "Gebruiker afmelden met toegangstoken.", + "860d1a0b8bd340411fb32baa72867989": "Transport biedt geen ondersteuning voor HTTP-omleidingen.", + "895b1f941d026870b3cc8e6af087c197": "{{username}} of {{email}} is verplicht", + "a50d10fc6e0959b220e085454c40381e": "Gebruiker is niet gevonden: {0}", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} is verplicht", + "c34fa20eea0091747fcc9eda204b8d37": "{{accessToken}} is niet gevonden", + "c68a93f0a9524fed4ff64372fc90c55f": "U moet een geldig e-mailadres opgeven", + "f58cdc481540cd1f69a4aa4da2e37981": "Ongeldige wachtwoord: {0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "resultaat:{0}", + "10e01c895dc0b2fecc385f9f462f1ca6": "een lijst van kleuren is beschikbaar op {{http://localhost:3000/colors}}", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "Aanvraag voor host {0}", + "a40684f5a9f546115258b76938d1de37": "Een lijst van kleuren is beschikbaar op {{http://localhost:3000/colors}}", + "1e85f822b547a75d7d385048030e4ecb": "Gemaakt: {0}", + "7d5e7ed0efaedf3f55f380caae0df8b8": "Mijn eerste mobiele toepassing", + "04bd8af876f001ceaf443aad6a9002f9": "Voor verificatie moet model {0} worden gedefinieerd.", + "095afbf2f1f0e5be678f5dac5c54e717": "Toegang geweigerd", + "2d3071e3b18681c80a090dc0efbdb349": "kan {0} met ID {1} niet vinden", + "316e5b82c203cf3de31a449ee07d0650": "Booleaanse waarde verwacht, {0} ontvangen", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Gegevensbron {0}: {1} kan n iet worden gemaakt", + "7e287fc885d9fdcf42da3a12f38572c1": "Verplichte verificatie", + "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} is verwijderd; gebruik in plaats daarvan de nieuwe module {{loopback-boot}}", + "1d7833c3ca2f05fdad8fad7537531c40": "\t ONDERWERP: {0}", + "275f22ab95671f095640ca99194b7635": "\t VAN: {0}", + "3d63008ccfb2af1db2142e8cc2716ace": "Waarschuwing: Geen e-mailtransport opgegeven voor verzending van e-mail. Configureer een transport om e-mailberichten te verzenden.", + "4a4f04a4e480fc5d4ee73b84d9a4b904": "Mail verzenden:", + "63a091ced88001ab6acb58f61ec041c5": "\t TEKST: {0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML: {0}", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t AAN: {0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORT: {0}", + "0da38687fed24275c1547e815914a8e3": "Gerelateerd item wissen op basis van ID voor {0}.", + "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "Criteria om te voldoen aan modelinstances", + "22fe62fa8d595b72c62208beddaa2a56": "Gerelateerd item bijwerken op basis van ID voor {0}.", + "528325f3cbf1b0ab9a08447515daac9a": "{0} van dit model bijwerken.", + "543d19bad5e47ee1e9eb8af688e857b4": "Externe sleutel voor {0}.", + "598ff0255ffd1d1b71e8de55dbe2c034": "Bestaan van {0}-relatie met item controleren op basis van ID.", + "5a36cc6ba0cc27c754f6c5ed6015ea3c": "Verwijder de {0}-relatie met een item op basis van ID.", + "5fa3afb425819ebde958043e598cb664": "geen model gevonden met {{id}} {0}", + "61e5deebaf44d68f4e6a508f30cc31a3": "Relatie '{0}' voor model '{1}' bestaat niet", + "651f0b3cbba001635152ec3d3d954d0a": "Gerelateerd item zoeken op basis van ID voor {0}.", + "7bc7b301ad9c4fc873029d57fb9740fe": "Query's {0} van {1}.", + "7c837b88fd0e509bd3fc722d7ddf0711": "Externe sleutel voor {0}", + "830cb6c862f8f364e9064cea0026f701": "Haalt hasOne-relatie {0} op.", + "855ecd4a64885ba272d782435f72a4d4": "Onbekend \"{0}\"-ID \"{1}\".", + "86254879d01a60826a851066987703f2": "Gerelateerd item toevoegen op basis van ID voor {0}.", + "8ae418c605b6a45f2651be9b1677c180": "Ongeldige niet-lokale methode: '{0}'", + "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Haalt belongsTo-relatie {0} op.", + "c0057a569ff9d3b509bac61a4b2f605d": "Verwijdert alle {0} van dit model.", + "cd0412f2f33a4a2a316acc834f3f21a6": "U moet een {{id}} of {{data}} opgeven", + "d6f43b266533b04d442bdb3955622592": "Maakt een nieuwe instance in {0} van dit model.", + "da13d3cdf21330557254670dddd8c5c7": "Aantal {0} van {1}.", + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "Onbekend \"{0}\" {{id}} \"{1}\".", + "f66ae3cf379b2fce28575a3282defe1a": "Verwijdert {0} van dit model.", + "03f79fa268fe199de2ce4345515431c1": "Geen wijzigingsrecord gevonden voor {0} met ID {1}", + "0f1c71f74b040bfbe8d384a414e31f03": "Een set deltawaarden en conflicten ophalen sinds het opgegeven checkpoint.", + "15254dec061d023d6c030083a0cef50f": "Maak een nieuwe instance van het model en bewaar dit in de gegevensbron.", + "16a11368d55b85a209fc6aea69ee5f7a": "Alle overeenkomende records wissen.", + "1bc1d489ddf347af47af3d9b1fc7cc15": "Meerdere updates tegelijk uitvoeren. Opmerking: dit is niet atomisch (atomic).", + "1bc7d8283c9abda512692925c8d8e3c0": "Het huidige checkpoint ophalen.", + "1caa7cc61266e7aef7db7d2f0e27ac3e": "Werk de eigenschappen van het meest recente wijzigingsrecord voor deze instance bij.", + "2a7df74fe6e8462e617b79d5fbb536ea": "Het meest recente wijzigingsrecord voor deze instance ophalen.", + "2a9684b3d5b3b67af74bac74eb1b0843": "Zoeken naar alle instances van model dat overeenkomt met filter uit gegevensbron.", + "2e50838caf0c927735eb15d12866bdd7": "Wijzigingen die zijn aangebracht op een model sinds een bepaald checkpoint ophalen. Geef een filterobject op om het aantal resultaten te beperken.", + "4203ab415ec66a78d3164345439ba76e": "Kan {0}.{1}() niet aanroepen. De methode {2} is niet geconfigureerd. De {{PersistedModel}} is niet correct gekoppeld aan een {{DataSource}}!", + "51ea9b6245bb5e672b236d640ca3b048": "Een object van Combinaties van eigenschapnaam/waarde wijzigen", + "55ddedd4c501348f82cb89db02ec85c1": "Een object van combinaties van eigenschapnaam/waarde", + "5aaa76c72ae1689fd3cf62589784a4ba": "Kenmerken voor modelinstance bijwerken en bewaren in gegevensbron.", + "5f659bbc15e6e2b249fa33b3879b5f69": "Zoeken naar modelinstance op basis van {{id}} uit gegevensbron.", + "62e8b0a733417978bab22c8dacf5d7e6": "Bulkupdates kunnen niet worden toegepast, de connector meldt niet het juiste aantal bijgewerkte records.", + "6329e0ac1de3c250ebb1df5afd5a2a7b": "Het aantal bijgewerkte instances", + "6bc376432cd9972cf991aad3de371e78": "Ontbrekende gegevens voor wijziging: {0}", + "79295ac04822d2e9702f0dd1d0240336": "Instances van model dat overeenkomt met {{where}} uit gegevensbron bijwerken.", + "7f2fde7f0f860ead224b11ba8d75aa1c": "Updatelijst maken op basis van deltalijst.", + "89b57e764c2267129294b07589dbfdc2": "Modelinstance op basis van {{id}} verwijderen uit gegevensbron.", + "8bab6720ecc58ec6412358c858a53484": "Bulkupdate is mislukt; connector heeft een onverwacht aantal records gewijzigd: {0}", + "a98b6cc6547706b5c6bffde0ed5fd55c": "Zoeken naar eerste instance van model dat overeenkomt met filter uit gegevensbron.", + "c46d4aba1f14809c16730faa46933495": "Gedefinieerde velden filteren en opnemen", + "c65600640f206f585d300b4bcb699d95": "Maak een checkpoint.", + "cf64c7afc74d3a8120abcd028f98c770": "Bestaande modelinstance bijwerken of nieuwe instance toevoegen aan gegevensbron.", + "dcb6261868ff0a7b928aa215b07d068c": "Maak een wijzigingsstroom.", + "e43e320a435ec1fa07648c1da0d558a7": "Controleer of er een modelinstance voorkomt in de gegevensbron.", + "e92aa25b6b864e3454b65a7c422bd114": "Bulkupdate is mislukt; connector heeft een onverwacht aantal records gewist: {0}", + "ea63d226b6968e328bdf6876010786b5": "Bulkupdates kunnen niet worden toegepast, de connector meldt niet het juiste aantal gewiste records.", + "f1d4ac54357cc0932f385d56814ba7e4": "Conflict", + "f37d94653793e33f4203e45b4a1cf106": "Instances van model dat overeenkomt met WHERE-clausule uit gegevensbron tellen.", + "0731d0109e46c21a4e34af3346ed4856": "Dit gedrag kan gewijzigd worden in de volgende hoofdversie.", + "2e110abee2c95bcfc2dafd48be7e2095": "Kan {0} niet configureren: {{config.dataSource}} moet een instance van {{DataSource}} zijn", + "308e1d484516a33df788f873e65faaff": "Model '{0}' is een uitbreiding van het gedeprecieerde 'DataModel'. Gebruik in plaats daarvan 'PersistedModel'.", + "3438fab56cc7ab92dfd88f0497e523e0": "De relaties-eigenschap van de '{0}'-configuratie moet een object zijn", + "4cac5f051ae431321673e04045d37772": "Model '{0}' is een uitbreiding van onbekend model '{1}'. 'PersistedModel' wordt gebruikt als basis.", + "734a7bebb65e10899935126ba63dd51f": "De opties-eigenschap van de '{0}'-configuratie moet een object zijn", + "779467f467862836e19f494a37d6ab77": "De acls-eigenschap van de '{0}'-configuratie moet een array objecten zijn", + "80a32e80cbed65eba2103201a7c94710": "Model is niet gevonden: {0}", + "83cbdc2560ba9f09155ccfc63e08f1a1": "Eigenschap '{0}' mag niet opnieuw worden geconfigureerd voor '{1}'", + "97795efe0c3eb7f35ce8cf8cfe70682b": "De eigenschap {{`dataSource`}} ontbreekt in de configuratie van '{0}'.\nGebruik 'null' of 'false' om modellen te markeren die niet gekoppeld zijn aan een gegevensbron.", + "a80038252430df2754884bf3c845c4cf": "Vlag \"isStatic\" ontbreekt in remoting (externe) metagegevens voor \"{0}.{1}\"; de methode wordt geregistreerd als instancemethode.", + "dc568bee32deb0f6eaf63e73b20e8ceb": "Niet-object \"methods\"-instelling \"{0}\" wordt genegeerd.", + "3aecb24fa8bdd3f79d168761ca8a6729": "Onbekende {{middleware}}-fase {0}" +} + diff --git a/intl/pt/messages.json b/intl/pt/messages.json new file mode 100644 index 000000000..d79399005 --- /dev/null +++ b/intl/pt/messages.json @@ -0,0 +1,116 @@ +{ + "3b46d3a780fd6ae5f95ade489a0efffe": "Contexto atual não é suportado no navegador.", + "7e0fca41d098607e1c9aa353c67e0fa1": "Token de Acesso Inválido", + "320c482401afa1207c04343ab162e803": "Tipo principal inválido: {0}", + "c2b5d51f007178170ca3952d59640ca4": "Não é possível retificar mudanças de {0}:\n{1}", + "5858e63efaa0e4ad86b61c0459ea32fa": "Deve-se conectar o Modelo de {{Email}} em um conector de {{Mail}}", + "0caffe1d763c8cca6a61814abe33b776": "E-mail é necessário", + "1b2a6076dccbe91a56f1672eb3b8598c": "O corpo de resposta contém propriedades do {{AccessToken}} criado no login.\nDependendo do valor do parâmetro `include`, o corpo poderá conter propriedades adicionais:\n\n - `user` - `U+007BUserU+007D` - Dados do usuário com login efetuado atualmente. {{(`include=user`)}}\n\n", + "2362ba55796c733e337af169922327a2": "Objetos relacionados a serem incluídos na resposta. Consulte a descrição do valor de retorno para obter mais detalhes.", + "306999d39387d87b2638199ff0bed8ad": "Reconfiguração de senha para um usuário com e-mail.", + "3aae63bb7e8e046641767571c1591441": "login com falha pois o e-mail não foi verificado", + "3caaa84fc103d6d5612173ae6d43b245": "Token inválido: {0}", + "42e3fa18945255412ebc6561e2c6f1dc": "Confirme um registro do usuário com o token de verificação de e-mail.", + "430b6326d7ebf6600a39f614ef516bc8": "Não forneça este argumento, ele será extraído automaticamente dos cabeçalhos da solicitação.", + "44a6c8b1ded4ed653d19ddeaaf89a606": "E-mail não encontrado", + "5e81ad3847a290dc650b47618b9cbc7e": "falha de login", + "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "Efetue login de um usuário com nome do usuário/e-mail e senha.", + "8608c28f5e6df0008266e3c497836176": "Efetue logout de um usuário com token de acesso.", + "860d1a0b8bd340411fb32baa72867989": "O transporte não suporta redirecionamentos de HTTP.", + "895b1f941d026870b3cc8e6af087c197": "{{username}} ou {{email}} é necessário", + "a50d10fc6e0959b220e085454c40381e": "Usuário não localizado: {0}", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} é obrigatório", + "c34fa20eea0091747fcc9eda204b8d37": "não foi possível localizar {{accessToken}}", + "c68a93f0a9524fed4ff64372fc90c55f": "Deve-se fornecer um e-mail válido", + "f58cdc481540cd1f69a4aa4da2e37981": "Senha inválida: {0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "resultado:{0}", + "10e01c895dc0b2fecc385f9f462f1ca6": "uma lista de cores está disponível em {{http://localhost:3000/colors}}", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "Solicitação para o host {0}", + "a40684f5a9f546115258b76938d1de37": "Uma lista de cores está disponível em {{http://localhost:3000/colors}}", + "1e85f822b547a75d7d385048030e4ecb": "Criado: {0}", + "7d5e7ed0efaedf3f55f380caae0df8b8": "Meu primeiro aplicativo móvel", + "04bd8af876f001ceaf443aad6a9002f9": "Autenticação requer que modelo {0} seja definido.", + "095afbf2f1f0e5be678f5dac5c54e717": "Acesso Negado", + "2d3071e3b18681c80a090dc0efbdb349": "não foi possível localizar {0} com ID {1}", + "316e5b82c203cf3de31a449ee07d0650": "Booleano esperado, obteve {0}", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Não é possível criar origem de dados {0}: {1}", + "7e287fc885d9fdcf42da3a12f38572c1": "Autorização Necessária", + "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} foi removido, use o novo módulo {{loopback-boot}} no lugar", + "1d7833c3ca2f05fdad8fad7537531c40": "\t ASSUNTO:{0}", + "275f22ab95671f095640ca99194b7635": "\t DE:{0}", + "3d63008ccfb2af1db2142e8cc2716ace": "Aviso: Nenhum transporte de e-mail especificado para enviar e-mail. Configure um transporte para enviar mensagens de e-mail.", + "4a4f04a4e480fc5d4ee73b84d9a4b904": "Enviando E-mail:", + "63a091ced88001ab6acb58f61ec041c5": "\t TEXTO:{0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t PARA:{0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORTE:{0}", + "0da38687fed24275c1547e815914a8e3": "Excluir um item relacionado por ID para {0}.", + "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "Critérios para corresponder instâncias do modelo", + "22fe62fa8d595b72c62208beddaa2a56": "Atualizar um item relacionado por ID para {0}.", + "528325f3cbf1b0ab9a08447515daac9a": "Atualizar {0} deste modelo.", + "543d19bad5e47ee1e9eb8af688e857b4": "Chave estrangeira para {0}.", + "598ff0255ffd1d1b71e8de55dbe2c034": "Verifique a existência da relação de {0} com um item por ID.", + "5a36cc6ba0cc27c754f6c5ed6015ea3c": "Remova a relação de {0} com um item por ID.", + "5fa3afb425819ebde958043e598cb664": "não foi possível localizar um modelo com {{id}} {0}", + "61e5deebaf44d68f4e6a508f30cc31a3": "Relação `{0}` não existe para o modelo `{1}`", + "651f0b3cbba001635152ec3d3d954d0a": "Localize um item relacionado por ID para {0}.", + "7bc7b301ad9c4fc873029d57fb9740fe": "{0} consultas de {1}.", + "7c837b88fd0e509bd3fc722d7ddf0711": "Chave estrangeira para {0}", + "830cb6c862f8f364e9064cea0026f701": "Busca relação {0} de hasOne.", + "855ecd4a64885ba272d782435f72a4d4": "ID \"{1}\" de \"{0}\" desconhecido.", + "86254879d01a60826a851066987703f2": "Inclua um item relacionado por ID para {0}.", + "8ae418c605b6a45f2651be9b1677c180": "Método remoto inválido: `{0}`", + "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Busca relação {0} de belongsTo.", + "c0057a569ff9d3b509bac61a4b2f605d": "Exclui todos os {0} deste modelo.", + "cd0412f2f33a4a2a316acc834f3f21a6": "deve-se especificar um {{id}} ou {{data}}", + "d6f43b266533b04d442bdb3955622592": "Cria uma nova instância no {0} deste modelo.", + "da13d3cdf21330557254670dddd8c5c7": "{0} contagens de {1}.", + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" {{id}} \"{1}\" desconhecido.", + "f66ae3cf379b2fce28575a3282defe1a": "Exclui {0} deste modelo.", + "03f79fa268fe199de2ce4345515431c1": "Nenhum registro de mudança localizado para {0} com o ID {1}", + "0f1c71f74b040bfbe8d384a414e31f03": "Obtenha um conjunto de deltas e conflitos desde o ponto de verificação fornecido.", + "15254dec061d023d6c030083a0cef50f": "Crie uma nova instância do modelo e a persista na origem de dados.", + "16a11368d55b85a209fc6aea69ee5f7a": "Exclua todos os registros correspondentes.", + "1bc1d489ddf347af47af3d9b1fc7cc15": "Execute múltiplas atualizações imediatamente. Nota: isso não é atômico.", + "1bc7d8283c9abda512692925c8d8e3c0": "Obtenha o ponto de verificação atual.", + "1caa7cc61266e7aef7db7d2f0e27ac3e": "Atualize as propriedades do registro de mudança mais recente mantido para esta instância.", + "2a7df74fe6e8462e617b79d5fbb536ea": "Obtenha o registro de mudança mais recente para esta instância.", + "2a9684b3d5b3b67af74bac74eb1b0843": "Localize todas as instâncias do modelo correspondido pelo filtro a partir da origem de dados.", + "2e50838caf0c927735eb15d12866bdd7": "Obtenha as mudanças em um modelo desde um ponto de verificação fornecido. Forneça um objeto de filtro para reduzir o número de resultados retornados.", + "4203ab415ec66a78d3164345439ba76e": "Não é possível chamar {0}.{1}(). O método {2} não foi configurado. O {{PersistedModel}} não foi conectado corretamente a uma {{DataSource}}!", + "51ea9b6245bb5e672b236d640ca3b048": "Um objeto de pares nome/valor da propriedade de Mudança", + "55ddedd4c501348f82cb89db02ec85c1": "Um objeto de pares nome/valor da propriedade do modelo", + "5aaa76c72ae1689fd3cf62589784a4ba": "Atualize atributos para uma instância de modelo e a persista na origem de dados.", + "5f659bbc15e6e2b249fa33b3879b5f69": "Localize uma instância do modelo por {{id}} a partir da origem de dados.", + "62e8b0a733417978bab22c8dacf5d7e6": "Não é possível aplicar atualizações em massa, o conector não relata o número de registros de atualização corretamente.", + "6329e0ac1de3c250ebb1df5afd5a2a7b": "O número de instâncias atualizadas", + "6bc376432cd9972cf991aad3de371e78": "Dados ausentes para a mudança: {0}", + "79295ac04822d2e9702f0dd1d0240336": "Atualize instâncias do modelo correspondido por {{where}} a partir da origem de dados.", + "7f2fde7f0f860ead224b11ba8d75aa1c": "Crie uma lista de atualizações a partir de uma lista delta.", + "89b57e764c2267129294b07589dbfdc2": "Exclua uma instância do modelo por {{id}} da origem de dados.", + "8bab6720ecc58ec6412358c858a53484": "Atualização em massa falhou, o conector modificou um número inesperado de registros: {0}", + "a98b6cc6547706b5c6bffde0ed5fd55c": "Localize a primeira instância do modelo correspondido pelo filtro a partir da origem de dados.", + "c46d4aba1f14809c16730faa46933495": "Filtrar definindo campos e incluir", + "c65600640f206f585d300b4bcb699d95": "Crie um ponto de verificação.", + "cf64c7afc74d3a8120abcd028f98c770": "Atualize uma instância do modelo existente ou insira uma nova na origem de dados.", + "dcb6261868ff0a7b928aa215b07d068c": "Crie um fluxo de mudança.", + "e43e320a435ec1fa07648c1da0d558a7": "Verifique se existe uma instância do modelo na origem de dados.", + "e92aa25b6b864e3454b65a7c422bd114": "Atualização em massa falhou, o conector excluiu um número inesperado de registros: {0}", + "ea63d226b6968e328bdf6876010786b5": "Não é possível aplicar atualizações em massa, o conector não relata o número de registros excluídos corretamente.", + "f1d4ac54357cc0932f385d56814ba7e4": "Conflito", + "f37d94653793e33f4203e45b4a1cf106": "Instâncias de contagem do modelo correspondidas por where da origem de dados.", + "0731d0109e46c21a4e34af3346ed4856": "Este comportamento pode mudar na próxima versão principal.", + "2e110abee2c95bcfc2dafd48be7e2095": "Não é possível configurar {0}: {{config.dataSource}} deve ser uma instância de {{DataSource}}", + "308e1d484516a33df788f873e65faaff": "O modelo `{0}` está estendendo `DataModel` descontinuado. Use `PersistedModel` no lugar.", + "3438fab56cc7ab92dfd88f0497e523e0": "A propriedade de relações da configuração de `{0}` deve ser um objeto", + "4cac5f051ae431321673e04045d37772": "O modelo `{0}` está estendendo um modelo `{1}` desconhecido. Usando `PersistedModel` como a base.", + "734a7bebb65e10899935126ba63dd51f": "A propriedade de opções da configuração de `{0}` deve ser um objeto", + "779467f467862836e19f494a37d6ab77": "A propriedade acls da configuração de `{0}` deve ser uma matriz de objetos", + "80a32e80cbed65eba2103201a7c94710": "Modelo não localizado: {0}", + "83cbdc2560ba9f09155ccfc63e08f1a1": "A propriedade `{0}` não pode ser reconfigurada para `{1}`", + "97795efe0c3eb7f35ce8cf8cfe70682b": "A configuração de `{0}` não possui a propriedade {{`dataSource`}}.\nUse `null` ou `false` para marcar modelos não conectados a nenhuma origem de dados.", + "a80038252430df2754884bf3c845c4cf": "Metadados remotos para \"{0}.{1}\" não possui sinalização \" isStatic\", o método foi registrado como um método de instância.", + "dc568bee32deb0f6eaf63e73b20e8ceb": "Ignorando configuração de \"methods\" de não objeto de \"{0}\".", + "3aecb24fa8bdd3f79d168761ca8a6729": "Fase {0} do {{middleware}} desconhecida" +} + diff --git a/intl/tr/messages.json b/intl/tr/messages.json new file mode 100644 index 000000000..a9a94ceeb --- /dev/null +++ b/intl/tr/messages.json @@ -0,0 +1,116 @@ +{ + "3b46d3a780fd6ae5f95ade489a0efffe": "Geçerli bağlam tarayıcıda desteklenmiyor.", + "7e0fca41d098607e1c9aa353c67e0fa1": "Geçersiz Erişim Belirteci", + "320c482401afa1207c04343ab162e803": "Geçersiz birincil kullanıcı tipi: {0}", + "c2b5d51f007178170ca3952d59640ca4": "{0} değişiklik düzeltilemiyor:\n{1}", + "5858e63efaa0e4ad86b61c0459ea32fa": "{{Email}} modelini bir {{Mail}} bağlayıcısına bağlamalısınız", + "0caffe1d763c8cca6a61814abe33b776": "E-posta zorunludur", + "1b2a6076dccbe91a56f1672eb3b8598c": "Yanıt gövdesi, oturum açma sırasında yaratılan {{AccessToken}} belirtecine ilişkin özellikleri içerir.\n`include` parametresinin değerine bağlı olarak, gövde ek özellikler içerebilir:\n\n - `user` - `U+007BUserU+007D` - Oturum açmış olan kullanıcıya ilişkin veriler. {{(`include=user`)}}\n\n", + "2362ba55796c733e337af169922327a2": "Yanıtın içereceği ilgili nesneler. Daha fazla ayrıntı için, dönüş değeriyle ilgili açıklamaya bakın.", + "306999d39387d87b2638199ff0bed8ad": "Kullanıcının parolasını e-postayla sıfırlar.", + "3aae63bb7e8e046641767571c1591441": "e-posta doğrulanmadığından oturum açma başarısız oldu", + "3caaa84fc103d6d5612173ae6d43b245": "Geçersiz belirteç: {0}", + "42e3fa18945255412ebc6561e2c6f1dc": "Kullanıcı kaydını e-posta doğrulama belirteciyle doğrular.", + "430b6326d7ebf6600a39f614ef516bc8": "Bu bağımsız değişkeni belirtmeyin; istek üstbilgilerinden otomatik olarak alınır.", + "44a6c8b1ded4ed653d19ddeaaf89a606": "E-posta bulunamadı", + "5e81ad3847a290dc650b47618b9cbc7e": "oturum açma başarısız oldu", + "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "Kullanıcı için kullanıcı adı/e-posta ve parola ile oturum açar.", + "8608c28f5e6df0008266e3c497836176": "Kullanıcı için erişim belirteciyle oturum kapatır.", + "860d1a0b8bd340411fb32baa72867989": "Aktarım HTTP yeniden yönlendirmelerini desteklemiyor.", + "895b1f941d026870b3cc8e6af087c197": "{{username}} ya da {{email}} zorunludur", + "a50d10fc6e0959b220e085454c40381e": "Kullanıcı bulunamadı: {0}", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} zorunludur", + "c34fa20eea0091747fcc9eda204b8d37": "{{accessToken}} bulunamadı", + "c68a93f0a9524fed4ff64372fc90c55f": "Geçerli bir e-posta belirtilmeli", + "f58cdc481540cd1f69a4aa4da2e37981": "Geçersiz parola: {0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "sonuç:{0}", + "10e01c895dc0b2fecc385f9f462f1ca6": "renklerin listesine şu adresle erişebilirsiniz: {{http://localhost:3000/colors}}", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "{0} ana makinesine yönelik istek", + "a40684f5a9f546115258b76938d1de37": "Renklerin listesine şu adresle erişebilirsiniz: {{http://localhost:3000/colors}}", + "1e85f822b547a75d7d385048030e4ecb": "Yaratıldığı tarih: {0}", + "7d5e7ed0efaedf3f55f380caae0df8b8": "İlk mobil uygulamam", + "04bd8af876f001ceaf443aad6a9002f9": "Kimlik doğrulaması {0} modelinin tanımlanmasını gerektiriyor.", + "095afbf2f1f0e5be678f5dac5c54e717": "Erişim Verilmedi", + "2d3071e3b18681c80a090dc0efbdb349": "{1} tanıtıcılı {0} bulunamadı", + "316e5b82c203cf3de31a449ee07d0650": "Boole beklenirken {0} alındı", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Veri kaynağı {0} yaratılamıyor: {1}", + "7e287fc885d9fdcf42da3a12f38572c1": "Yetkilendirme Gerekli", + "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} kaldırıldı, onun yerine yeni {{loopback-boot}} modülünü kullanın", + "1d7833c3ca2f05fdad8fad7537531c40": "\t KONU:{0}", + "275f22ab95671f095640ca99194b7635": "\t KİMDEN:{0}", + "3d63008ccfb2af1db2142e8cc2716ace": "Uyarı: E-posta göndermek için e-posta aktarımı belirtilmedi. Posta iletileri göndermek için aktarım ayarlayın.", + "4a4f04a4e480fc5d4ee73b84d9a4b904": "Posta Gönderiliyor:", + "63a091ced88001ab6acb58f61ec041c5": "\t METİN:{0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t KİME:{0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t AKTARIM:{0}", + "0da38687fed24275c1547e815914a8e3": "{0} için ilgili bir öğeyi tanıtıcı temelinde siler.", + "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "Model eşgörünümlerini eşleştirme ölçütleri", + "22fe62fa8d595b72c62208beddaa2a56": "{0} için ilgili bir öğeyi tanıtıcı temelinde günceller.", + "528325f3cbf1b0ab9a08447515daac9a": "Bu modele ilişkin {0} güncellemesi.", + "543d19bad5e47ee1e9eb8af688e857b4": "{0} için dış anahtar.", + "598ff0255ffd1d1b71e8de55dbe2c034": "Bir öğeye yönelik {0} ilişkisinin var olup olmadığını tanıtıcı temelinde denetler.", + "5a36cc6ba0cc27c754f6c5ed6015ea3c": "Bir öğeye yönelik {0} ilişkisini kaldırır.", + "5fa3afb425819ebde958043e598cb664": "{{id}} {0} tanıtıcılı bir model bulunamadı", + "61e5deebaf44d68f4e6a508f30cc31a3": "`{1}` modeli için `{0}` ilişkisi yok", + "651f0b3cbba001635152ec3d3d954d0a": "{0} için ilgili bir öğeyi tanıtıcı temelinde bulur.", + "7bc7b301ad9c4fc873029d57fb9740fe": "{1} ile ilişkili {0} öğesini sorgular.", + "7c837b88fd0e509bd3fc722d7ddf0711": "{0} için dış anahtar", + "830cb6c862f8f364e9064cea0026f701": "{0} hasOne ilişkisini alır.", + "855ecd4a64885ba272d782435f72a4d4": "Bilinmeyen \"{0}\" tanıtıcısı \"{1}\".", + "86254879d01a60826a851066987703f2": "{0} için ilgili bir öğeyi tanıtıcı temelinde ekler.", + "8ae418c605b6a45f2651be9b1677c180": "Uzak yöntem geçersiz: `{0}`", + "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "{0} belongsTo ilişkisini alır.", + "c0057a569ff9d3b509bac61a4b2f605d": "Bu modele ilişkin tüm {0} öğelerini siler.", + "cd0412f2f33a4a2a316acc834f3f21a6": "bir {{id}} ya da {{data}} belirtmelidir", + "d6f43b266533b04d442bdb3955622592": "Bu modele ilişkin {0} içinde yeni eşgörünüm yaratır.", + "da13d3cdf21330557254670dddd8c5c7": "{1} ile ilişkili {0} öğesini sayar.", + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "Bilinmeyen \"{0}\" {{id}} \"{1}\".", + "f66ae3cf379b2fce28575a3282defe1a": "Bu modele ilişkin {0} öğesini siler.", + "03f79fa268fe199de2ce4345515431c1": "{0} için {1} tanıtıcılı bir değişiklik kaydı bulunamadı", + "0f1c71f74b040bfbe8d384a414e31f03": "Söz konusu denetim noktasından bu yana oluşan bir fark ve çakışma kümesini alır.", + "15254dec061d023d6c030083a0cef50f": "Modelin yeni bir eşgörünümünü yaratır ve veri kaynağında kalıcı olarak saklar.", + "16a11368d55b85a209fc6aea69ee5f7a": "Eşleşen tüm kayıtları siler.", + "1bc1d489ddf347af47af3d9b1fc7cc15": "Bir kerede birden çok güncelleme çalıştırır. Not: Atomik değildir.", + "1bc7d8283c9abda512692925c8d8e3c0": "Geçerli denetim noktasını alır.", + "1caa7cc61266e7aef7db7d2f0e27ac3e": "Bu eşgörünüm için alıkonan en son değişiklik kaydının özelliklerini günceller.", + "2a7df74fe6e8462e617b79d5fbb536ea": "Bu eşgörünüme ilişkin en son değişiklik kaydını alır.", + "2a9684b3d5b3b67af74bac74eb1b0843": "Veri kaynağında modelin filtreyle eşleşen tüm eşgörünümlerini bulur.", + "2e50838caf0c927735eb15d12866bdd7": "Belirli bir denetim noktasından bu yana bir modelde yapılan değişiklikleri alır. Döndürülecek sonuç sayısını azaltmak için bir filtre nesnesi belirtin.", + "4203ab415ec66a78d3164345439ba76e": "Çağrılamıyor: {0}.{1}(). {2} yöntemi ayarlanmamış. {{PersistedModel}}, bir veri kaynağına ({{DataSource}}) doğru olarak eklenmedi!", + "51ea9b6245bb5e672b236d640ca3b048": "Değişiklik özellik adı ve değeri çiftlerine ilişkin bir nesne", + "55ddedd4c501348f82cb89db02ec85c1": "Model özellik adı ve değeri çiftlerine ilişkin bir nesne", + "5aaa76c72ae1689fd3cf62589784a4ba": "Bir model eşgörünümüne ilişkin öznitelikleri günceller ve veri kaynağında kalıcı olarak saklar.", + "5f659bbc15e6e2b249fa33b3879b5f69": "Veri kaynağında {{id}} temelinde model eşgörünümü bulur.", + "62e8b0a733417978bab22c8dacf5d7e6": "Toplu güncelleme uygulanamaz; bağlayıcı, güncellenen kayıtların sayısını doğru olarak bildirmiyor.", + "6329e0ac1de3c250ebb1df5afd5a2a7b": "Güncellenen eşgörünümlerin sayısı", + "6bc376432cd9972cf991aad3de371e78": "Değişiklik için veri eksik: {0}", + "79295ac04822d2e9702f0dd1d0240336": "Veri kaynağında modelin {{where}} ile eşleştirilen eşgörünümlerini günceller.", + "7f2fde7f0f860ead224b11ba8d75aa1c": "Fark listesinden güncelleme listesi yaratır.", + "89b57e764c2267129294b07589dbfdc2": "Bir model eşgörünümünü {{id}} temelinde veri kaynağından siler.", + "8bab6720ecc58ec6412358c858a53484": "Toplu güncelleme başarısız oldu, bağlayıcı beklenmeyen sayıda kaydı değiştirdi: {0}", + "a98b6cc6547706b5c6bffde0ed5fd55c": "Veri kaynağında modelin filtreyle eşleşen ilk eşgörünümünü bulur.", + "c46d4aba1f14809c16730faa46933495": "Filtre tanımlayan alanlar ve include", + "c65600640f206f585d300b4bcb699d95": "Denetim noktası yaratır.", + "cf64c7afc74d3a8120abcd028f98c770": "Var olan bir model eşgörünümünü günceller ya da veri kaynağına yeni model eşgörünümü ekler.", + "dcb6261868ff0a7b928aa215b07d068c": "Değişiklik akışı yaratır.", + "e43e320a435ec1fa07648c1da0d558a7": "Bir model eşgörünümünün veri kaynağında var olup olmadığını denetler.", + "e92aa25b6b864e3454b65a7c422bd114": "Toplu güncelleme başarısız oldu, bağlayıcı beklenmeyen sayıda kaydı sildi: {0}", + "ea63d226b6968e328bdf6876010786b5": "Toplu güncelleme uygulanamaz; bağlayıcı, silinen kayıtların sayısını doğru olarak bildirmiyor.", + "f1d4ac54357cc0932f385d56814ba7e4": "Çakışma", + "f37d94653793e33f4203e45b4a1cf106": "Veri kaynağında, modelin where ile eşleşen eşgörünümlerini sayar.", + "0731d0109e46c21a4e34af3346ed4856": "Bu davranış sonraki ana sürümde değişebilir.", + "2e110abee2c95bcfc2dafd48be7e2095": "{0} yapılandırılamıyor: {{config.dataSource}}, bir {{DataSource}} eşgörünümü olmalıdır", + "308e1d484516a33df788f873e65faaff": "`{0}` modeli kullanımdan kaldırılmış şu modeli genişletiyor: `DataModel. Onun yerine `PersistedModel` modelini kullanın.", + "3438fab56cc7ab92dfd88f0497e523e0": "`{0}` yapılandırmasının ilişkiler (relations) özelliği bir nesne olmalıdır", + "4cac5f051ae431321673e04045d37772": "`{0}` modeli, bilinmeyen `{1}` modelini genişletiyor. Temel olarak 'PersistedModel' kullanılıyor.", + "734a7bebb65e10899935126ba63dd51f": "`{0}` yapılandırmasının seçenekler (options) özelliği bir nesne olmalıdır.", + "779467f467862836e19f494a37d6ab77": "`{0}` yapılandırmasının erişim denetim listeleri (acls) özelliği bir nesne dizisi olmalıdır.", + "80a32e80cbed65eba2103201a7c94710": "Model bulunamadı: {0}", + "83cbdc2560ba9f09155ccfc63e08f1a1": "`{0}` özelliği `{1}` için yeniden yapılandırılamıyor", + "97795efe0c3eb7f35ce8cf8cfe70682b": "`{0}` yapılandırmasında {{`dataSource`}} özelliği eksik.\nHiçbir veri kaynağına eklenmemiş modelleri işaretlemek için `null` ya da `false` kullanın.", + "a80038252430df2754884bf3c845c4cf": "\"{0}.{1}\" ile ilgili uzaktan iletişim meta verisinde \"isStatic\" işareti eksik; yöntem bir eşgörünüm yöntemi olarak kaydedildi.", + "dc568bee32deb0f6eaf63e73b20e8ceb": "\"{0}\" öğesinin nesne olmayan \"methods\" atarı yoksayılıyor.", + "3aecb24fa8bdd3f79d168761ca8a6729": "Bilinmeyen {{middleware}} aşaması {0}" +} + diff --git a/intl/zh-Hans/messages.json b/intl/zh-Hans/messages.json new file mode 100644 index 000000000..4e573bdae --- /dev/null +++ b/intl/zh-Hans/messages.json @@ -0,0 +1,116 @@ +{ + "3b46d3a780fd6ae5f95ade489a0efffe": "浏览器中不支持当前上下文。", + "7e0fca41d098607e1c9aa353c67e0fa1": "无效的访问令牌", + "320c482401afa1207c04343ab162e803": "无效的主体类型:{0}", + "c2b5d51f007178170ca3952d59640ca4": "无法纠正 {0} 更改:\n{1}", + "5858e63efaa0e4ad86b61c0459ea32fa": "您必须将 {{Email}} 模型连接到 {{Mail}} 连接器", + "0caffe1d763c8cca6a61814abe33b776": "电子邮件是必需的", + "1b2a6076dccbe91a56f1672eb3b8598c": "响应主体包含在登录时创建的 {{AccessToken}} 的属性。\n根据“include”参数的值,主体可包含其他属性:\n\n - `user` - `U+007BUserU+007D` - 当前已登录用户的数据。 {{(`include=user`)}}\n\n", + "2362ba55796c733e337af169922327a2": "要包含在响应中的相关对象。请参阅返回值的描述以获取更多详细信息。", + "306999d39387d87b2638199ff0bed8ad": "使用电子邮件重置用户的密码。", + "3aae63bb7e8e046641767571c1591441": "因为尚未验证电子邮件,登录失败", + "3caaa84fc103d6d5612173ae6d43b245": "无效的令牌:{0}", + "42e3fa18945255412ebc6561e2c6f1dc": "使用电子邮件验证令牌,确认用户注册。", + "430b6326d7ebf6600a39f614ef516bc8": "请勿提供此自变量,其自动从请求头中抽取。", + "44a6c8b1ded4ed653d19ddeaaf89a606": "找不到电子邮件", + "5e81ad3847a290dc650b47618b9cbc7e": "登录失败", + "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "使用用户名/电子邮件和密码登录用户。", + "8608c28f5e6df0008266e3c497836176": "使用访问令牌注销用户。", + "860d1a0b8bd340411fb32baa72867989": "传输不支持 HTTP 重定向。", + "895b1f941d026870b3cc8e6af087c197": "{{username}} 或 {{email}} 是必需的", + "a50d10fc6e0959b220e085454c40381e": "找不到用户:{0}", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} 是必需的", + "c34fa20eea0091747fcc9eda204b8d37": "找不到 {{accessToken}}", + "c68a93f0a9524fed4ff64372fc90c55f": "必须提供有效电子邮件", + "f58cdc481540cd1f69a4aa4da2e37981": "无效的密码:{0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "结果:{0}", + "10e01c895dc0b2fecc385f9f462f1ca6": "颜色列表位于:{{http://localhost:3000/colors}}", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "到主机 {0} 的请求", + "a40684f5a9f546115258b76938d1de37": "颜色列表位于:{{http://localhost:3000/colors}}", + "1e85f822b547a75d7d385048030e4ecb": "创建时间:{0}", + "7d5e7ed0efaedf3f55f380caae0df8b8": "我的第一个移动应用程序", + "04bd8af876f001ceaf443aad6a9002f9": "认证需要定义模型 {0}。", + "095afbf2f1f0e5be678f5dac5c54e717": "拒绝访问", + "2d3071e3b18681c80a090dc0efbdb349": "无法找到标识为 {1} 的 {0}", + "316e5b82c203cf3de31a449ee07d0650": "期望布尔值,获取 {0}", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "无法创建数据源 {0}:{1}", + "7e287fc885d9fdcf42da3a12f38572c1": "需要授权", + "d5552322de5605c58b62f47ad26d2716": "已除去 {{`app.boot`}},请改用新模块 {{loopback-boot}}", + "1d7833c3ca2f05fdad8fad7537531c40": "\t主题:{0}", + "275f22ab95671f095640ca99194b7635": "\t发件人:{0}", + "3d63008ccfb2af1db2142e8cc2716ace": "警告:未指定用于发送电子邮件的电子邮件传输。设置传输以发送电子邮件消息。", + "4a4f04a4e480fc5d4ee73b84d9a4b904": "正在发送电子邮件:", + "63a091ced88001ab6acb58f61ec041c5": "\t 文本:{0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t 收件人:{0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t 传输:{0}", + "0da38687fed24275c1547e815914a8e3": "按标识删除 {0} 的相关项。", + "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "用于匹配模型实例的条件", + "22fe62fa8d595b72c62208beddaa2a56": "按标识更新 {0} 的相关项。", + "528325f3cbf1b0ab9a08447515daac9a": "更新此模型的 {0}。", + "543d19bad5e47ee1e9eb8af688e857b4": "{0} 的外键。", + "598ff0255ffd1d1b71e8de55dbe2c034": "按标识检查项的 {0} 关系是否存在。", + "5a36cc6ba0cc27c754f6c5ed6015ea3c": "按标识除去项的 {0} 关系。", + "5fa3afb425819ebde958043e598cb664": "找不到具有 {{id}} {0} 的模型", + "61e5deebaf44d68f4e6a508f30cc31a3": "对于模型“{1}”,关系“{0}”不存在", + "651f0b3cbba001635152ec3d3d954d0a": "按标识查找 {0} 的相关项。", + "7bc7b301ad9c4fc873029d57fb9740fe": "查询 {1} 的 {0}。", + "7c837b88fd0e509bd3fc722d7ddf0711": "{0} 的外键", + "830cb6c862f8f364e9064cea0026f701": "访存 hasOne 关系 {0}。", + "855ecd4a64885ba272d782435f72a4d4": "未知的“{0}”标识“{1}”。", + "86254879d01a60826a851066987703f2": "按标识添加 {0} 的相关项。", + "8ae418c605b6a45f2651be9b1677c180": "无效的远程方法:“{0}”", + "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "访存 belongsTo 关系 {0}。", + "c0057a569ff9d3b509bac61a4b2f605d": "删除此模型的所有 {0}。", + "cd0412f2f33a4a2a316acc834f3f21a6": "必须指定 {{id}} 或 {{data}}", + "d6f43b266533b04d442bdb3955622592": "在此模型的 {0} 中创建新实例。", + "da13d3cdf21330557254670dddd8c5c7": "计算 {0} 的数量({1})。", + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "未知的“{0}”{{id}}“{1}”。", + "f66ae3cf379b2fce28575a3282defe1a": "删除此模型的 {0}。", + "03f79fa268fe199de2ce4345515431c1": "对于标识为 {1} 的 {0},找不到任何更改记录", + "0f1c71f74b040bfbe8d384a414e31f03": "获取自指定的检查点以来的一组增量和冲突。", + "15254dec061d023d6c030083a0cef50f": "创建模型的新实例并将其持久存储到数据源。", + "16a11368d55b85a209fc6aea69ee5f7a": "删除所有匹配记录。", + "1bc1d489ddf347af47af3d9b1fc7cc15": "立刻运行多个更新。注:这不是原子更新。", + "1bc7d8283c9abda512692925c8d8e3c0": "获取当前检查点。", + "1caa7cc61266e7aef7db7d2f0e27ac3e": "更新针对此实例保留的最新更改记录的属性。", + "2a7df74fe6e8462e617b79d5fbb536ea": "获取此实例的最新更改记录。", + "2a9684b3d5b3b67af74bac74eb1b0843": "从数据源中查找按过滤器匹配的模型的所有实例。", + "2e50838caf0c927735eb15d12866bdd7": "获取自指定的检查点以来的模型更改。提供过滤器对象以减少返回的结果数量。", + "4203ab415ec66a78d3164345439ba76e": "无法调用 {0}.{1}()。尚未设置 {2} 方法。{{PersistedModel}} 未正确附加到 {{DataSource}}!", + "51ea9b6245bb5e672b236d640ca3b048": "更改属性名称/值对的对象", + "55ddedd4c501348f82cb89db02ec85c1": "模型属性名称/值对的对象", + "5aaa76c72ae1689fd3cf62589784a4ba": "更新模型实例的属性并将其持久存储到数据源。", + "5f659bbc15e6e2b249fa33b3879b5f69": "从数据源按 {{id}} 查找模型实例。", + "62e8b0a733417978bab22c8dacf5d7e6": "无法应用批量更新,连接器未正确报告更新的记录数。", + "6329e0ac1de3c250ebb1df5afd5a2a7b": "已更新实例数量", + "6bc376432cd9972cf991aad3de371e78": "缺少更改的数据:{0}", + "79295ac04822d2e9702f0dd1d0240336": "从数据源更新按 {{where}} 匹配的模型的实例。", + "7f2fde7f0f860ead224b11ba8d75aa1c": "根据增量列表创建更新列表。", + "89b57e764c2267129294b07589dbfdc2": "从数据源按 {{id}} 删除模型实例。", + "8bab6720ecc58ec6412358c858a53484": "批量更新失败,连接器已修改意外数量的记录:{0}", + "a98b6cc6547706b5c6bffde0ed5fd55c": "从数据源中查找按过滤器匹配的模型的第一个实例。", + "c46d4aba1f14809c16730faa46933495": "过滤定义字段并包含", + "c65600640f206f585d300b4bcb699d95": "创建检查点。", + "cf64c7afc74d3a8120abcd028f98c770": "更新现有模型实例或将新实例插入到数据源。", + "dcb6261868ff0a7b928aa215b07d068c": "创建更改流。", + "e43e320a435ec1fa07648c1da0d558a7": "检查数据源中是否存在模型实例。", + "e92aa25b6b864e3454b65a7c422bd114": "批量更新失败,连接器已删除意外数量的记录:{0}", + "ea63d226b6968e328bdf6876010786b5": "无法应用批量更新,连接器未正确报告删除的记录数。", + "f1d4ac54357cc0932f385d56814ba7e4": "冲突", + "f37d94653793e33f4203e45b4a1cf106": "从数据源中计算按 where 匹配的模型的实例数量。", + "0731d0109e46c21a4e34af3346ed4856": "在下一个主版本中,此行为可能进行更改。", + "2e110abee2c95bcfc2dafd48be7e2095": "无法配置 {0}:{{config.dataSource}} 必须是 {{DataSource}} 的实例", + "308e1d484516a33df788f873e65faaff": "模型“{0}”正在扩展不推荐使用的“DataModel”。请改用“PersistedModel”。", + "3438fab56cc7ab92dfd88f0497e523e0": "“{0}”配置的关系属性必须是对象。", + "4cac5f051ae431321673e04045d37772": "模型“{0}”正在扩展未知的模型“{1}”。使用“PersistedModel”作为基础。", + "734a7bebb65e10899935126ba63dd51f": "“{0}”配置的选项属性必须是对象。", + "779467f467862836e19f494a37d6ab77": "“{0}”配置的 acls 属性必须是对象数组。", + "80a32e80cbed65eba2103201a7c94710": "找不到模型:{0}", + "83cbdc2560ba9f09155ccfc63e08f1a1": "无法针对“{1}”重新配置属性“{0}”。", + "97795efe0c3eb7f35ce8cf8cfe70682b": "“{0}”的配置缺少 {{`dataSource`}} 属性。\n使用“null”或“false”来标记未附加到任何数据源的模型。", + "a80038252430df2754884bf3c845c4cf": "“{0}.{1}”的远程处理元数据缺少“isStatic”标志,方法注册为实例方法。", + "dc568bee32deb0f6eaf63e73b20e8ceb": "忽略“{0}”的非对象“方法”设置。", + "3aecb24fa8bdd3f79d168761ca8a6729": "未知的 {{middleware}} 阶段 {0}" +} + diff --git a/intl/zh-Hant/messages.json b/intl/zh-Hant/messages.json new file mode 100644 index 000000000..ac96fa222 --- /dev/null +++ b/intl/zh-Hant/messages.json @@ -0,0 +1,116 @@ +{ + "3b46d3a780fd6ae5f95ade489a0efffe": "瀏覽器不支援現行環境定義。", + "7e0fca41d098607e1c9aa353c67e0fa1": "存取記號無效", + "320c482401afa1207c04343ab162e803": "無效的主體類型:{0}", + "c2b5d51f007178170ca3952d59640ca4": "無法更正 {0} 個變更:\n{1}", + "5858e63efaa0e4ad86b61c0459ea32fa": "您必須將 {{Email}} 模型連接至 {{Mail}} 連接器", + "0caffe1d763c8cca6a61814abe33b776": "需要電子郵件", + "1b2a6076dccbe91a56f1672eb3b8598c": "回應內文包含登入時建立的 {{AccessToken}} 的內容。\n根據 `include` 參數的值而定,內文還可能包含其他內容:\n\n - `user` - `U+007BUserU+007D` - 目前登入的使用者的資料。 {{(`include=user`)}}\n\n", + "2362ba55796c733e337af169922327a2": "要包含在回應中的相關物件。如需詳細資料,請參閱回覆值的說明。", + "306999d39387d87b2638199ff0bed8ad": "透過電子郵件來重設使用者的密碼。", + "3aae63bb7e8e046641767571c1591441": "因為尚未驗證電子郵件,所以登入失敗", + "3caaa84fc103d6d5612173ae6d43b245": "無效記號:{0}", + "42e3fa18945255412ebc6561e2c6f1dc": "透過電子郵件驗證記號來確認使用者登錄。", + "430b6326d7ebf6600a39f614ef516bc8": "請勿提供這個引數,因為會自動從要求標頭中擷取它。", + "44a6c8b1ded4ed653d19ddeaaf89a606": "找不到電子郵件", + "5e81ad3847a290dc650b47618b9cbc7e": "登入失敗", + "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "透過使用者名稱/電子郵件和密碼將使用者登入。", + "8608c28f5e6df0008266e3c497836176": "透過存取記號將使用者登出。", + "860d1a0b8bd340411fb32baa72867989": "傳輸不支援 HTTP 重新導向。", + "895b1f941d026870b3cc8e6af087c197": "需要 {{username}} 或 {{email}}", + "a50d10fc6e0959b220e085454c40381e": "找不到使用者:{0}", + "ba96498b10c179f9cd75f75c8def4f70": "需要 {{realm}}", + "c34fa20eea0091747fcc9eda204b8d37": "找不到 {{accessToken}}", + "c68a93f0a9524fed4ff64372fc90c55f": "必須提供有效的電子郵件", + "f58cdc481540cd1f69a4aa4da2e37981": "無效密碼:{0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "結果:{0}", + "10e01c895dc0b2fecc385f9f462f1ca6": "{{http://localhost:3000/colors}} 提供顏色清單", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "向主機 {0} 要求", + "a40684f5a9f546115258b76938d1de37": "{{http://localhost:3000/colors}} 提供顏色清單", + "1e85f822b547a75d7d385048030e4ecb": "已建立:{0}", + "7d5e7ed0efaedf3f55f380caae0df8b8": "我的第一個行動式應用程式", + "04bd8af876f001ceaf443aad6a9002f9": "需要定義模型 {0} 才能鑑別。", + "095afbf2f1f0e5be678f5dac5c54e717": "拒絕存取", + "2d3071e3b18681c80a090dc0efbdb349": "找不到 id 為 {1} 的 {0}", + "316e5b82c203cf3de31a449ee07d0650": "預期為布林,但卻取得 {0}", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "無法建立資料來源 {0}:{1}", + "7e287fc885d9fdcf42da3a12f38572c1": "需要授權", + "d5552322de5605c58b62f47ad26d2716": "已移除 {{`app.boot`}},請改用新的模組 {{loopback-boot}}", + "1d7833c3ca2f05fdad8fad7537531c40": "\t 主旨:{0}", + "275f22ab95671f095640ca99194b7635": "\t 寄件者:{0}", + "3d63008ccfb2af1db2142e8cc2716ace": "警告:未指定用於傳送電子郵件的電子郵件傳輸。請設定傳輸來傳送郵件訊息。", + "4a4f04a4e480fc5d4ee73b84d9a4b904": "正在傳送郵件:", + "63a091ced88001ab6acb58f61ec041c5": "\t 文字:{0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t 收件者:{0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t 傳輸:{0}", + "0da38687fed24275c1547e815914a8e3": "依 id 刪除 {0} 的相關項目。", + "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "用於比對模型實例的準則", + "22fe62fa8d595b72c62208beddaa2a56": "依 id 更新 {0} 的相關項目。", + "528325f3cbf1b0ab9a08447515daac9a": "更新這個模型的 {0}。", + "543d19bad5e47ee1e9eb8af688e857b4": "{0} 的外部索引鍵。", + "598ff0255ffd1d1b71e8de55dbe2c034": "依 id 檢查項目的 {0} 關係是否存在。", + "5a36cc6ba0cc27c754f6c5ed6015ea3c": "依 id 移除項目的 {0} 關係。", + "5fa3afb425819ebde958043e598cb664": "找不到 {{id}} 為 {0} 的模型", + "61e5deebaf44d68f4e6a508f30cc31a3": "模型 `{1}` 的關係 `{0}` 不存在", + "651f0b3cbba001635152ec3d3d954d0a": "依 id 尋找 {0} 的相關項目。", + "7bc7b301ad9c4fc873029d57fb9740fe": "查詢 {0} 個(共 {1} 個)。", + "7c837b88fd0e509bd3fc722d7ddf0711": "{0} 的外部索引鍵", + "830cb6c862f8f364e9064cea0026f701": "提取 hasOne 關係 {0}。", + "855ecd4a64885ba272d782435f72a4d4": "\"{0}\" ID \"{1}\" 不明。", + "86254879d01a60826a851066987703f2": "依 id 新增 {0} 的相關項目。", + "8ae418c605b6a45f2651be9b1677c180": "無效的遠端方法:`{0}`", + "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "提取 belongsTo 關係 {0}。", + "c0057a569ff9d3b509bac61a4b2f605d": "刪除這個模型的所有 {0}。", + "cd0412f2f33a4a2a316acc834f3f21a6": "必須指定 {{id}} 或 {{data}}", + "d6f43b266533b04d442bdb3955622592": "在這個模型的 {0} 中建立新實例。", + "da13d3cdf21330557254670dddd8c5c7": "計算 {0} 個(共 {1} 個)。", + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" {{id}} \"{1}\" 不明。", + "f66ae3cf379b2fce28575a3282defe1a": "刪除這個模型的 {0}。", + "03f79fa268fe199de2ce4345515431c1": "對於 id 為 {1} 的 {0},找不到變更記錄", + "0f1c71f74b040bfbe8d384a414e31f03": "取得自給定的檢查點以來的一連串差異和衝突。", + "15254dec061d023d6c030083a0cef50f": "建立模型的新實例並保存到資料來源。", + "16a11368d55b85a209fc6aea69ee5f7a": "刪除所有相符記錄。", + "1bc1d489ddf347af47af3d9b1fc7cc15": "一次執行多個更新。附註:這可以分開進行。", + "1bc7d8283c9abda512692925c8d8e3c0": "取得現行檢查點。", + "1caa7cc61266e7aef7db7d2f0e27ac3e": "更新這個實例所保留的最新變更記錄的內容。", + "2a7df74fe6e8462e617b79d5fbb536ea": "取得這個實例的最新變更記錄。", + "2a9684b3d5b3b67af74bac74eb1b0843": "從資料來源,依 filter 尋找所有相符的模型實例。", + "2e50838caf0c927735eb15d12866bdd7": "取得自給定的檢查點以來的模型變更。請提供過濾器物件,以減少傳回的結果數。", + "4203ab415ec66a78d3164345439ba76e": "無法呼叫 {0}.{1}()。尚未設定 {2} 方法。{{PersistedModel}} 未正確連接至 {{DataSource}}!", + "51ea9b6245bb5e672b236d640ca3b048": "Change 內容名稱/值配對的物件", + "55ddedd4c501348f82cb89db02ec85c1": "model 內容名稱/值配對的物件", + "5aaa76c72ae1689fd3cf62589784a4ba": "更新模型實例的屬性並保存到資料來源。", + "5f659bbc15e6e2b249fa33b3879b5f69": "從資料來源,依 {{id}} 尋找模型實例。", + "62e8b0a733417978bab22c8dacf5d7e6": "無法套用大量更新,連接器未正確報告已更新的記錄數。", + "6329e0ac1de3c250ebb1df5afd5a2a7b": "已更新的實例數", + "6bc376432cd9972cf991aad3de371e78": "遺漏變更的資料:{0}", + "79295ac04822d2e9702f0dd1d0240336": "從資料來源,依 {{where}} 更新相符的模型實例。", + "7f2fde7f0f860ead224b11ba8d75aa1c": "從差異清單建立更新清單。", + "89b57e764c2267129294b07589dbfdc2": "從資料來源,依 {{id}} 刪除模型實例。", + "8bab6720ecc58ec6412358c858a53484": "大量更新失敗,連接器已修改超乎預期的記錄數:{0}", + "a98b6cc6547706b5c6bffde0ed5fd55c": "從資料來源,依 filter 尋找第一個相符的模型實例。", + "c46d4aba1f14809c16730faa46933495": "定義 fields 和 include 的過濾器", + "c65600640f206f585d300b4bcb699d95": "建立檢查點。", + "cf64c7afc74d3a8120abcd028f98c770": "更新現有的模型實例,或將新的模型實例插入資料來源中。", + "dcb6261868ff0a7b928aa215b07d068c": "建立變更串流。", + "e43e320a435ec1fa07648c1da0d558a7": "檢查資料來源中是否存在模型實例。", + "e92aa25b6b864e3454b65a7c422bd114": "大量更新失敗,連接器已刪除非預期的記錄數:{0}", + "ea63d226b6968e328bdf6876010786b5": "無法套用大量更新,連接器未正確報告已刪除的記錄數。", + "f1d4ac54357cc0932f385d56814ba7e4": "衝突", + "f37d94653793e33f4203e45b4a1cf106": "從資料來源,依 where 計算相符的模型實例數。", + "0731d0109e46c21a4e34af3346ed4856": "下一個主要版本中可能會變更這個行為。", + "2e110abee2c95bcfc2dafd48be7e2095": "無法配置 {0}:{{config.dataSource}} 必須是 {{DataSource}} 的實例", + "308e1d484516a33df788f873e65faaff": "模型 `{0}` 正在延伸已淘汰的 `DataModel。請改用 `PersistedModel`。", + "3438fab56cc7ab92dfd88f0497e523e0": "`{0}` 配置的 relations 內容必須是物件", + "4cac5f051ae431321673e04045d37772": "模型 `{0}` 正在延伸不明模型 `{1}`。請使用 `PersistedModel` 作為基礎。", + "734a7bebb65e10899935126ba63dd51f": "`{0}` 配置的 options 內容必須是物件", + "779467f467862836e19f494a37d6ab77": "`{0}` 配置的 acls 內容必須是物件陣列", + "80a32e80cbed65eba2103201a7c94710": "找不到模型:{0}", + "83cbdc2560ba9f09155ccfc63e08f1a1": "無法為 `{1}` 重新配置內容 `{0}`", + "97795efe0c3eb7f35ce8cf8cfe70682b": "`{0}` 的配置遺漏 {{`dataSource`}} 內容。\n請使用 `null` 或 `false` 來標示未連接至任何資料來源的模型。", + "a80038252430df2754884bf3c845c4cf": "\"{0}.{1}\" 的遠端 meta 資料遺漏 \"isStatic\" 旗標,這個方法已登錄為實例方法。", + "dc568bee32deb0f6eaf63e73b20e8ceb": "忽略 \"{0}\" 的非物件 \"methods\" 設定。", + "3aecb24fa8bdd3f79d168761ca8a6729": "{{middleware}} 階段 {0} 不明" +} + From 6ac1f694b997aa4a1eeb9afde80328761567653b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 22 Sep 2016 10:45:09 +0200 Subject: [PATCH 094/187] Temporarily disable Karma tests on Windows CI We are observing frequent test failures on Windows CI, where PhantomJS cannot start because there are no free handles available. Finding and fixing the process leaking handles is non-trivial and will take long time. This commit disables Karma tests on Windows CI machines to prevent build failures that we are ignoring anyways. --- Gruntfile.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index 25b0a880b..71312e60a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -224,6 +224,10 @@ module.exports = function(grunt) { }); }); + grunt.registerTask('skip-karma-on-windows', function() { + console.log('*** SKIPPING PHANTOM-JS BASED TESTS ON WINDOWS ***'); + }); + grunt.registerTask('e2e', ['e2e-server', 'karma:e2e']); // Default task. @@ -233,7 +237,9 @@ module.exports = function(grunt) { 'jscs', 'jshint', process.env.JENKINS_HOME ? 'mochaTest:unit-xml' : 'mochaTest:unit', - 'karma:unit-once']); + process.env.JENKINS_HOME && /^win/.test(process.platform) ? + 'skip-karma-on-windows' : 'karma:unit-once', + ]); // alias for sl-ci-run and `npm test` grunt.registerTask('mocha-and-karma', ['test']); From f7dbc97763f7cadec8a2426844bc1a3a65ca106e Mon Sep 17 00:00:00 2001 From: Richard Pringle Date: Mon, 19 Sep 2016 12:52:34 -0400 Subject: [PATCH 095/187] Call new disable remote method from model class. --- lib/model.js | 15 ++++++++++++++- test/app.test.js | 2 +- test/model.test.js | 16 +++++++++++++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/model.js b/lib/model.js index 61fe5c3fa..f3fd0bb71 100644 --- a/lib/model.js +++ b/lib/model.js @@ -441,7 +441,20 @@ module.exports = function(registry) { */ Model.disableRemoteMethod = function(name, isStatic) { - this.sharedClass.disableMethod(name, isStatic || false); + var key = this.sharedClass.getKeyFromMethodNameAndTarget(name, isStatic); + this.sharedClass.disableMethodByName(key); + this.emit('remoteMethodDisabled', this.sharedClass, key); + }; + + /** + * Disable remote invocation for the method with the given name. + * + * @param {String} name The name of the method (include "prototype." if the method is defined on the prototype). + * + */ + + Model.disableRemoteMethodByName = function(name) { + this.sharedClass.disableMethodByName(name); this.emit('remoteMethodDisabled', this.sharedClass, name); }; diff --git a/test/app.test.js b/test/app.test.js index ab1a09232..e2d472122 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -677,7 +677,7 @@ describe('app', function() { disabledRemoteMethod = methodName; }); app.model(Color); - app.models.Color.disableRemoteMethod('findOne'); + app.models.Color.disableRemoteMethodByName('findOne'); expect(remoteMethodDisabledClass).to.exist; expect(remoteMethodDisabledClass).to.eql(Color.sharedClass); expect(disabledRemoteMethod).to.exist; diff --git a/test/model.test.js b/test/model.test.js index dadfb728f..f7c8d079a 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -707,7 +707,21 @@ describe.onServer('Remote Methods', function() { var callbackSpy = require('sinon').spy(); var TestModel = app.models.TestModelForDisablingRemoteMethod; TestModel.on('remoteMethodDisabled', callbackSpy); - TestModel.disableRemoteMethod('findOne'); + TestModel.disableRemoteMethod('findOne', true); + + expect(callbackSpy).to.have.been.calledWith(TestModel.sharedClass, 'findOne'); + }); + + it('emits a `remoteMethodDisabled` event from disableRemoteMethodByName', function() { + var app = loopback(); + var model = PersistedModel.extend('TestModelForDisablingRemoteMethod'); + app.dataSource('db', { connector: 'memory' }); + app.model(model, { dataSource: 'db' }); + + var callbackSpy = require('sinon').spy(); + var TestModel = app.models.TestModelForDisablingRemoteMethod; + TestModel.on('remoteMethodDisabled', callbackSpy); + TestModel.disableRemoteMethodByName('findOne'); expect(callbackSpy).to.have.been.calledWith(TestModel.sharedClass, 'findOne'); }); From e244153eb74266510f91157f3843772128612c3c Mon Sep 17 00:00:00 2001 From: gunjpan Date: Fri, 3 Jun 2016 16:51:48 -0400 Subject: [PATCH 096/187] Update tests to use registry for model creation Current implementation of `app.model(modelName, settings)` works as a sugar for model creation. In LB 3.0, this is not supported anymore. This backporting: - keeps the sugar method for model creation for backward compatibility - updates test cases to use `app.registry.createModel()` for model creation Backport of #2401 --- example/colors/app.js | 6 +- example/replication/app.js | 8 +- test/acl.test.js | 16 ++-- test/app.test.js | 35 +++++---- test/change-stream.test.js | 7 +- test/integration.test.js | 3 +- test/registries.test.js | 20 ++--- test/relations.integration.js | 130 ++++++++++++++++++--------------- test/remoting-coercion.test.js | 7 +- 9 files changed, 128 insertions(+), 104 deletions(-) diff --git a/example/colors/app.js b/example/colors/app.js index b7a3038c2..44d75c493 100644 --- a/example/colors/app.js +++ b/example/colors/app.js @@ -14,9 +14,9 @@ var schema = { name: String }; -var Color = app.model('color', schema); - -app.dataSource('db', {adapter: 'memory'}).attach(Color); +app.dataSource('db', { connector: 'memory' }); +var Color = app.registry.createModel('color', schema); +app.model(Color, { dataSource: 'db' }); Color.create({name: 'red'}); Color.create({name: 'green'}); diff --git a/example/replication/app.js b/example/replication/app.js index 32e32299a..5ab3741d8 100644 --- a/example/replication/app.js +++ b/example/replication/app.js @@ -5,9 +5,11 @@ var loopback = require('../../'); var app = loopback(); -var db = app.dataSource('db', {connector: loopback.Memory}); -var Color = app.model('color', {dataSource: 'db', options: {trackChanges: true}}); -var Color2 = app.model('color2', {dataSource: 'db', options: {trackChanges: true}}); +var db = app.dataSource('db', { connector: 'memory' }); +var Color = app.registry.createModel('color', {}, { trackChanges: true }); +app.model(Color, { dataSource: 'db' }); +var Color2 = app.registry.createModel('color2', {}, { trackChanges: true }); +app.model(Color2, { dataSource: 'db' }); var target = Color2; var source = Color; var SPEED = process.env.SPEED || 100; diff --git a/test/acl.test.js b/test/acl.test.js index 3022ebe6b..430d71fe2 100644 --- a/test/acl.test.js +++ b/test/acl.test.js @@ -363,19 +363,17 @@ describe('security ACLs', function() { }); describe('access check', function() { - var app; - before(function() { - app = loopback(); - app.use(loopback.rest()); - app.enableAuth(); - app.dataSource('test', {connector: 'memory'}); - }); - it('should occur before other remote hooks', function(done) { - var MyTestModel = app.model('MyTestModel', {base: 'PersistedModel', dataSource: 'test'}); + var app = loopback(); + var MyTestModel = app.registry.createModel('MyTestModel'); var checkAccessCalled = false; var beforeHookCalled = false; + app.use(loopback.rest()); + app.enableAuth(); + app.dataSource('test', { connector: 'memory' }); + app.model(MyTestModel, { dataSource: 'test' }); + // fake / spy on the checkAccess method MyTestModel.checkAccess = function() { var cb = arguments[arguments.length - 1]; diff --git a/test/app.test.js b/test/app.test.js index e2d472122..66bac4e7d 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -617,11 +617,11 @@ describe('app', function() { }); describe('app.model(Model)', function() { - var app; - var db; + var app, db, MyTestModel; beforeEach(function() { app = loopback(); - db = loopback.createDataSource({connector: loopback.Memory}); + db = loopback.createDataSource({ connector: loopback.Memory }); + MyTestModel = app.registry.createModel('MyTestModel', {}, {base: 'Model'}); }); it('Expose a `Model` to remote clients', function() { @@ -632,8 +632,8 @@ describe('app', function() { expect(app.models()).to.eql([Color]); }); - it('uses singlar name as app.remoteObjects() key', function() { - var Color = PersistedModel.extend('color', {name: String}); + it('uses singular name as app.remoteObjects() key', function() { + var Color = PersistedModel.extend('color', { name: String }); app.model(Color); Color.attachTo(db); expect(app.remoteObjects()).to.eql({ color: Color }); @@ -695,18 +695,22 @@ describe('app', function() { }); }); - it('accepts null dataSource', function() { - app.model('MyTestModel', { dataSource: null }); + it('accepts null dataSource', function(done) { + app.model(MyTestModel, { dataSource: null }); + expect(MyTestModel.dataSource).to.eql(null); + done(); }); - it('accepts false dataSource', function() { - app.model('MyTestModel', { dataSource: false }); + it('accepts false dataSource', function(done) { + app.model(MyTestModel, { dataSource: false }); + expect(MyTestModel.getDataSource()).to.eql(null); + done(); }); - it('should not require dataSource', function() { - app.model('MyTestModel', {}); + it('does not require dataSource', function(done) { + app.model(MyTestModel); + done(); }); - }); describe('app.model(name, config)', function() { @@ -767,7 +771,6 @@ describe('app', function() { expect(app.models.foo.app).to.equal(app); expect(app.models.foo.shared).to.equal(true); }); - }); describe('app.model(ModelCtor, config)', function() { @@ -780,7 +783,8 @@ describe('app', function() { } assert(!previousModel || !previousModel.dataSource); - app.model('TestModel', { dataSource: 'db' }); + var TestModel = app.registry.createModel('TestModel'); + app.model(TestModel, { dataSource: 'db' }); expect(app.models.TestModel.dataSource).to.equal(app.dataSources.db); }); }); @@ -788,7 +792,8 @@ describe('app', function() { describe('app.models', function() { it('is unique per app instance', function() { app.dataSource('db', { connector: 'memory' }); - var Color = app.model('Color', { dataSource: 'db' }); + var Color = app.registry.createModel('Color'); + app.model(Color, { dataSource: 'db' }); expect(app.models.Color).to.equal(Color); var anotherApp = loopback(); expect(anotherApp.models.Color).to.equal(undefined); diff --git a/test/change-stream.test.js b/test/change-stream.test.js index cbd24ca23..86dfc77fd 100644 --- a/test/change-stream.test.js +++ b/test/change-stream.test.js @@ -7,9 +7,10 @@ describe('PersistedModel.createChangeStream()', function() { describe('configured to source changes locally', function() { before(function() { var test = this; - var app = loopback({localRegistry: true}); - var ds = app.dataSource('ds', {connector: 'memory'}); - this.Score = app.model('Score', { + var app = loopback({ localRegistry: true }); + var ds = app.dataSource('ds', { connector: 'memory' }); + var Score = app.registry.createModel('Score'); + this.Score = app.model(Score, { dataSource: 'ds', changeDataSource: false // use only local observers }); diff --git a/test/integration.test.js b/test/integration.test.js index 074bd2625..ed671376b 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -41,7 +41,8 @@ describe('loopback application', function() { loopback.ACL.attachTo(db); loopback.User.hasMany(loopback.AccessToken, { as: 'accessTokens' }); - var Streamer = app.model('Streamer', { dataSource: 'db' }); + var Streamer = app.registry.createModel('Streamer'); + app.model(Streamer, { dataSource: 'db' }); Streamer.read = function(req, res, cb) { var body = new Buffer(0); req.on('data', function(chunk) { diff --git a/test/registries.test.js b/test/registries.test.js index 2f9cda87f..9cdcd24b0 100644 --- a/test/registries.test.js +++ b/test/registries.test.js @@ -16,15 +16,17 @@ describe('Registry', function() { var dsFoo = appFoo.dataSource('dsFoo', {connector: 'memory'}); var dsBar = appFoo.dataSource('dsBar', {connector: 'memory'}); - var FooModel = appFoo.model(modelName, settings); - var FooSubModel = appFoo.model(subModelName, settings); - var BarModel = appBar.model(modelName, settings); - var BarSubModel = appBar.model(subModelName, settings); - - FooModel.attachTo(dsFoo); - FooSubModel.attachTo(dsFoo); - BarModel.attachTo(dsBar); - BarSubModel.attachTo(dsBar); + var FooModel = appFoo.registry.createModel(modelName, {}, settings); + appFoo.model(FooModel, { dataSource: dsFoo }); + + var FooSubModel = appFoo.registry.createModel(subModelName, {}, settings); + appFoo.model(FooSubModel, { dataSource: dsFoo }); + + var BarModel = appBar.registry.createModel(modelName, {}, settings); + appBar.model(BarModel, { dataSource: dsBar }); + + var BarSubModel = appBar.registry.createModel(subModelName, {}, settings); + appBar.model(BarSubModel, { dataSource: dsBar }); FooModel.hasMany(FooSubModel); BarModel.hasMany(BarSubModel); diff --git a/test/relations.integration.js b/test/relations.integration.js index 9871edae5..f6a70dc99 100644 --- a/test/relations.integration.js +++ b/test/relations.integration.js @@ -41,24 +41,14 @@ describe('relations - integration', function() { describe('polymorphicHasMany', function() { before(function defineProductAndCategoryModels() { - var Team = app.model( - 'Team', - { properties: { name: 'string' }, - dataSource: 'db' - } - ); - var Reader = app.model( - 'Reader', - { properties: { name: 'string' }, - dataSource: 'db' - } - ); - var Picture = app.model( - 'Picture', - { properties: { name: 'string', imageableId: 'number', imageableType: 'string'}, - dataSource: 'db' - } - ); + var Team = app.registry.createModel('Team', { name: 'string' }); + var Reader = app.registry.createModel('Reader', { name: 'string' }); + var Picture = app.registry.createModel('Picture', + { name: 'string', imageableId: 'number', imageableType: 'string' }); + + app.model(Team, { dataSource: 'db' }); + app.model(Reader, { dataSource: 'db' }); + app.model(Picture, { dataSource: 'db' }); Reader.hasMany(Picture, { polymorphic: { // alternative syntax as: 'imageable', // if not set, default to: reference @@ -684,15 +674,17 @@ describe('relations - integration', function() { describe('hasAndBelongsToMany', function() { beforeEach(function defineProductAndCategoryModels() { - var product = app.model( - 'product', - { properties: { id: 'string', name: 'string' }, dataSource: 'db' } - + var product = app.registry.createModel( + 'product', + { id: 'string', name: 'string' } ); - var category = app.model( + var category = app.registry.createModel( 'category', - { properties: { id: 'string', name: 'string' }, dataSource: 'db' } + { id: 'string', name: 'string' } ); + app.model(product, { dataSource: 'db' }); + app.model(category, { dataSource: 'db' }); + product.hasAndBelongsToMany(category); category.hasAndBelongsToMany(product); }); @@ -807,17 +799,18 @@ describe('relations - integration', function() { describe('embedsOne', function() { before(function defineGroupAndPosterModels() { - var group = app.model( - 'group', - { properties: { name: 'string' }, - dataSource: 'db', - plural: 'groups' - } + var group = app.registry.createModel('group', + { name: 'string' }, + { plural: 'groups' } ); - var poster = app.model( + app.model(group, { dataSource: 'db' }); + + var poster = app.registry.createModel( 'poster', - { properties: { url: 'string' }, dataSource: 'db' } + { url: 'string' } ); + app.model(poster, { dataSource: 'db' }); + group.embedsOne(poster, { as: 'cover' }); }); @@ -924,17 +917,18 @@ describe('relations - integration', function() { describe('embedsMany', function() { before(function defineProductAndCategoryModels() { - var todoList = app.model( + var todoList = app.registry.createModel( 'todoList', - { properties: { name: 'string' }, - dataSource: 'db', - plural: 'todo-lists' - } + { name: 'string' }, + { plural: 'todo-lists' } ); - var todoItem = app.model( + app.model(todoList, { dataSource: 'db' }); + + var todoItem = app.registry.createModel( 'todoItem', - { properties: { content: 'string' }, dataSource: 'db' } + { content: 'string' }, { forceId: false } ); + app.model(todoItem, { dataSource: 'db' }); todoList.embedsMany(todoItem, { as: 'items' }); }); @@ -1112,18 +1106,24 @@ describe('relations - integration', function() { describe('referencesMany', function() { before(function defineProductAndCategoryModels() { - var recipe = app.model( + var recipe = app.registry.createModel( 'recipe', - { properties: { name: 'string' }, dataSource: 'db' } + { name: 'string' } ); - var ingredient = app.model( + app.model(recipe, { dataSource: 'db' }); + + var ingredient = app.registry.createModel( 'ingredient', - { properties: { name: 'string' }, dataSource: 'db' } + { name: 'string' } ); - var photo = app.model( + app.model(ingredient, { dataSource: 'db' }); + + var photo = app.registry.createModel( 'photo', - { properties: { name: 'string' }, dataSource: 'db' } + { name: 'string' } ); + app.model(photo, { dataSource: 'db' }); + recipe.referencesMany(ingredient); // contrived example for test: recipe.hasOne(photo, { as: 'picture', options: { @@ -1460,31 +1460,41 @@ describe('relations - integration', function() { describe('nested relations', function() { before(function defineModels() { - var Book = app.model( + var Book = app.registry.createModel( 'Book', - { properties: { name: 'string' }, dataSource: 'db', - plural: 'books' } + { name: 'string' }, + { plural: 'books' } ); - var Page = app.model( + app.model(Book, { dataSource: 'db' }); + + var Page = app.registry.createModel( 'Page', - { properties: { name: 'string' }, dataSource: 'db', - plural: 'pages' } + { name: 'string' }, + { plural: 'pages' } ); - var Image = app.model( + app.model(Page, { dataSource: 'db' }); + + var Image = app.registry.createModel( 'Image', - { properties: { name: 'string' }, dataSource: 'db', - plural: 'images' } + { name: 'string' }, + { plural: 'images' } ); - var Note = app.model( + app.model(Image, { dataSource: 'db' }); + + var Note = app.registry.createModel( 'Note', - { properties: { text: 'string' }, dataSource: 'db', - plural: 'notes' } + { text: 'string' }, + { plural: 'notes' } ); - var Chapter = app.model( + app.model(Note, { dataSource: 'db' }); + + var Chapter = app.registry.createModel( 'Chapter', - { properties: { name: 'string' }, dataSource: 'db', - plural: 'chapters' } + { name: 'string' }, + { plural: 'chapters' } ); + app.model(Chapter, { dataSource: 'db' }); + Book.hasMany(Page); Book.hasMany(Chapter); Page.hasMany(Note); diff --git a/test/remoting-coercion.test.js b/test/remoting-coercion.test.js index bc6bd7950..6e86fa6d1 100644 --- a/test/remoting-coercion.test.js +++ b/test/remoting-coercion.test.js @@ -12,7 +12,12 @@ describe('remoting coercion', function() { var app = loopback(); app.use(loopback.rest()); - var TestModel = app.model('TestModel', {base: 'Model', dataSource: null, public: true}); + var TestModel = app.registry.createModel('TestModel', + {}, + { base: 'Model' } + ); + app.model(TestModel, { public: true }); + TestModel.test = function(inst, cb) { called = true; assert(inst instanceof TestModel); From 26147b6bc272cd8c5951090b8138c50ec4f512ef Mon Sep 17 00:00:00 2001 From: Candy Date: Wed, 28 Sep 2016 15:32:12 -0400 Subject: [PATCH 097/187] Update translation files - round#2 --- intl/es/messages.json | 18 +++--- intl/it/messages.json | 2 +- intl/ja/messages.json | 142 +++++++++++++++++++++--------------------- intl/tr/messages.json | 10 +-- 4 files changed, 86 insertions(+), 86 deletions(-) diff --git a/intl/es/messages.json b/intl/es/messages.json index e91c6c4f1..2a82383b5 100644 --- a/intl/es/messages.json +++ b/intl/es/messages.json @@ -1,15 +1,15 @@ { "3b46d3a780fd6ae5f95ade489a0efffe": "El contexto actual no está soportado en el navegador.", - "7e0fca41d098607e1c9aa353c67e0fa1": "Señal de acceso no válida ", + "7e0fca41d098607e1c9aa353c67e0fa1": "Señal de acceso no válida", "320c482401afa1207c04343ab162e803": "Tipo de principal no válido: {0}", "c2b5d51f007178170ca3952d59640ca4": "No se pueden rectificar los cambios de {0}:\n{1}", "5858e63efaa0e4ad86b61c0459ea32fa": "Debe conectar el modelo de {{Email}} a un conector de {{Mail}}", "0caffe1d763c8cca6a61814abe33b776": "Es necesario el correo electrónico", - "1b2a6076dccbe91a56f1672eb3b8598c": "El cuerpo de respuesta contiene propiedades de la {{AccessToken}} creada durante el inicio de la sesión. \nDependiendo del valor del parámetro `include`, el cuerpo puede contener propiedades adicionales:\n\n - `user` - `U+007BUserU+007D` - Datos del usuario conectado actualmente. {{(`include=user`)}}\n\n", + "1b2a6076dccbe91a56f1672eb3b8598c": "El cuerpo de respuesta contiene propiedades de la {{AccessToken}} creada durante el inicio de la sesión.\nDependiendo del valor del parámetro `include`, el cuerpo puede contener propiedades adicionales:\n\n - `user` - `U+007BUserU+007D` - Datos del usuario conectado actualmente. {{(`include=user`)}}\n\n", "2362ba55796c733e337af169922327a2": "Objetos relacionados a incluir en la respuesta. Consulte la descripción del valor de retorno para obtener más detalles.", "306999d39387d87b2638199ff0bed8ad": "Restablecer contraseña para un usuario con correo electrónico.", "3aae63bb7e8e046641767571c1591441": "el inicio de sesión ha fallado porque el correo electrónico no ha sido verificado", - "3caaa84fc103d6d5612173ae6d43b245": "La señal no es válida: {0} ", + "3caaa84fc103d6d5612173ae6d43b245": "La señal no es válida: {0}", "42e3fa18945255412ebc6561e2c6f1dc": "Confirmar un registro de usuario con la señal de verificación de correo electrónico.", "430b6326d7ebf6600a39f614ef516bc8": "No proporcione este argumento, se extrae automáticamente de las cabeceras de solicitud.", "44a6c8b1ded4ed653d19ddeaaf89a606": "Correo electrónico no encontrado", @@ -30,7 +30,7 @@ "1e85f822b547a75d7d385048030e4ecb": "Creado: {0}", "7d5e7ed0efaedf3f55f380caae0df8b8": "Mi primera aplicación móvil", "04bd8af876f001ceaf443aad6a9002f9": "La autenticación requiere la definición del modelo {0}.", - "095afbf2f1f0e5be678f5dac5c54e717": "Acceso denegado ", + "095afbf2f1f0e5be678f5dac5c54e717": "Acceso denegado", "2d3071e3b18681c80a090dc0efbdb349": "no se ha encontrado {0} con el ID {1}", "316e5b82c203cf3de31a449ee07d0650": "Se esperaba un booleano, se ha obtenido {0}", "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "No se puede crear el origen de datos {0}: {1}", @@ -97,19 +97,19 @@ "e43e320a435ec1fa07648c1da0d558a7": "Comprobar si una instancia de modelo existe en el origen de datos.", "e92aa25b6b864e3454b65a7c422bd114": "La actualización masiva ha fallado, el conector ha suprimido un número de registros inesperado: {0}", "ea63d226b6968e328bdf6876010786b5": "No pueden aplicarse actualizaciones masivas, el conector no notifica correctamente el número de registros suprimidos.", - "f1d4ac54357cc0932f385d56814ba7e4": "Conflicto ", + "f1d4ac54357cc0932f385d56814ba7e4": "Conflicto", "f37d94653793e33f4203e45b4a1cf106": "Recuento de instancias del modelo comparadas por where desde el origen de datos.", - "0731d0109e46c21a4e34af3346ed4856": "Este comportamiento puede cambiar en la próxima versión principal. ", + "0731d0109e46c21a4e34af3346ed4856": "Este comportamiento puede cambiar en la próxima versión principal.", "2e110abee2c95bcfc2dafd48be7e2095": "No se puede configurar {0}: {{config.dataSource}} debe ser una instancia de {{DataSource}}", - "308e1d484516a33df788f873e65faaff": "El modelo `{0}` está ampliando `DataModel` en desuso. Utilice `PersistedModel` en su lugar. ", + "308e1d484516a33df788f873e65faaff": "El modelo `{0}` está ampliando `DataModel` en desuso. Utilice `PersistedModel` en su lugar.", "3438fab56cc7ab92dfd88f0497e523e0": "La configuración de la propiedad relations de `{0}` debe ser un objeto", "4cac5f051ae431321673e04045d37772": "El modelo `{0}` está ampliando un modelo desconocido `{1}`. Se utiliza `PersistedModel` como base.", "734a7bebb65e10899935126ba63dd51f": "La configuración de la propiedad de options de `{0}` debe ser un objeto", "779467f467862836e19f494a37d6ab77": "La configuración de la propiedad acls de `{0}` debe ser una matriz de objetos", - "80a32e80cbed65eba2103201a7c94710": "No se ha encontrado el modelo: {0} ", + "80a32e80cbed65eba2103201a7c94710": "No se ha encontrado el modelo: {0}", "83cbdc2560ba9f09155ccfc63e08f1a1": "La propiedad `{0}` no puede reconfigurarse para `{1}`", "97795efe0c3eb7f35ce8cf8cfe70682b": "En la configuración de `{0}` falta la propiedad {{`dataSource`}}.\nUtilice `null` o `false` para marcar los modelos no conectados a ningún origen de datos.", - "a80038252430df2754884bf3c845c4cf": "En los metadatos de interacción remota para \"{0}.{1}\" falta el indicador \"isStatic\", el método está registrado como método de instancia. ", + "a80038252430df2754884bf3c845c4cf": "En los metadatos de interacción remota para \"{0}.{1}\" falta el indicador \"isStatic\", el método está registrado como método de instancia.", "dc568bee32deb0f6eaf63e73b20e8ceb": "Se ignora el valor \"methods\" no de objeto de \"{0}\".", "3aecb24fa8bdd3f79d168761ca8a6729": "Fase de {{middleware}} desconocida {0}" } diff --git a/intl/it/messages.json b/intl/it/messages.json index db2a718b6..d846e755a 100644 --- a/intl/it/messages.json +++ b/intl/it/messages.json @@ -5,7 +5,7 @@ "c2b5d51f007178170ca3952d59640ca4": "Impossibile correggere {0} modifiche:\n{1}", "5858e63efaa0e4ad86b61c0459ea32fa": "È necessario collegare il modello {{Email}} ad un connettore {{Mail}}", "0caffe1d763c8cca6a61814abe33b776": "L'email è obbligatoria", - "1b2a6076dccbe91a56f1672eb3b8598c": "Il corpo della risposta contiene proprietà del {{AccessToken}} creato all'accesso. \nIn base al valore del parametro `include`, il corpo può contenere ulteriori proprietà:\n\n - `user` - `U+007BUserU+007D` - Dati dell'utente attualmente collegato.. {{(`include=user`)}}\n\n", + "1b2a6076dccbe91a56f1672eb3b8598c": "Il corpo della risposta contiene proprietà del {{AccessToken}} creato all'accesso.\nIn base al valore del parametro `include`, il corpo può contenere ulteriori proprietà:\n\n - `user` - `U+007BUserU+007D` - Dati dell'utente attualmente collegato.. {{(`include=user`)}}\n\n", "2362ba55796c733e337af169922327a2": "Oggetti correlati da includere nella risposta. Per ulteriori dettagli, consultare la descrizione del valore di restituzione.", "306999d39387d87b2638199ff0bed8ad": "Reimpostare la password per un utente con email.", "3aae63bb7e8e046641767571c1591441": "login non riuscito perché l'email non è stata verificata", diff --git a/intl/ja/messages.json b/intl/ja/messages.json index 8f5ae814a..a3b8c5f06 100644 --- a/intl/ja/messages.json +++ b/intl/ja/messages.json @@ -1,116 +1,116 @@ { - "3b46d3a780fd6ae5f95ade489a0efffe": "現行コンテキストはブラウザーではサポートされていません。", + "3b46d3a780fd6ae5f95ade489a0efffe": "現行コンテキストはブラウザーでサポートされていません。", "7e0fca41d098607e1c9aa353c67e0fa1": "無効なアクセス・トークン", "320c482401afa1207c04343ab162e803": "無効なプリンシパル・タイプ: {0}", - "c2b5d51f007178170ca3952d59640ca4": "{0} 件の変更を修正できません:\n{1}", + "c2b5d51f007178170ca3952d59640ca4": "{0} の変更を修正できません:\n{1}", "5858e63efaa0e4ad86b61c0459ea32fa": "{{Email}} モデルを {{Mail}} コネクターに接続する必要があります", - "0caffe1d763c8cca6a61814abe33b776": "E メールが必要です", - "1b2a6076dccbe91a56f1672eb3b8598c": "応答の本文には、ログイン時に作成された {{AccessToken}} のプロパティーが含まれます。\n「include」パラメーターの値によっては、本文に追加プロパティーが含まれる場合があります:\n\n - 「user」 - 「U+007BUserU+007D」 - 現在ログインしているユーザーのデータ。 {{(`include=user`)}}\n\n", - "2362ba55796c733e337af169922327a2": "応答に含める関連オブジェクト。詳細については、戻り値の説明を参照してください。", + "0caffe1d763c8cca6a61814abe33b776": "E メールは必須です", + "1b2a6076dccbe91a56f1672eb3b8598c": "応答本文には、ログイン時に作成された {{AccessToken}} のプロパティーが含まれます。\n`include` パラメーターの値によっては、本文に追加のプロパティーが含まれる場合があります:\n\n - `user` - `U+007BUserU+007D` - 現在ログインしているユーザーのデータ。 {{(`include=user`)}}\n\n", + "2362ba55796c733e337af169922327a2": "応答に組み込む関連オブジェクト。詳細については、戻り値の説明を参照してください。", "306999d39387d87b2638199ff0bed8ad": "E メールを使用してユーザーのパスワードをリセットします。", - "3aae63bb7e8e046641767571c1591441": "E メールが検証されていないためログインに失敗しました", + "3aae63bb7e8e046641767571c1591441": "E メールが検証されていないため、ログインに失敗しました", "3caaa84fc103d6d5612173ae6d43b245": "無効なトークン: {0}", "42e3fa18945255412ebc6561e2c6f1dc": "E メール検証トークンを使用してユーザー登録を確認します。", - "430b6326d7ebf6600a39f614ef516bc8": "この引数は指定しないでください。要求ヘッダーから自動的に抽出されます。", + "430b6326d7ebf6600a39f614ef516bc8": "この引数を指定しないでください。これは要求ヘッダーから自動的に抽出されます。", "44a6c8b1ded4ed653d19ddeaaf89a606": "E メールが見つかりません", "5e81ad3847a290dc650b47618b9cbc7e": "ログインに失敗しました", - "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "ユーザー名/E メールおよびパスワードを使用してユーザーをログインさせます。", - "8608c28f5e6df0008266e3c497836176": "アクセス・トークンを使用してユーザーをログアウトさせます。", - "860d1a0b8bd340411fb32baa72867989": "トランスポートは HTTP リダイレクトをサポートしていません。", + "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "ユーザー名/E メールとパスワードを使用してユーザーにログインします。", + "8608c28f5e6df0008266e3c497836176": "アクセス・トークンを使用してユーザーをログアウトします。", + "860d1a0b8bd340411fb32baa72867989": "トランスポートでは HTTP リダイレクトはサポートされません。", "895b1f941d026870b3cc8e6af087c197": "{{username}} または {{email}} が必要です", "a50d10fc6e0959b220e085454c40381e": "ユーザーが見つかりません: {0}", - "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} が必要です", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} は必須です", "c34fa20eea0091747fcc9eda204b8d37": "{{accessToken}} が見つかりませんでした", "c68a93f0a9524fed4ff64372fc90c55f": "有効な E メールを指定する必要があります", "f58cdc481540cd1f69a4aa4da2e37981": "無効なパスワード: {0}", - "f0bd73df8714cefb925e3b8da2f4c5f6": "結果: {0}", - "10e01c895dc0b2fecc385f9f462f1ca6": "色のリストを {{http://localhost:3000/colors}} で参照できます", - "8a27e0c9ce3ebf0e0c3978efb456e13e": "ホスト {0} に対する要求", - "a40684f5a9f546115258b76938d1de37": "色のリストを {{http://localhost:3000/colors}} で参照できます", - "1e85f822b547a75d7d385048030e4ecb": "作成: {0}", - "7d5e7ed0efaedf3f55f380caae0df8b8": "自分の最初のモバイル・アプリケーション", - "04bd8af876f001ceaf443aad6a9002f9": "認証ではモデル {0} を定義する必要があります。", - "095afbf2f1f0e5be678f5dac5c54e717": "アクセスが拒否されました", - "2d3071e3b18681c80a090dc0efbdb349": "ID が {1} である {0} は見つかりませんでした", - "316e5b82c203cf3de31a449ee07d0650": "ブール値が必要ですが、{0} を受け取りました", - "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "データ・ソース {0} を作成できません: {1}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "結果:{0}", + "10e01c895dc0b2fecc385f9f462f1ca6": "カラー・リストは {{http://localhost:3000/colors}} で利用できます", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "ホスト {0} への要求", + "a40684f5a9f546115258b76938d1de37": "カラー・リストは {{http://localhost:3000/colors}} で利用できます", + "1e85f822b547a75d7d385048030e4ecb": "作成済み: {0}", + "7d5e7ed0efaedf3f55f380caae0df8b8": "最初のモバイル・アプリケーション", + "04bd8af876f001ceaf443aad6a9002f9": "認証では、モデル {0} を定義する必要があります。", + "095afbf2f1f0e5be678f5dac5c54e717": "アクセス拒否", + "2d3071e3b18681c80a090dc0efbdb349": "ID {1} の {0} が見つかりませんでした", + "316e5b82c203cf3de31a449ee07d0650": "ブール値が必要ですが、{0} が取得されました", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "データ・ソース {0}: {1} を作成できません", "7e287fc885d9fdcf42da3a12f38572c1": "許可が必要です", "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} は削除されました。代わりに新規モジュール {{loopback-boot}} を使用してください", "1d7833c3ca2f05fdad8fad7537531c40": "\t 件名:{0}", "275f22ab95671f095640ca99194b7635": "\t 送信元:{0}", - "3d63008ccfb2af1db2142e8cc2716ace": "警告: E メールを送信するための E メール・トランスポートが指定されていません。メール・メッセージを送信するためのトランスポートをセットアップしてください。", + "3d63008ccfb2af1db2142e8cc2716ace": "警告: E メール送信用の E メール・トランスポートが指定されていません。メール・メッセージを送信するためのトランスポートをセットアップしてください。", "4a4f04a4e480fc5d4ee73b84d9a4b904": "メールの送信:", "63a091ced88001ab6acb58f61ec041c5": "\t テキスト:{0}", "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", "ecb06666ef95e5db27a5ac1d6a17923b": "\t 宛先:{0}", "f0aed00a3d3d0b97d6594e4b70e0c201": "\t トランスポート:{0}", - "0da38687fed24275c1547e815914a8e3": "{0} の ID によって関連項目を削除します。", - "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "モデル・インスタンスと突き合わせる基準", - "22fe62fa8d595b72c62208beddaa2a56": "{0} の ID によって関連項目を更新します。", + "0da38687fed24275c1547e815914a8e3": "ID を指定して {0} の関連項目を削除します。", + "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "モデル・インスタンスの一致基準", + "22fe62fa8d595b72c62208beddaa2a56": "ID を指定して {0} の関連項目を更新します。", "528325f3cbf1b0ab9a08447515daac9a": "このモデルの {0} を更新します。", "543d19bad5e47ee1e9eb8af688e857b4": "{0} の外部キー。", - "598ff0255ffd1d1b71e8de55dbe2c034": "ID によって項目に対する {0} 関係の存在を検査します。", - "5a36cc6ba0cc27c754f6c5ed6015ea3c": "ID によって項目に対する {0} 関係を削除します。", - "5fa3afb425819ebde958043e598cb664": "{{id}} {0} のモデルは見つかりませんでした", - "61e5deebaf44d68f4e6a508f30cc31a3": "モデル `{1}` の関係 `{0}` は存在しません", - "651f0b3cbba001635152ec3d3d954d0a": "{0} の ID によって関連項目を検索します。", + "598ff0255ffd1d1b71e8de55dbe2c034": "ID を指定して項目との {0} 関係があることを確認します。", + "5a36cc6ba0cc27c754f6c5ed6015ea3c": "ID を指定して項目との {0} 関係を削除します。", + "5fa3afb425819ebde958043e598cb664": "{{id}} {0} のモデルが見つかりませんでした", + "61e5deebaf44d68f4e6a508f30cc31a3": "モデル `{1}` には関係 `{0}` が存在しません", + "651f0b3cbba001635152ec3d3d954d0a": "ID を指定して {0} の関連項目を検索します。", "7bc7b301ad9c4fc873029d57fb9740fe": "{1} の {0} を照会します。", "7c837b88fd0e509bd3fc722d7ddf0711": "{0} の外部キー", "830cb6c862f8f364e9064cea0026f701": "hasOne 関係 {0} をフェッチします。", - "855ecd4a64885ba272d782435f72a4d4": "不明な \"{0}\" ID \"{1}\"。", - "86254879d01a60826a851066987703f2": "{0} の ID によって関連項目を追加します。", + "855ecd4a64885ba272d782435f72a4d4": "\"{0}\" ID \"{1}\" が不明です。", + "86254879d01a60826a851066987703f2": "ID を指定して {0} の関連項目を追加します。", "8ae418c605b6a45f2651be9b1677c180": "無効なリモート・メソッド: `{0}`", "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "belongsTo 関係 {0} をフェッチします。", "c0057a569ff9d3b509bac61a4b2f605d": "このモデルのすべての {0} を削除します。", "cd0412f2f33a4a2a316acc834f3f21a6": "{{id}} または {{data}} を指定する必要があります", - "d6f43b266533b04d442bdb3955622592": "このモデルの {0} の新規インスタンスを作成します。", + "d6f43b266533b04d442bdb3955622592": "このモデルの {0} に新規インスタンスを作成します。", "da13d3cdf21330557254670dddd8c5c7": "{1} の {0} をカウントします。", - "e4434de4bb8f5a3cd1d416e4d80d7e0b": "不明な \"{0}\" {{id}} \"{1}\"。", + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" {{id}} \"{1}\" が不明です。", "f66ae3cf379b2fce28575a3282defe1a": "このモデルの {0} を削除します。", - "03f79fa268fe199de2ce4345515431c1": "ID が {1} である {0} の変更レコードは見つかりませんでした", - "0f1c71f74b040bfbe8d384a414e31f03": "指定のチェックポイント以降の差分および競合のセットを取得します。", - "15254dec061d023d6c030083a0cef50f": "モデルの新規インスタンスを作成し、データ・ソースに永続化します。", - "16a11368d55b85a209fc6aea69ee5f7a": "一致するレコードをすべて削除します。", + "03f79fa268fe199de2ce4345515431c1": "ID {1} の {0} の変更レコードが見つかりませんでした", + "0f1c71f74b040bfbe8d384a414e31f03": "指定されたチェックポイント以降の一連の差分および競合を取得します。", + "15254dec061d023d6c030083a0cef50f": "モデルの新規インスタンスを作成し、それをデータ・ソースに保管します。", + "16a11368d55b85a209fc6aea69ee5f7a": "一致するすべてのレコードを削除します。", "1bc1d489ddf347af47af3d9b1fc7cc15": "一度に複数の更新を実行します。注: これはアトミックではありません。", "1bc7d8283c9abda512692925c8d8e3c0": "現在のチェックポイントを取得します。", - "1caa7cc61266e7aef7db7d2f0e27ac3e": "このインスタンスに対して保持されている最新の変更レコードのプロパティーを更新します。", + "1caa7cc61266e7aef7db7d2f0e27ac3e": "このインスタンスについて保持されている最新の変更レコードのプロパティーを更新します。", "2a7df74fe6e8462e617b79d5fbb536ea": "このインスタンスの最新の変更レコードを取得します。", - "2a9684b3d5b3b67af74bac74eb1b0843": "データ・ソースからフィルターと一致するモデルのすべてのインスタンスを検索します。", - "2e50838caf0c927735eb15d12866bdd7": "指定のチェックポイント以降のモデルに対する変更を取得します。フィルター・オブジェクトを指定して、返される結果の数を減らします。", - "4203ab415ec66a78d3164345439ba76e": "{0}.{1}() を呼び出せません。{2} メソッドがセットアップされていません。{{PersistedModel}} が {{DataSource}} に正しく付加されていません。", - "51ea9b6245bb5e672b236d640ca3b048": "変更プロパティー名/値のペアのオブジェクト", - "55ddedd4c501348f82cb89db02ec85c1": "モデル・プロパティー名/値のペアのオブジェクト", - "5aaa76c72ae1689fd3cf62589784a4ba": "モデル・インスタンスの属性を更新し、データ・ソースに永続化します。", - "5f659bbc15e6e2b249fa33b3879b5f69": "データ・ソースから {{id}} によってモデル・インスタンスを検索します。", - "62e8b0a733417978bab22c8dacf5d7e6": "一括更新を適用できません。更新レコードの数がコネクターによって正しく報告されません。", + "2a9684b3d5b3b67af74bac74eb1b0843": "データ・ソースからフィルターで一致するモデルのすべてのインスタンスを検索します。", + "2e50838caf0c927735eb15d12866bdd7": "指定されたチェックポイント以降のモデルへの変更を取得します。フィルター・オブジェクトを指定すると、返される結果の件数が削減されます。", + "4203ab415ec66a78d3164345439ba76e": "{0}.{1}() を呼び出せません。{2} メソッドがセットアップされていません。{{PersistedModel}} は {{DataSource}} に正しく付加されていません。", + "51ea9b6245bb5e672b236d640ca3b048": "変更プロパティー名/値ペアのオブジェクト", + "55ddedd4c501348f82cb89db02ec85c1": "モデル・プロパティー名/値ペアのオブジェクト", + "5aaa76c72ae1689fd3cf62589784a4ba": "モデル・インスタンスの属性を更新し、それをデータ・ソースに保管します。", + "5f659bbc15e6e2b249fa33b3879b5f69": "{{id}} を指定してデータ・ソースからモデル・インスタンスを検索します。", + "62e8b0a733417978bab22c8dacf5d7e6": "一括更新を適用できません。コネクターは更新されたレコードの数を正しく報告していません。", "6329e0ac1de3c250ebb1df5afd5a2a7b": "更新されたインスタンスの数", - "6bc376432cd9972cf991aad3de371e78": "変更の欠落データ: {0}", - "79295ac04822d2e9702f0dd1d0240336": "データ・ソースから {{where}} と一致するモデルのインスタンスを更新します。", + "6bc376432cd9972cf991aad3de371e78": "変更用のデータがありません: {0}", + "79295ac04822d2e9702f0dd1d0240336": "データ・ソースから {{where}} で一致するモデルのインスタンスを更新します。", "7f2fde7f0f860ead224b11ba8d75aa1c": "差分リストから更新リストを作成します。", - "89b57e764c2267129294b07589dbfdc2": "データ・ソースから {{id}} によってモデル・インスタンスを削除します。", - "8bab6720ecc58ec6412358c858a53484": "一括更新に失敗しました。コネクターは、予期しない数のレコードを変更しました: {0}", - "a98b6cc6547706b5c6bffde0ed5fd55c": "データ・ソースからフィルターと一致するモデルの最初のインスタンスを検索します。", + "89b57e764c2267129294b07589dbfdc2": "{{id}} を指定してデータ・ソースからモデル・インスタンスを削除します。", + "8bab6720ecc58ec6412358c858a53484": "一括更新が失敗しました。コネクターは予期しない数のレコードを変更しました: {0}", + "a98b6cc6547706b5c6bffde0ed5fd55c": "データ・ソースからフィルターで一致するモデルの最初のインスタンスを検索します。", "c46d4aba1f14809c16730faa46933495": "フィルター定義フィールドおよび include", - "c65600640f206f585d300b4bcb699d95": "チェックポイントを作成します。 ", - "cf64c7afc74d3a8120abcd028f98c770": "既存のモデル・インスタンスを更新するか、新規のモデル・インスタンスをデータ・ソースに挿入します。", + "c65600640f206f585d300b4bcb699d95": "チェックポイントを作成します。", + "cf64c7afc74d3a8120abcd028f98c770": "既存のモデル・インスタンスを更新するか、新規モデル・インスタンスをデータ・ソースに挿入します。", "dcb6261868ff0a7b928aa215b07d068c": "変更ストリームを作成します。", - "e43e320a435ec1fa07648c1da0d558a7": "モデル・インスタンスがデータ・ソースに存在するかどうか検査します。", - "e92aa25b6b864e3454b65a7c422bd114": "一括更新に失敗しました。コネクターは、予期しない数のレコードを削除しました: {0}", - "ea63d226b6968e328bdf6876010786b5": "一括更新を適用できません。削除レコードの数がコネクターによって正しく報告されません。", + "e43e320a435ec1fa07648c1da0d558a7": "データ・ソースにモデル・インスタンスが存在するかどうかを確認します。", + "e92aa25b6b864e3454b65a7c422bd114": "一括更新が失敗しました。コネクターは予期しない数のレコードを削除しました: {0}", + "ea63d226b6968e328bdf6876010786b5": "一括更新を適用できません。コネクターは削除されたレコードの数を正しく報告していません。", "f1d4ac54357cc0932f385d56814ba7e4": "競合", - "f37d94653793e33f4203e45b4a1cf106": "データ・ソースから where と一致するモデルのインスタンスをカウントします。", - "0731d0109e46c21a4e34af3346ed4856": "この動作は、次のメジャー・バージョンで変わる可能性があります。", - "2e110abee2c95bcfc2dafd48be7e2095": "{0} を構成できません: {{config.dataSource}} は {{DataSource}} のインスタンスでなければなりません。 ", - "308e1d484516a33df788f873e65faaff": "モデル `{0}` は、非推奨の「DataModel」を拡張しています。代わりに「PersistedModel」を使用してください。", - "3438fab56cc7ab92dfd88f0497e523e0": "`{0}` 構成の relations プロパティーはオブジェクトでなければなりません", - "4cac5f051ae431321673e04045d37772": "モデル `{0}` は、不明なモデル `{1}` を拡張しています。ベースとして「PersistedModel」を使用します。", - "734a7bebb65e10899935126ba63dd51f": "`{0}` 構成の options プロパティーはオブジェクトでなければなりません", - "779467f467862836e19f494a37d6ab77": "`{0}` 構成の acls プロパティーはオブジェクトの配列でなければなりません", + "f37d94653793e33f4203e45b4a1cf106": "データ・ソースから where で一致するモデルのインスタンスをカウントします。", + "0731d0109e46c21a4e34af3346ed4856": "この動作は次のメジャー・バージョンで変更される可能性があります。", + "2e110abee2c95bcfc2dafd48be7e2095": "{0} を構成できません: {{config.dataSource}} は {{DataSource}} のインスタンスでなければなりません", + "308e1d484516a33df788f873e65faaff": "モデル `{0}` は非推奨の `DataModel を拡張しています。代わりに `PersistedModel` を使用してください。", + "3438fab56cc7ab92dfd88f0497e523e0": "`{0}` 構成の関係プロパティーはオブジェクトでなければなりません", + "4cac5f051ae431321673e04045d37772": "モデル `{0}` は不明のモデル `{1}` を拡張しています。ベースとして `PersistedModel` を使用します。", + "734a7bebb65e10899935126ba63dd51f": "`{0}` 構成のオプション・プロパティーはオブジェクトでなければなりません", + "779467f467862836e19f494a37d6ab77": "`{0}` 構成の ACL プロパティーはオブジェクトの配列でなければなりません", "80a32e80cbed65eba2103201a7c94710": "モデルが見つかりません: {0}", - "83cbdc2560ba9f09155ccfc63e08f1a1": "プロパティー`{0}` を `{1}` に対して再構成できません", - "97795efe0c3eb7f35ce8cf8cfe70682b": "`{0}` の構成に {{`dataSource`}} プロパティーが欠落しています。\n「null」または「false」を使用して、データ・ソースに付加されないモデルにマークを付けます。", - "a80038252430df2754884bf3c845c4cf": "\"{0}.{1}\" のリモート・メタデータに \"isStatic\" フラグが欠落しています。このメソッドはインスタンス・メソッドとして登録されています。", - "dc568bee32deb0f6eaf63e73b20e8ceb": "\"{0}\" の非オブジェクト \"methods\" 設定を無視します。", - "3aecb24fa8bdd3f79d168761ca8a6729": "不明な {{middleware}} のフェーズ {0}" + "83cbdc2560ba9f09155ccfc63e08f1a1": "`{1}` のプロパティー `{0}` を再構成できません", + "97795efe0c3eb7f35ce8cf8cfe70682b": "`{0}` の構成は {{`dataSource`}} プロパティーがありません。\nどのデータ・ソースにも付加されていないモデルにマークを付けるには `null` または `false` を使用します。", + "a80038252430df2754884bf3c845c4cf": "\"{0}.{1}\" のリモート・メタデータに「isStatic」フラグがありません。このメソッドはインスタンス・メソッドとして登録されます。", + "dc568bee32deb0f6eaf63e73b20e8ceb": "\"{0}\" の非オブジェクト「メソッド」設定を無視します。", + "3aecb24fa8bdd3f79d168761ca8a6729": "不明な {{middleware}} フェーズ {0}" } diff --git a/intl/tr/messages.json b/intl/tr/messages.json index a9a94ceeb..1d6803e74 100644 --- a/intl/tr/messages.json +++ b/intl/tr/messages.json @@ -75,8 +75,8 @@ "1bc7d8283c9abda512692925c8d8e3c0": "Geçerli denetim noktasını alır.", "1caa7cc61266e7aef7db7d2f0e27ac3e": "Bu eşgörünüm için alıkonan en son değişiklik kaydının özelliklerini günceller.", "2a7df74fe6e8462e617b79d5fbb536ea": "Bu eşgörünüme ilişkin en son değişiklik kaydını alır.", - "2a9684b3d5b3b67af74bac74eb1b0843": "Veri kaynağında modelin filtreyle eşleşen tüm eşgörünümlerini bulur.", - "2e50838caf0c927735eb15d12866bdd7": "Belirli bir denetim noktasından bu yana bir modelde yapılan değişiklikleri alır. Döndürülecek sonuç sayısını azaltmak için bir filtre nesnesi belirtin.", + "2a9684b3d5b3b67af74bac74eb1b0843": "Veri kaynağında modelin süzgeçle eşleşen tüm eşgörünümlerini bulur.", + "2e50838caf0c927735eb15d12866bdd7": "Belirli bir denetim noktasından bu yana bir modelde yapılan değişiklikleri alır. Döndürülecek sonuç sayısını azaltmak için bir süzgeç nesnesi belirtin.", "4203ab415ec66a78d3164345439ba76e": "Çağrılamıyor: {0}.{1}(). {2} yöntemi ayarlanmamış. {{PersistedModel}}, bir veri kaynağına ({{DataSource}}) doğru olarak eklenmedi!", "51ea9b6245bb5e672b236d640ca3b048": "Değişiklik özellik adı ve değeri çiftlerine ilişkin bir nesne", "55ddedd4c501348f82cb89db02ec85c1": "Model özellik adı ve değeri çiftlerine ilişkin bir nesne", @@ -89,8 +89,8 @@ "7f2fde7f0f860ead224b11ba8d75aa1c": "Fark listesinden güncelleme listesi yaratır.", "89b57e764c2267129294b07589dbfdc2": "Bir model eşgörünümünü {{id}} temelinde veri kaynağından siler.", "8bab6720ecc58ec6412358c858a53484": "Toplu güncelleme başarısız oldu, bağlayıcı beklenmeyen sayıda kaydı değiştirdi: {0}", - "a98b6cc6547706b5c6bffde0ed5fd55c": "Veri kaynağında modelin filtreyle eşleşen ilk eşgörünümünü bulur.", - "c46d4aba1f14809c16730faa46933495": "Filtre tanımlayan alanlar ve include", + "a98b6cc6547706b5c6bffde0ed5fd55c": "Veri kaynağında modelin süzgeçle eşleşen ilk eşgörünümünü bulur.", + "c46d4aba1f14809c16730faa46933495": "Süzgeç tanımlayan alanlar ve include", "c65600640f206f585d300b4bcb699d95": "Denetim noktası yaratır.", "cf64c7afc74d3a8120abcd028f98c770": "Var olan bir model eşgörünümünü günceller ya da veri kaynağına yeni model eşgörünümü ekler.", "dcb6261868ff0a7b928aa215b07d068c": "Değişiklik akışı yaratır.", @@ -101,7 +101,7 @@ "f37d94653793e33f4203e45b4a1cf106": "Veri kaynağında, modelin where ile eşleşen eşgörünümlerini sayar.", "0731d0109e46c21a4e34af3346ed4856": "Bu davranış sonraki ana sürümde değişebilir.", "2e110abee2c95bcfc2dafd48be7e2095": "{0} yapılandırılamıyor: {{config.dataSource}}, bir {{DataSource}} eşgörünümü olmalıdır", - "308e1d484516a33df788f873e65faaff": "`{0}` modeli kullanımdan kaldırılmış şu modeli genişletiyor: `DataModel. Onun yerine `PersistedModel` modelini kullanın.", + "308e1d484516a33df788f873e65faaff": "`{0}` modeli kullanım dışı bırakılmış şu modeli genişletiyor: `DataModel. Onun yerine `PersistedModel` modelini kullanın.", "3438fab56cc7ab92dfd88f0497e523e0": "`{0}` yapılandırmasının ilişkiler (relations) özelliği bir nesne olmalıdır", "4cac5f051ae431321673e04045d37772": "`{0}` modeli, bilinmeyen `{1}` modelini genişletiyor. Temel olarak 'PersistedModel' kullanılıyor.", "734a7bebb65e10899935126ba63dd51f": "`{0}` yapılandırmasının seçenekler (options) özelliği bir nesne olmalıdır.", From bdeaf654fac1fac527f9cb9d8ccede747bc1bf26 Mon Sep 17 00:00:00 2001 From: Loay Date: Tue, 27 Sep 2016 09:16:08 -0400 Subject: [PATCH 098/187] Validate non-email property partial update --- common/models/user.js | 2 ++ test/user.test.js | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/common/models/user.js b/common/models/user.js index 1295e6313..431548d81 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -681,7 +681,9 @@ module.exports = function(User) { UserModel.observe('after save', function afterEmailUpdate(ctx, next) { if (!ctx.Model.relations.accessTokens) return next(); var AccessToken = ctx.Model.relations.accessTokens.modelTo; + if (!ctx.instance && !ctx.data) return next(); var newEmail = (ctx.instance || ctx.data).email; + if (!newEmail) return next(); if (!ctx.hookState.originalUserData) return next(); var idsToExpire = ctx.hookState.originalUserData.filter(function(u) { return u.email !== newEmail; diff --git a/test/user.test.js b/test/user.test.js index b17abc5f4..1e6ae0287 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -1972,6 +1972,43 @@ describe('User', function() { ], done); }); + it('keeps sessions AS IS if non-email property is changed using updateAll', function(done) { + var userPartial; + async.series([ + function createPartialUser(next) { + User.create( + { email: 'partial@example.com', password: 'pass1', age: 25 }, + function(err, partialInstance) { + if (err) return next(err); + userPartial = partialInstance; + next(); + }); + }, + function loginPartiallUser(next) { + User.login({ email: 'partial@example.com', password: 'pass1' }, function(err, ats) { + if (err) return next(err); + next(); + }); + }, + function updatePartialUser(next) { + User.updateAll( + { id: userPartial.id }, + { age: userPartial.age + 1 }, + function(err, info) { + if (err) return next(err); + next(); + }); + }, + function verifyTokensOfPartialUser(next) { + AccessToken.find({ where: { userId: userPartial.id }}, function(err, tokens1) { + if (err) return next(err); + expect(tokens1.length).to.equal(1); + next(); + }); + }, + ], done); + }); + function assertPreservedToken(done) { AccessToken.find({ where: { userId: user.id }}, function(err, tokens) { if (err) return done(err); From b8b92fbeda9770a244300b9e35510119181d18da Mon Sep 17 00:00:00 2001 From: Tim van der Staaij Date: Thu, 15 Sep 2016 23:33:56 +0200 Subject: [PATCH 099/187] Fix support for remote hooks returning a Promise Fix beforeRemote/afterRemote to correctly return promises returned by the user-provided hook callback. --- lib/model.js | 4 ++-- test/model.test.js | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/model.js b/lib/model.js index f3fd0bb71..a85f4e2f2 100644 --- a/lib/model.js +++ b/lib/model.js @@ -202,7 +202,7 @@ module.exports = function(registry) { this._runWhenAttachedToApp(function(app) { var remotes = app.remotes(); remotes.before(className + '.' + name, function(ctx, next) { - fn(ctx, ctx.result, next); + return fn(ctx, ctx.result, next); }); }); }; @@ -213,7 +213,7 @@ module.exports = function(registry) { this._runWhenAttachedToApp(function(app) { var remotes = app.remotes(); remotes.after(className + '.' + name, function(ctx, next) { - fn(ctx, ctx.result, next); + return fn(ctx, ctx.result, next); }); }); }; diff --git a/test/model.test.js b/test/model.test.js index f7c8d079a..87d1c5fb7 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -10,6 +10,7 @@ var loopback = require('../'); var ACL = loopback.ACL; var defineModelTestsWithDataSource = require('./util/model-tests'); var PersistedModel = loopback.PersistedModel; +var Promise = require('bluebird'); var sinonChai = require('sinon-chai'); chai.use(sinonChai); @@ -308,6 +309,32 @@ describe.onServer('Remote Methods', function() { done(); }); }); + + it('Does not stop the hook chain after returning a promise', function(done) { + var hooksCalled = []; + + User.beforeRemote('create', function() { + hooksCalled.push('first'); + return Promise.resolve(); + }); + + User.beforeRemote('create', function(ctx, user, next) { + hooksCalled.push('second'); + next(); + }); + + // invoke save + request(app) + .post('/users') + .send({ data: { first: 'foo', last: 'bar' }}) + .expect('Content-Type', /json/) + .expect(200) + .end(function(err, res) { + if (err) return done(err); + expect(hooksCalled).to.eql(['first', 'second']); + done(); + }); + }); }); describe('Model.afterRemote(name, fn)', function() { From 6d08df4f0a8cf73d23eb2768b983b5b3db917bf8 Mon Sep 17 00:00:00 2001 From: Candy Date: Thu, 6 Oct 2016 14:17:01 -0400 Subject: [PATCH 100/187] Update ja and nl translation files --- intl/ja/messages.json | 12 ++++++------ intl/nl/messages.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/intl/ja/messages.json b/intl/ja/messages.json index a3b8c5f06..29ac69882 100644 --- a/intl/ja/messages.json +++ b/intl/ja/messages.json @@ -6,7 +6,7 @@ "5858e63efaa0e4ad86b61c0459ea32fa": "{{Email}} モデルを {{Mail}} コネクターに接続する必要があります", "0caffe1d763c8cca6a61814abe33b776": "E メールは必須です", "1b2a6076dccbe91a56f1672eb3b8598c": "応答本文には、ログイン時に作成された {{AccessToken}} のプロパティーが含まれます。\n`include` パラメーターの値によっては、本文に追加のプロパティーが含まれる場合があります:\n\n - `user` - `U+007BUserU+007D` - 現在ログインしているユーザーのデータ。 {{(`include=user`)}}\n\n", - "2362ba55796c733e337af169922327a2": "応答に組み込む関連オブジェクト。詳細については、戻り値の説明を参照してください。", + "2362ba55796c733e337af169922327a2": "応答に組み込む関連オブジェクト。 詳細については、戻り値の説明を参照してください。", "306999d39387d87b2638199ff0bed8ad": "E メールを使用してユーザーのパスワードをリセットします。", "3aae63bb7e8e046641767571c1591441": "E メールが検証されていないため、ログインに失敗しました", "3caaa84fc103d6d5612173ae6d43b245": "無効なトークン: {0}", @@ -38,7 +38,7 @@ "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} は削除されました。代わりに新規モジュール {{loopback-boot}} を使用してください", "1d7833c3ca2f05fdad8fad7537531c40": "\t 件名:{0}", "275f22ab95671f095640ca99194b7635": "\t 送信元:{0}", - "3d63008ccfb2af1db2142e8cc2716ace": "警告: E メール送信用の E メール・トランスポートが指定されていません。メール・メッセージを送信するためのトランスポートをセットアップしてください。", + "3d63008ccfb2af1db2142e8cc2716ace": "警告: E メール送信用の E メール・トランスポートが指定されていません。 メール・メッセージを送信するためのトランスポートをセットアップしてください。", "4a4f04a4e480fc5d4ee73b84d9a4b904": "メールの送信:", "63a091ced88001ab6acb58f61ec041c5": "\t テキスト:{0}", "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", @@ -71,13 +71,13 @@ "0f1c71f74b040bfbe8d384a414e31f03": "指定されたチェックポイント以降の一連の差分および競合を取得します。", "15254dec061d023d6c030083a0cef50f": "モデルの新規インスタンスを作成し、それをデータ・ソースに保管します。", "16a11368d55b85a209fc6aea69ee5f7a": "一致するすべてのレコードを削除します。", - "1bc1d489ddf347af47af3d9b1fc7cc15": "一度に複数の更新を実行します。注: これはアトミックではありません。", + "1bc1d489ddf347af47af3d9b1fc7cc15": "一度に複数の更新を実行します。 注: これはアトミックではありません。", "1bc7d8283c9abda512692925c8d8e3c0": "現在のチェックポイントを取得します。", "1caa7cc61266e7aef7db7d2f0e27ac3e": "このインスタンスについて保持されている最新の変更レコードのプロパティーを更新します。", "2a7df74fe6e8462e617b79d5fbb536ea": "このインスタンスの最新の変更レコードを取得します。", "2a9684b3d5b3b67af74bac74eb1b0843": "データ・ソースからフィルターで一致するモデルのすべてのインスタンスを検索します。", "2e50838caf0c927735eb15d12866bdd7": "指定されたチェックポイント以降のモデルへの変更を取得します。フィルター・オブジェクトを指定すると、返される結果の件数が削減されます。", - "4203ab415ec66a78d3164345439ba76e": "{0}.{1}() を呼び出せません。{2} メソッドがセットアップされていません。{{PersistedModel}} は {{DataSource}} に正しく付加されていません。", + "4203ab415ec66a78d3164345439ba76e": "{0}.{1}() を呼び出せません。 {2} メソッドがセットアップされていません。 {{PersistedModel}} は {{DataSource}} に正しく付加されていません。", "51ea9b6245bb5e672b236d640ca3b048": "変更プロパティー名/値ペアのオブジェクト", "55ddedd4c501348f82cb89db02ec85c1": "モデル・プロパティー名/値ペアのオブジェクト", "5aaa76c72ae1689fd3cf62589784a4ba": "モデル・インスタンスの属性を更新し、それをデータ・ソースに保管します。", @@ -101,9 +101,9 @@ "f37d94653793e33f4203e45b4a1cf106": "データ・ソースから where で一致するモデルのインスタンスをカウントします。", "0731d0109e46c21a4e34af3346ed4856": "この動作は次のメジャー・バージョンで変更される可能性があります。", "2e110abee2c95bcfc2dafd48be7e2095": "{0} を構成できません: {{config.dataSource}} は {{DataSource}} のインスタンスでなければなりません", - "308e1d484516a33df788f873e65faaff": "モデル `{0}` は非推奨の `DataModel を拡張しています。代わりに `PersistedModel` を使用してください。", + "308e1d484516a33df788f873e65faaff": "モデル `{0}` は非推奨の `DataModel を拡張しています。 代わりに `PersistedModel` を使用してください。", "3438fab56cc7ab92dfd88f0497e523e0": "`{0}` 構成の関係プロパティーはオブジェクトでなければなりません", - "4cac5f051ae431321673e04045d37772": "モデル `{0}` は不明のモデル `{1}` を拡張しています。ベースとして `PersistedModel` を使用します。", + "4cac5f051ae431321673e04045d37772": "モデル `{0}` は不明のモデル `{1}` を拡張しています。 ベースとして `PersistedModel` を使用します。", "734a7bebb65e10899935126ba63dd51f": "`{0}` 構成のオプション・プロパティーはオブジェクトでなければなりません", "779467f467862836e19f494a37d6ab77": "`{0}` 構成の ACL プロパティーはオブジェクトの配列でなければなりません", "80a32e80cbed65eba2103201a7c94710": "モデルが見つかりません: {0}", diff --git a/intl/nl/messages.json b/intl/nl/messages.json index 4a168cbfc..b5fdd6db4 100644 --- a/intl/nl/messages.json +++ b/intl/nl/messages.json @@ -77,7 +77,7 @@ "2a7df74fe6e8462e617b79d5fbb536ea": "Het meest recente wijzigingsrecord voor deze instance ophalen.", "2a9684b3d5b3b67af74bac74eb1b0843": "Zoeken naar alle instances van model dat overeenkomt met filter uit gegevensbron.", "2e50838caf0c927735eb15d12866bdd7": "Wijzigingen die zijn aangebracht op een model sinds een bepaald checkpoint ophalen. Geef een filterobject op om het aantal resultaten te beperken.", - "4203ab415ec66a78d3164345439ba76e": "Kan {0}.{1}() niet aanroepen. De methode {2} is niet geconfigureerd. De {{PersistedModel}} is niet correct gekoppeld aan een {{DataSource}}!", + "4203ab415ec66a78d3164345439ba76e": "{0} kan niet worden aangeroepen. {1}(). De methode {2} is niet geconfigureerd. De {{PersistedModel}} is niet correct gekoppeld aan een {{DataSource}}!", "51ea9b6245bb5e672b236d640ca3b048": "Een object van Combinaties van eigenschapnaam/waarde wijzigen", "55ddedd4c501348f82cb89db02ec85c1": "Een object van combinaties van eigenschapnaam/waarde", "5aaa76c72ae1689fd3cf62589784a4ba": "Kenmerken voor modelinstance bijwerken en bewaren in gegevensbron.", From b3497c6778ff23c6267d97529833ce92238fd872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 10 Oct 2016 13:27:22 +0200 Subject: [PATCH 101/187] Allow tokens with eternal TTL (value -1) - Add a new User setting 'allowEternalTokens' - Enhance 'AccessToken.validate' to support eternal tokens with ttl value -1 when the user model allows it. --- common/models/access-token.js | 10 +++++++- test/access-token.test.js | 48 ++++++++++++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/common/models/access-token.js b/common/models/access-token.js index ae75051f3..707ba4cec 100644 --- a/common/models/access-token.js +++ b/common/models/access-token.js @@ -149,11 +149,19 @@ module.exports = function(AccessToken) { assert(this.ttl, 'token.ttl must exist'); assert(this.ttl >= -1, 'token.ttl must be >= -1'); + var AccessToken = this.constructor; + var userRelation = AccessToken.relations.user; // may not be set up + var User = userRelation && userRelation.modelTo; + var now = Date.now(); var created = this.created.getTime(); var elapsedSeconds = (now - created) / 1000; var secondsToLive = this.ttl; - var isValid = elapsedSeconds < secondsToLive; + var eternalTokensAllowed = !!(User && User.settings.allowEternalTokens); + var isEternalToken = secondsToLive === -1; + var isValid = isEternalToken ? + eternalTokensAllowed : + elapsedSeconds < secondsToLive; if (isValid) { cb(null, isValid); diff --git a/test/access-token.test.js b/test/access-token.test.js index 917994da0..e86feca99 100644 --- a/test/access-token.test.js +++ b/test/access-token.test.js @@ -355,11 +355,38 @@ describe('AccessToken', function() { assert(Object.prototype.toString.call(this.token.created), '[object Date]'); }); - it('should be validateable', function(done) { - this.token.validate(function(err, isValid) { - assert(isValid); + describe('.validate()', function() { + it('accepts valid tokens', function(done) { + this.token.validate(function(err, isValid) { + assert(isValid); + done(); + }); + }); - done(); + it('rejects eternal TTL by default', function(done) { + this.token.ttl = -1; + this.token.validate(function(err, isValid) { + if (err) return done(err); + expect(isValid, 'isValid').to.equal(false); + done(); + }); + }); + + it('allows eternal tokens when enabled by User.allowEternalTokens', + function(done) { + var Token = givenLocalTokenModel(); + + // Overwrite User settings - enable eternal tokens + Token.app.models.User.settings.allowEternalTokens = true; + + Token.create({ userId: '123', ttl: -1 }, function(err, token) { + if (err) return done(err); + token.validate(function(err, isValid) { + if (err) return done(err); + expect(isValid, 'isValid').to.equal(true); + done(); + }); + }); }); }); @@ -622,3 +649,16 @@ function createTestApp(testToken, settings, done) { return app; } + +function givenLocalTokenModel() { + var app = loopback({ localRegistry: true, loadBuiltinModels: true }); + app.dataSource('db', { connector: 'memory' }); + + var User = app.registry.getModel('User'); + app.model(User, { dataSource: 'db' }); + + var Token = app.registry.getModel('AccessToken'); + app.model(Token, { dataSource: 'db' }); + + return Token; +} From bf5c206bd612e3ab15bd177afcdade4646e15ddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 11 Oct 2016 16:31:57 +0200 Subject: [PATCH 102/187] Fix description of updateAll response Correctly describe the first non-error callback arg as an `info` object containing a `count` property. --- lib/persisted-model.js | 13 +++++++++---- test/remoting.integration.js | 6 ++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/persisted-model.js b/lib/persisted-model.js index 3e4530e6f..ccbe2596e 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -795,10 +795,15 @@ module.exports = function(registry) { description: 'An object of model property name/value pairs'}, ], returns: { - arg: 'count', - description: 'The number of instances updated', - type: 'object', - root: true + arg: 'info', + description: 'Information related to the outcome of the operation', + type: { + count: { + type: 'number', + description: 'The number of instances updated', + }, + }, + root: true, }, http: {verb: 'post', path: '/update'} }); diff --git a/test/remoting.integration.js b/test/remoting.integration.js index 41ec366e5..f881ca4e7 100644 --- a/test/remoting.integration.js +++ b/test/remoting.integration.js @@ -269,6 +269,12 @@ function formatReturns(m) { return ''; } var type = returns[0].type; + + // handle anonymous type definitions, e.g + // { arg: 'info', type: { count: 'number' } } + if (typeof type === 'object' && !Array.isArray(type)) + type = 'object'; + return type ? ':' + type : ''; } From f72fb69cfea6bf378e00b1cb995e869d2b06c482 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Wed, 12 Oct 2016 17:08:56 -0700 Subject: [PATCH 103/187] Use GitHub issue templates (#2810) (#2852) Backport of #2810 --- .github/ISSUE_TEMPLATE.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..483d047f5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,35 @@ + + +### Bug or feature request + +- [ ] Bug +- [ ] Feature request + +### Description of feature (or steps to reproduce if bug) + + + +### Link to sample repo to reproduce issue (if bug) + + + +### Expected result + + + +### Actual result (if bug) + + + +### Additional information (Node.js version, LoopBack version, etc) + + From 81c951f2120990a24a38fbdbee6c904fe8ea535c Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Wed, 12 Oct 2016 17:27:22 -0700 Subject: [PATCH 104/187] Add how to tick checkbox in issue template (#2851) (#2853) Many people are putting "*" instead of x, which causes the markdown to render funny. Adding instructions to that particular section. Backport of #2851 --- .github/ISSUE_TEMPLATE.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 483d047f5..266ed0ebe 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -11,6 +11,10 @@ ### Bug or feature request + + - [ ] Bug - [ ] Feature request From 446d2a50780f263ada62b8673c93a7ef1434dea1 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Wed, 12 Oct 2016 18:03:09 -0700 Subject: [PATCH 105/187] Reword ticking checkbox note in issue template (#2855) Backport of #2854 --- .github/ISSUE_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 266ed0ebe..4ec275ddd 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -12,7 +12,7 @@ ### Bug or feature request - [ ] Bug From 060630aad6f38b3740c5cdd7ea3445054ba52f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 13 Oct 2016 10:12:11 +0200 Subject: [PATCH 106/187] 2.35.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Reword ticking checkbox note in issue template (#2855) (Simon Ho) * Add how to tick checkbox in issue template (#2851) (#2853) (Simon Ho) * Use GitHub issue templates (#2810) (#2852) (Simon Ho) * Allow tokens with eternal TTL (value -1) (Miroslav Bajtoš) * Update ja and nl translation files (Candy) * Fix support for remote hooks returning a Promise (Tim van der Staaij) * Validate non-email property partial update (Loay) * Update translation files - round#2 (Candy) * Update tests to use registry for model creation (gunjpan) * Call new disable remote method from model class. (Richard Pringle) * Temporarily disable Karma tests on Windows CI (Miroslav Bajtoš) * Add translation files for 2.x (Candy) * Allow resetPassword if email is verified (Loay) * Add docs for KeyValue model (Simon Ho) * Invalidate sessions after email change (Loay) * Upgrade loopback-testing to the latest ^1.4 (Miroslav Bajtoš) --- CHANGES.md | 36 ++++++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 50d67eb0d..de84ae4c4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,39 @@ +2016-10-13, Version 2.35.0 +========================== + + * Reword ticking checkbox note in issue template (#2855) (Simon Ho) + + * Add how to tick checkbox in issue template (#2851) (#2853) (Simon Ho) + + * Use GitHub issue templates (#2810) (#2852) (Simon Ho) + + * Allow tokens with eternal TTL (value -1) (Miroslav Bajtoš) + + * Update ja and nl translation files (Candy) + + * Fix support for remote hooks returning a Promise (Tim van der Staaij) + + * Validate non-email property partial update (Loay) + + * Update translation files - round#2 (Candy) + + * Update tests to use registry for model creation (gunjpan) + + * Call new disable remote method from model class. (Richard Pringle) + + * Temporarily disable Karma tests on Windows CI (Miroslav Bajtoš) + + * Add translation files for 2.x (Candy) + + * Allow resetPassword if email is verified (Loay) + + * Add docs for KeyValue model (Simon Ho) + + * Invalidate sessions after email change (Loay) + + * Upgrade loopback-testing to the latest ^1.4 (Miroslav Bajtoš) + + 2016-09-13, Version 2.34.1 ========================== diff --git a/package.json b/package.json index 88cff25de..2fc001a66 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.34.1", + "version": "2.35.0", "description": "LoopBack: Open Source Framework for Node.js", "homepage": "http://loopback.io/", "keywords": [ From f97906d3975b25f3dcd9af8950166b5232f412b2 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Mon, 17 Oct 2016 09:02:24 -0700 Subject: [PATCH 107/187] Add pull request template (#2843) (#2862) Backport of #2843 --- .github/PULL_REQUEST_TEMPLATE.md | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..d35410be3 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,34 @@ + + +### Type of change + + + +- [ ] SEMVER-MAJOR -- Backwards-incompatible changes +- [ ] SEMVER-MINOR -- Add functionality in a backwards-compatible manner +- [ ] SEMVER-PATCH -- Backwards-compatible bug fixes + +### Description of change(s) + + + +### Link(s) to related GitHub issues + + + +### Checklist + +- [ ] CLA signed +- [ ] Commit message conforms with the [Git commit message + guidelines](http://loopback.io/doc/en/contrib/git-commit-messages.html) +- [ ] New tests are added to cover all changes +- [ ] Code passes `npm test` +- [ ] Code conforms with the [style + guide](http://loopback.io/doc/en/contrib/style-guide.html) +- [ ] All CI builds are passing (semi-required, left up to discretion of the + reviewer) From 809ba35fdb70a857bfc84958a04dea79c15eb19b Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Tue, 18 Oct 2016 23:53:43 -0700 Subject: [PATCH 108/187] Refactor PR template based on feedback (#2865) (#2874) Updated based on feedback received during sprint demo. Backport of #2865 --- .github/PULL_REQUEST_TEMPLATE.md | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d35410be3..ab590509c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,34 +1,14 @@ - +### Description -### Type of change - - - -- [ ] SEMVER-MAJOR -- Backwards-incompatible changes -- [ ] SEMVER-MINOR -- Add functionality in a backwards-compatible manner -- [ ] SEMVER-PATCH -- Backwards-compatible bug fixes - -### Description of change(s) - - - -### Link(s) to related GitHub issues +#### Related issues (eg. strongloop/loopback#49) +- None ### Checklist - [ ] CLA signed -- [ ] Commit message conforms with the [Git commit message - guidelines](http://loopback.io/doc/en/contrib/git-commit-messages.html) - [ ] New tests are added to cover all changes -- [ ] Code passes `npm test` - [ ] Code conforms with the [style guide](http://loopback.io/doc/en/contrib/style-guide.html) -- [ ] All CI builds are passing (semi-required, left up to discretion of the - reviewer) +- [ ] All CI builds are passing From df13b094bb7a724b277751004674ffdb134d2953 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Wed, 19 Oct 2016 18:25:04 -0700 Subject: [PATCH 109/187] Remove redundant items in PR template (#2877) (#2878) - Remove sign CLA since CI already shows unsigned CLAs - Remove all tests must pass CI since we have to check each on a case by case basis anyways - Update example to include linking to the current repo using number sign (ie. #49 in addition to org/repo#49) Backport of #2877 --- .github/PULL_REQUEST_TEMPLATE.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ab590509c..b10885d27 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,14 +1,12 @@ ### Description -#### Related issues (eg. strongloop/loopback#49) +#### Related issues (eg. #49 or strongloop/loopback#49) - None ### Checklist -- [ ] CLA signed - [ ] New tests are added to cover all changes - [ ] Code conforms with the [style guide](http://loopback.io/doc/en/contrib/style-guide.html) -- [ ] All CI builds are passing From 3e0fd94f6066fb4e2313ab601f9d0d4bb0a1b7b9 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Fri, 21 Oct 2016 16:31:33 -0700 Subject: [PATCH 110/187] Need index on principalId for performance. (#2883) (#2884) Backport of #2883 --- common/models/role-mapping.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/common/models/role-mapping.json b/common/models/role-mapping.json index 592f29064..732e24af3 100644 --- a/common/models/role-mapping.json +++ b/common/models/role-mapping.json @@ -11,7 +11,10 @@ "type": "string", "description": "The principal type, such as user, application, or role" }, - "principalId": "string" + "principalId": { + "type": "string", + "index": true + } }, "relations": { "role": { From 4cb9f0d74d61b891a331dcaa39c70d9091f4dfca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 24 Oct 2016 10:40:26 +0200 Subject: [PATCH 111/187] 2.36.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Need index on principalId for performance. (#2883) (#2884) (Simon Ho) * Remove redundant items in PR template (#2877) (#2878) (Simon Ho) * Refactor PR template based on feedback (#2865) (#2874) (Simon Ho) * Add pull request template (#2843) (#2862) (Simon Ho) * Fix description of updateAll response (Miroslav Bajtoš) --- CHANGES.md | 14 ++++++++++++++ package.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index de84ae4c4..f1b7b5ad1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,17 @@ +2016-10-24, Version 2.36.0 +========================== + + * Need index on principalId for performance. (#2883) (#2884) (Simon Ho) + + * Remove redundant items in PR template (#2877) (#2878) (Simon Ho) + + * Refactor PR template based on feedback (#2865) (#2874) (Simon Ho) + + * Add pull request template (#2843) (#2862) (Simon Ho) + + * Fix description of updateAll response (Miroslav Bajtoš) + + 2016-10-13, Version 2.35.0 ========================== diff --git a/package.json b/package.json index 2fc001a66..794548a30 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.35.0", + "version": "2.36.0", "description": "LoopBack: Open Source Framework for Node.js", "homepage": "http://loopback.io/", "keywords": [ From f80b27880eb62161aa39f31a2b78d657227726ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 25 Oct 2016 02:08:40 +0200 Subject: [PATCH 112/187] Fix PR template to not link all PRs to #49 (#2887) - Add comment with syntax examples - Remove direct links to #49 in section header Backport of #2887 --- .github/PULL_REQUEST_TEMPLATE.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b10885d27..58c8d7b2d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,14 @@ ### Description -#### Related issues (eg. #49 or strongloop/loopback#49) +#### Related issues + + - None From 6e880137e427f6c6d9c052c442357add5163b1c8 Mon Sep 17 00:00:00 2001 From: Dhaval Trivedi Date: Sun, 26 Jun 2016 17:07:00 +0530 Subject: [PATCH 113/187] adding check of string for case insensitive emails --- common/models/user.js | 3 ++- test/user.test.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/common/models/user.js b/common/models/user.js index 431548d81..8b8156a14 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -649,7 +649,8 @@ module.exports = function(User) { // Access token to normalize email credentials UserModel.observe('access', function normalizeEmailCase(ctx, next) { - if (!ctx.Model.settings.caseSensitiveEmail && ctx.query.where && ctx.query.where.email) { + if (!ctx.Model.settings.caseSensitiveEmail && ctx.query.where && + ctx.query.where.email && typeof(ctx.query.where.email) === 'string') { ctx.query.where.email = ctx.query.where.email.toLowerCase(); } next(); diff --git a/test/user.test.js b/test/user.test.js index 1e6ae0287..62c109052 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -468,6 +468,36 @@ describe('User', function() { done(); }); }); + + it('Should be able to use query filters (email case-sensitivity off)', function(done) { + User.settings.caseSensitiveEmail = false; + var insensitiveUser = { email: 'insensitive@example.com', password: 'abc' }; + User.create(insensitiveUser, function(err, user) { + User.find({ where: { email: + { inq: [insensitiveUser.email] }, + }}, function(err, result) { + if (err) done(err); + assert(result[0], 'The query did not find the user'); + assert.equal(result[0].email, insensitiveUser.email); + done(); + }); + }); + }); + + it('Should be able to use query filters (email case-sensitivity on)', function(done) { + User.settings.caseSensitiveEmail = true; + var sensitiveUser = { email: 'senSiTive@example.com', password: 'abc' }; + User.create(sensitiveUser, function(err, user) { + User.find({ where: { email: + { inq: [sensitiveUser.email] }, + }}, function(err, result) { + if (err) done(err); + assert(result[0], 'The query did not find the user'); + assert.equal(result[0].email, sensitiveUser.email); + done(); + }); + }); + }); }); describe('User.login', function() { From 67e5c6ec1ec6cf450c1a1413c4b7749b542f290e Mon Sep 17 00:00:00 2001 From: Loay Date: Tue, 20 Sep 2016 11:10:18 -0400 Subject: [PATCH 114/187] Require verification after email change When the User model is configured to require email verification, then any change of the email address should trigger re-verification. --- common/models/user.js | 14 ++++++++++ test/user.test.js | 64 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/common/models/user.js b/common/models/user.js index 8b8156a14..3c010c605 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -667,6 +667,7 @@ module.exports = function(User) { // Delete old sessions once email is updated UserModel.observe('before save', function beforeEmailUpdate(ctx, next) { + var emailChanged; if (ctx.isNewInstance) return next(); if (!ctx.where && !ctx.instance) return next(); var where = ctx.where || { id: ctx.instance.id }; @@ -675,6 +676,19 @@ module.exports = function(User) { ctx.hookState.originalUserData = userInstances.map(function(u) { return { id: u.id, email: u.email }; }); + if (ctx.instance) { + emailChanged = ctx.instance.email !== ctx.hookState.originalUserData[0].email; + if (emailChanged && ctx.Model.settings.emailVerificationRequired) { + ctx.instance.emailVerified = false; + } + } else { + emailChanged = ctx.hookState.originalUserData.some(function(data) { + return data.email != ctx.data.email; + }); + if (emailChanged && ctx.Model.settings.emailVerificationRequired) { + ctx.data.emailVerified = false; + } + } next(); }); }); diff --git a/test/user.test.js b/test/user.test.js index 62c109052..10eb04a06 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -2188,6 +2188,70 @@ describe('User', function() { }); }); + describe('Verification after updating email', function() { + var NEW_EMAIL = 'updated@example.com'; + var userInstance; + + beforeEach(createOriginalUser); + + it('sets verification to false after email update if verification is required', + function(done) { + User.settings.emailVerificationRequired = true; + async.series([ + function updateUser(next) { + userInstance.updateAttribute('email', NEW_EMAIL, function(err, info) { + if (err) return next(err); + assert.equal(info.email, NEW_EMAIL); + next(); + }); + }, + function findUser(next) { + User.findById(userInstance.id, function(err, info) { + if (err) return next(err); + assert.equal(info.email, NEW_EMAIL); + assert.equal(info.emailVerified, false); + next(); + }); + }, + ], done); + }); + + it('leaves verification as is after email update if verification is not required', + function(done) { + User.settings.emailVerificationRequired = false; + async.series([ + function updateUser(next) { + userInstance.updateAttribute('email', NEW_EMAIL, function(err, info) { + if (err) return next(err); + assert.equal(info.email, NEW_EMAIL); + next(); + }); + }, + function findUser(next) { + User.findById(userInstance.id, function(err, info) { + if (err) return next(err); + assert.equal(info.email, NEW_EMAIL); + assert.equal(info.emailVerified, true); + next(); + }); + }, + ], done); + }); + + function createOriginalUser(done) { + var userData = { + email: 'original@example.com', + password: 'bar', + emailVerified: true, + }; + User.create(userData, function(err, instance) { + if (err) return done(err); + userInstance = instance; + done(); + }); + } + }); + describe('password reset with/without email verification', function() { it('allows resetPassword by email if email verification is required and done', function(done) { From bc923bd7813a2123ae87fe957626ab073af1ddcc Mon Sep 17 00:00:00 2001 From: Kogulan Baskaran Date: Tue, 15 Nov 2016 13:02:23 +1100 Subject: [PATCH 115/187] Add options to bulkUpdate --- lib/persisted-model.js | 41 +++++++++++++----- test/replication.test.js | 94 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 121 insertions(+), 14 deletions(-) diff --git a/lib/persisted-model.js b/lib/persisted-model.js index ccbe2596e..36cb11b61 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -1075,7 +1075,7 @@ module.exports = function(registry) { * * @param {Number} [since] Since this checkpoint * @param {Model} targetModel Target this model class - * @param {Object} [options] + * @param {Object} [options] An optional options object to pass to underlying data-access calls. * @param {Object} [options.filter] Replicate models that match this filter * @callback {Function} [callback] Callback function called with `(err, conflicts)` arguments. * @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object). @@ -1100,6 +1100,10 @@ module.exports = function(registry) { since = { source: since, target: since }; } + if (typeof options === 'function') { + options = {}; + } + options = options || {}; var sourceModel = this; @@ -1214,7 +1218,7 @@ module.exports = function(registry) { function bulkUpdate(_updates, cb) { debug('\tstarting bulk update'); updates = _updates; - targetModel.bulkUpdate(updates, function(err) { + targetModel.bulkUpdate(updates, options, function(err) { var conflicts = err && err.details && err.details.conflicts; if (conflicts && err.statusCode == 409) { diff.conflicts = conflicts; @@ -1328,15 +1332,28 @@ module.exports = function(registry) { * **Note: this is not atomic** * * @param {Array} updates An updates list, usually from [createUpdates()](#persistedmodel-createupdates). + * @param {Object} [options] An optional options object to pass to underlying data-access calls. * @param {Function} callback Callback function. */ - PersistedModel.bulkUpdate = function(updates, callback) { + PersistedModel.bulkUpdate = function(updates, options, callback) { var tasks = []; var Model = this; var Change = this.getChangeModel(); var conflicts = []; + var lastArg = arguments[arguments.length - 1]; + + if (typeof lastArg === 'function' && arguments.length > 1) { + callback = lastArg; + } + + if (typeof options === 'function') { + options = {}; + } + + options = options || {}; + buildLookupOfAffectedModelData(Model, updates, function(err, currentMap) { if (err) return callback(err); @@ -1346,18 +1363,18 @@ module.exports = function(registry) { switch (update.type) { case Change.UPDATE: tasks.push(function(cb) { - applyUpdate(Model, id, current, update.data, update.change, conflicts, cb); + applyUpdate(Model, id, current, update.data, update.change, conflicts, options, cb); }); break; case Change.CREATE: tasks.push(function(cb) { - applyCreate(Model, id, current, update.data, update.change, conflicts, cb); + applyCreate(Model, id, current, update.data, update.change, conflicts, options, cb); }); break; case Change.DELETE: tasks.push(function(cb) { - applyDelete(Model, id, current, update.change, conflicts, cb); + applyDelete(Model, id, current, update.change, conflicts, options, cb); }); break; } @@ -1391,7 +1408,7 @@ module.exports = function(registry) { }); } - function applyUpdate(Model, id, current, data, change, conflicts, cb) { + function applyUpdate(Model, id, current, data, change, conflicts, options, cb) { var Change = Model.getChangeModel(); var rev = current ? Change.revisionForInst(current) : null; @@ -1409,7 +1426,7 @@ module.exports = function(registry) { // but not included in `data` // See https://github.com/strongloop/loopback/issues/1215 - Model.updateAll(current.toObject(), data, function(err, result) { + Model.updateAll(current.toObject(), data, options, function(err, result) { if (err) return cb(err); var count = result && result.count; @@ -1444,8 +1461,8 @@ module.exports = function(registry) { }); } - function applyCreate(Model, id, current, data, change, conflicts, cb) { - Model.create(data, function(createErr) { + function applyCreate(Model, id, current, data, change, conflicts, options, cb) { + Model.create(data, options, function(createErr) { if (!createErr) return cb(); // We don't have a reliable way how to detect the situation @@ -1473,7 +1490,7 @@ module.exports = function(registry) { } } - function applyDelete(Model, id, current, change, conflicts, cb) { + function applyDelete(Model, id, current, change, conflicts, options, cb) { if (!current) { // The instance was either already deleted or not created at all, // we are done. @@ -1491,7 +1508,7 @@ module.exports = function(registry) { return Change.rectifyModelChanges(Model.modelName, [id], cb); } - Model.deleteAll(current.toObject(), function(err, result) { + Model.deleteAll(current.toObject(), options, function(err, result) { if (err) return cb(err); var count = result && result.count; diff --git a/test/replication.test.js b/test/replication.test.js index 3a9585b02..d559fd3c7 100644 --- a/test/replication.test.js +++ b/test/replication.test.js @@ -1537,6 +1537,96 @@ describe('Replication / Change APIs', function() { } }); + describe('ensure options object is set on context during bulkUpdate', function() { + var syncPropertyExists = false; + var OptionsSourceModel; + + beforeEach(function() { + OptionsSourceModel = PersistedModel.extend( + 'OptionsSourceModel-' + tid, + { id: { id: true, type: String, defaultFn: 'guid' } }, + { trackChanges: true }); + + OptionsSourceModel.attachTo(dataSource); + + OptionsSourceModel.observe('before save', function updateTimestamp(ctx, next) { + if (ctx.options.sync) { + syncPropertyExists = true; + } else { + syncPropertyExists = false; + } + next(); + }); + }); + + it('bulkUpdate should call Model updates with the provided options object', function(done) { + var testData = {name: 'Janie', surname: 'Doe'}; + var updates = [ + { + data: null, + change: null, + type: 'create' + } + ]; + + var options = { + sync: true + }; + + async.waterfall([ + function(callback) { + TargetModel.create(testData, callback); + }, + function(data, callback) { + updates[0].data = data; + TargetModel.getChangeModel().find({where: {modelId: data.id}}, callback); + }, + function(data, callback) { + updates[0].change = data; + OptionsSourceModel.bulkUpdate(updates, options, callback); + }], + function(err, result) { + if (err) return done(err); + + expect(syncPropertyExists).to.eql(true); + + done(); + } + ); + }); + }); + + describe('ensure bulkUpdate works with just 2 args', function() { + it('bulkUpdate should successfully finish without options', function(done) { + var testData = {name: 'Janie', surname: 'Doe'}; + var updates = [ + { + data: null, + change: null, + type: 'create' + } + ]; + + async.waterfall([ + function(callback) { + TargetModel.create(testData, callback); + }, + function(data, callback) { + updates[0].data = data; + TargetModel.getChangeModel().find({where: {modelId: data.id}}, callback); + }, + function(data, callback) { + updates[0].change = data; + SourceModel.bulkUpdate(updates, callback); + } + ], function(err, result) { + if (err) return done(err); + done(); + } + ); + }); + }); + var _since = {}; function replicate(source, target, since, next) { if (typeof since === 'function') { @@ -1591,14 +1681,14 @@ describe('Replication / Change APIs', function() { function setupRaceConditionInReplication(fn) { var bulkUpdate = TargetModel.bulkUpdate; - TargetModel.bulkUpdate = function(data, cb) { + TargetModel.bulkUpdate = function(data, options, cb) { // simulate the situation when a 3rd party modifies the database // while a replication run is in progress var self = this; fn(function(err) { if (err) return cb(err); - bulkUpdate.call(self, data, cb); + bulkUpdate.call(self, data, options, cb); }); // apply the 3rd party modification only once From 5c1558f969f8b682680248e0a2961614150deeaa Mon Sep 17 00:00:00 2001 From: Adrien Kiren Date: Fri, 11 Nov 2016 14:28:49 +0100 Subject: [PATCH 116/187] Add templateFn option to User#verify() --- common/models/user.js | 31 ++++++++++++++++++++++++++----- test/user.test.js | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index 3c010c605..8d1debfe4 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -365,6 +365,10 @@ module.exports = function(User) { * @property {String} text Text of email. * @property {String} template Name of template that displays verification * page, for example, `'verify.ejs'. + * @property {Function} templateFn A function generating the email HTML body + * from `verify()` options object and generated attributes like `options.verifyHref`. + * It must accept the option object and a callback function with `(err, html)` + * as parameters * @property {String} redirect Page to which user will be redirected after * they verify their email, for example `'/'` for root URI. * @property {Function} generateVerificationToken A function to be used to @@ -418,6 +422,8 @@ module.exports = function(User) { '&redirect=' + options.redirect; + options.templateFn = options.templateFn || createVerificationEmailBody; + // Email model var Email = options.mailer || this.constructor.email || registry.getModelByType(loopback.Email); @@ -452,20 +458,35 @@ module.exports = function(User) { options.headers = options.headers || {}; - var template = loopback.template(options.template); - options.html = template(options); - - Email.send(options, function(err, email) { + options.templateFn(options, function(err, html) { if (err) { fn(err); } else { - fn(null, {email: email, token: user.verificationToken, uid: user.id}); + setHtmlContentAndSend(html); } }); + + function setHtmlContentAndSend(html) { + options.html = html; + + Email.send(options, function(err, email) { + if (err) { + fn(err); + } else { + fn(null, { email: email, token: user.verificationToken, uid: user.id }); + } + }); + } } return fn.promise; }; + function createVerificationEmailBody(options, cb) { + var template = loopback.template(options.template); + var body = template(options); + cb(null, body); + } + /** * A default verification token generator which accepts the user the token is * being generated for and a callback function to indicate completion. diff --git a/test/user.test.js b/test/user.test.js index 10eb04a06..555e0acdf 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -1364,6 +1364,45 @@ describe('User', function() { }); }); + it('Verify a user\'s email address with custom template function', function(done) { + User.afterRemote('create', function(ctx, user, next) { + assert(user, 'afterRemote should include result'); + + var options = { + type: 'email', + to: user.email, + from: 'noreply@myapp.org', + redirect: '/', + protocol: ctx.req.protocol, + host: ctx.req.get('host'), + templateFn: function(options, cb) { + cb(null, 'custom template - verify url: ' + options.verifyHref); + }, + }; + + user.verify(options, function(err, result) { + assert(result.email); + assert(result.email.response); + assert(result.token); + var msg = result.email.response.toString('utf-8'); + assert(~msg.indexOf('/api/test-users/confirm')); + assert(~msg.indexOf('custom template')); + assert(~msg.indexOf('To: bar@bat.com')); + + done(); + }); + }); + + request(app) + .post('/test-users') + .expect('Content-Type', /json/) + .expect(200) + .send({ email: 'bar@bat.com', password: 'bar' }) + .end(function(err, res) { + if (err) return done(err); + }); + }); + it('Verify a user\'s email address with custom token generator', function(done) { User.afterRemote('create', function(ctx, user, next) { assert(user, 'afterRemote should include result'); From d99d608876b6f94dfe856b079847a0d6d87d2a7e Mon Sep 17 00:00:00 2001 From: codyolsen Date: Tue, 13 Sep 2016 13:43:34 -0600 Subject: [PATCH 117/187] Fix context within listByPrincipalType role method - Fix for current implimentation that returned all models that had any assigned roles. Context was not carried into listByPrincipalType, setting roleId as null. --- common/models/role.js | 7 ++-- test/role.test.js | 78 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/common/models/role.js b/common/models/role.js index c4e7e8b3e..680297967 100644 --- a/common/models/role.js +++ b/common/models/role.js @@ -80,26 +80,27 @@ module.exports = function(Role) { }; var model = relsToModels[rel]; - listByPrincipalType(model, relsToTypes[rel], query, callback); + listByPrincipalType(this, model, relsToTypes[rel], query, callback); }; }); /** * Fetch all models assigned to this role * @private + * @param {object} Context role context * @param {*} model model type to fetch * @param {String} [principalType] principalType used in the rolemapping for model * @param {object} [query] query object passed to model find call * @param {Function} [callback] callback function called with `(err, models)` arguments. */ - function listByPrincipalType(model, principalType, query, callback) { + function listByPrincipalType(context, model, principalType, query, callback) { if (callback === undefined) { callback = query; query = {}; } roleModel.roleMappingModel.find({ - where: {roleId: this.id, principalType: principalType} + where: {roleId: context.id, principalType: principalType}, }, function(err, mappings) { var ids; if (err) { diff --git a/test/role.test.js b/test/role.test.js index 1b1112f24..5362135a0 100644 --- a/test/role.test.js +++ b/test/role.test.js @@ -616,6 +616,84 @@ describe('role model', function() { }); }); + it('should fetch all models only assigned to the role', function(done) { + var principalTypesToModels = {}; + var mappings; + + principalTypesToModels[RoleMapping.USER] = User; + principalTypesToModels[RoleMapping.APPLICATION] = Application; + principalTypesToModels[RoleMapping.ROLE] = Role; + mappings = Object.keys(principalTypesToModels); + + async.each(mappings, function(principalType, eachCallback) { + var Model = principalTypesToModels[principalType]; + + async.waterfall([ + // Create models + function(next) { + Model.create([ + { name: 'test', email: 'x@y.com', password: 'foobar' }, + { name: 'test2', email: 'f@v.com', password: 'bargoo' }, + { name: 'test3', email: 'd@t.com', password: 'bluegoo' }], + function(err, models) { + if (err) return next(err); + next(null, models); + }); + }, + + // Create Roles + function(models, next) { + var uniqueRoleName = 'testRoleFor' + principalType; + var otherUniqueRoleName = 'otherTestRoleFor' + principalType; + Role.create([ + { name: uniqueRoleName }, + { name: otherUniqueRoleName }], + function(err, roles) { + if (err) return next(err); + next(null, models, roles); + }); + }, + + // Create principles + function(models, roles, next) { + async.parallel([ + function(callback) { + roles[0].principals.create( + { principalType: principalType, principalId: models[0].id }, + function(err, p) { + if (err) return callback(err); + callback(p); + }); + }, + function(callback) { + roles[1].principals.create( + { principalType: principalType, principalId: models[1].id }, + function(err, p) { + if (err) return callback(err); + callback(p); + }); + }], + function(err, principles) { + next(null, models, roles, principles); + }); + }, + + // Run tests against unique Role + function(models, roles, principles, next) { + var pluralName = Model.pluralModelName.toLowerCase(); + var uniqueRole = roles[0]; + uniqueRole[pluralName](function(err, models) { + if (err) return done(err); + assert.equal(models.length, 1); + next(); + }); + }], + eachCallback); + }, function(err) { + done(); + }); + }); + it('should apply query', function(done) { User.create({ name: 'Raymond', email: 'x@y.com', password: 'foobar' }, function(err, user) { if (err) return done(err); From a4a96eb39fe463b8fff9cdc885664be5c25dbf9f Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 23 Nov 2016 08:51:23 +0100 Subject: [PATCH 118/187] Add "returnOnlyRoleNames" option to Role.getRoles Currently the return type of Role.getRoles() method is inconsistent: role names are returned for smart roles and role ids are returned for static roles (configured through user-role mapping). This commit adds a new option to Role.getRoles() allowing the caller to request role names to be returned for all types of roles. --- common/models/role.js | 22 ++++++++++++++++++---- test/role.test.js | 14 ++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/common/models/role.js b/common/models/role.js index 680297967..e7e93c51c 100644 --- a/common/models/role.js +++ b/common/models/role.js @@ -383,7 +383,12 @@ module.exports = function(Role) { * @param {Error} err Error object. * @param {String[]} roles An array of role IDs */ - Role.getRoles = function(context, callback) { + Role.getRoles = function(context, options, callback) { + if (!callback && typeof options === 'function') { + callback = options; + options = {}; + } + if (!(context instanceof AccessContext)) { context = new AccessContext(context); } @@ -433,15 +438,24 @@ module.exports = function(Role) { if (principalType && principalId) { // Please find() treat undefined matches all values inRoleTasks.push(function(done) { - roleMappingModel.find({where: {principalType: principalType, - principalId: principalId}}, function(err, mappings) { + var filter = {where: {principalType: principalType, principalId: principalId}}; + if (options.returnOnlyRoleNames === true) { + filter.include = ['role']; + } + roleMappingModel.find(filter, function(err, mappings) { debug('Role mappings found: %s %j', err, mappings); if (err) { if (done) done(err); return; } mappings.forEach(function(m) { - addRole(m.roleId); + var role; + if (options.returnOnlyRoleNames === true) { + role = m.toJSON().role.name; + } else { + role = m.roleId; + } + addRole(role); }); if (done) done(); }); diff --git a/test/role.test.js b/test/role.test.js index 5362135a0..a7bbf8b7b 100644 --- a/test/role.test.js +++ b/test/role.test.js @@ -249,6 +249,20 @@ describe('role model', function() { next(); }); }, + function(next) { + Role.getRoles( + { principalType: RoleMapping.USER, principalId: user.id }, + { returnOnlyRoleNames: true }, + function(err, roles) { + if (err) return next(err); + expect(roles).to.eql([ + Role.AUTHENTICATED, + Role.EVERYONE, + role.name, + ]); + next(); + }); + }, function(next) { Role.getRoles( { principalType: RoleMapping.APP, principalId: user.id }, From e7831f6c4d53d6982c02731095fd4a5614fa49ad Mon Sep 17 00:00:00 2001 From: Bram Borggreve Date: Wed, 23 Nov 2016 18:29:43 -0500 Subject: [PATCH 119/187] Allow password reset request for users in realms --- common/models/user.js | 13 ++++++++--- test/user.test.js | 53 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index 8d1debfe4..41ad6232a 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -546,11 +546,12 @@ module.exports = function(User) { }; /** - * Create a short lived acess token for temporary login. Allows users + * Create a short lived access token for temporary login. Allows users * to change passwords if forgotten. * * @options {Object} options - * @prop {String} email The user's email address + * @property {String} email The user's email address + * @property {String} realm The user's realm (optional) * @callback {Function} callback * @param {Error} err */ @@ -575,7 +576,13 @@ module.exports = function(User) { } catch (err) { return cb(err); } - UserModel.findOne({ where: { email: options.email }}, function(err, user) { + var where = { + email: options.email + }; + if (options.realm) { + where.realm = options.realm; + } + UserModel.findOne({ where: where }, function(err, user) { if (err) { return cb(err); } diff --git a/test/user.test.js b/test/user.test.js index 555e0acdf..f3b4b8504 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -15,6 +15,7 @@ describe('User', function() { var validCredentials = {email: validCredentialsEmail, password: 'bar'}; var validCredentialsEmailVerified = {email: 'foo1@bar.com', password: 'bar1', emailVerified: true}; var validCredentialsEmailVerifiedOverREST = {email: 'foo2@bar.com', password: 'bar2', emailVerified: true}; + var validCredentialsWithRealm = {email: 'foo3@bar.com', password: 'bar', realm: 'foobar'}; var validCredentialsWithTTL = {email: 'foo@bar.com', password: 'bar', ttl: 3600}; var validCredentialsWithTTLAndScope = {email: 'foo@bar.com', password: 'bar', ttl: 3600, scope: 'all'}; var validMixedCaseEmailCredentials = {email: 'Foo@bar.com', password: 'bar'}; @@ -1878,6 +1879,58 @@ describe('User', function() { }); }); }); + + describe('User.resetPassword(options, cb) requiring realm', function() { + var realmUser; + + beforeEach(function(done) { + User.create(validCredentialsWithRealm, function(err, u) { + if (err) return done(err); + + realmUser = u; + done(); + }); + }); + + it('Reports when email is not found in realm', function(done) { + User.resetPassword({ + email: realmUser.email, + realm: 'unknown' + }, function(err) { + assert(err); + assert.equal(err.code, 'EMAIL_NOT_FOUND'); + assert.equal(err.statusCode, 404); + + done(); + }); + }); + + it('Creates a temp accessToken to allow a user in realm to change password', function(done) { + var calledBack = false; + + User.resetPassword({ + email: realmUser.email, + realm: realmUser.realm + }, function() { + calledBack = true; + }); + + User.once('resetPasswordRequest', function(info) { + assert(info.email); + assert(info.accessToken); + assert(info.accessToken.id); + assert.equal(info.accessToken.ttl / 60, 15); + assert(calledBack); + info.accessToken.user(function(err, user) { + if (err) return done(err); + + assert.equal(user.email, realmUser.email); + + done(); + }); + }); + }); + }); }); describe('Email Update', function() { From a759286330fbf3fcbf2c83957ba44b047b856b1f Mon Sep 17 00:00:00 2001 From: David Cheung Date: Thu, 1 Dec 2016 11:34:50 -0500 Subject: [PATCH 120/187] Opt-out downstream builds that are unstable repos that are opting out are not a good indicator of stability of this module, and are failing --- package.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 794548a30..2c1e4c663 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,11 @@ }, "config": { "ci": { - "debug": "*,-mocha:*,-eslint:*" + "debug": "*,-mocha:*,-eslint:*", + "downstreamIgnoreList" : [ + "dashboard-controller", + "gateway-director-management-interface" + ] } }, "license": "MIT" From 4d41c67c546d15f87b58bc985114223bf0391017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 6 Dec 2016 15:58:27 +0100 Subject: [PATCH 121/187] Remove "options.template" from Email payload Fix User.confirm to exclude "options.template" when sending the confirmation email. Certain nodemailer transport plugins are rejecting such requests. --- common/models/user.js | 4 ++++ test/user.test.js | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/common/models/user.js b/common/models/user.js index 41ad6232a..7cb72e208 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -469,6 +469,10 @@ module.exports = function(User) { function setHtmlContentAndSend(html) { options.html = html; + // Remove options.template to prevent rejection by certain + // nodemailer transport plugins. + delete options.template; + Email.send(options, function(err, email) { if (err) { fn(err); diff --git a/test/user.test.js b/test/user.test.js index f3b4b8504..cd764ff5b 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -1659,6 +1659,24 @@ describe('User', function() { done(); }); }); + + it('removes "options.template" from Email payload', function() { + var MailerMock = { + send: function(options, cb) { cb(null, options); }, + }; + + return User.create({email: 'user@example.com', password: 'pass'}) + .then(function(user) { + return user.verify({ + type: 'email', + from: 'noreply@example.com', + mailer: MailerMock, + }); + }) + .then(function(result) { + expect(result.email).to.not.have.property('template'); + }); + }); }); describe('User.confirm(options, fn)', function() { From 01b2faf14a08fb9e1b3af7ab0794694cca7afeb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 9 Dec 2016 14:12:43 +0100 Subject: [PATCH 122/187] Fix registration of operation hooks in User model Operation hooks are inherited by subclassed models, therefore they must be registered outside of `Model.setup()` function. This commit fixes this problem in the built-in User model. There are not tests verifying this change, as writing a test would be too cumbersome and not worth the cost IMO. --- common/models/user.js | 110 ++++++++++++++++++++++-------------------- 1 file changed, 57 insertions(+), 53 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index 7cb72e208..b598c8723 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -679,15 +679,6 @@ module.exports = function(User) { } }; - // Access token to normalize email credentials - UserModel.observe('access', function normalizeEmailCase(ctx, next) { - if (!ctx.Model.settings.caseSensitiveEmail && ctx.query.where && - ctx.query.where.email && typeof(ctx.query.where.email) === 'string') { - ctx.query.where.email = ctx.query.where.email.toLowerCase(); - } - next(); - }); - // Make sure emailVerified is not set by creation UserModel.beforeRemote('create', function(ctx, user, next) { var body = ctx.req.body; @@ -697,50 +688,6 @@ module.exports = function(User) { next(); }); - // Delete old sessions once email is updated - UserModel.observe('before save', function beforeEmailUpdate(ctx, next) { - var emailChanged; - if (ctx.isNewInstance) return next(); - if (!ctx.where && !ctx.instance) return next(); - var where = ctx.where || { id: ctx.instance.id }; - ctx.Model.find({ where: where }, function(err, userInstances) { - if (err) return next(err); - ctx.hookState.originalUserData = userInstances.map(function(u) { - return { id: u.id, email: u.email }; - }); - if (ctx.instance) { - emailChanged = ctx.instance.email !== ctx.hookState.originalUserData[0].email; - if (emailChanged && ctx.Model.settings.emailVerificationRequired) { - ctx.instance.emailVerified = false; - } - } else { - emailChanged = ctx.hookState.originalUserData.some(function(data) { - return data.email != ctx.data.email; - }); - if (emailChanged && ctx.Model.settings.emailVerificationRequired) { - ctx.data.emailVerified = false; - } - } - next(); - }); - }); - - UserModel.observe('after save', function afterEmailUpdate(ctx, next) { - if (!ctx.Model.relations.accessTokens) return next(); - var AccessToken = ctx.Model.relations.accessTokens.modelTo; - if (!ctx.instance && !ctx.data) return next(); - var newEmail = (ctx.instance || ctx.data).email; - if (!newEmail) return next(); - if (!ctx.hookState.originalUserData) return next(); - var idsToExpire = ctx.hookState.originalUserData.filter(function(u) { - return u.email !== newEmail; - }).map(function(u) { - return u.id; - }); - if (!idsToExpire.length) return next(); - AccessToken.deleteAll({ userId: { inq: idsToExpire }}, next); - }); - UserModel.remoteMethod( 'login', { @@ -843,6 +790,63 @@ module.exports = function(User) { User.setup(); + // --- OPERATION HOOKS --- + // + // Important: Operation hooks are inherited by subclassed models, + // therefore they must be registered outside of setup() function + + // Access token to normalize email credentials + User.observe('access', function normalizeEmailCase(ctx, next) { + if (!ctx.Model.settings.caseSensitiveEmail && ctx.query.where && + ctx.query.where.email && typeof(ctx.query.where.email) === 'string') { + ctx.query.where.email = ctx.query.where.email.toLowerCase(); + } + next(); + }); + + // Delete old sessions once email is updated + User.observe('before save', function beforeEmailUpdate(ctx, next) { + var emailChanged; + if (ctx.isNewInstance) return next(); + if (!ctx.where && !ctx.instance) return next(); + var where = ctx.where || { id: ctx.instance.id }; + ctx.Model.find({ where: where }, function(err, userInstances) { + if (err) return next(err); + ctx.hookState.originalUserData = userInstances.map(function(u) { + return { id: u.id, email: u.email }; + }); + if (ctx.instance) { + emailChanged = ctx.instance.email !== ctx.hookState.originalUserData[0].email; + if (emailChanged && ctx.Model.settings.emailVerificationRequired) { + ctx.instance.emailVerified = false; + } + } else { + emailChanged = ctx.hookState.originalUserData.some(function(data) { + return data.email != ctx.data.email; + }); + if (emailChanged && ctx.Model.settings.emailVerificationRequired) { + ctx.data.emailVerified = false; + } + } + next(); + }); + }); + + User.observe('after save', function afterEmailUpdate(ctx, next) { + if (!ctx.Model.relations.accessTokens) return next(); + var AccessToken = ctx.Model.relations.accessTokens.modelTo; + if (!ctx.instance && !ctx.data) return next(); + var newEmail = (ctx.instance || ctx.data).email; + if (!newEmail) return next(); + if (!ctx.hookState.originalUserData) return next(); + var idsToExpire = ctx.hookState.originalUserData.filter(function(u) { + return u.email !== newEmail; + }).map(function(u) { + return u.id; + }); + if (!idsToExpire.length) return next(); + AccessToken.deleteAll({ userId: { inq: idsToExpire }}, next); + }); }; function emailValidator(err, done) { From 4ee086dcd0da8c7a3d805b6e8d610ed3fc9c0081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 9 Dec 2016 15:36:54 +0100 Subject: [PATCH 123/187] Invalidate AccessTokens on password change Invalidate all existing sessions (delete all access tokens) after user's password was changed. --- common/models/user.js | 47 +++- test/user.test.js | 538 +++++++++++++++++++++--------------------- 2 files changed, 311 insertions(+), 274 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index b598c8723..c89c34e98 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -645,6 +645,19 @@ module.exports = function(User) { err.statusCode = 422; throw err; }; + + User._invalidateAccessTokensOfUsers = function(userIds, cb) { + if (!Array.isArray(userIds) || !userIds.length) + return process.nextTick(cb); + + var accessTokenRelation = this.relations.accessTokens; + if (!accessTokenRelation) + return process.nextTick(cb); + + var AccessToken = accessTokenRelation.modelTo; + AccessToken.deleteAll({userId: {inq: userIds}}, cb); + }; + /*! * Setup an extended user model. */ @@ -809,8 +822,20 @@ module.exports = function(User) { var emailChanged; if (ctx.isNewInstance) return next(); if (!ctx.where && !ctx.instance) return next(); - var where = ctx.where || { id: ctx.instance.id }; - ctx.Model.find({ where: where }, function(err, userInstances) { + + var isPartialUpdateChangingPassword = ctx.data && 'password' in ctx.data; + + // Full replace of User instance => assume password change. + // HashPassword returns a different value for each invocation, + // therefore we cannot tell whether ctx.instance.password is the same + // or not. + var isFullReplaceChangingPassword = !!ctx.instance; + + ctx.hookState.isPasswordChange = isPartialUpdateChangingPassword || + isFullReplaceChangingPassword; + + var where = ctx.where || {id: ctx.instance.id}; + ctx.Model.find({where: where}, function(err, userInstances) { if (err) return next(err); ctx.hookState.originalUserData = userInstances.map(function(u) { return { id: u.id, email: u.email }; @@ -828,24 +853,26 @@ module.exports = function(User) { ctx.data.emailVerified = false; } } + next(); }); }); User.observe('after save', function afterEmailUpdate(ctx, next) { - if (!ctx.Model.relations.accessTokens) return next(); - var AccessToken = ctx.Model.relations.accessTokens.modelTo; if (!ctx.instance && !ctx.data) return next(); - var newEmail = (ctx.instance || ctx.data).email; - if (!newEmail) return next(); if (!ctx.hookState.originalUserData) return next(); - var idsToExpire = ctx.hookState.originalUserData.filter(function(u) { - return u.email !== newEmail; + + var newEmail = (ctx.instance || ctx.data).email; + var isPasswordChange = ctx.hookState.isPasswordChange; + + if (!newEmail && !isPasswordChange) return next(); + + var userIdsToExpire = ctx.hookState.originalUserData.filter(function(u) { + return (newEmail && u.email !== newEmail) || isPasswordChange; }).map(function(u) { return u.id; }); - if (!idsToExpire.length) return next(); - AccessToken.deleteAll({ userId: { inq: idsToExpire }}, next); + ctx.Model._invalidateAccessTokensOfUsers(userIdsToExpire, next); }); }; diff --git a/test/user.test.js b/test/user.test.js index cd764ff5b..c9d0f7dfa 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -1951,295 +1951,245 @@ describe('User', function() { }); }); - describe('Email Update', function() { - describe('User changing email property', function() { - var user, originalUserToken1, originalUserToken2, newUserCreated; - var currentEmailCredentials = { email: 'original@example.com', password: 'bar' }; - var updatedEmailCredentials = { email: 'updated@example.com', password: 'bar' }; - var newUserCred = { email: 'newuser@example.com', password: 'newpass' }; - - beforeEach('create user then login', function createAndLogin(done) { - async.series([ - function createUserWithOriginalEmail(next) { - User.create(currentEmailCredentials, function(err, userCreated) { - if (err) return next(err); - user = userCreated; - next(); - }); - }, - function firstLoginWithOriginalEmail(next) { - User.login(currentEmailCredentials, function(err, accessToken1) { - if (err) return next(err); - assert(accessToken1.userId); - originalUserToken1 = accessToken1.id; - next(); - }); - }, - function secondLoginWithOriginalEmail(next) { - User.login(currentEmailCredentials, function(err, accessToken2) { - if (err) return next(err); - assert(accessToken2.userId); - originalUserToken2 = accessToken2.id; - next(); - }); - }, - ], done); - }); + describe('AccessToken (session) invalidation', function() { + var user, originalUserToken1, originalUserToken2, newUserCreated; + var currentEmailCredentials = {email: 'original@example.com', password: 'bar'}; + var updatedEmailCredentials = {email: 'updated@example.com', password: 'bar'}; + var newUserCred = {email: 'newuser@example.com', password: 'newpass'}; - it('invalidates sessions when email is changed using `updateAttributes`', function(done) { - user.updateAttributes( - { email: updatedEmailCredentials.email }, - function(err, userInstance) { - if (err) return done(err); - assertNoAccessTokens(done); + beforeEach('create user then login', function createAndLogin(done) { + async.series([ + function createUserWithOriginalEmail(next) { + User.create(currentEmailCredentials, function(err, userCreated) { + if (err) return next(err); + user = userCreated; + next(); }); - }); + }, + function firstLoginWithOriginalEmail(next) { + User.login(currentEmailCredentials, function(err, accessToken1) { + if (err) return next(err); + assert(accessToken1.userId); + originalUserToken1 = accessToken1.id; + next(); + }); + }, + function secondLoginWithOriginalEmail(next) { + User.login(currentEmailCredentials, function(err, accessToken2) { + if (err) return next(err); + assert(accessToken2.userId); + originalUserToken2 = accessToken2.id; + next(); + }); + }, + ], done); + }); - it('invalidates sessions when email is changed using `replaceAttributes`', function(done) { - user.replaceAttributes(updatedEmailCredentials, function(err, userInstance) { + it('invalidates sessions when email is changed using `updateAttributes`', function(done) { + user.updateAttributes( + {email: updatedEmailCredentials.email}, + function(err, userInstance) { if (err) return done(err); assertNoAccessTokens(done); }); - }); + }); - it('invalidates sessions when email is changed using `updateOrCreate`', function(done) { - User.updateOrCreate({ - id: user.id, - email: updatedEmailCredentials.email, - password: updatedEmailCredentials.password, - }, function(err, userInstance) { - if (err) return done(err); - assertNoAccessTokens(done); - }); + it('invalidates sessions after `replaceAttributes`', function(done) { + // The way how the invalidation is implemented now, all sessions + // are invalidated on a full replace + user.replaceAttributes(currentEmailCredentials, function(err, userInstance) { + if (err) return done(err); + assertNoAccessTokens(done); }); + }); - it('invalidates sessions when the email is changed using `replaceById`', function(done) { - User.replaceById(user.id, updatedEmailCredentials, function(err, userInstance) { - if (err) return done(err); - assertNoAccessTokens(done); - }); + it('invalidates sessions when email is changed using `updateOrCreate`', function(done) { + User.updateOrCreate({ + id: user.id, + email: updatedEmailCredentials.email, + }, function(err, userInstance) { + if (err) return done(err); + assertNoAccessTokens(done); }); + }); - it('invalidates sessions when the email is changed using `replaceOrCreate`', function(done) { - User.replaceOrCreate({ - id: user.id, - email: updatedEmailCredentials.email, - password: updatedEmailCredentials.password, - }, function(err, userInstance) { - if (err) return done(err); - assertNoAccessTokens(done); - }); + it('invalidates sessions after `replaceById`', function(done) { + // The way how the invalidation is implemented now, all sessions + // are invalidated on a full replace + User.replaceById(user.id, currentEmailCredentials, function(err, userInstance) { + if (err) return done(err); + assertNoAccessTokens(done); }); + }); - it('keeps sessions AS IS if firstName is added using `updateAttributes`', function(done) { - user.updateAttributes({ 'firstName': 'Janny' }, function(err, userInstance) { - if (err) return done(err); - assertUntouchedTokens(done); - }); + it('invalidates sessions after `replaceOrCreate`', function(done) { + // The way how the invalidation is implemented now, all sessions + // are invalidated on a full replace + User.replaceOrCreate({ + id: user.id, + email: currentEmailCredentials.email, + password: currentEmailCredentials.password, + }, function(err, userInstance) { + if (err) return done(err); + assertNoAccessTokens(done); }); + }); - it('keeps sessions AS IS if firstName is added using `replaceAttributes`', function(done) { - user.replaceAttributes({ - email: currentEmailCredentials.email, - password: currentEmailCredentials.password, - firstName: 'Candy', - }, function(err, userInstance) { - if (err) return done(err); - assertUntouchedTokens(done); - }); + it('keeps sessions AS IS if firstName is added using `updateAttributes`', function(done) { + user.updateAttributes({'firstName': 'Janny'}, function(err, userInstance) { + if (err) return done(err); + assertUntouchedTokens(done); }); + }); - it('keeps sessions AS IS if firstName is added using `updateOrCreate`', function(done) { - User.updateOrCreate({ - id: user.id, - firstName: 'Loay', - email: currentEmailCredentials.email, - password: currentEmailCredentials.password, - }, function(err, userInstance) { - if (err) return done(err); - assertUntouchedTokens(done); - }); + it('keeps sessions AS IS if firstName is added using `updateOrCreate`', function(done) { + User.updateOrCreate({ + id: user.id, + firstName: 'Loay', + email: currentEmailCredentials.email, + }, function(err, userInstance) { + if (err) return done(err); + assertUntouchedTokens(done); }); + }); - it('keeps sessions AS IS if firstName is added using `replaceById`', function(done) { - User.replaceById( - user.id, - { - firstName: 'Miroslav', - email: currentEmailCredentials.email, - password: currentEmailCredentials.password, - }, function(err, userInstance) { + it('keeps sessions AS IS if a new user is created using `create`', function(done) { + async.series([ + function(next) { + User.create(newUserCred, function(err, newUserInstance) { if (err) return done(err); - assertUntouchedTokens(done); + newUserCreated = newUserInstance; + next(); }); - }); - - it('keeps sessions AS IS if a new user is created using `create`', function(done) { - async.series([ - function(next) { - User.create(newUserCred, function(err, newUserInstance) { - if (err) return done(err); - newUserCreated = newUserInstance; - next(); - }); - }, - function(next) { - User.login(newUserCred, function(err, newAccessToken) { - if (err) return done(err); - assert(newAccessToken.id); - assertPreservedToken(next); - }); - }, - ], done); - }); + }, + function(next) { + User.login(newUserCred, function(err, newAccessToken) { + if (err) return done(err); + assert(newAccessToken.id); + assertPreservedTokens(next); + }); + }, + ], done); + }); - it('keeps sessions AS IS if a new user is created using `updateOrCreate`', function(done) { - async.series([ - function(next) { - User.create(newUserCred, function(err, newUserInstance2) { - if (err) return done(err); - newUserCreated = newUserInstance2; - next(); - }); - }, - function(next) { - User.login(newUserCred, function(err, newAccessToken2) { - if (err) return done(err); - assert(newAccessToken2.id); - assertPreservedToken(next); - }); - }, - ], done); - }); + it('keeps sessions AS IS if a new user is created using `updateOrCreate`', function(done) { + async.series([ + function(next) { + User.create(newUserCred, function(err, newUserInstance2) { + if (err) return done(err); + newUserCreated = newUserInstance2; + next(); + }); + }, + function(next) { + User.login(newUserCred, function(err, newAccessToken2) { + if (err) return done(err); + assert(newAccessToken2.id); + assertPreservedTokens(next); + }); + }, + ], done); + }); - it('keeps sessions AS IS if non-email property is changed using updateAll', function(done) { - var userPartial; - async.series([ - function createPartialUser(next) { - User.create( - { email: 'partial@example.com', password: 'pass1', age: 25 }, - function(err, partialInstance) { - if (err) return next(err); - userPartial = partialInstance; - next(); - }); - }, - function loginPartiallUser(next) { - User.login({ email: 'partial@example.com', password: 'pass1' }, function(err, ats) { + it('keeps sessions AS IS if non-email property is changed using updateAll', function(done) { + var userPartial; + async.series([ + function createPartialUser(next) { + User.create( + {email: 'partial@example.com', password: 'pass1', age: 25}, + function(err, partialInstance) { if (err) return next(err); + userPartial = partialInstance; next(); }); - }, - function updatePartialUser(next) { - User.updateAll( - { id: userPartial.id }, - { age: userPartial.age + 1 }, - function(err, info) { - if (err) return next(err); - next(); - }); - }, - function verifyTokensOfPartialUser(next) { - AccessToken.find({ where: { userId: userPartial.id }}, function(err, tokens1) { + }, + function loginPartiallUser(next) { + User.login({email: 'partial@example.com', password: 'pass1'}, function(err, ats) { + if (err) return next(err); + next(); + }); + }, + function updatePartialUser(next) { + User.updateAll( + {id: userPartial.id}, + {age: userPartial.age + 1}, + function(err, info) { if (err) return next(err); - expect(tokens1.length).to.equal(1); next(); }); - }, - ], done); - }); - - function assertPreservedToken(done) { - AccessToken.find({ where: { userId: user.id }}, function(err, tokens) { - if (err) return done(err); - expect(tokens.length).to.equal(2); - expect([tokens[0].id, tokens[1].id]).to.have.members([originalUserToken1, - originalUserToken2]); - done(); - }); - } - - function assertNoAccessTokens(done) { - AccessToken.find({ where: { userId: user.id }}, function(err, tokens) { - if (err) return done(err); - expect(tokens.length).to.equal(0); - done(); - }); - } - - function assertUntouchedTokens(done) { - AccessToken.find({ where: { userId: user.id }}, function(err, tokens) { - if (err) return done(err); - expect(tokens.length).to.equal(2); - done(); - }); - } + }, + function verifyTokensOfPartialUser(next) { + AccessToken.find({where: {userId: userPartial.id}}, function(err, tokens1) { + if (err) return next(err); + expect(tokens1.length).to.equal(1); + next(); + }); + }, + ], done); }); - describe('User not changing email property', function() { + it('preserves other users\' sessions if their email is untouched', function(done) { var user1, user2, user3; - it('preserves other users\' sessions if their email is untouched', function(done) { - async.series([ - function(next) { - User.create({ email: 'user1@example.com', password: 'u1pass' }, function(err, u1) { + async.series([ + function(next) { + User.create({email: 'user1@example.com', password: 'u1pass'}, function(err, u1) { + if (err) return done(err); + User.create({email: 'user2@example.com', password: 'u2pass'}, function(err, u2) { if (err) return done(err); - User.create({ email: 'user2@example.com', password: 'u2pass' }, function(err, u2) { + User.create({email: 'user3@example.com', password: 'u3pass'}, function(err, u3) { if (err) return done(err); - User.create({ email: 'user3@example.com', password: 'u3pass' }, function(err, u3) { - if (err) return done(err); - user1 = u1; - user2 = u2; - user3 = u3; - next(); - }); + user1 = u1; + user2 = u2; + user3 = u3; + next(); }); }); - }, - function(next) { - User.login( - { email: 'user1@example.com', password: 'u1pass' }, - function(err, accessToken1) { - if (err) return next(err); - User.login( - { email: 'user2@example.com', password: 'u2pass' }, - function(err, accessToken2) { + }); + }, + function(next) { + User.login( + {email: 'user1@example.com', password: 'u1pass'}, + function(err, accessToken1) { + if (err) return next(err); + User.login( + {email: 'user2@example.com', password: 'u2pass'}, + function(err, accessToken2) { + if (err) return next(err); + User.login({email: 'user3@example.com', password: 'u3pass'}, + function(err, accessToken3) { if (err) return next(err); - User.login({ email: 'user3@example.com', password: 'u3pass' }, - function(err, accessToken3) { - if (err) return next(err); - next(); - }); + next(); }); - }); - }, - function(next) { - user2.updateAttribute('email', 'user2Update@b.com', function(err, userInstance) { - if (err) return next(err); - assert.equal(userInstance.email, 'user2Update@b.com'); - next(); + }); }); - }, - function(next) { - AccessToken.find({ where: { userId: user1.id }}, function(err, tokens1) { + }, + function(next) { + user2.updateAttribute('email', 'user2Update@b.com', function(err, userInstance) { + if (err) return next(err); + assert.equal(userInstance.email, 'user2Update@b.com'); + next(); + }); + }, + function(next) { + AccessToken.find({where: {userId: user1.id}}, function(err, tokens1) { + if (err) return next(err); + AccessToken.find({where: {userId: user2.id}}, function(err, tokens2) { if (err) return next(err); - AccessToken.find({ where: { userId: user2.id }}, function(err, tokens2) { + AccessToken.find({where: {userId: user3.id}}, function(err, tokens3) { if (err) return next(err); - AccessToken.find({ where: { userId: user3.id }}, function(err, tokens3) { - if (err) return next(err); - expect(tokens1.length).to.equal(1); - expect(tokens2.length).to.equal(0); - expect(tokens3.length).to.equal(1); - next(); - }); + expect(tokens1.length).to.equal(1); + expect(tokens2.length).to.equal(0); + expect(tokens3.length).to.equal(1); + next(); }); }); - }, - ], done); - }); + }); + }, + ], done); }); - it('invalidates sessions after using updateAll', function(done) { + it('invalidates correct sessions after changing email using updateAll', function(done) { var userSpecial, userNormal; async.series([ function createSpecialUser(next) { @@ -2251,27 +2201,12 @@ describe('User', function() { next(); }); }, - function createNormaluser(next) { - User.create( - { email: 'normal@example.com', password: 'pass2' }, - function(err, normalInstance) { - if (err) return next(err); - userNormal = normalInstance; - next(); - }); - }, function loginSpecialUser(next) { User.login({ email: 'special@example.com', password: 'pass1' }, function(err, ats) { if (err) return next(err); next(); }); }, - function loginNormalUser(next) { - User.login({ email: 'normal@example.com', password: 'pass2' }, function(err, atn) { - if (err) return next(err); - next(); - }); - }, function updateSpecialUser(next) { User.updateAll( { name: 'Special' }, @@ -2281,21 +2216,96 @@ describe('User', function() { }); }, function verifyTokensOfSpecialUser(next) { - AccessToken.find({ where: { userId: userSpecial.id }}, function(err, tokens1) { + AccessToken.find({where: {userId: userSpecial.id}}, function(err, tokens1) { if (err) return done(err); - expect(tokens1.length).to.equal(0); + expect(tokens1.length, 'tokens - special user tokens').to.equal(0); next(); }); }, - function verifyTokensOfNormalUser(next) { - AccessToken.find({ userId: userNormal.userId }, function(err, tokens2) { + assertPreservedTokens, + ], done); + }); + + it('invalidates session when password is reset', function(done) { + user.updateAttribute('password', 'newPass', function(err, user2) { + if (err) return done(err); + assertNoAccessTokens(done); + }); + }); + + it('preserves other user sessions if their password is untouched', function(done) { + var user1, user2, user1Token; + async.series([ + function(next) { + User.create({email: 'user1@example.com', password: 'u1pass'}, function(err, u1) { if (err) return done(err); - expect(tokens2.length).to.equal(1); + User.create({email: 'user2@example.com', password: 'u2pass'}, function(err, u2) { + if (err) return done(err); + user1 = u1; + user2 = u2; + next(); + }); + }); + }, + function(next) { + User.login({email: 'user1@example.com', password: 'u1pass'}, function(err, at1) { + User.login({email: 'user2@example.com', password: 'u2pass'}, function(err, at2) { + assert(at1.userId); + assert(at2.userId); + user1Token = at1.id; + next(); + }); + }); + }, + function(next) { + user2.updateAttribute('password', 'newPass', function(err, user2Instance) { + if (err) return next(err); + assert(user2Instance); next(); }); }, - ], done); + function(next) { + AccessToken.find({where: {userId: user1.id}}, function(err, tokens1) { + if (err) return next(err); + AccessToken.find({where: {userId: user2.id}}, function(err, tokens2) { + if (err) return next(err); + expect(tokens1.length).to.equal(1); + expect(tokens2.length).to.equal(0); + assert.equal(tokens1[0].id, user1Token); + next(); + }); + }); + }, + ], function(err) { + done(); + }); }); + + function assertPreservedTokens(done) { + AccessToken.find({where: {userId: user.id}}, function(err, tokens) { + if (err) return done(err); + expect(tokens.length).to.equal(2); + expect([tokens[0].id, tokens[1].id]).to.have.members([originalUserToken1, + originalUserToken2]); + done(); + }); + } + + function assertNoAccessTokens(done) { + AccessToken.find({where: {userId: user.id}}, function(err, tokens) { + if (err) return done(err); + expect(tokens.length).to.equal(0); + done(); + }); + } + + function assertUntouchedTokens(done) { + AccessToken.find({where: {userId: user.id}}, function(err, tokens) { + if (err) return done(err); + expect(tokens.length).to.equal(2); + done(); + }); + } }); describe('Verification after updating email', function() { From fe1c0b605b852bae93356e0a9e1f532433a140ab Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Tue, 20 Dec 2016 11:32:12 -0800 Subject: [PATCH 124/187] Release LTS LB2 --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 2c1e4c663..4fc7fbbd5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,9 @@ { "name": "loopback", - "version": "2.36.0", + "version": "2.36.1", + "publishConfig": { + "tag": "lts" + }, "description": "LoopBack: Open Source Framework for Node.js", "homepage": "http://loopback.io/", "keywords": [ From b3a5bc739b0be4f4a902da5215172f8d8b690dd8 Mon Sep 17 00:00:00 2001 From: kobaska Date: Mon, 21 Nov 2016 19:19:11 +1100 Subject: [PATCH 125/187] Add option disabling periodic change rectification When `Model.settings.changeCleanupInterval` is set to a negative value, no periodic cleanup is performed at all. --- lib/persisted-model.js | 6 ++-- test/replication.test.js | 72 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/lib/persisted-model.js b/lib/persisted-model.js index 36cb11b61..feb231129 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -1597,14 +1597,16 @@ module.exports = function(registry) { var idDefn = idProp && idProp.defaultFn; if (idType !== String || !(idDefn === 'uuid' || idDefn === 'guid')) { deprecated('The model ' + this.modelName + ' is tracking changes, ' + - 'which requries a string id with GUID/UUID default value.'); + 'which requires a string id with GUID/UUID default value.'); } Model.observe('after save', rectifyOnSave); Model.observe('after delete', rectifyOnDelete); - if (runtime.isServer) { + // Only run if the run time is server + // Can switch off cleanup by setting the interval to -1 + if (runtime.isServer && cleanupInterval > 0) { // initial cleanup cleanup(); diff --git a/test/replication.test.js b/test/replication.test.js index d559fd3c7..32f5e4adf 100644 --- a/test/replication.test.js +++ b/test/replication.test.js @@ -11,6 +11,7 @@ var defineModelTestsWithDataSource = require('./util/model-tests'); var PersistedModel = loopback.PersistedModel; var expect = require('chai').expect; var debug = require('debug')('test'); +var runtime = require('./../lib/runtime'); describe('Replication / Change APIs', function() { this.timeout(10000); @@ -60,6 +61,77 @@ describe('Replication / Change APIs', function() { }; }); + describe('cleanup check for enableChangeTracking', function() { + describe('when no changeCleanupInterval set', function() { + it('should call rectifyAllChanges if running on server', function(done) { + var calls = mockRectifyAllChanges(SourceModel); + SourceModel.enableChangeTracking(); + + if (runtime.isServer) { + expect(calls).to.eql(['rectifyAllChanges']); + } else { + expect(calls).to.eql([]); + } + + done(); + }); + }); + + describe('when changeCleanupInterval set to -1', function() { + var Model; + beforeEach(function() { + Model = this.Model = PersistedModel.extend( + 'Model-' + tid, + {id: {id: true, type: String, defaultFn: 'guid'}}, + {trackChanges: true, changeCleanupInterval: -1}); + + Model.attachTo(dataSource); + }); + + it('should not call rectifyAllChanges', function(done) { + var calls = mockRectifyAllChanges(Model); + Model.enableChangeTracking(); + expect(calls).to.eql([]); + done(); + }); + }); + + describe('when changeCleanupInterval set to 10000', function() { + var Model; + beforeEach(function() { + Model = this.Model = PersistedModel.extend( + 'Model-' + tid, + {id: {id: true, type: String, defaultFn: 'guid'}}, + {trackChanges: true, changeCleanupInterval: 10000}); + + Model.attachTo(dataSource); + }); + + it('should call rectifyAllChanges if running on server', function(done) { + var calls = mockRectifyAllChanges(Model); + Model.enableChangeTracking(); + if (runtime.isServer) { + expect(calls).to.eql(['rectifyAllChanges']); + } else { + expect(calls).to.eql([]); + } + + done(); + }); + }); + + function mockRectifyAllChanges(Model) { + var calls = []; + + Model.rectifyAllChanges = function(cb) { + calls.push('rectifyAllChanges'); + process.nextTick(cb); + }; + + return calls; + } + }); + describe('optimization check rectifyChange Vs rectifyAllChanges', function() { beforeEach(function initialData(done) { var data = [{name: 'John', surname: 'Doe'}, {name: 'Jane', surname: 'Roe'}]; From 9c3d596106dea5406498823964f97d5142e711cf Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Wed, 21 Dec 2016 17:54:34 -0800 Subject: [PATCH 126/187] 2.36.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add option disabling periodic change rectification (kobaska) * Release LTS LB2 (Simon Ho) * Invalidate AccessTokens on password change (Miroslav Bajtoš) * Fix registration of operation hooks in User model (Miroslav Bajtoš) * Remove "options.template" from Email payload (Miroslav Bajtoš) * Opt-out downstream builds that are unstable (David Cheung) * Allow password reset request for users in realms (Bram Borggreve) * Add "returnOnlyRoleNames" option to Role.getRoles (Eric) * Fix context within listByPrincipalType role method (codyolsen) * Add templateFn option to User#verify() (Adrien Kiren) * Add options to bulkUpdate (Kogulan Baskaran) * Require verification after email change (Loay) * adding check of string for case insensitive emails (Dhaval Trivedi) * Fix PR template to not link all PRs to #49 (#2887) (Miroslav Bajtoš) --- CHANGES.md | 32 ++++++++++++++++++++++++++++++++ package.json | 4 ++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f1b7b5ad1..1d6cb8966 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,35 @@ +2016-12-22, Version 2.36.2 +========================== + + * Add option disabling periodic change rectification (kobaska) + + * Release LTS LB2 (Simon Ho) + + * Invalidate AccessTokens on password change (Miroslav Bajtoš) + + * Fix registration of operation hooks in User model (Miroslav Bajtoš) + + * Remove "options.template" from Email payload (Miroslav Bajtoš) + + * Opt-out downstream builds that are unstable (David Cheung) + + * Allow password reset request for users in realms (Bram Borggreve) + + * Add "returnOnlyRoleNames" option to Role.getRoles (Eric) + + * Fix context within listByPrincipalType role method (codyolsen) + + * Add templateFn option to User#verify() (Adrien Kiren) + + * Add options to bulkUpdate (Kogulan Baskaran) + + * Require verification after email change (Loay) + + * adding check of string for case insensitive emails (Dhaval Trivedi) + + * Fix PR template to not link all PRs to #49 (#2887) (Miroslav Bajtoš) + + 2016-10-24, Version 2.36.0 ========================== diff --git a/package.json b/package.json index 4fc7fbbd5..b2d0567ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.36.1", + "version": "2.36.2", "publishConfig": { "tag": "lts" }, @@ -114,7 +114,7 @@ "config": { "ci": { "debug": "*,-mocha:*,-eslint:*", - "downstreamIgnoreList" : [ + "downstreamIgnoreList": [ "dashboard-controller", "gateway-director-management-interface" ] From d53d0697638a4c15159756f7eb3ccb61f0b56257 Mon Sep 17 00:00:00 2001 From: David Cheung Date: Tue, 3 Jan 2017 13:56:13 -0500 Subject: [PATCH 127/187] Fix package.json CI downstreamIgnoreList nesting in packge.json strongloop/loopback#3000 ci should be a root element instead of under config:ci --- package.json | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index b2d0567ad..70ad8dc1b 100644 --- a/package.json +++ b/package.json @@ -113,12 +113,14 @@ }, "config": { "ci": { - "debug": "*,-mocha:*,-eslint:*", - "downstreamIgnoreList": [ - "dashboard-controller", - "gateway-director-management-interface" - ] + "debug": "*,-mocha:*,-eslint:*" } }, + "ci": { + "downstreamIgnoreList": [ + "dashboard-controller", + "gateway-director-management-interface" + ] + }, "license": "MIT" } From ee106e4e15f9471345f9f488ea1ec89377439dcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 20 Sep 2016 09:51:50 +0200 Subject: [PATCH 128/187] Implement new http arg mapping optionsFromRequest Define a new Model method "createOptionsFromRemotingContext" that allows models to define what "options" should be passed to methods invoked via strong-remoting (e.g. REST). Define a new http mapping `http: 'optionsFromRequest'` that invokes `Model.createOptionsFromRemotingContext` to build the value from remoting context. This should provide enough infrastructure for components and applications to implement their own ways of building the "options" object. --- lib/model.js | 70 ++++++++++++++++++++++++++++++ test/model.test.js | 104 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+) diff --git a/lib/model.js b/lib/model.js index a85f4e2f2..15bc18a8e 100644 --- a/lib/model.js +++ b/lib/model.js @@ -428,9 +428,39 @@ module.exports = function(registry) { if (options.isStatic === undefined) { options.isStatic = true; } + + if (options.accepts) { + options = extend({}, options); + options.accepts = setupOptionsArgs(options.accepts); + } + this.sharedClass.defineMethod(name, options); }; + function setupOptionsArgs(accepts) { + if (!Array.isArray(accepts)) + accepts = [accepts]; + + return accepts.map(function(arg) { + if (arg.http && arg.http === 'optionsFromRequest') { + // deep clone to preserve the input value + arg = extend({}, arg); + arg.http = createOptionsViaModelMethod; + } + return arg; + }); + } + + function createOptionsViaModelMethod(ctx) { + var EMPTY_OPTIONS = {}; + var ModelCtor = ctx.method && ctx.method.ctor; + if (!ModelCtor) + return EMPTY_OPTIONS; + if (typeof ModelCtor.createOptionsFromRemotingContext !== 'function') + return EMPTY_OPTIONS; + return ModelCtor.createOptionsFromRemotingContext(ctx); + } + /** * Disable remote invocation for the method with the given name. * @@ -869,6 +899,46 @@ module.exports = function(registry) { Model.ValidationError = require('loopback-datasource-juggler').ValidationError; + /** + * Create "options" value to use when invoking model methods + * via strong-remoting (e.g. REST). + * + * Example + * + * ```js + * MyModel.myMethod = function(options, cb) { + * // by default, options contains only one property "accessToken" + * var accessToken = options && options.accessToken; + * var userId = accessToken && accessToken.userId; + * var message = 'Hello ' + (userId ? 'user #' + userId : 'anonymous'); + * cb(null, message); + * }); + * + * MyModel.remoteMethod('myMethod', { + * accepts: { + * arg: 'options', + * type: 'object', + * // "optionsFromRequest" is a loopback-specific HTTP mapping that + * // calls Model's createOptionsFromRemotingContext + * // to build the argument value + * http: 'optionsFromRequest' + * }, + * returns: { + * arg: 'message', + * type: 'string' + * } + * }); + * ``` + * + * @param {Object} ctx A strong-remoting Context instance + * @returns {Object} The value to pass to "options" argument. + */ + Model.createOptionsFromRemotingContext = function(ctx) { + return { + accessToken: ctx.req.accessToken, + }; + }; + // setup the initial model Model.setup(); diff --git a/test/model.test.js b/test/model.test.js index 87d1c5fb7..2ed1f0f2f 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -786,4 +786,108 @@ describe.onServer('Remote Methods', function() { // fails on time-out when not implemented correctly }); }); + + describe('Model.createOptionsFromRemotingContext', function() { + var app, TestModel, accessToken, userId, actualOptions; + + before(setupAppAndRequest); + before(createUserAndAccessToken); + + it('sets empty options.accessToken for anonymous requests', function(done) { + request(app).get('/TestModels/saveOptions') + .expect(204, function(err) { + if (err) return done(err); + expect(actualOptions).to.eql({accessToken: null}); + done(); + }); + }); + + it('sets options.accessToken for authorized requests', function(done) { + request(app).get('/TestModels/saveOptions') + .set('Authorization', accessToken.id) + .expect(204, function(err) { + if (err) return done(err); + expect(actualOptions).to.have.property('accessToken'); + expect(actualOptions.accessToken.toObject()) + .to.eql(accessToken.toObject()); + done(); + }); + }); + + it('allows "beforeRemote" hooks to contribute options', function(done) { + TestModel.beforeRemote('saveOptions', function(ctx, unused, next) { + ctx.args.options.hooked = true; + next(); + }); + + request(app).get('/TestModels/saveOptions') + .expect(204, function(err) { + if (err) return done(err); + expect(actualOptions).to.have.property('hooked', true); + done(); + }); + }); + + it('allows apps to add options before remoting hooks', function(done) { + TestModel.createOptionsFromRemotingContext = function(ctx) { + return {hooks: []}; + }; + + TestModel.beforeRemote('saveOptions', function(ctx, unused, next) { + ctx.args.options.hooks.push('beforeRemote'); + next(); + }); + + // In real apps, this code can live in a component or in a boot script + app.remotes().phases + .addBefore('invoke', 'options-from-request') + .use(function(ctx, next) { + ctx.args.options.hooks.push('custom'); + next(); + }); + + request(app).get('/TestModels/saveOptions') + .expect(204, function(err) { + if (err) return done(err); + expect(actualOptions.hooks).to.eql(['custom', 'beforeRemote']); + done(); + }); + }); + + function setupAppAndRequest() { + app = loopback({localRegistry: true, loadBuiltinModels: true}); + + app.dataSource('db', {connector: 'memory'}); + + TestModel = app.registry.createModel('TestModel', {base: 'Model'}); + TestModel.saveOptions = function(options, cb) { + actualOptions = options; + cb(); + }; + + TestModel.remoteMethod('saveOptions', { + accepts: {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + http: {verb: 'GET', path: '/saveOptions'}, + }); + + app.model(TestModel, {dataSource: null}); + + app.enableAuth({dataSource: 'db'}); + + app.use(loopback.token()); + app.use(loopback.rest()); + } + + function createUserAndAccessToken() { + var CREDENTIALS = {email: 'context@example.com', password: 'pass'}; + var User = app.registry.getModel('User'); + return User.create(CREDENTIALS) + .then(function(u) { + return User.login(CREDENTIALS); + }).then(function(token) { + accessToken = token; + userId = token.userId; + }); + } + }); }); From 693d52fc59583c5f9e0750b27051ddcccdc7238a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 20 Sep 2016 10:54:41 +0200 Subject: [PATCH 129/187] Contextify DAO and relation methods Modify remoting metadata of data-access methods in PersistedModel and relation method in Model and add an "options" argument to "accepts" list. --- lib/model.js | 157 ++++++++++--- lib/persisted-model.js | 112 ++++++--- package.json | 3 +- test/context-options.test.js | 411 ++++++++++++++++++++++++++++++++++ test/remote-connector.test.js | 2 +- test/remoting.integration.js | 4 +- 6 files changed, 622 insertions(+), 67 deletions(-) create mode 100644 test/context-options.test.js diff --git a/lib/model.js b/lib/model.js index 15bc18a8e..05d114934 100644 --- a/lib/model.js +++ b/lib/model.js @@ -10,6 +10,7 @@ var g = require('strong-globalize')(); var assert = require('assert'); +var debug = require('debug')('loopback:model'); var RemoteObjects = require('strong-remoting'); var SharedClass = require('strong-remoting').SharedClass; var extend = require('util')._extend; @@ -141,15 +142,29 @@ module.exports = function(registry) { }); // support remoting prototype methods - ModelCtor.sharedCtor = function(data, id, fn) { + ModelCtor.sharedCtor = function(data, id, options, fn) { var ModelCtor = this; - if (typeof data === 'function') { + var isRemoteInvocationWithOptions = typeof data !== 'object' && + typeof id === 'object' && + typeof options === 'function'; + if (isRemoteInvocationWithOptions) { + // sharedCtor(id, options, fn) + fn = options; + options = id; + id = data; + data = null; + } else if (typeof data === 'function') { + // sharedCtor(fn) fn = data; data = null; id = null; + options = null; } else if (typeof id === 'function') { + // sharedCtor(data, fn) + // sharedCtor(id, fn) fn = id; + options = null; if (typeof data !== 'object') { id = data; @@ -166,7 +181,8 @@ module.exports = function(registry) { } else if (data) { fn(null, new ModelCtor(data)); } else if (id) { - ModelCtor.findById(id, function(err, model) { + var filter = {}; + ModelCtor.findById(id, filter, options, function(err, model) { if (err) { fn(err); } else if (model) { @@ -186,8 +202,9 @@ module.exports = function(registry) { var idDesc = ModelCtor.modelName + ' id'; ModelCtor.sharedCtor.accepts = [ {arg: 'id', type: 'any', required: true, http: {source: 'path'}, - description: idDesc} + description: idDesc}, // {arg: 'instance', type: 'object', http: {source: 'body'}} + {arg: 'options', type: 'object', http: createOptionsViaModelMethod}, ]; ModelCtor.sharedCtor.http = [ @@ -238,6 +255,14 @@ module.exports = function(registry) { sharedClass.resolve(function resolver(define) { var relations = ModelCtor.relations || {}; + var defineRaw = define; + define = function(name, options, fn) { + if (options.accepts) { + options = extend({}, options); + options.accepts = setupOptionsArgs(options.accepts); + } + defineRaw(name, options, fn); + }; // get the relations for (var relationName in relations) { @@ -443,7 +468,7 @@ module.exports = function(registry) { return accepts.map(function(arg) { if (arg.http && arg.http === 'optionsFromRequest') { - // deep clone to preserve the input value + // clone to preserve the input value arg = extend({}, arg); arg.http = createOptionsViaModelMethod; } @@ -458,6 +483,7 @@ module.exports = function(registry) { return EMPTY_OPTIONS; if (typeof ModelCtor.createOptionsFromRemotingContext !== 'function') return EMPTY_OPTIONS; + debug('createOptionsFromRemotingContext for %s', ctx.method.stringName); return ModelCtor.createOptionsFromRemotingContext(ctx); } @@ -496,7 +522,10 @@ module.exports = function(registry) { define('__get__' + relationName, { isStatic: false, http: {verb: 'get', path: '/' + pathName}, - accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}}, + accepts: [ + {arg: 'refresh', type: 'boolean', http: {source: 'query'}}, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], accessType: 'READ', description: format('Fetches belongsTo relation %s.', relationName), returns: {arg: relationName, type: modelName, root: true}, @@ -521,7 +550,10 @@ module.exports = function(registry) { define('__get__' + relationName, { isStatic: false, http: {verb: 'get', path: '/' + pathName}, - accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}}, + accepts: [ + {arg: 'refresh', type: 'boolean', http: {source: 'query'}}, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], description: format('Fetches hasOne relation %s.', relationName), accessType: 'READ', returns: {arg: relationName, type: relation.modelTo.modelName, root: true}, @@ -531,7 +563,13 @@ module.exports = function(registry) { define('__create__' + relationName, { isStatic: false, http: {verb: 'post', path: '/' + pathName}, - accepts: {arg: 'data', type: 'object', model: toModelName, http: {source: 'body'}}, + accepts: [ + { + arg: 'data', type: 'object', model: toModelName, + http: {source: 'body'}, + }, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], description: format('Creates a new instance in %s of this model.', relationName), accessType: 'WRITE', returns: {arg: 'data', type: toModelName, root: true} @@ -540,7 +578,13 @@ module.exports = function(registry) { define('__update__' + relationName, { isStatic: false, http: {verb: 'put', path: '/' + pathName}, - accepts: {arg: 'data', type: 'object', model: toModelName, http: {source: 'body'}}, + accepts: [ + { + arg: 'data', type: 'object', model: toModelName, + http: {source: 'body'}, + }, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], description: format('Update %s of this model.', relationName), accessType: 'WRITE', returns: {arg: 'data', type: toModelName, root: true} @@ -549,6 +593,9 @@ module.exports = function(registry) { define('__destroy__' + relationName, { isStatic: false, http: {verb: 'delete', path: '/' + pathName}, + accepts: [ + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], description: format('Deletes %s of this model.', relationName), accessType: 'WRITE', }); @@ -562,10 +609,15 @@ module.exports = function(registry) { define('__findById__' + relationName, { isStatic: false, http: {verb: 'get', path: '/' + pathName + '/:fk'}, - accepts: {arg: 'fk', type: 'any', - description: format('Foreign key for %s', relationName), - required: true, - http: {source: 'path'}}, + accepts: [ + { + arg: 'fk', type: 'any', + description: format('Foreign key for %s', relationName), + required: true, + http: {source: 'path'}, + }, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], description: format('Find a related item by id for %s.', relationName), accessType: 'READ', returns: {arg: 'result', type: toModelName, root: true}, @@ -576,10 +628,15 @@ module.exports = function(registry) { define('__destroyById__' + relationName, { isStatic: false, http: {verb: 'delete', path: '/' + pathName + '/:fk'}, - accepts: { arg: 'fk', type: 'any', - description: format('Foreign key for %s', relationName), - required: true, - http: {source: 'path'}}, + accepts: [ + { + arg: 'fk', type: 'any', + description: format('Foreign key for %s', relationName), + required: true, + http: {source: 'path'}, + }, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], description: format('Delete a related item by id for %s.', relationName), accessType: 'WRITE', returns: [] @@ -595,6 +652,7 @@ module.exports = function(registry) { required: true, http: { source: 'path' }}, {arg: 'data', type: 'object', model: toModelName, http: {source: 'body'}}, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, ], description: format('Update a related item by id for %s.', relationName), accessType: 'WRITE', @@ -617,7 +675,10 @@ module.exports = function(registry) { accepts: [{ arg: 'fk', type: 'any', description: format('Foreign key for %s', relationName), required: true, - http: {source: 'path'}}].concat(accepts), + http: {source: 'path'}}, + ].concat(accepts).concat([ + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ]), description: format('Add a related item by id for %s.', relationName), accessType: 'WRITE', returns: {arg: relationName, type: modelThrough.modelName, root: true} @@ -627,10 +688,15 @@ module.exports = function(registry) { define('__unlink__' + relationName, { isStatic: false, http: {verb: 'delete', path: '/' + pathName + '/rel/:fk'}, - accepts: {arg: 'fk', type: 'any', - description: format('Foreign key for %s', relationName), - required: true, - http: {source: 'path'}}, + accepts: [ + { + arg: 'fk', type: 'any', + description: format('Foreign key for %s', relationName), + required: true, + http: {source: 'path'}, + }, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], description: format('Remove the %s relation to an item by id.', relationName), accessType: 'WRITE', returns: [] @@ -642,10 +708,15 @@ module.exports = function(registry) { define('__exists__' + relationName, { isStatic: false, http: {verb: 'head', path: '/' + pathName + '/rel/:fk'}, - accepts: {arg: 'fk', type: 'any', - description: format('Foreign key for %s', relationName), - required: true, - http: {source: 'path'}}, + accepts: [ + { + arg: 'fk', type: 'any', + description: format('Foreign key for %s', relationName), + required: true, + http: {source: 'path'}, + }, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], description: format('Check the existence of %s relation to an item by id.', relationName), accessType: 'READ', returns: {arg: 'exists', type: 'boolean', root: true}, @@ -688,7 +759,10 @@ module.exports = function(registry) { define('__get__' + scopeName, { isStatic: isStatic, http: {verb: 'get', path: '/' + pathName}, - accepts: {arg: 'filter', type: 'object'}, + accepts: [ + {arg: 'filter', type: 'object'}, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], description: format('Queries %s of %s.', scopeName, this.modelName), accessType: 'READ', returns: {arg: scopeName, type: [toModelName], root: true} @@ -697,7 +771,15 @@ module.exports = function(registry) { define('__create__' + scopeName, { isStatic: isStatic, http: {verb: 'post', path: '/' + pathName}, - accepts: {arg: 'data', type: 'object', model: toModelName, http: {source: 'body'}}, + accepts: [ + { + arg: 'data', + type: 'object', + model: toModelName, + http: {source: 'body'}, + }, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], description: format('Creates a new instance in %s of this model.', scopeName), accessType: 'WRITE', returns: {arg: 'data', type: toModelName, root: true} @@ -706,6 +788,16 @@ module.exports = function(registry) { define('__delete__' + scopeName, { isStatic: isStatic, http: {verb: 'delete', path: '/' + pathName}, + accepts: [ + { + arg: 'where', type: 'object', + // The "where" argument is not exposed in the REST API + // but we need to provide a value so that we can pass "options" + // as the third argument. + http: function(ctx) { return undefined; }, + }, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], description: format('Deletes all %s of this model.', scopeName), accessType: 'WRITE', }); @@ -713,8 +805,13 @@ module.exports = function(registry) { define('__count__' + scopeName, { isStatic: isStatic, http: {verb: 'get', path: '/' + pathName + '/count'}, - accepts: {arg: 'where', type: 'object', - description: 'Criteria to match model instances'}, + accepts: [ + { + arg: 'where', type: 'object', + description: 'Criteria to match model instances', + }, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], description: format('Counts %s of %s.', scopeName, this.modelName), accessType: 'READ', returns: {arg: 'count', type: 'number'} diff --git a/lib/persisted-model.js b/lib/persisted-model.js index feb231129..e8e183483 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -639,7 +639,14 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'create', { description: 'Create a new instance of the model and persist it into the data source.', accessType: 'WRITE', - accepts: {arg: 'data', type: 'object', model: typeName, description: 'Model instance data', http: {source: 'body'}}, + accepts: [ + { + arg: 'data', type: 'object', model: typeName, allowArray: true, + description: 'Model instance data', + http: {source: 'body'}, + }, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], returns: {arg: 'data', type: typeName, root: true}, http: {verb: 'post', path: '/'} }); @@ -648,10 +655,15 @@ module.exports = function(registry) { aliases: ['patchOrCreate', 'updateOrCreate'], description: 'Patch an existing model instance or insert a new one into the data source.', accessType: 'WRITE', - accepts: { arg: 'data', type: 'object', model: typeName, http: { source: 'body' }, description: - 'Model instance data' }, - returns: { arg: 'data', type: typeName, root: true }, - http: [{ verb: 'patch', path: '/' }], + accepts: [ + { + arg: 'data', type: 'object', model: typeName, http: {source: 'body'}, + description: 'Model instance data', + }, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], + returns: {arg: 'data', type: typeName, root: true}, + http: [{verb: 'patch', path: '/'}], }; if (!options.replaceOnPUT) { @@ -662,10 +674,16 @@ module.exports = function(registry) { var replaceOrCreateOptions = { description: 'Replace an existing model instance or insert a new one into the data source.', accessType: 'WRITE', - accepts: { arg: 'data', type: 'object', model: typeName, http: { source: 'body' }, description: - 'Model instance data' }, - returns: { arg: 'data', type: typeName, root: true }, - http: [{ verb: 'post', path: '/replaceOrCreate' }], + accepts: [ + { + arg: 'data', type: 'object', model: typeName, + http: {source: 'body'}, + description: 'Model instance data', + }, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], + returns: {arg: 'data', type: typeName, root: true}, + http: [{verb: 'post', path: '/replaceOrCreate'}], }; if (options.replaceOnPUT) { @@ -680,10 +698,11 @@ module.exports = function(registry) { 'the data source based on the where criteria.', accessType: 'WRITE', accepts: [ - { arg: 'where', type: 'object', http: { source: 'query' }, - description: 'Criteria to match model instances' }, - { arg: 'data', type: 'object', model: typeName, http: { source: 'body' }, - description: 'An object of model property name/value pairs' }, + {arg: 'where', type: 'object', http: {source: 'query'}, + description: 'Criteria to match model instances'}, + {arg: 'data', type: 'object', model: typeName, http: {source: 'body'}, + description: 'An object of model property name/value pairs'}, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, ], returns: { arg: 'data', type: typeName, root: true }, http: { verb: 'post', path: '/upsertWithWhere' }, @@ -692,7 +711,10 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'exists', { description: 'Check whether a model instance exists in the data source.', accessType: 'READ', - accepts: {arg: 'id', type: 'any', description: 'Model id', required: true}, + accepts: [ + {arg: 'id', type: 'any', description: 'Model id', required: true}, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], returns: {arg: 'exists', type: 'boolean'}, http: [ {verb: 'get', path: '/:id/exists'}, @@ -726,8 +748,9 @@ module.exports = function(registry) { accepts: [ { arg: 'id', type: 'any', description: 'Model id', required: true, http: {source: 'path'}}, - { arg: 'filter', type: 'object', - description: 'Filter defining fields and include' }, + {arg: 'filter', type: 'object', + description: 'Filter defining fields and include'}, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, ], returns: {arg: 'data', type: typeName, root: true}, http: {verb: 'get', path: '/:id'}, @@ -738,10 +761,11 @@ module.exports = function(registry) { description: 'Replace attributes for a model instance and persist it into the data source.', accessType: 'WRITE', accepts: [ - { arg: 'id', type: 'any', description: 'Model id', required: true, - http: { source: 'path' }}, - { arg: 'data', type: 'object', model: typeName, http: { source: 'body' }, description: - 'Model instance data' }, + {arg: 'id', type: 'any', description: 'Model id', required: true, + http: {source: 'path'}}, + {arg: 'data', type: 'object', model: typeName, http: {source: 'body'}, description: + 'Model instance data'}, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, ], returns: { arg: 'data', type: typeName, root: true }, http: [{ verb: 'post', path: '/:id/replace' }], @@ -756,7 +780,11 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'find', { description: 'Find all instances of the model matched by filter from the data source.', accessType: 'READ', - accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, include, order, offset, and limit'}, + accepts: [ + {arg: 'filter', type: 'object', description: + 'Filter defining fields, where, include, order, offset, and limit'}, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], returns: {arg: 'data', type: [typeName], root: true}, http: {verb: 'get', path: '/'} }); @@ -764,7 +792,11 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'findOne', { description: 'Find first instance of the model matched by filter from the data source.', accessType: 'READ', - accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, include, order, offset, and limit'}, + accepts: [ + {arg: 'filter', type: 'object', description: + 'Filter defining fields, where, include, order, offset, and limit'}, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], returns: {arg: 'data', type: typeName, root: true}, http: {verb: 'get', path: '/findOne'}, rest: {after: convertNullToNotFoundError} @@ -773,7 +805,10 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'destroyAll', { description: 'Delete all matching records.', accessType: 'WRITE', - accepts: {arg: 'where', type: 'object', description: 'filter.where object'}, + accepts: [ + {arg: 'where', type: 'object', description: 'filter.where object'}, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], returns: { arg: 'count', type: 'object', @@ -793,6 +828,7 @@ module.exports = function(registry) { description: 'Criteria to match model instances'}, {arg: 'data', type: 'object', model: typeName, http: {source: 'body'}, description: 'An object of model property name/value pairs'}, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, ], returns: { arg: 'info', @@ -812,8 +848,11 @@ module.exports = function(registry) { aliases: ['destroyById', 'removeById'], description: 'Delete a model instance by {{id}} from the data source.', accessType: 'WRITE', - accepts: {arg: 'id', type: 'any', description: 'Model id', required: true, - http: {source: 'path'}}, + accepts: [ + {arg: 'id', type: 'any', description: 'Model id', required: true, + http: {source: 'path'}}, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], http: {verb: 'del', path: '/:id'}, returns: {arg: 'count', type: 'object', root: true} }); @@ -821,7 +860,10 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'count', { description: 'Count instances of the model matched by where from the data source.', accessType: 'READ', - accepts: {arg: 'where', type: 'object', description: 'Criteria to match model instances'}, + accepts: [ + {arg: 'where', type: 'object', description: 'Criteria to match model instances'}, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], returns: {arg: 'count', type: 'number'}, http: {verb: 'get', path: '/count'} }); @@ -830,14 +872,16 @@ module.exports = function(registry) { aliases: ['patchAttributes'], description: 'Patch attributes for a model instance and persist it into the data source.', accessType: 'WRITE', - - accepts: { - arg: 'data', type: 'object', model: typeName, - http: { source: 'body' }, - description: 'An object of model property name/value pairs' - }, - returns: { arg: 'data', type: typeName, root: true }, - http: [{ verb: 'patch', path: '/' }], + accepts: [ + { + arg: 'data', type: 'object', model: typeName, + http: {source: 'body'}, + description: 'An object of model property name/value pairs', + }, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], + returns: {arg: 'data', type: typeName, root: true}, + http: [{verb: 'patch', path: '/'}], }; setRemoting(PersistedModel.prototype, 'updateAttributes', updateAttributesOptions); diff --git a/package.json b/package.json index 70ad8dc1b..44f5f36f3 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,8 @@ "sinon-chai": "^2.8.0", "strong-error-handler": "^1.0.1", "strong-task-emitter": "^0.0.6", - "supertest": "^2.0.0" + "supertest": "^2.0.0", + "supertest-as-promised": "^4.0.2" }, "repository": { "type": "git", diff --git a/test/context-options.test.js b/test/context-options.test.js new file mode 100644 index 000000000..762f320bd --- /dev/null +++ b/test/context-options.test.js @@ -0,0 +1,411 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; + +var expect = require('chai').expect; +var loopback = require('..'); +var supertest = require('supertest-as-promised')(require('bluebird')); + +describe('OptionsFromRemotingContext', function() { + var app, request, accessToken, userId, Product, actualOptions; + + beforeEach(setupAppAndRequest); + beforeEach(resetActualOptions); + + context('when making updates via REST', function() { + beforeEach(observeOptionsBeforeSave); + + it('injects options to create()', function() { + return request.post('/products') + .send({name: 'Pen'}) + .expect(200) + .then(expectInjectedOptions); + }); + + it('injects options to patchOrCreate()', function() { + return request.patch('/products') + .send({id: 1, name: 'Pen'}) + .expect(200) + .then(expectInjectedOptions); + }); + + it('injects options to replaceOrCreate()', function() { + return request.put('/products') + .send({id: 1, name: 'Pen'}) + .expect(200) + .then(expectInjectedOptions); + }); + + it('injects options to patchOrCreateWithWhere()', function() { + return request.post('/products/upsertWithWhere?where[name]=Pen') + .send({name: 'Pencil'}) + .expect(200) + .then(expectInjectedOptions); + }); + + it('injects options to replaceById()', function() { + return Product.create({id: 1, name: 'Pen'}) + .then(function(p) { + return request.put('/products/1') + .send({name: 'Pencil'}) + .expect(200); + }) + .then(expectInjectedOptions); + }); + + it('injects options to prototype.patchAttributes()', function() { + return Product.create({id: 1, name: 'Pen'}) + .then(function(p) { + return request.patch('/products/1') + .send({name: 'Pencil'}) + .expect(200); + }) + .then(expectInjectedOptions); + }); + + it('injects options to updateAll()', function() { + return request.post('/products/update?where[name]=Pen') + .send({name: 'Pencil'}) + .expect(200) + .then(expectInjectedOptions); + }); + }); + + context('when deleting via REST', function() { + beforeEach(observeOptionsBeforeDelete); + + it('injects options to deleteById()', function() { + return Product.create({id: 1, name: 'Pen'}) + .then(function(p) { + return request.delete('/products/1').expect(200); + }) + .then(expectInjectedOptions); + }); + }); + + context('when querying via REST', function() { + beforeEach(observeOptionsOnAccess); + beforeEach(givenProductId1); + + it('injects options to find()', function() { + return request.get('/products').expect(200) + .then(expectInjectedOptions); + }); + + it('injects options to findById()', function() { + return request.get('/products/1').expect(200) + .then(expectInjectedOptions); + }); + + it('injects options to findOne()', function() { + return request.get('/products/findOne?where[id]=1').expect(200) + .then(expectInjectedOptions); + }); + + it('injects options to exists()', function() { + return request.head('/products/1').expect(200) + .then(expectInjectedOptions); + }); + + it('injects options to count()', function() { + return request.get('/products/count').expect(200) + .then(expectInjectedOptions); + }); + }); + + context('when invoking prototype methods', function() { + beforeEach(observeOptionsOnAccess); + beforeEach(givenProductId1); + + it('injects options to sharedCtor', function() { + Product.prototype.dummy = function(cb) { cb(); }; + Product.remoteMethod('dummy', {isStatic: false}); + return request.post('/products/1/dummy').expect(204) + .then(expectInjectedOptions); + }); + }); + + // Catch: because relations methods are defined on "modelFrom", + // they will invoke createOptionsFromRemotingContext on "modelFrom" too, + // despite the fact that under the hood a method on "modelTo" is called. + + context('hasManyThrough', function() { + var Category, ThroughModel; + + beforeEach(givenCategoryHasManyProductsThroughAnotherModel); + beforeEach(givenCategoryAndProduct); + + it('injects options to findById', function() { + observeOptionsOnAccess(Product); + return request.get('/categories/1/products/1').expect(200) + .then(expectOptionsInjectedFromCategory); + }); + + it('injects options to destroyById', function() { + observeOptionsBeforeDelete(Product); + return request.del('/categories/1/products/1').expect(204) + .then(expectOptionsInjectedFromCategory); + }); + + it('injects options to updateById', function() { + observeOptionsBeforeSave(Product); + return request.put('/categories/1/products/1') + .send({description: 'a description'}) + .expect(200) + .then(expectInjectedOptions); + }); + + context('through-model operations', function() { + it('injects options to link', function() { + observeOptionsBeforeSave(ThroughModel); + return Product.create({id: 2, name: 'Car2'}) + .then(function() { + return request.put('/categories/1/products/rel/2') + .send({description: 'a description'}) + .expect(200); + }) + .then(expectOptionsInjectedFromCategory); + }); + + it('injects options to unlink', function() { + observeOptionsBeforeDelete(ThroughModel); + return request.del('/categories/1/products/rel/1').expect(204) + .then(expectOptionsInjectedFromCategory); + }); + + it('injects options to exists', function() { + observeOptionsOnAccess(ThroughModel); + return request.head('/categories/1/products/rel/1').expect(200) + .then(expectOptionsInjectedFromCategory); + }); + }); + + context('scope operations', function() { + it('injects options to get', function() { + observeOptionsOnAccess(Product); + return request.get('/categories/1/products').expect(200) + .then(expectOptionsInjectedFromCategory); + }); + + it('injects options to create', function() { + observeOptionsBeforeSave(Product); + return request.post('/categories/1/products') + .send({name: 'Pen'}) + .expect(200) + .then(expectOptionsInjectedFromCategory); + }); + + it('injects options to delete', function() { + observeOptionsBeforeDelete(ThroughModel); + return request.del('/categories/1/products').expect(204) + .then(expectOptionsInjectedFromCategory); + }); + + it('injects options to count', function() { + observeOptionsOnAccess(ThroughModel); + return request.get('/categories/1/products/count').expect(200) + .then(expectOptionsInjectedFromCategory); + }); + }); + + function givenCategoryHasManyProductsThroughAnotherModel() { + Category = app.registry.createModel( + 'Category', + {name: String}, + {forceId: false, replaceOnPUT: true}); + + app.model(Category, {dataSource: 'db'}); + // This is a shortcut for creating CategoryProduct "through" model + Category.hasAndBelongsToMany(Product); + + Category.createOptionsFromRemotingContext = function(ctx) { + return {injectedFrom: 'Category'}; + }; + + ThroughModel = app.registry.getModel('CategoryProduct'); + } + + function givenCategoryAndProduct() { + return Category.create({id: 1, name: 'First Category'}) + .then(function(cat) { + return cat.products.create({id: 1, name: 'Pen'}); + }); + } + + function expectOptionsInjectedFromCategory() { + expect(actualOptions).to.have.property('injectedFrom', 'Category'); + } + }); + + context('hasOne', function() { + var Category; + + beforeEach(givenCategoryHasOneProduct); + beforeEach(givenCategoryId1); + + it('injects options to get', function() { + observeOptionsOnAccess(Product); + return givenProductInCategory1() + .then(function() { + return request.get('/categories/1/product').expect(200); + }) + .then(expectOptionsInjectedFromCategory); + }); + + it('injects options to create', function() { + observeOptionsBeforeSave(Product); + return request.post('/categories/1/product') + .send({name: 'Pen'}) + .expect(200) + .then(expectOptionsInjectedFromCategory); + }); + + it('injects options to update', function() { + return givenProductInCategory1() + .then(function() { + observeOptionsBeforeSave(Product); + return request.put('/categories/1/product') + .send({description: 'a description'}) + .expect(200); + }) + .then(expectInjectedOptions); + }); + + it('injects options to destroy', function() { + observeOptionsBeforeDelete(Product); + return givenProductInCategory1() + .then(function() { + return request.del('/categories/1/product').expect(204); + }) + .then(expectOptionsInjectedFromCategory); + }); + + function givenCategoryHasOneProduct() { + Category = app.registry.createModel( + 'Category', + {name: String}, + {forceId: false, replaceOnPUT: true}); + + app.model(Category, {dataSource: 'db'}); + Category.hasOne(Product); + + Category.createOptionsFromRemotingContext = function(ctx) { + return {injectedFrom: 'Category'}; + }; + } + + function givenCategoryId1() { + return Category.create({id: 1, name: 'First Category'}); + } + + function givenProductInCategory1() { + return Product.create({id: 1, name: 'Pen', categoryId: 1}); + } + + function expectOptionsInjectedFromCategory() { + expect(actualOptions).to.have.property('injectedFrom', 'Category'); + } + }); + + context('belongsTo', function() { + var Category; + + beforeEach(givenCategoryBelongsToProduct); + + it('injects options to get', function() { + observeOptionsOnAccess(Product); + return Product.create({id: 1, name: 'Pen'}) + .then(function() { + return Category.create({id: 1, name: 'a name', productId: 1}); + }) + .then(function() { + return request.get('/categories/1/product').expect(200); + }) + .then(expectOptionsInjectedFromCategory); + }); + + function givenCategoryBelongsToProduct() { + Category = app.registry.createModel( + 'Category', + {name: String}, + {forceId: false, replaceOnPUT: true}); + + app.model(Category, {dataSource: 'db'}); + Category.belongsTo(Product); + + Category.createOptionsFromRemotingContext = function(ctx) { + return {injectedFrom: 'Category'}; + }; + } + + function givenCategoryId1() { + return Category.create({id: 1, name: 'First Category'}); + } + + function givenProductInCategory1() { + return Product.create({id: 1, name: 'Pen', categoryId: 1}); + } + + function expectOptionsInjectedFromCategory() { + expect(actualOptions).to.have.property('injectedFrom', 'Category'); + } + }); + + function setupAppAndRequest() { + app = loopback({localRegistry: true}); + app.dataSource('db', {connector: 'memory'}); + + Product = app.registry.createModel( + 'Product', + {name: String}, + {forceId: false, replaceOnPUT: true}); + + Product.createOptionsFromRemotingContext = function(ctx) { + return {injectedFrom: 'Product'}; + }; + + app.model(Product, {dataSource: 'db'}); + + app.use(loopback.rest()); + request = supertest(app); + } + + function resetActualOptions() { + actualOptions = undefined; + } + + function observeOptionsBeforeSave() { + var Model = arguments[0] || Product; + Model.observe('before save', function(ctx, next) { + actualOptions = ctx.options; + next(); + }); + } + + function observeOptionsBeforeDelete() { + var Model = arguments[0] || Product; + Model.observe('before delete', function(ctx, next) { + actualOptions = ctx.options; + next(); + }); + } + + function observeOptionsOnAccess() { + var Model = arguments[0] || Product; + Model.observe('access', function(ctx, next) { + actualOptions = ctx.options; + next(); + }); + } + + function givenProductId1() { + return Product.create({id: 1, name: 'Pen'}); + } + + function expectInjectedOptions(name) { + expect(actualOptions).to.have.property('injectedFrom'); + } +}); diff --git a/test/remote-connector.test.js b/test/remote-connector.test.js index dd058da60..bc92d37e3 100644 --- a/test/remote-connector.test.js +++ b/test/remote-connector.test.js @@ -74,7 +74,7 @@ describe('RemoteConnector', function() { var ServerModel = this.ServerModel; - ServerModel.create = function(data, cb) { + ServerModel.create = function(data, options, cb) { calledServerCreate = true; data.id = 1; cb(null, data); diff --git a/test/remoting.integration.js b/test/remoting.integration.js index f881ca4e7..c4bdd7ffc 100644 --- a/test/remoting.integration.js +++ b/test/remoting.integration.js @@ -285,7 +285,9 @@ function formatMethod(m) { arr.push([ m.name, '(', - m.accepts.map(function(a) { + m.accepts.filter(function(a) { + return !(a.http && typeof a.http === 'function'); + }).map(function(a) { return a.arg + ':' + a.type + (a.model ? ':' + a.model : ''); }).join(','), ')', From 74bb1daf8a4cd147b695940a2b55f7708bc82a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 2 Jan 2017 15:28:47 +0100 Subject: [PATCH 130/187] Add new flag injectOptionsFromRemoteContext Hide the new "options" arguments behind a feature flag injectOptionsFromRemoteContext that is disabled by default for backwards compatibility. Fix construction of sharedCtor remoting metadata to prevent the situation when we are configuring remoting metadata after strong-remoting has already picked up data from our parent (base) model. --- lib/model.js | 105 +++++++++++++++++++--------------- lib/persisted-model.js | 56 +++++++++--------- test/context-options.test.js | 51 +++++++++++++++-- test/remote-connector.test.js | 2 +- test/remoting.integration.js | 28 +++++++++ 5 files changed, 162 insertions(+), 80 deletions(-) diff --git a/lib/model.js b/lib/model.js index 05d114934..a37dd40a4 100644 --- a/lib/model.js +++ b/lib/model.js @@ -126,22 +126,9 @@ module.exports = function(registry) { var options = this.settings; var typeName = this.modelName; - var remotingOptions = {}; - extend(remotingOptions, options.remoting || {}); - - // create a sharedClass - var sharedClass = ModelCtor.sharedClass = new SharedClass( - ModelCtor.modelName, - ModelCtor, - remotingOptions - ); - - // setup a remoting type converter for this model - RemoteObjects.convert(typeName, function(val) { - return val ? new ModelCtor(val) : val; - }); - // support remoting prototype methods + // it's important to setup this function *before* calling `new SharedClass` + // otherwise remoting metadata from our base model is picked up ModelCtor.sharedCtor = function(data, id, options, fn) { var ModelCtor = this; @@ -200,12 +187,12 @@ module.exports = function(registry) { }; var idDesc = ModelCtor.modelName + ' id'; - ModelCtor.sharedCtor.accepts = [ + ModelCtor.sharedCtor.accepts = this._removeOptionsArgIfDisabled([ {arg: 'id', type: 'any', required: true, http: {source: 'path'}, description: idDesc}, // {arg: 'instance', type: 'object', http: {source: 'body'}} {arg: 'options', type: 'object', http: createOptionsViaModelMethod}, - ]; + ]); ModelCtor.sharedCtor.http = [ {path: '/:id'} @@ -213,6 +200,21 @@ module.exports = function(registry) { ModelCtor.sharedCtor.returns = {root: true}; + var remotingOptions = {}; + extend(remotingOptions, options.remoting || {}); + + // create a sharedClass + var sharedClass = ModelCtor.sharedClass = new SharedClass( + ModelCtor.modelName, + ModelCtor, + remotingOptions + ); + + // setup a remoting type converter for this model + RemoteObjects.convert(typeName, function(val) { + return val ? new ModelCtor(val) : val; + }); + // before remote hook ModelCtor.beforeRemote = function(name, fn) { var className = this.modelName; @@ -522,10 +524,10 @@ module.exports = function(registry) { define('__get__' + relationName, { isStatic: false, http: {verb: 'get', path: '/' + pathName}, - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ {arg: 'refresh', type: 'boolean', http: {source: 'query'}}, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), accessType: 'READ', description: format('Fetches belongsTo relation %s.', relationName), returns: {arg: relationName, type: modelName, root: true}, @@ -550,10 +552,10 @@ module.exports = function(registry) { define('__get__' + relationName, { isStatic: false, http: {verb: 'get', path: '/' + pathName}, - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ {arg: 'refresh', type: 'boolean', http: {source: 'query'}}, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), description: format('Fetches hasOne relation %s.', relationName), accessType: 'READ', returns: {arg: relationName, type: relation.modelTo.modelName, root: true}, @@ -563,13 +565,13 @@ module.exports = function(registry) { define('__create__' + relationName, { isStatic: false, http: {verb: 'post', path: '/' + pathName}, - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ { arg: 'data', type: 'object', model: toModelName, http: {source: 'body'}, }, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), description: format('Creates a new instance in %s of this model.', relationName), accessType: 'WRITE', returns: {arg: 'data', type: toModelName, root: true} @@ -578,13 +580,13 @@ module.exports = function(registry) { define('__update__' + relationName, { isStatic: false, http: {verb: 'put', path: '/' + pathName}, - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ { arg: 'data', type: 'object', model: toModelName, http: {source: 'body'}, }, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), description: format('Update %s of this model.', relationName), accessType: 'WRITE', returns: {arg: 'data', type: toModelName, root: true} @@ -593,9 +595,9 @@ module.exports = function(registry) { define('__destroy__' + relationName, { isStatic: false, http: {verb: 'delete', path: '/' + pathName}, - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), description: format('Deletes %s of this model.', relationName), accessType: 'WRITE', }); @@ -609,7 +611,7 @@ module.exports = function(registry) { define('__findById__' + relationName, { isStatic: false, http: {verb: 'get', path: '/' + pathName + '/:fk'}, - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ { arg: 'fk', type: 'any', description: format('Foreign key for %s', relationName), @@ -617,7 +619,7 @@ module.exports = function(registry) { http: {source: 'path'}, }, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), description: format('Find a related item by id for %s.', relationName), accessType: 'READ', returns: {arg: 'result', type: toModelName, root: true}, @@ -628,7 +630,7 @@ module.exports = function(registry) { define('__destroyById__' + relationName, { isStatic: false, http: {verb: 'delete', path: '/' + pathName + '/:fk'}, - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ { arg: 'fk', type: 'any', description: format('Foreign key for %s', relationName), @@ -636,7 +638,7 @@ module.exports = function(registry) { http: {source: 'path'}, }, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), description: format('Delete a related item by id for %s.', relationName), accessType: 'WRITE', returns: [] @@ -646,14 +648,14 @@ module.exports = function(registry) { define('__updateById__' + relationName, { isStatic: false, http: {verb: 'put', path: '/' + pathName + '/:fk'}, - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ {arg: 'fk', type: 'any', description: format('Foreign key for %s', relationName), required: true, http: { source: 'path' }}, {arg: 'data', type: 'object', model: toModelName, http: {source: 'body'}}, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), description: format('Update a related item by id for %s.', relationName), accessType: 'WRITE', returns: {arg: 'result', type: toModelName, root: true} @@ -676,9 +678,9 @@ module.exports = function(registry) { description: format('Foreign key for %s', relationName), required: true, http: {source: 'path'}}, - ].concat(accepts).concat([ + ].concat(accepts).concat(this._removeOptionsArgIfDisabled([ {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ]), + ])), description: format('Add a related item by id for %s.', relationName), accessType: 'WRITE', returns: {arg: relationName, type: modelThrough.modelName, root: true} @@ -688,7 +690,7 @@ module.exports = function(registry) { define('__unlink__' + relationName, { isStatic: false, http: {verb: 'delete', path: '/' + pathName + '/rel/:fk'}, - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ { arg: 'fk', type: 'any', description: format('Foreign key for %s', relationName), @@ -696,7 +698,7 @@ module.exports = function(registry) { http: {source: 'path'}, }, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), description: format('Remove the %s relation to an item by id.', relationName), accessType: 'WRITE', returns: [] @@ -708,7 +710,7 @@ module.exports = function(registry) { define('__exists__' + relationName, { isStatic: false, http: {verb: 'head', path: '/' + pathName + '/rel/:fk'}, - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ { arg: 'fk', type: 'any', description: format('Foreign key for %s', relationName), @@ -716,7 +718,7 @@ module.exports = function(registry) { http: {source: 'path'}, }, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), description: format('Check the existence of %s relation to an item by id.', relationName), accessType: 'READ', returns: {arg: 'exists', type: 'boolean', root: true}, @@ -759,10 +761,10 @@ module.exports = function(registry) { define('__get__' + scopeName, { isStatic: isStatic, http: {verb: 'get', path: '/' + pathName}, - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ {arg: 'filter', type: 'object'}, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), description: format('Queries %s of %s.', scopeName, this.modelName), accessType: 'READ', returns: {arg: scopeName, type: [toModelName], root: true} @@ -771,7 +773,7 @@ module.exports = function(registry) { define('__create__' + scopeName, { isStatic: isStatic, http: {verb: 'post', path: '/' + pathName}, - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ { arg: 'data', type: 'object', @@ -779,7 +781,7 @@ module.exports = function(registry) { http: {source: 'body'}, }, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), description: format('Creates a new instance in %s of this model.', scopeName), accessType: 'WRITE', returns: {arg: 'data', type: toModelName, root: true} @@ -788,7 +790,7 @@ module.exports = function(registry) { define('__delete__' + scopeName, { isStatic: isStatic, http: {verb: 'delete', path: '/' + pathName}, - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ { arg: 'where', type: 'object', // The "where" argument is not exposed in the REST API @@ -797,7 +799,7 @@ module.exports = function(registry) { http: function(ctx) { return undefined; }, }, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), description: format('Deletes all %s of this model.', scopeName), accessType: 'WRITE', }); @@ -805,13 +807,13 @@ module.exports = function(registry) { define('__count__' + scopeName, { isStatic: isStatic, http: {verb: 'get', path: '/' + pathName + '/count'}, - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ { arg: 'where', type: 'object', description: 'Criteria to match model instances', }, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), description: format('Counts %s of %s.', scopeName, this.modelName), accessType: 'READ', returns: {arg: 'count', type: 'number'} @@ -819,6 +821,15 @@ module.exports = function(registry) { }; + Model._removeOptionsArgIfDisabled = function(accepts) { + if (this.settings.injectOptionsFromRemoteContext) + return accepts; + var lastArg = accepts[accepts.length - 1]; + var hasOptions = lastArg.arg === 'options' && lastArg.type === 'object'; + assert(hasOptions, 'last accepts argument is "options" arg'); + return accepts.slice(0, -1); + }; + /** * Enabled deeply-nested queries of related models via REST API. * diff --git a/lib/persisted-model.js b/lib/persisted-model.js index e8e183483..c19520eec 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -639,14 +639,14 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'create', { description: 'Create a new instance of the model and persist it into the data source.', accessType: 'WRITE', - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ { arg: 'data', type: 'object', model: typeName, allowArray: true, description: 'Model instance data', http: {source: 'body'}, }, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), returns: {arg: 'data', type: typeName, root: true}, http: {verb: 'post', path: '/'} }); @@ -655,13 +655,13 @@ module.exports = function(registry) { aliases: ['patchOrCreate', 'updateOrCreate'], description: 'Patch an existing model instance or insert a new one into the data source.', accessType: 'WRITE', - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ { arg: 'data', type: 'object', model: typeName, http: {source: 'body'}, description: 'Model instance data', }, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), returns: {arg: 'data', type: typeName, root: true}, http: [{verb: 'patch', path: '/'}], }; @@ -674,14 +674,14 @@ module.exports = function(registry) { var replaceOrCreateOptions = { description: 'Replace an existing model instance or insert a new one into the data source.', accessType: 'WRITE', - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ { arg: 'data', type: 'object', model: typeName, http: {source: 'body'}, description: 'Model instance data', }, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), returns: {arg: 'data', type: typeName, root: true}, http: [{verb: 'post', path: '/replaceOrCreate'}], }; @@ -697,13 +697,13 @@ module.exports = function(registry) { description: 'Update an existing model instance or insert a new one into ' + 'the data source based on the where criteria.', accessType: 'WRITE', - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ {arg: 'where', type: 'object', http: {source: 'query'}, description: 'Criteria to match model instances'}, {arg: 'data', type: 'object', model: typeName, http: {source: 'body'}, description: 'An object of model property name/value pairs'}, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), returns: { arg: 'data', type: typeName, root: true }, http: { verb: 'post', path: '/upsertWithWhere' }, }); @@ -711,10 +711,10 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'exists', { description: 'Check whether a model instance exists in the data source.', accessType: 'READ', - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ {arg: 'id', type: 'any', description: 'Model id', required: true}, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), returns: {arg: 'exists', type: 'boolean'}, http: [ {verb: 'get', path: '/:id/exists'}, @@ -745,13 +745,13 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'findById', { description: 'Find a model instance by {{id}} from the data source.', accessType: 'READ', - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ { arg: 'id', type: 'any', description: 'Model id', required: true, http: {source: 'path'}}, {arg: 'filter', type: 'object', description: 'Filter defining fields and include'}, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), returns: {arg: 'data', type: typeName, root: true}, http: {verb: 'get', path: '/:id'}, rest: {after: convertNullToNotFoundError} @@ -760,13 +760,13 @@ module.exports = function(registry) { var replaceByIdOptions = { description: 'Replace attributes for a model instance and persist it into the data source.', accessType: 'WRITE', - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ {arg: 'id', type: 'any', description: 'Model id', required: true, http: {source: 'path'}}, {arg: 'data', type: 'object', model: typeName, http: {source: 'body'}, description: 'Model instance data'}, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), returns: { arg: 'data', type: typeName, root: true }, http: [{ verb: 'post', path: '/:id/replace' }], }; @@ -780,11 +780,11 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'find', { description: 'Find all instances of the model matched by filter from the data source.', accessType: 'READ', - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ {arg: 'filter', type: 'object', description: 'Filter defining fields, where, include, order, offset, and limit'}, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), returns: {arg: 'data', type: [typeName], root: true}, http: {verb: 'get', path: '/'} }); @@ -792,11 +792,11 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'findOne', { description: 'Find first instance of the model matched by filter from the data source.', accessType: 'READ', - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ {arg: 'filter', type: 'object', description: 'Filter defining fields, where, include, order, offset, and limit'}, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), returns: {arg: 'data', type: typeName, root: true}, http: {verb: 'get', path: '/findOne'}, rest: {after: convertNullToNotFoundError} @@ -805,10 +805,10 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'destroyAll', { description: 'Delete all matching records.', accessType: 'WRITE', - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ {arg: 'where', type: 'object', description: 'filter.where object'}, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), returns: { arg: 'count', type: 'object', @@ -823,13 +823,13 @@ module.exports = function(registry) { aliases: ['update'], description: 'Update instances of the model matched by {{where}} from the data source.', accessType: 'WRITE', - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ {arg: 'where', type: 'object', http: { source: 'query'}, description: 'Criteria to match model instances'}, {arg: 'data', type: 'object', model: typeName, http: {source: 'body'}, description: 'An object of model property name/value pairs'}, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), returns: { arg: 'info', description: 'Information related to the outcome of the operation', @@ -848,11 +848,11 @@ module.exports = function(registry) { aliases: ['destroyById', 'removeById'], description: 'Delete a model instance by {{id}} from the data source.', accessType: 'WRITE', - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ {arg: 'id', type: 'any', description: 'Model id', required: true, http: {source: 'path'}}, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), http: {verb: 'del', path: '/:id'}, returns: {arg: 'count', type: 'object', root: true} }); @@ -860,10 +860,10 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'count', { description: 'Count instances of the model matched by where from the data source.', accessType: 'READ', - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ {arg: 'where', type: 'object', description: 'Criteria to match model instances'}, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), returns: {arg: 'count', type: 'number'}, http: {verb: 'get', path: '/count'} }); @@ -872,14 +872,14 @@ module.exports = function(registry) { aliases: ['patchAttributes'], description: 'Patch attributes for a model instance and persist it into the data source.', accessType: 'WRITE', - accepts: [ + accepts: this._removeOptionsArgIfDisabled([ { arg: 'data', type: 'object', model: typeName, http: {source: 'body'}, description: 'An object of model property name/value pairs', }, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, - ], + ]), returns: {arg: 'data', type: typeName, root: true}, http: [{verb: 'patch', path: '/'}], }; diff --git a/test/context-options.test.js b/test/context-options.test.js index 762f320bd..ac8f50412 100644 --- a/test/context-options.test.js +++ b/test/context-options.test.js @@ -128,6 +128,28 @@ describe('OptionsFromRemotingContext', function() { }); }); + it('honours injectOptionsFromRemoteContext in sharedCtor', function() { + var settings = { + forceId: false, + injectOptionsFromRemoteContext: false, + }; + var TestModel = app.registry.createModel('TestModel', {}, settings); + app.model(TestModel, {dataSource: 'db'}); + + TestModel.prototype.dummy = function(cb) { cb(); }; + TestModel.remoteMethod('dummy', {isStatic: false}); + + observeOptionsOnAccess(TestModel); + + return TestModel.create({id: 1}) + .then(function() { + return request.post('/TestModels/1/dummy').expect(204); + }) + .then(function() { + expect(actualOptions).to.eql({}); + }); + }); + // Catch: because relations methods are defined on "modelFrom", // they will invoke createOptionsFromRemotingContext on "modelFrom" too, // despite the fact that under the hood a method on "modelTo" is called. @@ -212,10 +234,15 @@ describe('OptionsFromRemotingContext', function() { }); function givenCategoryHasManyProductsThroughAnotherModel() { + var settings = { + forceId: false, + replaceOnPUT: true, + injectOptionsFromRemoteContext: true, + }; Category = app.registry.createModel( 'Category', {name: String}, - {forceId: false, replaceOnPUT: true}); + settings); app.model(Category, {dataSource: 'db'}); // This is a shortcut for creating CategoryProduct "through" model @@ -284,10 +311,15 @@ describe('OptionsFromRemotingContext', function() { }); function givenCategoryHasOneProduct() { + var settings = { + forceId: false, + replaceOnPUT: true, + injectOptionsFromRemoteContext: true, + }; Category = app.registry.createModel( 'Category', {name: String}, - {forceId: false, replaceOnPUT: true}); + settings); app.model(Category, {dataSource: 'db'}); Category.hasOne(Product); @@ -328,10 +360,15 @@ describe('OptionsFromRemotingContext', function() { }); function givenCategoryBelongsToProduct() { + var settings = { + forceId: false, + replaceOnPUT: true, + injectOptionsFromRemoteContext: true, + }; Category = app.registry.createModel( 'Category', {name: String}, - {forceId: false, replaceOnPUT: true}); + settings); app.model(Category, {dataSource: 'db'}); Category.belongsTo(Product); @@ -358,10 +395,16 @@ describe('OptionsFromRemotingContext', function() { app = loopback({localRegistry: true}); app.dataSource('db', {connector: 'memory'}); + var settings = { + forceId: false, + replaceOnPUT: true, + injectOptionsFromRemoteContext: true, + }; + Product = app.registry.createModel( 'Product', {name: String}, - {forceId: false, replaceOnPUT: true}); + settings); Product.createOptionsFromRemotingContext = function(ctx) { return {injectedFrom: 'Product'}; diff --git a/test/remote-connector.test.js b/test/remote-connector.test.js index bc92d37e3..dd058da60 100644 --- a/test/remote-connector.test.js +++ b/test/remote-connector.test.js @@ -74,7 +74,7 @@ describe('RemoteConnector', function() { var ServerModel = this.ServerModel; - ServerModel.create = function(data, options, cb) { + ServerModel.create = function(data, cb) { calledServerCreate = true; data.id = 1; cb(null, data); diff --git a/test/remoting.integration.js b/test/remoting.integration.js index c4bdd7ffc..dee8e99bd 100644 --- a/test/remoting.integration.js +++ b/test/remoting.integration.js @@ -9,6 +9,7 @@ var path = require('path'); var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-integration-app'); var app = require(path.join(SIMPLE_APP, 'server/server.js')); var assert = require('assert'); +var expect = require('chai').expect; describe('remoting - integration', function() { before(function(done) { @@ -263,6 +264,33 @@ describe('With model.settings.replaceOnPUT true', function() { }); }); +describe('injectContextFromRemotingContext', function() { + it('is disabled by default for DAO, scope and belongsTo methods', function() { + var storeClass = findClass('store'); + var violations = getMethodsAcceptingComputedOptions(storeClass.methods); + expect(violations).to.eql([]); + }); + + it('is disabled by default for belongsTo methods', function() { + var widgetClass = findClass('widget'); + var violations = getMethodsAcceptingComputedOptions(widgetClass.methods); + expect(violations).to.eql([]); + }); + + function getMethodsAcceptingComputedOptions(methods) { + return methods + .filter(function(m) { + return m.accepts.some(function(a) { + return a.arg === 'options' && a.type === 'object' && + a.http && typeof a.http === 'function'; + }); + }) + .map(function(m) { + return m.name; + }); + } +}); + function formatReturns(m) { var returns = m.returns; if (!returns || returns.length === 0) { From 659e9ce09b4699ed700ff462a3baacc440bc3d05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EB=8C=80=EC=84=A0?= Date: Fri, 23 Dec 2016 14:04:44 +0900 Subject: [PATCH 131/187] Fix false emailVerified on user model update We noticed that every time the user model updates, the emailVerified column would change to false, even though the email was not changed at all. I took a look and realized there might be an error in https://github.com/strongloop/loopback/commit/eb640d8 The intent of the commit just mention is to make emailVerified false when the email gets changed, but notice that ctx.data.email is null on updates, so the condition is always met and emailVerified always becomes false. This commit fixes the issue just mentioned. --- common/models/user.js | 2 +- test/user.test.js | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/common/models/user.js b/common/models/user.js index c89c34e98..dadc6f904 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -845,7 +845,7 @@ module.exports = function(User) { if (emailChanged && ctx.Model.settings.emailVerificationRequired) { ctx.instance.emailVerified = false; } - } else { + } else if (ctx.data.email) { emailChanged = ctx.hookState.originalUserData.some(function(data) { return data.email != ctx.data.email; }); diff --git a/test/user.test.js b/test/user.test.js index c9d0f7dfa..f58ce330a 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -2358,6 +2358,28 @@ describe('User', function() { ], done); }); + it('should not set verification to false after something other than email is updated', + function(done) { + User.settings.emailVerificationRequired = true; + async.series([ + function updateUser(next) { + userInstance.updateAttribute('realm', 'test', function(err, info) { + if (err) return next(err); + assert.equal(info.realm, 'test'); + next(); + }); + }, + function findUser(next) { + User.findById(userInstance.id, function(err, info) { + if (err) return next(err); + assert.equal(info.realm, 'test'); + assert.equal(info.emailVerified, true); + next(); + }); + }, + ], done); + }); + function createOriginalUser(done) { var userData = { email: 'original@example.com', From 5233dcb557b92271e7ab1912e562c11aa1fe3728 Mon Sep 17 00:00:00 2001 From: Sergey Reus Date: Wed, 30 Nov 2016 13:44:30 +0200 Subject: [PATCH 132/187] Emit resetPasswordRequest event with options --- common/models/user.js | 3 ++- test/user.test.js | 27 +++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index dadc6f904..27e1c9bdb 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -613,7 +613,8 @@ module.exports = function(User) { UserModel.emit('resetPasswordRequest', { email: options.email, accessToken: accessToken, - user: user + user: user, + options: options, }); }); }); diff --git a/test/user.test.js b/test/user.test.js index f58ce330a..bba023fef 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -1806,7 +1806,10 @@ describe('User', function() { describe('Password Reset', function() { describe('User.resetPassword(options, cb)', function() { - var email = 'foo@bar.com'; + var options = { + email: 'foo@bar.com', + redirect: 'http://foobar.com/reset-password', + }; it('Requires email address to reset password', function(done) { User.resetPassword({ }, function(err) { @@ -1840,11 +1843,27 @@ describe('User', function() { }); }); + it('Checks that options exist', function(done) { + var calledBack = false; + + User.resetPassword(options, function() { + calledBack = true; + }); + + User.once('resetPasswordRequest', function(info) { + assert(info.options); + assert.equal(info.options, options); + assert(calledBack); + + done(); + }); + }); + it('Creates a temp accessToken to allow a user to change password', function(done) { var calledBack = false; User.resetPassword({ - email: email + email: options.email, }, function() { calledBack = true; }); @@ -1858,7 +1877,7 @@ describe('User', function() { info.accessToken.user(function(err, user) { if (err) return done(err); - assert.equal(user.email, email); + assert.equal(user.email, options.email); done(); }); @@ -1887,7 +1906,7 @@ describe('User', function() { .post('/test-users/reset') .expect('Content-Type', /json/) .expect(204) - .send({ email: email }) + .send({email: options.email}) .end(function(err, res) { if (err) return done(err); From dc2b6530b71c48c35e3adc3cba13629c08b64f0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 9 Jan 2017 12:58:30 +0100 Subject: [PATCH 133/187] 2.37.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Emit resetPasswordRequest event with options (Sergey Reus) * Fix false emailVerified on user model update (박대선) * Add new flag injectOptionsFromRemoteContext (Miroslav Bajtoš) * Contextify DAO and relation methods (Miroslav Bajtoš) * Implement new http arg mapping optionsFromRequest (Miroslav Bajtoš) * Fix package.json CI downstreamIgnoreList nesting (David Cheung) --- CHANGES.md | 18 +++++++++++++++++- package.json | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1d6cb8966..302d42572 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,20 @@ -2016-12-22, Version 2.36.2 +2017-01-09, Version 2.37.0 +========================== + + * Emit resetPasswordRequest event with options (Sergey Reus) + + * Fix false emailVerified on user model update (박대선) + + * Add new flag injectOptionsFromRemoteContext (Miroslav Bajtoš) + + * Contextify DAO and relation methods (Miroslav Bajtoš) + + * Implement new http arg mapping optionsFromRequest (Miroslav Bajtoš) + + * Fix package.json CI downstreamIgnoreList nesting (David Cheung) + + +2016-12-21, Version 2.36.2 ========================== * Add option disabling periodic change rectification (kobaska) diff --git a/package.json b/package.json index 44f5f36f3..de8e2213b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.36.2", + "version": "2.37.0", "publishConfig": { "tag": "lts" }, From f8b013dab8d849fa46dbe4cc92b587240c6b2a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 13 Jan 2017 10:40:48 +0100 Subject: [PATCH 134/187] Clean up access-token-invalidation tests --- test/user.test.js | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/test/user.test.js b/test/user.test.js index bba023fef..808575f42 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -1989,7 +1989,7 @@ describe('User', function() { User.login(currentEmailCredentials, function(err, accessToken1) { if (err) return next(err); assert(accessToken1.userId); - originalUserToken1 = accessToken1.id; + originalUserToken1 = accessToken1; next(); }); }, @@ -1997,7 +1997,7 @@ describe('User', function() { User.login(currentEmailCredentials, function(err, accessToken2) { if (err) return next(err); assert(accessToken2.userId); - originalUserToken2 = accessToken2.id; + originalUserToken2 = accessToken2; next(); }); }, @@ -2057,7 +2057,7 @@ describe('User', function() { it('keeps sessions AS IS if firstName is added using `updateAttributes`', function(done) { user.updateAttributes({'firstName': 'Janny'}, function(err, userInstance) { if (err) return done(err); - assertUntouchedTokens(done); + assertPreservedTokens(done); }); }); @@ -2068,7 +2068,7 @@ describe('User', function() { email: currentEmailCredentials.email, }, function(err, userInstance) { if (err) return done(err); - assertUntouchedTokens(done); + assertPreservedTokens(done); }); }); @@ -2303,9 +2303,11 @@ describe('User', function() { function assertPreservedTokens(done) { AccessToken.find({where: {userId: user.id}}, function(err, tokens) { if (err) return done(err); - expect(tokens.length).to.equal(2); - expect([tokens[0].id, tokens[1].id]).to.have.members([originalUserToken1, - originalUserToken2]); + var actualIds = tokens.map(function(t) { return t.id; }); + actualIds.sort(); + var expectedIds = [originalUserToken1.id, originalUserToken2.id]; + expectedIds.sort(); + expect(actualIds).to.eql(expectedIds); done(); }); } @@ -2317,14 +2319,6 @@ describe('User', function() { done(); }); } - - function assertUntouchedTokens(done) { - AccessToken.find({where: {userId: user.id}}, function(err, tokens) { - if (err) return done(err); - expect(tokens.length).to.equal(2); - done(); - }); - } }); describe('Verification after updating email', function() { From afd6dd707361981c9b2b47005677316d97396158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 13 Jan 2017 11:03:06 +0100 Subject: [PATCH 135/187] Preserve current session when invalidating tokens Fix User model to preserve the current session (provided via "options.accessToken") when invalidating access tokens after a change of email or password property. --- common/models/user.js | 18 ++++++++++++--- test/user.integration.js | 48 ++++++++++++++++++++++++++++++++++++++++ test/user.test.js | 13 +++++++++++ 3 files changed, 76 insertions(+), 3 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index 27e1c9bdb..92df8f0ff 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -647,7 +647,12 @@ module.exports = function(User) { throw err; }; - User._invalidateAccessTokensOfUsers = function(userIds, cb) { + User._invalidateAccessTokensOfUsers = function(userIds, options, cb) { + if (typeof options === 'function' && cb === undefined) { + cb = options; + options = {}; + } + if (!Array.isArray(userIds) || !userIds.length) return process.nextTick(cb); @@ -656,7 +661,14 @@ module.exports = function(User) { return process.nextTick(cb); var AccessToken = accessTokenRelation.modelTo; - AccessToken.deleteAll({userId: {inq: userIds}}, cb); + + var query = {userId: {inq: userIds}}; + var tokenPK = AccessToken.definition.idName() || 'id'; + if (options.accessToken && tokenPK in options.accessToken) { + query[tokenPK] = {neq: options.accessToken[tokenPK]}; + } + + AccessToken.deleteAll(query, options, cb); }; /*! @@ -873,7 +885,7 @@ module.exports = function(User) { }).map(function(u) { return u.id; }); - ctx.Model._invalidateAccessTokensOfUsers(userIdsToExpire, next); + ctx.Model._invalidateAccessTokensOfUsers(userIdsToExpire, ctx.options, next); }); }; diff --git a/test/user.integration.js b/test/user.integration.js index 46f597c58..45ea04b93 100644 --- a/test/user.integration.js +++ b/test/user.integration.js @@ -100,6 +100,54 @@ describe('users - integration', function() { done(); }); }); + + it('invalidates current session when options are not injected', function(done) { + // "injectOptionsFromRemoteContext" is disabled by default, + // therefore the code invalidating sessions cannot tell what + // is the current session, and thus invalidates all sessions + var url = '/api/users/' + userId; + var self = this; + this.request.patch(url) + .send({email: 'new@example.com'}) + .set('Authorization', accessToken) + .expect(200, function(err) { + if (err) return done(err); + self.get(url) + .set('Authorization', accessToken) + .expect(401, done); + }); + }); + + it('should preserve current session when invalidating tokens', function(done) { + var UserWithContext = app.registry.createModel({ + name: 'UserWithContext', + plural: 'ctx-users', + base: 'User', + injectOptionsFromRemoteContext: true + }); + app.model(UserWithContext, {dataSource: 'db'}); + + var self = this; + var CREDENTIALS = {email: 'ctx@example.com', password: 'pass'}; + UserWithContext.create(CREDENTIALS, function(err, user) { + if (err) return done(err); + + UserWithContext.login(CREDENTIALS, function(err, token) { + if (err) return done(err); + + var url = '/api/ctx-users/' + user.id; + self.request.patch(url) + .send({email: 'new@example.com'}) + .set('Authorization', token.id) + .expect(200, function(err) { + if (err) return done(err); + self.get(url) + .set('Authorization', token.id) + .expect(200, done); + }); + }); + }); + }); }); describe('sub-user', function() { diff --git a/test/user.test.js b/test/user.test.js index 808575f42..01c42e7fb 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -2252,6 +2252,19 @@ describe('User', function() { }); }); + it('preserves current session', function(done) { + var options = {accessToken: originalUserToken1}; + user.updateAttribute('email', 'new@example.com', options, function(err) { + if (err) return done(err); + AccessToken.find({where: {userId: user.id}}, function(err, tokens) { + if (err) return done(err); + var tokenIds = tokens.map(function(t) { return t.id; }); + expect(tokenIds).to.eql([originalUserToken1.id]); + done(); + }); + }); + }); + it('preserves other user sessions if their password is untouched', function(done) { var user1, user2, user1Token; async.series([ From d35e1a1b6fa875a71ac3579f140c2ba5ca00d2d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 16 Jan 2017 12:00:57 +0100 Subject: [PATCH 136/187] 2.37.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Preserve current session when invalidating tokens (Miroslav Bajtoš) * Clean up access-token-invalidation tests (Miroslav Bajtoš) --- CHANGES.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 302d42572..9aa5d3a07 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,11 @@ +2017-01-16, Version 2.37.1 +========================== + + * Preserve current session when invalidating tokens (Miroslav Bajtoš) + + * Clean up access-token-invalidation tests (Miroslav Bajtoš) + + 2017-01-09, Version 2.37.0 ========================== diff --git a/package.json b/package.json index de8e2213b..958d90649 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.37.0", + "version": "2.37.1", "publishConfig": { "tag": "lts" }, From b8f9b85609754db56808766592aa97ff900b70b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ribeiro?= Date: Sun, 11 Dec 2016 03:12:54 +0000 Subject: [PATCH 137/187] Fix User.resetPassword to call createAccessToken() This allows User subclasses to override the algorithm used for building one-time access tokens for password recovery. --- common/models/user.js | 2 +- test/user.test.js | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/common/models/user.js b/common/models/user.js index 92df8f0ff..27fafa7c4 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -605,7 +605,7 @@ module.exports = function(User) { return cb(err); } - user.accessTokens.create({ ttl: ttl }, function(err, accessToken) { + user.createAccessToken(ttl, function(err, accessToken) { if (err) { return cb(err); } diff --git a/test/user.test.js b/test/user.test.js index 01c42e7fb..18967744f 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -1884,6 +1884,19 @@ describe('User', function() { }); }); + it('calls createAccessToken() to create the token', function(done) { + User.prototype.createAccessToken = function(ttl, cb) { + cb(null, new AccessToken({id: 'custom-token'})); + }; + + User.resetPassword({email: options.email}, function() {}); + + User.once('resetPasswordRequest', function(info) { + expect(info.accessToken.id).to.equal('custom-token'); + done(); + }); + }); + it('Password reset over REST rejected without email address', function(done) { request(app) .post('/test-users/reset') From f1e31ca50ca8a7bb58291f0b463245108287e312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 17 Jan 2017 10:44:25 +0100 Subject: [PATCH 138/187] Add app setting logoutSessionsOnSensitiveChanges Disable invalidation of access tokens by default to restore backwards compatibility with older 2.x versions. Add a new application-wide flag logoutSessionsOnSensitiveChanges that can be used to explicitly turn on/off the token invalidation. When the flag is not set, a verbose warning is printed to nudge the user to make a decision how they want to handle token invalidation. --- common/models/user.js | 29 +++++++++++++++++++ test/access-token.test.js | 2 ++ test/app.test.js | 3 ++ .../access-control/server/config.json | 3 +- .../server/config.json | 1 + .../simple-integration-app/server/config.json | 3 +- .../user-integration-app/server/config.json | 3 +- test/model.test.js | 1 + test/replication.rest.test.js | 2 ++ test/rest.middleware.test.js | 1 + test/role.test.js | 2 ++ test/support.js | 1 + test/user.test.js | 12 ++++++++ 13 files changed, 60 insertions(+), 3 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index 27fafa7c4..6288734b4 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -807,6 +807,31 @@ module.exports = function(User) { UserModel.validatesUniquenessOf('username', {message: 'User already exists'}); } + UserModel.once('attached', function() { + if (UserModel.app.get('logoutSessionsOnSensitiveChanges') !== undefined) + return; + + g.warn([ + '', + 'The user model %j is attached to an application that does not specify', + 'whether other sessions should be invalidated when a password or', + 'an email has changed. Session invalidation is important for security', + 'reasons as it allows users to recover from various account breach', + 'situations.', + '', + 'We recommend turning this feature on by setting', + '"{{logoutSessionsOnSensitiveChanges}}" to {{true}} in', + '{{server/config.json}} (unless you have implemented your own solution', + 'for token invalidation).', + '', + 'We also recommend enabling "{{injectOptionsFromRemoteContext}}" in', + '%s\'s settings (typically via common/models/*.json file).', + 'This setting is required for the invalidation algorithm to keep ', + 'the current session valid.', + '' + ].join('\n'), UserModel.modelName, UserModel.modelName); + }); + return UserModel; }; @@ -832,6 +857,8 @@ module.exports = function(User) { // Delete old sessions once email is updated User.observe('before save', function beforeEmailUpdate(ctx, next) { + if (!ctx.Model.app.get('logoutSessionsOnSensitiveChanges')) return next(); + var emailChanged; if (ctx.isNewInstance) return next(); if (!ctx.where && !ctx.instance) return next(); @@ -872,6 +899,8 @@ module.exports = function(User) { }); User.observe('after save', function afterEmailUpdate(ctx, next) { + if (!ctx.Model.app.get('logoutSessionsOnSensitiveChanges')) return next(); + if (!ctx.instance && !ctx.data) return next(); if (!ctx.hookState.originalUserData) return next(); diff --git a/test/access-token.test.js b/test/access-token.test.js index e86feca99..9bf2d0b80 100644 --- a/test/access-token.test.js +++ b/test/access-token.test.js @@ -590,6 +590,7 @@ function createTestApp(testToken, settings, done) { }, settings.token); var app = loopback(); + app.set('logoutSessionsOnSensitiveChanges', true); app.use(cookieParser('secret')); app.use(loopback.token(tokenSettings)); @@ -652,6 +653,7 @@ function createTestApp(testToken, settings, done) { function givenLocalTokenModel() { var app = loopback({ localRegistry: true, loadBuiltinModels: true }); + app.set('logoutSessionsOnSensitiveChanges', true); app.dataSource('db', { connector: 'memory' }); var User = app.registry.getModel('User'); diff --git a/test/app.test.js b/test/app.test.js index 66bac4e7d..7df1e7937 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -718,6 +718,7 @@ describe('app', function() { beforeEach(function() { app = loopback(); + app.set('logoutSessionsOnSensitiveChanges', true); app.dataSource('db', { connector: 'memory' }); @@ -922,6 +923,7 @@ describe('app', function() { var AUTH_MODELS = ['User', 'ACL', 'AccessToken', 'Role', 'RoleMapping']; var app = loopback({ localRegistry: true, loadBuiltinModels: true }); require('../lib/builtin-models')(app.registry); + app.set('logoutSessionsOnSensitiveChanges', true); var db = app.dataSource('db', { connector: 'memory' }); app.enableAuth({ dataSource: 'db' }); @@ -937,6 +939,7 @@ describe('app', function() { it('detects already configured subclass of a required model', function() { var app = loopback({ localRegistry: true, loadBuiltinModels: true }); + app.set('logoutSessionsOnSensitiveChanges', true); var db = app.dataSource('db', { connector: 'memory' }); var Customer = app.registry.createModel('Customer', {}, { base: 'User' }); app.model(Customer, { dataSource: 'db' }); diff --git a/test/fixtures/access-control/server/config.json b/test/fixtures/access-control/server/config.json index 67364ab47..087d25bef 100644 --- a/test/fixtures/access-control/server/config.json +++ b/test/fixtures/access-control/server/config.json @@ -1,5 +1,6 @@ { "port": 3000, "host": "0.0.0.0", + "logoutSessionsOnSensitiveChanges": true, "legacyExplorer": false -} \ No newline at end of file +} diff --git a/test/fixtures/shared-methods/model-config-defined-false/server/config.json b/test/fixtures/shared-methods/model-config-defined-false/server/config.json index 61ed16d6c..69e75d3a9 100644 --- a/test/fixtures/shared-methods/model-config-defined-false/server/config.json +++ b/test/fixtures/shared-methods/model-config-defined-false/server/config.json @@ -23,6 +23,7 @@ "disableStackTrace": false } }, + "logoutSessionsOnSensitiveChanges": true, "legacyExplorer": false } diff --git a/test/fixtures/simple-integration-app/server/config.json b/test/fixtures/simple-integration-app/server/config.json index c7cce73ed..2dfdfd1fd 100644 --- a/test/fixtures/simple-integration-app/server/config.json +++ b/test/fixtures/simple-integration-app/server/config.json @@ -11,5 +11,6 @@ "limit": "8kb" } }, + "logoutSessionsOnSensitiveChanges": true, "legacyExplorer": false -} \ No newline at end of file +} diff --git a/test/fixtures/user-integration-app/server/config.json b/test/fixtures/user-integration-app/server/config.json index c7cce73ed..2dfdfd1fd 100644 --- a/test/fixtures/user-integration-app/server/config.json +++ b/test/fixtures/user-integration-app/server/config.json @@ -11,5 +11,6 @@ "limit": "8kb" } }, + "logoutSessionsOnSensitiveChanges": true, "legacyExplorer": false -} \ No newline at end of file +} diff --git a/test/model.test.js b/test/model.test.js index 2ed1f0f2f..727d9fa99 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -856,6 +856,7 @@ describe.onServer('Remote Methods', function() { function setupAppAndRequest() { app = loopback({localRegistry: true, loadBuiltinModels: true}); + app.set('logoutSessionsOnSensitiveChanges', true); app.dataSource('db', {connector: 'memory'}); diff --git a/test/replication.rest.test.js b/test/replication.rest.test.js index 9c436b2f4..a6b5026ae 100644 --- a/test/replication.rest.test.js +++ b/test/replication.rest.test.js @@ -462,6 +462,7 @@ describe('Replication over REST', function() { function setupServer(done) { serverApp = loopback(); + serverApp.set('logoutSessionsOnSensitiveChanges', true); serverApp.enableAuth(); serverApp.dataSource('db', { connector: 'memory' }); @@ -514,6 +515,7 @@ describe('Replication over REST', function() { function setupClient() { clientApp = loopback(); + clientApp.set('logoutSessionsOnSensitiveChanges', true); clientApp.dataSource('db', { connector: 'memory' }); clientApp.dataSource('remote', { connector: 'remote', diff --git a/test/rest.middleware.test.js b/test/rest.middleware.test.js index 58fd91ab3..56307d5ce 100644 --- a/test/rest.middleware.test.js +++ b/test/rest.middleware.test.js @@ -13,6 +13,7 @@ describe('loopback.rest', function() { // override the global app object provided by test/support.js // and create a local one that does not share state with other tests app = loopback({ localRegistry: true, loadBuiltinModels: true }); + app.set('logoutSessionsOnSensitiveChanges', true); var db = app.dataSource('db', { connector: 'memory' }); MyModel = app.registry.createModel('MyModel'); MyModel.attachTo(db); diff --git a/test/role.test.js b/test/role.test.js index a7bbf8b7b..aeb9f5cd2 100644 --- a/test/role.test.js +++ b/test/role.test.js @@ -23,6 +23,7 @@ describe('role model', function() { // Use local app registry to ensure models are isolated to avoid // pollutions from other tests app = loopback({ localRegistry: true, loadBuiltinModels: true }); + app.set('logoutSessionsOnSensitiveChanges', true); app.dataSource('db', { connector: 'memory' }); ACL = app.registry.getModel('ACL'); @@ -735,6 +736,7 @@ describe('role model', function() { describe('isOwner', function() { it('supports app-local model registry', function(done) { var app = loopback({ localRegistry: true, loadBuiltinModels: true }); + app.set('logoutSessionsOnSensitiveChanges', true); app.dataSource('db', { connector: 'memory' }); // attach all auth-related models to 'db' datasource app.enableAuth({ dataSource: 'db' }); diff --git a/test/support.js b/test/support.js index 915f38dc4..327f0fc0f 100644 --- a/test/support.js +++ b/test/support.js @@ -23,6 +23,7 @@ loopback.User.settings.saltWorkFactor = 4; beforeEach(function() { this.app = app = loopback(); + app.set('logoutSessionsOnSensitiveChanges', true); // setup default data sources loopback.setDefaultDataSourceForType('db', { diff --git a/test/user.test.js b/test/user.test.js index 18967744f..ec88d6716 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -31,6 +31,7 @@ describe('User', function() { // override the global app object provided by test/support.js // and create a local one that does not share state with other tests app = loopback({ localRegistry: true, loadBuiltinModels: true }); + app.set('logoutSessionsOnSensitiveChanges', true); app.dataSource('db', { connector: 'memory' }); // setup Email model, it's needed by User tests @@ -2326,6 +2327,17 @@ describe('User', function() { }); }); + it('preserves all sessions when logoutSessionsOnSensitiveChanges is disabled', + function(done) { + app.set('logoutSessionsOnSensitiveChanges', false); + user.updateAttributes( + {email: updatedEmailCredentials.email}, + function(err, userInstance) { + if (err) return done(err); + assertPreservedTokens(done); + }); + }); + function assertPreservedTokens(done) { AccessToken.find({where: {userId: user.id}}, function(err, tokens) { if (err) return done(err); From 6fcb7dba6a244cd64eec34c513b37f0aecd7f7be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 20 Jan 2017 15:10:26 +0100 Subject: [PATCH 139/187] 2.38.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add app setting logoutSessionsOnSensitiveChanges (Miroslav Bajtoš) * Fix User.resetPassword to call createAccessToken() (João Ribeiro) --- CHANGES.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 9aa5d3a07..e100ebd11 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,11 @@ +2017-01-20, Version 2.38.0 +========================== + + * Add app setting logoutSessionsOnSensitiveChanges (Miroslav Bajtoš) + + * Fix User.resetPassword to call createAccessToken() (João Ribeiro) + + 2017-01-16, Version 2.37.1 ========================== diff --git a/package.json b/package.json index 958d90649..353f7476f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.37.1", + "version": "2.38.0", "publishConfig": { "tag": "lts" }, From 5e7e7ca7e9063c5d5f366d703ebb6eb1853386d9 Mon Sep 17 00:00:00 2001 From: Aris Kemper Date: Thu, 5 Jan 2017 16:34:15 +0100 Subject: [PATCH 140/187] Fix User methods to use correct Primary Key Do not use hard-coded "id" property name, call `idName()` to get the name of the PK property. --- common/models/user.js | 23 ++++++++++--- test/user.test.js | 76 +++++++++++++++++++++++-------------------- 2 files changed, 59 insertions(+), 40 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index 6288734b4..c193ac42a 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -384,6 +384,7 @@ module.exports = function(User) { var user = this; var userModel = this.constructor; var registry = userModel.registry; + var pkName = userModel.definition.idName() || 'id'; assert(typeof options === 'object', 'options required when calling user.verify()'); assert(options.type, 'You must supply a verification type (options.type)'); assert(options.type === 'email', 'Unsupported verification type'); @@ -418,7 +419,7 @@ module.exports = function(User) { displayPort + urlPath + '?uid=' + - options.user.id + + options.user[pkName] + '&redirect=' + options.redirect; @@ -477,7 +478,7 @@ module.exports = function(User) { if (err) { fn(err); } else { - fn(null, { email: email, token: user.verificationToken, uid: user.id }); + fn(null, {email: email, token: user.verificationToken, uid: user[pkName]}); } }); } @@ -862,6 +863,7 @@ module.exports = function(User) { var emailChanged; if (ctx.isNewInstance) return next(); if (!ctx.where && !ctx.instance) return next(); + var pkName = ctx.Model.definition.idName() || 'id'; var isPartialUpdateChangingPassword = ctx.data && 'password' in ctx.data; @@ -874,11 +876,21 @@ module.exports = function(User) { ctx.hookState.isPasswordChange = isPartialUpdateChangingPassword || isFullReplaceChangingPassword; - var where = ctx.where || {id: ctx.instance.id}; + var where; + if (ctx.where) { + where = ctx.where; + } else { + where = {}; + where[pkName] = ctx.instance[pkName]; + } + ctx.Model.find({where: where}, function(err, userInstances) { if (err) return next(err); ctx.hookState.originalUserData = userInstances.map(function(u) { - return { id: u.id, email: u.email }; + var user = {}; + user[pkName] = u[pkName]; + user['email'] = u['email']; + return user; }); if (ctx.instance) { emailChanged = ctx.instance.email !== ctx.hookState.originalUserData[0].email; @@ -904,6 +916,7 @@ module.exports = function(User) { if (!ctx.instance && !ctx.data) return next(); if (!ctx.hookState.originalUserData) return next(); + var pkName = ctx.Model.definition.idName() || 'id'; var newEmail = (ctx.instance || ctx.data).email; var isPasswordChange = ctx.hookState.isPasswordChange; @@ -912,7 +925,7 @@ module.exports = function(User) { var userIdsToExpire = ctx.hookState.originalUserData.filter(function(u) { return (newEmail && u.email !== newEmail) || isPasswordChange; }).map(function(u) { - return u.id; + return u[pkName]; }); ctx.Model._invalidateAccessTokensOfUsers(userIdsToExpire, ctx.options, next); }); diff --git a/test/user.test.js b/test/user.test.js index ec88d6716..7969497ec 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -43,11 +43,17 @@ describe('User', function() { app.model(Email, { dataSource: 'email' }); // attach User and related models - // forceId is set to false for the purpose of updating the same affected user within the - // `Email Update` test cases. - User = app.registry.createModel('TestUser', {}, { + User = app.registry.createModel({ + name: 'TestUser', base: 'User', - http: { path: 'test-users' }, + properties: { + // Use a custom id property to verify that User methods + // are correctly looking up the primary key + pk: {type: 'String', defaultFn: 'guid', id: true}, + }, + http: {path: 'test-users'}, + // forceId is set to false for the purpose of updating the same affected user within the + // `Email Update` test cases. forceId: false, }); app.model(User, { dataSource: 'db' }); @@ -83,7 +89,7 @@ describe('User', function() { it('Create a new user', function(done) { User.create({email: 'f@b.com', password: 'bar'}, function(err, user) { assert(!err); - assert(user.id); + assert(user.pk); assert(user.email); done(); @@ -95,7 +101,7 @@ describe('User', function() { User.create({email: 'F@b.com', password: 'bar'}, function(err, user) { if (err) return done(err); - assert(user.id); + assert(user.pk); assert.equal(user.email, user.email.toLowerCase()); done(); @@ -106,7 +112,7 @@ describe('User', function() { User.create({email: 'F@b.com', password: 'bar'}, function(err, user) { if (err) return done(err); - assert(user.id); + assert(user.pk); assert(user.email); assert.notEqual(user.email, user.email.toLowerCase()); @@ -246,7 +252,7 @@ describe('User', function() { async.series([ function(next) { User.create({ email: 'b@c.com', password: 'bar' }, function(err, user) { - usersId = user.id; + usersId = user.pk; next(err); }); }, @@ -289,7 +295,7 @@ describe('User', function() { { name: 'myname', email: 'd@c.com', password: 'bar' }, ], function(err, users) { userIds = users.map(function(u) { - return u.id; + return u.pk; }); next(err); }); @@ -418,7 +424,7 @@ describe('User', function() { it('accepts passwords that are exactly 72 characters long', function(done) { User.create({ email: 'b@c.com', password: pass72Char }, function(err, user) { if (err) return done(err); - User.findById(user.id, function(err, userFound) { + User.findById(user.pk, function(err, userFound) { if (err) return done(err); assert(userFound); done(); @@ -1068,7 +1074,7 @@ describe('User', function() { it('logs in a user by with realm', function(done) { User.login(credentialWithRealm, function(err, accessToken) { assertGoodToken(accessToken); - assert.equal(accessToken.userId, user1.id); + assert.equal(accessToken.userId, user1.pk); done(); }); @@ -1077,7 +1083,7 @@ describe('User', function() { it('logs in a user by with realm in username', function(done) { User.login(credentialRealmInUsername, function(err, accessToken) { assertGoodToken(accessToken); - assert.equal(accessToken.userId, user1.id); + assert.equal(accessToken.userId, user1.pk); done(); }); @@ -1086,7 +1092,7 @@ describe('User', function() { it('logs in a user by with realm in email', function(done) { User.login(credentialRealmInEmail, function(err, accessToken) { assertGoodToken(accessToken); - assert.equal(accessToken.userId, user1.id); + assert.equal(accessToken.userId, user1.pk); done(); }); @@ -1104,7 +1110,7 @@ describe('User', function() { it('logs in a user by with realm', function(done) { User.login(credentialWithRealm, function(err, accessToken) { assertGoodToken(accessToken); - assert.equal(accessToken.userId, user1.id); + assert.equal(accessToken.userId, user1.pk); done(); }); @@ -1222,7 +1228,7 @@ describe('User', function() { var u = new User({username: 'a', password: 'b', email: 'z@z.net'}); u.save(function(err, user) { - User.findById(user.id, function(err, uu) { + User.findById(user.pk, function(err, uu) { uu.hasPassword('b', function(err, isMatch) { assert(isMatch); @@ -1234,7 +1240,7 @@ describe('User', function() { it('should match a password after it is changed', function(done) { User.create({email: 'foo@baz.net', username: 'bat', password: 'baz'}, function(err, user) { - User.findById(user.id, function(err, foundUser) { + User.findById(user.pk, function(err, foundUser) { assert(foundUser); foundUser.hasPassword('baz', function(err, isMatch) { assert(isMatch); @@ -1242,7 +1248,7 @@ describe('User', function() { foundUser.save(function(err, updatedUser) { updatedUser.hasPassword('baz2', function(err, isMatch) { assert(isMatch); - User.findById(user.id, function(err, uu) { + User.findById(user.pk, function(err, uu) { uu.hasPassword('baz2', function(err, isMatch) { assert(isMatch); @@ -2038,7 +2044,7 @@ describe('User', function() { it('invalidates sessions when email is changed using `updateOrCreate`', function(done) { User.updateOrCreate({ - id: user.id, + pk: user.pk, email: updatedEmailCredentials.email, }, function(err, userInstance) { if (err) return done(err); @@ -2049,7 +2055,7 @@ describe('User', function() { it('invalidates sessions after `replaceById`', function(done) { // The way how the invalidation is implemented now, all sessions // are invalidated on a full replace - User.replaceById(user.id, currentEmailCredentials, function(err, userInstance) { + User.replaceById(user.pk, currentEmailCredentials, function(err, userInstance) { if (err) return done(err); assertNoAccessTokens(done); }); @@ -2059,7 +2065,7 @@ describe('User', function() { // The way how the invalidation is implemented now, all sessions // are invalidated on a full replace User.replaceOrCreate({ - id: user.id, + pk: user.pk, email: currentEmailCredentials.email, password: currentEmailCredentials.password, }, function(err, userInstance) { @@ -2077,7 +2083,7 @@ describe('User', function() { it('keeps sessions AS IS if firstName is added using `updateOrCreate`', function(done) { User.updateOrCreate({ - id: user.id, + pk: user.pk, firstName: 'Loay', email: currentEmailCredentials.email, }, function(err, userInstance) { @@ -2144,7 +2150,7 @@ describe('User', function() { }, function updatePartialUser(next) { User.updateAll( - {id: userPartial.id}, + {pk: userPartial.pk}, {age: userPartial.age + 1}, function(err, info) { if (err) return next(err); @@ -2152,7 +2158,7 @@ describe('User', function() { }); }, function verifyTokensOfPartialUser(next) { - AccessToken.find({where: {userId: userPartial.id}}, function(err, tokens1) { + AccessToken.find({where: {userId: userPartial.pk}}, function(err, tokens1) { if (err) return next(err); expect(tokens1.length).to.equal(1); next(); @@ -2204,11 +2210,11 @@ describe('User', function() { }); }, function(next) { - AccessToken.find({where: {userId: user1.id}}, function(err, tokens1) { + AccessToken.find({where: {userId: user1.pk}}, function(err, tokens1) { if (err) return next(err); - AccessToken.find({where: {userId: user2.id}}, function(err, tokens2) { + AccessToken.find({where: {userId: user2.pk}}, function(err, tokens2) { if (err) return next(err); - AccessToken.find({where: {userId: user3.id}}, function(err, tokens3) { + AccessToken.find({where: {userId: user3.pk}}, function(err, tokens3) { if (err) return next(err); expect(tokens1.length).to.equal(1); @@ -2249,7 +2255,7 @@ describe('User', function() { }); }, function verifyTokensOfSpecialUser(next) { - AccessToken.find({where: {userId: userSpecial.id}}, function(err, tokens1) { + AccessToken.find({where: {userId: userSpecial.pk}}, function(err, tokens1) { if (err) return done(err); expect(tokens1.length, 'tokens - special user tokens').to.equal(0); next(); @@ -2270,7 +2276,7 @@ describe('User', function() { var options = {accessToken: originalUserToken1}; user.updateAttribute('email', 'new@example.com', options, function(err) { if (err) return done(err); - AccessToken.find({where: {userId: user.id}}, function(err, tokens) { + AccessToken.find({where: {userId: user.pk}}, function(err, tokens) { if (err) return done(err); var tokenIds = tokens.map(function(t) { return t.id; }); expect(tokenIds).to.eql([originalUserToken1.id]); @@ -2311,9 +2317,9 @@ describe('User', function() { }); }, function(next) { - AccessToken.find({where: {userId: user1.id}}, function(err, tokens1) { + AccessToken.find({where: {userId: user1.pk}}, function(err, tokens1) { if (err) return next(err); - AccessToken.find({where: {userId: user2.id}}, function(err, tokens2) { + AccessToken.find({where: {userId: user2.pk}}, function(err, tokens2) { if (err) return next(err); expect(tokens1.length).to.equal(1); expect(tokens2.length).to.equal(0); @@ -2339,7 +2345,7 @@ describe('User', function() { }); function assertPreservedTokens(done) { - AccessToken.find({where: {userId: user.id}}, function(err, tokens) { + AccessToken.find({where: {userId: user.pk}}, function(err, tokens) { if (err) return done(err); var actualIds = tokens.map(function(t) { return t.id; }); actualIds.sort(); @@ -2351,7 +2357,7 @@ describe('User', function() { } function assertNoAccessTokens(done) { - AccessToken.find({where: {userId: user.id}}, function(err, tokens) { + AccessToken.find({where: {userId: user.pk}}, function(err, tokens) { if (err) return done(err); expect(tokens.length).to.equal(0); done(); @@ -2377,7 +2383,7 @@ describe('User', function() { }); }, function findUser(next) { - User.findById(userInstance.id, function(err, info) { + User.findById(userInstance.pk, function(err, info) { if (err) return next(err); assert.equal(info.email, NEW_EMAIL); assert.equal(info.emailVerified, false); @@ -2399,7 +2405,7 @@ describe('User', function() { }); }, function findUser(next) { - User.findById(userInstance.id, function(err, info) { + User.findById(userInstance.pk, function(err, info) { if (err) return next(err); assert.equal(info.email, NEW_EMAIL); assert.equal(info.emailVerified, true); @@ -2421,7 +2427,7 @@ describe('User', function() { }); }, function findUser(next) { - User.findById(userInstance.id, function(err, info) { + User.findById(userInstance.pk, function(err, info) { if (err) return next(err); assert.equal(info.realm, 'test'); assert.equal(info.emailVerified, true); From 56ad85ae2ad88f1e5e93cc81363b44d08363c8d8 Mon Sep 17 00:00:00 2001 From: Benjamin Kroeger Date: Mon, 16 Jan 2017 15:23:33 +0100 Subject: [PATCH 141/187] Role model: resolves related models by name Resolve models related to the `Role` model by name instead of class. --- common/models/role.js | 6 +++--- test/role.test.js | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/common/models/role.js b/common/models/role.js index e7e93c51c..a09f7563b 100644 --- a/common/models/role.js +++ b/common/models/role.js @@ -36,9 +36,9 @@ module.exports = function(Role) { Role.resolveRelatedModels = function() { if (!this.userModel) { var reg = this.registry; - this.roleMappingModel = reg.getModelByType(loopback.RoleMapping); - this.userModel = reg.getModelByType(loopback.User); - this.applicationModel = reg.getModelByType(loopback.Application); + this.roleMappingModel = reg.getModelByType('RoleMapping'); + this.userModel = reg.getModelByType('User'); + this.applicationModel = reg.getModelByType('Application'); } }; diff --git a/test/role.test.js b/test/role.test.js index aeb9f5cd2..2e9875951 100644 --- a/test/role.test.js +++ b/test/role.test.js @@ -45,9 +45,6 @@ describe('role model', function() { ACL.roleMappingModel = RoleMapping; ACL.userModel = User; ACL.applicationModel = Application; - Role.roleMappingModel = RoleMapping; - Role.userModel = User; - Role.applicationModel = Application; }); it('should define role/role relations', function(done) { From 98110f1b84c38fb5a3661a79a0bf14763e1dd6ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 26 Jan 2017 10:10:11 +0100 Subject: [PATCH 142/187] Use English when running Mocha tests --- Gruntfile.js | 3 ++- test/helpers/use-english.js | 10 ++++++++++ test/mocha.opts | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 test/helpers/use-english.js create mode 100644 test/mocha.opts diff --git a/Gruntfile.js b/Gruntfile.js index 71312e60a..4ff9df7f6 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -88,7 +88,8 @@ module.exports = function(grunt) { src: 'test/*.js', options: { reporter: 'dot', - } + require: require.resolve('./test/helpers/use-english.js'), + }, }, 'unit-xml': { src: 'test/*.js', diff --git a/test/helpers/use-english.js b/test/helpers/use-english.js new file mode 100644 index 000000000..0d5c81a1e --- /dev/null +++ b/test/helpers/use-english.js @@ -0,0 +1,10 @@ +'use strict'; + +var env = process.env; + +// delete any user-provided language settings +delete env.LC_ALL; +delete env.LC_MESSAGES; +delete env.LANG; +delete env.LANGUAGE; +delete env.STRONGLOOP_GLOBALIZE_APP_LANGUAGE; diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 000000000..1d8363384 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1 @@ +--require ./test/helpers/use-english From 1dac9ada0b471bd78d59d0c357848bf936003005 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Wed, 1 Jul 2015 12:13:09 -0700 Subject: [PATCH 143/187] Fix logout to handle no or missing accessToken Return 401 when the request does not provide any accessToken argument or the token was not found. Also simplify the implementation of the `logout` method to make only a single database call (`deleteById`) instead of `findById` + `delete`. --- common/models/user.js | 22 ++++++++++++++++------ test/user.integration.js | 11 +++++++++++ test/user.test.js | 16 ++++++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index c193ac42a..be45933df 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -293,13 +293,23 @@ module.exports = function(User) { User.logout = function(tokenId, fn) { fn = fn || utils.createPromiseCallback(); - this.relations.accessTokens.modelTo.findById(tokenId, function(err, accessToken) { + + if (!tokenId) { + var err = new Error(g.f('{{accessToken}} is required to logout')); + err.status = 401; + process.nextTick(function() { fn(err); }); + return fn.promise; + } + + this.relations.accessTokens.modelTo.destroyById(tokenId, function(err, info) { if (err) { fn(err); - } else if (accessToken) { - accessToken.destroy(fn); + } else if ('count' in info && info.count === 0) { + err = new Error(g.f('Could not find {{accessToken}}')); + err.status = 401; + fn(err); } else { - fn(new Error(g.f('could not find {{accessToken}}'))); + fn(); } }); return fn.promise; @@ -743,10 +753,10 @@ module.exports = function(User) { { description: 'Logout a user with access token.', accepts: [ - {arg: 'access_token', type: 'string', required: true, http: function(ctx) { + {arg: 'access_token', type: 'string', http: function(ctx) { var req = ctx && ctx.req; var accessToken = req && req.accessToken; - var tokenID = accessToken && accessToken.id; + var tokenID = accessToken ? accessToken.id : undefined; return tokenID; }, description: 'Do not supply this argument, it is automatically extracted ' + 'from request headers.', diff --git a/test/user.integration.js b/test/user.integration.js index 45ea04b93..a27b4ed4b 100644 --- a/test/user.integration.js +++ b/test/user.integration.js @@ -148,6 +148,17 @@ describe('users - integration', function() { }); }); }); + + it('returns 401 on logout with no access token', function(done) { + this.post('/api/users/logout') + .expect(401, done); + }); + + it('returns 401 on logout with invalid access token', function(done) { + this.post('/api/users/logout') + .set('Authorization', 'unknown-token') + .expect(401, done); + }); }); describe('sub-user', function() { diff --git a/test/user.test.js b/test/user.test.js index 7969497ec..2b7dbffc5 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -1186,6 +1186,22 @@ describe('User', function() { } }); + it('fails when accessToken is not provided', function(done) { + User.logout(undefined, function(err) { + expect(err).to.have.property('message'); + expect(err).to.have.property('status', 401); + done(); + }); + }); + + it('fails when accessToken is not found', function(done) { + User.logout('expired-access-token', function(err) { + expect(err).to.have.property('message'); + expect(err).to.have.property('status', 401); + done(); + }); + }); + function verify(token, done) { assert(token); From 6a4198896ff73ec0553d1824eb774244d443bd64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 30 Jan 2017 15:09:59 +0100 Subject: [PATCH 144/187] Remove unused dependencies - strong-error-handler - eslint These dependencies were most likely added accidentally by fea3b781. --- package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package.json b/package.json index 353f7476f..084f66e8a 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,6 @@ "browserify": "^13.1.0", "chai": "^3.5.0", "es5-shim": "^4.1.0", - "eslint-config-loopback": "^1.0.0", "express-session": "^1.14.0", "grunt": "^1.0.1", "grunt-browserify": "^5.0.0", @@ -94,7 +93,6 @@ "phantomjs-prebuilt": "^2.1.7", "sinon": "^1.13.0", "sinon-chai": "^2.8.0", - "strong-error-handler": "^1.0.1", "strong-task-emitter": "^0.0.6", "supertest": "^2.0.0", "supertest-as-promised": "^4.0.2" From 05db4337cf2663e5b5997915bb39a64058c1b5bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 30 Jan 2017 11:30:05 +0100 Subject: [PATCH 145/187] Preserve sessions on User.save() making no changes --- common/models/user.js | 35 ++++++++++++----------------------- test/user.test.js | 7 +++++++ 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index be45933df..03056188c 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -866,30 +866,16 @@ module.exports = function(User) { next(); }); - // Delete old sessions once email is updated - User.observe('before save', function beforeEmailUpdate(ctx, next) { + User.observe('before save', function prepareForTokenInvalidation(ctx, next) { if (!ctx.Model.app.get('logoutSessionsOnSensitiveChanges')) return next(); - var emailChanged; if (ctx.isNewInstance) return next(); if (!ctx.where && !ctx.instance) return next(); - var pkName = ctx.Model.definition.idName() || 'id'; - - var isPartialUpdateChangingPassword = ctx.data && 'password' in ctx.data; - - // Full replace of User instance => assume password change. - // HashPassword returns a different value for each invocation, - // therefore we cannot tell whether ctx.instance.password is the same - // or not. - var isFullReplaceChangingPassword = !!ctx.instance; - ctx.hookState.isPasswordChange = isPartialUpdateChangingPassword || - isFullReplaceChangingPassword; + var pkName = ctx.Model.definition.idName() || 'id'; - var where; - if (ctx.where) { - where = ctx.where; - } else { + var where = ctx.where; + if (!where) { where = {}; where[pkName] = ctx.instance[pkName]; } @@ -899,9 +885,11 @@ module.exports = function(User) { ctx.hookState.originalUserData = userInstances.map(function(u) { var user = {}; user[pkName] = u[pkName]; - user['email'] = u['email']; + user.email = u.email; + user.password = u.password; return user; }); + var emailChanged; if (ctx.instance) { emailChanged = ctx.instance.email !== ctx.hookState.originalUserData[0].email; if (emailChanged && ctx.Model.settings.emailVerificationRequired) { @@ -920,7 +908,7 @@ module.exports = function(User) { }); }); - User.observe('after save', function afterEmailUpdate(ctx, next) { + User.observe('after save', function invalidateOtherTokens(ctx, next) { if (!ctx.Model.app.get('logoutSessionsOnSensitiveChanges')) return next(); if (!ctx.instance && !ctx.data) return next(); @@ -928,12 +916,13 @@ module.exports = function(User) { var pkName = ctx.Model.definition.idName() || 'id'; var newEmail = (ctx.instance || ctx.data).email; - var isPasswordChange = ctx.hookState.isPasswordChange; + var newPassword = (ctx.instance || ctx.data).password; - if (!newEmail && !isPasswordChange) return next(); + if (!newEmail && !newPassword) return next(); var userIdsToExpire = ctx.hookState.originalUserData.filter(function(u) { - return (newEmail && u.email !== newEmail) || isPasswordChange; + return (newEmail && u.email !== newEmail) || + (newPassword && u.password !== newPassword); }).map(function(u) { return u[pkName]; }); diff --git a/test/user.test.js b/test/user.test.js index 2b7dbffc5..d468d4b81 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -2097,6 +2097,13 @@ describe('User', function() { }); }); + it('keeps sessions AS IS when calling save() with no changes', function(done) { + user.save(function(err) { + if (err) return done(err); + assertPreservedTokens(done); + }); + }); + it('keeps sessions AS IS if firstName is added using `updateOrCreate`', function(done) { User.updateOrCreate({ pk: user.pk, From 0cc2b5b8db280154466e3bce6850a5dfde963d94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 31 Jan 2017 15:53:41 +0100 Subject: [PATCH 146/187] Fix detection of logoutSessionsOnSensitiveChanges Modify the code detecting whether logoutSessionsOnSensitiveChanges is enabled to correctly handle the case when the model is not attached to any application, as is the case with loopback-component-passport tests. --- common/models/user.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index 03056188c..4929fb843 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -867,7 +867,9 @@ module.exports = function(User) { }); User.observe('before save', function prepareForTokenInvalidation(ctx, next) { - if (!ctx.Model.app.get('logoutSessionsOnSensitiveChanges')) return next(); + var invalidationEnabled = ctx.Model.app && + ctx.Model.app.get('logoutSessionsOnSensitiveChanges'); + if (!invalidationEnabled) return next(); if (ctx.isNewInstance) return next(); if (!ctx.where && !ctx.instance) return next(); @@ -909,7 +911,9 @@ module.exports = function(User) { }); User.observe('after save', function invalidateOtherTokens(ctx, next) { - if (!ctx.Model.app.get('logoutSessionsOnSensitiveChanges')) return next(); + var invalidationEnabled = ctx.Model.app && + ctx.Model.app.get('logoutSessionsOnSensitiveChanges'); + if (!invalidationEnabled) return next(); if (!ctx.instance && !ctx.data) return next(); if (!ctx.hookState.originalUserData) return next(); From 8c76d7fc0141565f0d0bce8c2ab247bfa0d608b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 8 Feb 2017 09:02:39 +0100 Subject: [PATCH 147/187] Include link to docs in logoutSessions warning --- common/models/user.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/common/models/user.js b/common/models/user.js index 4929fb843..a06f0c43b 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -839,7 +839,10 @@ module.exports = function(User) { '%s\'s settings (typically via common/models/*.json file).', 'This setting is required for the invalidation algorithm to keep ', 'the current session valid.', - '' + '', + 'Learn more in our documentation at', + 'https://loopback.io/doc/en/lb2/AccessToken-invalidation.html', + '', ].join('\n'), UserModel.modelName, UserModel.modelName); }); From 09b1fce34b681ff3b23040b2d452acab74c7e851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 6 Feb 2017 12:39:21 +0100 Subject: [PATCH 148/187] Fix creation of verification links Fix User.prototype.verify to call `querystring.stringify` instead of concatenating query-string components directly. In particular, this fixes the bug where `options.redirect` containing a hash fragment like `#/home?arg1=value1&arg2=value2` produced incorrect URL, because the `redirect` value was not correctly encoded. --- common/models/user.js | 9 +++++---- test/user.test.js | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index a06f0c43b..077cc3c17 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -13,6 +13,7 @@ var isEmail = require('isemail'); var loopback = require('../../lib/loopback'); var utils = require('../../lib/utils'); var path = require('path'); +var qs = require('querystring'); var SALT_WORK_FACTOR = 10; var crypto = require('crypto'); var MAX_PASSWORD_LENGTH = 72; @@ -428,10 +429,10 @@ module.exports = function(User) { options.host + displayPort + urlPath + - '?uid=' + - options.user[pkName] + - '&redirect=' + - options.redirect; + '?' + qs.stringify({ + uid: options.user[pkName], + redirect: options.redirect, + }); options.templateFn = options.templateFn || createVerificationEmailBody; diff --git a/test/user.test.js b/test/user.test.js index d468d4b81..8074a5fef 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -7,6 +7,7 @@ require('./support'); var loopback = require('../'); var User, AccessToken; var async = require('async'); +var url = require('url'); describe('User', function() { this.timeout(10000); @@ -1700,6 +1701,29 @@ describe('User', function() { expect(result.email).to.not.have.property('template'); }); }); + + it('allows hash fragment in redirectUrl', function() { + return User.create({email: 'test@example.com', password: 'pass'}) + .then(function(user) { + var actualVerifyHref; + return user.verify({ + type: 'email', + to: user.email, + from: 'noreply@myapp.org', + redirect: '#/some-path?a=1&b=2', + templateFn: function(options, cb) { + actualVerifyHref = options.verifyHref; + cb(null, 'dummy body'); + }, + }) + .then(function() { return actualVerifyHref; }); + }) + .then(function(verifyHref) { + var parsed = url.parse(verifyHref, true); + expect(parsed.query.redirect, 'redirect query') + .to.equal('#/some-path?a=1&b=2'); + }); + }); }); describe('User.confirm(options, fn)', function() { From a4154caf59effd324a8790d0005b95acc72f9900 Mon Sep 17 00:00:00 2001 From: Raymond Camden Date: Mon, 20 Feb 2017 10:30:01 -0600 Subject: [PATCH 149/187] Improve "filter" arg description Add an example showing how to serialize object values as JSON. --- lib/persisted-model.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/persisted-model.js b/lib/persisted-model.js index c19520eec..65e32d934 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -749,7 +749,9 @@ module.exports = function(registry) { { arg: 'id', type: 'any', description: 'Model id', required: true, http: {source: 'path'}}, {arg: 'filter', type: 'object', - description: 'Filter defining fields and include'}, + description: + 'Filter defining fields and include - must be a JSON-encoded string (' + + '{"something":"value"})'}, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, ]), returns: {arg: 'data', type: typeName, root: true}, @@ -782,7 +784,8 @@ module.exports = function(registry) { accessType: 'READ', accepts: this._removeOptionsArgIfDisabled([ {arg: 'filter', type: 'object', description: - 'Filter defining fields, where, include, order, offset, and limit'}, + 'Filter defining fields, where, include, order, offset, and limit - must be a ' + + 'JSON-encoded string ({"something":"value"})'}, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, ]), returns: {arg: 'data', type: [typeName], root: true}, @@ -794,7 +797,8 @@ module.exports = function(registry) { accessType: 'READ', accepts: this._removeOptionsArgIfDisabled([ {arg: 'filter', type: 'object', description: - 'Filter defining fields, where, include, order, offset, and limit'}, + 'Filter defining fields, where, include, order, offset, and limit - must be a ' + + 'JSON-encoded string ({"something":"value"})'}, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, ]), returns: {arg: 'data', type: typeName, root: true}, From e334884fb19a900b201fabc3ef19c3902b3704c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 22 Feb 2017 15:19:55 +0100 Subject: [PATCH 150/187] Configure Travis CI to cache phantomjs binaries This should speed up our CI builds and also save a lot of bandwidth for people providing phantomjs-prebuilt module. See also https://www.npmjs.com/package/phantomjs-prebuilt#continuous-integration --- .travis.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.travis.yml b/.travis.yml index d431fa429..501feed6b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,3 +6,15 @@ node_js: - "4" - "6" +# see https://www.npmjs.com/package/phantomjs-prebuilt#continuous-integration +cache: + directories: + - travis_phantomjs +before_install: + # Upgrade PhantomJS to v2.1.1. + - "export PHANTOMJS_VERSION=2.1.1" + - "export PATH=$PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64/bin:$PATH" + - "if [ $(phantomjs --version) != $PHANTOMJS_VERSION ]; then rm -rf $PWD/travis_phantomjs; mkdir -p $PWD/travis_phantomjs; fi" + - "if [ $(phantomjs --version) != $PHANTOMJS_VERSION ]; then wget https://github.com/Medium/phantomjs/releases/download/v$PHANTOMJS_VERSION/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 -O $PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2; fi" + - "if [ $(phantomjs --version) != $PHANTOMJS_VERSION ]; then tar -xvf $PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 -C $PWD/travis_phantomjs; fi" + - "phantomjs --version" From 91502db9f1da0f4263e85f5074bf559844cd33d8 Mon Sep 17 00:00:00 2001 From: phairow Date: Mon, 6 Mar 2017 19:27:40 -0800 Subject: [PATCH 151/187] Fix User.verify to convert uid to string Applications using MongoDB connectors typically have `user.id` property of type ObjectID. This commit fixes the code building the verification URL to correctly convert the user id value into string. --- common/models/user.js | 2 +- test/user.test.js | 47 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/common/models/user.js b/common/models/user.js index 077cc3c17..d2a445a33 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -430,7 +430,7 @@ module.exports = function(User) { displayPort + urlPath + '?' + qs.stringify({ - uid: options.user[pkName], + uid: '' + options.user[pkName], redirect: options.redirect, }); diff --git a/test/user.test.js b/test/user.test.js index 8074a5fef..daa94eea9 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -1428,6 +1428,53 @@ describe('User', function() { }); }); + it('converts uid value to string', function(done) { + var idString = '58be263abc88dd483956030a'; + var actualVerifyHref; + + User.afterRemote('create', function(ctx, user, next) { + assert(user, 'afterRemote should include result'); + + var options = { + type: 'email', + to: user.email, + from: 'noreply@myapp.org', + redirect: '/', + protocol: ctx.req.protocol, + host: ctx.req.get('host'), + templateFn: function(options, cb) { + actualVerifyHref = options.verifyHref; + cb(null, 'dummy body'); + }, + }; + + // replace the string id with an object + // TODO: find a better way to do this + Object.defineProperty(user, 'pk', { + get: function() { return this.__data.pk; }, + set: function(value) { this.__data.pk = value; }, + }); + user.pk = {toString: function() { return idString; }}; + + user.verify(options, function(err, result) { + expect(result.uid).to.be.an('object'); + expect(result.uid.toString()).to.equal(idString); + var parsed = url.parse(actualVerifyHref, true); + expect(parsed.query.uid, 'uid query field').to.eql(idString); + done(); + }); + }); + + request(app) + .post('/test-users') + .expect('Content-Type', /json/) + .expect(200) + .send({email: 'bar@bat.com', password: 'bar', pk: idString}) + .end(function(err, res) { + if (err) return done(err); + }); + }); + it('Verify a user\'s email address with custom token generator', function(done) { User.afterRemote('create', function(ctx, user, next) { assert(user, 'afterRemote should include result'); From 45284c3bf96b32b0fbc58be99e7349e3e3ef9543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 13 Mar 2017 16:22:17 +0100 Subject: [PATCH 152/187] 2.38.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix User.verify to convert uid to string (phairow) * Configure Travis CI to cache phantomjs binaries (Miroslav Bajtoš) * Improve "filter" arg description (Raymond Camden) * Fix creation of verification links (Miroslav Bajtoš) * Include link to docs in logoutSessions warning (Miroslav Bajtoš) * Fix detection of logoutSessionsOnSensitiveChanges (Miroslav Bajtoš) * Preserve sessions on User.save() making no changes (Miroslav Bajtoš) * Remove unused dependencies (Miroslav Bajtoš) * Fix logout to handle no or missing accessToken (Ritchie Martori) * Use English when running Mocha tests (Miroslav Bajtoš) * Role model: resolves related models by name (Benjamin Kroeger) * Fix User methods to use correct Primary Key (Aris Kemper) --- CHANGES.md | 28 ++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index e100ebd11..88c2034c0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,31 @@ +2017-03-13, Version 2.38.1 +========================== + + * Fix User.verify to convert uid to string (phairow) + + * Configure Travis CI to cache phantomjs binaries (Miroslav Bajtoš) + + * Improve "filter" arg description (Raymond Camden) + + * Fix creation of verification links (Miroslav Bajtoš) + + * Include link to docs in logoutSessions warning (Miroslav Bajtoš) + + * Fix detection of logoutSessionsOnSensitiveChanges (Miroslav Bajtoš) + + * Preserve sessions on User.save() making no changes (Miroslav Bajtoš) + + * Remove unused dependencies (Miroslav Bajtoš) + + * Fix logout to handle no or missing accessToken (Ritchie Martori) + + * Use English when running Mocha tests (Miroslav Bajtoš) + + * Role model: resolves related models by name (Benjamin Kroeger) + + * Fix User methods to use correct Primary Key (Aris Kemper) + + 2017-01-20, Version 2.38.0 ========================== diff --git a/package.json b/package.json index 084f66e8a..7c68870e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.38.0", + "version": "2.38.1", "publishConfig": { "tag": "lts" }, From 4713e5e7ea421938219e1b2fa8c247c231fdaff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 31 Jan 2017 13:22:22 +0100 Subject: [PATCH 153/187] Add nyc coverage, report data to coveralls.io --- .gitignore | 1 + .nycrc | 7 +++++++ .travis.yml | 1 + package.json | 5 ++++- 4 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 .nycrc diff --git a/.gitignore b/.gitignore index a30033bdf..ba976d833 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ node_modules dist *xunit.xml +.nyc_output/ diff --git a/.nycrc b/.nycrc new file mode 100644 index 000000000..b64607eee --- /dev/null +++ b/.nycrc @@ -0,0 +1,7 @@ +{ + "exclude": [ + "Gruntfile.js", + "test/**/*.js" + ], + "cache": true +} diff --git a/.travis.yml b/.travis.yml index 501feed6b..ec6072c9e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ node_js: - "0.12" - "4" - "6" +after_success: npm run coverage # see https://www.npmjs.com/package/phantomjs-prebuilt#continuous-integration cache: diff --git a/package.json b/package.json index 7c68870e2..30a0f33e8 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "mBaaS" ], "scripts": { - "test": "grunt mocha-and-karma" + "coverage": "nyc report --reporter=text-lcov | coveralls", + "test": "nyc grunt mocha-and-karma" }, "dependencies": { "async": "^2.0.1", @@ -67,6 +68,7 @@ "browserify": "^13.1.0", "chai": "^3.5.0", "es5-shim": "^4.1.0", + "coveralls": "^2.11.15", "express-session": "^1.14.0", "grunt": "^1.0.1", "grunt-browserify": "^5.0.0", @@ -90,6 +92,7 @@ "loopback-datasource-juggler": "^2.19.1", "loopback-testing": "^1.4.0", "mocha": "^3.0.0", + "nyc": "^10.1.2", "phantomjs-prebuilt": "^2.1.7", "sinon": "^1.13.0", "sinon-chai": "^2.8.0", From cfb0148e53dabe69159a2986fb654b669d164b9a Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 17 Mar 2017 08:46:35 -0700 Subject: [PATCH 154/187] Fix file patch --- docs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs.json b/docs.json index 29bb0008b..c951ec91f 100644 --- a/docs.json +++ b/docs.json @@ -5,7 +5,7 @@ "lib/server-app.js", "lib/loopback.js", "lib/registry.js", - "server/current-context.js", + "lib/current-context.js", "lib/access-context.js", { "title": "Base models", "depth": 2 }, "lib/model.js", From 78161ccd9b867d7cea7717210b51ade0a2f3e74c Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 17 Mar 2017 08:54:26 -0700 Subject: [PATCH 155/187] 2.38.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix file patch (Raymond Feng) * Add nyc coverage, report data to coveralls.io (Miroslav Bajtoš) --- CHANGES.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 88c2034c0..242939ef6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,11 @@ +2017-03-17, Version 2.38.2 +========================== + + * Fix file patch (Raymond Feng) + + * Add nyc coverage, report data to coveralls.io (Miroslav Bajtoš) + + 2017-03-13, Version 2.38.1 ========================== diff --git a/package.json b/package.json index 30a0f33e8..de4a1d4b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.38.1", + "version": "2.38.2", "publishConfig": { "tag": "lts" }, From a5ac1506e66b2149756e0d3e500402c7950608eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 24 Mar 2017 14:57:41 +0100 Subject: [PATCH 156/187] Forward options in prepareForTokenInvalidation --- common/models/user.js | 2 +- test/user.test.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/common/models/user.js b/common/models/user.js index d2a445a33..d44ccf35c 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -886,7 +886,7 @@ module.exports = function(User) { where[pkName] = ctx.instance[pkName]; } - ctx.Model.find({where: where}, function(err, userInstances) { + ctx.Model.find({where: where}, ctx.options, function(err, userInstances) { if (err) return next(err); ctx.hookState.originalUserData = userInstances.map(function(u) { var user = {}; diff --git a/test/user.test.js b/test/user.test.js index daa94eea9..db2fbb4de 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -8,6 +8,7 @@ var loopback = require('../'); var User, AccessToken; var async = require('async'); var url = require('url'); +var extend = require('util')._extend; describe('User', function() { this.timeout(10000); @@ -2379,6 +2380,37 @@ describe('User', function() { }); }); + it('forwards the "options" argument', function(done) { + var options = {testFlag: true}; + var observedOptions = []; + + saveObservedOptionsForHook('access', User); + saveObservedOptionsForHook('before delete', AccessToken); + + user.updateAttribute('password', 'newPass', options, function(err, updated) { + if (err) return done(err); + + expect(observedOptions).to.eql([ + // prepareForTokenInvalidation - load current instance data + {hook: 'access', testFlag: true}, + + // validate uniqueness of User.email + {hook: 'access', testFlag: true}, + + // _invalidateAccessTokensOfUsers - deleteAll + {hook: 'before delete', testFlag: true}, + ]); + done(); + }); + + function saveObservedOptionsForHook(name, model) { + model.observe(name, function(ctx, next) { + observedOptions.push(extend({hook: name}, ctx.options)); + next(); + }); + } + }); + it('preserves other user sessions if their password is untouched', function(done) { var user1, user2, user1Token; async.series([ From 50e0e4808aa00e7fb03e184434acb9ba19e7c61e Mon Sep 17 00:00:00 2001 From: Aaron Buchanan Date: Fri, 17 Mar 2017 17:08:17 -0700 Subject: [PATCH 157/187] Fix user-literal rewrite for anonymous requests Currently any `currentUserLiteral` routes when accessed with a bad token throw a 500 due to a SQL error that is raised because `Model.findById` is invoked with `id={currentUserLiteral}` (`id=me` in our case) when the url rewrite fails. This commit changes the token middleware to return 401 Not Authorized when the client is requesting a currentUserLiteral route without a valid access token. --- server/middleware/token.js | 38 ++++++++++++++++++++++++++++---------- test/access-token.test.js | 20 ++++++++++++++++++++ 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/server/middleware/token.js b/server/middleware/token.js index 2ceb701d4..58ca056fc 100644 --- a/server/middleware/token.js +++ b/server/middleware/token.js @@ -7,6 +7,8 @@ * Module dependencies. */ +'use strict'; +var g = require('strong-globalize')(); var loopback = require('../../lib/loopback'); var assert = require('assert'); var debug = require('debug')('loopback:middleware:token'); @@ -20,18 +22,33 @@ module.exports = token; /* * Rewrite the url to replace current user literal with the logged in user id */ -function rewriteUserLiteral(req, currentUserLiteral) { - if (req.accessToken && req.accessToken.userId && currentUserLiteral) { +function rewriteUserLiteral(req, currentUserLiteral, next) { + if (!currentUserLiteral) return next(); + var literalRegExp = new RegExp('/' + currentUserLiteral + '(/|$|\\?)', 'g'); + + if (req.accessToken && req.accessToken.userId) { // Replace /me/ with /current-user-id/ var urlBeforeRewrite = req.url; - req.url = req.url.replace( - new RegExp('/' + currentUserLiteral + '(/|$|\\?)', 'g'), + req.url = req.url.replace(literalRegExp, '/' + req.accessToken.userId + '$1'); + if (req.url !== urlBeforeRewrite) { debug('req.url has been rewritten from %s to %s', urlBeforeRewrite, req.url); } + } else if (!req.accessToken && literalRegExp.test(req.url)) { + debug( + 'URL %s matches current-user literal %s,' + + ' but no (valid) access token was provided.', + req.url, currentUserLiteral); + + var e = new Error(g.f('Authorization Required')); + e.status = e.statusCode = 401; + e.code = 'AUTHORIZATION_REQUIRED'; + return next(e); } + + next(); } function escapeRegExp(str) { @@ -110,23 +127,24 @@ function token(options) { if (!enableDoublecheck) { // req.accessToken is defined already (might also be "null" or "false") and enableDoublecheck // has not been set --> skip searching for credentials - rewriteUserLiteral(req, currentUserLiteral); - return next(); + return rewriteUserLiteral(req, currentUserLiteral, next); } if (req.accessToken && req.accessToken.id && !overwriteExistingToken) { // req.accessToken.id is defined, which means that some other middleware has identified a valid user. // when overwriteExistingToken is not set to a truthy value, skip searching for credentials. - rewriteUserLiteral(req, currentUserLiteral); - return next(); + return rewriteUserLiteral(req, currentUserLiteral, next); } // continue normal operation (as if req.accessToken was undefined) } + TokenModel.findForRequest(req, options, function(err, token) { req.accessToken = token || null; - rewriteUserLiteral(req, currentUserLiteral); + var ctx = req.loopbackContext; if (ctx && ctx.active) ctx.set('accessToken', token); - next(err); + + if (err) return next(err); + rewriteUserLiteral(req, currentUserLiteral, next); }); }; } diff --git a/test/access-token.test.js b/test/access-token.test.js index 9bf2d0b80..7e4363eb9 100644 --- a/test/access-token.test.js +++ b/test/access-token.test.js @@ -190,6 +190,26 @@ describe('loopback.token(options)', function() { }); }); + it('should generate a 401 on a current user literal route without an authToken', + function(done) { + var app = createTestApp(null, done); + request(app) + .get('/users/me') + .set('authorization', null) + .expect(401) + .end(done); + }); + + it('should generate a 401 on a current user literal route with invalid authToken', + function(done) { + var app = createTestApp(this.token, done); + request(app) + .get('/users/me') + .set('Authorization', 'invald-token-id') + .expect(401) + .end(done); + }); + it('should skip when req.token is already present', function(done) { var tokenStub = { id: 'stub id' }; app.use(function(req, res, next) { From 1ec7a265a7293db124d282a1e6b4dc3a0358a6a1 Mon Sep 17 00:00:00 2001 From: Diana Lau Date: Tue, 11 Apr 2017 14:36:41 -0400 Subject: [PATCH 158/187] update translation msg --- intl/en/messages.json | 150 ++++++++++++++++-------------------------- 1 file changed, 55 insertions(+), 95 deletions(-) diff --git a/intl/en/messages.json b/intl/en/messages.json index 10e29a3ac..4e6decd4b 100644 --- a/intl/en/messages.json +++ b/intl/en/messages.json @@ -1,115 +1,75 @@ { - "3b46d3a780fd6ae5f95ade489a0efffe": "Current context is not supported in the browser.", - "7e0fca41d098607e1c9aa353c67e0fa1": "Invalid Access Token", - "320c482401afa1207c04343ab162e803": "Invalid principal type: {0}", - "c2b5d51f007178170ca3952d59640ca4": "Cannot rectify {0} changes:\n{1}", - "5858e63efaa0e4ad86b61c0459ea32fa": "You must connect the {{Email}} Model to a {{Mail}} connector", + "03f79fa268fe199de2ce4345515431c1": "No change record found for {0} with id {1}", + "04bd8af876f001ceaf443aad6a9002f9": "Authentication requires model {0} to be defined.", + "0731d0109e46c21a4e34af3346ed4856": "This behaviour may change in the next major version.", + "095afbf2f1f0e5be678f5dac5c54e717": "Access Denied", "0caffe1d763c8cca6a61814abe33b776": "Email is required", - "1b2a6076dccbe91a56f1672eb3b8598c": "The response body contains properties of the {{AccessToken}} created on login.\nDepending on the value of `include` parameter, the body may contain additional properties:\n\n - `user` - `U+007BUserU+007D` - Data of the currently logged in user. {{(`include=user`)}}\n\n", - "2362ba55796c733e337af169922327a2": "Related objects to include in the response. See the description of return value for more details.", - "306999d39387d87b2638199ff0bed8ad": "Reset password for a user with email.", - "3aae63bb7e8e046641767571c1591441": "login failed as the email has not been verified", - "3caaa84fc103d6d5612173ae6d43b245": "Invalid token: {0}", - "42e3fa18945255412ebc6561e2c6f1dc": "Confirm a user registration with email verification token.", - "430b6326d7ebf6600a39f614ef516bc8": "Do not supply this argument, it is automatically extracted from request headers.", - "44a6c8b1ded4ed653d19ddeaaf89a606": "Email not found", - "5e81ad3847a290dc650b47618b9cbc7e": "login failed", - "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "Login a user with username/email and password.", - "8608c28f5e6df0008266e3c497836176": "Logout a user with access token.", - "860d1a0b8bd340411fb32baa72867989": "The transport does not support HTTP redirects.", - "895b1f941d026870b3cc8e6af087c197": "{{username}} or {{email}} is required", - "a50d10fc6e0959b220e085454c40381e": "User not found: {0}", - "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} is required", - "c34fa20eea0091747fcc9eda204b8d37": "could not find {{accessToken}}", - "c68a93f0a9524fed4ff64372fc90c55f": "Must provide a valid email", - "f58cdc481540cd1f69a4aa4da2e37981": "Invalid password: {0}", - "f0bd73df8714cefb925e3b8da2f4c5f6": "result:{0}", "10e01c895dc0b2fecc385f9f462f1ca6": "a list of colors is available at {{http://localhost:3000/colors}}", - "8a27e0c9ce3ebf0e0c3978efb456e13e": "Request to host {0}", - "a40684f5a9f546115258b76938d1de37": "A list of colors is available at {{http://localhost:3000/colors}}", + "1b2a6076dccbe91a56f1672eb3b8598c": "The response body contains properties of the {{AccessToken}} created on login.\nDepending on the value of `include` parameter, the body may contain additional properties:\n\n - `user` - `U+007BUserU+007D` - Data of the currently logged in user. {{(`include=user`)}}\n\n", + "1d7833c3ca2f05fdad8fad7537531c40": "\t SUBJECT:{0}", "1e85f822b547a75d7d385048030e4ecb": "Created: {0}", - "7d5e7ed0efaedf3f55f380caae0df8b8": "My first mobile application", - "04bd8af876f001ceaf443aad6a9002f9": "Authentication requires model {0} to be defined.", - "095afbf2f1f0e5be678f5dac5c54e717": "Access Denied", + "275f22ab95671f095640ca99194b7635": "\t FROM:{0}", "2d3071e3b18681c80a090dc0efbdb349": "could not find {0} with id {1}", + "2d78192c43fd2ec52ec18f3918894f9a": "{0} middleware is deprecated. See {1} for more details.", + "308e1d484516a33df788f873e65faaff": "Model `{0}` is extending deprecated `DataModel. Use `PersistedModel` instead.", "316e5b82c203cf3de31a449ee07d0650": "Expected boolean, got {0}", - "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Cannot create data source {0}: {1}", - "7e287fc885d9fdcf42da3a12f38572c1": "Authorization Required", - "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} was removed, use the new module {{loopback-boot}} instead", - "1d7833c3ca2f05fdad8fad7537531c40": "\t SUBJECT:{0}", - "275f22ab95671f095640ca99194b7635": "\t FROM:{0}", + "320c482401afa1207c04343ab162e803": "Invalid principal type: {0}", + "3438fab56cc7ab92dfd88f0497e523e0": "The relations property of `{0}` configuration must be an object", + "35e5252c62d80f8c54a5290d30f4c7d0": "Please verify your email by opening this link in a web browser:\n\t{0}", + "3aae63bb7e8e046641767571c1591441": "login failed as the email has not been verified", + "3aecb24fa8bdd3f79d168761ca8a6729": "Unknown {{middleware}} phase {0}", + "3caaa84fc103d6d5612173ae6d43b245": "Invalid token: {0}", + "3d617953470be16d0c2b32f0bcfbb5ee": "Thanks for Registering", "3d63008ccfb2af1db2142e8cc2716ace": "Warning: No email transport specified for sending email. Setup a transport to send mail messages.", + "4203ab415ec66a78d3164345439ba76e": "Cannot call {0}.{1}(). The {2} method has not been setup. The {{PersistedModel}} has not been correctly attached to a {{DataSource}}!", + "44a6c8b1ded4ed653d19ddeaaf89a606": "Email not found", "4a4f04a4e480fc5d4ee73b84d9a4b904": "Sending Mail:", - "63a091ced88001ab6acb58f61ec041c5": "\t TEXT:{0}", - "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", - "ecb06666ef95e5db27a5ac1d6a17923b": "\t TO:{0}", - "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORT:{0}", - "0da38687fed24275c1547e815914a8e3": "Delete a related item by id for {0}.", - "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "Criteria to match model instances", - "22fe62fa8d595b72c62208beddaa2a56": "Update a related item by id for {0}.", - "528325f3cbf1b0ab9a08447515daac9a": "Update {0} of this model.", - "543d19bad5e47ee1e9eb8af688e857b4": "Foreign key for {0}.", - "598ff0255ffd1d1b71e8de55dbe2c034": "Check the existence of {0} relation to an item by id.", - "5a36cc6ba0cc27c754f6c5ed6015ea3c": "Remove the {0} relation to an item by id.", + "4b494de07f524703ac0879addbd64b13": "Email has not been verified", + "4cac5f051ae431321673e04045d37772": "Model `{0}` is extending an unknown model `{1}`. Using `PersistedModel` as the base.", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Cannot create data source {0}: {1}", + "5858e63efaa0e4ad86b61c0459ea32fa": "You must connect the {{Email}} Model to a {{Mail}} connector", + "5e81ad3847a290dc650b47618b9cbc7e": "login failed", "5fa3afb425819ebde958043e598cb664": "could not find a model with {{id}} {0}", "61e5deebaf44d68f4e6a508f30cc31a3": "Relation `{0}` does not exist for model `{1}`", - "651f0b3cbba001635152ec3d3d954d0a": "Find a related item by id for {0}.", - "7bc7b301ad9c4fc873029d57fb9740fe": "Queries {0} of {1}.", - "7c837b88fd0e509bd3fc722d7ddf0711": "Foreign key for {0}", - "830cb6c862f8f364e9064cea0026f701": "Fetches hasOne relation {0}.", - "855ecd4a64885ba272d782435f72a4d4": "Unknown \"{0}\" id \"{1}\".", - "86254879d01a60826a851066987703f2": "Add a related item by id for {0}.", - "8ae418c605b6a45f2651be9b1677c180": "Invalid remote method: `{0}`", - "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Fetches belongsTo relation {0}.", - "c0057a569ff9d3b509bac61a4b2f605d": "Deletes all {0} of this model.", - "cd0412f2f33a4a2a316acc834f3f21a6": "must specify an {{id}} or {{data}}", - "d6f43b266533b04d442bdb3955622592": "Creates a new instance in {0} of this model.", - "da13d3cdf21330557254670dddd8c5c7": "Counts {0} of {1}.", - "e4434de4bb8f5a3cd1d416e4d80d7e0b": "Unknown \"{0}\" {{id}} \"{1}\".", - "f66ae3cf379b2fce28575a3282defe1a": "Deletes {0} of this model.", - "03f79fa268fe199de2ce4345515431c1": "No change record found for {0} with id {1}", - "0f1c71f74b040bfbe8d384a414e31f03": "Get a set of deltas and conflicts since the given checkpoint.", - "15254dec061d023d6c030083a0cef50f": "Create a new instance of the model and persist it into the data source.", - "16a11368d55b85a209fc6aea69ee5f7a": "Delete all matching records.", - "1bc1d489ddf347af47af3d9b1fc7cc15": "Run multiple updates at once. Note: this is not atomic.", - "1bc7d8283c9abda512692925c8d8e3c0": "Get the current checkpoint.", - "1caa7cc61266e7aef7db7d2f0e27ac3e": "Update the properties of the most recent change record kept for this instance.", - "2a7df74fe6e8462e617b79d5fbb536ea": "Get the most recent change record for this instance.", - "2a9684b3d5b3b67af74bac74eb1b0843": "Find all instances of the model matched by filter from the data source.", - "2e50838caf0c927735eb15d12866bdd7": "Get the changes to a model since a given checkpoint.Provide a filter object to reduce the number of results returned.", - "4203ab415ec66a78d3164345439ba76e": "Cannot call {0}.{1}(). The {2} method has not been setup. The {{PersistedModel}} has not been correctly attached to a {{DataSource}}!", - "51ea9b6245bb5e672b236d640ca3b048": "An object of Change property name/value pairs", - "55ddedd4c501348f82cb89db02ec85c1": "An object of model property name/value pairs", - "5aaa76c72ae1689fd3cf62589784a4ba": "Update attributes for a model instance and persist it into the data source.", - "5f659bbc15e6e2b249fa33b3879b5f69": "Find a model instance by {{id}} from the data source.", "62e8b0a733417978bab22c8dacf5d7e6": "Cannot apply bulk updates, the connector does not correctly report the number of updated records.", - "6329e0ac1de3c250ebb1df5afd5a2a7b": "The number of instances updated", + "63a091ced88001ab6acb58f61ec041c5": "\t TEXT:{0}", "6bc376432cd9972cf991aad3de371e78": "Missing data for change: {0}", - "79295ac04822d2e9702f0dd1d0240336": "Update instances of the model matched by {{where}} from the data source.", - "7f2fde7f0f860ead224b11ba8d75aa1c": "Create an update list from a delta list.", - "89b57e764c2267129294b07589dbfdc2": "Delete a model instance by {{id}} from the data source.", - "8bab6720ecc58ec6412358c858a53484": "Bulk update failed, the connector has modified unexpected number of records: {0}", - "a98b6cc6547706b5c6bffde0ed5fd55c": "Find first instance of the model matched by filter from the data source.", - "c46d4aba1f14809c16730faa46933495": "Filter defining fields and include", - "c65600640f206f585d300b4bcb699d95": "Create a checkpoint.", - "cf64c7afc74d3a8120abcd028f98c770": "Update an existing model instance or insert a new one into the data source.", - "dcb6261868ff0a7b928aa215b07d068c": "Create a change stream.", - "e43e320a435ec1fa07648c1da0d558a7": "Check whether a model instance exists in the data source.", - "e92aa25b6b864e3454b65a7c422bd114": "Bulk update failed, the connector has deleted unexpected number of records: {0}", - "ea63d226b6968e328bdf6876010786b5": "Cannot apply bulk updates, the connector does not correctly report the number of deleted records.", - "f1d4ac54357cc0932f385d56814ba7e4": "Conflict", - "f37d94653793e33f4203e45b4a1cf106": "Count instances of the model matched by where from the data source.", - "0731d0109e46c21a4e34af3346ed4856": "This behaviour may change in the next major version.", - "2e110abee2c95bcfc2dafd48be7e2095": "Cannot configure {0}: {{config.dataSource}} must be an instance of {{DataSource}}", - "308e1d484516a33df788f873e65faaff": "Model `{0}` is extending deprecated `DataModel. Use `PersistedModel` instead.", - "3438fab56cc7ab92dfd88f0497e523e0": "The relations property of `{0}` configuration must be an object", - "4cac5f051ae431321673e04045d37772": "Model `{0}` is extending an unknown model `{1}`. Using `PersistedModel` as the base.", + "705c2d456a3e204c4af56e671ec3225c": "Could not find {{accessToken}}", "734a7bebb65e10899935126ba63dd51f": "The options property of `{0}` configuration must be an object", "779467f467862836e19f494a37d6ab77": "The acls property of `{0}` configuration must be an array of objects", + "7d5e7ed0efaedf3f55f380caae0df8b8": "My first mobile application", + "7e0fca41d098607e1c9aa353c67e0fa1": "Invalid Access Token", + "7e287fc885d9fdcf42da3a12f38572c1": "Authorization Required", + "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} is required to logout", "80a32e80cbed65eba2103201a7c94710": "Model not found: {0}", "83cbdc2560ba9f09155ccfc63e08f1a1": "Property `{0}` cannot be reconfigured for `{1}`", + "855ecd4a64885ba272d782435f72a4d4": "Unknown \"{0}\" id \"{1}\".", + "860d1a0b8bd340411fb32baa72867989": "The transport does not support HTTP redirects.", + "895b1f941d026870b3cc8e6af087c197": "{{username}} or {{email}} is required", + "8a17c5ef611e2e7535792316e66b8fca": "Password too long: {0}", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "Request to host {0}", + "8ae418c605b6a45f2651be9b1677c180": "Invalid remote method: `{0}`", + "8bab6720ecc58ec6412358c858a53484": "Bulk update failed, the connector has modified unexpected number of records: {0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", "97795efe0c3eb7f35ce8cf8cfe70682b": "The configuration of `{0}` is missing {{`dataSource`}} property.\nUse `null` or `false` to mark models not attached to any data source.", + "a40684f5a9f546115258b76938d1de37": "A list of colors is available at {{http://localhost:3000/colors}}", + "a50d10fc6e0959b220e085454c40381e": "User not found: {0}", "a80038252430df2754884bf3c845c4cf": "Remoting metadata for \"{0}.{1}\" is missing \"isStatic\" flag, the method is registered as an instance method.", + "b6f740aeb6f2eb9bee9cb049dbfe6a28": "Unknown \"{0}\" {{key}} \"{1}\".", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} is required", + "c2b5d51f007178170ca3952d59640ca4": "Cannot rectify {0} changes:\n{1}", + "c68a93f0a9524fed4ff64372fc90c55f": "Must provide a valid email", + "cd0412f2f33a4a2a316acc834f3f21a6": "must specify an {{id}} or {{data}}", + "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} was removed, use the new module {{loopback-boot}} instead", + "d9ef6dc3770dd8f80a129e92a79851f3": "{0} is deprecated. See {1} for more details.", "dc568bee32deb0f6eaf63e73b20e8ceb": "Ignoring non-object \"methods\" setting of \"{0}\".", - "3aecb24fa8bdd3f79d168761ca8a6729": "Unknown {{middleware}} phase {0}" + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "Unknown \"{0}\" {{id}} \"{1}\".", + "e92aa25b6b864e3454b65a7c422bd114": "Bulk update failed, the connector has deleted unexpected number of records: {0}", + "ea63d226b6968e328bdf6876010786b5": "Cannot apply bulk updates, the connector does not correctly report the number of deleted records.", + "ead044e2b4bce74b4357f8a03fb78ec4": "Cannot call {0}.{1}(). The {2} method has not been setup. The {{KeyValueModel}} has not been correctly attached to a {{DataSource}}!", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t TO:{0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORT:{0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "result:{0}", + "f1d4ac54357cc0932f385d56814ba7e4": "Conflict", + "f58cdc481540cd1f69a4aa4da2e37981": "Invalid password: {0}" } From 645d5c615bc37ba993f40846686d312d62b939ee Mon Sep 17 00:00:00 2001 From: Diana Lau Date: Tue, 11 Apr 2017 10:35:04 -0400 Subject: [PATCH 159/187] update karma-browserify to 5.x update karma-browserify to 5.x --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index de4a1d4b7..2faee3581 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "grunt-karma": "^2.0.0", "grunt-mocha-test": "^0.12.7", "karma": "^1.1.2", - "karma-browserify": "^4.4.2", + "karma-browserify": "^5.1.1", "karma-chrome-launcher": "^1.0.1", "karma-firefox-launcher": "^1.0.0", "karma-html2js-preprocessor": "^1.0.0", From f1f9aab606e32b243e672a6db3215d5bb0bed1c1 Mon Sep 17 00:00:00 2001 From: Diana Lau Date: Thu, 13 Apr 2017 15:28:48 -0400 Subject: [PATCH 160/187] use lower version of karma-browserify --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2faee3581..024edad03 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "grunt-karma": "^2.0.0", "grunt-mocha-test": "^0.12.7", "karma": "^1.1.2", - "karma-browserify": "^5.1.1", + "karma-browserify": "^5.0.5", "karma-chrome-launcher": "^1.0.1", "karma-firefox-launcher": "^1.0.0", "karma-html2js-preprocessor": "^1.0.0", From 2135abc1dbeecabefd934c5777f77360f09986f4 Mon Sep 17 00:00:00 2001 From: Candy Date: Mon, 17 Apr 2017 16:40:50 -0400 Subject: [PATCH 161/187] 2.38.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * use lower version of karma-browserify (Diana Lau) * update karma-browserify to 5.x (Diana Lau) * update translation msg (Diana Lau) * Fix user-literal rewrite for anonymous requests (Aaron Buchanan) * Forward options in prepareForTokenInvalidation (Miroslav Bajtoš) --- CHANGES.md | 14 ++++++++++++++ package.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 242939ef6..c33563a04 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,17 @@ +2017-04-17, Version 2.38.3 +========================== + + * use lower version of karma-browserify (Diana Lau) + + * update karma-browserify to 5.x (Diana Lau) + + * update translation msg (Diana Lau) + + * Fix user-literal rewrite for anonymous requests (Aaron Buchanan) + + * Forward options in prepareForTokenInvalidation (Miroslav Bajtoš) + + 2017-03-17, Version 2.38.2 ========================== diff --git a/package.json b/package.json index 024edad03..afc9ea4d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.38.2", + "version": "2.38.3", "publishConfig": { "tag": "lts" }, From 41c31118d8bd69f2bcf634ef43249cedf8351a56 Mon Sep 17 00:00:00 2001 From: Allen Boone Date: Mon, 22 May 2017 16:45:34 -0400 Subject: [PATCH 162/187] Update translated strings Q2 2017 --- intl/de/messages.json | 150 ++++++++++++++----------------------- intl/es/messages.json | 150 ++++++++++++++----------------------- intl/fr/messages.json | 150 ++++++++++++++----------------------- intl/it/messages.json | 150 ++++++++++++++----------------------- intl/ja/messages.json | 150 ++++++++++++++----------------------- intl/ko/messages.json | 150 ++++++++++++++----------------------- intl/nl/messages.json | 150 ++++++++++++++----------------------- intl/pt/messages.json | 150 ++++++++++++++----------------------- intl/tr/messages.json | 150 ++++++++++++++----------------------- intl/zh-Hans/messages.json | 150 ++++++++++++++----------------------- intl/zh-Hant/messages.json | 150 ++++++++++++++----------------------- 11 files changed, 605 insertions(+), 1045 deletions(-) diff --git a/intl/de/messages.json b/intl/de/messages.json index f80f10e81..31d7d99c3 100644 --- a/intl/de/messages.json +++ b/intl/de/messages.json @@ -1,116 +1,76 @@ { - "3b46d3a780fd6ae5f95ade489a0efffe": "Der aktuelle Kontext wird im Browser nicht unterstützt.", - "7e0fca41d098607e1c9aa353c67e0fa1": "Ungültiges Zugriffstoken", - "320c482401afa1207c04343ab162e803": "Ungültiger Prinzipaltyp: {0}", - "c2b5d51f007178170ca3952d59640ca4": "{0} Änderungen können nicht behoben werden:\n{1}", - "5858e63efaa0e4ad86b61c0459ea32fa": "Sie müssen das {{Email}}-Modell mit einem {{Mail}}-Konnektor verbinden", + "03f79fa268fe199de2ce4345515431c1": "Kein Änderungssatz gefunden für {0} mit ID {1}", + "04bd8af876f001ceaf443aad6a9002f9": "Für die Authentifizierung muss Modell {0} definiert sein.", + "0731d0109e46c21a4e34af3346ed4856": "Dieses Verhalten kann sich in der nächsten Hauptversion ändern.", + "095afbf2f1f0e5be678f5dac5c54e717": "Zugriff verweigert", "0caffe1d763c8cca6a61814abe33b776": "E-Mail ist erforderlich", - "1b2a6076dccbe91a56f1672eb3b8598c": "Der Antworthauptteil enthält Eigenschaften des bei der Anmeldung erstellten {{AccessToken}}.\nAbhängig vom Wert des Parameters 'include' kann der Hauptteil zusätzliche Eigenschaften enthalten:\n\n - user - U+007BUserU+007D - Daten des derzeit angemeldeten Benutzers. {{(`include=user`)}}\n\n", - "2362ba55796c733e337af169922327a2": "In die Antwort einzuschließende zugehörige Objekte. Weitere Details enthält die Beschreibung des Rückgabewerts.", - "306999d39387d87b2638199ff0bed8ad": "Kennwort zurücksetzen für einen Benutzer mit E-Mail.", - "3aae63bb7e8e046641767571c1591441": "Anmeldung fehlgeschlagen, da die E-Mail-Adresse nicht bestätigt wurde", - "3caaa84fc103d6d5612173ae6d43b245": "Ungültiges Token: {0}", - "42e3fa18945255412ebc6561e2c6f1dc": "Bestätigen Sie eine Benutzerregistrierung mit E-Mail-Bestätigungs-Token.", - "430b6326d7ebf6600a39f614ef516bc8": "Geben Sie dieses Argument nicht an, es wird automatisch aus Anforderungsheaders extrahiert.", - "44a6c8b1ded4ed653d19ddeaaf89a606": "E-Mail nicht gefunden", - "5e81ad3847a290dc650b47618b9cbc7e": "Anmeldung fehlgeschlagen", - "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "Benutzer mit Benutzername/E-Mail und Kennwort anmelden.", - "8608c28f5e6df0008266e3c497836176": "Benutzer mit Zugriffstoken abmelden.", - "860d1a0b8bd340411fb32baa72867989": "Die Transportmethode unterstützt keine HTTP-Umleitungen.", - "895b1f941d026870b3cc8e6af087c197": "{{username}} oder {{email}} ist erforderlich", - "a50d10fc6e0959b220e085454c40381e": "Benutzer nicht gefunden: {0}", - "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} ist erforderlich", - "c34fa20eea0091747fcc9eda204b8d37": "{{accessToken}} konnte nicht gefunden werden", - "c68a93f0a9524fed4ff64372fc90c55f": "Eine gültige E-Mail-Adresse muss angegeben werden", - "f58cdc481540cd1f69a4aa4da2e37981": "Ungültiges Kennwort: {0}", - "f0bd73df8714cefb925e3b8da2f4c5f6": "Ergebnis:{0}", "10e01c895dc0b2fecc385f9f462f1ca6": "eine Liste mit Farben ist verfügbar unter {{http://localhost:3000/colors}}", - "8a27e0c9ce3ebf0e0c3978efb456e13e": "Anforderung an Host {0}", - "a40684f5a9f546115258b76938d1de37": "Eine Liste mit Farben ist verfügbar unter {{http://localhost:3000/colors}}", + "1b2a6076dccbe91a56f1672eb3b8598c": "Der Antworthauptteil enthält Eigenschaften des bei der Anmeldung erstellten {{AccessToken}}.\nAbhängig vom Wert des Parameters 'include' kann der Hauptteil zusätzliche Eigenschaften enthalten:\n\n - user - U+007BUserU+007D - Daten des derzeit angemeldeten Benutzers. {{(`include=user`)}}\n\n", + "1d7833c3ca2f05fdad8fad7537531c40": "\t BETREFF:{0}", "1e85f822b547a75d7d385048030e4ecb": "Erstellt: {0}", - "7d5e7ed0efaedf3f55f380caae0df8b8": "Meine erste mobile Anwendung", - "04bd8af876f001ceaf443aad6a9002f9": "Für die Authentifizierung muss Modell {0} definiert sein.", - "095afbf2f1f0e5be678f5dac5c54e717": "Zugriff verweigert", + "275f22ab95671f095640ca99194b7635": "\t VON:{0}", "2d3071e3b18681c80a090dc0efbdb349": "{0} mit ID {1} konnte nicht gefunden werden", + "2d78192c43fd2ec52ec18f3918894f9a": "Middleware {0} ist veraltet. Siehe {1} für weitere Details.", + "308e1d484516a33df788f873e65faaff": "Modell '{0}' bietet veraltetes 'DataModel' an. Verwenden Sie stattdessen 'PersistedModel'.", "316e5b82c203cf3de31a449ee07d0650": "Erwartet wurde boolescher Wert, {0} empfangen", - "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Kann Datenquelle {0} nicht erstellen: {1}", - "7e287fc885d9fdcf42da3a12f38572c1": "Berechtigung erforderlich", - "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} wurde entfernt, verwenden Sie stattdessen das neue Modul {{loopback-boot}}", - "1d7833c3ca2f05fdad8fad7537531c40": "\t BETREFF:{0}", - "275f22ab95671f095640ca99194b7635": "\t VON:{0}", + "320c482401afa1207c04343ab162e803": "Ungültiger Prinzipaltyp: {0}", + "3438fab56cc7ab92dfd88f0497e523e0": "Die relations-Eigenschaft der Konfiguration '{0}' muss ein Objekt sein", + "35e5252c62d80f8c54a5290d30f4c7d0": "Bestätigen Sie Ihre E-Mail-Adresse, indem Sie diesen Link in einem Web-Browser öffnen:\n\t{0}", + "3aae63bb7e8e046641767571c1591441": "Anmeldung fehlgeschlagen, da die E-Mail-Adresse nicht bestätigt wurde", + "3aecb24fa8bdd3f79d168761ca8a6729": "Unbekannte {{middleware}}-Phase {0}", + "3caaa84fc103d6d5612173ae6d43b245": "Ungültiges Token: {0}", + "3d617953470be16d0c2b32f0bcfbb5ee": "Vielen Dank für die Registrierung", "3d63008ccfb2af1db2142e8cc2716ace": "Warnung: Keine E-Mail-Transportmethode für das Senden von E-Mails angegeben. Richten Sie eine Transportmethode für das Senden von E-Mails ein.", + "4203ab415ec66a78d3164345439ba76e": "{0}.{1}() kann nicht aufgerufen werden. Die Methode {2} wurde nicht konfiguriert. {{PersistedModel}} wurde nicht ordnungsgemäß an eine {{DataSource}} angehängt!", + "44a6c8b1ded4ed653d19ddeaaf89a606": "E-Mail nicht gefunden", "4a4f04a4e480fc5d4ee73b84d9a4b904": "E-Mail senden:", - "63a091ced88001ab6acb58f61ec041c5": "\t TEXT:{0}", - "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", - "ecb06666ef95e5db27a5ac1d6a17923b": "\t AN:{0}", - "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORTMETHODE:{0}", - "0da38687fed24275c1547e815914a8e3": "Zugehöriges Element nach ID für {0} löschen.", - "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "Kriterien für den Abgleich von Modellinstanzen", - "22fe62fa8d595b72c62208beddaa2a56": "Zugehöriges Element nach ID für {0} aktualisieren.", - "528325f3cbf1b0ab9a08447515daac9a": "{0} von diesem Modell aktualisieren.", - "543d19bad5e47ee1e9eb8af688e857b4": "Fremdschlüssel für {0}.", - "598ff0255ffd1d1b71e8de55dbe2c034": "Vorhandensein von {0}-Beziehung zu einem Element nach ID prüfen.", - "5a36cc6ba0cc27c754f6c5ed6015ea3c": "{0}-Beziehung zu einem Element nach ID entfernen.", + "4b494de07f524703ac0879addbd64b13": "E-Mail-Adresse wurde nicht bestätigt", + "4cac5f051ae431321673e04045d37772": "Modell '{0}' bietet das unbekannte Modell '{1}' an. 'PersistedModel' wird als Basis verwendet.", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Kann Datenquelle {0} nicht erstellen: {1}", + "5858e63efaa0e4ad86b61c0459ea32fa": "Sie müssen das {{Email}}-Modell mit einem {{Mail}}-Konnektor verbinden", + "5e81ad3847a290dc650b47618b9cbc7e": "Anmeldung fehlgeschlagen", "5fa3afb425819ebde958043e598cb664": "Modell mit {{id}} {0} konnte nicht gefunden werden", "61e5deebaf44d68f4e6a508f30cc31a3": "Beziehung '{0} ist für Modell {1} nicht vorhanden", - "651f0b3cbba001635152ec3d3d954d0a": "Zugehöriges Element nach ID für {0} suchen.", - "7bc7b301ad9c4fc873029d57fb9740fe": "Abfrage von {0} von {1}.", - "7c837b88fd0e509bd3fc722d7ddf0711": "Fremdschlüssel für {0}", - "830cb6c862f8f364e9064cea0026f701": "Ruft hasOne-Beziehung {0} ab.", - "855ecd4a64885ba272d782435f72a4d4": "\"{0}\" unbekannt, ID \"{1}\".", - "86254879d01a60826a851066987703f2": "Zugehöriges Element nach ID für {0} hinzufügen.", - "8ae418c605b6a45f2651be9b1677c180": "Ungültige Remote-Methode: '{0}'", - "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Ruft belongsTo-Beziehung {0} ab.", - "c0057a569ff9d3b509bac61a4b2f605d": "Löscht alle {0} von diesem Modell.", - "cd0412f2f33a4a2a316acc834f3f21a6": "muss {{id}} oder {{data}} angeben", - "d6f43b266533b04d442bdb3955622592": "Erstellt eine neue Instanz in {0} von diesem Modell.", - "da13d3cdf21330557254670dddd8c5c7": "Zählt {0} von {1}.", - "e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" unbekannt, {{id}} \"{1}\".", - "f66ae3cf379b2fce28575a3282defe1a": "Löscht {0} von diesem Modell.", - "03f79fa268fe199de2ce4345515431c1": "Kein Änderungssatz gefunden für {0} mit ID {1}", - "0f1c71f74b040bfbe8d384a414e31f03": "Rufen Sie einen Satz an Deltas und Konflikten seit dem angegebenen Prüfpunkt ab.", - "15254dec061d023d6c030083a0cef50f": "Erstellen Sie eine neue Instanz des Modells und definieren Sie es für die Datenquelle als persistent.", - "16a11368d55b85a209fc6aea69ee5f7a": "Löschen Sie alle übereinstimmenden Datensätze.", - "1bc1d489ddf347af47af3d9b1fc7cc15": "Führen Sie mehrere Aktualisierungen gleichzeitig aus. Anmerkung: Dies ist nicht atomar.", - "1bc7d8283c9abda512692925c8d8e3c0": "Rufen Sie den aktuellen Prüfpunkt ab.", - "1caa7cc61266e7aef7db7d2f0e27ac3e": "Aktualisieren Sie die Eigenschaften der neuesten Änderungssätze für diese Instanz.", - "2a7df74fe6e8462e617b79d5fbb536ea": "Rufen Sie den neuesten Änderungssatz für diese Instanz ab.", - "2a9684b3d5b3b67af74bac74eb1b0843": "Suchen Sie alle Instanzen des Modells aus der Datenquelle, die laut Filter übereinstimmen.", - "2e50838caf0c927735eb15d12866bdd7": "Rufen Sie die Änderungen an einem Modell seit einem angegebenen Prüfpunkt ab. Geben Sie ein Filterobjekt zum Reduzieren der Anzahl an zurückgegebenen Ergebnissen an.", - "4203ab415ec66a78d3164345439ba76e": "{0}.{1}() kann nicht aufgerufen werden. Die Methode {2} wurde nicht konfiguriert. {{PersistedModel}} wurde nicht ordnungsgemäß an eine {{DataSource}} angehängt!", - "51ea9b6245bb5e672b236d640ca3b048": "Ein Objekt aus Paaren aus Änderungseigenschaftsnamen und -werten", - "55ddedd4c501348f82cb89db02ec85c1": "Ein Objekt aus Paaren aus Modelleigenschaftsnamen und -werten", - "5aaa76c72ae1689fd3cf62589784a4ba": "Aktualisieren Sie Attribute für eine Modellinstanz und definieren Sie sie für die Datenquelle als persistent.", - "5f659bbc15e6e2b249fa33b3879b5f69": "Suchen Sie eine Modellinstanz nach {{id}} aus der Datenquelle.", "62e8b0a733417978bab22c8dacf5d7e6": "Massenaktualisierungen können nicht angewendet werden, der Konnektor meldet die Anzahl aktualisierter Datensätze nicht richtig.", - "6329e0ac1de3c250ebb1df5afd5a2a7b": "Die Anzahl der aktualisierten Instanzen", + "63a091ced88001ab6acb58f61ec041c5": "\t TEXT:{0}", "6bc376432cd9972cf991aad3de371e78": "Fehlende Daten für Änderung: {0}", - "79295ac04822d2e9702f0dd1d0240336": "Aktualisieren Sie die Instanzen des Modells aus der Datenquelle, die laut {{where}} übereinstimmen.", - "7f2fde7f0f860ead224b11ba8d75aa1c": "Erstellen Sie eine Aktualisierungsliste aus einer Deltaliste.", - "89b57e764c2267129294b07589dbfdc2": "Löschen Sie eine Modellinstanz nach {{id}} aus der Datenquelle.", - "8bab6720ecc58ec6412358c858a53484": "Massenaktualisierung fehlgeschlagen, der Konnektor hat eine unerwartete Anzahl an Datensätzen geändert: {0}", - "a98b6cc6547706b5c6bffde0ed5fd55c": "Suchen Sie die erste Instanz des Modells aus der Datenquelle, die laut Filter übereinstimmen.", - "c46d4aba1f14809c16730faa46933495": "Definierende Felder filtern und einschließen", - "c65600640f206f585d300b4bcb699d95": "Erstellen Sie einen Prüfpunkt.", - "cf64c7afc74d3a8120abcd028f98c770": "Aktualisieren Sie eine vorhandene Modellinstanz oder fügen Sie eine neue in die Datenquelle ein.", - "dcb6261868ff0a7b928aa215b07d068c": "Erstellen Sie einen Änderungsdatenstrom.", - "e43e320a435ec1fa07648c1da0d558a7": "Überprüfen Sie, ob in der neuen Datenquelle eine Modellinstanz vorhanden ist.", - "e92aa25b6b864e3454b65a7c422bd114": "Massenaktualisierung fehlgeschlagen, der Konnektor hat eine unerwartete Anzahl an Datensätzen gelöscht : {0}", - "ea63d226b6968e328bdf6876010786b5": "Massenaktualisierungen können nicht angewendet werden, der Konnektor meldet die Anzahl gelöschter Datensätze nicht richtig.", - "f1d4ac54357cc0932f385d56814ba7e4": "Konflikt", - "f37d94653793e33f4203e45b4a1cf106": "Zählen Sie die Instanzen des Modells aus der Datenquelle, die laut Position (where) übereinstimmen.", - "0731d0109e46c21a4e34af3346ed4856": "Dieses Verhalten kann sich in der nächsten Hauptversion ändern.", - "2e110abee2c95bcfc2dafd48be7e2095": "{0} kann nicht konfiguriert werden: {{config.dataSource}} muss eine Instanz von {{DataSource}} sein", - "308e1d484516a33df788f873e65faaff": "Modell '{0}' bietet veraltetes 'DataModel' an. Verwenden Sie stattdessen 'PersistedModel'.", - "3438fab56cc7ab92dfd88f0497e523e0": "Die relations-Eigenschaft der Konfiguration '{0}' muss ein Objekt sein", - "4cac5f051ae431321673e04045d37772": "Modell '{0}' bietet das unbekannte Modell '{1}' an. 'PersistedModel' wird als Basis verwendet.", + "705c2d456a3e204c4af56e671ec3225c": "{{accessToken}} konnte nicht gefunden werden", "734a7bebb65e10899935126ba63dd51f": "Die options-Eigenschaft der Konfiguration '{0}' muss ein Objekt sein", "779467f467862836e19f494a37d6ab77": "Die acls-Eigenschaft der Konfiguration '{0}' muss eine Reihe von Objekten sein", + "7d5e7ed0efaedf3f55f380caae0df8b8": "Meine erste mobile Anwendung", + "7e0fca41d098607e1c9aa353c67e0fa1": "Ungültiges Zugriffstoken", + "7e287fc885d9fdcf42da3a12f38572c1": "Berechtigung erforderlich", + "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} muss sich abmelden", "80a32e80cbed65eba2103201a7c94710": "Modell nicht gefunden: {0}", "83cbdc2560ba9f09155ccfc63e08f1a1": "Eigenschaft '{0}' kann für {1} nicht rekonfiguriert werden", + "855ecd4a64885ba272d782435f72a4d4": "\"{0}\" unbekannt, ID \"{1}\".", + "860d1a0b8bd340411fb32baa72867989": "Die Transportmethode unterstützt keine HTTP-Umleitungen.", + "895b1f941d026870b3cc8e6af087c197": "{{username}} oder {{email}} ist erforderlich", + "8a17c5ef611e2e7535792316e66b8fca": "Kennwort zu lang: {0}", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "Anforderung an Host {0}", + "8ae418c605b6a45f2651be9b1677c180": "Ungültige Remote-Methode: '{0}'", + "8bab6720ecc58ec6412358c858a53484": "Massenaktualisierung fehlgeschlagen, der Konnektor hat eine unerwartete Anzahl an Datensätzen geändert: {0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", "97795efe0c3eb7f35ce8cf8cfe70682b": "Der Konfiguration von {0} fehlt die {{`dataSource`}}-Eigenschaft.\nVerwenden Sie 'null' oder 'false', um Modelle zu kennzeichnen, die mit keiner Datenquelle verbunden sind.", + "a40684f5a9f546115258b76938d1de37": "Eine Liste mit Farben ist verfügbar unter {{http://localhost:3000/colors}}", + "a50d10fc6e0959b220e085454c40381e": "Benutzer nicht gefunden: {0}", "a80038252430df2754884bf3c845c4cf": "Den Remote-Anbindungs-Metadaten für \"{0}.{1}\" fehlt das Flag \"isStatic\"; die Methode ist als Instanzdefinitionsmethode registriert.", + "b6f740aeb6f2eb9bee9cb049dbfe6a28": "\"{0}\" unbekannt, {{key}} \"{1}\".", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} ist erforderlich", + "c2b5d51f007178170ca3952d59640ca4": "{0} Änderungen können nicht behoben werden:\n{1}", + "c68a93f0a9524fed4ff64372fc90c55f": "Eine gültige E-Mail-Adresse muss angegeben werden", + "cd0412f2f33a4a2a316acc834f3f21a6": "muss {{id}} oder {{data}} angeben", + "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} wurde entfernt, verwenden Sie stattdessen das neue Modul {{loopback-boot}}", + "d9ef6dc3770dd8f80a129e92a79851f3": "{0} ist veraltet. Siehe {1} für weitere Details.", "dc568bee32deb0f6eaf63e73b20e8ceb": "Nicht-Objekt-Einstellung \"{0}\" von \"methods\" wird ignoriert.", - "3aecb24fa8bdd3f79d168761ca8a6729": "Unbekannte {{middleware}}-Phase {0}" + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" unbekannt, {{id}} \"{1}\".", + "e92aa25b6b864e3454b65a7c422bd114": "Massenaktualisierung fehlgeschlagen, der Konnektor hat eine unerwartete Anzahl an Datensätzen gelöscht : {0}", + "ea63d226b6968e328bdf6876010786b5": "Massenaktualisierungen können nicht angewendet werden, der Konnektor meldet die Anzahl gelöschter Datensätze nicht richtig.", + "ead044e2b4bce74b4357f8a03fb78ec4": "{0}.{1}() kann nicht aufgerufen werden. Die Methode {2} wurde nicht konfiguriert. {{KeyValueModel}} wurde nicht ordnungsgemäß an eine {{DataSource}} angehängt!", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t AN:{0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORTMETHODE:{0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "Ergebnis:{0}", + "f1d4ac54357cc0932f385d56814ba7e4": "Konflikt", + "f58cdc481540cd1f69a4aa4da2e37981": "Ungültiges Kennwort: {0}" } diff --git a/intl/es/messages.json b/intl/es/messages.json index 2a82383b5..7abc0eca4 100644 --- a/intl/es/messages.json +++ b/intl/es/messages.json @@ -1,116 +1,76 @@ { - "3b46d3a780fd6ae5f95ade489a0efffe": "El contexto actual no está soportado en el navegador.", - "7e0fca41d098607e1c9aa353c67e0fa1": "Señal de acceso no válida", - "320c482401afa1207c04343ab162e803": "Tipo de principal no válido: {0}", - "c2b5d51f007178170ca3952d59640ca4": "No se pueden rectificar los cambios de {0}:\n{1}", - "5858e63efaa0e4ad86b61c0459ea32fa": "Debe conectar el modelo de {{Email}} a un conector de {{Mail}}", + "03f79fa268fe199de2ce4345515431c1": "No se ha encontrado ningún registro de cambio para {0} con el id {1}", + "04bd8af876f001ceaf443aad6a9002f9": "La autenticación requiere la definición del modelo {0}.", + "0731d0109e46c21a4e34af3346ed4856": "Este comportamiento puede cambiar en la próxima versión principal.", + "095afbf2f1f0e5be678f5dac5c54e717": "Acceso denegado", "0caffe1d763c8cca6a61814abe33b776": "Es necesario el correo electrónico", - "1b2a6076dccbe91a56f1672eb3b8598c": "El cuerpo de respuesta contiene propiedades de la {{AccessToken}} creada durante el inicio de la sesión.\nDependiendo del valor del parámetro `include`, el cuerpo puede contener propiedades adicionales:\n\n - `user` - `U+007BUserU+007D` - Datos del usuario conectado actualmente. {{(`include=user`)}}\n\n", - "2362ba55796c733e337af169922327a2": "Objetos relacionados a incluir en la respuesta. Consulte la descripción del valor de retorno para obtener más detalles.", - "306999d39387d87b2638199ff0bed8ad": "Restablecer contraseña para un usuario con correo electrónico.", - "3aae63bb7e8e046641767571c1591441": "el inicio de sesión ha fallado porque el correo electrónico no ha sido verificado", - "3caaa84fc103d6d5612173ae6d43b245": "La señal no es válida: {0}", - "42e3fa18945255412ebc6561e2c6f1dc": "Confirmar un registro de usuario con la señal de verificación de correo electrónico.", - "430b6326d7ebf6600a39f614ef516bc8": "No proporcione este argumento, se extrae automáticamente de las cabeceras de solicitud.", - "44a6c8b1ded4ed653d19ddeaaf89a606": "Correo electrónico no encontrado", - "5e81ad3847a290dc650b47618b9cbc7e": "el inicio de sesión ha fallado", - "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "Iniciar una sesión de usuario con nombre de usuario/correo electrónico y contraseña.", - "8608c28f5e6df0008266e3c497836176": "Finalizar una sesión de usuario con señal de acceso.", - "860d1a0b8bd340411fb32baa72867989": "El transporte no admite redirecciones HTTP.", - "895b1f941d026870b3cc8e6af087c197": "{{username}} o {{email}} es obligatorio", - "a50d10fc6e0959b220e085454c40381e": "No se ha encontrado el usuario: {0}", - "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} es obligatorio", - "c34fa20eea0091747fcc9eda204b8d37": "no se ha encontrado {{accessToken}}", - "c68a93f0a9524fed4ff64372fc90c55f": "Debe proporcionar un correo electrónico válido", - "f58cdc481540cd1f69a4aa4da2e37981": "Contraseña no válida: {0}", - "f0bd73df8714cefb925e3b8da2f4c5f6": "resultado:{0}", "10e01c895dc0b2fecc385f9f462f1ca6": "una lista de colores está disponible en {{http://localhost:3000/colors}}", - "8a27e0c9ce3ebf0e0c3978efb456e13e": "Solicitud al host {0}", - "a40684f5a9f546115258b76938d1de37": "Una lista de colores está disponible en {{http://localhost:3000/colors}}", + "1b2a6076dccbe91a56f1672eb3b8598c": "El cuerpo de respuesta contiene propiedades de la {{AccessToken}} creada durante el inicio de la sesión.\nDependiendo del valor del parámetro `include`, el cuerpo puede contener propiedades adicionales:\n\n - `user` - `U+007BUserU+007D` - Datos del usuario conectado actualmente. {{(`include=user`)}}\n\n", + "1d7833c3ca2f05fdad8fad7537531c40": "\t ASUNTO:{0}", "1e85f822b547a75d7d385048030e4ecb": "Creado: {0}", - "7d5e7ed0efaedf3f55f380caae0df8b8": "Mi primera aplicación móvil", - "04bd8af876f001ceaf443aad6a9002f9": "La autenticación requiere la definición del modelo {0}.", - "095afbf2f1f0e5be678f5dac5c54e717": "Acceso denegado", + "275f22ab95671f095640ca99194b7635": "\t DESDE:{0}", "2d3071e3b18681c80a090dc0efbdb349": "no se ha encontrado {0} con el ID {1}", + "2d78192c43fd2ec52ec18f3918894f9a": "El middleware {0} está en desuso. Consulte {1} para obtener detalles.", + "308e1d484516a33df788f873e65faaff": "El modelo `{0}` está ampliando `DataModel` en desuso. Utilice `PersistedModel` en su lugar.", "316e5b82c203cf3de31a449ee07d0650": "Se esperaba un booleano, se ha obtenido {0}", - "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "No se puede crear el origen de datos {0}: {1}", - "7e287fc885d9fdcf42da3a12f38572c1": "Autorización necesaria", - "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} se ha eliminado, utilice el nuevo módulo {{loopback-boot}} en su lugar", - "1d7833c3ca2f05fdad8fad7537531c40": "\t ASUNTO:{0}", - "275f22ab95671f095640ca99194b7635": "\t DESDE:{0}", + "320c482401afa1207c04343ab162e803": "Tipo de principal no válido: {0}", + "3438fab56cc7ab92dfd88f0497e523e0": "La configuración de la propiedad relations de `{0}` debe ser un objeto", + "35e5252c62d80f8c54a5290d30f4c7d0": "Verifique su correo electrónico abriendo este enlace en un navegador:\n\t {0}", + "3aae63bb7e8e046641767571c1591441": "el inicio de sesión ha fallado porque el correo electrónico no ha sido verificado", + "3aecb24fa8bdd3f79d168761ca8a6729": "Fase de {{middleware}} desconocida {0}", + "3caaa84fc103d6d5612173ae6d43b245": "La señal no es válida: {0}", + "3d617953470be16d0c2b32f0bcfbb5ee": "Gracias por registrarse", "3d63008ccfb2af1db2142e8cc2716ace": "Aviso: No se ha especificado ningún transporte de correo electrónico para enviar correo electrónico. Configure un transporte para enviar mensajes de correo.", + "4203ab415ec66a78d3164345439ba76e": "No se puede llamar a {0}.{1}(). El método {2} no se ha configurado. {{PersistedModel}} no se ha conectado correctamente a un {{DataSource}}.", + "44a6c8b1ded4ed653d19ddeaaf89a606": "Correo electrónico no encontrado", "4a4f04a4e480fc5d4ee73b84d9a4b904": "Enviando correo:", - "63a091ced88001ab6acb58f61ec041c5": "\t TEXTO:{0}", - "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", - "ecb06666ef95e5db27a5ac1d6a17923b": "\t A:{0}", - "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORTE:{0}", - "0da38687fed24275c1547e815914a8e3": "Suprimir un elemento relacionado por id para {0}.", - "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "Criterios de coincidencia de instancias de modelo", - "22fe62fa8d595b72c62208beddaa2a56": "Actualizar un elemento relacionado por id para {0}.", - "528325f3cbf1b0ab9a08447515daac9a": "Actualizar {0} de este modelo.", - "543d19bad5e47ee1e9eb8af688e857b4": "Clave foránea para {0}.", - "598ff0255ffd1d1b71e8de55dbe2c034": "Comprobar la existencia de la relación {0} con un elemento por id.", - "5a36cc6ba0cc27c754f6c5ed6015ea3c": "Eliminar la relación {0} con un elemento por id.", + "4b494de07f524703ac0879addbd64b13": "El correo electrónico no se ha verificado", + "4cac5f051ae431321673e04045d37772": "El modelo `{0}` está ampliando un modelo desconocido `{1}`. Se utiliza `PersistedModel` como base.", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "No se puede crear el origen de datos {0}: {1}", + "5858e63efaa0e4ad86b61c0459ea32fa": "Debe conectar el modelo de {{Email}} a un conector de {{Mail}}", + "5e81ad3847a290dc650b47618b9cbc7e": "el inicio de sesión ha fallado", "5fa3afb425819ebde958043e598cb664": "no se ha encontrado un modelo con {{id}} {0}", "61e5deebaf44d68f4e6a508f30cc31a3": "La relación `{0}` no existe para el modelo `{1}`", - "651f0b3cbba001635152ec3d3d954d0a": "Buscar un elemento relacionado por id para {0}.", - "7bc7b301ad9c4fc873029d57fb9740fe": "{0} consultas de {1}.", - "7c837b88fd0e509bd3fc722d7ddf0711": "Clave foránea para {0}", - "830cb6c862f8f364e9064cea0026f701": "Capta la relación hasOne {0}.", - "855ecd4a64885ba272d782435f72a4d4": "Id de \"{0}\" desconocido \"{1}\".", - "86254879d01a60826a851066987703f2": "Añadir un elemento relacionado por id para {0}.", - "8ae418c605b6a45f2651be9b1677c180": "Método remoto no válido: `{0}`", - "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Capta la relación belongsTo {0}.", - "c0057a569ff9d3b509bac61a4b2f605d": "Suprime todos los {0} de este modelo.", - "cd0412f2f33a4a2a316acc834f3f21a6": "debe especificar un {{id}} o {{data}}", - "d6f43b266533b04d442bdb3955622592": "Crea una nueva instancia en {0} de este modelo.", - "da13d3cdf21330557254670dddd8c5c7": "Recuentos {0} de {1}.", - "e4434de4bb8f5a3cd1d416e4d80d7e0b": "{{id}} de \"{0}\" desconocido \"{1}\".", - "f66ae3cf379b2fce28575a3282defe1a": "Suprime {0} de este modelo.", - "03f79fa268fe199de2ce4345515431c1": "No se ha encontrado ningún registro de cambio para {0} con el id {1}", - "0f1c71f74b040bfbe8d384a414e31f03": "Obtener un conjunto de deltas y conflictos desde el punto de comprobación especificado.", - "15254dec061d023d6c030083a0cef50f": "Crear una nueva instancia del modelo y hacerla persistente en el origen de datos.", - "16a11368d55b85a209fc6aea69ee5f7a": "Suprimir todos los registros coincidentes.", - "1bc1d489ddf347af47af3d9b1fc7cc15": "Ejecutar varias actualizaciones a la vez. Nota: no es atómico.", - "1bc7d8283c9abda512692925c8d8e3c0": "Obtener el punto de comprobación actual.", - "1caa7cc61266e7aef7db7d2f0e27ac3e": "Actualizar las propiedades del registro de cambio más reciente conservado para esta instancia.", - "2a7df74fe6e8462e617b79d5fbb536ea": "Obtener el registro de cambio más reciente para esta instancia.", - "2a9684b3d5b3b67af74bac74eb1b0843": "Buscar todas las instancias del modelo coincidentes por filtro del origen de datos.", - "2e50838caf0c927735eb15d12866bdd7": "Obtener los cambios de un modelo desde un punto de comprobación determinado. Proporcione un objeto de filtro para reducir el número de resultados devueltos.", - "4203ab415ec66a78d3164345439ba76e": "No se puede llamar a {0}.{1}(). El método {2} no se ha configurado. {{PersistedModel}} no se ha conectado correctamente a un {{DataSource}}.", - "51ea9b6245bb5e672b236d640ca3b048": "Un objeto de pares de nombre/valor de propiedad de cambio", - "55ddedd4c501348f82cb89db02ec85c1": "Un objeto de pares de nombre/valor de propiedad de modelo", - "5aaa76c72ae1689fd3cf62589784a4ba": "Actualizar atributos para una instancia de modelo y hacerla persistente en el origen de datos.", - "5f659bbc15e6e2b249fa33b3879b5f69": "Buscar una instancia de modelo por {{id}} del origen de datos.", "62e8b0a733417978bab22c8dacf5d7e6": "No pueden aplicarse actualizaciones masivas, el conector no notifica correctamente el número de registros actualizados.", - "6329e0ac1de3c250ebb1df5afd5a2a7b": "El número de instancias actualizadas", + "63a091ced88001ab6acb58f61ec041c5": "\t TEXTO:{0}", "6bc376432cd9972cf991aad3de371e78": "Faltan datos para el cambio: {0}", - "79295ac04822d2e9702f0dd1d0240336": "Actualizar instancias del modelo comparadas por {{where}} del origen de datos.", - "7f2fde7f0f860ead224b11ba8d75aa1c": "Crear una lista de actualización desde una lista delta.", - "89b57e764c2267129294b07589dbfdc2": "Suprimir una instancia de modelo por {{id}} del origen de datos.", - "8bab6720ecc58ec6412358c858a53484": "La actualización masiva ha fallado, el conector ha modificado un número de registros inesperado: {0}", - "a98b6cc6547706b5c6bffde0ed5fd55c": "Buscar primera instancia del modelo comparado por filtro del origen de datos.", - "c46d4aba1f14809c16730faa46933495": "Filtrar definiendo campos e incluir", - "c65600640f206f585d300b4bcb699d95": "Crear un punto de comprobación.", - "cf64c7afc74d3a8120abcd028f98c770": "Actualizar una instancia de modelo existente o insertar una nueva en el origen de datos.", - "dcb6261868ff0a7b928aa215b07d068c": "Crear una corriente de cambio.", - "e43e320a435ec1fa07648c1da0d558a7": "Comprobar si una instancia de modelo existe en el origen de datos.", - "e92aa25b6b864e3454b65a7c422bd114": "La actualización masiva ha fallado, el conector ha suprimido un número de registros inesperado: {0}", - "ea63d226b6968e328bdf6876010786b5": "No pueden aplicarse actualizaciones masivas, el conector no notifica correctamente el número de registros suprimidos.", - "f1d4ac54357cc0932f385d56814ba7e4": "Conflicto", - "f37d94653793e33f4203e45b4a1cf106": "Recuento de instancias del modelo comparadas por where desde el origen de datos.", - "0731d0109e46c21a4e34af3346ed4856": "Este comportamiento puede cambiar en la próxima versión principal.", - "2e110abee2c95bcfc2dafd48be7e2095": "No se puede configurar {0}: {{config.dataSource}} debe ser una instancia de {{DataSource}}", - "308e1d484516a33df788f873e65faaff": "El modelo `{0}` está ampliando `DataModel` en desuso. Utilice `PersistedModel` en su lugar.", - "3438fab56cc7ab92dfd88f0497e523e0": "La configuración de la propiedad relations de `{0}` debe ser un objeto", - "4cac5f051ae431321673e04045d37772": "El modelo `{0}` está ampliando un modelo desconocido `{1}`. Se utiliza `PersistedModel` como base.", + "705c2d456a3e204c4af56e671ec3225c": "No se ha encontrado {{accessToken}}", "734a7bebb65e10899935126ba63dd51f": "La configuración de la propiedad de options de `{0}` debe ser un objeto", "779467f467862836e19f494a37d6ab77": "La configuración de la propiedad acls de `{0}` debe ser una matriz de objetos", + "7d5e7ed0efaedf3f55f380caae0df8b8": "Mi primera aplicación móvil", + "7e0fca41d098607e1c9aa353c67e0fa1": "Señal de acceso no válida", + "7e287fc885d9fdcf42da3a12f38572c1": "Autorización necesaria", + "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "Es necesario {{accessToken}} para cerrar la sesión", "80a32e80cbed65eba2103201a7c94710": "No se ha encontrado el modelo: {0}", "83cbdc2560ba9f09155ccfc63e08f1a1": "La propiedad `{0}` no puede reconfigurarse para `{1}`", + "855ecd4a64885ba272d782435f72a4d4": "Id de \"{0}\" desconocido \"{1}\".", + "860d1a0b8bd340411fb32baa72867989": "El transporte no admite redirecciones HTTP.", + "895b1f941d026870b3cc8e6af087c197": "{{username}} o {{email}} es obligatorio", + "8a17c5ef611e2e7535792316e66b8fca": "Contraseña demasiado larga: {0}", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "Solicitud al host {0}", + "8ae418c605b6a45f2651be9b1677c180": "Método remoto no válido: `{0}`", + "8bab6720ecc58ec6412358c858a53484": "La actualización masiva ha fallado, el conector ha modificado un número de registros inesperado: {0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", "97795efe0c3eb7f35ce8cf8cfe70682b": "En la configuración de `{0}` falta la propiedad {{`dataSource`}}.\nUtilice `null` o `false` para marcar los modelos no conectados a ningún origen de datos.", + "a40684f5a9f546115258b76938d1de37": "Una lista de colores está disponible en {{http://localhost:3000/colors}}", + "a50d10fc6e0959b220e085454c40381e": "No se ha encontrado el usuario: {0}", "a80038252430df2754884bf3c845c4cf": "En los metadatos de interacción remota para \"{0}.{1}\" falta el indicador \"isStatic\", el método está registrado como método de instancia.", + "b6f740aeb6f2eb9bee9cb049dbfe6a28": "{{key}} de \"{0}\" desconocido \"{1}\".", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} es obligatorio", + "c2b5d51f007178170ca3952d59640ca4": "No se pueden rectificar los cambios de {0}:\n{1}", + "c68a93f0a9524fed4ff64372fc90c55f": "Debe proporcionar un correo electrónico válido", + "cd0412f2f33a4a2a316acc834f3f21a6": "debe especificar un {{id}} o {{data}}", + "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} se ha eliminado, utilice el nuevo módulo {{loopback-boot}} en su lugar", + "d9ef6dc3770dd8f80a129e92a79851f3": "{0} está en desuso. Consulte {1} para obtener detalles.", "dc568bee32deb0f6eaf63e73b20e8ceb": "Se ignora el valor \"methods\" no de objeto de \"{0}\".", - "3aecb24fa8bdd3f79d168761ca8a6729": "Fase de {{middleware}} desconocida {0}" + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "{{id}} de \"{0}\" desconocido \"{1}\".", + "e92aa25b6b864e3454b65a7c422bd114": "La actualización masiva ha fallado, el conector ha suprimido un número de registros inesperado: {0}", + "ea63d226b6968e328bdf6876010786b5": "No pueden aplicarse actualizaciones masivas, el conector no notifica correctamente el número de registros suprimidos.", + "ead044e2b4bce74b4357f8a03fb78ec4": "No se puede llamar a {0}.{1}(). El método {2} no se ha configurado. {{KeyValueModel}} no se ha conectado correctamente a un {{DataSource}}.", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t A:{0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORTE:{0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "resultado:{0}", + "f1d4ac54357cc0932f385d56814ba7e4": "Conflicto", + "f58cdc481540cd1f69a4aa4da2e37981": "Contraseña no válida: {0}" } diff --git a/intl/fr/messages.json b/intl/fr/messages.json index 3b087758a..cd5537d46 100644 --- a/intl/fr/messages.json +++ b/intl/fr/messages.json @@ -1,116 +1,76 @@ { - "3b46d3a780fd6ae5f95ade489a0efffe": "Le contexte en cours n'est pas pris en charge dans le navigateur.", - "7e0fca41d098607e1c9aa353c67e0fa1": "Jeton d'accès non valide", - "320c482401afa1207c04343ab162e803": "Type de principal non valide : {0}", - "c2b5d51f007178170ca3952d59640ca4": "Impossible de rectifier les modifications {0} :\n{1}", - "5858e63efaa0e4ad86b61c0459ea32fa": "Vous devez connecter le modèle {{Email}} à un connecteur {{Mail}}", + "03f79fa268fe199de2ce4345515431c1": "Aucun enregistrement de changement trouvé pour {0} avec l'id {1}", + "04bd8af876f001ceaf443aad6a9002f9": "L'authentification exige que le modèle {0} soit défini.", + "0731d0109e46c21a4e34af3346ed4856": "Ce comportement peut changer dans la version principale suivante.", + "095afbf2f1f0e5be678f5dac5c54e717": "Accès refusé", "0caffe1d763c8cca6a61814abe33b776": "L'adresse électronique est obligatoire", - "1b2a6076dccbe91a56f1672eb3b8598c": "Le corps de réponse contient les propriétés de {{AccessToken}} créées lors de la connexion.\nEn fonction de la valeur du paramètre `include`, le corps peut contenir des propriétés supplémentaires :\n\n - `user` - `U+007BUserU+007D` - Données de l'utilisateur connecté. {{(`include=user`)}}\n\n", - "2362ba55796c733e337af169922327a2": "Objets associés à inclure dans la réponse. Pour plus de détails, voir la description de la valeur de retour.", - "306999d39387d87b2638199ff0bed8ad": "Réinitialisez le mot de passe pour un utilisateur avec une adresse électronique.", - "3aae63bb7e8e046641767571c1591441": "la connexion a échoué car l'adresse électronique n'a pas été vérifiée", - "3caaa84fc103d6d5612173ae6d43b245": "Jeton non valide : {0}", - "42e3fa18945255412ebc6561e2c6f1dc": "Confirmez un enregistrement d'utilisateur avec jeton de vérification d'adresse électronique.", - "430b6326d7ebf6600a39f614ef516bc8": "Ne fournissez pas cet argument ; il est extrait automatiquement des en-têtes de la demande.", - "44a6c8b1ded4ed653d19ddeaaf89a606": "Adresse électronique introuvable", - "5e81ad3847a290dc650b47618b9cbc7e": "échec de la connexion", - "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "Connectez un utilisateur avec nom d'utilisateur/adresse électronique et mot de passe.", - "8608c28f5e6df0008266e3c497836176": "Déconnectez un utilisateur avec jeton d'accès.", - "860d1a0b8bd340411fb32baa72867989": "Le transport ne prend pas en charge les réacheminements HTTP.", - "895b1f941d026870b3cc8e6af087c197": "{{username}} ou {{email}} est obligatoire", - "a50d10fc6e0959b220e085454c40381e": "Utilisateur introuvable : {0}", - "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} est obligatoire", - "c34fa20eea0091747fcc9eda204b8d37": "impossible de trouver {{accessToken}}", - "c68a93f0a9524fed4ff64372fc90c55f": "Obligation de fournir une adresse électronique valide", - "f58cdc481540cd1f69a4aa4da2e37981": "Mot de passe non valide : {0}", - "f0bd73df8714cefb925e3b8da2f4c5f6": "résultat :{0}", "10e01c895dc0b2fecc385f9f462f1ca6": "une liste de couleurs est disponible sur {{http://localhost:3000/colors}}", - "8a27e0c9ce3ebf0e0c3978efb456e13e": "Demande à l'hôte {0}", - "a40684f5a9f546115258b76938d1de37": "Une liste de couleurs est disponible sur {{http://localhost:3000/colors}}", + "1b2a6076dccbe91a56f1672eb3b8598c": "Le corps de réponse contient les propriétés de {{AccessToken}} créées lors de la connexion.\nEn fonction de la valeur du paramètre `include`, le corps peut contenir des propriétés supplémentaires :\n\n - `user` - `U+007BUserU+007D` - Données de l'utilisateur connecté. {{(`include=user`)}}\n\n", + "1d7833c3ca2f05fdad8fad7537531c40": "\t SUJET :{0}", "1e85f822b547a75d7d385048030e4ecb": "Création de : {0}", - "7d5e7ed0efaedf3f55f380caae0df8b8": "Ma première application mobile", - "04bd8af876f001ceaf443aad6a9002f9": "L'authentification exige que le modèle {0} soit défini.", - "095afbf2f1f0e5be678f5dac5c54e717": "Accès refusé", + "275f22ab95671f095640ca99194b7635": "\t DE :{0}", "2d3071e3b18681c80a090dc0efbdb349": "impossible de trouver {0} avec l'id {1}", + "2d78192c43fd2ec52ec18f3918894f9a": "Le middleware {0} est obsolète. Pour plus de détails, voir {1}.", + "308e1d484516a33df788f873e65faaff": "Le modèle `{0}` étend le `DataModel obsolète. Utilisez à la place `PersistedModel`.", "316e5b82c203cf3de31a449ee07d0650": "Valeur booléenne attendue, {0} obtenu", - "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Impossible de créer la source de données {0} : {1}", - "7e287fc885d9fdcf42da3a12f38572c1": "Autorisation requise", - "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} a été supprimé ; utilisez à la place le nouveau module {{loopback-boot}}", - "1d7833c3ca2f05fdad8fad7537531c40": "\t SUJET :{0}", - "275f22ab95671f095640ca99194b7635": "\t DE :{0}", + "320c482401afa1207c04343ab162e803": "Type de principal non valide : {0}", + "3438fab56cc7ab92dfd88f0497e523e0": "La propriété relations de la configuration `{0}` doit être un objet", + "35e5252c62d80f8c54a5290d30f4c7d0": "Vérifiez votre courrier électronique en ouvrant ce lien dans un navigateur Web :\n\t{0}", + "3aae63bb7e8e046641767571c1591441": "la connexion a échoué car l'adresse électronique n'a pas été vérifiée", + "3aecb24fa8bdd3f79d168761ca8a6729": "Phase {{middleware}} inconnue {0}", + "3caaa84fc103d6d5612173ae6d43b245": "Jeton non valide : {0}", + "3d617953470be16d0c2b32f0bcfbb5ee": "Merci pour votre inscription", "3d63008ccfb2af1db2142e8cc2716ace": "Avertissement : Aucun transport de courrier électronique n'est spécifié pour l'envoi d'un message électronique. Configurez un transport pour envoyer des messages électroniques.", + "4203ab415ec66a78d3164345439ba76e": "Impossible d'appeler {0}.{1}(). La méthode {2} n'a pas été configurée. {{PersistedModel}} n'a pas été associé correctement à {{DataSource}} !", + "44a6c8b1ded4ed653d19ddeaaf89a606": "Adresse électronique introuvable", "4a4f04a4e480fc5d4ee73b84d9a4b904": "Envoi d'un message électronique :", - "63a091ced88001ab6acb58f61ec041c5": "\t TEXTE :{0}", - "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML :{0}", - "ecb06666ef95e5db27a5ac1d6a17923b": "\t A :{0}", - "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORT :{0}", - "0da38687fed24275c1547e815914a8e3": "Supprimez un élément lié par id pour {0}.", - "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "Critères de mise en concordance des instances de modèle", - "22fe62fa8d595b72c62208beddaa2a56": "Mettez à jour un élément lié par id pour {0}.", - "528325f3cbf1b0ab9a08447515daac9a": "Mettez à jour {0} de ce modèle.", - "543d19bad5e47ee1e9eb8af688e857b4": "Clé externe pour {0}.", - "598ff0255ffd1d1b71e8de55dbe2c034": "Vérifiez l'existence de la relation {0} à un élément par id.", - "5a36cc6ba0cc27c754f6c5ed6015ea3c": "Supprimez la relation {0} à un élément par id.", + "4b494de07f524703ac0879addbd64b13": "Le courrier électronique n'a pas été vérifié", + "4cac5f051ae431321673e04045d37772": "Le modèle `{0}` étend un modèle inconnu `{1}`. Utilisation de `PersistedModel` comme base.", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Impossible de créer la source de données {0} : {1}", + "5858e63efaa0e4ad86b61c0459ea32fa": "Vous devez connecter le modèle {{Email}} à un connecteur {{Mail}}", + "5e81ad3847a290dc650b47618b9cbc7e": "échec de la connexion", "5fa3afb425819ebde958043e598cb664": "impossible de trouver un modèle avec {{id}} {0}", "61e5deebaf44d68f4e6a508f30cc31a3": "La relation `{0}` n'existe pas pour le modèle `{1}`", - "651f0b3cbba001635152ec3d3d954d0a": "Recherchez un élément lié par id pour {0}.", - "7bc7b301ad9c4fc873029d57fb9740fe": "Demandes {0} de {1}.", - "7c837b88fd0e509bd3fc722d7ddf0711": "Clé externe pour {0}", - "830cb6c862f8f364e9064cea0026f701": "Extrait la relation hasOne {0}.", - "855ecd4a64885ba272d782435f72a4d4": "ID \"{0}\" inconnu \"{1}\".", - "86254879d01a60826a851066987703f2": "Ajoutez un élément lié par id pour {0}.", - "8ae418c605b6a45f2651be9b1677c180": "Méthode distante non valide : `{0}`", - "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Extrait la relation belongsTo {0}.", - "c0057a569ff9d3b509bac61a4b2f605d": "Supprime tous les {0} de ce modèle.", - "cd0412f2f33a4a2a316acc834f3f21a6": "obligation de spécifier {{id}} ou {{data}}", - "d6f43b266533b04d442bdb3955622592": "Crée une instance dans {0} de ce modèle.", - "da13d3cdf21330557254670dddd8c5c7": "Compte {0} de {1}.", - "e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" {{id}} \"{1}\" inconnu.", - "f66ae3cf379b2fce28575a3282defe1a": "Supprime {0} de ce modèle.", - "03f79fa268fe199de2ce4345515431c1": "Aucun enregistrement de changement trouvé pour {0} avec l'id {1}", - "0f1c71f74b040bfbe8d384a414e31f03": "Obtenez un ensemble d'écarts et de conflits depuis le point de contrôle donné.", - "15254dec061d023d6c030083a0cef50f": "Créez une instance du modèle et rendez-la persistante dans la source de données.", - "16a11368d55b85a209fc6aea69ee5f7a": "Supprimez tous les enregistrements correspondants.", - "1bc1d489ddf347af47af3d9b1fc7cc15": "Exécutez plusieurs mises à jour simultanément. Remarque : ce n'est pas atomique.", - "1bc7d8283c9abda512692925c8d8e3c0": "Obtenez le point de contrôle en cours.", - "1caa7cc61266e7aef7db7d2f0e27ac3e": "Mettez à jour les propriétés de l'enregistrement d'un changement le plus récent conservé pour cette instance.", - "2a7df74fe6e8462e617b79d5fbb536ea": "Obtenez l'enregistrement d'un changement le plus récent pour cette instance.", - "2a9684b3d5b3b67af74bac74eb1b0843": "Recherchez toutes les instances du modèle correspondant au filtre à partir de la source de données.", - "2e50838caf0c927735eb15d12866bdd7": "Obtenez les changements apportés à un modèle depuis un point de contrôle donné. Fournissez un objet de filtre pour réduire le nombre de résultats renvoyés.", - "4203ab415ec66a78d3164345439ba76e": "Impossible d'appeler {0}.{1}(). La méthode {2} n'a pas été configurée. {{PersistedModel}} n'a pas été associé correctement à {{DataSource}} !", - "51ea9b6245bb5e672b236d640ca3b048": "Objet des paires nom-valeur de la propriété change", - "55ddedd4c501348f82cb89db02ec85c1": "Objet des paires nom-valeur de la propriété model", - "5aaa76c72ae1689fd3cf62589784a4ba": "Mettez à jour les attributs pour une instance de modèle et rendez-la persistante dans la source de données.", - "5f659bbc15e6e2b249fa33b3879b5f69": "Recherchez une instance de modèle par {{id}} à partir de la source de données.", "62e8b0a733417978bab22c8dacf5d7e6": "Impossible d'appliquer des mises à jour en bloc ; le connecteur ne signale pas correctement le nombre d'enregistrements mis à jour.", - "6329e0ac1de3c250ebb1df5afd5a2a7b": "Nombre d'instances mises à jour", + "63a091ced88001ab6acb58f61ec041c5": "\t TEXTE :{0}", "6bc376432cd9972cf991aad3de371e78": "Données manquantes pour le changement : {0}", - "79295ac04822d2e9702f0dd1d0240336": "Mettez à jour les instances du modèle correspondant à {{where}} à partir de la source de données.", - "7f2fde7f0f860ead224b11ba8d75aa1c": "Créez une liste de mises à jour à partir d'une liste d'écarts.", - "89b57e764c2267129294b07589dbfdc2": "Supprimez une instance de modèle par {{id}} à partir de la source de données.", - "8bab6720ecc58ec6412358c858a53484": "La mise à jour en bloc a échoué ; le connecteur a modifié un nombre inattendu d'enregistrements : {0}", - "a98b6cc6547706b5c6bffde0ed5fd55c": "Recherchez la première instance du modèle correspondant au filtre à partir de la source de données.", - "c46d4aba1f14809c16730faa46933495": "Filtrer en définissant fields et include", - "c65600640f206f585d300b4bcb699d95": "Créez un point de contrôle.", - "cf64c7afc74d3a8120abcd028f98c770": "Mettez à jour une instance de modèle existante ou insérez-en une nouvelle dans la source de données.", - "dcb6261868ff0a7b928aa215b07d068c": "Créez un flux de changements.", - "e43e320a435ec1fa07648c1da0d558a7": "Vérifiez si une instance de modèle existe dans la source de données.", - "e92aa25b6b864e3454b65a7c422bd114": "La mise à jour en bloc a échoué ; le connecteur a supprimé un nombre inattendu d'enregistrements : {0}", - "ea63d226b6968e328bdf6876010786b5": "Impossible d'appliquer des mises à jour en bloc ; le connecteur ne signale pas correctement le nombre d'enregistrements supprimés.", - "f1d4ac54357cc0932f385d56814ba7e4": "Conflit", - "f37d94653793e33f4203e45b4a1cf106": "Dénombrez les instances du modèle correspondant à where à partir de la source de données.", - "0731d0109e46c21a4e34af3346ed4856": "Ce comportement peut changer dans la version principale suivante.", - "2e110abee2c95bcfc2dafd48be7e2095": "Impossible de configurer {0} : {{config.dataSource}} doit être une instance de {{DataSource}}", - "308e1d484516a33df788f873e65faaff": "Le modèle `{0}` étend le `DataModel obsolète. Utilisez à la place `PersistedModel`.", - "3438fab56cc7ab92dfd88f0497e523e0": "La propriété relations de la configuration `{0}` doit être un objet", - "4cac5f051ae431321673e04045d37772": "Le modèle `{0}` étend un modèle inconnu `{1}`. Utilisation de `PersistedModel` comme base.", + "705c2d456a3e204c4af56e671ec3225c": "{{accessToken}} introuvable", "734a7bebb65e10899935126ba63dd51f": "La propriété options de la configuration `{0}` doit être un objet", "779467f467862836e19f494a37d6ab77": "La propriété acls de la configuration `{0}` doit être un tableau d'objets", + "7d5e7ed0efaedf3f55f380caae0df8b8": "Ma première application mobile", + "7e0fca41d098607e1c9aa353c67e0fa1": "Jeton d'accès non valide", + "7e287fc885d9fdcf42da3a12f38572c1": "Autorisation requise", + "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} est nécessaire pour la déconnexion", "80a32e80cbed65eba2103201a7c94710": "Modèle introuvable : {0}", "83cbdc2560ba9f09155ccfc63e08f1a1": "La propriété `{0}` ne peut pas être reconfigurée pour `{1}`", + "855ecd4a64885ba272d782435f72a4d4": "ID \"{0}\" inconnu \"{1}\".", + "860d1a0b8bd340411fb32baa72867989": "Le transport ne prend pas en charge les réacheminements HTTP.", + "895b1f941d026870b3cc8e6af087c197": "{{username}} ou {{email}} est obligatoire", + "8a17c5ef611e2e7535792316e66b8fca": "Mot de passe trop long : {0}", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "Demande à l'hôte {0}", + "8ae418c605b6a45f2651be9b1677c180": "Méthode distante non valide : `{0}`", + "8bab6720ecc58ec6412358c858a53484": "La mise à jour en bloc a échoué ; le connecteur a modifié un nombre inattendu d'enregistrements : {0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML :{0}", "97795efe0c3eb7f35ce8cf8cfe70682b": "La propriété {{`dataSource`}} est manquante dans la configuration de `{0}`.\nUtilisez `null` ou `false` pour marquer les modèles non associés à une source de données.", + "a40684f5a9f546115258b76938d1de37": "Une liste de couleurs est disponible sur {{http://localhost:3000/colors}}", + "a50d10fc6e0959b220e085454c40381e": "Utilisateur introuvable : {0}", "a80038252430df2754884bf3c845c4cf": "Métadonnées remoting pour \"{0}.{1}\" ne comporte pas l'indicateur \"isStatic\" ; la méthode est enregistrée en tant que méthode instance.", + "b6f740aeb6f2eb9bee9cb049dbfe6a28": "\"{0}\" {{key}} \"{1}\" inconnu.", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} est obligatoire", + "c2b5d51f007178170ca3952d59640ca4": "Impossible de rectifier les modifications {0} :\n{1}", + "c68a93f0a9524fed4ff64372fc90c55f": "Obligation de fournir une adresse électronique valide", + "cd0412f2f33a4a2a316acc834f3f21a6": "obligation de spécifier {{id}} ou {{data}}", + "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} a été supprimé ; utilisez à la place le nouveau module {{loopback-boot}}", + "d9ef6dc3770dd8f80a129e92a79851f3": "{0} est obsolète. Pour plus de détails, voir {1}.", "dc568bee32deb0f6eaf63e73b20e8ceb": "Le paramètre \"methods\" non objet de \"{0}\" est ignoré.", - "3aecb24fa8bdd3f79d168761ca8a6729": "Phase {{middleware}} inconnue {0}" + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" {{id}} \"{1}\" inconnu.", + "e92aa25b6b864e3454b65a7c422bd114": "La mise à jour en bloc a échoué ; le connecteur a supprimé un nombre inattendu d'enregistrements : {0}", + "ea63d226b6968e328bdf6876010786b5": "Impossible d'appliquer des mises à jour en bloc ; le connecteur ne signale pas correctement le nombre d'enregistrements supprimés.", + "ead044e2b4bce74b4357f8a03fb78ec4": "Impossible d'appeler {0}.{1}(). La méthode {2} n'a pas été configurée. {{KeyValueModel}} n'a pas été associé correctement à {{DataSource}} !", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t A :{0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORT :{0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "résultat :{0}", + "f1d4ac54357cc0932f385d56814ba7e4": "Conflit", + "f58cdc481540cd1f69a4aa4da2e37981": "Mot de passe non valide : {0}" } diff --git a/intl/it/messages.json b/intl/it/messages.json index d846e755a..f4b23f5cc 100644 --- a/intl/it/messages.json +++ b/intl/it/messages.json @@ -1,116 +1,76 @@ { - "3b46d3a780fd6ae5f95ade489a0efffe": "Il contesto corrente non è supportato nel browser.", - "7e0fca41d098607e1c9aa353c67e0fa1": "Token di accesso non valido", - "320c482401afa1207c04343ab162e803": "Tipo principal non valido: {0}", - "c2b5d51f007178170ca3952d59640ca4": "Impossibile correggere {0} modifiche:\n{1}", - "5858e63efaa0e4ad86b61c0459ea32fa": "È necessario collegare il modello {{Email}} ad un connettore {{Mail}}", + "03f79fa268fe199de2ce4345515431c1": "Nessun record di modifica trovato per {0} con id {1}", + "04bd8af876f001ceaf443aad6a9002f9": "L'autenticazione richiede che sia definito il modello {0}.", + "0731d0109e46c21a4e34af3346ed4856": "Questo funzionamento può essere modificato nella versione principale successiva.", + "095afbf2f1f0e5be678f5dac5c54e717": "Accesso negato", "0caffe1d763c8cca6a61814abe33b776": "L'email è obbligatoria", - "1b2a6076dccbe91a56f1672eb3b8598c": "Il corpo della risposta contiene proprietà del {{AccessToken}} creato all'accesso.\nIn base al valore del parametro `include`, il corpo può contenere ulteriori proprietà:\n\n - `user` - `U+007BUserU+007D` - Dati dell'utente attualmente collegato.. {{(`include=user`)}}\n\n", - "2362ba55796c733e337af169922327a2": "Oggetti correlati da includere nella risposta. Per ulteriori dettagli, consultare la descrizione del valore di restituzione.", - "306999d39387d87b2638199ff0bed8ad": "Reimpostare la password per un utente con email.", - "3aae63bb7e8e046641767571c1591441": "login non riuscito perché l'email non è stata verificata", - "3caaa84fc103d6d5612173ae6d43b245": "Token non valido: {0}", - "42e3fa18945255412ebc6561e2c6f1dc": "Confermare una registrazione utente con token di verifica email.", - "430b6326d7ebf6600a39f614ef516bc8": "Non fornire questo argomento, viene automaticamente estratto dalle intestazioni richiesta.", - "44a6c8b1ded4ed653d19ddeaaf89a606": "Email non trovata", - "5e81ad3847a290dc650b47618b9cbc7e": "login non riuscito", - "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "Eseguire il login di un utente con nome utente/email e password.", - "8608c28f5e6df0008266e3c497836176": "Scollegare un utente con token di accesso.", - "860d1a0b8bd340411fb32baa72867989": "Il trasporto non supporta i reindirizzamenti HTTP.", - "895b1f941d026870b3cc8e6af087c197": "Sono richiesti {{username}} o {{email}}", - "a50d10fc6e0959b220e085454c40381e": "Utente non trovato: {0}", - "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} è obbligatorio", - "c34fa20eea0091747fcc9eda204b8d37": "impossibile trovare {{accessToken}}", - "c68a93f0a9524fed4ff64372fc90c55f": "È necessario fornire una email valida", - "f58cdc481540cd1f69a4aa4da2e37981": "Password non valida: {0}", - "f0bd73df8714cefb925e3b8da2f4c5f6": "risultato:{0}", "10e01c895dc0b2fecc385f9f462f1ca6": "un elenco dei colori è disponibile all'indirizzo {{http://localhost:3000/colors}}", - "8a27e0c9ce3ebf0e0c3978efb456e13e": "Richiesta all'host {0}", - "a40684f5a9f546115258b76938d1de37": "Un elenco dei colori è disponibile all'indirizzo {{http://localhost:3000/colors}}", + "1b2a6076dccbe91a56f1672eb3b8598c": "Il corpo della risposta contiene proprietà del {{AccessToken}} creato all'accesso.\nIn base al valore del parametro `include`, il corpo può contenere ulteriori proprietà:\n\n - `user` - `U+007BUserU+007D` - Dati dell'utente attualmente collegato.. {{(`include=user`)}}\n\n", + "1d7833c3ca2f05fdad8fad7537531c40": "\t OGGETTO:{0}", "1e85f822b547a75d7d385048030e4ecb": "Creato: {0}", - "7d5e7ed0efaedf3f55f380caae0df8b8": "Prima applicazione mobile personale", - "04bd8af876f001ceaf443aad6a9002f9": "L'autenticazione richiede che sia definito il modello {0}.", - "095afbf2f1f0e5be678f5dac5c54e717": "Accesso negato", + "275f22ab95671f095640ca99194b7635": "\t DA:{0}", "2d3071e3b18681c80a090dc0efbdb349": "impossibile trovare {0} con id {1}", + "2d78192c43fd2ec52ec18f3918894f9a": "{0} middleware is deprecated. Consultare {1} per ulteriori dettagli.", + "308e1d484516a33df788f873e65faaff": "Il modello `{0}` estende il modello `DataModel obsoleto. Utilizzare `PersistedModel`.", "316e5b82c203cf3de31a449ee07d0650": "Previsto valore booleano, ricevuto {0}", - "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Impossibile creare l'origine dati {0}: {1}", - "7e287fc885d9fdcf42da3a12f38572c1": "Autorizzazione richiesta", - "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} è stato rimosso, utilizzare il nuovo modulo {{loopback-boot}}", - "1d7833c3ca2f05fdad8fad7537531c40": "\t OGGETTO:{0}", - "275f22ab95671f095640ca99194b7635": "\t DA:{0}", + "320c482401afa1207c04343ab162e803": "Tipo principal non valido: {0}", + "3438fab56cc7ab92dfd88f0497e523e0": "La proprietà relations della configurazione `{0}` deve essere un oggetto", + "35e5252c62d80f8c54a5290d30f4c7d0": "Verificare la e-mail aprendo questo link in un browser web:\n\t{0}", + "3aae63bb7e8e046641767571c1591441": "login non riuscito perché l'email non è stata verificata", + "3aecb24fa8bdd3f79d168761ca8a6729": "Fase {{middleware}} sconosciuta {0}", + "3caaa84fc103d6d5612173ae6d43b245": "Token non valido: {0}", + "3d617953470be16d0c2b32f0bcfbb5ee": "Grazie per essersi registrati", "3d63008ccfb2af1db2142e8cc2716ace": "Avvertenza: nessun trasporto email specificato per l'invio della email. Configurare un trasporto per inviare messaggi email.", + "4203ab415ec66a78d3164345439ba76e": "Impossibile richiamare {0}.{1}(). Il metodo {2} non è stato configurato. {{PersistedModel}} non è stato correttamente collegato ad una {{DataSource}}!", + "44a6c8b1ded4ed653d19ddeaaf89a606": "Email non trovata", "4a4f04a4e480fc5d4ee73b84d9a4b904": "Invio email:", - "63a091ced88001ab6acb58f61ec041c5": "\t TESTO:{0}", - "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", - "ecb06666ef95e5db27a5ac1d6a17923b": "\t A:{0}", - "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRASPORTO:{0}", - "0da38687fed24275c1547e815914a8e3": "Eliminare un elemento correlato in base all'ID per {0}.", - "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "Criteri per la corrispondenza delle istanze del modello", - "22fe62fa8d595b72c62208beddaa2a56": "Aggiornare un elemento correlato in base all'ID per {0}.", - "528325f3cbf1b0ab9a08447515daac9a": "Aggiornare {0} di questo modello.", - "543d19bad5e47ee1e9eb8af688e857b4": "Chiave esterna per {0}.", - "598ff0255ffd1d1b71e8de55dbe2c034": "Verificare l'esistenza della relazione {0} ad un elemento in base all'ID.", - "5a36cc6ba0cc27c754f6c5ed6015ea3c": "Rimuovere la relazione {0} ad un elemento in base all'ID.", + "4b494de07f524703ac0879addbd64b13": "La e-mail non è stata verificata", + "4cac5f051ae431321673e04045d37772": "Il modello `{0}` estende un modello sconosciuto `{1}`. Viene utilizzato `PersistedModel` come base.", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Impossibile creare l'origine dati {0}: {1}", + "5858e63efaa0e4ad86b61c0459ea32fa": "È necessario collegare il modello {{Email}} ad un connettore {{Mail}}", + "5e81ad3847a290dc650b47618b9cbc7e": "login non riuscito", "5fa3afb425819ebde958043e598cb664": "impossibile trovare un modello con {{id}} {0}", "61e5deebaf44d68f4e6a508f30cc31a3": "La relazione `{0}` non esiste per il modello `{1}`", - "651f0b3cbba001635152ec3d3d954d0a": "Trovare un elemento correlato in base all'ID per {0}.", - "7bc7b301ad9c4fc873029d57fb9740fe": "Query {0} di {1}.", - "7c837b88fd0e509bd3fc722d7ddf0711": "Chiave esterna per {0}", - "830cb6c862f8f364e9064cea0026f701": "Recupera la relazione hasOne {0}.", - "855ecd4a64885ba272d782435f72a4d4": "ID sconosciuto \"{0}\" \"{1}\".", - "86254879d01a60826a851066987703f2": "Aggiungere un elemento correlato in base all'ID per {0}.", - "8ae418c605b6a45f2651be9b1677c180": "Metodo remoto non valido: `{0}`", - "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Recupera la relazione belongsTo {0}.", - "c0057a569ff9d3b509bac61a4b2f605d": "Elimina tutti i {0} di questo modello.", - "cd0412f2f33a4a2a316acc834f3f21a6": "è necessario specificare {{id}} o {{data}}", - "d6f43b266533b04d442bdb3955622592": "Crea una nuova istanza di questo modello in {0}.", - "da13d3cdf21330557254670dddd8c5c7": "{0} di {1}.", - "e4434de4bb8f5a3cd1d416e4d80d7e0b": "{{id}} \"{0}\" sconosciuto \"{1}\".", - "f66ae3cf379b2fce28575a3282defe1a": "Elimina {0} di questo modello.", - "03f79fa268fe199de2ce4345515431c1": "Nessun record di modifica trovato per {0} con id {1}", - "0f1c71f74b040bfbe8d384a414e31f03": "Acquisire una serie di delta e conflitti a partire dal checkpoint fornito.", - "15254dec061d023d6c030083a0cef50f": "Creare una nuova istanza del modello e renderla permanente nell'origine dati.", - "16a11368d55b85a209fc6aea69ee5f7a": "Eliminare tutti i record corrispondenti.", - "1bc1d489ddf347af47af3d9b1fc7cc15": "Eseguire più aggiornamenti contemporaneamente. Nota: questa operazione non è atomica.", - "1bc7d8283c9abda512692925c8d8e3c0": "Acquisire il checkpoint corrente.", - "1caa7cc61266e7aef7db7d2f0e27ac3e": "Aggiornare le proprietà del record di modifiche più recente conservato per questa istanza.", - "2a7df74fe6e8462e617b79d5fbb536ea": "Acquisire il record di modifiche più recente per questa istanza.", - "2a9684b3d5b3b67af74bac74eb1b0843": "Trovare tutte le istanze del modello corrispondenti in base al filtro dall'origine dati.", - "2e50838caf0c927735eb15d12866bdd7": "Acquisire le modifiche ad un modello a partire da un determinato checkpoint. Fornire un oggetto filtro per ridurre il numero di risultati restituiti.", - "4203ab415ec66a78d3164345439ba76e": "Impossibile richiamare {0}.{1}(). Il metodo {2} non è stato configurato. {{PersistedModel}} non è stato correttamente collegato ad una {{DataSource}}!", - "51ea9b6245bb5e672b236d640ca3b048": "Un oggetto della coppia nome/valore della proprietà di modifica", - "55ddedd4c501348f82cb89db02ec85c1": "In oggetto della coppia nome/valore della proprietà del modello", - "5aaa76c72ae1689fd3cf62589784a4ba": "Aggiornare gli attributi per un'istanza del modello e renderli permanenti nell'origine dati.", - "5f659bbc15e6e2b249fa33b3879b5f69": "Trovare un'istanza del modello in base a {{id}} dall'origine dati.", "62e8b0a733417978bab22c8dacf5d7e6": "Impossibile applicare gli aggiornamenti in massa, il connettore non indica correttamente il numero di record aggiornati.", - "6329e0ac1de3c250ebb1df5afd5a2a7b": "Il numero di istanze aggiornate", + "63a091ced88001ab6acb58f61ec041c5": "\t TESTO:{0}", "6bc376432cd9972cf991aad3de371e78": "Dati mancanti per la modifica: {0}", - "79295ac04822d2e9702f0dd1d0240336": "Aggiornare le istanze del modello corrispondenti in base a {{where}} dall'origine dati.", - "7f2fde7f0f860ead224b11ba8d75aa1c": "Creare un elenco di aggiornamento da un elenco delta.", - "89b57e764c2267129294b07589dbfdc2": "Eliminare un'istanza del modello in base a {{id}} dall'origine dati.", - "8bab6720ecc58ec6412358c858a53484": "Aggiornamento in massa non riuscito, il connettore ha modificato un numero non previsto di record: {0}", - "a98b6cc6547706b5c6bffde0ed5fd55c": "Trovare la prima istanza del modello corrispondente in base al filtro dall'origine dati.", - "c46d4aba1f14809c16730faa46933495": "Filtro che definisce campi ed include", - "c65600640f206f585d300b4bcb699d95": "Creare un checkpoint.", - "cf64c7afc74d3a8120abcd028f98c770": "Aggiornare un'istanza del modello esistente oppure inserire una nuova istanza nell'origine dati.", - "dcb6261868ff0a7b928aa215b07d068c": "Creare un flusso di modifica.", - "e43e320a435ec1fa07648c1da0d558a7": "Verificare se nell'origine dati esiste un'istanza del modello.", - "e92aa25b6b864e3454b65a7c422bd114": "Aggiornamento in massa non riuscito, il connettore ha eliminato un numero non previsto di record: {0}", - "ea63d226b6968e328bdf6876010786b5": "Impossibile applicare gli aggiornamenti in massa, il connettore non indica correttamente il numero di record eliminati.", - "f1d4ac54357cc0932f385d56814ba7e4": "Conflitto", - "f37d94653793e33f4203e45b4a1cf106": "Conteggiare le istanze del modello corrispondenti in base a where dall'origine dati.", - "0731d0109e46c21a4e34af3346ed4856": "Questo funzionamento può essere modificato nella versione principale successiva.", - "2e110abee2c95bcfc2dafd48be7e2095": "Impossibile configurare {0}: {{config.dataSource}} deve essere un'istanza di {{DataSource}}", - "308e1d484516a33df788f873e65faaff": "Il modello `{0}` estende il modello `DataModel obsoleto. Utilizzare `PersistedModel`.", - "3438fab56cc7ab92dfd88f0497e523e0": "La proprietà relations della configurazione `{0}` deve essere un oggetto", - "4cac5f051ae431321673e04045d37772": "Il modello `{0}` estende un modello sconosciuto `{1}`. Viene utilizzato `PersistedModel` come base.", + "705c2d456a3e204c4af56e671ec3225c": "Could not find {{accessToken}}", "734a7bebb65e10899935126ba63dd51f": "La proprietà options della configurazione `{0}` deve essere un oggetto", "779467f467862836e19f494a37d6ab77": "La proprietà acls della configurazione `{0}` deve essere un array di oggetti", + "7d5e7ed0efaedf3f55f380caae0df8b8": "Prima applicazione mobile personale", + "7e0fca41d098607e1c9aa353c67e0fa1": "Token di accesso non valido", + "7e287fc885d9fdcf42da3a12f38572c1": "Autorizzazione richiesta", + "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} is required to logout", "80a32e80cbed65eba2103201a7c94710": "Modello non trovato: {0}", "83cbdc2560ba9f09155ccfc63e08f1a1": "Impossibile riconfigurare la proprietà `{0}` per `{1}`", + "855ecd4a64885ba272d782435f72a4d4": "ID sconosciuto \"{0}\" \"{1}\".", + "860d1a0b8bd340411fb32baa72867989": "Il trasporto non supporta i reindirizzamenti HTTP.", + "895b1f941d026870b3cc8e6af087c197": "Sono richiesti {{username}} o {{email}}", + "8a17c5ef611e2e7535792316e66b8fca": "Password troppo lunga: {0}", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "Richiesta all'host {0}", + "8ae418c605b6a45f2651be9b1677c180": "Metodo remoto non valido: `{0}`", + "8bab6720ecc58ec6412358c858a53484": "Aggiornamento in massa non riuscito, il connettore ha modificato un numero non previsto di record: {0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", "97795efe0c3eb7f35ce8cf8cfe70682b": "La configurazione di `{0}` non contiene la proprietà {{`dataSource`}}.\nUtilizzare `null` o `false` per contrassegnare i modelli non collegati ad alcuna origine dati.", + "a40684f5a9f546115258b76938d1de37": "Un elenco dei colori è disponibile all'indirizzo {{http://localhost:3000/colors}}", + "a50d10fc6e0959b220e085454c40381e": "Utente non trovato: {0}", "a80038252430df2754884bf3c845c4cf": "Metadati della comunicazione in remoto per \"{0}.{1}\" non presenta l'indicatore \"isStatic\", il metodo è registrato come metodo dell'istanza.", + "b6f740aeb6f2eb9bee9cb049dbfe6a28": "{{key}} \"{0}\" sconosciuto \"{1}\".", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} è obbligatorio", + "c2b5d51f007178170ca3952d59640ca4": "Impossibile correggere {0} modifiche:\n{1}", + "c68a93f0a9524fed4ff64372fc90c55f": "È necessario fornire una email valida", + "cd0412f2f33a4a2a316acc834f3f21a6": "è necessario specificare {{id}} o {{data}}", + "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} è stato rimosso, utilizzare il nuovo modulo {{loopback-boot}}", + "d9ef6dc3770dd8f80a129e92a79851f3": "{0} is deprecated. Consultare {1} per ulteriori dettagli.", "dc568bee32deb0f6eaf63e73b20e8ceb": "L'impostazione \"methods\" non oggetto di \"{0}\" viene ignorata.", - "3aecb24fa8bdd3f79d168761ca8a6729": "Fase {{middleware}} sconosciuta {0}" + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "{{id}} \"{0}\" sconosciuto \"{1}\".", + "e92aa25b6b864e3454b65a7c422bd114": "Aggiornamento in massa non riuscito, il connettore ha eliminato un numero non previsto di record: {0}", + "ea63d226b6968e328bdf6876010786b5": "Impossibile applicare gli aggiornamenti in massa, il connettore non indica correttamente il numero di record eliminati.", + "ead044e2b4bce74b4357f8a03fb78ec4": "Impossibile richiamare {0}.{1}(). Il metodo {2} non è stato configurato. {{KeyValueModel}} non è stato correttamente collegato ad una {{DataSource}}!", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t A:{0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRASPORTO:{0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "risultato:{0}", + "f1d4ac54357cc0932f385d56814ba7e4": "Conflitto", + "f58cdc481540cd1f69a4aa4da2e37981": "Password non valida: {0}" } diff --git a/intl/ja/messages.json b/intl/ja/messages.json index 29ac69882..7f793f7be 100644 --- a/intl/ja/messages.json +++ b/intl/ja/messages.json @@ -1,116 +1,76 @@ { - "3b46d3a780fd6ae5f95ade489a0efffe": "現行コンテキストはブラウザーでサポートされていません。", - "7e0fca41d098607e1c9aa353c67e0fa1": "無効なアクセス・トークン", - "320c482401afa1207c04343ab162e803": "無効なプリンシパル・タイプ: {0}", - "c2b5d51f007178170ca3952d59640ca4": "{0} の変更を修正できません:\n{1}", - "5858e63efaa0e4ad86b61c0459ea32fa": "{{Email}} モデルを {{Mail}} コネクターに接続する必要があります", + "03f79fa268fe199de2ce4345515431c1": "ID {1} の {0} の変更レコードが見つかりませんでした", + "04bd8af876f001ceaf443aad6a9002f9": "認証では、モデル {0} を定義する必要があります。", + "0731d0109e46c21a4e34af3346ed4856": "この動作は次のメジャー・バージョンで変更される可能性があります。", + "095afbf2f1f0e5be678f5dac5c54e717": "アクセス拒否", "0caffe1d763c8cca6a61814abe33b776": "E メールは必須です", - "1b2a6076dccbe91a56f1672eb3b8598c": "応答本文には、ログイン時に作成された {{AccessToken}} のプロパティーが含まれます。\n`include` パラメーターの値によっては、本文に追加のプロパティーが含まれる場合があります:\n\n - `user` - `U+007BUserU+007D` - 現在ログインしているユーザーのデータ。 {{(`include=user`)}}\n\n", - "2362ba55796c733e337af169922327a2": "応答に組み込む関連オブジェクト。 詳細については、戻り値の説明を参照してください。", - "306999d39387d87b2638199ff0bed8ad": "E メールを使用してユーザーのパスワードをリセットします。", - "3aae63bb7e8e046641767571c1591441": "E メールが検証されていないため、ログインに失敗しました", - "3caaa84fc103d6d5612173ae6d43b245": "無効なトークン: {0}", - "42e3fa18945255412ebc6561e2c6f1dc": "E メール検証トークンを使用してユーザー登録を確認します。", - "430b6326d7ebf6600a39f614ef516bc8": "この引数を指定しないでください。これは要求ヘッダーから自動的に抽出されます。", - "44a6c8b1ded4ed653d19ddeaaf89a606": "E メールが見つかりません", - "5e81ad3847a290dc650b47618b9cbc7e": "ログインに失敗しました", - "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "ユーザー名/E メールとパスワードを使用してユーザーにログインします。", - "8608c28f5e6df0008266e3c497836176": "アクセス・トークンを使用してユーザーをログアウトします。", - "860d1a0b8bd340411fb32baa72867989": "トランスポートでは HTTP リダイレクトはサポートされません。", - "895b1f941d026870b3cc8e6af087c197": "{{username}} または {{email}} が必要です", - "a50d10fc6e0959b220e085454c40381e": "ユーザーが見つかりません: {0}", - "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} は必須です", - "c34fa20eea0091747fcc9eda204b8d37": "{{accessToken}} が見つかりませんでした", - "c68a93f0a9524fed4ff64372fc90c55f": "有効な E メールを指定する必要があります", - "f58cdc481540cd1f69a4aa4da2e37981": "無効なパスワード: {0}", - "f0bd73df8714cefb925e3b8da2f4c5f6": "結果:{0}", "10e01c895dc0b2fecc385f9f462f1ca6": "カラー・リストは {{http://localhost:3000/colors}} で利用できます", - "8a27e0c9ce3ebf0e0c3978efb456e13e": "ホスト {0} への要求", - "a40684f5a9f546115258b76938d1de37": "カラー・リストは {{http://localhost:3000/colors}} で利用できます", + "1b2a6076dccbe91a56f1672eb3b8598c": "応答本文には、ログイン時に作成された {{AccessToken}} のプロパティーが含まれます。\n`include` パラメーターの値によっては、本文に追加のプロパティーが含まれる場合があります:\n\n - `user` - `U+007BUserU+007D` - 現在ログインしているユーザーのデータ。 {{(`include=user`)}}\n\n", + "1d7833c3ca2f05fdad8fad7537531c40": "\t 件名:{0}", "1e85f822b547a75d7d385048030e4ecb": "作成済み: {0}", - "7d5e7ed0efaedf3f55f380caae0df8b8": "最初のモバイル・アプリケーション", - "04bd8af876f001ceaf443aad6a9002f9": "認証では、モデル {0} を定義する必要があります。", - "095afbf2f1f0e5be678f5dac5c54e717": "アクセス拒否", + "275f22ab95671f095640ca99194b7635": "\t 送信元:{0}", "2d3071e3b18681c80a090dc0efbdb349": "ID {1} の {0} が見つかりませんでした", + "2d78192c43fd2ec52ec18f3918894f9a": "{0} ミドルウェアは非推奨です。詳しくは、{1} を参照してください。", + "308e1d484516a33df788f873e65faaff": "モデル `{0}` は非推奨の `DataModel を拡張しています。 代わりに `PersistedModel` を使用してください。", "316e5b82c203cf3de31a449ee07d0650": "ブール値が必要ですが、{0} が取得されました", - "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "データ・ソース {0}: {1} を作成できません", - "7e287fc885d9fdcf42da3a12f38572c1": "許可が必要です", - "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} は削除されました。代わりに新規モジュール {{loopback-boot}} を使用してください", - "1d7833c3ca2f05fdad8fad7537531c40": "\t 件名:{0}", - "275f22ab95671f095640ca99194b7635": "\t 送信元:{0}", + "320c482401afa1207c04343ab162e803": "無効なプリンシパル・タイプ: {0}", + "3438fab56cc7ab92dfd88f0497e523e0": "`{0}` 構成の関係プロパティーはオブジェクトでなければなりません", + "35e5252c62d80f8c54a5290d30f4c7d0": "Web ブラウザーで次のリンクを開いて、E メールを検証してください: \n\t{0}", + "3aae63bb7e8e046641767571c1591441": "E メールが検証されていないため、ログインに失敗しました", + "3aecb24fa8bdd3f79d168761ca8a6729": "不明な {{middleware}} フェーズ {0}", + "3caaa84fc103d6d5612173ae6d43b245": "無効なトークン: {0}", + "3d617953470be16d0c2b32f0bcfbb5ee": "ご登録いただき、ありがとうございます。", "3d63008ccfb2af1db2142e8cc2716ace": "警告: E メール送信用の E メール・トランスポートが指定されていません。 メール・メッセージを送信するためのトランスポートをセットアップしてください。", + "4203ab415ec66a78d3164345439ba76e": "{0}.{1}() を呼び出せません。 {2} メソッドがセットアップされていません。 {{PersistedModel}} は {{DataSource}} に正しく付加されていません。", + "44a6c8b1ded4ed653d19ddeaaf89a606": "E メールが見つかりません", "4a4f04a4e480fc5d4ee73b84d9a4b904": "メールの送信:", - "63a091ced88001ab6acb58f61ec041c5": "\t テキスト:{0}", - "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", - "ecb06666ef95e5db27a5ac1d6a17923b": "\t 宛先:{0}", - "f0aed00a3d3d0b97d6594e4b70e0c201": "\t トランスポート:{0}", - "0da38687fed24275c1547e815914a8e3": "ID を指定して {0} の関連項目を削除します。", - "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "モデル・インスタンスの一致基準", - "22fe62fa8d595b72c62208beddaa2a56": "ID を指定して {0} の関連項目を更新します。", - "528325f3cbf1b0ab9a08447515daac9a": "このモデルの {0} を更新します。", - "543d19bad5e47ee1e9eb8af688e857b4": "{0} の外部キー。", - "598ff0255ffd1d1b71e8de55dbe2c034": "ID を指定して項目との {0} 関係があることを確認します。", - "5a36cc6ba0cc27c754f6c5ed6015ea3c": "ID を指定して項目との {0} 関係を削除します。", + "4b494de07f524703ac0879addbd64b13": "E メールが検証されていません", + "4cac5f051ae431321673e04045d37772": "モデル `{0}` は不明のモデル `{1}` を拡張しています。 ベースとして `PersistedModel` を使用します。", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "データ・ソース {0}: {1} を作成できません", + "5858e63efaa0e4ad86b61c0459ea32fa": "{{Email}} モデルを {{Mail}} コネクターに接続する必要があります", + "5e81ad3847a290dc650b47618b9cbc7e": "ログインに失敗しました", "5fa3afb425819ebde958043e598cb664": "{{id}} {0} のモデルが見つかりませんでした", "61e5deebaf44d68f4e6a508f30cc31a3": "モデル `{1}` には関係 `{0}` が存在しません", - "651f0b3cbba001635152ec3d3d954d0a": "ID を指定して {0} の関連項目を検索します。", - "7bc7b301ad9c4fc873029d57fb9740fe": "{1} の {0} を照会します。", - "7c837b88fd0e509bd3fc722d7ddf0711": "{0} の外部キー", - "830cb6c862f8f364e9064cea0026f701": "hasOne 関係 {0} をフェッチします。", - "855ecd4a64885ba272d782435f72a4d4": "\"{0}\" ID \"{1}\" が不明です。", - "86254879d01a60826a851066987703f2": "ID を指定して {0} の関連項目を追加します。", - "8ae418c605b6a45f2651be9b1677c180": "無効なリモート・メソッド: `{0}`", - "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "belongsTo 関係 {0} をフェッチします。", - "c0057a569ff9d3b509bac61a4b2f605d": "このモデルのすべての {0} を削除します。", - "cd0412f2f33a4a2a316acc834f3f21a6": "{{id}} または {{data}} を指定する必要があります", - "d6f43b266533b04d442bdb3955622592": "このモデルの {0} に新規インスタンスを作成します。", - "da13d3cdf21330557254670dddd8c5c7": "{1} の {0} をカウントします。", - "e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" {{id}} \"{1}\" が不明です。", - "f66ae3cf379b2fce28575a3282defe1a": "このモデルの {0} を削除します。", - "03f79fa268fe199de2ce4345515431c1": "ID {1} の {0} の変更レコードが見つかりませんでした", - "0f1c71f74b040bfbe8d384a414e31f03": "指定されたチェックポイント以降の一連の差分および競合を取得します。", - "15254dec061d023d6c030083a0cef50f": "モデルの新規インスタンスを作成し、それをデータ・ソースに保管します。", - "16a11368d55b85a209fc6aea69ee5f7a": "一致するすべてのレコードを削除します。", - "1bc1d489ddf347af47af3d9b1fc7cc15": "一度に複数の更新を実行します。 注: これはアトミックではありません。", - "1bc7d8283c9abda512692925c8d8e3c0": "現在のチェックポイントを取得します。", - "1caa7cc61266e7aef7db7d2f0e27ac3e": "このインスタンスについて保持されている最新の変更レコードのプロパティーを更新します。", - "2a7df74fe6e8462e617b79d5fbb536ea": "このインスタンスの最新の変更レコードを取得します。", - "2a9684b3d5b3b67af74bac74eb1b0843": "データ・ソースからフィルターで一致するモデルのすべてのインスタンスを検索します。", - "2e50838caf0c927735eb15d12866bdd7": "指定されたチェックポイント以降のモデルへの変更を取得します。フィルター・オブジェクトを指定すると、返される結果の件数が削減されます。", - "4203ab415ec66a78d3164345439ba76e": "{0}.{1}() を呼び出せません。 {2} メソッドがセットアップされていません。 {{PersistedModel}} は {{DataSource}} に正しく付加されていません。", - "51ea9b6245bb5e672b236d640ca3b048": "変更プロパティー名/値ペアのオブジェクト", - "55ddedd4c501348f82cb89db02ec85c1": "モデル・プロパティー名/値ペアのオブジェクト", - "5aaa76c72ae1689fd3cf62589784a4ba": "モデル・インスタンスの属性を更新し、それをデータ・ソースに保管します。", - "5f659bbc15e6e2b249fa33b3879b5f69": "{{id}} を指定してデータ・ソースからモデル・インスタンスを検索します。", "62e8b0a733417978bab22c8dacf5d7e6": "一括更新を適用できません。コネクターは更新されたレコードの数を正しく報告していません。", - "6329e0ac1de3c250ebb1df5afd5a2a7b": "更新されたインスタンスの数", + "63a091ced88001ab6acb58f61ec041c5": "\t テキスト:{0}", "6bc376432cd9972cf991aad3de371e78": "変更用のデータがありません: {0}", - "79295ac04822d2e9702f0dd1d0240336": "データ・ソースから {{where}} で一致するモデルのインスタンスを更新します。", - "7f2fde7f0f860ead224b11ba8d75aa1c": "差分リストから更新リストを作成します。", - "89b57e764c2267129294b07589dbfdc2": "{{id}} を指定してデータ・ソースからモデル・インスタンスを削除します。", - "8bab6720ecc58ec6412358c858a53484": "一括更新が失敗しました。コネクターは予期しない数のレコードを変更しました: {0}", - "a98b6cc6547706b5c6bffde0ed5fd55c": "データ・ソースからフィルターで一致するモデルの最初のインスタンスを検索します。", - "c46d4aba1f14809c16730faa46933495": "フィルター定義フィールドおよび include", - "c65600640f206f585d300b4bcb699d95": "チェックポイントを作成します。", - "cf64c7afc74d3a8120abcd028f98c770": "既存のモデル・インスタンスを更新するか、新規モデル・インスタンスをデータ・ソースに挿入します。", - "dcb6261868ff0a7b928aa215b07d068c": "変更ストリームを作成します。", - "e43e320a435ec1fa07648c1da0d558a7": "データ・ソースにモデル・インスタンスが存在するかどうかを確認します。", - "e92aa25b6b864e3454b65a7c422bd114": "一括更新が失敗しました。コネクターは予期しない数のレコードを削除しました: {0}", - "ea63d226b6968e328bdf6876010786b5": "一括更新を適用できません。コネクターは削除されたレコードの数を正しく報告していません。", - "f1d4ac54357cc0932f385d56814ba7e4": "競合", - "f37d94653793e33f4203e45b4a1cf106": "データ・ソースから where で一致するモデルのインスタンスをカウントします。", - "0731d0109e46c21a4e34af3346ed4856": "この動作は次のメジャー・バージョンで変更される可能性があります。", - "2e110abee2c95bcfc2dafd48be7e2095": "{0} を構成できません: {{config.dataSource}} は {{DataSource}} のインスタンスでなければなりません", - "308e1d484516a33df788f873e65faaff": "モデル `{0}` は非推奨の `DataModel を拡張しています。 代わりに `PersistedModel` を使用してください。", - "3438fab56cc7ab92dfd88f0497e523e0": "`{0}` 構成の関係プロパティーはオブジェクトでなければなりません", - "4cac5f051ae431321673e04045d37772": "モデル `{0}` は不明のモデル `{1}` を拡張しています。 ベースとして `PersistedModel` を使用します。", + "705c2d456a3e204c4af56e671ec3225c": "{{accessToken}} が見つかりませんでした", "734a7bebb65e10899935126ba63dd51f": "`{0}` 構成のオプション・プロパティーはオブジェクトでなければなりません", "779467f467862836e19f494a37d6ab77": "`{0}` 構成の ACL プロパティーはオブジェクトの配列でなければなりません", + "7d5e7ed0efaedf3f55f380caae0df8b8": "最初のモバイル・アプリケーション", + "7e0fca41d098607e1c9aa353c67e0fa1": "無効なアクセス・トークン", + "7e287fc885d9fdcf42da3a12f38572c1": "許可が必要です", + "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "ログアウトするには {{accessToken}} が必要です", "80a32e80cbed65eba2103201a7c94710": "モデルが見つかりません: {0}", "83cbdc2560ba9f09155ccfc63e08f1a1": "`{1}` のプロパティー `{0}` を再構成できません", + "855ecd4a64885ba272d782435f72a4d4": "\"{0}\" ID \"{1}\" が不明です。", + "860d1a0b8bd340411fb32baa72867989": "トランスポートでは HTTP リダイレクトはサポートされません。", + "895b1f941d026870b3cc8e6af087c197": "{{username}} または {{email}} が必要です", + "8a17c5ef611e2e7535792316e66b8fca": "パスワードが長すぎます: {0}", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "ホスト {0} への要求", + "8ae418c605b6a45f2651be9b1677c180": "無効なリモート・メソッド: `{0}`", + "8bab6720ecc58ec6412358c858a53484": "一括更新が失敗しました。コネクターは予期しない数のレコードを変更しました: {0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", "97795efe0c3eb7f35ce8cf8cfe70682b": "`{0}` の構成は {{`dataSource`}} プロパティーがありません。\nどのデータ・ソースにも付加されていないモデルにマークを付けるには `null` または `false` を使用します。", + "a40684f5a9f546115258b76938d1de37": "カラー・リストは {{http://localhost:3000/colors}} で利用できます", + "a50d10fc6e0959b220e085454c40381e": "ユーザーが見つかりません: {0}", "a80038252430df2754884bf3c845c4cf": "\"{0}.{1}\" のリモート・メタデータに「isStatic」フラグがありません。このメソッドはインスタンス・メソッドとして登録されます。", + "b6f740aeb6f2eb9bee9cb049dbfe6a28": "\"{0}\" {{key}} \"{1}\" が不明です。", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} は必須です", + "c2b5d51f007178170ca3952d59640ca4": "{0} の変更を修正できません:\n{1}", + "c68a93f0a9524fed4ff64372fc90c55f": "有効な E メールを指定する必要があります", + "cd0412f2f33a4a2a316acc834f3f21a6": "{{id}} または {{data}} を指定する必要があります", + "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} は削除されました。代わりに新規モジュール {{loopback-boot}} を使用してください", + "d9ef6dc3770dd8f80a129e92a79851f3": "{0} は非推奨です。詳しくは、{1} を参照してください。", "dc568bee32deb0f6eaf63e73b20e8ceb": "\"{0}\" の非オブジェクト「メソッド」設定を無視します。", - "3aecb24fa8bdd3f79d168761ca8a6729": "不明な {{middleware}} フェーズ {0}" + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" {{id}} \"{1}\" が不明です。", + "e92aa25b6b864e3454b65a7c422bd114": "一括更新が失敗しました。コネクターは予期しない数のレコードを削除しました: {0}", + "ea63d226b6968e328bdf6876010786b5": "一括更新を適用できません。コネクターは削除されたレコードの数を正しく報告していません。", + "ead044e2b4bce74b4357f8a03fb78ec4": "{0}.{1}() を呼び出せません。 {2} メソッドがセットアップされていません。 {{KeyValueModel}} は {{DataSource}} に正しく付加されていません。", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t 宛先:{0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t トランスポート:{0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "結果:{0}", + "f1d4ac54357cc0932f385d56814ba7e4": "競合", + "f58cdc481540cd1f69a4aa4da2e37981": "無効なパスワード: {0}" } diff --git a/intl/ko/messages.json b/intl/ko/messages.json index a1a057b7b..b027adee0 100644 --- a/intl/ko/messages.json +++ b/intl/ko/messages.json @@ -1,116 +1,76 @@ { - "3b46d3a780fd6ae5f95ade489a0efffe": "현재 컨텍스트가 브라우저에서 지원되지 않습니다. ", - "7e0fca41d098607e1c9aa353c67e0fa1": "올바르지 않은 액세스 토큰", - "320c482401afa1207c04343ab162e803": "올바르지 않은 프린시펄 유형: {0}", - "c2b5d51f007178170ca3952d59640ca4": "{0} 변경사항을 교정할 수 없음:\n{1}", - "5858e63efaa0e4ad86b61c0459ea32fa": "{{Email}} 모델을 {{Mail}} 커넥터에 연결해야 합니다. ", + "03f79fa268fe199de2ce4345515431c1": "ID가 {1}인 {0}에 대한 변경 레코드를 찾을 수 없음", + "04bd8af876f001ceaf443aad6a9002f9": "인증을 위해 {0} 모델이 정의되어야 함", + "0731d0109e46c21a4e34af3346ed4856": "이 동작은 다음 주요 버전에서 변경될 수 있습니다.", + "095afbf2f1f0e5be678f5dac5c54e717": "액세스 거부", "0caffe1d763c8cca6a61814abe33b776": "이메일은 필수입니다.", - "1b2a6076dccbe91a56f1672eb3b8598c": "응답 본문에 로그인 시 작성한 {{AccessToken}} 특성이 포함됩니다. \n`include` 매개변수 값에 따라 본문에 추가 특성이 포함될 수 있습니다. \n\n - `user` - `U+007BUserU+007D` - 현재 로그인된 사용자의 데이터. {{(`include=user`)}}\n\n", - "2362ba55796c733e337af169922327a2": "응답에 포함시킬 관련 오브젝트입니다. 자세한 정보는 리턴 값의 설명을 참조하십시오. ", - "306999d39387d87b2638199ff0bed8ad": "이메일을 사용하여 사용자 비밀번호를 재설정하십시오. ", - "3aae63bb7e8e046641767571c1591441": "이메일이 확인되지 않아서 로그인에 실패했습니다. ", - "3caaa84fc103d6d5612173ae6d43b245": "올바르지 않은 토큰: {0}", - "42e3fa18945255412ebc6561e2c6f1dc": "이메일 확인 토큰을 사용하여 사용자 등록을 확인하십시오. ", - "430b6326d7ebf6600a39f614ef516bc8": "이 인수를 제공하지 마십시오. 이는 요청 헤더에서 자동으로 추출됩니다. ", - "44a6c8b1ded4ed653d19ddeaaf89a606": "이메일을 찾을 수 없음", - "5e81ad3847a290dc650b47618b9cbc7e": "로그인 실패", - "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "사용자 이름/이메일 및 비밀번호를 가진 사용자로 로그인하십시오. ", - "8608c28f5e6df0008266e3c497836176": "액세스 토큰을 가진 사용자로 로그아웃하십시오. ", - "860d1a0b8bd340411fb32baa72867989": "전송에서 HTTP 경로 재지원을 지원하지 않습니다.", - "895b1f941d026870b3cc8e6af087c197": "{{username}} 또는 {{email}}은(는) 필수입니다.", - "a50d10fc6e0959b220e085454c40381e": "사용자를 찾을 수 없음: {0}", - "ba96498b10c179f9cd75f75c8def4f70": "{{realm}}은(는) 필수입니다.", - "c34fa20eea0091747fcc9eda204b8d37": "{{accessToken}}을(를) 찾을 수 없음", - "c68a93f0a9524fed4ff64372fc90c55f": "올바른 이메일을 제공해야 함", - "f58cdc481540cd1f69a4aa4da2e37981": "올바르지 않은 비밀번호: {0}", - "f0bd73df8714cefb925e3b8da2f4c5f6": "결과: {0}", "10e01c895dc0b2fecc385f9f462f1ca6": "색상 목록은 {{http://localhost:3000/colors}}에 있음", - "8a27e0c9ce3ebf0e0c3978efb456e13e": "호스트 {0}에 요청", - "a40684f5a9f546115258b76938d1de37": "색상 목록은 {{http://localhost:3000/colors}}에 있음", + "1b2a6076dccbe91a56f1672eb3b8598c": "응답 본문에 로그인 시 작성한 {{AccessToken}} 특성이 포함됩니다. \n`include` 매개변수 값에 따라 본문에 추가 특성이 포함될 수 있습니다. \n\n - `user` - `U+007BUserU+007D` - 현재 로그인된 사용자의 데이터. {{(`include=user`)}}\n\n", + "1d7833c3ca2f05fdad8fad7537531c40": "\t 제목:{0}", "1e85f822b547a75d7d385048030e4ecb": "작성 날짜: {0}", - "7d5e7ed0efaedf3f55f380caae0df8b8": "내 첫 번째 모바일 애플리케이션", - "04bd8af876f001ceaf443aad6a9002f9": "인증을 위해 {0} 모델이 정의되어야 함", - "095afbf2f1f0e5be678f5dac5c54e717": "액세스 거부", + "275f22ab95671f095640ca99194b7635": "\t 발신인:{0}", "2d3071e3b18681c80a090dc0efbdb349": "ID {1}(으)로 {0}을(를) 찾을 수 없음 ", + "2d78192c43fd2ec52ec18f3918894f9a": "{0} 미들웨어는 더 이상 사용되지 않습니다. 자세한 정보는 {1}을(를) 참조하십시오. ", + "308e1d484516a33df788f873e65faaff": "모델 `{0}`은(는) 더 이상 사용되지 않는 `DataModel`의 확장입니다. 대신 `PersistedModel`을 사용하십시오.", "316e5b82c203cf3de31a449ee07d0650": "예상 부울, 실제 {0}", - "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "데이터 소스 {0}을(를) 작성할 수 없음: {1}", - "7e287fc885d9fdcf42da3a12f38572c1": "권한 필수", - "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}}이(가) 제거되었습니다. 대신 새 모듈 {{loopback-boot}}을(를) 사용하십시오. ", - "1d7833c3ca2f05fdad8fad7537531c40": "\t 제목:{0}", - "275f22ab95671f095640ca99194b7635": "\t 발신인:{0}", + "320c482401afa1207c04343ab162e803": "올바르지 않은 프린시펄 유형: {0}", + "3438fab56cc7ab92dfd88f0497e523e0": "`{0}` 구성의 관계 특성은 오브젝트여야 함", + "35e5252c62d80f8c54a5290d30f4c7d0": "웹 브라우저에서 이 링크를 열어 이메일을 확인하십시오.\n\t{0}", + "3aae63bb7e8e046641767571c1591441": "이메일이 확인되지 않아서 로그인에 실패했습니다. ", + "3aecb24fa8bdd3f79d168761ca8a6729": "알 수 없는 {{middleware}} 단계 {0}", + "3caaa84fc103d6d5612173ae6d43b245": "올바르지 않은 토큰: {0}", + "3d617953470be16d0c2b32f0bcfbb5ee": "등록해 주셔서 감사합니다.", "3d63008ccfb2af1db2142e8cc2716ace": "경고: 이메일 발송을 위해 이메일 전송이 지정되지 않았습니다. 메일 메시지를 보내려면 전송을 설정하십시오. ", + "4203ab415ec66a78d3164345439ba76e": "{0}.{1}()을(를) 호출할 수 없습니다. {2} 메소드가 설정되지 않았습니다. {{PersistedModel}}이(가) {{DataSource}}에 재대로 첨부되지 않았습니다!", + "44a6c8b1ded4ed653d19ddeaaf89a606": "이메일을 찾을 수 없음", "4a4f04a4e480fc5d4ee73b84d9a4b904": "메일 발송 중:", - "63a091ced88001ab6acb58f61ec041c5": "\t 텍스트:{0}", - "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", - "ecb06666ef95e5db27a5ac1d6a17923b": "\t 수신인:{0}", - "f0aed00a3d3d0b97d6594e4b70e0c201": "\t 전송:{0}", - "0da38687fed24275c1547e815914a8e3": "{0}에 대해 ID별 관련 항목을 삭제하십시오.", - "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "모델 인스턴스에 일치하는 기준", - "22fe62fa8d595b72c62208beddaa2a56": "{0}에 대해 ID별 관련 항목을 업데이트하십시오.", - "528325f3cbf1b0ab9a08447515daac9a": "이 모델의 {0}을(를) 업데이트하십시오.", - "543d19bad5e47ee1e9eb8af688e857b4": "{0}의 외부 키.", - "598ff0255ffd1d1b71e8de55dbe2c034": "ID별 항목에 대해 {0} 관계가 있는지 확인하십시오.", - "5a36cc6ba0cc27c754f6c5ed6015ea3c": "ID별 항목에 대한 {0} 관계를 제거하십시오.", + "4b494de07f524703ac0879addbd64b13": "이메일이 확인되지 않았습니다.", + "4cac5f051ae431321673e04045d37772": "모델 `{0}`은(는) 알 수 없는 모델 `{1}`의 확장입니다. `PersistedModel`을 기본으로 사용하십시오.", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "데이터 소스 {0}을(를) 작성할 수 없음: {1}", + "5858e63efaa0e4ad86b61c0459ea32fa": "{{Email}} 모델을 {{Mail}} 커넥터에 연결해야 합니다. ", + "5e81ad3847a290dc650b47618b9cbc7e": "로그인 실패", "5fa3afb425819ebde958043e598cb664": "{{id}} {0}인 모델을 찾을 수 없음", "61e5deebaf44d68f4e6a508f30cc31a3": "모델 `{1}`에 대해 관계 `{0}`이(가) 없습니다. ", - "651f0b3cbba001635152ec3d3d954d0a": "{0}에 대해 ID별 관련 항목을 찾으십시오.", - "7bc7b301ad9c4fc873029d57fb9740fe": "{0} / {1} 조회.", - "7c837b88fd0e509bd3fc722d7ddf0711": "{0}의 외부 키", - "830cb6c862f8f364e9064cea0026f701": "페치에 하나의 관계 {0}이(가) 있습니다.", - "855ecd4a64885ba272d782435f72a4d4": "알 수 없는 \"{0}\" ID \"{1}\".", - "86254879d01a60826a851066987703f2": "{0}에 대해 ID별 관련 항목을 추가하십시오. ", - "8ae418c605b6a45f2651be9b1677c180": "올바르지 않은 원격 메소드: `{0}`", - "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "페치가 관계 {0}에 속합니다.", - "c0057a569ff9d3b509bac61a4b2f605d": "이 모델의 모든 {0}을(를) 삭제하십시오.", - "cd0412f2f33a4a2a316acc834f3f21a6": "{{id}} 또는 {{data}}을(를) 지정해야 함", - "d6f43b266533b04d442bdb3955622592": "이 모델의 {0}에서 새 인스턴스를 작성합니다. ", - "da13d3cdf21330557254670dddd8c5c7": "{0} / {1} 계수.", - "e4434de4bb8f5a3cd1d416e4d80d7e0b": "알 수 없는 \"{0}\" {{id}} \"{1}\".", - "f66ae3cf379b2fce28575a3282defe1a": "이 모델의 {0}을(를) 삭제합니다.", - "03f79fa268fe199de2ce4345515431c1": "ID가 {1}인 {0}에 대한 변경 레코드를 찾을 수 없음", - "0f1c71f74b040bfbe8d384a414e31f03": "주어진 체크포인트 이후의 델타와 충돌 세트를 확보하십시오.", - "15254dec061d023d6c030083a0cef50f": "모델의 새 인스턴스를 작성하고 이를 데이터 소스로 지속시킵니다.", - "16a11368d55b85a209fc6aea69ee5f7a": "일치하는 모든 레코드를 삭제하십시오.", - "1bc1d489ddf347af47af3d9b1fc7cc15": "여러 업데이트를 한 번에 실행하십시오. 참고: 이는 Atomic 업데이트가 아닙니다. ", - "1bc7d8283c9abda512692925c8d8e3c0": "현재 체크포인트를 확보하십시오.", - "1caa7cc61266e7aef7db7d2f0e27ac3e": "이 인스턴스에 대해 보존된 최근의 변경 레코드 특성을 업데이트하십시오.", - "2a7df74fe6e8462e617b79d5fbb536ea": "이 인스턴스에 대한 최근의 변경 레코드를 확보하십시오.", - "2a9684b3d5b3b67af74bac74eb1b0843": "데이터 소스에서 필터링하여 일치하는 모든 모델 인스턴스를 찾으십시오.", - "2e50838caf0c927735eb15d12866bdd7": "주어진 체크포인트 이후에 모델에 대한 변경사항을 확보하십시오. 리턴되는 결과 수를 줄이려면 필터 오브젝트를 제공하십시오.", - "4203ab415ec66a78d3164345439ba76e": "{0}.{1}()을(를) 호출할 수 없습니다. {2} 메소드가 설정되지 않았습니다. {{PersistedModel}}이(가) {{DataSource}}에 재대로 첨부되지 않았습니다!", - "51ea9b6245bb5e672b236d640ca3b048": "변경 특성 이름/값 쌍의 오브젝트", - "55ddedd4c501348f82cb89db02ec85c1": "모델 특성 이름/값 쌍의 오브젝트", - "5aaa76c72ae1689fd3cf62589784a4ba": "모델 인스턴스의 속성을 업데이트하고 이를 데이터 소스로 지속시킵니다.", - "5f659bbc15e6e2b249fa33b3879b5f69": "데이터 소스에서 {{id}}(으)로 모델 인스턴스를 찾으십시오.", "62e8b0a733417978bab22c8dacf5d7e6": "벌크 업데이트를 적용할 수 없습니다. 커넥터가 업데이트된 레코드 수를 제대로 보고하지 않습니다. ", - "6329e0ac1de3c250ebb1df5afd5a2a7b": "업데이트된 인스턴스 수", + "63a091ced88001ab6acb58f61ec041c5": "\t 텍스트:{0}", "6bc376432cd9972cf991aad3de371e78": "변경을 위한 데이터 누락: {0}", - "79295ac04822d2e9702f0dd1d0240336": "데이터 소스에서 {{where}}에 일치하는 모델 인스턴스를 업데이트하십시오. ", - "7f2fde7f0f860ead224b11ba8d75aa1c": "델타 목록에서 업데이트 목록을 작성하십시오. ", - "89b57e764c2267129294b07589dbfdc2": "데이터 소스에서 {{id}}(으)로 모델 인스턴스를 삭제하십시오.", - "8bab6720ecc58ec6412358c858a53484": "벌크 업데이트에 실패했습니다. 커넥터가 예상치 못한 수의 레코드를 수정했습니다. {0}", - "a98b6cc6547706b5c6bffde0ed5fd55c": "데이터 소스에서 필터링하여 일치하는 첫 번째 모델 인스턴스를 찾으십시오.", - "c46d4aba1f14809c16730faa46933495": "정의 필드를 필터링하여 포함", - "c65600640f206f585d300b4bcb699d95": "체크포인트를 작성하십시오. ", - "cf64c7afc74d3a8120abcd028f98c770": "기존 모델 인스턴스를 업데이트하거나 새 인스턴스를 데이터 소스에 삽입하십시오.", - "dcb6261868ff0a7b928aa215b07d068c": "변경 스트림을 작성하십시오.", - "e43e320a435ec1fa07648c1da0d558a7": "모델 인스턴스가 데이터 소스에 있는지 확인하십시오.", - "e92aa25b6b864e3454b65a7c422bd114": "벌크 업데이트에 실패했습니다. 커넥터가 예상치 못한 수의 레코드를 삭제했습니다. {0}", - "ea63d226b6968e328bdf6876010786b5": "벌크 업데이트를 적용할 수 없습니다. 커넥터가 삭제된 레코드 수를 제대로 보고하지 않습니다. ", - "f1d4ac54357cc0932f385d56814ba7e4": "충돌", - "f37d94653793e33f4203e45b4a1cf106": "데이터 소스에서 where에 일치하는 모델 인스턴스 수를 세십시오.", - "0731d0109e46c21a4e34af3346ed4856": "이 동작은 다음 주요 버전에서 변경될 수 있습니다.", - "2e110abee2c95bcfc2dafd48be7e2095": "{0}을(를) 구성할 수 없음: {{config.dataSource}}이(가) {{DataSource}}의 인스턴스여야 함", - "308e1d484516a33df788f873e65faaff": "모델 `{0}`은(는) 더 이상 사용되지 않는 `DataModel`의 확장입니다. 대신 `PersistedModel`을 사용하십시오.", - "3438fab56cc7ab92dfd88f0497e523e0": "`{0}` 구성의 관계 특성은 오브젝트여야 함", - "4cac5f051ae431321673e04045d37772": "모델 `{0}`은(는) 알 수 없는 모델 `{1}`의 확장입니다. `PersistedModel`을 기본으로 사용하십시오.", + "705c2d456a3e204c4af56e671ec3225c": "{{accessToken}}을(를) 찾을 수 없음", "734a7bebb65e10899935126ba63dd51f": "`{0}` 구성의 옵션 특성은 오브젝트여야 함", "779467f467862836e19f494a37d6ab77": "`{0}` 구성의 acls 특성은 오브젝트 배열이어야 함", + "7d5e7ed0efaedf3f55f380caae0df8b8": "내 첫 번째 모바일 애플리케이션", + "7e0fca41d098607e1c9aa353c67e0fa1": "올바르지 않은 액세스 토큰", + "7e287fc885d9fdcf42da3a12f38572c1": "권한 필수", + "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}}이(가) 로그아웃해야 함", "80a32e80cbed65eba2103201a7c94710": "모델을 찾을 수 없음: {0}", "83cbdc2560ba9f09155ccfc63e08f1a1": "`{1}`에 대해 `{0}` 특성을 다시 구성할 수 없음", + "855ecd4a64885ba272d782435f72a4d4": "알 수 없는 \"{0}\" ID \"{1}\".", + "860d1a0b8bd340411fb32baa72867989": "전송에서 HTTP 경로 재지원을 지원하지 않습니다.", + "895b1f941d026870b3cc8e6af087c197": "{{username}} 또는 {{email}}은(는) 필수입니다.", + "8a17c5ef611e2e7535792316e66b8fca": "비밀번호가 너무 김: {0}", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "호스트 {0}에 요청", + "8ae418c605b6a45f2651be9b1677c180": "올바르지 않은 원격 메소드: `{0}`", + "8bab6720ecc58ec6412358c858a53484": "벌크 업데이트에 실패했습니다. 커넥터가 예상치 못한 수의 레코드를 수정했습니다. {0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", "97795efe0c3eb7f35ce8cf8cfe70682b": "`{0}`의 구성에 {{`dataSource`}} 특성이 누락되었습니다.\n데이터 소스에 첨부되지 않은 모델을 표시하려면 `null` 또는 `false`를 사용하십시오.", + "a40684f5a9f546115258b76938d1de37": "색상 목록은 {{http://localhost:3000/colors}}에 있음", + "a50d10fc6e0959b220e085454c40381e": "사용자를 찾을 수 없음: {0}", "a80038252430df2754884bf3c845c4cf": "\"{0}.{1}\"에 대한 원격 메타데이터에 \"isStatic\" 플래그가 누락되었습니다. 이 메소드는 인스턴스 메소드로 등록되어 있습니다.", + "b6f740aeb6f2eb9bee9cb049dbfe6a28": "알 수 없는 \"{0}\" {{key}} \"{1}\".", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}}은(는) 필수입니다.", + "c2b5d51f007178170ca3952d59640ca4": "{0} 변경사항을 교정할 수 없음:\n{1}", + "c68a93f0a9524fed4ff64372fc90c55f": "올바른 이메일을 제공해야 함", + "cd0412f2f33a4a2a316acc834f3f21a6": "{{id}} 또는 {{data}}을(를) 지정해야 함", + "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}}이(가) 제거되었습니다. 대신 새 모듈 {{loopback-boot}}을(를) 사용하십시오. ", + "d9ef6dc3770dd8f80a129e92a79851f3": "{0}은(는) 더 이상 사용되지 않습니다. 자세한 정보는 {1}을(를) 참조하십시오. ", "dc568bee32deb0f6eaf63e73b20e8ceb": "\"{0}\"의 비오브젝트 \"methods\" 설정 무시", - "3aecb24fa8bdd3f79d168761ca8a6729": "알 수 없는 {{middleware}} 단계 {0}" + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "알 수 없는 \"{0}\" {{id}} \"{1}\".", + "e92aa25b6b864e3454b65a7c422bd114": "벌크 업데이트에 실패했습니다. 커넥터가 예상치 못한 수의 레코드를 삭제했습니다. {0}", + "ea63d226b6968e328bdf6876010786b5": "벌크 업데이트를 적용할 수 없습니다. 커넥터가 삭제된 레코드 수를 제대로 보고하지 않습니다. ", + "ead044e2b4bce74b4357f8a03fb78ec4": "{0}.{1}()을(를) 호출할 수 없습니다. {2} 메소드가 설정되지 않았습니다. {{KeyValueModel}}이(가) {{DataSource}}에 재대로 첨부되지 않았습니다!", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t 수신인:{0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t 전송:{0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "결과: {0}", + "f1d4ac54357cc0932f385d56814ba7e4": "충돌", + "f58cdc481540cd1f69a4aa4da2e37981": "올바르지 않은 비밀번호: {0}" } diff --git a/intl/nl/messages.json b/intl/nl/messages.json index b5fdd6db4..080ed46e1 100644 --- a/intl/nl/messages.json +++ b/intl/nl/messages.json @@ -1,116 +1,76 @@ { - "3b46d3a780fd6ae5f95ade489a0efffe": "Huidige context wordt niet ondersteund in de browser.", - "7e0fca41d098607e1c9aa353c67e0fa1": "Ongeldig toegangstoken", - "320c482401afa1207c04343ab162e803": "Ongeldig type principal: {0}", - "c2b5d51f007178170ca3952d59640ca4": "Wijzigingen van {0} kunnen niet worden hersteld:\n{1}", - "5858e63efaa0e4ad86b61c0459ea32fa": "U moet verbinding maken tussen het model {{Email}} en een {{Mail}}-connector", + "03f79fa268fe199de2ce4345515431c1": "Geen wijzigingsrecord gevonden voor {0} met ID {1}", + "04bd8af876f001ceaf443aad6a9002f9": "Voor verificatie moet model {0} worden gedefinieerd.", + "0731d0109e46c21a4e34af3346ed4856": "Dit gedrag kan gewijzigd worden in de volgende hoofdversie.", + "095afbf2f1f0e5be678f5dac5c54e717": "Toegang geweigerd", "0caffe1d763c8cca6a61814abe33b776": "E-mail is vereist", - "1b2a6076dccbe91a56f1672eb3b8598c": "De lopende tekst van het antwoord bevat eigenschappen van het {{AccessToken}} dat is gemaakt bij aanmelding.\nAfhankelijk van de waarde van de parameter 'include' kan de lopende tekst aanvullende eigenschappen bevatten:\n\n - 'user' - 'U+007BUserU+007D' - Gegevens van de aangemelde gebruiker. {{(`include=user`)}}\n\n", - "2362ba55796c733e337af169922327a2": "Gerelateerde objecten die moeten worden opgenomen in de respons. Zie de beschrijving van de retourwaarde voor meer informatie.", - "306999d39387d87b2638199ff0bed8ad": "Wachtwoord resetten voor een gebruiker met e-mail.", - "3aae63bb7e8e046641767571c1591441": "Aanmelding mislukt omdat e-mail niet is gecontroleerd", - "3caaa84fc103d6d5612173ae6d43b245": "Ongeldig token: {0}", - "42e3fa18945255412ebc6561e2c6f1dc": "Bevestig een gebruikersregistratie met e-mailverificatietoken.", - "430b6326d7ebf6600a39f614ef516bc8": "Geef dit argument niet op, het wordt automatisch geëxtraheerd uit opdrachtheaders.", - "44a6c8b1ded4ed653d19ddeaaf89a606": "E-mail is niet gevonden", - "5e81ad3847a290dc650b47618b9cbc7e": "Aanmelden is mislukt", - "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "Meld u aan als gebruiker met gebruikersnaam/e-mailadres en wachtwoord.", - "8608c28f5e6df0008266e3c497836176": "Gebruiker afmelden met toegangstoken.", - "860d1a0b8bd340411fb32baa72867989": "Transport biedt geen ondersteuning voor HTTP-omleidingen.", - "895b1f941d026870b3cc8e6af087c197": "{{username}} of {{email}} is verplicht", - "a50d10fc6e0959b220e085454c40381e": "Gebruiker is niet gevonden: {0}", - "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} is verplicht", - "c34fa20eea0091747fcc9eda204b8d37": "{{accessToken}} is niet gevonden", - "c68a93f0a9524fed4ff64372fc90c55f": "U moet een geldig e-mailadres opgeven", - "f58cdc481540cd1f69a4aa4da2e37981": "Ongeldige wachtwoord: {0}", - "f0bd73df8714cefb925e3b8da2f4c5f6": "resultaat:{0}", "10e01c895dc0b2fecc385f9f462f1ca6": "een lijst van kleuren is beschikbaar op {{http://localhost:3000/colors}}", - "8a27e0c9ce3ebf0e0c3978efb456e13e": "Aanvraag voor host {0}", - "a40684f5a9f546115258b76938d1de37": "Een lijst van kleuren is beschikbaar op {{http://localhost:3000/colors}}", + "1b2a6076dccbe91a56f1672eb3b8598c": "De lopende tekst van het antwoord bevat eigenschappen van het {{AccessToken}} dat is gemaakt bij aanmelding.\nAfhankelijk van de waarde van de parameter 'include' kan de lopende tekst aanvullende eigenschappen bevatten:\n\n - 'user' - 'U+007BUserU+007D' - Gegevens van de aangemelde gebruiker. {{(`include=user`)}}\n\n", + "1d7833c3ca2f05fdad8fad7537531c40": "\t ONDERWERP: {0}", "1e85f822b547a75d7d385048030e4ecb": "Gemaakt: {0}", - "7d5e7ed0efaedf3f55f380caae0df8b8": "Mijn eerste mobiele toepassing", - "04bd8af876f001ceaf443aad6a9002f9": "Voor verificatie moet model {0} worden gedefinieerd.", - "095afbf2f1f0e5be678f5dac5c54e717": "Toegang geweigerd", + "275f22ab95671f095640ca99194b7635": "\t VAN: {0}", "2d3071e3b18681c80a090dc0efbdb349": "kan {0} met ID {1} niet vinden", + "2d78192c43fd2ec52ec18f3918894f9a": "{0}-middleware is gedeprecieerd. Zie {1} voor meer informatie.", + "308e1d484516a33df788f873e65faaff": "Model '{0}' is een uitbreiding van het gedeprecieerde 'DataModel'. Gebruik in plaats daarvan 'PersistedModel'.", "316e5b82c203cf3de31a449ee07d0650": "Booleaanse waarde verwacht, {0} ontvangen", - "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Gegevensbron {0}: {1} kan n iet worden gemaakt", - "7e287fc885d9fdcf42da3a12f38572c1": "Verplichte verificatie", - "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} is verwijderd; gebruik in plaats daarvan de nieuwe module {{loopback-boot}}", - "1d7833c3ca2f05fdad8fad7537531c40": "\t ONDERWERP: {0}", - "275f22ab95671f095640ca99194b7635": "\t VAN: {0}", + "320c482401afa1207c04343ab162e803": "Ongeldig type principal: {0}", + "3438fab56cc7ab92dfd88f0497e523e0": "De relaties-eigenschap van de '{0}'-configuratie moet een object zijn", + "35e5252c62d80f8c54a5290d30f4c7d0": "Controleer uw e-mail door deze link te openen in een webbrowser:\n\t{0}", + "3aae63bb7e8e046641767571c1591441": "Aanmelding mislukt omdat e-mail niet is gecontroleerd", + "3aecb24fa8bdd3f79d168761ca8a6729": "Onbekende {{middleware}}-fase {0}", + "3caaa84fc103d6d5612173ae6d43b245": "Ongeldig token: {0}", + "3d617953470be16d0c2b32f0bcfbb5ee": "Hartelijk dank voor uw registratie", "3d63008ccfb2af1db2142e8cc2716ace": "Waarschuwing: Geen e-mailtransport opgegeven voor verzending van e-mail. Configureer een transport om e-mailberichten te verzenden.", + "4203ab415ec66a78d3164345439ba76e": "{0} kan niet worden aangeroepen. {1}(). De methode {2} is niet geconfigureerd. De {{PersistedModel}} is niet correct gekoppeld aan een {{DataSource}}!", + "44a6c8b1ded4ed653d19ddeaaf89a606": "E-mail is niet gevonden", "4a4f04a4e480fc5d4ee73b84d9a4b904": "Mail verzenden:", - "63a091ced88001ab6acb58f61ec041c5": "\t TEKST: {0}", - "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML: {0}", - "ecb06666ef95e5db27a5ac1d6a17923b": "\t AAN: {0}", - "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORT: {0}", - "0da38687fed24275c1547e815914a8e3": "Gerelateerd item wissen op basis van ID voor {0}.", - "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "Criteria om te voldoen aan modelinstances", - "22fe62fa8d595b72c62208beddaa2a56": "Gerelateerd item bijwerken op basis van ID voor {0}.", - "528325f3cbf1b0ab9a08447515daac9a": "{0} van dit model bijwerken.", - "543d19bad5e47ee1e9eb8af688e857b4": "Externe sleutel voor {0}.", - "598ff0255ffd1d1b71e8de55dbe2c034": "Bestaan van {0}-relatie met item controleren op basis van ID.", - "5a36cc6ba0cc27c754f6c5ed6015ea3c": "Verwijder de {0}-relatie met een item op basis van ID.", + "4b494de07f524703ac0879addbd64b13": "E-mail is niet geverifieerd", + "4cac5f051ae431321673e04045d37772": "Model '{0}' is een uitbreiding van onbekend model '{1}'. 'PersistedModel' wordt gebruikt als basis.", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Gegevensbron {0}: {1} kan n iet worden gemaakt", + "5858e63efaa0e4ad86b61c0459ea32fa": "U moet verbinding maken tussen het model {{Email}} en een {{Mail}}-connector", + "5e81ad3847a290dc650b47618b9cbc7e": "Aanmelden is mislukt", "5fa3afb425819ebde958043e598cb664": "geen model gevonden met {{id}} {0}", "61e5deebaf44d68f4e6a508f30cc31a3": "Relatie '{0}' voor model '{1}' bestaat niet", - "651f0b3cbba001635152ec3d3d954d0a": "Gerelateerd item zoeken op basis van ID voor {0}.", - "7bc7b301ad9c4fc873029d57fb9740fe": "Query's {0} van {1}.", - "7c837b88fd0e509bd3fc722d7ddf0711": "Externe sleutel voor {0}", - "830cb6c862f8f364e9064cea0026f701": "Haalt hasOne-relatie {0} op.", - "855ecd4a64885ba272d782435f72a4d4": "Onbekend \"{0}\"-ID \"{1}\".", - "86254879d01a60826a851066987703f2": "Gerelateerd item toevoegen op basis van ID voor {0}.", - "8ae418c605b6a45f2651be9b1677c180": "Ongeldige niet-lokale methode: '{0}'", - "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Haalt belongsTo-relatie {0} op.", - "c0057a569ff9d3b509bac61a4b2f605d": "Verwijdert alle {0} van dit model.", - "cd0412f2f33a4a2a316acc834f3f21a6": "U moet een {{id}} of {{data}} opgeven", - "d6f43b266533b04d442bdb3955622592": "Maakt een nieuwe instance in {0} van dit model.", - "da13d3cdf21330557254670dddd8c5c7": "Aantal {0} van {1}.", - "e4434de4bb8f5a3cd1d416e4d80d7e0b": "Onbekend \"{0}\" {{id}} \"{1}\".", - "f66ae3cf379b2fce28575a3282defe1a": "Verwijdert {0} van dit model.", - "03f79fa268fe199de2ce4345515431c1": "Geen wijzigingsrecord gevonden voor {0} met ID {1}", - "0f1c71f74b040bfbe8d384a414e31f03": "Een set deltawaarden en conflicten ophalen sinds het opgegeven checkpoint.", - "15254dec061d023d6c030083a0cef50f": "Maak een nieuwe instance van het model en bewaar dit in de gegevensbron.", - "16a11368d55b85a209fc6aea69ee5f7a": "Alle overeenkomende records wissen.", - "1bc1d489ddf347af47af3d9b1fc7cc15": "Meerdere updates tegelijk uitvoeren. Opmerking: dit is niet atomisch (atomic).", - "1bc7d8283c9abda512692925c8d8e3c0": "Het huidige checkpoint ophalen.", - "1caa7cc61266e7aef7db7d2f0e27ac3e": "Werk de eigenschappen van het meest recente wijzigingsrecord voor deze instance bij.", - "2a7df74fe6e8462e617b79d5fbb536ea": "Het meest recente wijzigingsrecord voor deze instance ophalen.", - "2a9684b3d5b3b67af74bac74eb1b0843": "Zoeken naar alle instances van model dat overeenkomt met filter uit gegevensbron.", - "2e50838caf0c927735eb15d12866bdd7": "Wijzigingen die zijn aangebracht op een model sinds een bepaald checkpoint ophalen. Geef een filterobject op om het aantal resultaten te beperken.", - "4203ab415ec66a78d3164345439ba76e": "{0} kan niet worden aangeroepen. {1}(). De methode {2} is niet geconfigureerd. De {{PersistedModel}} is niet correct gekoppeld aan een {{DataSource}}!", - "51ea9b6245bb5e672b236d640ca3b048": "Een object van Combinaties van eigenschapnaam/waarde wijzigen", - "55ddedd4c501348f82cb89db02ec85c1": "Een object van combinaties van eigenschapnaam/waarde", - "5aaa76c72ae1689fd3cf62589784a4ba": "Kenmerken voor modelinstance bijwerken en bewaren in gegevensbron.", - "5f659bbc15e6e2b249fa33b3879b5f69": "Zoeken naar modelinstance op basis van {{id}} uit gegevensbron.", "62e8b0a733417978bab22c8dacf5d7e6": "Bulkupdates kunnen niet worden toegepast, de connector meldt niet het juiste aantal bijgewerkte records.", - "6329e0ac1de3c250ebb1df5afd5a2a7b": "Het aantal bijgewerkte instances", + "63a091ced88001ab6acb58f61ec041c5": "\t TEKST: {0}", "6bc376432cd9972cf991aad3de371e78": "Ontbrekende gegevens voor wijziging: {0}", - "79295ac04822d2e9702f0dd1d0240336": "Instances van model dat overeenkomt met {{where}} uit gegevensbron bijwerken.", - "7f2fde7f0f860ead224b11ba8d75aa1c": "Updatelijst maken op basis van deltalijst.", - "89b57e764c2267129294b07589dbfdc2": "Modelinstance op basis van {{id}} verwijderen uit gegevensbron.", - "8bab6720ecc58ec6412358c858a53484": "Bulkupdate is mislukt; connector heeft een onverwacht aantal records gewijzigd: {0}", - "a98b6cc6547706b5c6bffde0ed5fd55c": "Zoeken naar eerste instance van model dat overeenkomt met filter uit gegevensbron.", - "c46d4aba1f14809c16730faa46933495": "Gedefinieerde velden filteren en opnemen", - "c65600640f206f585d300b4bcb699d95": "Maak een checkpoint.", - "cf64c7afc74d3a8120abcd028f98c770": "Bestaande modelinstance bijwerken of nieuwe instance toevoegen aan gegevensbron.", - "dcb6261868ff0a7b928aa215b07d068c": "Maak een wijzigingsstroom.", - "e43e320a435ec1fa07648c1da0d558a7": "Controleer of er een modelinstance voorkomt in de gegevensbron.", - "e92aa25b6b864e3454b65a7c422bd114": "Bulkupdate is mislukt; connector heeft een onverwacht aantal records gewist: {0}", - "ea63d226b6968e328bdf6876010786b5": "Bulkupdates kunnen niet worden toegepast, de connector meldt niet het juiste aantal gewiste records.", - "f1d4ac54357cc0932f385d56814ba7e4": "Conflict", - "f37d94653793e33f4203e45b4a1cf106": "Instances van model dat overeenkomt met WHERE-clausule uit gegevensbron tellen.", - "0731d0109e46c21a4e34af3346ed4856": "Dit gedrag kan gewijzigd worden in de volgende hoofdversie.", - "2e110abee2c95bcfc2dafd48be7e2095": "Kan {0} niet configureren: {{config.dataSource}} moet een instance van {{DataSource}} zijn", - "308e1d484516a33df788f873e65faaff": "Model '{0}' is een uitbreiding van het gedeprecieerde 'DataModel'. Gebruik in plaats daarvan 'PersistedModel'.", - "3438fab56cc7ab92dfd88f0497e523e0": "De relaties-eigenschap van de '{0}'-configuratie moet een object zijn", - "4cac5f051ae431321673e04045d37772": "Model '{0}' is een uitbreiding van onbekend model '{1}'. 'PersistedModel' wordt gebruikt als basis.", + "705c2d456a3e204c4af56e671ec3225c": "{{accessToken}} is niet gevonden", "734a7bebb65e10899935126ba63dd51f": "De opties-eigenschap van de '{0}'-configuratie moet een object zijn", "779467f467862836e19f494a37d6ab77": "De acls-eigenschap van de '{0}'-configuratie moet een array objecten zijn", + "7d5e7ed0efaedf3f55f380caae0df8b8": "Mijn eerste mobiele toepassing", + "7e0fca41d098607e1c9aa353c67e0fa1": "Ongeldig toegangstoken", + "7e287fc885d9fdcf42da3a12f38572c1": "Verplichte verificatie", + "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} is vereist voor afmelding", "80a32e80cbed65eba2103201a7c94710": "Model is niet gevonden: {0}", "83cbdc2560ba9f09155ccfc63e08f1a1": "Eigenschap '{0}' mag niet opnieuw worden geconfigureerd voor '{1}'", + "855ecd4a64885ba272d782435f72a4d4": "Onbekend \"{0}\"-ID \"{1}\".", + "860d1a0b8bd340411fb32baa72867989": "Transport biedt geen ondersteuning voor HTTP-omleidingen.", + "895b1f941d026870b3cc8e6af087c197": "{{username}} of {{email}} is verplicht", + "8a17c5ef611e2e7535792316e66b8fca": "Wachtwoord is te lang: {0}", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "Aanvraag voor host {0}", + "8ae418c605b6a45f2651be9b1677c180": "Ongeldige niet-lokale methode: '{0}'", + "8bab6720ecc58ec6412358c858a53484": "Bulkupdate is mislukt; connector heeft een onverwacht aantal records gewijzigd: {0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML: {0}", "97795efe0c3eb7f35ce8cf8cfe70682b": "De eigenschap {{`dataSource`}} ontbreekt in de configuratie van '{0}'.\nGebruik 'null' of 'false' om modellen te markeren die niet gekoppeld zijn aan een gegevensbron.", + "a40684f5a9f546115258b76938d1de37": "Een lijst van kleuren is beschikbaar op {{http://localhost:3000/colors}}", + "a50d10fc6e0959b220e085454c40381e": "Gebruiker is niet gevonden: {0}", "a80038252430df2754884bf3c845c4cf": "Vlag \"isStatic\" ontbreekt in remoting (externe) metagegevens voor \"{0}.{1}\"; de methode wordt geregistreerd als instancemethode.", + "b6f740aeb6f2eb9bee9cb049dbfe6a28": "Onbekend \"{0}\" {{key}} \"{1}\".", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} is verplicht", + "c2b5d51f007178170ca3952d59640ca4": "Wijzigingen van {0} kunnen niet worden hersteld:\n{1}", + "c68a93f0a9524fed4ff64372fc90c55f": "U moet een geldig e-mailadres opgeven", + "cd0412f2f33a4a2a316acc834f3f21a6": "U moet een {{id}} of {{data}} opgeven", + "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} is verwijderd; gebruik in plaats daarvan de nieuwe module {{loopback-boot}}", + "d9ef6dc3770dd8f80a129e92a79851f3": "{0} is gedeprecieerd. Zie {1} voor meer informatie.", "dc568bee32deb0f6eaf63e73b20e8ceb": "Niet-object \"methods\"-instelling \"{0}\" wordt genegeerd.", - "3aecb24fa8bdd3f79d168761ca8a6729": "Onbekende {{middleware}}-fase {0}" + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "Onbekend \"{0}\" {{id}} \"{1}\".", + "e92aa25b6b864e3454b65a7c422bd114": "Bulkupdate is mislukt; connector heeft een onverwacht aantal records gewist: {0}", + "ea63d226b6968e328bdf6876010786b5": "Bulkupdates kunnen niet worden toegepast, de connector meldt niet het juiste aantal gewiste records.", + "ead044e2b4bce74b4357f8a03fb78ec4": "{0} kan niet worden aangeroepen. {1}(). De methode {2} is niet geconfigureerd. De {{KeyValueModel}} is niet correct gekoppeld aan een {{DataSource}}!", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t AAN: {0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORT: {0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "resultaat:{0}", + "f1d4ac54357cc0932f385d56814ba7e4": "Conflict", + "f58cdc481540cd1f69a4aa4da2e37981": "Ongeldige wachtwoord: {0}" } diff --git a/intl/pt/messages.json b/intl/pt/messages.json index d79399005..d5698b4e6 100644 --- a/intl/pt/messages.json +++ b/intl/pt/messages.json @@ -1,116 +1,76 @@ { - "3b46d3a780fd6ae5f95ade489a0efffe": "Contexto atual não é suportado no navegador.", - "7e0fca41d098607e1c9aa353c67e0fa1": "Token de Acesso Inválido", - "320c482401afa1207c04343ab162e803": "Tipo principal inválido: {0}", - "c2b5d51f007178170ca3952d59640ca4": "Não é possível retificar mudanças de {0}:\n{1}", - "5858e63efaa0e4ad86b61c0459ea32fa": "Deve-se conectar o Modelo de {{Email}} em um conector de {{Mail}}", + "03f79fa268fe199de2ce4345515431c1": "Nenhum registro de mudança localizado para {0} com o ID {1}", + "04bd8af876f001ceaf443aad6a9002f9": "Autenticação requer que modelo {0} seja definido.", + "0731d0109e46c21a4e34af3346ed4856": "Este comportamento pode mudar na próxima versão principal.", + "095afbf2f1f0e5be678f5dac5c54e717": "Acesso Negado", "0caffe1d763c8cca6a61814abe33b776": "E-mail é necessário", - "1b2a6076dccbe91a56f1672eb3b8598c": "O corpo de resposta contém propriedades do {{AccessToken}} criado no login.\nDependendo do valor do parâmetro `include`, o corpo poderá conter propriedades adicionais:\n\n - `user` - `U+007BUserU+007D` - Dados do usuário com login efetuado atualmente. {{(`include=user`)}}\n\n", - "2362ba55796c733e337af169922327a2": "Objetos relacionados a serem incluídos na resposta. Consulte a descrição do valor de retorno para obter mais detalhes.", - "306999d39387d87b2638199ff0bed8ad": "Reconfiguração de senha para um usuário com e-mail.", - "3aae63bb7e8e046641767571c1591441": "login com falha pois o e-mail não foi verificado", - "3caaa84fc103d6d5612173ae6d43b245": "Token inválido: {0}", - "42e3fa18945255412ebc6561e2c6f1dc": "Confirme um registro do usuário com o token de verificação de e-mail.", - "430b6326d7ebf6600a39f614ef516bc8": "Não forneça este argumento, ele será extraído automaticamente dos cabeçalhos da solicitação.", - "44a6c8b1ded4ed653d19ddeaaf89a606": "E-mail não encontrado", - "5e81ad3847a290dc650b47618b9cbc7e": "falha de login", - "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "Efetue login de um usuário com nome do usuário/e-mail e senha.", - "8608c28f5e6df0008266e3c497836176": "Efetue logout de um usuário com token de acesso.", - "860d1a0b8bd340411fb32baa72867989": "O transporte não suporta redirecionamentos de HTTP.", - "895b1f941d026870b3cc8e6af087c197": "{{username}} ou {{email}} é necessário", - "a50d10fc6e0959b220e085454c40381e": "Usuário não localizado: {0}", - "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} é obrigatório", - "c34fa20eea0091747fcc9eda204b8d37": "não foi possível localizar {{accessToken}}", - "c68a93f0a9524fed4ff64372fc90c55f": "Deve-se fornecer um e-mail válido", - "f58cdc481540cd1f69a4aa4da2e37981": "Senha inválida: {0}", - "f0bd73df8714cefb925e3b8da2f4c5f6": "resultado:{0}", "10e01c895dc0b2fecc385f9f462f1ca6": "uma lista de cores está disponível em {{http://localhost:3000/colors}}", - "8a27e0c9ce3ebf0e0c3978efb456e13e": "Solicitação para o host {0}", - "a40684f5a9f546115258b76938d1de37": "Uma lista de cores está disponível em {{http://localhost:3000/colors}}", + "1b2a6076dccbe91a56f1672eb3b8598c": "O corpo de resposta contém propriedades do {{AccessToken}} criado no login.\nDependendo do valor do parâmetro `include`, o corpo poderá conter propriedades adicionais:\n\n - `user` - `U+007BUserU+007D` - Dados do usuário com login efetuado atualmente. {{(`include=user`)}}\n\n", + "1d7833c3ca2f05fdad8fad7537531c40": "\t ASSUNTO:{0}", "1e85f822b547a75d7d385048030e4ecb": "Criado: {0}", - "7d5e7ed0efaedf3f55f380caae0df8b8": "Meu primeiro aplicativo móvel", - "04bd8af876f001ceaf443aad6a9002f9": "Autenticação requer que modelo {0} seja definido.", - "095afbf2f1f0e5be678f5dac5c54e717": "Acesso Negado", + "275f22ab95671f095640ca99194b7635": "\t DE:{0}", "2d3071e3b18681c80a090dc0efbdb349": "não foi possível localizar {0} com ID {1}", + "2d78192c43fd2ec52ec18f3918894f9a": "O middleware {0} foi descontinuado. Consulte {1} para obter mais detalhes.", + "308e1d484516a33df788f873e65faaff": "O modelo `{0}` está estendendo `DataModel` descontinuado. Use `PersistedModel` no lugar.", "316e5b82c203cf3de31a449ee07d0650": "Booleano esperado, obteve {0}", - "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Não é possível criar origem de dados {0}: {1}", - "7e287fc885d9fdcf42da3a12f38572c1": "Autorização Necessária", - "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} foi removido, use o novo módulo {{loopback-boot}} no lugar", - "1d7833c3ca2f05fdad8fad7537531c40": "\t ASSUNTO:{0}", - "275f22ab95671f095640ca99194b7635": "\t DE:{0}", + "320c482401afa1207c04343ab162e803": "Tipo principal inválido: {0}", + "3438fab56cc7ab92dfd88f0497e523e0": "A propriedade de relações da configuração de `{0}` deve ser um objeto", + "35e5252c62d80f8c54a5290d30f4c7d0": "Verifique seu e-mail abrindo este link em um navegador da web:\n\t{0}", + "3aae63bb7e8e046641767571c1591441": "login com falha pois o e-mail não foi verificado", + "3aecb24fa8bdd3f79d168761ca8a6729": "Fase {0} do {{middleware}} desconhecida", + "3caaa84fc103d6d5612173ae6d43b245": "Token inválido: {0}", + "3d617953470be16d0c2b32f0bcfbb5ee": "Obrigado por se Registrar", "3d63008ccfb2af1db2142e8cc2716ace": "Aviso: Nenhum transporte de e-mail especificado para enviar e-mail. Configure um transporte para enviar mensagens de e-mail.", + "4203ab415ec66a78d3164345439ba76e": "Não é possível chamar {0}.{1}(). O método {2} não foi configurado. O {{PersistedModel}} não foi conectado corretamente a uma {{DataSource}}!", + "44a6c8b1ded4ed653d19ddeaaf89a606": "E-mail não encontrado", "4a4f04a4e480fc5d4ee73b84d9a4b904": "Enviando E-mail:", - "63a091ced88001ab6acb58f61ec041c5": "\t TEXTO:{0}", - "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", - "ecb06666ef95e5db27a5ac1d6a17923b": "\t PARA:{0}", - "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORTE:{0}", - "0da38687fed24275c1547e815914a8e3": "Excluir um item relacionado por ID para {0}.", - "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "Critérios para corresponder instâncias do modelo", - "22fe62fa8d595b72c62208beddaa2a56": "Atualizar um item relacionado por ID para {0}.", - "528325f3cbf1b0ab9a08447515daac9a": "Atualizar {0} deste modelo.", - "543d19bad5e47ee1e9eb8af688e857b4": "Chave estrangeira para {0}.", - "598ff0255ffd1d1b71e8de55dbe2c034": "Verifique a existência da relação de {0} com um item por ID.", - "5a36cc6ba0cc27c754f6c5ed6015ea3c": "Remova a relação de {0} com um item por ID.", + "4b494de07f524703ac0879addbd64b13": "E-mail não foi verificado", + "4cac5f051ae431321673e04045d37772": "O modelo `{0}` está estendendo um modelo `{1}` desconhecido. Usando `PersistedModel` como a base.", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Não é possível criar origem de dados {0}: {1}", + "5858e63efaa0e4ad86b61c0459ea32fa": "Deve-se conectar o Modelo de {{Email}} em um conector de {{Mail}}", + "5e81ad3847a290dc650b47618b9cbc7e": "falha de login", "5fa3afb425819ebde958043e598cb664": "não foi possível localizar um modelo com {{id}} {0}", "61e5deebaf44d68f4e6a508f30cc31a3": "Relação `{0}` não existe para o modelo `{1}`", - "651f0b3cbba001635152ec3d3d954d0a": "Localize um item relacionado por ID para {0}.", - "7bc7b301ad9c4fc873029d57fb9740fe": "{0} consultas de {1}.", - "7c837b88fd0e509bd3fc722d7ddf0711": "Chave estrangeira para {0}", - "830cb6c862f8f364e9064cea0026f701": "Busca relação {0} de hasOne.", - "855ecd4a64885ba272d782435f72a4d4": "ID \"{1}\" de \"{0}\" desconhecido.", - "86254879d01a60826a851066987703f2": "Inclua um item relacionado por ID para {0}.", - "8ae418c605b6a45f2651be9b1677c180": "Método remoto inválido: `{0}`", - "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Busca relação {0} de belongsTo.", - "c0057a569ff9d3b509bac61a4b2f605d": "Exclui todos os {0} deste modelo.", - "cd0412f2f33a4a2a316acc834f3f21a6": "deve-se especificar um {{id}} ou {{data}}", - "d6f43b266533b04d442bdb3955622592": "Cria uma nova instância no {0} deste modelo.", - "da13d3cdf21330557254670dddd8c5c7": "{0} contagens de {1}.", - "e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" {{id}} \"{1}\" desconhecido.", - "f66ae3cf379b2fce28575a3282defe1a": "Exclui {0} deste modelo.", - "03f79fa268fe199de2ce4345515431c1": "Nenhum registro de mudança localizado para {0} com o ID {1}", - "0f1c71f74b040bfbe8d384a414e31f03": "Obtenha um conjunto de deltas e conflitos desde o ponto de verificação fornecido.", - "15254dec061d023d6c030083a0cef50f": "Crie uma nova instância do modelo e a persista na origem de dados.", - "16a11368d55b85a209fc6aea69ee5f7a": "Exclua todos os registros correspondentes.", - "1bc1d489ddf347af47af3d9b1fc7cc15": "Execute múltiplas atualizações imediatamente. Nota: isso não é atômico.", - "1bc7d8283c9abda512692925c8d8e3c0": "Obtenha o ponto de verificação atual.", - "1caa7cc61266e7aef7db7d2f0e27ac3e": "Atualize as propriedades do registro de mudança mais recente mantido para esta instância.", - "2a7df74fe6e8462e617b79d5fbb536ea": "Obtenha o registro de mudança mais recente para esta instância.", - "2a9684b3d5b3b67af74bac74eb1b0843": "Localize todas as instâncias do modelo correspondido pelo filtro a partir da origem de dados.", - "2e50838caf0c927735eb15d12866bdd7": "Obtenha as mudanças em um modelo desde um ponto de verificação fornecido. Forneça um objeto de filtro para reduzir o número de resultados retornados.", - "4203ab415ec66a78d3164345439ba76e": "Não é possível chamar {0}.{1}(). O método {2} não foi configurado. O {{PersistedModel}} não foi conectado corretamente a uma {{DataSource}}!", - "51ea9b6245bb5e672b236d640ca3b048": "Um objeto de pares nome/valor da propriedade de Mudança", - "55ddedd4c501348f82cb89db02ec85c1": "Um objeto de pares nome/valor da propriedade do modelo", - "5aaa76c72ae1689fd3cf62589784a4ba": "Atualize atributos para uma instância de modelo e a persista na origem de dados.", - "5f659bbc15e6e2b249fa33b3879b5f69": "Localize uma instância do modelo por {{id}} a partir da origem de dados.", "62e8b0a733417978bab22c8dacf5d7e6": "Não é possível aplicar atualizações em massa, o conector não relata o número de registros de atualização corretamente.", - "6329e0ac1de3c250ebb1df5afd5a2a7b": "O número de instâncias atualizadas", + "63a091ced88001ab6acb58f61ec041c5": "\t TEXTO:{0}", "6bc376432cd9972cf991aad3de371e78": "Dados ausentes para a mudança: {0}", - "79295ac04822d2e9702f0dd1d0240336": "Atualize instâncias do modelo correspondido por {{where}} a partir da origem de dados.", - "7f2fde7f0f860ead224b11ba8d75aa1c": "Crie uma lista de atualizações a partir de uma lista delta.", - "89b57e764c2267129294b07589dbfdc2": "Exclua uma instância do modelo por {{id}} da origem de dados.", - "8bab6720ecc58ec6412358c858a53484": "Atualização em massa falhou, o conector modificou um número inesperado de registros: {0}", - "a98b6cc6547706b5c6bffde0ed5fd55c": "Localize a primeira instância do modelo correspondido pelo filtro a partir da origem de dados.", - "c46d4aba1f14809c16730faa46933495": "Filtrar definindo campos e incluir", - "c65600640f206f585d300b4bcb699d95": "Crie um ponto de verificação.", - "cf64c7afc74d3a8120abcd028f98c770": "Atualize uma instância do modelo existente ou insira uma nova na origem de dados.", - "dcb6261868ff0a7b928aa215b07d068c": "Crie um fluxo de mudança.", - "e43e320a435ec1fa07648c1da0d558a7": "Verifique se existe uma instância do modelo na origem de dados.", - "e92aa25b6b864e3454b65a7c422bd114": "Atualização em massa falhou, o conector excluiu um número inesperado de registros: {0}", - "ea63d226b6968e328bdf6876010786b5": "Não é possível aplicar atualizações em massa, o conector não relata o número de registros excluídos corretamente.", - "f1d4ac54357cc0932f385d56814ba7e4": "Conflito", - "f37d94653793e33f4203e45b4a1cf106": "Instâncias de contagem do modelo correspondidas por where da origem de dados.", - "0731d0109e46c21a4e34af3346ed4856": "Este comportamento pode mudar na próxima versão principal.", - "2e110abee2c95bcfc2dafd48be7e2095": "Não é possível configurar {0}: {{config.dataSource}} deve ser uma instância de {{DataSource}}", - "308e1d484516a33df788f873e65faaff": "O modelo `{0}` está estendendo `DataModel` descontinuado. Use `PersistedModel` no lugar.", - "3438fab56cc7ab92dfd88f0497e523e0": "A propriedade de relações da configuração de `{0}` deve ser um objeto", - "4cac5f051ae431321673e04045d37772": "O modelo `{0}` está estendendo um modelo `{1}` desconhecido. Usando `PersistedModel` como a base.", + "705c2d456a3e204c4af56e671ec3225c": "Não foi possível localizar o {{accessToken}}", "734a7bebb65e10899935126ba63dd51f": "A propriedade de opções da configuração de `{0}` deve ser um objeto", "779467f467862836e19f494a37d6ab77": "A propriedade acls da configuração de `{0}` deve ser uma matriz de objetos", + "7d5e7ed0efaedf3f55f380caae0df8b8": "Meu primeiro aplicativo móvel", + "7e0fca41d098607e1c9aa353c67e0fa1": "Token de Acesso Inválido", + "7e287fc885d9fdcf42da3a12f38572c1": "Autorização Necessária", + "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} é necessário para efetuar logout", "80a32e80cbed65eba2103201a7c94710": "Modelo não localizado: {0}", "83cbdc2560ba9f09155ccfc63e08f1a1": "A propriedade `{0}` não pode ser reconfigurada para `{1}`", + "855ecd4a64885ba272d782435f72a4d4": "ID \"{1}\" de \"{0}\" desconhecido.", + "860d1a0b8bd340411fb32baa72867989": "O transporte não suporta redirecionamentos de HTTP.", + "895b1f941d026870b3cc8e6af087c197": "{{username}} ou {{email}} é necessário", + "8a17c5ef611e2e7535792316e66b8fca": "Senha muito longa: {0}", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "Solicitação para o host {0}", + "8ae418c605b6a45f2651be9b1677c180": "Método remoto inválido: `{0}`", + "8bab6720ecc58ec6412358c858a53484": "Atualização em massa falhou, o conector modificou um número inesperado de registros: {0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", "97795efe0c3eb7f35ce8cf8cfe70682b": "A configuração de `{0}` não possui a propriedade {{`dataSource`}}.\nUse `null` ou `false` para marcar modelos não conectados a nenhuma origem de dados.", + "a40684f5a9f546115258b76938d1de37": "Uma lista de cores está disponível em {{http://localhost:3000/colors}}", + "a50d10fc6e0959b220e085454c40381e": "Usuário não localizado: {0}", "a80038252430df2754884bf3c845c4cf": "Metadados remotos para \"{0}.{1}\" não possui sinalização \" isStatic\", o método foi registrado como um método de instância.", + "b6f740aeb6f2eb9bee9cb049dbfe6a28": "\"{0}\" {{key}} \"{1}\" desconhecido.", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} é obrigatório", + "c2b5d51f007178170ca3952d59640ca4": "Não é possível retificar mudanças de {0}:\n{1}", + "c68a93f0a9524fed4ff64372fc90c55f": "Deve-se fornecer um e-mail válido", + "cd0412f2f33a4a2a316acc834f3f21a6": "deve-se especificar um {{id}} ou {{data}}", + "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} foi removido, use o novo módulo {{loopback-boot}} no lugar", + "d9ef6dc3770dd8f80a129e92a79851f3": "{0} foi descontinuado. Consulte {1} para obter mais detalhes.", "dc568bee32deb0f6eaf63e73b20e8ceb": "Ignorando configuração de \"methods\" de não objeto de \"{0}\".", - "3aecb24fa8bdd3f79d168761ca8a6729": "Fase {0} do {{middleware}} desconhecida" + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" {{id}} \"{1}\" desconhecido.", + "e92aa25b6b864e3454b65a7c422bd114": "Atualização em massa falhou, o conector excluiu um número inesperado de registros: {0}", + "ea63d226b6968e328bdf6876010786b5": "Não é possível aplicar atualizações em massa, o conector não relata o número de registros excluídos corretamente.", + "ead044e2b4bce74b4357f8a03fb78ec4": "Não é possível chamar {0}.{1}(). O método {2} não foi configurado. O {{KeyValueModel}} não foi conectado corretamente a uma {{DataSource}}!", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t PARA:{0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORTE:{0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "resultado:{0}", + "f1d4ac54357cc0932f385d56814ba7e4": "Conflito", + "f58cdc481540cd1f69a4aa4da2e37981": "Senha inválida: {0}" } diff --git a/intl/tr/messages.json b/intl/tr/messages.json index 1d6803e74..e56e50381 100644 --- a/intl/tr/messages.json +++ b/intl/tr/messages.json @@ -1,116 +1,76 @@ { - "3b46d3a780fd6ae5f95ade489a0efffe": "Geçerli bağlam tarayıcıda desteklenmiyor.", - "7e0fca41d098607e1c9aa353c67e0fa1": "Geçersiz Erişim Belirteci", - "320c482401afa1207c04343ab162e803": "Geçersiz birincil kullanıcı tipi: {0}", - "c2b5d51f007178170ca3952d59640ca4": "{0} değişiklik düzeltilemiyor:\n{1}", - "5858e63efaa0e4ad86b61c0459ea32fa": "{{Email}} modelini bir {{Mail}} bağlayıcısına bağlamalısınız", + "03f79fa268fe199de2ce4345515431c1": "{0} için {1} tanıtıcılı bir değişiklik kaydı bulunamadı", + "04bd8af876f001ceaf443aad6a9002f9": "Kimlik doğrulaması {0} modelinin tanımlanmasını gerektiriyor.", + "0731d0109e46c21a4e34af3346ed4856": "Bu davranış sonraki ana sürümde değişebilir.", + "095afbf2f1f0e5be678f5dac5c54e717": "Erişim Verilmedi", "0caffe1d763c8cca6a61814abe33b776": "E-posta zorunludur", - "1b2a6076dccbe91a56f1672eb3b8598c": "Yanıt gövdesi, oturum açma sırasında yaratılan {{AccessToken}} belirtecine ilişkin özellikleri içerir.\n`include` parametresinin değerine bağlı olarak, gövde ek özellikler içerebilir:\n\n - `user` - `U+007BUserU+007D` - Oturum açmış olan kullanıcıya ilişkin veriler. {{(`include=user`)}}\n\n", - "2362ba55796c733e337af169922327a2": "Yanıtın içereceği ilgili nesneler. Daha fazla ayrıntı için, dönüş değeriyle ilgili açıklamaya bakın.", - "306999d39387d87b2638199ff0bed8ad": "Kullanıcının parolasını e-postayla sıfırlar.", - "3aae63bb7e8e046641767571c1591441": "e-posta doğrulanmadığından oturum açma başarısız oldu", - "3caaa84fc103d6d5612173ae6d43b245": "Geçersiz belirteç: {0}", - "42e3fa18945255412ebc6561e2c6f1dc": "Kullanıcı kaydını e-posta doğrulama belirteciyle doğrular.", - "430b6326d7ebf6600a39f614ef516bc8": "Bu bağımsız değişkeni belirtmeyin; istek üstbilgilerinden otomatik olarak alınır.", - "44a6c8b1ded4ed653d19ddeaaf89a606": "E-posta bulunamadı", - "5e81ad3847a290dc650b47618b9cbc7e": "oturum açma başarısız oldu", - "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "Kullanıcı için kullanıcı adı/e-posta ve parola ile oturum açar.", - "8608c28f5e6df0008266e3c497836176": "Kullanıcı için erişim belirteciyle oturum kapatır.", - "860d1a0b8bd340411fb32baa72867989": "Aktarım HTTP yeniden yönlendirmelerini desteklemiyor.", - "895b1f941d026870b3cc8e6af087c197": "{{username}} ya da {{email}} zorunludur", - "a50d10fc6e0959b220e085454c40381e": "Kullanıcı bulunamadı: {0}", - "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} zorunludur", - "c34fa20eea0091747fcc9eda204b8d37": "{{accessToken}} bulunamadı", - "c68a93f0a9524fed4ff64372fc90c55f": "Geçerli bir e-posta belirtilmeli", - "f58cdc481540cd1f69a4aa4da2e37981": "Geçersiz parola: {0}", - "f0bd73df8714cefb925e3b8da2f4c5f6": "sonuç:{0}", "10e01c895dc0b2fecc385f9f462f1ca6": "renklerin listesine şu adresle erişebilirsiniz: {{http://localhost:3000/colors}}", - "8a27e0c9ce3ebf0e0c3978efb456e13e": "{0} ana makinesine yönelik istek", - "a40684f5a9f546115258b76938d1de37": "Renklerin listesine şu adresle erişebilirsiniz: {{http://localhost:3000/colors}}", + "1b2a6076dccbe91a56f1672eb3b8598c": "Yanıt gövdesi, oturum açma sırasında yaratılan {{AccessToken}} belirtecine ilişkin özellikleri içerir.\n`include` parametresinin değerine bağlı olarak, gövde ek özellikler içerebilir:\n\n - `user` - `U+007BUserU+007D` - Oturum açmış olan kullanıcıya ilişkin veriler. {{(`include=user`)}}\n\n", + "1d7833c3ca2f05fdad8fad7537531c40": "\t KONU:{0}", "1e85f822b547a75d7d385048030e4ecb": "Yaratıldığı tarih: {0}", - "7d5e7ed0efaedf3f55f380caae0df8b8": "İlk mobil uygulamam", - "04bd8af876f001ceaf443aad6a9002f9": "Kimlik doğrulaması {0} modelinin tanımlanmasını gerektiriyor.", - "095afbf2f1f0e5be678f5dac5c54e717": "Erişim Verilmedi", + "275f22ab95671f095640ca99194b7635": "\t KİMDEN:{0}", "2d3071e3b18681c80a090dc0efbdb349": "{1} tanıtıcılı {0} bulunamadı", + "2d78192c43fd2ec52ec18f3918894f9a": "{0} ara katman yazılımı kullanım dışı bırakıldı. Daha fazla ayrıntı için bkz. {1}.", + "308e1d484516a33df788f873e65faaff": "`{0}` modeli kullanım dışı bırakılmış şu modeli genişletiyor: `DataModel. Onun yerine `PersistedModel` modelini kullanın.", "316e5b82c203cf3de31a449ee07d0650": "Boole beklenirken {0} alındı", - "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Veri kaynağı {0} yaratılamıyor: {1}", - "7e287fc885d9fdcf42da3a12f38572c1": "Yetkilendirme Gerekli", - "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} kaldırıldı, onun yerine yeni {{loopback-boot}} modülünü kullanın", - "1d7833c3ca2f05fdad8fad7537531c40": "\t KONU:{0}", - "275f22ab95671f095640ca99194b7635": "\t KİMDEN:{0}", + "320c482401afa1207c04343ab162e803": "Geçersiz birincil kullanıcı tipi: {0}", + "3438fab56cc7ab92dfd88f0497e523e0": "`{0}` yapılandırmasının ilişkiler (relations) özelliği bir nesne olmalıdır", + "35e5252c62d80f8c54a5290d30f4c7d0": "Lütfen bu bağlantıyı bir web tarayıcısında açarak e-postanızı doğrulayın:\n\t{0}", + "3aae63bb7e8e046641767571c1591441": "e-posta doğrulanmadığından oturum açma başarısız oldu", + "3aecb24fa8bdd3f79d168761ca8a6729": "Bilinmeyen {{middleware}} aşaması {0}", + "3caaa84fc103d6d5612173ae6d43b245": "Geçersiz belirteç: {0}", + "3d617953470be16d0c2b32f0bcfbb5ee": "Kaydolduğunuz için teşekkürler", "3d63008ccfb2af1db2142e8cc2716ace": "Uyarı: E-posta göndermek için e-posta aktarımı belirtilmedi. Posta iletileri göndermek için aktarım ayarlayın.", + "4203ab415ec66a78d3164345439ba76e": "Çağrılamıyor: {0}.{1}(). {2} yöntemi ayarlanmamış. {{PersistedModel}}, bir veri kaynağına ({{DataSource}}) doğru olarak eklenmedi!", + "44a6c8b1ded4ed653d19ddeaaf89a606": "E-posta bulunamadı", "4a4f04a4e480fc5d4ee73b84d9a4b904": "Posta Gönderiliyor:", - "63a091ced88001ab6acb58f61ec041c5": "\t METİN:{0}", - "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", - "ecb06666ef95e5db27a5ac1d6a17923b": "\t KİME:{0}", - "f0aed00a3d3d0b97d6594e4b70e0c201": "\t AKTARIM:{0}", - "0da38687fed24275c1547e815914a8e3": "{0} için ilgili bir öğeyi tanıtıcı temelinde siler.", - "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "Model eşgörünümlerini eşleştirme ölçütleri", - "22fe62fa8d595b72c62208beddaa2a56": "{0} için ilgili bir öğeyi tanıtıcı temelinde günceller.", - "528325f3cbf1b0ab9a08447515daac9a": "Bu modele ilişkin {0} güncellemesi.", - "543d19bad5e47ee1e9eb8af688e857b4": "{0} için dış anahtar.", - "598ff0255ffd1d1b71e8de55dbe2c034": "Bir öğeye yönelik {0} ilişkisinin var olup olmadığını tanıtıcı temelinde denetler.", - "5a36cc6ba0cc27c754f6c5ed6015ea3c": "Bir öğeye yönelik {0} ilişkisini kaldırır.", + "4b494de07f524703ac0879addbd64b13": "E-posta doğrulanmadı", + "4cac5f051ae431321673e04045d37772": "`{0}` modeli, bilinmeyen `{1}` modelini genişletiyor. Temel olarak 'PersistedModel' kullanılıyor.", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Veri kaynağı {0} yaratılamıyor: {1}", + "5858e63efaa0e4ad86b61c0459ea32fa": "{{Email}} modelini bir {{Mail}} bağlayıcısına bağlamalısınız", + "5e81ad3847a290dc650b47618b9cbc7e": "oturum açma başarısız oldu", "5fa3afb425819ebde958043e598cb664": "{{id}} {0} tanıtıcılı bir model bulunamadı", "61e5deebaf44d68f4e6a508f30cc31a3": "`{1}` modeli için `{0}` ilişkisi yok", - "651f0b3cbba001635152ec3d3d954d0a": "{0} için ilgili bir öğeyi tanıtıcı temelinde bulur.", - "7bc7b301ad9c4fc873029d57fb9740fe": "{1} ile ilişkili {0} öğesini sorgular.", - "7c837b88fd0e509bd3fc722d7ddf0711": "{0} için dış anahtar", - "830cb6c862f8f364e9064cea0026f701": "{0} hasOne ilişkisini alır.", - "855ecd4a64885ba272d782435f72a4d4": "Bilinmeyen \"{0}\" tanıtıcısı \"{1}\".", - "86254879d01a60826a851066987703f2": "{0} için ilgili bir öğeyi tanıtıcı temelinde ekler.", - "8ae418c605b6a45f2651be9b1677c180": "Uzak yöntem geçersiz: `{0}`", - "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "{0} belongsTo ilişkisini alır.", - "c0057a569ff9d3b509bac61a4b2f605d": "Bu modele ilişkin tüm {0} öğelerini siler.", - "cd0412f2f33a4a2a316acc834f3f21a6": "bir {{id}} ya da {{data}} belirtmelidir", - "d6f43b266533b04d442bdb3955622592": "Bu modele ilişkin {0} içinde yeni eşgörünüm yaratır.", - "da13d3cdf21330557254670dddd8c5c7": "{1} ile ilişkili {0} öğesini sayar.", - "e4434de4bb8f5a3cd1d416e4d80d7e0b": "Bilinmeyen \"{0}\" {{id}} \"{1}\".", - "f66ae3cf379b2fce28575a3282defe1a": "Bu modele ilişkin {0} öğesini siler.", - "03f79fa268fe199de2ce4345515431c1": "{0} için {1} tanıtıcılı bir değişiklik kaydı bulunamadı", - "0f1c71f74b040bfbe8d384a414e31f03": "Söz konusu denetim noktasından bu yana oluşan bir fark ve çakışma kümesini alır.", - "15254dec061d023d6c030083a0cef50f": "Modelin yeni bir eşgörünümünü yaratır ve veri kaynağında kalıcı olarak saklar.", - "16a11368d55b85a209fc6aea69ee5f7a": "Eşleşen tüm kayıtları siler.", - "1bc1d489ddf347af47af3d9b1fc7cc15": "Bir kerede birden çok güncelleme çalıştırır. Not: Atomik değildir.", - "1bc7d8283c9abda512692925c8d8e3c0": "Geçerli denetim noktasını alır.", - "1caa7cc61266e7aef7db7d2f0e27ac3e": "Bu eşgörünüm için alıkonan en son değişiklik kaydının özelliklerini günceller.", - "2a7df74fe6e8462e617b79d5fbb536ea": "Bu eşgörünüme ilişkin en son değişiklik kaydını alır.", - "2a9684b3d5b3b67af74bac74eb1b0843": "Veri kaynağında modelin süzgeçle eşleşen tüm eşgörünümlerini bulur.", - "2e50838caf0c927735eb15d12866bdd7": "Belirli bir denetim noktasından bu yana bir modelde yapılan değişiklikleri alır. Döndürülecek sonuç sayısını azaltmak için bir süzgeç nesnesi belirtin.", - "4203ab415ec66a78d3164345439ba76e": "Çağrılamıyor: {0}.{1}(). {2} yöntemi ayarlanmamış. {{PersistedModel}}, bir veri kaynağına ({{DataSource}}) doğru olarak eklenmedi!", - "51ea9b6245bb5e672b236d640ca3b048": "Değişiklik özellik adı ve değeri çiftlerine ilişkin bir nesne", - "55ddedd4c501348f82cb89db02ec85c1": "Model özellik adı ve değeri çiftlerine ilişkin bir nesne", - "5aaa76c72ae1689fd3cf62589784a4ba": "Bir model eşgörünümüne ilişkin öznitelikleri günceller ve veri kaynağında kalıcı olarak saklar.", - "5f659bbc15e6e2b249fa33b3879b5f69": "Veri kaynağında {{id}} temelinde model eşgörünümü bulur.", "62e8b0a733417978bab22c8dacf5d7e6": "Toplu güncelleme uygulanamaz; bağlayıcı, güncellenen kayıtların sayısını doğru olarak bildirmiyor.", - "6329e0ac1de3c250ebb1df5afd5a2a7b": "Güncellenen eşgörünümlerin sayısı", + "63a091ced88001ab6acb58f61ec041c5": "\t METİN:{0}", "6bc376432cd9972cf991aad3de371e78": "Değişiklik için veri eksik: {0}", - "79295ac04822d2e9702f0dd1d0240336": "Veri kaynağında modelin {{where}} ile eşleştirilen eşgörünümlerini günceller.", - "7f2fde7f0f860ead224b11ba8d75aa1c": "Fark listesinden güncelleme listesi yaratır.", - "89b57e764c2267129294b07589dbfdc2": "Bir model eşgörünümünü {{id}} temelinde veri kaynağından siler.", - "8bab6720ecc58ec6412358c858a53484": "Toplu güncelleme başarısız oldu, bağlayıcı beklenmeyen sayıda kaydı değiştirdi: {0}", - "a98b6cc6547706b5c6bffde0ed5fd55c": "Veri kaynağında modelin süzgeçle eşleşen ilk eşgörünümünü bulur.", - "c46d4aba1f14809c16730faa46933495": "Süzgeç tanımlayan alanlar ve include", - "c65600640f206f585d300b4bcb699d95": "Denetim noktası yaratır.", - "cf64c7afc74d3a8120abcd028f98c770": "Var olan bir model eşgörünümünü günceller ya da veri kaynağına yeni model eşgörünümü ekler.", - "dcb6261868ff0a7b928aa215b07d068c": "Değişiklik akışı yaratır.", - "e43e320a435ec1fa07648c1da0d558a7": "Bir model eşgörünümünün veri kaynağında var olup olmadığını denetler.", - "e92aa25b6b864e3454b65a7c422bd114": "Toplu güncelleme başarısız oldu, bağlayıcı beklenmeyen sayıda kaydı sildi: {0}", - "ea63d226b6968e328bdf6876010786b5": "Toplu güncelleme uygulanamaz; bağlayıcı, silinen kayıtların sayısını doğru olarak bildirmiyor.", - "f1d4ac54357cc0932f385d56814ba7e4": "Çakışma", - "f37d94653793e33f4203e45b4a1cf106": "Veri kaynağında, modelin where ile eşleşen eşgörünümlerini sayar.", - "0731d0109e46c21a4e34af3346ed4856": "Bu davranış sonraki ana sürümde değişebilir.", - "2e110abee2c95bcfc2dafd48be7e2095": "{0} yapılandırılamıyor: {{config.dataSource}}, bir {{DataSource}} eşgörünümü olmalıdır", - "308e1d484516a33df788f873e65faaff": "`{0}` modeli kullanım dışı bırakılmış şu modeli genişletiyor: `DataModel. Onun yerine `PersistedModel` modelini kullanın.", - "3438fab56cc7ab92dfd88f0497e523e0": "`{0}` yapılandırmasının ilişkiler (relations) özelliği bir nesne olmalıdır", - "4cac5f051ae431321673e04045d37772": "`{0}` modeli, bilinmeyen `{1}` modelini genişletiyor. Temel olarak 'PersistedModel' kullanılıyor.", + "705c2d456a3e204c4af56e671ec3225c": "{{accessToken}} bulunamadı", "734a7bebb65e10899935126ba63dd51f": "`{0}` yapılandırmasının seçenekler (options) özelliği bir nesne olmalıdır.", "779467f467862836e19f494a37d6ab77": "`{0}` yapılandırmasının erişim denetim listeleri (acls) özelliği bir nesne dizisi olmalıdır.", + "7d5e7ed0efaedf3f55f380caae0df8b8": "İlk mobil uygulamam", + "7e0fca41d098607e1c9aa353c67e0fa1": "Geçersiz Erişim Belirteci", + "7e287fc885d9fdcf42da3a12f38572c1": "Yetkilendirme Gerekli", + "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "Oturumu kapatmak için {{accessToken}} zorunludur", "80a32e80cbed65eba2103201a7c94710": "Model bulunamadı: {0}", "83cbdc2560ba9f09155ccfc63e08f1a1": "`{0}` özelliği `{1}` için yeniden yapılandırılamıyor", + "855ecd4a64885ba272d782435f72a4d4": "Bilinmeyen \"{0}\" tanıtıcısı \"{1}\".", + "860d1a0b8bd340411fb32baa72867989": "Aktarım HTTP yeniden yönlendirmelerini desteklemiyor.", + "895b1f941d026870b3cc8e6af087c197": "{{username}} ya da {{email}} zorunludur", + "8a17c5ef611e2e7535792316e66b8fca": "Parola çok uzun: {0}", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "{0} ana makinesine yönelik istek", + "8ae418c605b6a45f2651be9b1677c180": "Uzak yöntem geçersiz: `{0}`", + "8bab6720ecc58ec6412358c858a53484": "Toplu güncelleme başarısız oldu, bağlayıcı beklenmeyen sayıda kaydı değiştirdi: {0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", "97795efe0c3eb7f35ce8cf8cfe70682b": "`{0}` yapılandırmasında {{`dataSource`}} özelliği eksik.\nHiçbir veri kaynağına eklenmemiş modelleri işaretlemek için `null` ya da `false` kullanın.", + "a40684f5a9f546115258b76938d1de37": "Renklerin listesine şu adresle erişebilirsiniz: {{http://localhost:3000/colors}}", + "a50d10fc6e0959b220e085454c40381e": "Kullanıcı bulunamadı: {0}", "a80038252430df2754884bf3c845c4cf": "\"{0}.{1}\" ile ilgili uzaktan iletişim meta verisinde \"isStatic\" işareti eksik; yöntem bir eşgörünüm yöntemi olarak kaydedildi.", + "b6f740aeb6f2eb9bee9cb049dbfe6a28": "Bilinmeyen \"{0}\" {{key}} \"{1}\".", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} zorunludur", + "c2b5d51f007178170ca3952d59640ca4": "{0} değişiklik düzeltilemiyor:\n{1}", + "c68a93f0a9524fed4ff64372fc90c55f": "Geçerli bir e-posta belirtilmeli", + "cd0412f2f33a4a2a316acc834f3f21a6": "bir {{id}} ya da {{data}} belirtmelidir", + "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} kaldırıldı, onun yerine yeni {{loopback-boot}} modülünü kullanın", + "d9ef6dc3770dd8f80a129e92a79851f3": "{0} kullanım dışı bırakıldı. Daha fazla ayrıntı için bkz. {1}.", "dc568bee32deb0f6eaf63e73b20e8ceb": "\"{0}\" öğesinin nesne olmayan \"methods\" atarı yoksayılıyor.", - "3aecb24fa8bdd3f79d168761ca8a6729": "Bilinmeyen {{middleware}} aşaması {0}" + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "Bilinmeyen \"{0}\" {{id}} \"{1}\".", + "e92aa25b6b864e3454b65a7c422bd114": "Toplu güncelleme başarısız oldu, bağlayıcı beklenmeyen sayıda kaydı sildi: {0}", + "ea63d226b6968e328bdf6876010786b5": "Toplu güncelleme uygulanamaz; bağlayıcı, silinen kayıtların sayısını doğru olarak bildirmiyor.", + "ead044e2b4bce74b4357f8a03fb78ec4": "Çağrılamıyor: {0}.{1}(). {2} yöntemi ayarlanmamış. {{KeyValueModel}}, bir veri kaynağına ({{DataSource}}) doğru olarak eklenmedi!", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t KİME:{0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t AKTARIM:{0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "sonuç:{0}", + "f1d4ac54357cc0932f385d56814ba7e4": "Çakışma", + "f58cdc481540cd1f69a4aa4da2e37981": "Geçersiz parola: {0}" } diff --git a/intl/zh-Hans/messages.json b/intl/zh-Hans/messages.json index 4e573bdae..8cfebffef 100644 --- a/intl/zh-Hans/messages.json +++ b/intl/zh-Hans/messages.json @@ -1,116 +1,76 @@ { - "3b46d3a780fd6ae5f95ade489a0efffe": "浏览器中不支持当前上下文。", - "7e0fca41d098607e1c9aa353c67e0fa1": "无效的访问令牌", - "320c482401afa1207c04343ab162e803": "无效的主体类型:{0}", - "c2b5d51f007178170ca3952d59640ca4": "无法纠正 {0} 更改:\n{1}", - "5858e63efaa0e4ad86b61c0459ea32fa": "您必须将 {{Email}} 模型连接到 {{Mail}} 连接器", + "03f79fa268fe199de2ce4345515431c1": "对于标识为 {1} 的 {0},找不到任何更改记录", + "04bd8af876f001ceaf443aad6a9002f9": "认证需要定义模型 {0}。", + "0731d0109e46c21a4e34af3346ed4856": "在下一个主版本中,此行为可能进行更改。", + "095afbf2f1f0e5be678f5dac5c54e717": "拒绝访问", "0caffe1d763c8cca6a61814abe33b776": "电子邮件是必需的", - "1b2a6076dccbe91a56f1672eb3b8598c": "响应主体包含在登录时创建的 {{AccessToken}} 的属性。\n根据“include”参数的值,主体可包含其他属性:\n\n - `user` - `U+007BUserU+007D` - 当前已登录用户的数据。 {{(`include=user`)}}\n\n", - "2362ba55796c733e337af169922327a2": "要包含在响应中的相关对象。请参阅返回值的描述以获取更多详细信息。", - "306999d39387d87b2638199ff0bed8ad": "使用电子邮件重置用户的密码。", - "3aae63bb7e8e046641767571c1591441": "因为尚未验证电子邮件,登录失败", - "3caaa84fc103d6d5612173ae6d43b245": "无效的令牌:{0}", - "42e3fa18945255412ebc6561e2c6f1dc": "使用电子邮件验证令牌,确认用户注册。", - "430b6326d7ebf6600a39f614ef516bc8": "请勿提供此自变量,其自动从请求头中抽取。", - "44a6c8b1ded4ed653d19ddeaaf89a606": "找不到电子邮件", - "5e81ad3847a290dc650b47618b9cbc7e": "登录失败", - "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "使用用户名/电子邮件和密码登录用户。", - "8608c28f5e6df0008266e3c497836176": "使用访问令牌注销用户。", - "860d1a0b8bd340411fb32baa72867989": "传输不支持 HTTP 重定向。", - "895b1f941d026870b3cc8e6af087c197": "{{username}} 或 {{email}} 是必需的", - "a50d10fc6e0959b220e085454c40381e": "找不到用户:{0}", - "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} 是必需的", - "c34fa20eea0091747fcc9eda204b8d37": "找不到 {{accessToken}}", - "c68a93f0a9524fed4ff64372fc90c55f": "必须提供有效电子邮件", - "f58cdc481540cd1f69a4aa4da2e37981": "无效的密码:{0}", - "f0bd73df8714cefb925e3b8da2f4c5f6": "结果:{0}", "10e01c895dc0b2fecc385f9f462f1ca6": "颜色列表位于:{{http://localhost:3000/colors}}", - "8a27e0c9ce3ebf0e0c3978efb456e13e": "到主机 {0} 的请求", - "a40684f5a9f546115258b76938d1de37": "颜色列表位于:{{http://localhost:3000/colors}}", + "1b2a6076dccbe91a56f1672eb3b8598c": "响应主体包含在登录时创建的 {{AccessToken}} 的属性。\n根据“include”参数的值,主体可包含其他属性:\n\n - `user` - `U+007BUserU+007D` - 当前已登录用户的数据。 {{(`include=user`)}}\n\n", + "1d7833c3ca2f05fdad8fad7537531c40": "\t主题:{0}", "1e85f822b547a75d7d385048030e4ecb": "创建时间:{0}", - "7d5e7ed0efaedf3f55f380caae0df8b8": "我的第一个移动应用程序", - "04bd8af876f001ceaf443aad6a9002f9": "认证需要定义模型 {0}。", - "095afbf2f1f0e5be678f5dac5c54e717": "拒绝访问", + "275f22ab95671f095640ca99194b7635": "\t发件人:{0}", "2d3071e3b18681c80a090dc0efbdb349": "无法找到标识为 {1} 的 {0}", + "2d78192c43fd2ec52ec18f3918894f9a": "不推荐 {0} 中间件。请参阅 {1} 以获取更多详细信息。", + "308e1d484516a33df788f873e65faaff": "模型“{0}”正在扩展不推荐使用的“DataModel”。请改用“PersistedModel”。", "316e5b82c203cf3de31a449ee07d0650": "期望布尔值,获取 {0}", - "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "无法创建数据源 {0}:{1}", - "7e287fc885d9fdcf42da3a12f38572c1": "需要授权", - "d5552322de5605c58b62f47ad26d2716": "已除去 {{`app.boot`}},请改用新模块 {{loopback-boot}}", - "1d7833c3ca2f05fdad8fad7537531c40": "\t主题:{0}", - "275f22ab95671f095640ca99194b7635": "\t发件人:{0}", + "320c482401afa1207c04343ab162e803": "无效的主体类型:{0}", + "3438fab56cc7ab92dfd88f0497e523e0": "“{0}”配置的关系属性必须是对象。", + "35e5252c62d80f8c54a5290d30f4c7d0": "请通过在 Web 浏览器中打开此链接来验证您的电子邮件:\n\t{0}", + "3aae63bb7e8e046641767571c1591441": "因为尚未验证电子邮件,登录失败", + "3aecb24fa8bdd3f79d168761ca8a6729": "未知的 {{middleware}} 阶段 {0}", + "3caaa84fc103d6d5612173ae6d43b245": "无效的令牌:{0}", + "3d617953470be16d0c2b32f0bcfbb5ee": "感谢您注册", "3d63008ccfb2af1db2142e8cc2716ace": "警告:未指定用于发送电子邮件的电子邮件传输。设置传输以发送电子邮件消息。", + "4203ab415ec66a78d3164345439ba76e": "无法调用 {0}.{1}()。尚未设置 {2} 方法。{{PersistedModel}} 未正确附加到 {{DataSource}}!", + "44a6c8b1ded4ed653d19ddeaaf89a606": "找不到电子邮件", "4a4f04a4e480fc5d4ee73b84d9a4b904": "正在发送电子邮件:", - "63a091ced88001ab6acb58f61ec041c5": "\t 文本:{0}", - "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", - "ecb06666ef95e5db27a5ac1d6a17923b": "\t 收件人:{0}", - "f0aed00a3d3d0b97d6594e4b70e0c201": "\t 传输:{0}", - "0da38687fed24275c1547e815914a8e3": "按标识删除 {0} 的相关项。", - "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "用于匹配模型实例的条件", - "22fe62fa8d595b72c62208beddaa2a56": "按标识更新 {0} 的相关项。", - "528325f3cbf1b0ab9a08447515daac9a": "更新此模型的 {0}。", - "543d19bad5e47ee1e9eb8af688e857b4": "{0} 的外键。", - "598ff0255ffd1d1b71e8de55dbe2c034": "按标识检查项的 {0} 关系是否存在。", - "5a36cc6ba0cc27c754f6c5ed6015ea3c": "按标识除去项的 {0} 关系。", + "4b494de07f524703ac0879addbd64b13": "尚未验证电子邮件", + "4cac5f051ae431321673e04045d37772": "模型“{0}”正在扩展未知的模型“{1}”。使用“PersistedModel”作为基础。", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "无法创建数据源 {0}:{1}", + "5858e63efaa0e4ad86b61c0459ea32fa": "您必须将 {{Email}} 模型连接到 {{Mail}} 连接器", + "5e81ad3847a290dc650b47618b9cbc7e": "登录失败", "5fa3afb425819ebde958043e598cb664": "找不到具有 {{id}} {0} 的模型", "61e5deebaf44d68f4e6a508f30cc31a3": "对于模型“{1}”,关系“{0}”不存在", - "651f0b3cbba001635152ec3d3d954d0a": "按标识查找 {0} 的相关项。", - "7bc7b301ad9c4fc873029d57fb9740fe": "查询 {1} 的 {0}。", - "7c837b88fd0e509bd3fc722d7ddf0711": "{0} 的外键", - "830cb6c862f8f364e9064cea0026f701": "访存 hasOne 关系 {0}。", - "855ecd4a64885ba272d782435f72a4d4": "未知的“{0}”标识“{1}”。", - "86254879d01a60826a851066987703f2": "按标识添加 {0} 的相关项。", - "8ae418c605b6a45f2651be9b1677c180": "无效的远程方法:“{0}”", - "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "访存 belongsTo 关系 {0}。", - "c0057a569ff9d3b509bac61a4b2f605d": "删除此模型的所有 {0}。", - "cd0412f2f33a4a2a316acc834f3f21a6": "必须指定 {{id}} 或 {{data}}", - "d6f43b266533b04d442bdb3955622592": "在此模型的 {0} 中创建新实例。", - "da13d3cdf21330557254670dddd8c5c7": "计算 {0} 的数量({1})。", - "e4434de4bb8f5a3cd1d416e4d80d7e0b": "未知的“{0}”{{id}}“{1}”。", - "f66ae3cf379b2fce28575a3282defe1a": "删除此模型的 {0}。", - "03f79fa268fe199de2ce4345515431c1": "对于标识为 {1} 的 {0},找不到任何更改记录", - "0f1c71f74b040bfbe8d384a414e31f03": "获取自指定的检查点以来的一组增量和冲突。", - "15254dec061d023d6c030083a0cef50f": "创建模型的新实例并将其持久存储到数据源。", - "16a11368d55b85a209fc6aea69ee5f7a": "删除所有匹配记录。", - "1bc1d489ddf347af47af3d9b1fc7cc15": "立刻运行多个更新。注:这不是原子更新。", - "1bc7d8283c9abda512692925c8d8e3c0": "获取当前检查点。", - "1caa7cc61266e7aef7db7d2f0e27ac3e": "更新针对此实例保留的最新更改记录的属性。", - "2a7df74fe6e8462e617b79d5fbb536ea": "获取此实例的最新更改记录。", - "2a9684b3d5b3b67af74bac74eb1b0843": "从数据源中查找按过滤器匹配的模型的所有实例。", - "2e50838caf0c927735eb15d12866bdd7": "获取自指定的检查点以来的模型更改。提供过滤器对象以减少返回的结果数量。", - "4203ab415ec66a78d3164345439ba76e": "无法调用 {0}.{1}()。尚未设置 {2} 方法。{{PersistedModel}} 未正确附加到 {{DataSource}}!", - "51ea9b6245bb5e672b236d640ca3b048": "更改属性名称/值对的对象", - "55ddedd4c501348f82cb89db02ec85c1": "模型属性名称/值对的对象", - "5aaa76c72ae1689fd3cf62589784a4ba": "更新模型实例的属性并将其持久存储到数据源。", - "5f659bbc15e6e2b249fa33b3879b5f69": "从数据源按 {{id}} 查找模型实例。", "62e8b0a733417978bab22c8dacf5d7e6": "无法应用批量更新,连接器未正确报告更新的记录数。", - "6329e0ac1de3c250ebb1df5afd5a2a7b": "已更新实例数量", + "63a091ced88001ab6acb58f61ec041c5": "\t 文本:{0}", "6bc376432cd9972cf991aad3de371e78": "缺少更改的数据:{0}", - "79295ac04822d2e9702f0dd1d0240336": "从数据源更新按 {{where}} 匹配的模型的实例。", - "7f2fde7f0f860ead224b11ba8d75aa1c": "根据增量列表创建更新列表。", - "89b57e764c2267129294b07589dbfdc2": "从数据源按 {{id}} 删除模型实例。", - "8bab6720ecc58ec6412358c858a53484": "批量更新失败,连接器已修改意外数量的记录:{0}", - "a98b6cc6547706b5c6bffde0ed5fd55c": "从数据源中查找按过滤器匹配的模型的第一个实例。", - "c46d4aba1f14809c16730faa46933495": "过滤定义字段并包含", - "c65600640f206f585d300b4bcb699d95": "创建检查点。", - "cf64c7afc74d3a8120abcd028f98c770": "更新现有模型实例或将新实例插入到数据源。", - "dcb6261868ff0a7b928aa215b07d068c": "创建更改流。", - "e43e320a435ec1fa07648c1da0d558a7": "检查数据源中是否存在模型实例。", - "e92aa25b6b864e3454b65a7c422bd114": "批量更新失败,连接器已删除意外数量的记录:{0}", - "ea63d226b6968e328bdf6876010786b5": "无法应用批量更新,连接器未正确报告删除的记录数。", - "f1d4ac54357cc0932f385d56814ba7e4": "冲突", - "f37d94653793e33f4203e45b4a1cf106": "从数据源中计算按 where 匹配的模型的实例数量。", - "0731d0109e46c21a4e34af3346ed4856": "在下一个主版本中,此行为可能进行更改。", - "2e110abee2c95bcfc2dafd48be7e2095": "无法配置 {0}:{{config.dataSource}} 必须是 {{DataSource}} 的实例", - "308e1d484516a33df788f873e65faaff": "模型“{0}”正在扩展不推荐使用的“DataModel”。请改用“PersistedModel”。", - "3438fab56cc7ab92dfd88f0497e523e0": "“{0}”配置的关系属性必须是对象。", - "4cac5f051ae431321673e04045d37772": "模型“{0}”正在扩展未知的模型“{1}”。使用“PersistedModel”作为基础。", + "705c2d456a3e204c4af56e671ec3225c": "无法找到 {{accessToken}}", "734a7bebb65e10899935126ba63dd51f": "“{0}”配置的选项属性必须是对象。", "779467f467862836e19f494a37d6ab77": "“{0}”配置的 acls 属性必须是对象数组。", + "7d5e7ed0efaedf3f55f380caae0df8b8": "我的第一个移动应用程序", + "7e0fca41d098607e1c9aa353c67e0fa1": "无效的访问令牌", + "7e287fc885d9fdcf42da3a12f38572c1": "需要授权", + "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} 需要注销", "80a32e80cbed65eba2103201a7c94710": "找不到模型:{0}", "83cbdc2560ba9f09155ccfc63e08f1a1": "无法针对“{1}”重新配置属性“{0}”。", + "855ecd4a64885ba272d782435f72a4d4": "未知的“{0}”标识“{1}”。", + "860d1a0b8bd340411fb32baa72867989": "传输不支持 HTTP 重定向。", + "895b1f941d026870b3cc8e6af087c197": "{{username}} 或 {{email}} 是必需的", + "8a17c5ef611e2e7535792316e66b8fca": "密码过长:{0}", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "到主机 {0} 的请求", + "8ae418c605b6a45f2651be9b1677c180": "无效的远程方法:“{0}”", + "8bab6720ecc58ec6412358c858a53484": "批量更新失败,连接器已修改意外数量的记录:{0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", "97795efe0c3eb7f35ce8cf8cfe70682b": "“{0}”的配置缺少 {{`dataSource`}} 属性。\n使用“null”或“false”来标记未附加到任何数据源的模型。", + "a40684f5a9f546115258b76938d1de37": "颜色列表位于:{{http://localhost:3000/colors}}", + "a50d10fc6e0959b220e085454c40381e": "找不到用户:{0}", "a80038252430df2754884bf3c845c4cf": "“{0}.{1}”的远程处理元数据缺少“isStatic”标志,方法注册为实例方法。", + "b6f740aeb6f2eb9bee9cb049dbfe6a28": "未知的“{0}”{{key}}“{1}”。", + "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} 是必需的", + "c2b5d51f007178170ca3952d59640ca4": "无法纠正 {0} 更改:\n{1}", + "c68a93f0a9524fed4ff64372fc90c55f": "必须提供有效电子邮件", + "cd0412f2f33a4a2a316acc834f3f21a6": "必须指定 {{id}} 或 {{data}}", + "d5552322de5605c58b62f47ad26d2716": "已除去 {{`app.boot`}},请改用新模块 {{loopback-boot}}", + "d9ef6dc3770dd8f80a129e92a79851f3": "不推荐使用 {0}。请参阅 {1} 以获取更多详细信息。", "dc568bee32deb0f6eaf63e73b20e8ceb": "忽略“{0}”的非对象“方法”设置。", - "3aecb24fa8bdd3f79d168761ca8a6729": "未知的 {{middleware}} 阶段 {0}" + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "未知的“{0}”{{id}}“{1}”。", + "e92aa25b6b864e3454b65a7c422bd114": "批量更新失败,连接器已删除意外数量的记录:{0}", + "ea63d226b6968e328bdf6876010786b5": "无法应用批量更新,连接器未正确报告删除的记录数。", + "ead044e2b4bce74b4357f8a03fb78ec4": "无法调用 {0}.{1}()。尚未设置 {2} 方法。{{KeyValueModel}} 未正确附加到 {{DataSource}}!", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t 收件人:{0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t 传输:{0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "结果:{0}", + "f1d4ac54357cc0932f385d56814ba7e4": "冲突", + "f58cdc481540cd1f69a4aa4da2e37981": "无效的密码:{0}" } diff --git a/intl/zh-Hant/messages.json b/intl/zh-Hant/messages.json index ac96fa222..a1d13ec88 100644 --- a/intl/zh-Hant/messages.json +++ b/intl/zh-Hant/messages.json @@ -1,116 +1,76 @@ { - "3b46d3a780fd6ae5f95ade489a0efffe": "瀏覽器不支援現行環境定義。", - "7e0fca41d098607e1c9aa353c67e0fa1": "存取記號無效", - "320c482401afa1207c04343ab162e803": "無效的主體類型:{0}", - "c2b5d51f007178170ca3952d59640ca4": "無法更正 {0} 個變更:\n{1}", - "5858e63efaa0e4ad86b61c0459ea32fa": "您必須將 {{Email}} 模型連接至 {{Mail}} 連接器", + "03f79fa268fe199de2ce4345515431c1": "對於 id 為 {1} 的 {0},找不到變更記錄", + "04bd8af876f001ceaf443aad6a9002f9": "需要定義模型 {0} 才能鑑別。", + "0731d0109e46c21a4e34af3346ed4856": "下一個主要版本中可能會變更這個行為。", + "095afbf2f1f0e5be678f5dac5c54e717": "拒絕存取", "0caffe1d763c8cca6a61814abe33b776": "需要電子郵件", - "1b2a6076dccbe91a56f1672eb3b8598c": "回應內文包含登入時建立的 {{AccessToken}} 的內容。\n根據 `include` 參數的值而定,內文還可能包含其他內容:\n\n - `user` - `U+007BUserU+007D` - 目前登入的使用者的資料。 {{(`include=user`)}}\n\n", - "2362ba55796c733e337af169922327a2": "要包含在回應中的相關物件。如需詳細資料,請參閱回覆值的說明。", - "306999d39387d87b2638199ff0bed8ad": "透過電子郵件來重設使用者的密碼。", - "3aae63bb7e8e046641767571c1591441": "因為尚未驗證電子郵件,所以登入失敗", - "3caaa84fc103d6d5612173ae6d43b245": "無效記號:{0}", - "42e3fa18945255412ebc6561e2c6f1dc": "透過電子郵件驗證記號來確認使用者登錄。", - "430b6326d7ebf6600a39f614ef516bc8": "請勿提供這個引數,因為會自動從要求標頭中擷取它。", - "44a6c8b1ded4ed653d19ddeaaf89a606": "找不到電子郵件", - "5e81ad3847a290dc650b47618b9cbc7e": "登入失敗", - "83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "透過使用者名稱/電子郵件和密碼將使用者登入。", - "8608c28f5e6df0008266e3c497836176": "透過存取記號將使用者登出。", - "860d1a0b8bd340411fb32baa72867989": "傳輸不支援 HTTP 重新導向。", - "895b1f941d026870b3cc8e6af087c197": "需要 {{username}} 或 {{email}}", - "a50d10fc6e0959b220e085454c40381e": "找不到使用者:{0}", - "ba96498b10c179f9cd75f75c8def4f70": "需要 {{realm}}", - "c34fa20eea0091747fcc9eda204b8d37": "找不到 {{accessToken}}", - "c68a93f0a9524fed4ff64372fc90c55f": "必須提供有效的電子郵件", - "f58cdc481540cd1f69a4aa4da2e37981": "無效密碼:{0}", - "f0bd73df8714cefb925e3b8da2f4c5f6": "結果:{0}", "10e01c895dc0b2fecc385f9f462f1ca6": "{{http://localhost:3000/colors}} 提供顏色清單", - "8a27e0c9ce3ebf0e0c3978efb456e13e": "向主機 {0} 要求", - "a40684f5a9f546115258b76938d1de37": "{{http://localhost:3000/colors}} 提供顏色清單", + "1b2a6076dccbe91a56f1672eb3b8598c": "回應內文包含登入時建立的 {{AccessToken}} 的內容。\n根據 `include` 參數的值而定,內文還可能包含其他內容:\n\n - `user` - `U+007BUserU+007D` - 目前登入的使用者的資料。 {{(`include=user`)}}\n\n", + "1d7833c3ca2f05fdad8fad7537531c40": "\t 主旨:{0}", "1e85f822b547a75d7d385048030e4ecb": "已建立:{0}", - "7d5e7ed0efaedf3f55f380caae0df8b8": "我的第一個行動式應用程式", - "04bd8af876f001ceaf443aad6a9002f9": "需要定義模型 {0} 才能鑑別。", - "095afbf2f1f0e5be678f5dac5c54e717": "拒絕存取", + "275f22ab95671f095640ca99194b7635": "\t 寄件者:{0}", "2d3071e3b18681c80a090dc0efbdb349": "找不到 id 為 {1} 的 {0}", + "2d78192c43fd2ec52ec18f3918894f9a": "{0} 中介軟體已淘汰。如需詳細資料,請參閱 {1}。", + "308e1d484516a33df788f873e65faaff": "模型 `{0}` 正在延伸已淘汰的 `DataModel。請改用 `PersistedModel`。", "316e5b82c203cf3de31a449ee07d0650": "預期為布林,但卻取得 {0}", - "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "無法建立資料來源 {0}:{1}", - "7e287fc885d9fdcf42da3a12f38572c1": "需要授權", - "d5552322de5605c58b62f47ad26d2716": "已移除 {{`app.boot`}},請改用新的模組 {{loopback-boot}}", - "1d7833c3ca2f05fdad8fad7537531c40": "\t 主旨:{0}", - "275f22ab95671f095640ca99194b7635": "\t 寄件者:{0}", + "320c482401afa1207c04343ab162e803": "無效的主體類型:{0}", + "3438fab56cc7ab92dfd88f0497e523e0": "`{0}` 配置的 relations 內容必須是物件", + "35e5252c62d80f8c54a5290d30f4c7d0": "請在 Web 瀏覽器中開啟此鏈結來驗證電子郵件:\n\t{0}", + "3aae63bb7e8e046641767571c1591441": "因為尚未驗證電子郵件,所以登入失敗", + "3aecb24fa8bdd3f79d168761ca8a6729": "{{middleware}} 階段 {0} 不明", + "3caaa84fc103d6d5612173ae6d43b245": "無效記號:{0}", + "3d617953470be16d0c2b32f0bcfbb5ee": "感謝您登錄", "3d63008ccfb2af1db2142e8cc2716ace": "警告:未指定用於傳送電子郵件的電子郵件傳輸。請設定傳輸來傳送郵件訊息。", + "4203ab415ec66a78d3164345439ba76e": "無法呼叫 {0}.{1}()。尚未設定 {2} 方法。{{PersistedModel}} 未正確連接至 {{DataSource}}!", + "44a6c8b1ded4ed653d19ddeaaf89a606": "找不到電子郵件", "4a4f04a4e480fc5d4ee73b84d9a4b904": "正在傳送郵件:", - "63a091ced88001ab6acb58f61ec041c5": "\t 文字:{0}", - "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", - "ecb06666ef95e5db27a5ac1d6a17923b": "\t 收件者:{0}", - "f0aed00a3d3d0b97d6594e4b70e0c201": "\t 傳輸:{0}", - "0da38687fed24275c1547e815914a8e3": "依 id 刪除 {0} 的相關項目。", - "0e4f89f8dde1e88cbfc6c1d88f0f77cb": "用於比對模型實例的準則", - "22fe62fa8d595b72c62208beddaa2a56": "依 id 更新 {0} 的相關項目。", - "528325f3cbf1b0ab9a08447515daac9a": "更新這個模型的 {0}。", - "543d19bad5e47ee1e9eb8af688e857b4": "{0} 的外部索引鍵。", - "598ff0255ffd1d1b71e8de55dbe2c034": "依 id 檢查項目的 {0} 關係是否存在。", - "5a36cc6ba0cc27c754f6c5ed6015ea3c": "依 id 移除項目的 {0} 關係。", + "4b494de07f524703ac0879addbd64b13": "尚未驗證電子郵件", + "4cac5f051ae431321673e04045d37772": "模型 `{0}` 正在延伸不明模型 `{1}`。請使用 `PersistedModel` 作為基礎。", + "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "無法建立資料來源 {0}:{1}", + "5858e63efaa0e4ad86b61c0459ea32fa": "您必須將 {{Email}} 模型連接至 {{Mail}} 連接器", + "5e81ad3847a290dc650b47618b9cbc7e": "登入失敗", "5fa3afb425819ebde958043e598cb664": "找不到 {{id}} 為 {0} 的模型", "61e5deebaf44d68f4e6a508f30cc31a3": "模型 `{1}` 的關係 `{0}` 不存在", - "651f0b3cbba001635152ec3d3d954d0a": "依 id 尋找 {0} 的相關項目。", - "7bc7b301ad9c4fc873029d57fb9740fe": "查詢 {0} 個(共 {1} 個)。", - "7c837b88fd0e509bd3fc722d7ddf0711": "{0} 的外部索引鍵", - "830cb6c862f8f364e9064cea0026f701": "提取 hasOne 關係 {0}。", - "855ecd4a64885ba272d782435f72a4d4": "\"{0}\" ID \"{1}\" 不明。", - "86254879d01a60826a851066987703f2": "依 id 新增 {0} 的相關項目。", - "8ae418c605b6a45f2651be9b1677c180": "無效的遠端方法:`{0}`", - "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "提取 belongsTo 關係 {0}。", - "c0057a569ff9d3b509bac61a4b2f605d": "刪除這個模型的所有 {0}。", - "cd0412f2f33a4a2a316acc834f3f21a6": "必須指定 {{id}} 或 {{data}}", - "d6f43b266533b04d442bdb3955622592": "在這個模型的 {0} 中建立新實例。", - "da13d3cdf21330557254670dddd8c5c7": "計算 {0} 個(共 {1} 個)。", - "e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" {{id}} \"{1}\" 不明。", - "f66ae3cf379b2fce28575a3282defe1a": "刪除這個模型的 {0}。", - "03f79fa268fe199de2ce4345515431c1": "對於 id 為 {1} 的 {0},找不到變更記錄", - "0f1c71f74b040bfbe8d384a414e31f03": "取得自給定的檢查點以來的一連串差異和衝突。", - "15254dec061d023d6c030083a0cef50f": "建立模型的新實例並保存到資料來源。", - "16a11368d55b85a209fc6aea69ee5f7a": "刪除所有相符記錄。", - "1bc1d489ddf347af47af3d9b1fc7cc15": "一次執行多個更新。附註:這可以分開進行。", - "1bc7d8283c9abda512692925c8d8e3c0": "取得現行檢查點。", - "1caa7cc61266e7aef7db7d2f0e27ac3e": "更新這個實例所保留的最新變更記錄的內容。", - "2a7df74fe6e8462e617b79d5fbb536ea": "取得這個實例的最新變更記錄。", - "2a9684b3d5b3b67af74bac74eb1b0843": "從資料來源,依 filter 尋找所有相符的模型實例。", - "2e50838caf0c927735eb15d12866bdd7": "取得自給定的檢查點以來的模型變更。請提供過濾器物件,以減少傳回的結果數。", - "4203ab415ec66a78d3164345439ba76e": "無法呼叫 {0}.{1}()。尚未設定 {2} 方法。{{PersistedModel}} 未正確連接至 {{DataSource}}!", - "51ea9b6245bb5e672b236d640ca3b048": "Change 內容名稱/值配對的物件", - "55ddedd4c501348f82cb89db02ec85c1": "model 內容名稱/值配對的物件", - "5aaa76c72ae1689fd3cf62589784a4ba": "更新模型實例的屬性並保存到資料來源。", - "5f659bbc15e6e2b249fa33b3879b5f69": "從資料來源,依 {{id}} 尋找模型實例。", "62e8b0a733417978bab22c8dacf5d7e6": "無法套用大量更新,連接器未正確報告已更新的記錄數。", - "6329e0ac1de3c250ebb1df5afd5a2a7b": "已更新的實例數", + "63a091ced88001ab6acb58f61ec041c5": "\t 文字:{0}", "6bc376432cd9972cf991aad3de371e78": "遺漏變更的資料:{0}", - "79295ac04822d2e9702f0dd1d0240336": "從資料來源,依 {{where}} 更新相符的模型實例。", - "7f2fde7f0f860ead224b11ba8d75aa1c": "從差異清單建立更新清單。", - "89b57e764c2267129294b07589dbfdc2": "從資料來源,依 {{id}} 刪除模型實例。", - "8bab6720ecc58ec6412358c858a53484": "大量更新失敗,連接器已修改超乎預期的記錄數:{0}", - "a98b6cc6547706b5c6bffde0ed5fd55c": "從資料來源,依 filter 尋找第一個相符的模型實例。", - "c46d4aba1f14809c16730faa46933495": "定義 fields 和 include 的過濾器", - "c65600640f206f585d300b4bcb699d95": "建立檢查點。", - "cf64c7afc74d3a8120abcd028f98c770": "更新現有的模型實例,或將新的模型實例插入資料來源中。", - "dcb6261868ff0a7b928aa215b07d068c": "建立變更串流。", - "e43e320a435ec1fa07648c1da0d558a7": "檢查資料來源中是否存在模型實例。", - "e92aa25b6b864e3454b65a7c422bd114": "大量更新失敗,連接器已刪除非預期的記錄數:{0}", - "ea63d226b6968e328bdf6876010786b5": "無法套用大量更新,連接器未正確報告已刪除的記錄數。", - "f1d4ac54357cc0932f385d56814ba7e4": "衝突", - "f37d94653793e33f4203e45b4a1cf106": "從資料來源,依 where 計算相符的模型實例數。", - "0731d0109e46c21a4e34af3346ed4856": "下一個主要版本中可能會變更這個行為。", - "2e110abee2c95bcfc2dafd48be7e2095": "無法配置 {0}:{{config.dataSource}} 必須是 {{DataSource}} 的實例", - "308e1d484516a33df788f873e65faaff": "模型 `{0}` 正在延伸已淘汰的 `DataModel。請改用 `PersistedModel`。", - "3438fab56cc7ab92dfd88f0497e523e0": "`{0}` 配置的 relations 內容必須是物件", - "4cac5f051ae431321673e04045d37772": "模型 `{0}` 正在延伸不明模型 `{1}`。請使用 `PersistedModel` 作為基礎。", + "705c2d456a3e204c4af56e671ec3225c": "找不到 {{accessToken}}", "734a7bebb65e10899935126ba63dd51f": "`{0}` 配置的 options 內容必須是物件", "779467f467862836e19f494a37d6ab77": "`{0}` 配置的 acls 內容必須是物件陣列", + "7d5e7ed0efaedf3f55f380caae0df8b8": "我的第一個行動式應用程式", + "7e0fca41d098607e1c9aa353c67e0fa1": "存取記號無效", + "7e287fc885d9fdcf42da3a12f38572c1": "需要授權", + "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "需要 {{accessToken}} 才能登出", "80a32e80cbed65eba2103201a7c94710": "找不到模型:{0}", "83cbdc2560ba9f09155ccfc63e08f1a1": "無法為 `{1}` 重新配置內容 `{0}`", + "855ecd4a64885ba272d782435f72a4d4": "\"{0}\" ID \"{1}\" 不明。", + "860d1a0b8bd340411fb32baa72867989": "傳輸不支援 HTTP 重新導向。", + "895b1f941d026870b3cc8e6af087c197": "需要 {{username}} 或 {{email}}", + "8a17c5ef611e2e7535792316e66b8fca": "密碼太長:{0}", + "8a27e0c9ce3ebf0e0c3978efb456e13e": "向主機 {0} 要求", + "8ae418c605b6a45f2651be9b1677c180": "無效的遠端方法:`{0}`", + "8bab6720ecc58ec6412358c858a53484": "大量更新失敗,連接器已修改超乎預期的記錄數:{0}", + "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", "97795efe0c3eb7f35ce8cf8cfe70682b": "`{0}` 的配置遺漏 {{`dataSource`}} 內容。\n請使用 `null` 或 `false` 來標示未連接至任何資料來源的模型。", + "a40684f5a9f546115258b76938d1de37": "{{http://localhost:3000/colors}} 提供顏色清單", + "a50d10fc6e0959b220e085454c40381e": "找不到使用者:{0}", "a80038252430df2754884bf3c845c4cf": "\"{0}.{1}\" 的遠端 meta 資料遺漏 \"isStatic\" 旗標,這個方法已登錄為實例方法。", + "b6f740aeb6f2eb9bee9cb049dbfe6a28": "\"{0}\" {{key}} \"{1}\" 不明。", + "ba96498b10c179f9cd75f75c8def4f70": "需要 {{realm}}", + "c2b5d51f007178170ca3952d59640ca4": "無法更正 {0} 個變更:\n{1}", + "c68a93f0a9524fed4ff64372fc90c55f": "必須提供有效的電子郵件", + "cd0412f2f33a4a2a316acc834f3f21a6": "必須指定 {{id}} 或 {{data}}", + "d5552322de5605c58b62f47ad26d2716": "已移除 {{`app.boot`}},請改用新的模組 {{loopback-boot}}", + "d9ef6dc3770dd8f80a129e92a79851f3": "{0} 已淘汰。如需詳細資料,請參閱 {1}。", "dc568bee32deb0f6eaf63e73b20e8ceb": "忽略 \"{0}\" 的非物件 \"methods\" 設定。", - "3aecb24fa8bdd3f79d168761ca8a6729": "{{middleware}} 階段 {0} 不明" + "e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" {{id}} \"{1}\" 不明。", + "e92aa25b6b864e3454b65a7c422bd114": "大量更新失敗,連接器已刪除非預期的記錄數:{0}", + "ea63d226b6968e328bdf6876010786b5": "無法套用大量更新,連接器未正確報告已刪除的記錄數。", + "ead044e2b4bce74b4357f8a03fb78ec4": "無法呼叫 {0}.{1}()。尚未設定 {2} 方法。{{KeyValueModel}} 未正確連接至 {{DataSource}}!", + "ecb06666ef95e5db27a5ac1d6a17923b": "\t 收件者:{0}", + "f0aed00a3d3d0b97d6594e4b70e0c201": "\t 傳輸:{0}", + "f0bd73df8714cefb925e3b8da2f4c5f6": "結果:{0}", + "f1d4ac54357cc0932f385d56814ba7e4": "衝突", + "f58cdc481540cd1f69a4aa4da2e37981": "無效密碼:{0}" } From 22bd0fc81faff4a11c617eb15d8cd56696862c32 Mon Sep 17 00:00:00 2001 From: loay Date: Tue, 11 Jul 2017 11:18:32 -0400 Subject: [PATCH 163/187] Add unit test for empty password --- test/user.test.js | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/test/user.test.js b/test/user.test.js index db2fbb4de..522512cbb 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -401,16 +401,31 @@ describe('User', function() { var pass73Char = pass72Char + '3'; var passTooLong = pass72Char + 'WXYZ1234'; - it('rejects passwords longer than 72 characters', function(done) { - try { - User.create({ email: 'b@c.com', password: pass73Char }, function(err) { + it('rejects empty passwords creation', function(done) { + User.create({email: 'b@c.com', password: ''}, function(err) { + expect(err.code).to.equal('INVALID_PASSWORD'); + expect(err.statusCode).to.equal(422); + done(); + }); + }); + + it('rejects updating with empty password', function(done) { + User.create({email: 'blank@c.com', password: pass72Char}, function(err, userCreated) { if (err) return done(err); - done(new Error('User.create() should have thrown an error.')); + userCreated.updateAttribute('password', '', function(err, userUpdated) { + expect(err.code).to.equal('INVALID_PASSWORD'); + expect(err.statusCode).to.equal(422); + done(); + }); }); - } catch (e) { - expect(e).to.match(/Password too long/); + }); + + it('rejects passwords longer than 72 characters', function(done) { + User.create({ email: 'b@c.com', password: pass73Char }, function(err) { + expect(err.code).to.equal('PASSWORD_TOO_LONG'); + expect(err.statusCode).to.equal(422); done(); - } + }); }); it('rejects a new user with password longer than 72 characters', function(done) { From 4f928bf965cd76947ff939de1917b99448743891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 4 Oct 2017 10:31:50 +0200 Subject: [PATCH 164/187] test: fix too strict test assertion Rework the test verifying properties of `loopback` to ignore new express properties added after the test was written. Ignore "json" and "urlencoded" middleware that was added back to Express, keep using our wrappers printing a deprecation message. --- package.json | 2 +- test/loopback.test.js | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index afc9ea4d6..9f1fff658 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "depd": "^1.0.0", "ejs": "^2.3.1", "errorhandler": "^1.3.4", - "express": "^4.12.2", + "express": "^4.16.2", "inflection": "^1.6.0", "isemail": "^1.2.0", "loopback-connector-remote": "^1.0.3", diff --git a/test/loopback.test.js b/test/loopback.test.js index 3f833b0a1..7b9b9f41f 100644 --- a/test/loopback.test.js +++ b/test/loopback.test.js @@ -117,7 +117,7 @@ describe('loopback', function() { var actual = Object.getOwnPropertyNames(loopback); actual.sort(); - expect(actual).to.eql(EXPECTED); + expect(actual).to.include.members(EXPECTED); }); }); @@ -530,6 +530,11 @@ describe('loopback', function() { it('inherits properties from express', function() { var express = require('express'); for (var i in express) { + // Express added back body-parsing middleware in v4.16.0, see + // https://github.com/expressjs/express/issues/2211 + // However, we are keeping them deprecated in 2.x, + // because it's in LTS mode + if (i === 'json' || i === 'urlencoded') continue; expect(loopback).to.have.property(i, express[i]); } }); From 538bc9a7d5a97efb91cf08d1e99ff2968dab72f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 19 Oct 2017 13:46:48 +0200 Subject: [PATCH 165/187] Drop support for Node.js versions 0.10 and 0.12 Some of our dependencies are no longer supporting pre-4.0 versions of Node.js. As a result, our CI builds are failing on these platforms. This pull request removes 0.10 and 0.12 from our Travis CI build matrix and also adds "engines" field to package.json to tell our internal Jenkins CI to stop testing 0.10 and 0.12 versions too. --- .travis.yml | 4 ++-- package.json | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ec6072c9e..bdf39070f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ sudo: false language: node_js node_js: - - "0.10" - - "0.12" - "4" - "6" + - "8" + after_success: npm run coverage # see https://www.npmjs.com/package/phantomjs-prebuilt#continuous-integration diff --git a/package.json b/package.json index 9f1fff658..b7e93418f 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,9 @@ "coverage": "nyc report --reporter=text-lcov | coveralls", "test": "nyc grunt mocha-and-karma" }, + "engines": { + "node": ">=4.0.0" + }, "dependencies": { "async": "^2.0.1", "bcryptjs": "^2.1.0", From 6e0e60c2a215546d42c6a7b06b6d814409e99272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 23 Oct 2017 09:22:09 +0200 Subject: [PATCH 166/187] 2.39.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Drop support for Node.js versions 0.10 and 0.12 (Miroslav Bajtoš) * test: fix too strict test assertion (Miroslav Bajtoš) * Add unit test for empty password (loay) * Update translated strings Q2 2017 (Allen Boone) --- CHANGES.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index c33563a04..bcda27ab0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,15 @@ +2017-10-23, Version 2.39.0 +========================== + + * Drop support for Node.js versions 0.10 and 0.12 (Miroslav Bajtoš) + + * test: fix too strict test assertion (Miroslav Bajtoš) + + * Add unit test for empty password (loay) + + * Update translated strings Q2 2017 (Allen Boone) + + 2017-04-17, Version 2.38.3 ========================== diff --git a/package.json b/package.json index b7e93418f..546a5576f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.38.3", + "version": "2.39.0", "publishConfig": { "tag": "lts" }, From 787f393c7c8dc209dbf91286e0be3a7b33fa9a38 Mon Sep 17 00:00:00 2001 From: Samuel Reed Date: Wed, 15 Nov 2017 12:49:13 -0600 Subject: [PATCH 167/187] fix(AccessContext): Tighten userid/appid checks An application may have a use for a falsy ID. --- lib/access-context.js | 8 ++++---- test/role.test.js | 48 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/lib/access-context.js b/lib/access-context.js index af04bdbba..21aa7fc5b 100644 --- a/lib/access-context.js +++ b/lib/access-context.js @@ -61,16 +61,16 @@ function AccessContext(context) { var principalType = context.principalType || Principal.USER; var principalId = context.principalId || undefined; var principalName = context.principalName || undefined; - if (principalId) { + if (principalId != null) { this.addPrincipal(principalType, principalId, principalName); } var token = this.accessToken || {}; - if (token.userId) { + if (token.userId != null) { this.addPrincipal(Principal.USER, token.userId); } - if (token.appId) { + if (token.appId != null) { this.addPrincipal(Principal.APPLICATION, token.appId); } this.remotingContext = context.remotingContext; @@ -151,7 +151,7 @@ AccessContext.prototype.getAppId = function() { * @returns {boolean} */ AccessContext.prototype.isAuthenticated = function() { - return !!(this.getUserId() || this.getAppId()); + return this.getUserId() != null || this.getAppId() != null; }; /*! diff --git a/test/role.test.js b/test/role.test.js index 2e9875951..7b06a5839 100644 --- a/test/role.test.js +++ b/test/role.test.js @@ -303,6 +303,54 @@ describe('role model', function() { }); }); + it('should be properly authenticated with 0 userId', function(done) { + User.create({ name: 'Raymond', email: 'x@y.com', password: 'foobar', id: 0 }, function(err, user) { + if (err) return done(err); + Role.create({ name: 'userRole' }, function(err, role) { + if (err) return done(err); + role.principals.create({ principalType: RoleMapping.USER, principalId: user.id }, + function(err, p) { + if (err) return done(err); + async.series([ + function(next) { + Role.isInRole( + 'userRole', + { principalType: RoleMapping.USER, principalId: user.id }, + function(err, inRole) { + if (err) return next(err); + assert(!!inRole); + next(); + }); + }, + function(next) { + Role.isInRole( + 'userRole', + { principalType: RoleMapping.APP, principalId: user.id }, + function(err, inRole) { + if (err) return next(err); + assert(!inRole); + next(); + }); + }, + function(next) { + Role.getRoles( + { principalType: RoleMapping.USER, principalId: user.id }, + function(err, roles) { + if (err) return next(err); + expect(roles).to.eql([ + Role.AUTHENTICATED, + Role.EVERYONE, + role.id, + ]); + next(); + }); + }, + ], done); + }); + }); + }); + }); + it('should support owner role resolver', function(done) { Role.registerResolver('returnPromise', function(role, context) { return new Promise(function(resolve) { From 2e0f3d15f9d615fcf9099012d2e4f83d3c77b3be Mon Sep 17 00:00:00 2001 From: Samuel Reed Date: Tue, 5 Dec 2017 09:49:50 -0600 Subject: [PATCH 168/187] fix(id): replace with != null Ref: #2356, #2374, #3130, #3693 --- common/models/access-token.js | 2 +- lib/model.js | 4 ++-- lib/persisted-model.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/common/models/access-token.js b/common/models/access-token.js index 707ba4cec..860ef53ec 100644 --- a/common/models/access-token.js +++ b/common/models/access-token.js @@ -103,7 +103,7 @@ module.exports = function(AccessToken) { var id = tokenIdForRequest(req, options); - if (id) { + if (id != null) { this.findById(id, function(err, token) { if (err) { cb(err); diff --git a/lib/model.js b/lib/model.js index a37dd40a4..89cd47c4d 100644 --- a/lib/model.js +++ b/lib/model.js @@ -161,13 +161,13 @@ module.exports = function(registry) { } } - if (id && data) { + if (id != null && data) { var model = new ModelCtor(data); model.id = id; fn(null, model); } else if (data) { fn(null, new ModelCtor(data)); - } else if (id) { + } else if (id != null) { var filter = {}; ModelCtor.findById(id, filter, options, function(err, model) { if (err) { diff --git a/lib/persisted-model.js b/lib/persisted-model.js index 65e32d934..fd7d9db11 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -1683,7 +1683,7 @@ module.exports = function(registry) { ctx.instance, ctx.currentInstance, ctx.where, ctx.data); } - if (id) { + if (id != null) { ctx.Model.rectifyChange(id, reportErrorAndNext); } else { ctx.Model.rectifyAllChanges(reportErrorAndNext); @@ -1707,7 +1707,7 @@ module.exports = function(registry) { debug('context instance:%j where:%j', ctx.instance, ctx.where); } - if (id) { + if (id != null) { ctx.Model.rectifyChange(id, reportErrorAndNext); } else { ctx.Model.rectifyAllChanges(reportErrorAndNext); From 7ddc0b14cf508222d03ff16c854bbede6af3b5e0 Mon Sep 17 00:00:00 2001 From: Taranveer Virk Date: Wed, 31 Jan 2018 14:40:37 -0500 Subject: [PATCH 169/187] update juggler dep --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 546a5576f..7bb42e1e4 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "underscore.string": "^3.0.3" }, "peerDependencies": { - "loopback-datasource-juggler": "^2.19.0" + "loopback-datasource-juggler": "^2.56.0" }, "devDependencies": { "bluebird": "^3.4.1", @@ -92,7 +92,7 @@ "karma-phantomjs-launcher": "^1.0.0", "karma-script-launcher": "^1.0.0", "loopback-boot": "^2.7.0", - "loopback-datasource-juggler": "^2.19.1", + "loopback-datasource-juggler": "^2.56.0", "loopback-testing": "^1.4.0", "mocha": "^3.0.0", "nyc": "^10.1.2", From c650b0db875c390a8888e252aa3823d22eb27876 Mon Sep 17 00:00:00 2001 From: Taranveer Virk Date: Wed, 31 Jan 2018 16:58:02 -0500 Subject: [PATCH 170/187] 2.39.1 * update juggler dep (Taranveer Virk) * fix(id): replace with != null (Samuel Reed) * fix(AccessContext): Tighten userid/appid checks (Samuel Reed) --- CHANGES.md | 10 ++++++++++ package.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index bcda27ab0..7a11d89c6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,13 @@ +2018-01-31, Version 2.39.1 +========================== + + * update juggler dep (Taranveer Virk) + + * fix(id): replace with != null (Samuel Reed) + + * fix(AccessContext): Tighten userid/appid checks (Samuel Reed) + + 2017-10-23, Version 2.39.0 ========================== diff --git a/package.json b/package.json index 7bb42e1e4..55814c90b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.39.0", + "version": "2.39.1", "publishConfig": { "tag": "lts" }, From b2cf877d14c958fdd554a2fa1cbb86537f2f76cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 1 Feb 2017 11:50:12 +0100 Subject: [PATCH 171/187] Fix Karma config to babelify node_modules too Before this change, dependencies in node_modules (e.g. strong-remoting) were not transformed to ES5 and thus crashed the tests in PhantomJS. Note that loopback-datasource-juggler cannot be babelified to ES5 because it does not correctly support strict mode yet. --- test/karma.conf.js | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/test/karma.conf.js b/test/karma.conf.js index 9c1110258..00d95b7d4 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -102,7 +102,28 @@ module.exports = function(config) { 'passport', 'passport-local', 'superagent', - 'supertest' + 'supertest', + ], + transform: [ + ['babelify', { + presets: 'es2015', + // By default, browserify does not transform node_modules + // As a result, our dependencies like strong-remoting and juggler + // are kept in original ES6 form that does not work in PhantomJS + global: true, + // Prevent SyntaxError in strong-task-emitter: + // strong-task-emitter/lib/task.js (83:4): + // arguments is a reserved word in strict mode + // Prevent TypeError in chai: + // 'caller', 'callee', and 'arguments' properties may not be + // accessed on strict mode functions or the arguments objects + // for calls to them + // Prevent TypeError in loopback-datasource-juggler: + // 'caller', 'callee', and 'arguments' properties may not be + // accessed on strict mode functions or the arguments objects + // for calls to them + ignore: /node_modules\/(strong-task-emitter|chai|loopback-datasource-juggler)\//, + }], ], // transform: ['coffeeify'], debug: true, @@ -111,6 +132,6 @@ module.exports = function(config) { }, // Add browserify to preprocessors - preprocessors: {'test/*': ['browserify']} + preprocessors: {'test/**/*.js': ['browserify']}, }); }; From 1575becb92367c2424bbea3b45d1771780024887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 1 Feb 2017 14:00:43 +0100 Subject: [PATCH 172/187] Babelify juggler for Karma tests Fix configuration of Karma: - Disable ES6 modules. The ES6 module transpiler is adding "use strict" to all source files, this breaks e.g. chai or juggler - Relax "ignore" setting to exclude only strong-task-emitter, thus bring back Babel transpilation for chai and juggler. --- package.json | 4 +++- test/karma.conf.js | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 55814c90b..cfde062a0 100644 --- a/package.json +++ b/package.json @@ -67,11 +67,13 @@ "loopback-datasource-juggler": "^2.56.0" }, "devDependencies": { + "babel-preset-es2015": "^6.24.1", + "babelify": "^7.3.0", "bluebird": "^3.4.1", "browserify": "^13.1.0", "chai": "^3.5.0", - "es5-shim": "^4.1.0", "coveralls": "^2.11.15", + "es5-shim": "^4.1.0", "express-session": "^1.14.0", "grunt": "^1.0.1", "grunt-browserify": "^5.0.0", diff --git a/test/karma.conf.js b/test/karma.conf.js index 00d95b7d4..706ffa3cb 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -106,7 +106,15 @@ module.exports = function(config) { ], transform: [ ['babelify', { - presets: 'es2015', + presets: [ + ['es2015', { + // Disable transform-es2015-modules-commonjs which adds + // "use strict" to all files, even those that don't work + // in strict mode + // (e.g. chai, loopback-datasource-juggler, etc.) + modules: false, + }], + ], // By default, browserify does not transform node_modules // As a result, our dependencies like strong-remoting and juggler // are kept in original ES6 form that does not work in PhantomJS @@ -114,15 +122,7 @@ module.exports = function(config) { // Prevent SyntaxError in strong-task-emitter: // strong-task-emitter/lib/task.js (83:4): // arguments is a reserved word in strict mode - // Prevent TypeError in chai: - // 'caller', 'callee', and 'arguments' properties may not be - // accessed on strict mode functions or the arguments objects - // for calls to them - // Prevent TypeError in loopback-datasource-juggler: - // 'caller', 'callee', and 'arguments' properties may not be - // accessed on strict mode functions or the arguments objects - // for calls to them - ignore: /node_modules\/(strong-task-emitter|chai|loopback-datasource-juggler)\//, + ignore: /node_modules\/(strong-task-emitter)\//, }], ], // transform: ['coffeeify'], From 8be91b81291eca83fb2e73c893d60095b009bfef Mon Sep 17 00:00:00 2001 From: Kevin Delisle Date: Mon, 12 Feb 2018 12:03:02 -0500 Subject: [PATCH 173/187] 2.39.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Babelify juggler for Karma tests (Miroslav Bajtoš) * Fix Karma config to babelify node_modules too (Miroslav Bajtoš) --- CHANGES.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 7a11d89c6..05c84132f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,11 @@ +2018-02-12, Version 2.39.2 +========================== + + * Babelify juggler for Karma tests (Miroslav Bajtoš) + + * Fix Karma config to babelify node_modules too (Miroslav Bajtoš) + + 2018-01-31, Version 2.39.1 ========================== diff --git a/package.json b/package.json index cfde062a0..7dae4d6c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.39.1", + "version": "2.39.2", "publishConfig": { "tag": "lts" }, From c36f9e88a372dbde10a6fe51a9394dde3bba6ab3 Mon Sep 17 00:00:00 2001 From: virkt25 Date: Wed, 8 Aug 2018 16:13:07 -0400 Subject: [PATCH 174/187] fix: accessToken create default acl --- common/models/access-token.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/common/models/access-token.json b/common/models/access-token.json index a5f360c46..b7ee32f5a 100644 --- a/common/models/access-token.json +++ b/common/models/access-token.json @@ -27,12 +27,6 @@ "principalType": "ROLE", "principalId": "$everyone", "permission": "DENY" - }, - { - "principalType": "ROLE", - "principalId": "$everyone", - "property": "create", - "permission": "ALLOW" } ] } From b27971e07456e03a0cea13c19f88b5d27837a058 Mon Sep 17 00:00:00 2001 From: virkt25 Date: Wed, 8 Aug 2018 18:17:11 -0400 Subject: [PATCH 175/187] 2.40.0 * fix: accessToken create default acl (virkt25) --- CHANGES.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 05c84132f..f74158289 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +2018-08-08, Version 2.40.0 +========================== + + * fix: accessToken create default acl (virkt25) + + 2018-02-12, Version 2.39.2 ========================== diff --git a/package.json b/package.json index 7dae4d6c7..692d7ba40 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.39.2", + "version": "2.40.0", "publishConfig": { "tag": "lts" }, From 087dae6a13f5a874ce52b3c7ed3425a865d4e18f Mon Sep 17 00:00:00 2001 From: Diana Lau Date: Fri, 5 Oct 2018 22:36:09 -0400 Subject: [PATCH 176/187] Update LB2 LTS version --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 3ff4a2c3f..aaeccc06c 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,20 @@ StrongLoop provides a number of example applications that illustrate various key See [loopback-example](https://github.com/strongloop/loopback-example) for details. +## Module Long Term Support Policy + +LoopBack 2.x is now in maintenance LTS. + +This module adopts the [Module Long Term Support (LTS)](http://github.com/CloudNativeJS/ModuleLTS) policy, with the following End Of Life (EOL) dates: + +| Version | Status | Published | EOL | +| ---------- | --------------- | --------- | -------------------- | +| LoopBack 4 | Current | Oct 2018 | Apr 2021 _(minimum)_ | +| Loopback 3 | Active LTS | Dec 2016 | Dec 2019 | +| Loopback 2 | Maintenance LTS | Jul 2014 | Apr 2019 | + +Learn more about our LTS plan in [docs](https://loopback.io/doc/en/contrib/Long-term-support.html). + ## Resources * [Documentation](http://docs.strongloop.com/display/LB/LoopBack). From 43a1f537db1b82ba42d84fa884aa70e3ca9ce17b Mon Sep 17 00:00:00 2001 From: Diana Lau Date: Tue, 9 Oct 2018 16:06:34 -0700 Subject: [PATCH 177/187] 2.41.0 * Update LB2 LTS version (Diana Lau) --- CHANGES.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index f74158289..0498af975 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +2018-10-09, Version 2.41.0 +========================== + + * Update LB2 LTS version (Diana Lau) + + 2018-08-08, Version 2.40.0 ========================== diff --git a/package.json b/package.json index 692d7ba40..47f49b350 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.40.0", + "version": "2.41.0", "publishConfig": { "tag": "lts" }, From 228bc7519b1ef06124d2b4d2dc4e2d28700c9ef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 15 Oct 2018 13:30:20 +0200 Subject: [PATCH 178/187] Fix context propagation broken by async@2.x Rework the REST middleware to use a hand-written version of "async.eachSeries". Before this change, we were loosing CLS context when the application was relying on the REST middleware to load the context middleware. This is fixing a problem introduced by post-1.0 versions of async, which we upgraded to via fea3b781a. --- server/middleware/rest.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/server/middleware/rest.js b/server/middleware/rest.js index 2ec5a200d..f1eb3ecec 100644 --- a/server/middleware/rest.js +++ b/server/middleware/rest.js @@ -73,8 +73,21 @@ function rest() { if (handlers.length === 1) { return handlers[0](req, res, next); } - async.eachSeries(handlers, function(handler, done) { - handler(req, res, done); - }, next); + + executeHandlers(handlers, req, res, next); }; } + +// A trimmed-down version of async.series that preserves current CLS context +function executeHandlers(handlers, req, res, cb) { + var ix = -1; + next(); + + function next(err) { + if (err || ++ix >= handlers.length) { + cb(err); + } else { + handlers[ix](req, res, next); + } + } +} From 21e69f0c14a91171f713768e6321f59a834b75b6 Mon Sep 17 00:00:00 2001 From: andrey-abramow Date: Fri, 23 Nov 2018 18:11:27 +0200 Subject: [PATCH 179/187] Fix: treat empty access token string as undefined Fix AccessToken's method tokenIdForRequest to treat an empty string as if no access token was provided. This is needed to accomodate the changes made in loopback-datasource-juggler@2.56.0. --- common/models/access-token.js | 5 +++++ test/access-token.test.js | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/common/models/access-token.js b/common/models/access-token.js index 860ef53ec..38aad7856 100644 --- a/common/models/access-token.js +++ b/common/models/access-token.js @@ -209,6 +209,11 @@ module.exports = function(AccessToken) { if (typeof id === 'string') { // Add support for oAuth 2.0 bearer token // http://tools.ietf.org/html/rfc6750 + + // To prevent Error: Model::findById requires the id argument + // with loopback-datasource-juggler 2.56.0+ + if (id === '') continue; + if (id.indexOf('Bearer ') === 0) { id = id.substring(7); // Decode from base64 diff --git a/test/access-token.test.js b/test/access-token.test.js index 7e4363eb9..d12f17abe 100644 --- a/test/access-token.test.js +++ b/test/access-token.test.js @@ -200,6 +200,16 @@ describe('loopback.token(options)', function() { .end(done); }); + it('should generate a 401 on a current user literal route with empty authToken', + function(done) { + var app = createTestApp(null, done); + request(app) + .get('/users/me') + .set('authorization', '') + .expect(401) + .end(done); + }); + it('should generate a 401 on a current user literal route with invalid authToken', function(done) { var app = createTestApp(this.token, done); From 6638992b99b3186001b37f1750356ded553c5789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 26 Nov 2018 11:14:37 +0100 Subject: [PATCH 180/187] 2.41.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: treat empty access token string as undefined (andrey-abramow) * Fix context propagation broken by async@2.x (Miroslav Bajtoš) --- CHANGES.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 0498af975..6dad02541 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,11 @@ +2018-11-26, Version 2.41.1 +========================== + + * Fix: treat empty access token string as undefined (andrey-abramow) + + * Fix context propagation broken by async@2.x (Miroslav Bajtoš) + + 2018-10-09, Version 2.41.0 ========================== diff --git a/package.json b/package.json index 47f49b350..3bccbf4ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.41.0", + "version": "2.41.1", "publishConfig": { "tag": "lts" }, From 2a78d953d961bec0591dc64712d13355b005dac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 26 Nov 2018 11:07:21 +0100 Subject: [PATCH 181/187] test: improve assertion error messages --- test/user.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/user.test.js b/test/user.test.js index 522512cbb..997030f53 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -1520,7 +1520,7 @@ describe('User', function() { assert(result.token); assert.equal(result.token, 'token-123456'); var msg = result.email.response.toString('utf-8'); - assert(~msg.indexOf('token-123456')); + expect(msg).to.contain('token-123456'); done(); }); From 9647294f7c4ed20f0dbbe0c2b607c128abd5b075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 26 Nov 2018 11:22:53 +0100 Subject: [PATCH 182/187] test: fix User test for custom token generator --- test/user.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/user.test.js b/test/user.test.js index 997030f53..4d7f2bfc4 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -1501,7 +1501,7 @@ describe('User', function() { from: 'noreply@myapp.org', redirect: '/', protocol: ctx.req.protocol, - host: ctx.req.get('host'), + host: 'lb.io', // use a short hostname to avoid a line break generateVerificationToken: function(user, cb) { assert(user); assert.equal(user.email, 'bar@bat.com'); From d4e8116023cdb825e9c954dd72791843382ac98c Mon Sep 17 00:00:00 2001 From: Matheus Horstmann Date: Tue, 8 Jan 2019 11:51:21 -0200 Subject: [PATCH 183/187] Fix crash when modifying an unknown user MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matheus Horstmann Signed-off-by: Miroslav Bajtoš --- common/models/user.js | 9 ++++++++- test/user.integration.js | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/common/models/user.js b/common/models/user.js index d44ccf35c..dca6a3c08 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -897,7 +897,14 @@ module.exports = function(User) { }); var emailChanged; if (ctx.instance) { - emailChanged = ctx.instance.email !== ctx.hookState.originalUserData[0].email; + // Check if map does not return an empty array + // Fix server crashes when try to PUT a non existent id + if (ctx.hookState.originalUserData.length > 0) { + emailChanged = ctx.instance.email !== ctx.hookState.originalUserData[0].email; + } else { + emailChanged = true; + } + if (emailChanged && ctx.Model.settings.emailVerificationRequired) { ctx.instance.emailVerified = false; } diff --git a/test/user.integration.js b/test/user.integration.js index a27b4ed4b..398f57790 100644 --- a/test/user.integration.js +++ b/test/user.integration.js @@ -69,6 +69,44 @@ describe('users - integration', function() { }); }); + it('returns error when replacing user that does not exist', function() { + var self = this; + var credentials = {email: 'temp@example.com', password: 'pass'}; + var User = app.models.User; + var user; + + // verify that logoutSessionsOnSensitiveChanges is enabled, + // otherwise this test always passes + expect(app.get('logoutSessionsOnSensitiveChanges')).to.equal(true); + + var hookEnabled = true; + User.beforeRemote('replaceOrCreate', function(ctx, unused, next) { + // don't affect subsequent tests! + if (!hookEnabled) return; + hookEnabled = false; + + // Delete the user *AFTER* the PUT request was authorized + // but *BEFORE* replaceOrCreate is invoked + User.deleteById(user.id, next); + }); + + return User.create(credentials) + .then(function(u) { + user = u; + return User.login(credentials); + }) + .then(function(token) { + return self.post('/api/users/replaceOrCreate') + .set('Authorization', token.id) + .send({ + id: user.id, + email: 'x@x.com', + password: 'x', + }) + .expect(200); + }); + }); + it('should create post for a given user', function(done) { var url = '/api/users/' + userId + '/posts?access_token=' + accessToken; this.post(url) From 85b076c9b9a0e840036ce4380eb4e535553059fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 11 Jan 2019 16:46:32 +0100 Subject: [PATCH 184/187] 2.41.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix crash when modifying an unknown user (Matheus Horstmann) * test: fix User test for custom token generator (Miroslav Bajtoš) * test: improve assertion error messages (Miroslav Bajtoš) --- CHANGES.md | 10 ++++++++++ package.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 6dad02541..4dee63f51 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,13 @@ +2019-01-11, Version 2.41.2 +========================== + + * Fix crash when modifying an unknown user (Matheus Horstmann) + + * test: fix User test for custom token generator (Miroslav Bajtoš) + + * test: improve assertion error messages (Miroslav Bajtoš) + + 2018-11-26, Version 2.41.1 ========================== diff --git a/package.json b/package.json index 3bccbf4ca..d2844e32e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.41.1", + "version": "2.41.2", "publishConfig": { "tag": "lts" }, From d413c4e215dcec3781a801d082c229ff2d640395 Mon Sep 17 00:00:00 2001 From: Diana Lau Date: Mon, 29 Apr 2019 19:27:57 -0400 Subject: [PATCH 185/187] update README for 2.x EOL --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index aaeccc06c..54ff6c343 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/strongloop/loopback?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +**LoopBack 2.x has reached end of life in April 2019. It is no longer supported.** + LoopBack is a highly-extensible, open-source Node.js framework that enables you to: * Create dynamic end-to-end REST APIs with little or no coding. @@ -75,15 +77,15 @@ See [loopback-example](https://github.com/strongloop/loopback-example) for detai ## Module Long Term Support Policy -LoopBack 2.x is now in maintenance LTS. +LoopBack 2.x has reached end of life in April 2019. It is no longer supported. This module adopts the [Module Long Term Support (LTS)](http://github.com/CloudNativeJS/ModuleLTS) policy, with the following End Of Life (EOL) dates: | Version | Status | Published | EOL | | ---------- | --------------- | --------- | -------------------- | | LoopBack 4 | Current | Oct 2018 | Apr 2021 _(minimum)_ | -| Loopback 3 | Active LTS | Dec 2016 | Dec 2019 | -| Loopback 2 | Maintenance LTS | Jul 2014 | Apr 2019 | +| LoopBack 3 | Maintenance LTS | Dec 2016 | Dec 2020 | +| LoopBack 2 | End-of-Life | Jul 2014 | Apr 2019 | Learn more about our LTS plan in [docs](https://loopback.io/doc/en/contrib/Long-term-support.html). From 2dd98a368b719e85644c7cd901694ac38393d808 Mon Sep 17 00:00:00 2001 From: Hage Yaapa Date: Wed, 29 May 2019 20:24:29 +0530 Subject: [PATCH 186/187] fix: disallow queries in username and email fields Username and email fields should not allow queries. --- common/models/user.js | 33 +++++++++++++++++++++++++++------ test/user.test.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index dca6a3c08..73bbda792 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -208,12 +208,20 @@ module.exports = function(User) { var query = self.normalizeCredentials(credentials, realmRequired, realmDelimiter); - if (realmRequired && !query.realm) { - var err1 = new Error(g.f('{{realm}} is required')); - err1.statusCode = 400; - err1.code = 'REALM_REQUIRED'; - fn(err1); - return fn.promise; + if (realmRequired) { + if (!query.realm) { + var err1 = new Error(g.f('{{realm}} is required')); + err1.statusCode = 400; + err1.code = 'REALM_REQUIRED'; + fn(err1); + return fn.promise; + } else if (typeof query.realm !== 'string') { + var err5 = new Error(g.f('Invalid realm')); + err5.statusCode = 400; + err5.code = 'INVALID_REALM'; + fn(err5); + return fn.promise; + } } if (!query.email && !query.username) { var err2 = new Error(g.f('{{username}} or {{email}} is required')); @@ -222,6 +230,19 @@ module.exports = function(User) { fn(err2); return fn.promise; } + if (query.username && typeof query.username !== 'string') { + var err3 = new Error(g.f('Invalid username')); + err3.statusCode = 400; + err3.code = 'INVALID_USERNAME'; + fn(err3); + return fn.promise; + } else if (query.email && typeof query.email !== 'string') { + var err4 = new Error(g.f('Invalid email')); + err4.statusCode = 400; + err4.code = 'INVALID_EMAIL'; + fn(err4); + return fn.promise; + } self.findOne({where: query}, function(err, user) { var defaultError = new Error(g.f('login failed')); diff --git a/test/user.test.js b/test/user.test.js index 4d7f2bfc4..f562fef0d 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -555,6 +555,37 @@ describe('User', function() { }); }); + it('should not allow queries in email field', function(done) { + User.login({email: {'neq': 'x'}, password: 'x'}, function(err, accessToken) { + assert(err); + assert.equal(err.code, 'INVALID_EMAIL'); + assert(!accessToken); + + done(); + }); + }); + + it('should not allow queries in username field', function(done) { + User.login({username: {'neq': 'x'}, password: 'x'}, function(err, accessToken) { + assert(err); + assert.equal(err.code, 'INVALID_USERNAME'); + assert(!accessToken); + + done(); + }); + }); + + it('should not allow queries in realm field', function(done) { + User.settings.realmRequired = true; + User.login({username: 'x', password: 'x', realm: {'neq': 'x'}}, function(err, accessToken) { + assert(err); + assert.equal(err.code, 'INVALID_REALM'); + assert(!accessToken); + + done(); + }); + }); + it('Login a user by providing credentials with TTL', function(done) { User.login(validCredentialsWithTTL, function(err, accessToken) { assert(accessToken.userId); From 80cb175105ee542334cda0c0ce881cf9d454d2b0 Mon Sep 17 00:00:00 2001 From: jannyHou Date: Tue, 4 Jun 2019 13:45:02 -0400 Subject: [PATCH 187/187] 2.42.0 * fix: disallow queries in username and email fields (Hage Yaapa) * update README for 2.x EOL (Diana Lau) --- CHANGES.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 4dee63f51..54c931e59 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,11 @@ +2019-06-04, Version 2.42.0 +========================== + + * fix: disallow queries in username and email fields (Hage Yaapa) + + * update README for 2.x EOL (Diana Lau) + + 2019-01-11, Version 2.41.2 ========================== diff --git a/package.json b/package.json index d2844e32e..941c37598 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.41.2", + "version": "2.42.0", "publishConfig": { "tag": "lts" },