@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.
@@ -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;