@fairwords/loopback-connector-es 1.4.2
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/.eslintrc.js +18 -0
- package/CHANGELOG.md +66 -0
- package/Jenkinsfile +7 -0
- package/LICENCE +19 -0
- package/README.md +430 -0
- package/cacert.pem +141 -0
- package/docker-compose-for-testing-all.yml +22 -0
- package/docker-compose-for-testing-v1.yml +11 -0
- package/docker-compose-for-testing-v2.yml +11 -0
- package/docker-compose-for-testing-v5.yml +8 -0
- package/docker-compose.yml +20 -0
- package/docker-entrypoint-es1-plugins.sh +10 -0
- package/docker-entrypoint-es2-plugins.sh +21 -0
- package/index.js +1 -0
- package/lib/automigrate.js +75 -0
- package/lib/esConnector.js +1357 -0
- package/lib/setupIndex.js +57 -0
- package/lib/setupMapping.js +70 -0
- package/lib/setupMappings.js +44 -0
- package/package.json +54 -0
|
@@ -0,0 +1,1357 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var util = require('util');
|
|
4
|
+
var fs = require('fs');
|
|
5
|
+
var path = require('path');
|
|
6
|
+
var _ = require('lodash');
|
|
7
|
+
var Promise = require('bluebird');
|
|
8
|
+
|
|
9
|
+
var log = require('debug')('loopback:connector:elasticsearch');
|
|
10
|
+
|
|
11
|
+
var elasticsearch = require('elasticsearch');
|
|
12
|
+
var deleteByQuery;
|
|
13
|
+
var Connector = require('loopback-connector').Connector;
|
|
14
|
+
|
|
15
|
+
/*eslint no-console: ["error", { allow: ["trace","log"] }] */
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize connector with datasource, configure settings and return
|
|
19
|
+
* @param {object} dataSource
|
|
20
|
+
* @param {function} done callback
|
|
21
|
+
*/
|
|
22
|
+
module.exports.initialize = function (dataSource, callback) {
|
|
23
|
+
if (!elasticsearch) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
var settings = dataSource.settings || {};
|
|
28
|
+
|
|
29
|
+
dataSource.connector = new ESConnector(settings, dataSource);
|
|
30
|
+
|
|
31
|
+
if (callback) {
|
|
32
|
+
dataSource.connector.connect(callback);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Connector constructor
|
|
38
|
+
* @param {object} datasource settings
|
|
39
|
+
* @param {object} dataSource
|
|
40
|
+
* @constructor
|
|
41
|
+
*/
|
|
42
|
+
var ESConnector = function (settings, dataSource) {
|
|
43
|
+
Connector.call(this, 'elasticsearch', settings);
|
|
44
|
+
|
|
45
|
+
this.searchIndex = settings.index || '';
|
|
46
|
+
this.searchIndexSettings = settings.settings || {};
|
|
47
|
+
this.searchType = settings.type || '';
|
|
48
|
+
this.defaultSize = (settings.defaultSize || 10);
|
|
49
|
+
this.idField = 'id';
|
|
50
|
+
this.apiVersion = (settings.apiVersion || '2.x');
|
|
51
|
+
this.refreshOn = (settings.refreshOn || ['create', 'save', 'destroy', 'destroyAll', 'updateAttributes', 'updateOrCreate', 'updateAll']);
|
|
52
|
+
|
|
53
|
+
this.debug = settings.debug || log.enabled;
|
|
54
|
+
if (this.debug) {
|
|
55
|
+
log('Settings: %j', settings);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.dataSource = dataSource;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Inherit the prototype methods
|
|
63
|
+
*/
|
|
64
|
+
util.inherits(ESConnector, Connector);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Generate a client configuration object based on settings.
|
|
68
|
+
*/
|
|
69
|
+
ESConnector.prototype.getClientConfig = function () {
|
|
70
|
+
// http://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/configuration.html
|
|
71
|
+
var config = {
|
|
72
|
+
hosts: this.settings.hosts || {host: '127.0.0.1', port: 9200},
|
|
73
|
+
requestTimeout: this.settings.requestTimeout,
|
|
74
|
+
apiVersion: this.settings.apiVersion,
|
|
75
|
+
log: this.settings.log || 'error',
|
|
76
|
+
suggestCompression: true
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (this.settings.amazonES) {
|
|
80
|
+
config.connectionClass = require('http-aws-es');
|
|
81
|
+
config.amazonES = this.settings.amazonES || {
|
|
82
|
+
region: 'us-east-1',
|
|
83
|
+
accessKey: 'AKID',
|
|
84
|
+
secretKey: 'secret'
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (this.settings.ssl) {
|
|
89
|
+
config.ssl = {
|
|
90
|
+
ca: (this.settings.ssl.ca) ? fs.readFileSync(path.join(__dirname, this.settings.ssl.ca)) : fs.readFileSync(path.join(__dirname, '..', 'cacert.pem')),
|
|
91
|
+
rejectUnauthorized: this.settings.ssl.rejectUnauthorized || true
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// Note: http://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/configuration.html
|
|
95
|
+
// Due to the complex nature of the configuration, the config object you pass in will be modified
|
|
96
|
+
// and can only be used to create one Client instance.
|
|
97
|
+
// Related Github issue: https://github.com/elasticsearch/elasticsearch-js/issues/33
|
|
98
|
+
// Luckily getClientConfig() pretty much clones settings so we shouldn't have to worry about it.
|
|
99
|
+
return config;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Connect to Elasticsearch client
|
|
104
|
+
* @param {Function} [callback] The callback function
|
|
105
|
+
*
|
|
106
|
+
* @callback callback
|
|
107
|
+
* @param {Error} err The error object
|
|
108
|
+
* @param {Db} db The elasticsearch client
|
|
109
|
+
*/
|
|
110
|
+
ESConnector.prototype.connect = function (callback) {
|
|
111
|
+
// TODO: throw error if callback isn't provided?
|
|
112
|
+
// what are the corner-cases when the loopback framework does not provide callback
|
|
113
|
+
// and we need to be able to live with that?
|
|
114
|
+
var self = this;
|
|
115
|
+
if (self.db) {
|
|
116
|
+
process.nextTick(function () {
|
|
117
|
+
callback && callback(null, self.db);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
self.db = new elasticsearch.Client(self.getClientConfig());
|
|
122
|
+
if (self.settings.apiVersion.indexOf('2') === 0) {
|
|
123
|
+
log('injecting deleteByQuery');
|
|
124
|
+
deleteByQuery = require('elastic-deletebyquery');
|
|
125
|
+
deleteByQuery(self.db);
|
|
126
|
+
self.db.deleteByQuery = Promise.promisify(self.db.deleteByQuery);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// NOTE: any & all indices and mappings will be created or their existence verified before proceeding
|
|
130
|
+
if (self.settings.mappings) {
|
|
131
|
+
self.setupMappings()
|
|
132
|
+
.then(function () {
|
|
133
|
+
log('ESConnector.prototype.connect', 'setupMappings', 'finished');
|
|
134
|
+
callback && callback(null, self.db);
|
|
135
|
+
})
|
|
136
|
+
.catch(function (err) {
|
|
137
|
+
log('ESConnector.prototype.connect', 'setupMappings', 'failed', err);
|
|
138
|
+
callback && callback(err);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
process.nextTick(function () {
|
|
143
|
+
callback && callback(null, self.db);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Delete a mapping (type definition) along with its data.
|
|
151
|
+
*
|
|
152
|
+
* @param modelNames
|
|
153
|
+
* @param callback
|
|
154
|
+
*/
|
|
155
|
+
ESConnector.prototype.removeMappings = function (modelNames, callback) {
|
|
156
|
+
var self = this;
|
|
157
|
+
var db = self.db;
|
|
158
|
+
var settings = self.settings;
|
|
159
|
+
if (_.isFunction(modelNames)) {
|
|
160
|
+
callback = modelNames;
|
|
161
|
+
modelNames = _.pluck(settings.mappings, 'name');
|
|
162
|
+
}
|
|
163
|
+
log('ESConnector.prototype.removeMappings', 'modelNames', modelNames);
|
|
164
|
+
|
|
165
|
+
var mappingsToRemove = _.filter(settings.mappings, function (mapping) {
|
|
166
|
+
return !modelNames || _.includes(modelNames, mapping.name);
|
|
167
|
+
});
|
|
168
|
+
log('ESConnector.prototype.removeMappings', 'mappingsToRemove', _.pluck(mappingsToRemove, 'name'));
|
|
169
|
+
|
|
170
|
+
Promise.map(
|
|
171
|
+
mappingsToRemove,
|
|
172
|
+
function (mapping) {
|
|
173
|
+
var defaults = self.addDefaults(mapping.name, 'removeMappings');
|
|
174
|
+
log('ESConnector.prototype.removeMappings', 'calling self.db.indices.existsType()');
|
|
175
|
+
return db.indices.existsType(defaults).then(function (exists) {
|
|
176
|
+
if (!exists) return Promise.resolve();
|
|
177
|
+
log('ESConnector.prototype.removeMappings', 'calling self.db.indices.deleteMapping()');
|
|
178
|
+
return db.indices.deleteMapping(defaults)
|
|
179
|
+
.then(function (body) {
|
|
180
|
+
log('ESConnector.prototype.removeMappings', mapping.name, body);
|
|
181
|
+
return Promise.resolve();
|
|
182
|
+
},
|
|
183
|
+
function (err) {
|
|
184
|
+
console.trace(err.message);
|
|
185
|
+
return Promise.reject(err);
|
|
186
|
+
});
|
|
187
|
+
}, function (err) {
|
|
188
|
+
console.trace(err.message);
|
|
189
|
+
return Promise.reject(err);
|
|
190
|
+
});
|
|
191
|
+
},
|
|
192
|
+
{concurrency: 1}
|
|
193
|
+
)
|
|
194
|
+
.then(function () {
|
|
195
|
+
log('ESConnector.prototype.removeMappings', 'finished');
|
|
196
|
+
callback(null, self.db); // TODO: what does the connector framework want back as arguments here?
|
|
197
|
+
})
|
|
198
|
+
.catch(function (err) {
|
|
199
|
+
log('ESConnector.prototype.removeMappings', 'failed');
|
|
200
|
+
callback(err);
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
ESConnector.prototype.setupMappings = require('./setupMappings.js')({
|
|
205
|
+
log: log
|
|
206
|
+
, lodash: _
|
|
207
|
+
, bluebird: Promise
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
ESConnector.prototype.setupMapping = require('./setupMapping.js')({
|
|
211
|
+
log: log
|
|
212
|
+
, lodash: _
|
|
213
|
+
, bluebird: Promise
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
ESConnector.prototype.setupIndex = require('./setupIndex.js')({
|
|
217
|
+
log: log
|
|
218
|
+
, bluebird: Promise
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Ping to test elastic connection
|
|
224
|
+
* @returns {String} with ping result
|
|
225
|
+
*/
|
|
226
|
+
ESConnector.prototype.ping = function (cb) {
|
|
227
|
+
this.db.ping({
|
|
228
|
+
requestTimeout: 1000
|
|
229
|
+
}, function (error) {
|
|
230
|
+
if (error) {
|
|
231
|
+
log('Could not ping ES.');
|
|
232
|
+
cb(error);
|
|
233
|
+
} else {
|
|
234
|
+
log('Pinged ES successfully.');
|
|
235
|
+
cb();
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Return connector type
|
|
242
|
+
* @returns {String} type description
|
|
243
|
+
*/
|
|
244
|
+
ESConnector.prototype.getTypes = function () {
|
|
245
|
+
return [this.name];
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get value from property checking type
|
|
250
|
+
* @param {object} property
|
|
251
|
+
* @param {String} value
|
|
252
|
+
* @returns {object}
|
|
253
|
+
*/
|
|
254
|
+
ESConnector.prototype.getValueFromProperty = function (property, value) {
|
|
255
|
+
if (property.type instanceof Array) {
|
|
256
|
+
if (!value || (value.length === 0)) {
|
|
257
|
+
return new Array();
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
return value;
|
|
261
|
+
}
|
|
262
|
+
} else if (property.type === String) {
|
|
263
|
+
return value.toString();
|
|
264
|
+
} else if (property.type === Number) {
|
|
265
|
+
return Number(value);
|
|
266
|
+
} else if (property.type === Date) {
|
|
267
|
+
return new Date(value);
|
|
268
|
+
} else {
|
|
269
|
+
return value;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Match and transform data structure to modelName
|
|
275
|
+
* @param {String} modelName name
|
|
276
|
+
* @param {Object} data from DB
|
|
277
|
+
* @returns {object} modeled document
|
|
278
|
+
*/
|
|
279
|
+
ESConnector.prototype.matchDataToModel = function (modelName, data, esId, idName) {
|
|
280
|
+
//log('ESConnector.prototype.matchDataToModel', 'modelName', modelName, 'data', JSON.stringify(data,null,0));
|
|
281
|
+
var self = this;
|
|
282
|
+
if (!data) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
var document = {};
|
|
287
|
+
|
|
288
|
+
var properties = this._models[modelName].properties;
|
|
289
|
+
_.assign(document, data); // it can't be this easy, can it?
|
|
290
|
+
document[idName] = esId;
|
|
291
|
+
|
|
292
|
+
for (var propertyName in properties) {
|
|
293
|
+
var propertyValue = data[propertyName];
|
|
294
|
+
// log('ESConnector.prototype.matchDataToModel', propertyName, propertyValue);
|
|
295
|
+
if (propertyValue !== undefined && propertyValue !== null) {
|
|
296
|
+
document[propertyName] = self.getValueFromProperty(properties[propertyName], propertyValue);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
log('ESConnector.prototype.matchDataToModel', 'document', JSON.stringify(document, null, 0));
|
|
300
|
+
return document;
|
|
301
|
+
} catch (err) {
|
|
302
|
+
console.trace(err.message);
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Convert data source to model
|
|
309
|
+
* @param {String} model name
|
|
310
|
+
* @param {Object} data object
|
|
311
|
+
* @returns {object} modeled document
|
|
312
|
+
*/
|
|
313
|
+
ESConnector.prototype.dataSourceToModel = function (modelName, data, idName) {
|
|
314
|
+
log('ESConnector.prototype.dataSourceToModel', 'modelName', modelName, 'data', JSON.stringify(data, null, 0));
|
|
315
|
+
|
|
316
|
+
//return data._source; // TODO: super-simplify?
|
|
317
|
+
return this.matchDataToModel(modelName, data._source, data._id, idName);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Add defaults such as index name and type
|
|
322
|
+
*
|
|
323
|
+
* @param {String} modelName
|
|
324
|
+
* @param {String} functionName The caller function name
|
|
325
|
+
* @returns {object} Filter with index and type
|
|
326
|
+
*/
|
|
327
|
+
ESConnector.prototype.addDefaults = function (modelName, functionName) {
|
|
328
|
+
var self = this;
|
|
329
|
+
log('ESConnector.prototype.addDefaults', 'modelName:', modelName);
|
|
330
|
+
|
|
331
|
+
//TODO: fetch index and type from `self.settings.mappings` too
|
|
332
|
+
var indexFromDatasource, typeFromDatasource;
|
|
333
|
+
var mappingFromDatasource = _.find(self.settings.mappings,
|
|
334
|
+
function (mapping) {
|
|
335
|
+
return mapping.name === modelName;
|
|
336
|
+
});
|
|
337
|
+
if (mappingFromDatasource) {
|
|
338
|
+
indexFromDatasource = mappingFromDatasource.index;
|
|
339
|
+
typeFromDatasource = mappingFromDatasource.type;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
var filter = {};
|
|
343
|
+
if (this.searchIndex) {
|
|
344
|
+
filter.index = indexFromDatasource || this.searchIndex;
|
|
345
|
+
}
|
|
346
|
+
filter.type = typeFromDatasource || this.searchType || modelName;
|
|
347
|
+
|
|
348
|
+
// When changing data, wait until the change has been indexed so it is instantly available for search
|
|
349
|
+
if(this.refreshOn.indexOf(functionName) != -1) {
|
|
350
|
+
filter.refresh = true;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
var modelClass = this._models[modelName];
|
|
354
|
+
if (modelClass && _.isObject(modelClass.settings.elasticsearch) && _.isObject(modelClass.settings.elasticsearch.settings)) {
|
|
355
|
+
_.extend(filter, modelClass.settings.elasticsearch.settings);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (functionName && modelClass && _.isObject(modelClass.settings.elasticsearch) && _.isObject(modelClass.settings.elasticsearch[functionName])) {
|
|
359
|
+
_.extend(filter, modelClass.settings.elasticsearch[functionName]);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
log('ESConnector.prototype.addDefaults', 'filter:', filter);
|
|
363
|
+
return filter;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Make filter from criteria, data index and type
|
|
368
|
+
* Ex:
|
|
369
|
+
* {"body": {"query": {"match": {"title": "Futuro"}}}}
|
|
370
|
+
* {"q" : "Futuro"}
|
|
371
|
+
* @param {String} modelName filter
|
|
372
|
+
* @param {Object} criteria filter
|
|
373
|
+
* @param {number} size of rows to return, if null then skip
|
|
374
|
+
* @param {number} offset to return, if null then skip
|
|
375
|
+
* @returns {object} filter
|
|
376
|
+
*/
|
|
377
|
+
ESConnector.prototype.buildFilter = function (modelName, idName, criteria, size, offset) {
|
|
378
|
+
var self = this;
|
|
379
|
+
log('ESConnector.prototype.buildFilter', 'model', modelName, 'idName', idName,
|
|
380
|
+
'criteria', JSON.stringify(criteria, null, 0));
|
|
381
|
+
|
|
382
|
+
if (idName === undefined || idName === null) {
|
|
383
|
+
throw new Error('idName not set!');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
var filter = this.addDefaults(modelName, 'buildFilter');
|
|
387
|
+
filter.body = {};
|
|
388
|
+
|
|
389
|
+
if (size !== undefined && size !== null) {
|
|
390
|
+
filter.size = size;
|
|
391
|
+
}
|
|
392
|
+
if (offset !== undefined && offset !== null) {
|
|
393
|
+
filter.from = offset;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (criteria) {
|
|
397
|
+
// `criteria` is set by app-devs, therefore, it overrides any connector level arguments
|
|
398
|
+
if (criteria.limit !== undefined && criteria.limit !== null) {
|
|
399
|
+
filter.size = criteria.limit;
|
|
400
|
+
}
|
|
401
|
+
if (criteria.skip !== undefined && criteria.skip !== null) {
|
|
402
|
+
filter.from = criteria.skip;
|
|
403
|
+
}
|
|
404
|
+
else if (criteria.offset !== undefined && criteria.offset !== null) { // use offset as an alias for skip
|
|
405
|
+
filter.from = criteria.offset;
|
|
406
|
+
}
|
|
407
|
+
if (criteria.fields) {
|
|
408
|
+
// { fields: {propertyName: <true|false>, propertyName: <true|false>, ... } }
|
|
409
|
+
//filter.body.fields = self.buildOrder(model, idName, criteria.fields);
|
|
410
|
+
// TODO: make it so
|
|
411
|
+
// http://www.elastic.co/guide/en/elasticsearch/reference/1.x/search-request-source-filtering.html
|
|
412
|
+
// http://www.elastic.co/guide/en/elasticsearch/reference/1.x/search-request-fields.html
|
|
413
|
+
/*POST /shakespeare/User/_search
|
|
414
|
+
{
|
|
415
|
+
"_source": {
|
|
416
|
+
"include": ["seq"],
|
|
417
|
+
"exclude": ["seq"]
|
|
418
|
+
}
|
|
419
|
+
}*/
|
|
420
|
+
|
|
421
|
+
/* @raymondfeng and @bajtos - I'm observing something super strange,
|
|
422
|
+
i haven't implemented the FIELDS filter for elasticsearch connector
|
|
423
|
+
but the test which should fail until I implement such a feature ... is actually passing!
|
|
424
|
+
... did someone at some point of time implement an in-memory filter for FIELDS
|
|
425
|
+
in the underlying loopback-connector implementation? */
|
|
426
|
+
}
|
|
427
|
+
if (criteria.order) {
|
|
428
|
+
log('ESConnector.prototype.buildFilter', 'will delegate sorting to buildOrder()');
|
|
429
|
+
filter.body.sort = self.buildOrder(modelName, idName, criteria.order);
|
|
430
|
+
}
|
|
431
|
+
else { // TODO: expensive~ish and no clear guidelines so turn it off?
|
|
432
|
+
//var idNames = this.idNames(model); // TODO: support for compound ids?
|
|
433
|
+
var modelProperties = this._models[modelName].properties;
|
|
434
|
+
if (idName === 'id' && modelProperties.id.generated) {
|
|
435
|
+
//filter.body.sort = ['_id']; // requires mapping to contain: '_id' : {'index' : 'not_analyzed','store' : true}
|
|
436
|
+
log('ESConnector.prototype.buildFilter', 'will sort on _uid by default when IDs are meant to be auto-generated by elasticsearch');
|
|
437
|
+
filter.body.sort = ['_uid'];
|
|
438
|
+
} else {
|
|
439
|
+
log('ESConnector.prototype.buildFilter', 'will sort on loopback specified IDs');
|
|
440
|
+
filter.body.sort = [idName]; // default sort should be based on fields marked as id
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (criteria.where) {
|
|
444
|
+
filter.body.query = self.buildWhere(modelName, idName, criteria.where).query;
|
|
445
|
+
}
|
|
446
|
+
// TODO: Include filter
|
|
447
|
+
else if (criteria.suggests) { // TODO: remove HACK!!!
|
|
448
|
+
if (self.settings.apiVersion.indexOf('5') === 0) {
|
|
449
|
+
filter.body = {
|
|
450
|
+
suggest: criteria.suggests
|
|
451
|
+
}; // assume that the developer has provided ES compatible DSL
|
|
452
|
+
}
|
|
453
|
+
else if (
|
|
454
|
+
self.settings.apiVersion.indexOf('2') === 0 ||
|
|
455
|
+
self.settings.apiVersion.indexOf('1') === 0)
|
|
456
|
+
{
|
|
457
|
+
filter.body = criteria.suggests; // assume that the developer has provided ES compatible DSL
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
else if (criteria.native) {
|
|
461
|
+
filter.body = criteria.native; // assume that the developer has provided ES compatible DSL
|
|
462
|
+
}
|
|
463
|
+
else if (_.keys(criteria).length === 0) {
|
|
464
|
+
filter.body = {
|
|
465
|
+
'query': {
|
|
466
|
+
'match_all': {}
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
log('ESConnector.prototype.buildFilter', 'constructed', JSON.stringify(filter, null, 0));
|
|
473
|
+
return filter;
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* 1. Words of wisdom from @doublemarked:
|
|
478
|
+
* > When writing a query without an order specified, the author should not assume any reliable order.
|
|
479
|
+
* > So if we’re not assuming any order, there is not a compelling reason to potentially slow down
|
|
480
|
+
* > the query by enforcing a default order.
|
|
481
|
+
* 2. Yet, most connector implementations do enforce a default order ... what to do?
|
|
482
|
+
*
|
|
483
|
+
* @param model
|
|
484
|
+
* @param idName
|
|
485
|
+
* @param order
|
|
486
|
+
* @returns {Array}
|
|
487
|
+
*/
|
|
488
|
+
ESConnector.prototype.buildOrder = function (model, idName, order) {
|
|
489
|
+
var sort = [];
|
|
490
|
+
|
|
491
|
+
var keys = order;
|
|
492
|
+
if (typeof keys === 'string') {
|
|
493
|
+
keys = keys.split(',');
|
|
494
|
+
}
|
|
495
|
+
for (var index = 0, len = keys.length; index < len; index++) {
|
|
496
|
+
var m = keys[index].match(/\s+(A|DE)SC$/);
|
|
497
|
+
var key = keys[index];
|
|
498
|
+
key = key.replace(/\s+(A|DE)SC$/, '').trim();
|
|
499
|
+
if (key === 'id' || key === idName) {
|
|
500
|
+
key = '_uid';
|
|
501
|
+
}
|
|
502
|
+
if (m && m[1] === 'DE') {
|
|
503
|
+
//sort[key] = -1;
|
|
504
|
+
var temp = {};
|
|
505
|
+
temp[key] = 'desc';
|
|
506
|
+
sort.push(temp);
|
|
507
|
+
} else {
|
|
508
|
+
//sort[key] = 1;
|
|
509
|
+
sort.push(key);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return sort;
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
ESConnector.prototype.buildWhere = function (model, idName, where) {
|
|
517
|
+
var self = this;
|
|
518
|
+
log('ESConnector.prototype.buildWhere', 'model', model, 'idName', idName, 'where', JSON.stringify(where, null, 0));
|
|
519
|
+
|
|
520
|
+
var body = {
|
|
521
|
+
query: {
|
|
522
|
+
bool: {
|
|
523
|
+
must: [],
|
|
524
|
+
should: [],
|
|
525
|
+
must_not: []
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
self.buildNestedQueries(body, model, idName, where);
|
|
531
|
+
if (body && body.query && body.query.bool && body.query.bool.must && body.query.bool.must.length === 0) {
|
|
532
|
+
delete body.query.bool['must']; // jshint ignore:line
|
|
533
|
+
}
|
|
534
|
+
if (body && body.query && body.query.bool && body.query.bool.should && body.query.bool.should.length === 0) {
|
|
535
|
+
delete body.query.bool['should']; // jshint ignore:line
|
|
536
|
+
}
|
|
537
|
+
if (body && body.query && body.query.bool && body.query.bool.must_not && body.query.bool.must_not.length === 0) { // jshint ignore:line
|
|
538
|
+
delete body.query.bool['must_not']; // jshint ignore:line
|
|
539
|
+
}
|
|
540
|
+
if (body && body.query && body.query.bool && _.isEmpty(body.query.bool)) {
|
|
541
|
+
delete body.query['bool'];// jshint ignore:line
|
|
542
|
+
}
|
|
543
|
+
if (body && body.query && _.isEmpty(body.query)) {
|
|
544
|
+
body.query = {
|
|
545
|
+
match_all: {}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return body;
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
ESConnector.prototype.buildNestedQueries = function (body, model, idName, where) {
|
|
552
|
+
/**
|
|
553
|
+
* Return an empty match all object if no property is set in where filter
|
|
554
|
+
* @example {where: {}}
|
|
555
|
+
*/
|
|
556
|
+
if (_.keys(where).length === 0) {
|
|
557
|
+
body.match_all = {};
|
|
558
|
+
log('ESConnector.prototype.buildNestedQueries', '\nbody', JSON.stringify(body, null, 0));
|
|
559
|
+
return body;
|
|
560
|
+
}
|
|
561
|
+
var rootPath = body.query;
|
|
562
|
+
ESConnector.prototype.buildDeepNestedQueries(true, idName, where, body, rootPath);
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
ESConnector.prototype.buildDeepNestedQueries = function (root, idName, where, body, path) {
|
|
566
|
+
var self = this;
|
|
567
|
+
_.forEach(where, function (value, key) {
|
|
568
|
+
var cond = value;
|
|
569
|
+
if (key === 'id' || key === idName) {
|
|
570
|
+
key = '_id';
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (key === 'and' && Array.isArray(value)) {
|
|
574
|
+
var andPath;
|
|
575
|
+
if (root) {
|
|
576
|
+
andPath = path.bool.must;
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
var andObject = {bool: {must: []}};
|
|
580
|
+
andPath = andObject.bool.must;
|
|
581
|
+
path.push(andObject);
|
|
582
|
+
}
|
|
583
|
+
cond.map(function (c) {
|
|
584
|
+
log('ESConnector.prototype.buildDeepNestedQueries', 'mapped', 'body', JSON.stringify(body, null, 0));
|
|
585
|
+
self.buildDeepNestedQueries(false, idName, c, body, andPath);
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
else if (key === 'or' && Array.isArray(value)) {
|
|
589
|
+
var orPath;
|
|
590
|
+
if (root) {
|
|
591
|
+
orPath = path.bool.should;
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
var orObject = {bool: {should: []}};
|
|
595
|
+
orPath = orObject.bool.should;
|
|
596
|
+
path.push(orObject);
|
|
597
|
+
}
|
|
598
|
+
cond.map(function (c) {
|
|
599
|
+
log('ESConnector.prototype.buildDeepNestedQueries', 'mapped', 'body', JSON.stringify(body, null, 0));
|
|
600
|
+
self.buildDeepNestedQueries(false, idName, c, body, orPath);
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
else if (key === 'not') {
|
|
604
|
+
var notPath;
|
|
605
|
+
if (root) {
|
|
606
|
+
notPath = path.bool.must_not;
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
var notObject = {bool: {must_not: []}};
|
|
610
|
+
notPath = notObject.bool.must_not;
|
|
611
|
+
path.push(notObject);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if(Array.isArray(value)) {
|
|
615
|
+
cond.map(function (c) {
|
|
616
|
+
log('ESConnector.prototype.buildDeepNestedQueries', 'mapped', 'body', JSON.stringify(body, null, 0));
|
|
617
|
+
self.buildDeepNestedQueries(false, idName, c, body, notPath);
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
log('ESConnector.prototype.buildDeepNestedQueries', 'mapped', 'body', JSON.stringify(body, null, 0));
|
|
622
|
+
self.buildDeepNestedQueries(false, idName, cond, body, notPath);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
else if (key == "match"){
|
|
626
|
+
/* special case for phrases
|
|
627
|
+
usage in Loopback : {where: {"match": {match obj...}}
|
|
628
|
+
*/
|
|
629
|
+
var query = {match: cond};
|
|
630
|
+
if (root) {
|
|
631
|
+
path.bool.must.push(query);
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
path.push(query);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
else if (key == "match_phrase"){
|
|
638
|
+
/* special case for phrases
|
|
639
|
+
usage in Loopback : {where: {"match_phrase": {"message" : "best wishes"}}
|
|
640
|
+
*/
|
|
641
|
+
var phraseQuery = {match: {}};
|
|
642
|
+
|
|
643
|
+
var matchField = Object.keys(cond)[0];
|
|
644
|
+
|
|
645
|
+
phraseQuery.match[matchField] = {
|
|
646
|
+
query: cond[matchField],
|
|
647
|
+
type: 'phrase'
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
if (root) {
|
|
651
|
+
path.bool.must.push(phraseQuery);
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
path.push(phraseQuery);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
else if (key == "wildcard"){
|
|
658
|
+
/* special case for phrases
|
|
659
|
+
usage in Loopback : {where: {"wildcard": {"message" : "be*shes" }}
|
|
660
|
+
*/
|
|
661
|
+
var query = {wildcard: cond};
|
|
662
|
+
if (root) {
|
|
663
|
+
path.bool.must.push(query);
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
path.push(query);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
var spec = false;
|
|
671
|
+
var options = null;
|
|
672
|
+
if (cond && cond.constructor.name === 'Object') { // need to understand
|
|
673
|
+
options = cond.options;
|
|
674
|
+
spec = Object.keys(cond)[0];
|
|
675
|
+
cond = cond[spec];
|
|
676
|
+
}
|
|
677
|
+
log('ESConnector.prototype.buildNestedQueries',
|
|
678
|
+
'spec', spec, 'key', key, 'cond', JSON.stringify(cond,null,0), 'options', options);
|
|
679
|
+
if (spec) {
|
|
680
|
+
if (spec === 'gte' || spec === 'gt' || spec === 'lte' || spec === 'lt') {
|
|
681
|
+
var rangeQuery = {range:{}};
|
|
682
|
+
var rangeQueryGuts = {};
|
|
683
|
+
rangeQueryGuts[spec] = cond;
|
|
684
|
+
rangeQuery.range[key] = rangeQueryGuts;
|
|
685
|
+
if(root){
|
|
686
|
+
path.bool.must.push(rangeQuery);
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
path.push(rangeQuery);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Logic for loopback `between` filter of where
|
|
695
|
+
* @example {where: {size: {between: [0,7]}}}
|
|
696
|
+
*/
|
|
697
|
+
if (spec === 'between') {
|
|
698
|
+
if (cond.length == 2 && (cond[0] <= cond[1])) {
|
|
699
|
+
var betweenArray = {range: {}};
|
|
700
|
+
betweenArray.range[key] = {
|
|
701
|
+
gte: cond[0],
|
|
702
|
+
lte: cond[1]
|
|
703
|
+
};
|
|
704
|
+
if(root){
|
|
705
|
+
path.bool.must.push(betweenArray);
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
path.push(betweenArray);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Logic for loopback `inq`(include) filter of where
|
|
714
|
+
* @example {where: { property: { inq: [val1, val2, ...]}}}
|
|
715
|
+
*/
|
|
716
|
+
if (spec === 'inq') {
|
|
717
|
+
var inArray = {terms: {}};
|
|
718
|
+
inArray.terms[key] = cond;
|
|
719
|
+
if (root) {
|
|
720
|
+
path.bool.must.push(inArray);
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
path.push(inArray);
|
|
724
|
+
}
|
|
725
|
+
log('ESConnector.prototype.buildDeepNestedQueries',
|
|
726
|
+
'body', body,
|
|
727
|
+
'inArray', JSON.stringify(inArray, null, 0));
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Logic for loopback `nin`(not include) filter of where
|
|
732
|
+
* @example {where: { property: { nin: [val1, val2, ...]}}}
|
|
733
|
+
*/
|
|
734
|
+
if (spec === 'nin') {
|
|
735
|
+
var notInArray = { terms : {}};
|
|
736
|
+
notInArray.terms[key] = cond;
|
|
737
|
+
if(root){
|
|
738
|
+
path.bool.must_not.push(notInArray);
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
path.push({bool: {must_not: [notInArray]}});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Logic for loopback `neq` (not equal) filter of where
|
|
747
|
+
* @example {where: {role: {neq: 'lead' }}}
|
|
748
|
+
*/
|
|
749
|
+
if (spec === 'neq') {
|
|
750
|
+
/**
|
|
751
|
+
* First - filter the documents where the given property exists
|
|
752
|
+
* @type {{exists: {field: *}}}
|
|
753
|
+
*/
|
|
754
|
+
// var missingFilter = {exists :{field : key}};
|
|
755
|
+
/**
|
|
756
|
+
* Second - find the document where value not equals the given value
|
|
757
|
+
* @type {{term: {}}}
|
|
758
|
+
*/
|
|
759
|
+
var notEqual = { term : {}};
|
|
760
|
+
notEqual.term[key] = cond;
|
|
761
|
+
/**
|
|
762
|
+
* Apply the given filter in the main filter(body) and on given path
|
|
763
|
+
*/
|
|
764
|
+
if (root) {
|
|
765
|
+
path.bool.must_not.push(notEqual);
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
path.push({bool:{must_not: [notEqual]}});
|
|
769
|
+
}
|
|
770
|
+
// body.query.bool.must.push(missingFilter);
|
|
771
|
+
}
|
|
772
|
+
// TODO: near - For geolocations, return the closest points, sorted in order of distance. Use with limit to return the n closest points.
|
|
773
|
+
// TODO: like, nlike
|
|
774
|
+
// TODO: ilike, inlike
|
|
775
|
+
// TODO: regex
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
var nestedQuery = {match: {}};
|
|
779
|
+
// var nestedQuery = {query: { match: {}}};
|
|
780
|
+
nestedQuery.match[key] = value;
|
|
781
|
+
if (root) {
|
|
782
|
+
path.bool.must.push(nestedQuery)
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
path.push(nestedQuery);
|
|
786
|
+
}
|
|
787
|
+
log('ESConnector.prototype.buildDeepNestedQueries',
|
|
788
|
+
'body', body,
|
|
789
|
+
'nestedQuery', JSON.stringify(nestedQuery, null, 0));
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Get document Id validating data
|
|
797
|
+
* @param {String} id
|
|
798
|
+
* @returns {Number} Id
|
|
799
|
+
* @constructor
|
|
800
|
+
*/
|
|
801
|
+
ESConnector.prototype.getDocumentId = function (id) {
|
|
802
|
+
try {
|
|
803
|
+
if (typeof id !== 'string') {
|
|
804
|
+
return id.toString();
|
|
805
|
+
} else {
|
|
806
|
+
return id;
|
|
807
|
+
}
|
|
808
|
+
} catch (e) {
|
|
809
|
+
return id;
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Implement CRUD Level I - Key methods to be implemented by a connector to support full CRUD
|
|
815
|
+
* > Create a new model instance
|
|
816
|
+
* > CRUDConnector.prototype.create = function (model, data, callback) {...};
|
|
817
|
+
* > Query model instances by filter
|
|
818
|
+
* > CRUDConnector.prototype.all = function (model, filter, callback) {...};
|
|
819
|
+
* > Delete model instances by query
|
|
820
|
+
* > CRUDConnector.prototype.destroyAll = function (model, where, callback) {...};
|
|
821
|
+
* > Update model instances by query
|
|
822
|
+
* > CRUDConnector.prototype.updateAll = function (model, where, data, callback) {...};
|
|
823
|
+
* > Count model instances by query
|
|
824
|
+
* > CRUDConnector.prototype.count = function (model, callback, where) {...};
|
|
825
|
+
* > getDefaultIdType
|
|
826
|
+
* > very useful for setting a default type for IDs like "string" rather than "number"
|
|
827
|
+
};
|
|
828
|
+
*/
|
|
829
|
+
|
|
830
|
+
ESConnector.prototype.getDefaultIdType = function () {
|
|
831
|
+
return String;
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Create a new model instance
|
|
836
|
+
* @param {String} model name
|
|
837
|
+
* @param {object} data info
|
|
838
|
+
* @param {Function} done - invoke the callback with the created model's id as an argument
|
|
839
|
+
*/
|
|
840
|
+
ESConnector.prototype.create = function (model, data, done) {
|
|
841
|
+
var self = this;
|
|
842
|
+
if (self.debug) {
|
|
843
|
+
log('ESConnector.prototype.create', model, data);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
var idValue = self.getIdValue(model, data);
|
|
847
|
+
var idName = self.idName(model);
|
|
848
|
+
log('ESConnector.prototype.create', 'idName', idName, 'idValue', idValue);
|
|
849
|
+
//TODO: If model has custom id with generated false and if Id field is not prepopulated then create should fail.
|
|
850
|
+
//TODO: If model Id is not string and generated is true then create should fail because the auto generated es id is of type string and we cannot change it.
|
|
851
|
+
var document = self.addDefaults(model, 'create');
|
|
852
|
+
document[self.idField] = self.getDocumentId(idValue);
|
|
853
|
+
document.body = {};
|
|
854
|
+
_.assign(document.body, data);
|
|
855
|
+
log('ESConnector.prototype.create', 'document', document);
|
|
856
|
+
var method = 'create';
|
|
857
|
+
if(!document[self.idField]) {
|
|
858
|
+
method = 'index'; // if there is no/empty id field, we must use the index method to create it (API 5.0)
|
|
859
|
+
}
|
|
860
|
+
self.db[method](
|
|
861
|
+
document
|
|
862
|
+
).then(
|
|
863
|
+
function (response) {
|
|
864
|
+
log('ESConnector.prototype.create', 'response', response);
|
|
865
|
+
log('ESConnector.prototype.create', 'will invoke callback with id:', response._id);
|
|
866
|
+
done(null, response._id); // the connector framework expects the id as a return value
|
|
867
|
+
}, function (err) {
|
|
868
|
+
console.trace(err.message);
|
|
869
|
+
if (err) {
|
|
870
|
+
return done(err, null);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
);
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Query model instances by filter
|
|
878
|
+
* @param {String} model The model name
|
|
879
|
+
* @param {Object} filter The filter
|
|
880
|
+
* @param {Function} done callback function
|
|
881
|
+
*
|
|
882
|
+
* NOTE: UNLIKE create() where the ID is returned not as a part of the created content
|
|
883
|
+
* but rather individually as an argument to the callback ... in the all() method
|
|
884
|
+
* it makes sense to return the id with the content! So for a datasource like elasticsearch,
|
|
885
|
+
* make sure to map "_id" into the content, just in case its an auto-generated one.
|
|
886
|
+
*/
|
|
887
|
+
ESConnector.prototype.all = function all(model, filter, done) {
|
|
888
|
+
var self = this;
|
|
889
|
+
log('ESConnector.prototype.all', 'model', model, 'filter', JSON.stringify(filter, null, 0));
|
|
890
|
+
|
|
891
|
+
var idName = self.idName(model);
|
|
892
|
+
log('ESConnector.prototype.all', 'idName', idName);
|
|
893
|
+
|
|
894
|
+
if (filter && filter.suggests) { // TODO: remove HACK!!!
|
|
895
|
+
self.db.suggest(
|
|
896
|
+
self.buildFilter(model, idName, filter, self.defaultSize)
|
|
897
|
+
).then(
|
|
898
|
+
function (body) {
|
|
899
|
+
var result = [];
|
|
900
|
+
if (body.hits) {
|
|
901
|
+
body.hits.hits.forEach(function (item) {
|
|
902
|
+
result.push(self.dataSourceToModel(model, item, idName));
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
log('ESConnector.prototype.all', 'model', model, 'result', JSON.stringify(result, null, 2));
|
|
906
|
+
if (filter && filter.include) {
|
|
907
|
+
self._models[model].model.include(result, filter.include, done);
|
|
908
|
+
} else {
|
|
909
|
+
done(null, result);
|
|
910
|
+
}
|
|
911
|
+
}, function (err) {
|
|
912
|
+
console.trace(err.message);
|
|
913
|
+
if (err) {
|
|
914
|
+
return done(err, null);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
else {
|
|
920
|
+
self.db.search(
|
|
921
|
+
self.buildFilter(model, idName, filter, self.defaultSize)
|
|
922
|
+
).then(
|
|
923
|
+
function (body) {
|
|
924
|
+
var result = [];
|
|
925
|
+
body.hits.hits.forEach(function (item) {
|
|
926
|
+
result.push(self.dataSourceToModel(model, item, idName));
|
|
927
|
+
});
|
|
928
|
+
log('ESConnector.prototype.all', 'model', model, 'result', JSON.stringify(result, null, 2));
|
|
929
|
+
if (filter && filter.include) {
|
|
930
|
+
self._models[model].model.include(result, filter.include, done);
|
|
931
|
+
} else {
|
|
932
|
+
done(null, result);
|
|
933
|
+
}
|
|
934
|
+
}, function (err) {
|
|
935
|
+
console.trace(err.message);
|
|
936
|
+
if (err) {
|
|
937
|
+
return done(err, null);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* Delete model instances by query
|
|
946
|
+
* @param {String} modelName name
|
|
947
|
+
* @param {String} whereClause criteria
|
|
948
|
+
* @param {Function} cb callback
|
|
949
|
+
*/
|
|
950
|
+
ESConnector.prototype.destroyAll = function destroyAll(modelName, whereClause, cb) {
|
|
951
|
+
var self = this;
|
|
952
|
+
|
|
953
|
+
if ((!cb) && _.isFunction(whereClause)) {
|
|
954
|
+
cb = whereClause;
|
|
955
|
+
whereClause = {};
|
|
956
|
+
}
|
|
957
|
+
log('ESConnector.prototype.destroyAll', 'modelName', modelName, 'whereClause', JSON.stringify(whereClause, null, 0));
|
|
958
|
+
|
|
959
|
+
var idName = self.idName(modelName);
|
|
960
|
+
var body = {
|
|
961
|
+
query: self.buildWhere(modelName, idName, whereClause).query
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
var defaults = self.addDefaults(modelName, 'destroyAll');
|
|
965
|
+
var options = _.defaults({body: body}, defaults);
|
|
966
|
+
log('ESConnector.prototype.destroyAll', 'options:', JSON.stringify(options, null, 2));
|
|
967
|
+
self.db.deleteByQuery(options)
|
|
968
|
+
.then(function (response) {
|
|
969
|
+
cb(null, response);
|
|
970
|
+
})
|
|
971
|
+
.catch(function (err) {
|
|
972
|
+
console.trace(err.message);
|
|
973
|
+
return cb(err, null);
|
|
974
|
+
});
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Update model instances by query
|
|
979
|
+
*
|
|
980
|
+
* NOTES:
|
|
981
|
+
* > Without an update by query plugin, this isn't supported by ES out-of-the-box
|
|
982
|
+
* > To run updateAll these parameters should be enabled in elasticsearch config
|
|
983
|
+
* -> script.inline: true
|
|
984
|
+
* -> script.indexed: true
|
|
985
|
+
* -> script.engine.groovy.inline.search: on
|
|
986
|
+
* -> script.engine.groovy.inline.update: on
|
|
987
|
+
*
|
|
988
|
+
* @param {String} model The model name
|
|
989
|
+
* @param {Object} where The search criteria
|
|
990
|
+
* @param {Object} data The property/value pairs to be updated
|
|
991
|
+
* @callback {Function} cb - should be invoked with a second callback argument
|
|
992
|
+
* that provides the count of affected rows in the callback
|
|
993
|
+
* such as cb(err, {count: affectedRows}).
|
|
994
|
+
* Notice the second argument is an object with the count property
|
|
995
|
+
* representing the number of rows that were updated.
|
|
996
|
+
*/
|
|
997
|
+
ESConnector.prototype.updateAll = function updateAll(model, where, data, options, cb) {
|
|
998
|
+
var self = this;
|
|
999
|
+
if (self.debug) {
|
|
1000
|
+
log('ESConnector.prototype.updateAll', 'model', model,'options', options,'where', where, 'date', data);
|
|
1001
|
+
}
|
|
1002
|
+
var idName = self.idName(model);
|
|
1003
|
+
log('ESConnector.prototype.updateAll', 'idName', idName);
|
|
1004
|
+
|
|
1005
|
+
var defaults = self.addDefaults(model,'updateAll');
|
|
1006
|
+
|
|
1007
|
+
var body = {
|
|
1008
|
+
query: self.buildWhere(model, idName, where).query
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
body.script = {
|
|
1012
|
+
inline: "",
|
|
1013
|
+
params: {}
|
|
1014
|
+
};
|
|
1015
|
+
_.forEach(data, function (value, key) {
|
|
1016
|
+
if (key !== '_id' || key !== idName) {
|
|
1017
|
+
if (self.apiVersion[0] > 2) {
|
|
1018
|
+
// default language for inline scripts is painless if ES 5, so this needs the extra params.
|
|
1019
|
+
body.script.inline += 'ctx._source.' + key + '=params.' + key + ';';
|
|
1020
|
+
} else {
|
|
1021
|
+
// groovy syntax
|
|
1022
|
+
body.script.inline += 'ctx._source.' + key + '=' + key + ';';
|
|
1023
|
+
}
|
|
1024
|
+
body.script.params[key] = value;
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
var document = _.defaults({body: body}, defaults);
|
|
1029
|
+
log('ESConnector.prototype.updateAll', 'document to update', document);
|
|
1030
|
+
|
|
1031
|
+
self.db.updateByQuery(document)
|
|
1032
|
+
.then(function (response) {
|
|
1033
|
+
log('ESConnector.prototype.updateAll', 'response', response);
|
|
1034
|
+
cb(null, {updated: response.updated, total: response.total});
|
|
1035
|
+
}, function (err) {
|
|
1036
|
+
console.trace(err.message);
|
|
1037
|
+
if (err) {
|
|
1038
|
+
return cb(err, null);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
);
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
ESConnector.prototype.update = ESConnector.prototype.updateAll;
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Count model instances by query
|
|
1048
|
+
* @param {String} model name
|
|
1049
|
+
* @param {String} where criteria
|
|
1050
|
+
* @param {Function} done callback
|
|
1051
|
+
*/
|
|
1052
|
+
ESConnector.prototype.count = function count(modelName, done, where) {
|
|
1053
|
+
var self = this;
|
|
1054
|
+
log('ESConnector.prototype.count', 'model', modelName, 'where', where);
|
|
1055
|
+
|
|
1056
|
+
var idName = self.idName(modelName);
|
|
1057
|
+
var body = where.native ? where.native : {
|
|
1058
|
+
query: self.buildWhere(modelName, idName, where).query
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
var defaults = self.addDefaults(modelName, 'count');
|
|
1062
|
+
self.db.count(_.defaults({body: body}, defaults)).then(
|
|
1063
|
+
function (response) {
|
|
1064
|
+
done(null, response.count);
|
|
1065
|
+
}, function (err) {
|
|
1066
|
+
console.trace(err.message);
|
|
1067
|
+
if (err) {
|
|
1068
|
+
return done(err, null);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
);
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Implement CRUD Level II - A connector can choose to implement the following methods,
|
|
1076
|
+
* otherwise, they will be mapped to those from CRUD Level I.
|
|
1077
|
+
* > Find a model instance by id
|
|
1078
|
+
* > CRUDConnector.prototype.find = function (model, id, callback) {...};
|
|
1079
|
+
* > Delete a model instance by id
|
|
1080
|
+
* > CRUDConnector.prototype.destroy = function (model, id, callback) {...};
|
|
1081
|
+
* > Update a model instance by id
|
|
1082
|
+
* > CRUDConnector.prototype.updateAttributes = function (model, id, data, callback) {...};
|
|
1083
|
+
* > Check existence of a model instance by id
|
|
1084
|
+
* > CRUDConnector.prototype.exists = function (model, id, callback) {...};
|
|
1085
|
+
*/
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Find a model instance by id
|
|
1089
|
+
* @param {String} model name
|
|
1090
|
+
* @param {String} id row identifier
|
|
1091
|
+
* @param {Function} done callback
|
|
1092
|
+
*/
|
|
1093
|
+
ESConnector.prototype.find = function find(modelName, id, done) {
|
|
1094
|
+
var self = this;
|
|
1095
|
+
log('ESConnector.prototype.find', 'model', modelName, 'id', id);
|
|
1096
|
+
|
|
1097
|
+
if (id === undefined || id === null) {
|
|
1098
|
+
throw new Error('id not set!');
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
var defaults = self.addDefaults(modelName, 'find');
|
|
1102
|
+
self.db.get(_.defaults({
|
|
1103
|
+
id: self.getDocumentId(id)
|
|
1104
|
+
}, defaults)).then(
|
|
1105
|
+
function (response) {
|
|
1106
|
+
done(null, self.dataSourceToModel(modelName, response));
|
|
1107
|
+
}, function (err) {
|
|
1108
|
+
console.trace(err.message);
|
|
1109
|
+
if (err) {
|
|
1110
|
+
return done(err, null);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
);
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Delete a model instance by id
|
|
1118
|
+
* @param {String} model name
|
|
1119
|
+
* @param {String} id row identifier
|
|
1120
|
+
* @param {Function} done callback
|
|
1121
|
+
*/
|
|
1122
|
+
ESConnector.prototype.destroy = function destroy(modelName, id, done) {
|
|
1123
|
+
var self = this;
|
|
1124
|
+
if (self.debug) {
|
|
1125
|
+
log('destroy', 'model', modelName, 'id', id);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
var filter = self.addDefaults(modelName, 'destroy');
|
|
1129
|
+
filter[self.idField] = self.getDocumentId(id);
|
|
1130
|
+
if (!filter[self.idField]) {
|
|
1131
|
+
throw new Error('Document id not setted!');
|
|
1132
|
+
}
|
|
1133
|
+
self.db.delete(
|
|
1134
|
+
filter
|
|
1135
|
+
).then(
|
|
1136
|
+
function (response) {
|
|
1137
|
+
done(null, response);
|
|
1138
|
+
}, function (err) {
|
|
1139
|
+
console.trace(err.message);
|
|
1140
|
+
if (err) {
|
|
1141
|
+
return done(err, null);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
);
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* Update a model instance by id
|
|
1149
|
+
*
|
|
1150
|
+
* NOTES:
|
|
1151
|
+
* > The _source field need to be enabled for this feature to work.
|
|
1152
|
+
*/
|
|
1153
|
+
ESConnector.prototype.updateAttributes = function updateAttrs(modelName, id, data, callback) {
|
|
1154
|
+
var self = this;
|
|
1155
|
+
log('ESConnector.prototype.updateAttributes', 'modelName', modelName, 'id', id, 'data', data);
|
|
1156
|
+
|
|
1157
|
+
if (id === undefined || id === null) {
|
|
1158
|
+
throw new Error('id not set!');
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
var defaults = self.addDefaults(modelName, 'updateAttributes');
|
|
1162
|
+
self.db.update(_.defaults({
|
|
1163
|
+
id: id,
|
|
1164
|
+
body: {
|
|
1165
|
+
doc: data,
|
|
1166
|
+
'doc_as_upsert': false
|
|
1167
|
+
}
|
|
1168
|
+
}, defaults)).then(
|
|
1169
|
+
function (response) {
|
|
1170
|
+
// TODO: what does the framework want us to return as arguments w/ callback?
|
|
1171
|
+
callback(null, response);
|
|
1172
|
+
//callback(null, response._id);
|
|
1173
|
+
//callback(null, data);
|
|
1174
|
+
}, function (err) {
|
|
1175
|
+
console.trace(err.message);
|
|
1176
|
+
if (err) {
|
|
1177
|
+
return callback(err, null);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
);
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* Check existence of a model instance by id
|
|
1185
|
+
* @param {String} model name
|
|
1186
|
+
* @param {String} id row identifier
|
|
1187
|
+
* @param {function} done callback
|
|
1188
|
+
*/
|
|
1189
|
+
ESConnector.prototype.exists = function (modelName, id, done) {
|
|
1190
|
+
var self = this;
|
|
1191
|
+
log('ESConnector.prototype.exists', 'model', modelName, 'id', id);
|
|
1192
|
+
|
|
1193
|
+
if (id === undefined || id === null) {
|
|
1194
|
+
throw new Error('id not set!');
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
var defaults = self.addDefaults(modelName, 'exists');
|
|
1198
|
+
self.db.exists(_.defaults({
|
|
1199
|
+
id: self.getDocumentId(id)
|
|
1200
|
+
}, defaults)).then(
|
|
1201
|
+
function (exists) {
|
|
1202
|
+
done(null, exists);
|
|
1203
|
+
}, function (err) {
|
|
1204
|
+
console.trace(err.message);
|
|
1205
|
+
if (err) {
|
|
1206
|
+
return done(err, null);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
);
|
|
1210
|
+
};
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* Implement CRUD Level III - A connector can also optimize certain methods
|
|
1214
|
+
* if the underlying database provides native/atomic
|
|
1215
|
+
* operations to avoid multiple calls.
|
|
1216
|
+
* > Save a model instance
|
|
1217
|
+
* > CRUDConnector.prototype.save = function (model, data, callback) {...};
|
|
1218
|
+
* > Find or create a model instance
|
|
1219
|
+
* > CRUDConnector.prototype.findOrCreate = function (model, data, callback) {...};
|
|
1220
|
+
* > Update or insert a model instance
|
|
1221
|
+
* > CRUDConnector.prototype.updateOrCreate = function (model, data, callback) {...};
|
|
1222
|
+
*/
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* Update document data
|
|
1226
|
+
* @param {String} model name
|
|
1227
|
+
* @param {Object} data document
|
|
1228
|
+
* @param {Function} done callback
|
|
1229
|
+
*/
|
|
1230
|
+
ESConnector.prototype.save = function (model, data, done) {
|
|
1231
|
+
var self = this;
|
|
1232
|
+
if (self.debug) {
|
|
1233
|
+
log('ESConnector.prototype.save ', 'model', model, 'data', data);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
var idName = self.idName(model);
|
|
1237
|
+
var defaults = self.addDefaults(model, 'save');
|
|
1238
|
+
var id = self.getDocumentId(data[idName]);
|
|
1239
|
+
|
|
1240
|
+
if (id === undefined || id === null) {
|
|
1241
|
+
return done('Document id not setted!', null);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
self.db.update(_.defaults({
|
|
1245
|
+
id: id,
|
|
1246
|
+
body: {
|
|
1247
|
+
doc: data,
|
|
1248
|
+
'doc_as_upsert': false
|
|
1249
|
+
}
|
|
1250
|
+
}, defaults))
|
|
1251
|
+
.then(function (response) {
|
|
1252
|
+
done(null, response);
|
|
1253
|
+
}, function (err) {
|
|
1254
|
+
if (err) {
|
|
1255
|
+
return done(err, null);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
);
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* Find or create a model instance
|
|
1263
|
+
*/
|
|
1264
|
+
//ESConnector.prototype.findOrCreate = function (model, data, callback) {...};
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* Update or insert a model instance
|
|
1268
|
+
* @param modelName
|
|
1269
|
+
* @param data
|
|
1270
|
+
* @param callback - should pass the following arguments to the callback:
|
|
1271
|
+
* err object (null on success)
|
|
1272
|
+
* data object containing the property values as found in the database
|
|
1273
|
+
* info object providing more details about the result of the operation.
|
|
1274
|
+
* At the moment, it should have a single property isNewInstance
|
|
1275
|
+
* with the value true if a new model was created
|
|
1276
|
+
* and the value false is an existing model was found & updated.
|
|
1277
|
+
*/
|
|
1278
|
+
ESConnector.prototype.updateOrCreate = function updateOrCreate(modelName, data, callback) {
|
|
1279
|
+
var self = this;
|
|
1280
|
+
log('ESConnector.prototype.updateOrCreate', 'modelName', modelName, 'data', data);
|
|
1281
|
+
|
|
1282
|
+
var idName = self.idName(modelName);
|
|
1283
|
+
var id = self.getDocumentId(data[idName]);
|
|
1284
|
+
if (id === undefined || id === null) {
|
|
1285
|
+
throw new Error('id not set!');
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
var defaults = self.addDefaults(modelName, 'updateOrCreate');
|
|
1289
|
+
self.db.update(_.defaults({
|
|
1290
|
+
id: id,
|
|
1291
|
+
body: {
|
|
1292
|
+
doc: data,
|
|
1293
|
+
'doc_as_upsert': true
|
|
1294
|
+
}
|
|
1295
|
+
}, defaults)).then(
|
|
1296
|
+
function (response) {
|
|
1297
|
+
/**
|
|
1298
|
+
* In the case of an update, elasticsearch only provides a confirmation that it worked
|
|
1299
|
+
* but does not provide any model data back. So what should be passed back in
|
|
1300
|
+
* the data object (second argument of callback)?
|
|
1301
|
+
* Q1) Should we just pass back the data that was meant to be updated
|
|
1302
|
+
* and came in as an argument to the updateOrCreate() call? This is what
|
|
1303
|
+
* the memory connector seems to do.
|
|
1304
|
+
* A: [Victor Law] Yes, that's fine to do. The reason why we are passing the data there
|
|
1305
|
+
* and back is to support databases that can add default values to undefined properties,
|
|
1306
|
+
* typically the id property is often generated by the backend.
|
|
1307
|
+
* Q2) OR, should we make an additional call to fetch the data for that id internally,
|
|
1308
|
+
* within updateOrCreate()? So we can make sure to pass back a data object?
|
|
1309
|
+
* A: [Victor Law]
|
|
1310
|
+
* - Most connectors don't fetch the inserted/updated data and hope the data stored into DB
|
|
1311
|
+
* will be the same as the data sent to DB for create/update.
|
|
1312
|
+
* - It's true in most cases but not always. For example, the DB might have triggers
|
|
1313
|
+
* that change the value after the insert/update.
|
|
1314
|
+
* - We don't support that yet.
|
|
1315
|
+
* - In the future, that can be controlled via an options property,
|
|
1316
|
+
* such as fetchNewInstance = true.
|
|
1317
|
+
*
|
|
1318
|
+
* NOTE: Q1 based approach has been implemented for now.
|
|
1319
|
+
*/
|
|
1320
|
+
if (response._version === 1) { // distinguish if it was an update or create operation in ES
|
|
1321
|
+
data[idName] = response._id;
|
|
1322
|
+
log('ESConnector.prototype.updateOrCreate', 'assigned ID', idName, '=', response._id);
|
|
1323
|
+
}
|
|
1324
|
+
callback(null, data, {isNewInstance: response.created});
|
|
1325
|
+
}, function (err) {
|
|
1326
|
+
console.trace(err.message);
|
|
1327
|
+
if (err) {
|
|
1328
|
+
return callback(err, null);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
);
|
|
1332
|
+
};
|
|
1333
|
+
|
|
1334
|
+
/**
|
|
1335
|
+
* Migration
|
|
1336
|
+
* automigrate - Create/recreate DB objects (such as table/column/constraint/trigger/index)
|
|
1337
|
+
* to match the model definitions
|
|
1338
|
+
* autoupdate - Alter DB objects to match the model definitions
|
|
1339
|
+
*/
|
|
1340
|
+
|
|
1341
|
+
/**
|
|
1342
|
+
* Perform automigrate for the given models. Create/recreate DB objects
|
|
1343
|
+
* (such as table/column/constraint/trigger/index) to match the model definitions
|
|
1344
|
+
* --> Drop the corresponding indices: both mappings and data are done away with
|
|
1345
|
+
* --> create/recreate mappings and indices
|
|
1346
|
+
*
|
|
1347
|
+
* @param {String[]} [models] A model name or an array of model names. If not present, apply to all models
|
|
1348
|
+
* @param {Function} [cb] The callback function
|
|
1349
|
+
*/
|
|
1350
|
+
ESConnector.prototype.automigrate = require('./automigrate.js')({
|
|
1351
|
+
log: log
|
|
1352
|
+
, lodash: _
|
|
1353
|
+
, bluebird: Promise
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
module.exports.name = ESConnector.name;
|
|
1357
|
+
module.exports.ESConnector = ESConnector;
|