@vsaas/loopback 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +25 -0
- package/README.md +91 -0
- package/common/models/README.md +109 -0
- package/common/models/access-token.json +37 -0
- package/common/models/acl.json +17 -0
- package/common/models/application.json +130 -0
- package/common/models/change.json +25 -0
- package/common/models/checkpoint.json +14 -0
- package/common/models/email.json +11 -0
- package/common/models/key-value-model.json +4 -0
- package/common/models/role-mapping.json +26 -0
- package/common/models/role.json +30 -0
- package/common/models/scope.json +14 -0
- package/common/models/user.json +118 -0
- package/dist/_virtual/_rolldown/runtime.cjs +32 -0
- package/dist/common/models/access-token.cjs +144 -0
- package/dist/common/models/access-token2.cjs +43 -0
- package/dist/common/models/acl.cjs +428 -0
- package/dist/common/models/acl2.cjs +27 -0
- package/dist/common/models/application.cjs +100 -0
- package/dist/common/models/application2.cjs +118 -0
- package/dist/common/models/change.cjs +404 -0
- package/dist/common/models/change2.cjs +25 -0
- package/dist/common/models/checkpoint.cjs +43 -0
- package/dist/common/models/checkpoint2.cjs +18 -0
- package/dist/common/models/email.cjs +18 -0
- package/dist/common/models/email2.cjs +30 -0
- package/dist/common/models/key-value-model.cjs +140 -0
- package/dist/common/models/key-value-model2.cjs +14 -0
- package/dist/common/models/role-mapping.cjs +57 -0
- package/dist/common/models/role-mapping2.cjs +34 -0
- package/dist/common/models/role.cjs +396 -0
- package/dist/common/models/role2.cjs +38 -0
- package/dist/common/models/scope.cjs +30 -0
- package/dist/common/models/scope2.cjs +21 -0
- package/dist/common/models/user.cjs +810 -0
- package/dist/common/models/user2.cjs +118 -0
- package/dist/index.cjs +16 -0
- package/dist/lib/access-context.cjs +228 -0
- package/dist/lib/application.cjs +450 -0
- package/dist/lib/builtin-models.cjs +60 -0
- package/dist/lib/configure-shared-methods.cjs +41 -0
- package/dist/lib/connectors/base-connector.cjs +23 -0
- package/dist/lib/connectors/mail-direct-transport.cjs +375 -0
- package/dist/lib/connectors/mail-stub-transport.cjs +86 -0
- package/dist/lib/connectors/mail.cjs +128 -0
- package/dist/lib/connectors/memory.cjs +19 -0
- package/dist/lib/current-context.cjs +22 -0
- package/dist/lib/globalize.cjs +29 -0
- package/dist/lib/loopback.cjs +313 -0
- package/dist/lib/model.cjs +1009 -0
- package/dist/lib/persisted-model.cjs +1835 -0
- package/dist/lib/registry.cjs +291 -0
- package/dist/lib/runtime.cjs +25 -0
- package/dist/lib/server-app.cjs +231 -0
- package/dist/lib/utils.cjs +154 -0
- package/dist/package.cjs +124 -0
- package/dist/server/middleware/context.cjs +7 -0
- package/dist/server/middleware/error-handler.cjs +6 -0
- package/dist/server/middleware/favicon.cjs +13 -0
- package/dist/server/middleware/rest.cjs +44 -0
- package/dist/server/middleware/static.cjs +14 -0
- package/dist/server/middleware/status.cjs +28 -0
- package/dist/server/middleware/token.cjs +66 -0
- package/dist/server/middleware/url-not-found.cjs +20 -0
- package/favicon.ico +0 -0
- package/package.json +121 -0
- package/templates/reset-form.ejs +3 -0
- package/templates/verify.ejs +9 -0
|
@@ -0,0 +1,1835 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const require_runtime$1 = require("../_virtual/_rolldown/runtime.cjs");
|
|
3
|
+
const require_lib_globalize = require("./globalize.cjs");
|
|
4
|
+
const require_lib_runtime = require("./runtime.cjs");
|
|
5
|
+
const require_lib_utils = require("./utils.cjs");
|
|
6
|
+
//#region src/lib/persisted-model.ts
|
|
7
|
+
/*!
|
|
8
|
+
* Module Dependencies.
|
|
9
|
+
*/
|
|
10
|
+
var require_persisted_model = /* @__PURE__ */ require_runtime$1.__commonJSMin(((exports, module) => {
|
|
11
|
+
const g = require_lib_globalize;
|
|
12
|
+
const runtime = require_lib_runtime;
|
|
13
|
+
const assert = require("assert");
|
|
14
|
+
const deprecated = require("depd")("loopback");
|
|
15
|
+
const debug = require("debug")("loopback:persisted-model");
|
|
16
|
+
const PassThrough = require("stream").PassThrough;
|
|
17
|
+
const utils = require_lib_utils;
|
|
18
|
+
const filterNodes = require("loopback-filters");
|
|
19
|
+
const REPLICATION_CHUNK_SIZE = -1;
|
|
20
|
+
module.exports = function(registry) {
|
|
21
|
+
const Model = registry.getModel("Model");
|
|
22
|
+
/**
|
|
23
|
+
* Extends Model with basic query and CRUD support.
|
|
24
|
+
*
|
|
25
|
+
* **Change Event**
|
|
26
|
+
*
|
|
27
|
+
* Listen for model changes using the `change` event.
|
|
28
|
+
*
|
|
29
|
+
* ```js
|
|
30
|
+
* MyPersistedModel.on('changed', function(obj) {
|
|
31
|
+
* console.log(obj) // => the changed model
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @class PersistedModel
|
|
36
|
+
*/
|
|
37
|
+
const PersistedModel = Model.extend("PersistedModel");
|
|
38
|
+
/*!
|
|
39
|
+
* Setup the `PersistedModel` constructor.
|
|
40
|
+
*/
|
|
41
|
+
PersistedModel.setup = function setupPersistedModel() {
|
|
42
|
+
Model.setup.call(this);
|
|
43
|
+
const PersistedModel = this;
|
|
44
|
+
if (this.settings.trackChanges) {
|
|
45
|
+
PersistedModel._defineChangeModel();
|
|
46
|
+
PersistedModel.once("dataSourceAttached", function() {
|
|
47
|
+
PersistedModel.enableChangeTracking();
|
|
48
|
+
});
|
|
49
|
+
} else if (this.settings.enableRemoteReplication) PersistedModel._defineChangeModel();
|
|
50
|
+
PersistedModel.setupRemoting();
|
|
51
|
+
};
|
|
52
|
+
/*!
|
|
53
|
+
* Throw an error telling the user that the method is not available and why.
|
|
54
|
+
*/
|
|
55
|
+
function throwNotAttached(modelName, methodName) {
|
|
56
|
+
throw new Error(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));
|
|
57
|
+
}
|
|
58
|
+
/*!
|
|
59
|
+
* Convert null callbacks to 404 error objects.
|
|
60
|
+
* @param {HttpContext} ctx
|
|
61
|
+
* @param {Function} cb
|
|
62
|
+
*/
|
|
63
|
+
function convertNullToNotFoundError(ctx, cb) {
|
|
64
|
+
if (ctx.result !== null) return cb();
|
|
65
|
+
const modelName = ctx.method.sharedClass.name;
|
|
66
|
+
const id = ctx.getArgByName("id");
|
|
67
|
+
const msg = g.f("Unknown \"%s\" {{id}} \"%s\".", modelName, id);
|
|
68
|
+
const error = new Error(msg);
|
|
69
|
+
error.statusCode = error.status = 404;
|
|
70
|
+
error.code = "MODEL_NOT_FOUND";
|
|
71
|
+
cb(error);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Create new instance of Model, and save to database.
|
|
75
|
+
*
|
|
76
|
+
* @param {Object|Object[]} [data] Optional data argument. Can be either a single model instance or an array of instances.
|
|
77
|
+
*
|
|
78
|
+
* @callback {Function} callback Callback function called with `cb(err, obj)` signature.
|
|
79
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
80
|
+
* @param {Object} models Model instances or null.
|
|
81
|
+
*/
|
|
82
|
+
PersistedModel.create = function(data, callback) {
|
|
83
|
+
throwNotAttached(this.modelName, "create");
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Update or insert a model instance
|
|
87
|
+
* @param {Object} data The model instance data to insert.
|
|
88
|
+
* @callback {Function} callback Callback function called with `cb(err, obj)` signature.
|
|
89
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
90
|
+
* @param {Object} model Updated model instance.
|
|
91
|
+
*/
|
|
92
|
+
PersistedModel.upsert = PersistedModel.updateOrCreate = PersistedModel.patchOrCreate = function upsert(data, callback) {
|
|
93
|
+
throwNotAttached(this.modelName, "upsert");
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Update or insert a model instance based on the search criteria.
|
|
97
|
+
* If there is a single instance retrieved, update the retrieved model.
|
|
98
|
+
* Creates a new model if no model instances were found.
|
|
99
|
+
* Returns an error if multiple instances are found.
|
|
100
|
+
* @param {Object} [where] `where` filter, like
|
|
101
|
+
* ```
|
|
102
|
+
* { key: val, key2: {gt: 'val2'}, ...}
|
|
103
|
+
* ```
|
|
104
|
+
* <br/>see
|
|
105
|
+
* [Where filter](http://loopback.io/doc/en/lb2/Where-filter.html#where-clause-for-other-methods).
|
|
106
|
+
* @param {Object} data The model instance data to insert.
|
|
107
|
+
* @callback {Function} callback Callback function called with `cb(err, obj)` signature.
|
|
108
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
109
|
+
* @param {Object} model Updated model instance.
|
|
110
|
+
*/
|
|
111
|
+
PersistedModel.upsertWithWhere = PersistedModel.patchOrCreateWithWhere = function upsertWithWhere(where, data, callback) {
|
|
112
|
+
throwNotAttached(this.modelName, "upsertWithWhere");
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Replace or insert a model instance; replace existing record if one is found,
|
|
116
|
+
* such that parameter `data.id` matches `id` of model instance; otherwise,
|
|
117
|
+
* insert a new record.
|
|
118
|
+
* @param {Object} data The model instance data.
|
|
119
|
+
* @options {Object} [options] Options for replaceOrCreate
|
|
120
|
+
* @property {Boolean} validate Perform validation before saving. Default is true.
|
|
121
|
+
* @callback {Function} callback Callback function called with `cb(err, obj)` signature.
|
|
122
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
123
|
+
* @param {Object} model Replaced model instance.
|
|
124
|
+
*/
|
|
125
|
+
PersistedModel.replaceOrCreate = function replaceOrCreate(data, callback) {
|
|
126
|
+
throwNotAttached(this.modelName, "replaceOrCreate");
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Finds one record matching the optional filter object. If not found, creates
|
|
130
|
+
* the object using the data provided as second argument. In this sense it is
|
|
131
|
+
* the same as `find`, but limited to one object. Returns an object, not
|
|
132
|
+
* collection. If you don't provide the filter object argument, it tries to
|
|
133
|
+
* locate an existing object that matches the `data` argument.
|
|
134
|
+
*
|
|
135
|
+
* @options {Object} [filter] Optional Filter object; see below.
|
|
136
|
+
* @property {String|Object|Array} fields Identify fields to include in return result.
|
|
137
|
+
* <br/>See [Fields filter](http://loopback.io/doc/en/lb2/Fields-filter.html).
|
|
138
|
+
* @property {String|Object|Array} include See PersistedModel.include documentation.
|
|
139
|
+
* <br/>See [Include filter](http://loopback.io/doc/en/lb2/Include-filter.html).
|
|
140
|
+
* @property {Number} limit Maximum number of instances to return.
|
|
141
|
+
* <br/>See [Limit filter](http://loopback.io/doc/en/lb2/Limit-filter.html).
|
|
142
|
+
* @property {String} order Sort order: either "ASC" for ascending or "DESC" for descending.
|
|
143
|
+
* <br/>See [Order filter](http://loopback.io/doc/en/lb2/Order-filter.html).
|
|
144
|
+
* @property {Number} skip Number of results to skip.
|
|
145
|
+
* <br/>See [Skip filter](http://loopback.io/doc/en/lb2/Skip-filter.html).
|
|
146
|
+
* @property {Object} where Where clause, like
|
|
147
|
+
* ```
|
|
148
|
+
* {where: {key: val, key2: {gt: val2}, ...}}
|
|
149
|
+
* ```
|
|
150
|
+
* <br/>See
|
|
151
|
+
* [Where filter](http://loopback.io/doc/en/lb2/Where-filter.html#where-clause-for-queries).
|
|
152
|
+
* @param {Object} data Data to insert if object matching the `where` filter is not found.
|
|
153
|
+
* @callback {Function} callback Callback function called with `cb(err, instance, created)` arguments. Required.
|
|
154
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
155
|
+
* @param {Object} instance Model instance matching the `where` filter, if found.
|
|
156
|
+
* @param {Boolean} created True if the instance does not exist and gets created.
|
|
157
|
+
*/
|
|
158
|
+
PersistedModel.findOrCreate = function findOrCreate(query, data, callback) {
|
|
159
|
+
throwNotAttached(this.modelName, "findOrCreate");
|
|
160
|
+
};
|
|
161
|
+
PersistedModel.findOrCreate._delegate = true;
|
|
162
|
+
/**
|
|
163
|
+
* Check whether a model instance exists in database.
|
|
164
|
+
*
|
|
165
|
+
* @param {id} id Identifier of object (primary key value).
|
|
166
|
+
*
|
|
167
|
+
* @callback {Function} callback Callback function called with `(err, exists)` arguments. Required.
|
|
168
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
169
|
+
* @param {Boolean} exists True if the instance with the specified ID exists; false otherwise.
|
|
170
|
+
*/
|
|
171
|
+
PersistedModel.exists = function exists(id, cb) {
|
|
172
|
+
throwNotAttached(this.modelName, "exists");
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Find object by ID with an optional filter for include/fields.
|
|
176
|
+
*
|
|
177
|
+
* @param {*} id Primary key value
|
|
178
|
+
* @options {Object} [filter] Optional Filter JSON object; see below.
|
|
179
|
+
* @property {String|Object|Array} fields Identify fields to include in return result.
|
|
180
|
+
* <br/>See [Fields filter](http://loopback.io/doc/en/lb2/Fields-filter.html).
|
|
181
|
+
* @property {String|Object|Array} include See PersistedModel.include documentation.
|
|
182
|
+
* <br/>See [Include filter](http://loopback.io/doc/en/lb2/Include-filter.html).
|
|
183
|
+
* @callback {Function} callback Callback function called with `(err, instance)` arguments. Required.
|
|
184
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
185
|
+
* @param {Object} instance Model instance matching the specified ID or null if no instance matches.
|
|
186
|
+
*/
|
|
187
|
+
PersistedModel.findById = function findById(id, filter, cb) {
|
|
188
|
+
throwNotAttached(this.modelName, "findById");
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* Find all model instances that match `filter` specification.
|
|
192
|
+
* See [Querying models](http://loopback.io/doc/en/lb2/Querying-data.html).
|
|
193
|
+
*
|
|
194
|
+
* @options {Object} [filter] Optional Filter JSON object; see below.
|
|
195
|
+
* @property {String|Object|Array} fields Identify fields to include in return result.
|
|
196
|
+
* <br/>See [Fields filter](http://loopback.io/doc/en/lb2/Fields-filter.html).
|
|
197
|
+
* @property {String|Object|Array} include See PersistedModel.include documentation.
|
|
198
|
+
* <br/>See [Include filter](http://loopback.io/doc/en/lb2/Include-filter.html).
|
|
199
|
+
* @property {Number} limit Maximum number of instances to return.
|
|
200
|
+
* <br/>See [Limit filter](http://loopback.io/doc/en/lb2/Limit-filter.html).
|
|
201
|
+
* @property {String} order Sort order: either "ASC" for ascending or "DESC" for descending.
|
|
202
|
+
* <br/>See [Order filter](http://loopback.io/doc/en/lb2/Order-filter.html).
|
|
203
|
+
* @property {Number} skip Number of results to skip.
|
|
204
|
+
* <br/>See [Skip filter](http://loopback.io/doc/en/lb2/Skip-filter.html).
|
|
205
|
+
* @property {Object} where Where clause, like
|
|
206
|
+
* ```
|
|
207
|
+
* { where: { key: val, key2: {gt: 'val2'}, ...} }
|
|
208
|
+
* ```
|
|
209
|
+
* <br/>See
|
|
210
|
+
* [Where filter](http://loopback.io/doc/en/lb2/Where-filter.html#where-clause-for-queries).
|
|
211
|
+
*
|
|
212
|
+
* @callback {Function} callback Callback function called with `(err, returned-instances)` arguments. Required.
|
|
213
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
214
|
+
* @param {Array} models Model instances matching the filter, or null if none found.
|
|
215
|
+
*/
|
|
216
|
+
PersistedModel.find = function find(filter, cb) {
|
|
217
|
+
throwNotAttached(this.modelName, "find");
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* Find one model instance that matches `filter` specification.
|
|
221
|
+
* Same as `find`, but limited to one result;
|
|
222
|
+
* Returns object, not collection.
|
|
223
|
+
*
|
|
224
|
+
* @options {Object} [filter] Optional Filter JSON object; see below.
|
|
225
|
+
* @property {String|Object|Array} fields Identify fields to include in return result.
|
|
226
|
+
* <br/>See [Fields filter](http://loopback.io/doc/en/lb2/Fields-filter.html).
|
|
227
|
+
* @property {String|Object|Array} include See PersistedModel.include documentation.
|
|
228
|
+
* <br/>See [Include filter](http://loopback.io/doc/en/lb2/Include-filter.html).
|
|
229
|
+
* @property {String} order Sort order: either "ASC" for ascending or "DESC" for descending.
|
|
230
|
+
* <br/>See [Order filter](http://loopback.io/doc/en/lb2/Order-filter.html).
|
|
231
|
+
* @property {Number} skip Number of results to skip.
|
|
232
|
+
* <br/>See [Skip filter](http://loopback.io/doc/en/lb2/Skip-filter.html).
|
|
233
|
+
* @property {Object} where Where clause, like
|
|
234
|
+
* ```
|
|
235
|
+
* {where: { key: val, key2: {gt: 'val2'}, ...} }
|
|
236
|
+
* ```
|
|
237
|
+
* <br/>See
|
|
238
|
+
* [Where filter](http://loopback.io/doc/en/lb2/Where-filter.html#where-clause-for-queries).
|
|
239
|
+
*
|
|
240
|
+
* @callback {Function} callback Callback function called with `(err, returned-instance)` arguments. Required.
|
|
241
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
242
|
+
* @param {Array} model First model instance that matches the filter or null if none found.
|
|
243
|
+
*/
|
|
244
|
+
PersistedModel.findOne = function findOne(filter, cb) {
|
|
245
|
+
throwNotAttached(this.modelName, "findOne");
|
|
246
|
+
};
|
|
247
|
+
/**
|
|
248
|
+
* Destroy all model instances that match the optional `where` specification.
|
|
249
|
+
*
|
|
250
|
+
* @param {Object} [where] Optional where filter, like:
|
|
251
|
+
* ```
|
|
252
|
+
* {key: val, key2: {gt: 'val2'}, ...}
|
|
253
|
+
* ```
|
|
254
|
+
* <br/>See
|
|
255
|
+
* [Where filter](http://loopback.io/doc/en/lb2/Where-filter.html#where-clause-for-other-methods).
|
|
256
|
+
*
|
|
257
|
+
* @callback {Function} callback Optional callback function called with `(err, info)` arguments.
|
|
258
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
259
|
+
* @param {Object} info Additional information about the command outcome.
|
|
260
|
+
* @param {Number} info.count Number of instances (rows, documents) destroyed.
|
|
261
|
+
*/
|
|
262
|
+
PersistedModel.destroyAll = function destroyAll(where, cb) {
|
|
263
|
+
throwNotAttached(this.modelName, "destroyAll");
|
|
264
|
+
};
|
|
265
|
+
/**
|
|
266
|
+
* Alias for `destroyAll`
|
|
267
|
+
*/
|
|
268
|
+
PersistedModel.remove = PersistedModel.destroyAll;
|
|
269
|
+
/**
|
|
270
|
+
* Alias for `destroyAll`
|
|
271
|
+
*/
|
|
272
|
+
PersistedModel.deleteAll = PersistedModel.destroyAll;
|
|
273
|
+
/**
|
|
274
|
+
* Update multiple instances that match the where clause.
|
|
275
|
+
*
|
|
276
|
+
* Example:
|
|
277
|
+
*
|
|
278
|
+
*```js
|
|
279
|
+
* Employee.updateAll({managerId: 'x001'}, {managerId: 'x002'}, function(err, info) {
|
|
280
|
+
* ...
|
|
281
|
+
* });
|
|
282
|
+
* ```
|
|
283
|
+
*
|
|
284
|
+
* @param {Object} [where] Optional `where` filter, like
|
|
285
|
+
* ```
|
|
286
|
+
* { key: val, key2: {gt: 'val2'}, ...}
|
|
287
|
+
* ```
|
|
288
|
+
* <br/>see
|
|
289
|
+
* [Where filter](http://loopback.io/doc/en/lb2/Where-filter.html#where-clause-for-other-methods).
|
|
290
|
+
* @param {Object} data Object containing data to replace matching instances, if any.
|
|
291
|
+
*
|
|
292
|
+
* @callback {Function} callback Callback function called with `(err, info)` arguments. Required.
|
|
293
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
294
|
+
* @param {Object} info Additional information about the command outcome.
|
|
295
|
+
* @param {Number} info.count Number of instances (rows, documents) updated.
|
|
296
|
+
*
|
|
297
|
+
*/
|
|
298
|
+
PersistedModel.updateAll = function updateAll(where, data, cb) {
|
|
299
|
+
throwNotAttached(this.modelName, "updateAll");
|
|
300
|
+
};
|
|
301
|
+
/**
|
|
302
|
+
* Alias for updateAll.
|
|
303
|
+
*/
|
|
304
|
+
PersistedModel.update = PersistedModel.updateAll;
|
|
305
|
+
/**
|
|
306
|
+
* Destroy model instance with the specified ID.
|
|
307
|
+
* @param {*} id The ID value of model instance to delete.
|
|
308
|
+
* @callback {Function} callback Callback function called with `(err)` arguments. Required.
|
|
309
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
310
|
+
*/
|
|
311
|
+
PersistedModel.destroyById = function deleteById(id, cb) {
|
|
312
|
+
throwNotAttached(this.modelName, "deleteById");
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* Alias for destroyById.
|
|
316
|
+
*/
|
|
317
|
+
PersistedModel.removeById = PersistedModel.destroyById;
|
|
318
|
+
/**
|
|
319
|
+
* Alias for destroyById.
|
|
320
|
+
*/
|
|
321
|
+
PersistedModel.deleteById = PersistedModel.destroyById;
|
|
322
|
+
/**
|
|
323
|
+
* Return the number of records that match the optional "where" filter.
|
|
324
|
+
* @param {Object} [where] Optional where filter, like
|
|
325
|
+
* ```
|
|
326
|
+
* { key: val, key2: {gt: 'val2'}, ...}
|
|
327
|
+
* ```
|
|
328
|
+
* <br/>See
|
|
329
|
+
* [Where filter](http://loopback.io/doc/en/lb2/Where-filter.html#where-clause-for-other-methods).
|
|
330
|
+
* @callback {Function} callback Callback function called with `(err, count)` arguments. Required.
|
|
331
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
332
|
+
* @param {Number} count Number of instances.
|
|
333
|
+
*/
|
|
334
|
+
PersistedModel.count = function(where, cb) {
|
|
335
|
+
throwNotAttached(this.modelName, "count");
|
|
336
|
+
};
|
|
337
|
+
/**
|
|
338
|
+
* Save model instance. If the instance doesn't have an ID, then calls [create](#persistedmodelcreatedata-cb) instead.
|
|
339
|
+
* Triggers: validate, save, update, or create.
|
|
340
|
+
* @options {Object} [options] See below.
|
|
341
|
+
* @property {Boolean} validate Perform validation before saving. Default is true.
|
|
342
|
+
* @property {Boolean} throws If true, throw a validation error; WARNING: This can crash Node.
|
|
343
|
+
* If false, report the error via callback. Default is false.
|
|
344
|
+
* @callback {Function} callback Optional callback function called with `(err, obj)` arguments.
|
|
345
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
346
|
+
* @param {Object} instance Model instance saved or created.
|
|
347
|
+
*/
|
|
348
|
+
PersistedModel.prototype.save = function(options, callback) {
|
|
349
|
+
const Model = this.constructor;
|
|
350
|
+
if (typeof options == "function") {
|
|
351
|
+
callback = options;
|
|
352
|
+
options = {};
|
|
353
|
+
}
|
|
354
|
+
callback = callback || function() {};
|
|
355
|
+
options = options || {};
|
|
356
|
+
if (!("validate" in options)) options.validate = true;
|
|
357
|
+
if (!("throws" in options)) options.throws = false;
|
|
358
|
+
const inst = this;
|
|
359
|
+
const data = inst.toObject(true);
|
|
360
|
+
if (!this.getId()) return Model.create(this, callback);
|
|
361
|
+
if (!options.validate) return save();
|
|
362
|
+
inst.isValid(function(valid) {
|
|
363
|
+
if (valid) save();
|
|
364
|
+
else {
|
|
365
|
+
const err = new Model.ValidationError(inst);
|
|
366
|
+
if (options.throws) throw err;
|
|
367
|
+
callback(err, inst);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
function save() {
|
|
371
|
+
inst.trigger("save", function(saveDone) {
|
|
372
|
+
inst.trigger("update", function(updateDone) {
|
|
373
|
+
Model.upsert(inst, function(err) {
|
|
374
|
+
inst._initProperties(data);
|
|
375
|
+
updateDone.call(inst, function() {
|
|
376
|
+
saveDone.call(inst, function() {
|
|
377
|
+
callback(err, inst);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
}, data);
|
|
382
|
+
}, data);
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
/**
|
|
386
|
+
* Determine if the data model is new.
|
|
387
|
+
* @returns {Boolean} Returns true if the data model is new; false otherwise.
|
|
388
|
+
*/
|
|
389
|
+
PersistedModel.prototype.isNewRecord = function() {
|
|
390
|
+
throwNotAttached(this.constructor.modelName, "isNewRecord");
|
|
391
|
+
};
|
|
392
|
+
/**
|
|
393
|
+
* Deletes the model from persistence.
|
|
394
|
+
* Triggers `destroy` hook (async) before and after destroying object.
|
|
395
|
+
* @param {Function} callback Callback function.
|
|
396
|
+
*/
|
|
397
|
+
PersistedModel.prototype.destroy = function(cb) {
|
|
398
|
+
throwNotAttached(this.constructor.modelName, "destroy");
|
|
399
|
+
};
|
|
400
|
+
/**
|
|
401
|
+
* Alias for destroy.
|
|
402
|
+
* @header PersistedModel.remove
|
|
403
|
+
*/
|
|
404
|
+
PersistedModel.prototype.remove = PersistedModel.prototype.destroy;
|
|
405
|
+
/**
|
|
406
|
+
* Alias for destroy.
|
|
407
|
+
* @header PersistedModel.delete
|
|
408
|
+
*/
|
|
409
|
+
PersistedModel.prototype.delete = PersistedModel.prototype.destroy;
|
|
410
|
+
PersistedModel.prototype.destroy._delegate = true;
|
|
411
|
+
/**
|
|
412
|
+
* Update a single attribute.
|
|
413
|
+
* Equivalent to `updateAttributes({name: 'value'}, cb)`
|
|
414
|
+
*
|
|
415
|
+
* @param {String} name Name of property.
|
|
416
|
+
* @param {Mixed} value Value of property.
|
|
417
|
+
* @callback {Function} callback Callback function called with `(err, instance)` arguments. Required.
|
|
418
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
419
|
+
* @param {Object} instance Updated instance.
|
|
420
|
+
*/
|
|
421
|
+
PersistedModel.prototype.updateAttribute = function updateAttribute(name, value, callback) {
|
|
422
|
+
throwNotAttached(this.constructor.modelName, "updateAttribute");
|
|
423
|
+
};
|
|
424
|
+
/**
|
|
425
|
+
* Update set of attributes. Performs validation before updating.
|
|
426
|
+
*
|
|
427
|
+
* Triggers: `validation`, `save` and `update` hooks
|
|
428
|
+
* @param {Object} data Data to update.
|
|
429
|
+
* @callback {Function} callback Callback function called with `(err, instance)` arguments. Required.
|
|
430
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
431
|
+
* @param {Object} instance Updated instance.
|
|
432
|
+
*/
|
|
433
|
+
PersistedModel.prototype.updateAttributes = PersistedModel.prototype.patchAttributes = function updateAttributes(data, cb) {
|
|
434
|
+
throwNotAttached(this.modelName, "updateAttributes");
|
|
435
|
+
};
|
|
436
|
+
/**
|
|
437
|
+
* Replace attributes for a model instance and persist it into the datasource.
|
|
438
|
+
* Performs validation before replacing.
|
|
439
|
+
*
|
|
440
|
+
* @param {Object} data Data to replace.
|
|
441
|
+
* @options {Object} [options] Options for replace
|
|
442
|
+
* @property {Boolean} validate Perform validation before saving. Default is true.
|
|
443
|
+
* @callback {Function} callback Callback function called with `(err, instance)` arguments.
|
|
444
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
445
|
+
* @param {Object} instance Replaced instance.
|
|
446
|
+
*/
|
|
447
|
+
PersistedModel.prototype.replaceAttributes = function replaceAttributes(data, cb) {
|
|
448
|
+
throwNotAttached(this.modelName, "replaceAttributes");
|
|
449
|
+
};
|
|
450
|
+
/**
|
|
451
|
+
* Replace attributes for a model instance whose id is the first input
|
|
452
|
+
* argument and persist it into the datasource.
|
|
453
|
+
* Performs validation before replacing.
|
|
454
|
+
*
|
|
455
|
+
* @param {*} id The ID value of model instance to replace.
|
|
456
|
+
* @param {Object} data Data to replace.
|
|
457
|
+
* @options {Object} [options] Options for replace
|
|
458
|
+
* @property {Boolean} validate Perform validation before saving. Default is true.
|
|
459
|
+
* @callback {Function} callback Callback function called with `(err, instance)` arguments.
|
|
460
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
461
|
+
* @param {Object} instance Replaced instance.
|
|
462
|
+
*/
|
|
463
|
+
PersistedModel.replaceById = function replaceById(id, data, cb) {
|
|
464
|
+
throwNotAttached(this.modelName, "replaceById");
|
|
465
|
+
};
|
|
466
|
+
/**
|
|
467
|
+
* Reload object from persistence. Requires `id` member of `object` to be able to call `find`.
|
|
468
|
+
* @callback {Function} callback Callback function called with `(err, instance)` arguments. Required.
|
|
469
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
470
|
+
* @param {Object} instance Model instance.
|
|
471
|
+
*/
|
|
472
|
+
PersistedModel.prototype.reload = function reload(callback) {
|
|
473
|
+
throwNotAttached(this.constructor.modelName, "reload");
|
|
474
|
+
};
|
|
475
|
+
/**
|
|
476
|
+
* Set the correct `id` property for the `PersistedModel`. Uses the `setId` method if the model is attached to
|
|
477
|
+
* connector that defines it. Otherwise, uses the default lookup.
|
|
478
|
+
* Override this method to handle complex IDs.
|
|
479
|
+
*
|
|
480
|
+
* @param {*} val The `id` value. Will be converted to the type that the `id` property specifies.
|
|
481
|
+
*/
|
|
482
|
+
PersistedModel.prototype.setId = function(val) {
|
|
483
|
+
this.getDataSource();
|
|
484
|
+
this[this.getIdName()] = val;
|
|
485
|
+
};
|
|
486
|
+
/**
|
|
487
|
+
* Get the `id` value for the `PersistedModel`.
|
|
488
|
+
*
|
|
489
|
+
* @returns {*} The `id` value
|
|
490
|
+
*/
|
|
491
|
+
PersistedModel.prototype.getId = function() {
|
|
492
|
+
const data = this.toObject();
|
|
493
|
+
if (!data) return;
|
|
494
|
+
return data[this.getIdName()];
|
|
495
|
+
};
|
|
496
|
+
/**
|
|
497
|
+
* Get the `id` property name of the constructor.
|
|
498
|
+
*
|
|
499
|
+
* @returns {String} The `id` property name
|
|
500
|
+
*/
|
|
501
|
+
PersistedModel.prototype.getIdName = function() {
|
|
502
|
+
return this.constructor.getIdName();
|
|
503
|
+
};
|
|
504
|
+
/**
|
|
505
|
+
* Get the `id` property name of the constructor.
|
|
506
|
+
*
|
|
507
|
+
* @returns {String} The `id` property name
|
|
508
|
+
*/
|
|
509
|
+
PersistedModel.getIdName = function() {
|
|
510
|
+
const Model = this;
|
|
511
|
+
const ds = Model.getDataSource();
|
|
512
|
+
if (ds.idName) return ds.idName(Model.modelName);
|
|
513
|
+
else return "id";
|
|
514
|
+
};
|
|
515
|
+
PersistedModel.setupRemoting = function() {
|
|
516
|
+
const PersistedModel = this;
|
|
517
|
+
const typeName = PersistedModel.modelName;
|
|
518
|
+
const options = PersistedModel.settings;
|
|
519
|
+
const updateOnlyProps = this.getUpdateOnlyProperties ? this.getUpdateOnlyProperties() : false;
|
|
520
|
+
const hasUpdateOnlyProps = updateOnlyProps && updateOnlyProps.length > 0;
|
|
521
|
+
options.replaceOnPUT = options.replaceOnPUT !== false;
|
|
522
|
+
function setRemoting(scope, name, options) {
|
|
523
|
+
const fn = scope[name];
|
|
524
|
+
fn._delegate = true;
|
|
525
|
+
options.isStatic = scope === PersistedModel;
|
|
526
|
+
PersistedModel.remoteMethod(name, options);
|
|
527
|
+
}
|
|
528
|
+
setRemoting(PersistedModel, "create", {
|
|
529
|
+
description: "Create a new instance of the model and persist it into the data source.",
|
|
530
|
+
accessType: "WRITE",
|
|
531
|
+
accepts: [{
|
|
532
|
+
arg: "data",
|
|
533
|
+
type: "object",
|
|
534
|
+
model: typeName,
|
|
535
|
+
allowArray: true,
|
|
536
|
+
createOnlyInstance: hasUpdateOnlyProps,
|
|
537
|
+
description: "Model instance data",
|
|
538
|
+
http: { source: "body" }
|
|
539
|
+
}, {
|
|
540
|
+
arg: "options",
|
|
541
|
+
type: "object",
|
|
542
|
+
http: "optionsFromRequest"
|
|
543
|
+
}],
|
|
544
|
+
returns: {
|
|
545
|
+
arg: "data",
|
|
546
|
+
type: typeName,
|
|
547
|
+
root: true
|
|
548
|
+
},
|
|
549
|
+
http: {
|
|
550
|
+
verb: "post",
|
|
551
|
+
path: "/"
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
const upsertOptions = {
|
|
555
|
+
aliases: ["upsert", "updateOrCreate"],
|
|
556
|
+
description: "Patch an existing model instance or insert a new one into the data source.",
|
|
557
|
+
accessType: "WRITE",
|
|
558
|
+
accepts: [{
|
|
559
|
+
arg: "data",
|
|
560
|
+
type: "object",
|
|
561
|
+
model: typeName,
|
|
562
|
+
http: { source: "body" },
|
|
563
|
+
description: "Model instance data"
|
|
564
|
+
}, {
|
|
565
|
+
arg: "options",
|
|
566
|
+
type: "object",
|
|
567
|
+
http: "optionsFromRequest"
|
|
568
|
+
}],
|
|
569
|
+
returns: {
|
|
570
|
+
arg: "data",
|
|
571
|
+
type: typeName,
|
|
572
|
+
root: true
|
|
573
|
+
},
|
|
574
|
+
http: [{
|
|
575
|
+
verb: "patch",
|
|
576
|
+
path: "/"
|
|
577
|
+
}]
|
|
578
|
+
};
|
|
579
|
+
if (!options.replaceOnPUT) upsertOptions.http.unshift({
|
|
580
|
+
verb: "put",
|
|
581
|
+
path: "/"
|
|
582
|
+
});
|
|
583
|
+
setRemoting(PersistedModel, "patchOrCreate", upsertOptions);
|
|
584
|
+
const replaceOrCreateOptions = {
|
|
585
|
+
description: "Replace an existing model instance or insert a new one into the data source.",
|
|
586
|
+
accessType: "WRITE",
|
|
587
|
+
accepts: [{
|
|
588
|
+
arg: "data",
|
|
589
|
+
type: "object",
|
|
590
|
+
model: typeName,
|
|
591
|
+
http: { source: "body" },
|
|
592
|
+
description: "Model instance data"
|
|
593
|
+
}, {
|
|
594
|
+
arg: "options",
|
|
595
|
+
type: "object",
|
|
596
|
+
http: "optionsFromRequest"
|
|
597
|
+
}],
|
|
598
|
+
returns: {
|
|
599
|
+
arg: "data",
|
|
600
|
+
type: typeName,
|
|
601
|
+
root: true
|
|
602
|
+
},
|
|
603
|
+
http: [{
|
|
604
|
+
verb: "post",
|
|
605
|
+
path: "/replaceOrCreate"
|
|
606
|
+
}]
|
|
607
|
+
};
|
|
608
|
+
if (options.replaceOnPUT) replaceOrCreateOptions.http.push({
|
|
609
|
+
verb: "put",
|
|
610
|
+
path: "/"
|
|
611
|
+
});
|
|
612
|
+
setRemoting(PersistedModel, "replaceOrCreate", replaceOrCreateOptions);
|
|
613
|
+
setRemoting(PersistedModel, "upsertWithWhere", {
|
|
614
|
+
aliases: ["patchOrCreateWithWhere"],
|
|
615
|
+
description: "Update an existing model instance or insert a new one into the data source based on the where criteria.",
|
|
616
|
+
accessType: "WRITE",
|
|
617
|
+
accepts: [
|
|
618
|
+
{
|
|
619
|
+
arg: "where",
|
|
620
|
+
type: "object",
|
|
621
|
+
http: { source: "query" },
|
|
622
|
+
description: "Criteria to match model instances"
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
arg: "data",
|
|
626
|
+
type: "object",
|
|
627
|
+
model: typeName,
|
|
628
|
+
http: { source: "body" },
|
|
629
|
+
description: "An object of model property name/value pairs"
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
arg: "options",
|
|
633
|
+
type: "object",
|
|
634
|
+
http: "optionsFromRequest"
|
|
635
|
+
}
|
|
636
|
+
],
|
|
637
|
+
returns: {
|
|
638
|
+
arg: "data",
|
|
639
|
+
type: typeName,
|
|
640
|
+
root: true
|
|
641
|
+
},
|
|
642
|
+
http: {
|
|
643
|
+
verb: "post",
|
|
644
|
+
path: "/upsertWithWhere"
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
setRemoting(PersistedModel, "exists", {
|
|
648
|
+
description: "Check whether a model instance exists in the data source.",
|
|
649
|
+
accessType: "READ",
|
|
650
|
+
accepts: [{
|
|
651
|
+
arg: "id",
|
|
652
|
+
type: "any",
|
|
653
|
+
description: "Model id",
|
|
654
|
+
required: true,
|
|
655
|
+
http: { source: "path" }
|
|
656
|
+
}, {
|
|
657
|
+
arg: "options",
|
|
658
|
+
type: "object",
|
|
659
|
+
http: "optionsFromRequest"
|
|
660
|
+
}],
|
|
661
|
+
returns: {
|
|
662
|
+
arg: "exists",
|
|
663
|
+
type: "boolean"
|
|
664
|
+
},
|
|
665
|
+
http: [{
|
|
666
|
+
verb: "get",
|
|
667
|
+
path: "/:id/exists"
|
|
668
|
+
}, {
|
|
669
|
+
verb: "head",
|
|
670
|
+
path: "/:id"
|
|
671
|
+
}],
|
|
672
|
+
rest: { after: function(ctx, cb) {
|
|
673
|
+
if (ctx.req.method === "GET") return cb();
|
|
674
|
+
if (!ctx.result.exists) {
|
|
675
|
+
const modelName = ctx.method.sharedClass.name;
|
|
676
|
+
const id = ctx.getArgByName("id");
|
|
677
|
+
const msg = "Unknown \"" + modelName + "\" id \"" + id + "\".";
|
|
678
|
+
const error = new Error(msg);
|
|
679
|
+
error.statusCode = error.status = 404;
|
|
680
|
+
error.code = "MODEL_NOT_FOUND";
|
|
681
|
+
cb(error);
|
|
682
|
+
} else cb();
|
|
683
|
+
} }
|
|
684
|
+
});
|
|
685
|
+
setRemoting(PersistedModel, "findById", {
|
|
686
|
+
description: "Find a model instance by {{id}} from the data source.",
|
|
687
|
+
accessType: "READ",
|
|
688
|
+
accepts: [
|
|
689
|
+
{
|
|
690
|
+
arg: "id",
|
|
691
|
+
type: "any",
|
|
692
|
+
description: "Model id",
|
|
693
|
+
required: true,
|
|
694
|
+
http: { source: "path" }
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
arg: "filter",
|
|
698
|
+
type: "object",
|
|
699
|
+
description: "Filter defining fields and include - must be a JSON-encoded string ({\"something\":\"value\"})"
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
arg: "options",
|
|
703
|
+
type: "object",
|
|
704
|
+
http: "optionsFromRequest"
|
|
705
|
+
}
|
|
706
|
+
],
|
|
707
|
+
returns: {
|
|
708
|
+
arg: "data",
|
|
709
|
+
type: typeName,
|
|
710
|
+
root: true
|
|
711
|
+
},
|
|
712
|
+
http: {
|
|
713
|
+
verb: "get",
|
|
714
|
+
path: "/:id"
|
|
715
|
+
},
|
|
716
|
+
rest: { after: convertNullToNotFoundError }
|
|
717
|
+
});
|
|
718
|
+
const replaceByIdOptions = {
|
|
719
|
+
description: "Replace attributes for a model instance and persist it into the data source.",
|
|
720
|
+
accessType: "WRITE",
|
|
721
|
+
accepts: [
|
|
722
|
+
{
|
|
723
|
+
arg: "id",
|
|
724
|
+
type: "any",
|
|
725
|
+
description: "Model id",
|
|
726
|
+
required: true,
|
|
727
|
+
http: { source: "path" }
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
arg: "data",
|
|
731
|
+
type: "object",
|
|
732
|
+
model: typeName,
|
|
733
|
+
http: { source: "body" },
|
|
734
|
+
description: "Model instance data"
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
arg: "options",
|
|
738
|
+
type: "object",
|
|
739
|
+
http: "optionsFromRequest"
|
|
740
|
+
}
|
|
741
|
+
],
|
|
742
|
+
returns: {
|
|
743
|
+
arg: "data",
|
|
744
|
+
type: typeName,
|
|
745
|
+
root: true
|
|
746
|
+
},
|
|
747
|
+
http: [{
|
|
748
|
+
verb: "post",
|
|
749
|
+
path: "/:id/replace"
|
|
750
|
+
}]
|
|
751
|
+
};
|
|
752
|
+
if (options.replaceOnPUT) replaceByIdOptions.http.push({
|
|
753
|
+
verb: "put",
|
|
754
|
+
path: "/:id"
|
|
755
|
+
});
|
|
756
|
+
setRemoting(PersistedModel, "replaceById", replaceByIdOptions);
|
|
757
|
+
setRemoting(PersistedModel, "find", {
|
|
758
|
+
description: "Find all instances of the model matched by filter from the data source.",
|
|
759
|
+
accessType: "READ",
|
|
760
|
+
accepts: [{
|
|
761
|
+
arg: "filter",
|
|
762
|
+
type: "object",
|
|
763
|
+
description: "Filter defining fields, where, include, order, offset, and limit - must be a JSON-encoded string (`{\"where\":{\"something\":\"value\"}}`). See https://loopback.io/doc/en/lb3/Querying-data.html#using-stringified-json-in-rest-queries for more details."
|
|
764
|
+
}, {
|
|
765
|
+
arg: "options",
|
|
766
|
+
type: "object",
|
|
767
|
+
http: "optionsFromRequest"
|
|
768
|
+
}],
|
|
769
|
+
returns: {
|
|
770
|
+
arg: "data",
|
|
771
|
+
type: [typeName],
|
|
772
|
+
root: true
|
|
773
|
+
},
|
|
774
|
+
http: {
|
|
775
|
+
verb: "get",
|
|
776
|
+
path: "/"
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
setRemoting(PersistedModel, "findOne", {
|
|
780
|
+
description: "Find first instance of the model matched by filter from the data source.",
|
|
781
|
+
accessType: "READ",
|
|
782
|
+
accepts: [{
|
|
783
|
+
arg: "filter",
|
|
784
|
+
type: "object",
|
|
785
|
+
description: "Filter defining fields, where, include, order, offset, and limit - must be a JSON-encoded string (`{\"where\":{\"something\":\"value\"}}`). See https://loopback.io/doc/en/lb3/Querying-data.html#using-stringified-json-in-rest-queries for more details."
|
|
786
|
+
}, {
|
|
787
|
+
arg: "options",
|
|
788
|
+
type: "object",
|
|
789
|
+
http: "optionsFromRequest"
|
|
790
|
+
}],
|
|
791
|
+
returns: {
|
|
792
|
+
arg: "data",
|
|
793
|
+
type: typeName,
|
|
794
|
+
root: true
|
|
795
|
+
},
|
|
796
|
+
http: {
|
|
797
|
+
verb: "get",
|
|
798
|
+
path: "/findOne"
|
|
799
|
+
},
|
|
800
|
+
rest: { after: convertNullToNotFoundError }
|
|
801
|
+
});
|
|
802
|
+
setRemoting(PersistedModel, "destroyAll", {
|
|
803
|
+
description: "Delete all matching records.",
|
|
804
|
+
accessType: "WRITE",
|
|
805
|
+
accepts: [{
|
|
806
|
+
arg: "where",
|
|
807
|
+
type: "object",
|
|
808
|
+
description: "filter.where object"
|
|
809
|
+
}, {
|
|
810
|
+
arg: "options",
|
|
811
|
+
type: "object",
|
|
812
|
+
http: "optionsFromRequest"
|
|
813
|
+
}],
|
|
814
|
+
returns: {
|
|
815
|
+
arg: "count",
|
|
816
|
+
type: "object",
|
|
817
|
+
description: "The number of instances deleted",
|
|
818
|
+
root: true
|
|
819
|
+
},
|
|
820
|
+
http: {
|
|
821
|
+
verb: "del",
|
|
822
|
+
path: "/"
|
|
823
|
+
},
|
|
824
|
+
shared: false
|
|
825
|
+
});
|
|
826
|
+
setRemoting(PersistedModel, "updateAll", {
|
|
827
|
+
aliases: ["update"],
|
|
828
|
+
description: "Update instances of the model matched by {{where}} from the data source.",
|
|
829
|
+
accessType: "WRITE",
|
|
830
|
+
accepts: [
|
|
831
|
+
{
|
|
832
|
+
arg: "where",
|
|
833
|
+
type: "object",
|
|
834
|
+
http: { source: "query" },
|
|
835
|
+
description: "Criteria to match model instances"
|
|
836
|
+
},
|
|
837
|
+
{
|
|
838
|
+
arg: "data",
|
|
839
|
+
type: "object",
|
|
840
|
+
model: typeName,
|
|
841
|
+
http: { source: "body" },
|
|
842
|
+
description: "An object of model property name/value pairs"
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
arg: "options",
|
|
846
|
+
type: "object",
|
|
847
|
+
http: "optionsFromRequest"
|
|
848
|
+
}
|
|
849
|
+
],
|
|
850
|
+
returns: {
|
|
851
|
+
arg: "info",
|
|
852
|
+
description: "Information related to the outcome of the operation",
|
|
853
|
+
type: { count: {
|
|
854
|
+
type: "number",
|
|
855
|
+
description: "The number of instances updated"
|
|
856
|
+
} },
|
|
857
|
+
root: true
|
|
858
|
+
},
|
|
859
|
+
http: {
|
|
860
|
+
verb: "post",
|
|
861
|
+
path: "/update"
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
setRemoting(PersistedModel, "deleteById", {
|
|
865
|
+
aliases: ["destroyById", "removeById"],
|
|
866
|
+
description: "Delete a model instance by {{id}} from the data source.",
|
|
867
|
+
accessType: "WRITE",
|
|
868
|
+
accepts: [{
|
|
869
|
+
arg: "id",
|
|
870
|
+
type: "any",
|
|
871
|
+
description: "Model id",
|
|
872
|
+
required: true,
|
|
873
|
+
http: { source: "path" }
|
|
874
|
+
}, {
|
|
875
|
+
arg: "options",
|
|
876
|
+
type: "object",
|
|
877
|
+
http: "optionsFromRequest"
|
|
878
|
+
}],
|
|
879
|
+
http: {
|
|
880
|
+
verb: "del",
|
|
881
|
+
path: "/:id"
|
|
882
|
+
},
|
|
883
|
+
returns: {
|
|
884
|
+
arg: "count",
|
|
885
|
+
type: "object",
|
|
886
|
+
root: true
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
setRemoting(PersistedModel, "count", {
|
|
890
|
+
description: "Count instances of the model matched by where from the data source.",
|
|
891
|
+
accessType: "READ",
|
|
892
|
+
accepts: [{
|
|
893
|
+
arg: "where",
|
|
894
|
+
type: "object",
|
|
895
|
+
description: "Criteria to match model instances"
|
|
896
|
+
}, {
|
|
897
|
+
arg: "options",
|
|
898
|
+
type: "object",
|
|
899
|
+
http: "optionsFromRequest"
|
|
900
|
+
}],
|
|
901
|
+
returns: {
|
|
902
|
+
arg: "count",
|
|
903
|
+
type: "number"
|
|
904
|
+
},
|
|
905
|
+
http: {
|
|
906
|
+
verb: "get",
|
|
907
|
+
path: "/count"
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
const updateAttributesOptions = {
|
|
911
|
+
aliases: ["updateAttributes"],
|
|
912
|
+
description: "Patch attributes for a model instance and persist it into the data source.",
|
|
913
|
+
accessType: "WRITE",
|
|
914
|
+
accepts: [{
|
|
915
|
+
arg: "data",
|
|
916
|
+
type: "object",
|
|
917
|
+
model: typeName,
|
|
918
|
+
http: { source: "body" },
|
|
919
|
+
description: "An object of model property name/value pairs"
|
|
920
|
+
}, {
|
|
921
|
+
arg: "options",
|
|
922
|
+
type: "object",
|
|
923
|
+
http: "optionsFromRequest"
|
|
924
|
+
}],
|
|
925
|
+
returns: {
|
|
926
|
+
arg: "data",
|
|
927
|
+
type: typeName,
|
|
928
|
+
root: true
|
|
929
|
+
},
|
|
930
|
+
http: [{
|
|
931
|
+
verb: "patch",
|
|
932
|
+
path: "/"
|
|
933
|
+
}]
|
|
934
|
+
};
|
|
935
|
+
setRemoting(PersistedModel.prototype, "patchAttributes", updateAttributesOptions);
|
|
936
|
+
if (!options.replaceOnPUT) updateAttributesOptions.http.unshift({
|
|
937
|
+
verb: "put",
|
|
938
|
+
path: "/"
|
|
939
|
+
});
|
|
940
|
+
if (options.trackChanges || options.enableRemoteReplication) {
|
|
941
|
+
setRemoting(PersistedModel, "diff", {
|
|
942
|
+
description: "Get a set of deltas and conflicts since the given checkpoint.",
|
|
943
|
+
accessType: "READ",
|
|
944
|
+
accepts: [{
|
|
945
|
+
arg: "since",
|
|
946
|
+
type: "number",
|
|
947
|
+
description: "Find deltas since this checkpoint"
|
|
948
|
+
}, {
|
|
949
|
+
arg: "remoteChanges",
|
|
950
|
+
type: "array",
|
|
951
|
+
description: "an array of change objects",
|
|
952
|
+
http: { source: "body" }
|
|
953
|
+
}],
|
|
954
|
+
returns: {
|
|
955
|
+
arg: "result",
|
|
956
|
+
type: "object",
|
|
957
|
+
root: true
|
|
958
|
+
},
|
|
959
|
+
http: {
|
|
960
|
+
verb: "post",
|
|
961
|
+
path: "/diff"
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
setRemoting(PersistedModel, "changes", {
|
|
965
|
+
description: "Get the changes to a model since a given checkpoint.Provide a filter object to reduce the number of results returned.",
|
|
966
|
+
accessType: "READ",
|
|
967
|
+
accepts: [{
|
|
968
|
+
arg: "since",
|
|
969
|
+
type: "number",
|
|
970
|
+
description: "Only return changes since this checkpoint"
|
|
971
|
+
}, {
|
|
972
|
+
arg: "filter",
|
|
973
|
+
type: "object",
|
|
974
|
+
description: "Only include changes that match this filter"
|
|
975
|
+
}],
|
|
976
|
+
returns: {
|
|
977
|
+
arg: "changes",
|
|
978
|
+
type: "array",
|
|
979
|
+
root: true
|
|
980
|
+
},
|
|
981
|
+
http: {
|
|
982
|
+
verb: "get",
|
|
983
|
+
path: "/changes"
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
setRemoting(PersistedModel, "checkpoint", {
|
|
987
|
+
description: "Create a checkpoint.",
|
|
988
|
+
accessType: "REPLICATE",
|
|
989
|
+
returns: {
|
|
990
|
+
arg: "checkpoint",
|
|
991
|
+
type: "object",
|
|
992
|
+
root: true
|
|
993
|
+
},
|
|
994
|
+
http: {
|
|
995
|
+
verb: "post",
|
|
996
|
+
path: "/checkpoint"
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
setRemoting(PersistedModel, "currentCheckpoint", {
|
|
1000
|
+
description: "Get the current checkpoint.",
|
|
1001
|
+
accessType: "READ",
|
|
1002
|
+
returns: {
|
|
1003
|
+
arg: "checkpoint",
|
|
1004
|
+
type: "object",
|
|
1005
|
+
root: true
|
|
1006
|
+
},
|
|
1007
|
+
http: {
|
|
1008
|
+
verb: "get",
|
|
1009
|
+
path: "/checkpoint"
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
setRemoting(PersistedModel, "createUpdates", {
|
|
1013
|
+
description: "Create an update list from a delta list.",
|
|
1014
|
+
accessType: "READ",
|
|
1015
|
+
accepts: {
|
|
1016
|
+
arg: "deltas",
|
|
1017
|
+
type: "array",
|
|
1018
|
+
http: { source: "body" }
|
|
1019
|
+
},
|
|
1020
|
+
returns: {
|
|
1021
|
+
arg: "updates",
|
|
1022
|
+
type: "array",
|
|
1023
|
+
root: true
|
|
1024
|
+
},
|
|
1025
|
+
http: {
|
|
1026
|
+
verb: "post",
|
|
1027
|
+
path: "/create-updates"
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
setRemoting(PersistedModel, "bulkUpdate", {
|
|
1031
|
+
description: "Run multiple updates at once. Note: this is not atomic.",
|
|
1032
|
+
accessType: "WRITE",
|
|
1033
|
+
accepts: {
|
|
1034
|
+
arg: "updates",
|
|
1035
|
+
type: "array"
|
|
1036
|
+
},
|
|
1037
|
+
http: {
|
|
1038
|
+
verb: "post",
|
|
1039
|
+
path: "/bulk-update"
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
setRemoting(PersistedModel, "findLastChange", {
|
|
1043
|
+
description: "Get the most recent change record for this instance.",
|
|
1044
|
+
accessType: "READ",
|
|
1045
|
+
accepts: {
|
|
1046
|
+
arg: "id",
|
|
1047
|
+
type: "any",
|
|
1048
|
+
required: true,
|
|
1049
|
+
http: { source: "path" },
|
|
1050
|
+
description: "Model id"
|
|
1051
|
+
},
|
|
1052
|
+
returns: {
|
|
1053
|
+
arg: "result",
|
|
1054
|
+
type: this.Change.modelName,
|
|
1055
|
+
root: true
|
|
1056
|
+
},
|
|
1057
|
+
http: {
|
|
1058
|
+
verb: "get",
|
|
1059
|
+
path: "/:id/changes/last"
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
setRemoting(PersistedModel, "updateLastChange", {
|
|
1063
|
+
description: "Update the properties of the most recent change record kept for this instance.",
|
|
1064
|
+
accessType: "WRITE",
|
|
1065
|
+
accepts: [{
|
|
1066
|
+
arg: "id",
|
|
1067
|
+
type: "any",
|
|
1068
|
+
required: true,
|
|
1069
|
+
http: { source: "path" },
|
|
1070
|
+
description: "Model id"
|
|
1071
|
+
}, {
|
|
1072
|
+
arg: "data",
|
|
1073
|
+
type: "object",
|
|
1074
|
+
model: typeName,
|
|
1075
|
+
http: { source: "body" },
|
|
1076
|
+
description: "An object of Change property name/value pairs"
|
|
1077
|
+
}],
|
|
1078
|
+
returns: {
|
|
1079
|
+
arg: "result",
|
|
1080
|
+
type: this.Change.modelName,
|
|
1081
|
+
root: true
|
|
1082
|
+
},
|
|
1083
|
+
http: {
|
|
1084
|
+
verb: "put",
|
|
1085
|
+
path: "/:id/changes/last"
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
setRemoting(PersistedModel, "createChangeStream", {
|
|
1090
|
+
description: "Create a change stream.",
|
|
1091
|
+
accessType: "READ",
|
|
1092
|
+
http: [{
|
|
1093
|
+
verb: "post",
|
|
1094
|
+
path: "/change-stream"
|
|
1095
|
+
}, {
|
|
1096
|
+
verb: "get",
|
|
1097
|
+
path: "/change-stream"
|
|
1098
|
+
}],
|
|
1099
|
+
accepts: {
|
|
1100
|
+
arg: "options",
|
|
1101
|
+
type: "object"
|
|
1102
|
+
},
|
|
1103
|
+
returns: {
|
|
1104
|
+
arg: "changes",
|
|
1105
|
+
type: "ReadableStream",
|
|
1106
|
+
json: true
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
};
|
|
1110
|
+
/**
|
|
1111
|
+
* Get a set of deltas and conflicts since the given checkpoint.
|
|
1112
|
+
*
|
|
1113
|
+
* See [Change.diff()](#change-diff) for details.
|
|
1114
|
+
*
|
|
1115
|
+
* @param {Number} since Find deltas since this checkpoint.
|
|
1116
|
+
* @param {Array} remoteChanges An array of change objects.
|
|
1117
|
+
* @callback {Function} callback Callback function called with `(err, result)` arguments. Required.
|
|
1118
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
1119
|
+
* @param {Object} result Object with `deltas` and `conflicts` properties; see [Change.diff()](#change-diff) for details.
|
|
1120
|
+
*/
|
|
1121
|
+
PersistedModel.diff = function(since, remoteChanges, callback) {
|
|
1122
|
+
this.getChangeModel().diff(this.modelName, since, remoteChanges, callback);
|
|
1123
|
+
};
|
|
1124
|
+
/**
|
|
1125
|
+
* Get the changes to a model since the specified checkpoint. Provide a filter object
|
|
1126
|
+
* to reduce the number of results returned.
|
|
1127
|
+
* @param {Number} since Return only changes since this checkpoint.
|
|
1128
|
+
* @param {Object} filter Include only changes that match this filter, the same as for [#persistedmodel-find](find()).
|
|
1129
|
+
* @callback {Function} callback Callback function called with `(err, changes)` arguments. Required.
|
|
1130
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
1131
|
+
* @param {Array} changes An array of [Change](#change) objects.
|
|
1132
|
+
*/
|
|
1133
|
+
PersistedModel.changes = function(since, filter, callback) {
|
|
1134
|
+
if (typeof since === "function") {
|
|
1135
|
+
filter = {};
|
|
1136
|
+
callback = since;
|
|
1137
|
+
since = -1;
|
|
1138
|
+
}
|
|
1139
|
+
if (typeof filter === "function") {
|
|
1140
|
+
callback = filter;
|
|
1141
|
+
since = -1;
|
|
1142
|
+
filter = {};
|
|
1143
|
+
}
|
|
1144
|
+
const idName = this.dataSource.idName(this.modelName);
|
|
1145
|
+
const Change = this.getChangeModel();
|
|
1146
|
+
const model = this;
|
|
1147
|
+
const changeFilter = this.createChangeFilter(since, filter);
|
|
1148
|
+
filter = filter || {};
|
|
1149
|
+
const modelFilter = Object.assign({}, filter);
|
|
1150
|
+
const modelFilterWhere = Object.assign({}, filter.where);
|
|
1151
|
+
modelFilter.fields = {};
|
|
1152
|
+
modelFilter.where = modelFilterWhere;
|
|
1153
|
+
modelFilter.fields[idName] = true;
|
|
1154
|
+
Change.find(changeFilter, function(err, changes) {
|
|
1155
|
+
if (err) return callback(err);
|
|
1156
|
+
if (!Array.isArray(changes) || changes.length === 0) return callback(null, []);
|
|
1157
|
+
const ids = [];
|
|
1158
|
+
const changeIdLookup = Object.create(null);
|
|
1159
|
+
for (let i = 0; i < changes.length; i++) {
|
|
1160
|
+
const id = changes[i].getModelId();
|
|
1161
|
+
const lookupKey = String(id);
|
|
1162
|
+
if (changeIdLookup[lookupKey] !== true) {
|
|
1163
|
+
changeIdLookup[lookupKey] = true;
|
|
1164
|
+
ids.push(id);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
modelFilter.where[idName] = { inq: ids };
|
|
1168
|
+
model.find(modelFilter, function(err, models) {
|
|
1169
|
+
if (err) return callback(err);
|
|
1170
|
+
const modelIdLookup = Object.create(null);
|
|
1171
|
+
for (let i = 0; i < models.length; i++) modelIdLookup[String(models[i][idName])] = true;
|
|
1172
|
+
const filteredChanges = [];
|
|
1173
|
+
for (let i = 0; i < changes.length; i++) {
|
|
1174
|
+
const change = changes[i];
|
|
1175
|
+
if (change.type() === Change.DELETE || modelIdLookup[String(change.modelId)] === true) filteredChanges.push(change);
|
|
1176
|
+
}
|
|
1177
|
+
callback(null, filteredChanges);
|
|
1178
|
+
});
|
|
1179
|
+
});
|
|
1180
|
+
};
|
|
1181
|
+
/**
|
|
1182
|
+
* Create a checkpoint.
|
|
1183
|
+
*
|
|
1184
|
+
* @param {Function} callback
|
|
1185
|
+
*/
|
|
1186
|
+
PersistedModel.checkpoint = function(cb) {
|
|
1187
|
+
const Checkpoint = this.getChangeModel().getCheckpointModel();
|
|
1188
|
+
if (!cb) return Checkpoint.bumpLastSeq();
|
|
1189
|
+
Checkpoint.bumpLastSeq(cb);
|
|
1190
|
+
};
|
|
1191
|
+
/**
|
|
1192
|
+
* Get the current checkpoint ID.
|
|
1193
|
+
*
|
|
1194
|
+
* @callback {Function} callback Callback function called with `(err, currentCheckpointId)` arguments. Required.
|
|
1195
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
1196
|
+
* @param {Number} currentCheckpointId Current checkpoint ID.
|
|
1197
|
+
*/
|
|
1198
|
+
PersistedModel.currentCheckpoint = function(cb) {
|
|
1199
|
+
const Checkpoint = this.getChangeModel().getCheckpointModel();
|
|
1200
|
+
if (!cb) return Checkpoint.current();
|
|
1201
|
+
Checkpoint.current(cb);
|
|
1202
|
+
};
|
|
1203
|
+
/**
|
|
1204
|
+
* Replicate changes since the given checkpoint to the given target model.
|
|
1205
|
+
*
|
|
1206
|
+
* @param {Number} [since] Since this checkpoint
|
|
1207
|
+
* @param {Model} targetModel Target this model class
|
|
1208
|
+
* @param {Object} [options] An optional options object to pass to underlying data-access calls.
|
|
1209
|
+
* @param {Object} [options.filter] Replicate models that match this filter
|
|
1210
|
+
* @callback {Function} [callback] Callback function called with `(err, conflicts)` arguments.
|
|
1211
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
1212
|
+
* @param {Conflict[]} conflicts A list of changes that could not be replicated due to conflicts.
|
|
1213
|
+
* @param {Object} checkpoints The new checkpoints to use as the "since"
|
|
1214
|
+
* argument for the next replication.
|
|
1215
|
+
*
|
|
1216
|
+
* @promise
|
|
1217
|
+
*/
|
|
1218
|
+
PersistedModel.replicate = function(since, targetModel, options, callback) {
|
|
1219
|
+
const lastArg = arguments[arguments.length - 1];
|
|
1220
|
+
if (typeof lastArg === "function" && arguments.length > 1) callback = lastArg;
|
|
1221
|
+
if (typeof since === "function" && since.modelName) {
|
|
1222
|
+
targetModel = since;
|
|
1223
|
+
since = -1;
|
|
1224
|
+
}
|
|
1225
|
+
if (typeof since !== "object") since = {
|
|
1226
|
+
source: since,
|
|
1227
|
+
target: since
|
|
1228
|
+
};
|
|
1229
|
+
if (typeof options === "function") options = {};
|
|
1230
|
+
options = options || {};
|
|
1231
|
+
const sourceModel = this;
|
|
1232
|
+
callback = callback || utils.createPromiseCallback();
|
|
1233
|
+
debug("replicating %s since %s to %s since %s", sourceModel.modelName, since.source, targetModel.modelName, since.target);
|
|
1234
|
+
if (options.filter) debug(" with filter %j", options.filter);
|
|
1235
|
+
const MAX_ATTEMPTS = 3;
|
|
1236
|
+
run(1, since);
|
|
1237
|
+
return callback.promise;
|
|
1238
|
+
function run(attempt, since) {
|
|
1239
|
+
debug(" iteration #%s", attempt);
|
|
1240
|
+
tryReplicate(sourceModel, targetModel, since, options, next);
|
|
1241
|
+
function next(err, conflicts, cps, updates) {
|
|
1242
|
+
if (err || conflicts.length || !updates || updates.length === 0 || attempt >= MAX_ATTEMPTS) return callback(err, conflicts, cps);
|
|
1243
|
+
run(attempt + 1, cps);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
function tryReplicate(sourceModel, targetModel, since, options, callback) {
|
|
1248
|
+
const Change = sourceModel.getChangeModel();
|
|
1249
|
+
const TargetChange = targetModel.getChangeModel();
|
|
1250
|
+
const changeTrackingEnabled = Change && TargetChange;
|
|
1251
|
+
let replicationChunkSize = REPLICATION_CHUNK_SIZE;
|
|
1252
|
+
if (sourceModel.settings && sourceModel.settings.replicationChunkSize) replicationChunkSize = sourceModel.settings.replicationChunkSize;
|
|
1253
|
+
assert(changeTrackingEnabled, "You must enable change tracking before replicating");
|
|
1254
|
+
let diff, updates, newSourceCp, newTargetCp;
|
|
1255
|
+
run().then(function() {
|
|
1256
|
+
done();
|
|
1257
|
+
}, done);
|
|
1258
|
+
async function run() {
|
|
1259
|
+
await checkpoints();
|
|
1260
|
+
const createdUpdates = await createSourceUpdates(await getDiffFromTarget(await getSourceChanges()));
|
|
1261
|
+
if (!createdUpdates) return;
|
|
1262
|
+
await bulkUpdate(createdUpdates);
|
|
1263
|
+
}
|
|
1264
|
+
function getSourceChanges() {
|
|
1265
|
+
const chunkPromise = utils.downloadInChunks(options.filter, replicationChunkSize, function(filter, pagingCallback) {
|
|
1266
|
+
sourceModel.changes(since.source, filter, pagingCallback);
|
|
1267
|
+
});
|
|
1268
|
+
if (!debug.enabled) return chunkPromise;
|
|
1269
|
+
return chunkPromise.then(function(result) {
|
|
1270
|
+
debug(" using source changes");
|
|
1271
|
+
result.forEach(function(it) {
|
|
1272
|
+
debug(" %j", it);
|
|
1273
|
+
});
|
|
1274
|
+
return result;
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
function getDiffFromTarget(sourceChanges) {
|
|
1278
|
+
const diffPromise = utils.uploadInChunks(sourceChanges, replicationChunkSize, function(smallArray, chunkCallback) {
|
|
1279
|
+
return targetModel.diff(since.target, smallArray, chunkCallback);
|
|
1280
|
+
});
|
|
1281
|
+
if (!debug.enabled) return diffPromise;
|
|
1282
|
+
return diffPromise.then(function(result) {
|
|
1283
|
+
if (result.conflicts && result.conflicts.length) {
|
|
1284
|
+
debug(" diff conflicts");
|
|
1285
|
+
result.conflicts.forEach(function(d) {
|
|
1286
|
+
debug(" %j", d);
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
if (result.deltas && result.deltas.length) {
|
|
1290
|
+
debug(" diff deltas");
|
|
1291
|
+
result.deltas.forEach(function(it) {
|
|
1292
|
+
debug(" %j", it);
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
return result;
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
function createSourceUpdates(_diff) {
|
|
1299
|
+
diff = _diff;
|
|
1300
|
+
diff.conflicts = diff.conflicts || [];
|
|
1301
|
+
if (diff && diff.deltas && diff.deltas.length) {
|
|
1302
|
+
debug(" building a list of updates");
|
|
1303
|
+
return utils.uploadInChunks(diff.deltas, replicationChunkSize, function(smallArray, chunkCallback) {
|
|
1304
|
+
return sourceModel.createUpdates(smallArray, chunkCallback);
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
function bulkUpdate(_updates) {
|
|
1309
|
+
debug(" starting bulk update");
|
|
1310
|
+
updates = _updates;
|
|
1311
|
+
if (!updates || !updates.length) return Promise.resolve();
|
|
1312
|
+
return utils.uploadInChunks(updates, replicationChunkSize, function(smallArray, chunkCallback) {
|
|
1313
|
+
return targetModel.bulkUpdate(smallArray, options, function(err) {
|
|
1314
|
+
chunkCallback(null, err);
|
|
1315
|
+
});
|
|
1316
|
+
}).then(function(err) {
|
|
1317
|
+
const conflicts = err && err.details && err.details.conflicts;
|
|
1318
|
+
if (conflicts && err.statusCode == 409) {
|
|
1319
|
+
diff.conflicts = conflicts;
|
|
1320
|
+
const conflictedIds = Object.create(null);
|
|
1321
|
+
for (let i = 0; i < conflicts.length; i++) conflictedIds[String(conflicts[i].modelId)] = true;
|
|
1322
|
+
updates = updates.filter(function(u) {
|
|
1323
|
+
return conflictedIds[String(u.change.modelId)] !== true;
|
|
1324
|
+
});
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
if (err) throw err;
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
function checkpoints() {
|
|
1331
|
+
return utils.invokeWithCallback(sourceModel.checkpoint, sourceModel, []).then(function(source) {
|
|
1332
|
+
newSourceCp = source.seq;
|
|
1333
|
+
return utils.invokeWithCallback(targetModel.checkpoint, targetModel, []);
|
|
1334
|
+
}).then(function(target) {
|
|
1335
|
+
newTargetCp = target.seq;
|
|
1336
|
+
debug(" created checkpoints");
|
|
1337
|
+
debug(" %s for source model %s", newSourceCp, sourceModel.modelName);
|
|
1338
|
+
debug(" %s for target model %s", newTargetCp, targetModel.modelName);
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
function done(err) {
|
|
1342
|
+
if (err) return callback(err);
|
|
1343
|
+
debug(" replication finished");
|
|
1344
|
+
debug(" %s conflict(s) detected", diff.conflicts.length);
|
|
1345
|
+
debug(" %s change(s) applied", updates ? updates.length : 0);
|
|
1346
|
+
debug(" new checkpoints: { source: %j, target: %j }", newSourceCp, newTargetCp);
|
|
1347
|
+
const conflicts = new Array(diff.conflicts.length);
|
|
1348
|
+
for (let i = 0; i < diff.conflicts.length; i++) {
|
|
1349
|
+
const change = diff.conflicts[i];
|
|
1350
|
+
conflicts[i] = new Change.Conflict(change.modelId, sourceModel, targetModel);
|
|
1351
|
+
}
|
|
1352
|
+
if (conflicts.length) sourceModel.emit("conflicts", conflicts);
|
|
1353
|
+
if (callback) callback(null, conflicts, {
|
|
1354
|
+
source: newSourceCp,
|
|
1355
|
+
target: newTargetCp
|
|
1356
|
+
}, updates);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Create an update list (for `Model.bulkUpdate()`) from a delta list
|
|
1361
|
+
* (result of `Change.diff()`).
|
|
1362
|
+
*
|
|
1363
|
+
* @param {Array} deltas
|
|
1364
|
+
* @param {Function} callback
|
|
1365
|
+
*/
|
|
1366
|
+
PersistedModel.createUpdates = function(deltas, cb) {
|
|
1367
|
+
const Change = this.getChangeModel();
|
|
1368
|
+
const updates = [];
|
|
1369
|
+
const Model = this;
|
|
1370
|
+
const tasks = [];
|
|
1371
|
+
deltas.forEach(function(change) {
|
|
1372
|
+
change = new Change(change);
|
|
1373
|
+
const type = change.type();
|
|
1374
|
+
const update = {
|
|
1375
|
+
type,
|
|
1376
|
+
change
|
|
1377
|
+
};
|
|
1378
|
+
switch (type) {
|
|
1379
|
+
case Change.CREATE:
|
|
1380
|
+
case Change.UPDATE:
|
|
1381
|
+
tasks.push(function(cb) {
|
|
1382
|
+
Model.findById(change.modelId, function(err, inst) {
|
|
1383
|
+
if (err) return cb(err);
|
|
1384
|
+
if (!inst) return cb && cb(new Error(g.f("Missing data for change: %s", change.modelId)));
|
|
1385
|
+
if (inst.toObject) update.data = inst.toObject();
|
|
1386
|
+
else update.data = inst;
|
|
1387
|
+
updates.push(update);
|
|
1388
|
+
cb();
|
|
1389
|
+
});
|
|
1390
|
+
});
|
|
1391
|
+
break;
|
|
1392
|
+
case Change.DELETE:
|
|
1393
|
+
updates.push(update);
|
|
1394
|
+
break;
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
Promise.all(tasks.map(function(task) {
|
|
1398
|
+
return utils.invokeWithCallback(task, null, []);
|
|
1399
|
+
})).then(function() {
|
|
1400
|
+
cb(null, updates);
|
|
1401
|
+
}, cb);
|
|
1402
|
+
};
|
|
1403
|
+
/**
|
|
1404
|
+
* Apply an update list.
|
|
1405
|
+
*
|
|
1406
|
+
* **Note: this is not atomic**
|
|
1407
|
+
*
|
|
1408
|
+
* @param {Array} updates An updates list, usually from [createUpdates()](#persistedmodel-createupdates).
|
|
1409
|
+
* @param {Object} [options] An optional options object to pass to underlying data-access calls.
|
|
1410
|
+
* @param {Function} callback Callback function.
|
|
1411
|
+
*/
|
|
1412
|
+
PersistedModel.bulkUpdate = function(updates, options, callback) {
|
|
1413
|
+
const tasks = [];
|
|
1414
|
+
const Model = this;
|
|
1415
|
+
const Change = this.getChangeModel();
|
|
1416
|
+
const conflicts = [];
|
|
1417
|
+
const lastArg = arguments[arguments.length - 1];
|
|
1418
|
+
if (typeof lastArg === "function" && arguments.length > 1) callback = lastArg;
|
|
1419
|
+
if (typeof options === "function") options = {};
|
|
1420
|
+
options = options || {};
|
|
1421
|
+
buildLookupOfAffectedModelData(Model, updates, function(err, currentMap) {
|
|
1422
|
+
if (err) return callback(err);
|
|
1423
|
+
updates.forEach(function(update) {
|
|
1424
|
+
const id = update.change.modelId;
|
|
1425
|
+
const current = currentMap[id];
|
|
1426
|
+
switch (update.type) {
|
|
1427
|
+
case Change.UPDATE:
|
|
1428
|
+
tasks.push(function(cb) {
|
|
1429
|
+
applyUpdate(Model, id, current, update.data, update.change, conflicts, options, cb);
|
|
1430
|
+
});
|
|
1431
|
+
break;
|
|
1432
|
+
case Change.CREATE:
|
|
1433
|
+
tasks.push(function(cb) {
|
|
1434
|
+
applyCreate(Model, id, current, update.data, update.change, conflicts, options, cb);
|
|
1435
|
+
});
|
|
1436
|
+
break;
|
|
1437
|
+
case Change.DELETE:
|
|
1438
|
+
tasks.push(function(cb) {
|
|
1439
|
+
applyDelete(Model, id, current, update.change, conflicts, options, cb);
|
|
1440
|
+
});
|
|
1441
|
+
break;
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1444
|
+
Promise.all(tasks.map(function(task) {
|
|
1445
|
+
return utils.invokeWithCallback(task, null, []);
|
|
1446
|
+
})).then(function() {
|
|
1447
|
+
if (conflicts.length) {
|
|
1448
|
+
err = new Error(g.f("Conflict"));
|
|
1449
|
+
err.statusCode = 409;
|
|
1450
|
+
err.details = { conflicts };
|
|
1451
|
+
return callback(err);
|
|
1452
|
+
}
|
|
1453
|
+
callback();
|
|
1454
|
+
}, callback);
|
|
1455
|
+
});
|
|
1456
|
+
};
|
|
1457
|
+
function buildLookupOfAffectedModelData(Model, updates, callback) {
|
|
1458
|
+
const idName = Model.dataSource.idName(Model.modelName);
|
|
1459
|
+
const affectedIds = [];
|
|
1460
|
+
const affectedIdLookup = Object.create(null);
|
|
1461
|
+
for (let i = 0; i < updates.length; i++) {
|
|
1462
|
+
const modelId = updates[i].change.modelId;
|
|
1463
|
+
const lookupKey = String(modelId);
|
|
1464
|
+
if (affectedIdLookup[lookupKey] !== true) {
|
|
1465
|
+
affectedIdLookup[lookupKey] = true;
|
|
1466
|
+
affectedIds.push(modelId);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
const whereAffected = {};
|
|
1470
|
+
whereAffected[idName] = { inq: affectedIds };
|
|
1471
|
+
Model.find({ where: whereAffected }, function(err, affectedList) {
|
|
1472
|
+
if (err) return callback(err);
|
|
1473
|
+
const dataLookup = {};
|
|
1474
|
+
for (let i = 0; i < affectedList.length; i++) {
|
|
1475
|
+
const it = affectedList[i];
|
|
1476
|
+
dataLookup[it[idName]] = it;
|
|
1477
|
+
}
|
|
1478
|
+
callback(null, dataLookup);
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
function applyUpdate(Model, id, current, data, change, conflicts, options, cb) {
|
|
1482
|
+
const Change = Model.getChangeModel();
|
|
1483
|
+
const rev = current ? Change.revisionForInst(current) : null;
|
|
1484
|
+
if (rev !== change.prev) {
|
|
1485
|
+
debug("Detected non-rectified change of %s %j", Model.modelName, id);
|
|
1486
|
+
debug(" Expected revision: %s", change.rev);
|
|
1487
|
+
debug(" Actual revision: %s", rev);
|
|
1488
|
+
conflicts.push(change);
|
|
1489
|
+
return Change.rectifyModelChanges(Model.modelName, [id], cb);
|
|
1490
|
+
}
|
|
1491
|
+
Model.updateAll(current.toObject(), data, options, function(err, result) {
|
|
1492
|
+
if (err) return cb(err);
|
|
1493
|
+
const count = result && result.count;
|
|
1494
|
+
switch (count) {
|
|
1495
|
+
case 1: return cb();
|
|
1496
|
+
case 0:
|
|
1497
|
+
debug("UpdateAll detected non-rectified change of %s %j", Model.modelName, id);
|
|
1498
|
+
conflicts.push(change);
|
|
1499
|
+
return cb();
|
|
1500
|
+
case void 0:
|
|
1501
|
+
case null: return cb(new Error(g.f("Cannot apply bulk updates, the connector does not correctly report the number of updated records.")));
|
|
1502
|
+
default:
|
|
1503
|
+
debug("%s.updateAll modified unexpected number of instances: %j", Model.modelName, count);
|
|
1504
|
+
return cb(new Error(g.f("Bulk update failed, the connector has modified unexpected number of records: %s", JSON.stringify(count))));
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
function applyCreate(Model, id, current, data, change, conflicts, options, cb) {
|
|
1509
|
+
Model.create(data, options, function(createErr) {
|
|
1510
|
+
if (!createErr) return cb();
|
|
1511
|
+
Model.findById(id, function(findErr, inst) {
|
|
1512
|
+
if (findErr || !inst) return cb(createErr);
|
|
1513
|
+
return conflict();
|
|
1514
|
+
});
|
|
1515
|
+
});
|
|
1516
|
+
function conflict() {
|
|
1517
|
+
debug("Detected non-rectified new instance of %s %j", Model.modelName, id);
|
|
1518
|
+
conflicts.push(change);
|
|
1519
|
+
return Model.getChangeModel().rectifyModelChanges(Model.modelName, [id], cb);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
function applyDelete(Model, id, current, change, conflicts, options, cb) {
|
|
1523
|
+
if (!current) return cb();
|
|
1524
|
+
const Change = Model.getChangeModel();
|
|
1525
|
+
const rev = Change.revisionForInst(current);
|
|
1526
|
+
if (rev !== change.prev) {
|
|
1527
|
+
debug("Detected non-rectified change of %s %j", Model.modelName, id);
|
|
1528
|
+
debug(" Expected revision: %s", change.rev);
|
|
1529
|
+
debug(" Actual revision: %s", rev);
|
|
1530
|
+
conflicts.push(change);
|
|
1531
|
+
return Change.rectifyModelChanges(Model.modelName, [id], cb);
|
|
1532
|
+
}
|
|
1533
|
+
Model.deleteAll(current.toObject(), options, function(err, result) {
|
|
1534
|
+
if (err) return cb(err);
|
|
1535
|
+
const count = result && result.count;
|
|
1536
|
+
switch (count) {
|
|
1537
|
+
case 1: return cb();
|
|
1538
|
+
case 0:
|
|
1539
|
+
debug("DeleteAll detected non-rectified change of %s %j", Model.modelName, id);
|
|
1540
|
+
conflicts.push(change);
|
|
1541
|
+
return cb();
|
|
1542
|
+
case void 0:
|
|
1543
|
+
case null: return cb(new Error(g.f("Cannot apply bulk updates, the connector does not correctly report the number of deleted records.")));
|
|
1544
|
+
default:
|
|
1545
|
+
debug("%s.deleteAll modified unexpected number of instances: %j", Model.modelName, count);
|
|
1546
|
+
return cb(new Error(g.f("Bulk update failed, the connector has deleted unexpected number of records: %s", JSON.stringify(count))));
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Get the `Change` model.
|
|
1552
|
+
* Throws an error if the change model is not correctly setup.
|
|
1553
|
+
* @return {Change}
|
|
1554
|
+
*/
|
|
1555
|
+
PersistedModel.getChangeModel = function() {
|
|
1556
|
+
const changeModel = this.Change;
|
|
1557
|
+
assert(changeModel && changeModel.dataSource, "Cannot get a setup Change model for " + this.modelName);
|
|
1558
|
+
return changeModel;
|
|
1559
|
+
};
|
|
1560
|
+
/**
|
|
1561
|
+
* Get the source identifier for this model or dataSource.
|
|
1562
|
+
*
|
|
1563
|
+
* @callback {Function} callback Callback function called with `(err, id)` arguments.
|
|
1564
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
1565
|
+
* @param {String} sourceId Source identifier for the model or dataSource.
|
|
1566
|
+
*/
|
|
1567
|
+
PersistedModel.getSourceId = function(cb) {
|
|
1568
|
+
const dataSource = this.dataSource;
|
|
1569
|
+
if (!dataSource) this.once("dataSourceAttached", this.getSourceId.bind(this, cb));
|
|
1570
|
+
assert(dataSource.connector.name, "Model.getSourceId: cannot get id without dataSource.connector.name");
|
|
1571
|
+
cb(null, [dataSource.connector.name, this.modelName].join("-"));
|
|
1572
|
+
};
|
|
1573
|
+
/**
|
|
1574
|
+
* Enable the tracking of changes made to the model. Usually for replication.
|
|
1575
|
+
*/
|
|
1576
|
+
PersistedModel.enableChangeTracking = function() {
|
|
1577
|
+
const Model = this;
|
|
1578
|
+
this.Change || this._defineChangeModel();
|
|
1579
|
+
const cleanupInterval = Model.settings.changeCleanupInterval || 3e4;
|
|
1580
|
+
assert(this.dataSource, "Cannot enableChangeTracking(): " + this.modelName + " is not attached to a dataSource");
|
|
1581
|
+
const idName = this.getIdName();
|
|
1582
|
+
const idProp = this.definition.properties[idName];
|
|
1583
|
+
const idType = idProp && idProp.type;
|
|
1584
|
+
const idDefn = idProp && idProp.defaultFn;
|
|
1585
|
+
if (idType !== String || !(idDefn === "uuid" || idDefn === "guid")) deprecated("The model " + this.modelName + " is tracking changes, which requires a string id with GUID/UUID default value.");
|
|
1586
|
+
Model.observe("after save", rectifyOnSave);
|
|
1587
|
+
Model.observe("after delete", rectifyOnDelete);
|
|
1588
|
+
if (runtime.isServer && cleanupInterval > 0) {
|
|
1589
|
+
cleanup();
|
|
1590
|
+
const cleanupTimer = setInterval(cleanup, cleanupInterval);
|
|
1591
|
+
if (cleanupTimer && typeof cleanupTimer.unref === "function") cleanupTimer.unref();
|
|
1592
|
+
}
|
|
1593
|
+
function cleanup() {
|
|
1594
|
+
Model.rectifyAllChanges(function(err) {
|
|
1595
|
+
if (err) Model.handleChangeError(err, "cleanup");
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
};
|
|
1599
|
+
function rectifyOnSave(ctx, next) {
|
|
1600
|
+
const instance = ctx.instance || ctx.currentInstance;
|
|
1601
|
+
const id = instance ? instance.getId() : getIdFromWhereByModelId(ctx.Model, ctx.where);
|
|
1602
|
+
if (debug.enabled) {
|
|
1603
|
+
debug("rectifyOnSave %s -> " + (id ? "id %j" : "%s"), ctx.Model.modelName, id ? id : "ALL");
|
|
1604
|
+
debug("context instance:%j currentInstance:%j where:%j data %j", ctx.instance, ctx.currentInstance, ctx.where, ctx.data);
|
|
1605
|
+
}
|
|
1606
|
+
if (id != null) ctx.Model.rectifyChange(id, reportErrorAndNext);
|
|
1607
|
+
else ctx.Model.rectifyAllChanges(reportErrorAndNext);
|
|
1608
|
+
function reportErrorAndNext(err) {
|
|
1609
|
+
if (err) ctx.Model.handleChangeError(err, "after save");
|
|
1610
|
+
next();
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
function rectifyOnDelete(ctx, next) {
|
|
1614
|
+
const id = ctx.instance ? ctx.instance.getId() : getIdFromWhereByModelId(ctx.Model, ctx.where);
|
|
1615
|
+
if (debug.enabled) {
|
|
1616
|
+
debug("rectifyOnDelete %s -> " + (id ? "id %j" : "%s"), ctx.Model.modelName, id ? id : "ALL");
|
|
1617
|
+
debug("context instance:%j where:%j", ctx.instance, ctx.where);
|
|
1618
|
+
}
|
|
1619
|
+
if (id != null) ctx.Model.rectifyChange(id, reportErrorAndNext);
|
|
1620
|
+
else ctx.Model.rectifyAllChanges(reportErrorAndNext);
|
|
1621
|
+
function reportErrorAndNext(err) {
|
|
1622
|
+
if (err) ctx.Model.handleChangeError(err, "after delete");
|
|
1623
|
+
next();
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
function getIdFromWhereByModelId(Model, where) {
|
|
1627
|
+
const idName = Model.getIdName();
|
|
1628
|
+
if (!(idName in where)) return void 0;
|
|
1629
|
+
const id = where[idName];
|
|
1630
|
+
if (typeof id === "string" || typeof id === "number") return id;
|
|
1631
|
+
}
|
|
1632
|
+
PersistedModel._defineChangeModel = function() {
|
|
1633
|
+
const BaseChangeModel = this.registry.getModel("Change");
|
|
1634
|
+
assert(BaseChangeModel, "Change model must be defined before enabling change replication");
|
|
1635
|
+
const additionalChangeModelProperties = this.settings.additionalChangeModelProperties || {};
|
|
1636
|
+
this.Change = BaseChangeModel.extend(this.modelName + "-change", additionalChangeModelProperties, { trackModel: this });
|
|
1637
|
+
if (this.dataSource) attachRelatedModels(this);
|
|
1638
|
+
const self = this;
|
|
1639
|
+
this.on("dataSourceAttached", function() {
|
|
1640
|
+
attachRelatedModels(self);
|
|
1641
|
+
});
|
|
1642
|
+
return this.Change;
|
|
1643
|
+
function attachRelatedModels(self) {
|
|
1644
|
+
self.Change.attachTo(self.dataSource);
|
|
1645
|
+
self.Change.getCheckpointModel().attachTo(self.dataSource);
|
|
1646
|
+
}
|
|
1647
|
+
};
|
|
1648
|
+
PersistedModel.rectifyAllChanges = function(callback) {
|
|
1649
|
+
this.getChangeModel().rectifyAll(callback);
|
|
1650
|
+
};
|
|
1651
|
+
/**
|
|
1652
|
+
* Handle a change error. Override this method in a subclassing model to customize
|
|
1653
|
+
* change error handling.
|
|
1654
|
+
*
|
|
1655
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
|
|
1656
|
+
*/
|
|
1657
|
+
PersistedModel.handleChangeError = function(err, operationName) {
|
|
1658
|
+
if (!err) return;
|
|
1659
|
+
this.emit("error", err, operationName);
|
|
1660
|
+
};
|
|
1661
|
+
/**
|
|
1662
|
+
* Specify that a change to the model with the given ID has occurred.
|
|
1663
|
+
*
|
|
1664
|
+
* @param {*} id The ID of the model that has changed.
|
|
1665
|
+
* @callback {Function} callback
|
|
1666
|
+
* @param {Error} err
|
|
1667
|
+
*/
|
|
1668
|
+
PersistedModel.rectifyChange = function(id, callback) {
|
|
1669
|
+
this.getChangeModel().rectifyModelChanges(this.modelName, [id], callback);
|
|
1670
|
+
};
|
|
1671
|
+
PersistedModel.findLastChange = function(id, cb) {
|
|
1672
|
+
this.getChangeModel().findOne({ where: { modelId: id } }, cb);
|
|
1673
|
+
};
|
|
1674
|
+
PersistedModel.updateLastChange = function(id, data, cb) {
|
|
1675
|
+
const self = this;
|
|
1676
|
+
this.findLastChange(id, function(err, inst) {
|
|
1677
|
+
if (err) return cb(err);
|
|
1678
|
+
if (!inst) {
|
|
1679
|
+
err = new Error(g.f("No change record found for %s with id %s", self.modelName, id));
|
|
1680
|
+
err.statusCode = 404;
|
|
1681
|
+
return cb(err);
|
|
1682
|
+
}
|
|
1683
|
+
inst.updateAttributes(data, cb);
|
|
1684
|
+
});
|
|
1685
|
+
};
|
|
1686
|
+
/**
|
|
1687
|
+
* Create a change stream. [See here for more info](http://loopback.io/doc/en/lb2/Realtime-server-sent-events.html)
|
|
1688
|
+
*
|
|
1689
|
+
* @param {Object} options
|
|
1690
|
+
* @param {Object} options.where Only changes to models matching this where filter will be included in the `ChangeStream`.
|
|
1691
|
+
* @callback {Function} callback
|
|
1692
|
+
* @param {Error} err
|
|
1693
|
+
* @param {ChangeStream} changes
|
|
1694
|
+
*/
|
|
1695
|
+
PersistedModel.createChangeStream = function(options, cb) {
|
|
1696
|
+
if (typeof options === "function") {
|
|
1697
|
+
cb = options;
|
|
1698
|
+
options = void 0;
|
|
1699
|
+
}
|
|
1700
|
+
cb = cb || utils.createPromiseCallback();
|
|
1701
|
+
const idName = this.getIdName();
|
|
1702
|
+
const Model = this;
|
|
1703
|
+
const changes = new PassThrough({ objectMode: true });
|
|
1704
|
+
changes._destroy = function() {
|
|
1705
|
+
changes.end();
|
|
1706
|
+
changes.emit("end");
|
|
1707
|
+
changes.emit("close");
|
|
1708
|
+
};
|
|
1709
|
+
changes.destroy = changes.destroy || changes._destroy;
|
|
1710
|
+
changes.on("error", removeHandlers);
|
|
1711
|
+
changes.on("close", removeHandlers);
|
|
1712
|
+
changes.on("finish", removeHandlers);
|
|
1713
|
+
changes.on("end", removeHandlers);
|
|
1714
|
+
process.nextTick(function() {
|
|
1715
|
+
cb(null, changes);
|
|
1716
|
+
});
|
|
1717
|
+
Model.observe("after save", changeHandler);
|
|
1718
|
+
Model.observe("after delete", deleteHandler);
|
|
1719
|
+
return cb.promise;
|
|
1720
|
+
function changeHandler(ctx, next) {
|
|
1721
|
+
const change = createChangeObject(ctx, "save");
|
|
1722
|
+
if (change) changes.write(change);
|
|
1723
|
+
next();
|
|
1724
|
+
}
|
|
1725
|
+
function deleteHandler(ctx, next) {
|
|
1726
|
+
const change = createChangeObject(ctx, "delete");
|
|
1727
|
+
if (change) changes.write(change);
|
|
1728
|
+
next();
|
|
1729
|
+
}
|
|
1730
|
+
function createChangeObject(ctx, type) {
|
|
1731
|
+
const where = ctx.where;
|
|
1732
|
+
let data = ctx.instance || ctx.data;
|
|
1733
|
+
where && where[idName];
|
|
1734
|
+
let target;
|
|
1735
|
+
if (data && (data[idName] || data[idName] === 0)) target = data[idName];
|
|
1736
|
+
else if (where && (where[idName] || where[idName] === 0)) target = where[idName];
|
|
1737
|
+
const hasTarget = target === 0 || !!target;
|
|
1738
|
+
if (options) {
|
|
1739
|
+
const filtered = filterNodes([data], options);
|
|
1740
|
+
if (filtered.length !== 1) return null;
|
|
1741
|
+
data = filtered[0];
|
|
1742
|
+
}
|
|
1743
|
+
const change = {
|
|
1744
|
+
target,
|
|
1745
|
+
where,
|
|
1746
|
+
data
|
|
1747
|
+
};
|
|
1748
|
+
switch (type) {
|
|
1749
|
+
case "save":
|
|
1750
|
+
if (ctx.isNewInstance === void 0) change.type = hasTarget ? "update" : "create";
|
|
1751
|
+
else change.type = ctx.isNewInstance ? "create" : "update";
|
|
1752
|
+
break;
|
|
1753
|
+
case "delete":
|
|
1754
|
+
change.type = "remove";
|
|
1755
|
+
break;
|
|
1756
|
+
}
|
|
1757
|
+
return change;
|
|
1758
|
+
}
|
|
1759
|
+
function removeHandlers() {
|
|
1760
|
+
Model.removeObserver("after save", changeHandler);
|
|
1761
|
+
Model.removeObserver("after delete", deleteHandler);
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1764
|
+
/**
|
|
1765
|
+
* Get the filter for searching related changes.
|
|
1766
|
+
*
|
|
1767
|
+
* Models should override this function to copy properties
|
|
1768
|
+
* from the model instance filter into the change search filter.
|
|
1769
|
+
*
|
|
1770
|
+
* ```js
|
|
1771
|
+
* module.exports = (TargetModel, config) => {
|
|
1772
|
+
* TargetModel.createChangeFilter = function(since, modelFilter) {
|
|
1773
|
+
* const filter = this.base.createChangeFilter.apply(this, arguments);
|
|
1774
|
+
* if (modelFilter && modelFilter.where && modelFilter.where.tenantId) {
|
|
1775
|
+
* filter.where.tenantId = modelFilter.where.tenantId;
|
|
1776
|
+
* }
|
|
1777
|
+
* return filter;
|
|
1778
|
+
* };
|
|
1779
|
+
* };
|
|
1780
|
+
* ```
|
|
1781
|
+
*
|
|
1782
|
+
* @param {Number} since Return only changes since this checkpoint.
|
|
1783
|
+
* @param {Object} modelFilter Filter describing which model instances to
|
|
1784
|
+
* include in the list of changes.
|
|
1785
|
+
* @returns {Object} The filter object to pass to `Change.find()`. Default:
|
|
1786
|
+
* ```
|
|
1787
|
+
* {where: {checkpoint: {gte: since}, modelName: this.modelName}}
|
|
1788
|
+
* ```
|
|
1789
|
+
*/
|
|
1790
|
+
PersistedModel.createChangeFilter = function(since, modelFilter) {
|
|
1791
|
+
return { where: {
|
|
1792
|
+
checkpoint: { gte: since },
|
|
1793
|
+
modelName: this.modelName
|
|
1794
|
+
} };
|
|
1795
|
+
};
|
|
1796
|
+
/**
|
|
1797
|
+
* Add custom data to the Change instance.
|
|
1798
|
+
*
|
|
1799
|
+
* Models should override this function to duplicate model instance properties
|
|
1800
|
+
* to the Change instance properties, typically to allow the changes() method
|
|
1801
|
+
* to filter the changes using these duplicated properties directly while
|
|
1802
|
+
* querying the Change model.
|
|
1803
|
+
*
|
|
1804
|
+
* ```js
|
|
1805
|
+
* module.exports = (TargetModel, config) => {
|
|
1806
|
+
* TargetModel.prototype.fillCustomChangeProperties = function(change, cb) {
|
|
1807
|
+
* var inst = this;
|
|
1808
|
+
* const base = this.constructor.base;
|
|
1809
|
+
* base.prototype.fillCustomChangeProperties.call(this, change, err => {
|
|
1810
|
+
* if (err) return cb(err);
|
|
1811
|
+
*
|
|
1812
|
+
* if (inst && inst.tenantId) {
|
|
1813
|
+
* change.tenantId = inst.tenantId;
|
|
1814
|
+
* } else {
|
|
1815
|
+
* change.tenantId = null;
|
|
1816
|
+
* }
|
|
1817
|
+
*
|
|
1818
|
+
* cb();
|
|
1819
|
+
* });
|
|
1820
|
+
* };
|
|
1821
|
+
* };
|
|
1822
|
+
* ```
|
|
1823
|
+
*
|
|
1824
|
+
* @callback {Function} callback
|
|
1825
|
+
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb3/Error-object.html).
|
|
1826
|
+
*/
|
|
1827
|
+
PersistedModel.prototype.fillCustomChangeProperties = function(change, cb) {
|
|
1828
|
+
cb();
|
|
1829
|
+
};
|
|
1830
|
+
PersistedModel.setup();
|
|
1831
|
+
return PersistedModel;
|
|
1832
|
+
};
|
|
1833
|
+
}));
|
|
1834
|
+
//#endregion
|
|
1835
|
+
module.exports = require_persisted_model();
|