├── .eslintignore ├── examples ├── .jshintignore ├── client │ ├── README.md │ └── client.js ├── server │ ├── boot │ │ ├── authentication.js │ │ ├── rest-api.js │ │ ├── root.js │ │ ├── boot.js │ │ └── explorer.js │ ├── component-config.json │ ├── config.json │ ├── model-config.json │ ├── server.js │ ├── users │ │ ├── 03-create-user.js │ │ ├── 01-find-user.js │ │ └── 02-findOrCreate-user.js │ ├── roles │ │ ├── 01-create-role.js │ │ └── 02-findOrCreate-role.js │ └── datasources.json ├── .npmignore ├── .editorconfig ├── .jshintrc ├── common │ └── models │ │ ├── user-model.js │ │ └── user-model.json └── package.json ├── index.js ├── .gitignore ├── .npmignore ├── .eslintrc ├── test ├── resource │ ├── mock-data-test.json │ ├── model-test.json │ └── datasource-test.json ├── es-v6 │ ├── datasource-test-v6-plain.json │ ├── init.js │ ├── 04.add-defaults-refresh-true.test.js │ └── 01.filters.test.js └── es-v7 │ ├── datasource-test-v7-plain.json │ ├── init.js │ ├── 04.add-defaults-refresh-true.test.js │ └── 01.filters.test.js ├── SECURITY.md ├── lib ├── exists.js ├── destroy.js ├── count.js ├── buildOrder.js ├── find.js ├── destroyAll.js ├── save.js ├── all.js ├── setupIndex.js ├── updateAll.js ├── replaceById.js ├── buildWhere.js ├── replaceOrCreate.js ├── updateAttributes.js ├── buildNestedQueries.js ├── create.js ├── automigrate.js ├── updateOrCreate.js ├── buildFilter.js ├── buildDeepNestedQueries.js └── esConnector.js ├── LICENCE ├── package.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | examples/ 2 | test/ -------------------------------------------------------------------------------- /examples/.jshintignore: -------------------------------------------------------------------------------- 1 | /client/ 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/esConnector'); 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | examples/db.json 4 | test/**/*.log 5 | package-lock.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /test 2 | /examples 3 | .idea 4 | .jshintrc 5 | docker-compose.json 6 | docker-entrypoint.sh 7 | -------------------------------------------------------------------------------- /examples/client/README.md: -------------------------------------------------------------------------------- 1 | ## Client 2 | 3 | This is the place for your application front-end files. 4 | -------------------------------------------------------------------------------- /examples/server/boot/authentication.js: -------------------------------------------------------------------------------- 1 | module.exports = function enableAuthentication(server) { 2 | // enable authentication 3 | server.enableAuth(); 4 | }; 5 | -------------------------------------------------------------------------------- /examples/server/boot/rest-api.js: -------------------------------------------------------------------------------- 1 | module.exports = function mountRestApi(server) { 2 | var restApiRoot = server.get('restApiRoot'); 3 | server.use(restApiRoot, server.loopback.rest()); 4 | }; 5 | -------------------------------------------------------------------------------- /examples/.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .project 3 | *.sublime-* 4 | .DS_Store 5 | *.seed 6 | *.log 7 | *.csv 8 | *.dat 9 | *.out 10 | *.pid 11 | *.swp 12 | *.swo 13 | node_modules 14 | coverage 15 | *.tgz 16 | *.xml 17 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "comma-dangle": "off", 5 | "global-require":"off", 6 | "no-param-reassign": "off" 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 8 10 | } 11 | } -------------------------------------------------------------------------------- /examples/server/boot/root.js: -------------------------------------------------------------------------------- 1 | module.exports = function (server) { 2 | // Install a `/` route that returns server status 3 | var router = server.loopback.Router(); 4 | router.get('/', server.loopback.status()); 5 | server.use(router); 6 | }; 7 | -------------------------------------------------------------------------------- /examples/server/component-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "loopback-component-explorer": { 3 | "apiInfo": { 4 | "title": "Elastic App APIs", 5 | "description": "Explore Elastic App APIs" 6 | }, 7 | "mountPath": "/api/explorer", 8 | "generateOperationScopedModels": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/resource/mock-data-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "line_id": 42, 3 | "play_name": "Henry IV", 4 | "speech_number": 2, 5 | "line_number": "1.1.39", 6 | "speaker": "WESTMORELAND", 7 | "text_entry": "Leading the men of Herefordshire to fight", 8 | "test_object": { 9 | "name" : "test" 10 | } 11 | } -------------------------------------------------------------------------------- /examples/server/boot/boot.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | //require('./../users/01-find-user.js')(app); 3 | require('./../users/02-findOrCreate-user.js')(app); 4 | //require('./../users/03-create-user.js')(app); 5 | 6 | //require('./../roles/01-create-role.js')(app); 7 | //require('./../roles/02-findOrCreate-role.js')(app); 8 | }; 9 | -------------------------------------------------------------------------------- /examples/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | -------------------------------------------------------------------------------- /examples/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "camelcase": true, 6 | "eqeqeq": true, 7 | "eqnull": true, 8 | "immed": true, 9 | "indent": 4, 10 | "latedef": "nofunc", 11 | "newcap": true, 12 | "nonew": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": false, 18 | "trailing": true, 19 | "sub": true, 20 | "maxlen": 80 21 | } 22 | -------------------------------------------------------------------------------- /examples/server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "restApiRoot": "/api", 3 | "host": "0.0.0.0", 4 | "port": 3000, 5 | "remoting": { 6 | "types": { 7 | "warnOnUnknownType": false 8 | }, 9 | "context": false, 10 | "rest": { 11 | "handleErrors": false, 12 | "normalizeHttpPath": false, 13 | "xml": false 14 | }, 15 | "json": { 16 | "strict": false, 17 | "limit": "100kb" 18 | }, 19 | "urlencoded": { 20 | "extended": true, 21 | "limit": "100kb" 22 | }, 23 | "cors": false 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/common/models/user-model.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'); 2 | 3 | module.exports = function(UserModel) { 4 | 5 | // https://github.com/strongloop/loopback/issues/418 6 | // once a model is attached to the data source 7 | UserModel.on('dataSourceAttached', function(obj){ 8 | // wrap the whole model in Promise 9 | // but we need to avoid 'validate' method 10 | UserModel = Promise.promisifyAll( 11 | UserModel, 12 | { 13 | filter: function(name, func, target){ 14 | return !( name == 'validate'); 15 | } 16 | } 17 | ); 18 | }); 19 | 20 | }; 21 | -------------------------------------------------------------------------------- /test/resource/model-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "entry", 3 | "plural": "entries", 4 | "properties": { 5 | "play_name": { 6 | "type": "string" 7 | }, 8 | "speech_number": { 9 | "type": "number" 10 | }, 11 | "line_number": { 12 | "type": "string", 13 | "required": true 14 | }, 15 | "speaker": { 16 | "type": "string" 17 | }, 18 | "test_object": { 19 | "type": "object" 20 | } 21 | }, 22 | "validations": [], 23 | "relations": {}, 24 | "acls": [], 25 | "methods": [] 26 | } 27 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "2.0.0", 4 | "main": "server/server.js", 5 | "scripts": { 6 | "pretest": "jshint ." 7 | }, 8 | "dependencies": { 9 | "compression": "1.7.3", 10 | "cors": "2.8.5", 11 | "helmet": "3.16.0", 12 | "loopback": "3.25.0", 13 | "loopback-boot": "3.2.0", 14 | "loopback-component-explorer": "6.3.1", 15 | "loopback-component-storage": "3.5.0", 16 | "loopback-connector-esv6": "1.3.2", 17 | "ramda": "0.26.1", 18 | "serve-favicon": "2.5.0", 19 | "strong-error-handler": "3.2.0" 20 | }, 21 | "optionalDependencies": { 22 | "loopback-explorer": "^1.1.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Security advisories 4 | 5 | Security advisories can be found on the 6 | [LoopBack website](https://loopback.io/doc/en/sec/index.html). 7 | 8 | ## Reporting a vulnerability 9 | 10 | If you think you have discovered a new security issue with any LoopBack package, 11 | **please do not report it on GitHub**. Instead, send an email to 12 | [security@loopback.io](mailto:security@loopback.io) with the following details: 13 | 14 | - Full description of the vulnerability. 15 | - Steps to reproduce the issue. 16 | - Possible solutions. 17 | 18 | If you are sending us any logs as part of the report, then make sure to redact 19 | any sensitive data from them. -------------------------------------------------------------------------------- /lib/exists.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const log = require('debug')('loopback:connector:elasticsearch'); 3 | 4 | function exists(modelName, id, done) { 5 | const self = this; 6 | log('ESConnector.prototype.exists', 'model', modelName, 'id', id); 7 | 8 | if (id === undefined || id === null) { 9 | throw new Error('id not set!'); 10 | } 11 | 12 | const defaults = self.addDefaults(modelName, 'exists'); 13 | self.db.exists(_.defaults({ 14 | id: self.getDocumentId(id) 15 | }, defaults)).then(({ body }) => { 16 | done(null, body); 17 | }).catch((error) => { 18 | log('ESConnector.prototype.exists', error.message); 19 | done(error); 20 | }); 21 | } 22 | 23 | module.exports.exists = exists; 24 | -------------------------------------------------------------------------------- /lib/destroy.js: -------------------------------------------------------------------------------- 1 | const log = require('debug')('loopback:connector:elasticsearch'); 2 | 3 | function destroy(modelName, id, done) { 4 | const self = this; 5 | if (self.debug) { 6 | log('ESConnector.prototype.destroy', 'model', modelName, 'id', id); 7 | } 8 | 9 | const filter = self.addDefaults(modelName, 'destroy'); 10 | filter[self.idField] = self.getDocumentId(id); 11 | if (!filter[self.idField]) { 12 | throw new Error('Document id not setted!'); 13 | } 14 | self.db.delete( 15 | filter 16 | ).then(({ body }) => { 17 | done(null, body); 18 | }).catch((error) => { 19 | log('ESConnector.prototype.destroy', error.message); 20 | done(error, null); 21 | }); 22 | } 23 | 24 | module.exports.destroy = destroy; 25 | -------------------------------------------------------------------------------- /lib/count.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const log = require('debug')('loopback:connector:elasticsearch'); 3 | 4 | function count(modelName, done, where) { 5 | const self = this; 6 | log('ESConnector.prototype.count', 'model', modelName, 'where', where); 7 | 8 | const idName = self.idName(modelName); 9 | const query = { 10 | query: self.buildWhere(modelName, idName, where).query 11 | }; 12 | 13 | const defaults = self.addDefaults(modelName, 'count'); 14 | self.db.count(_.defaults({ 15 | body: query 16 | }, defaults)).then(({ body }) => { 17 | done(null, body.count); 18 | }).catch((error) => { 19 | log('ESConnector.prototype.count', error.message); 20 | done(error, null); 21 | }); 22 | } 23 | 24 | module.exports.count = count; 25 | -------------------------------------------------------------------------------- /lib/buildOrder.js: -------------------------------------------------------------------------------- 1 | function buildOrder(model, idName, order) { 2 | const sort = []; 3 | 4 | let keys = order; 5 | if (typeof keys === 'string') { 6 | keys = keys.split(','); 7 | } 8 | for (let index = 0, len = keys.length; index < len; index += 1) { 9 | const m = keys[index].match(/\s+(A|DE)SC$/); 10 | let key = keys[index]; 11 | key = key.replace(/\s+(A|DE)SC$/, '').trim(); 12 | if (key === 'id' || key === idName) { 13 | key = '_id'; 14 | } 15 | if (m && m[1] === 'DE') { 16 | // sort[key] = -1; 17 | const temp = {}; 18 | temp[key] = 'desc'; 19 | sort.push(temp); 20 | } else { 21 | // sort[key] = 1; 22 | sort.push(key); 23 | } 24 | } 25 | 26 | return sort; 27 | } 28 | 29 | module.exports.buildOrder = buildOrder; 30 | -------------------------------------------------------------------------------- /examples/server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "loopback/common/models", 5 | "loopback/server/models", 6 | "../common/models", 7 | "./models" 8 | ], 9 | "mixins": [ 10 | "loopback/common/mixins", 11 | "loopback/server/mixins", 12 | "../common/mixins", 13 | "./mixins" 14 | ] 15 | }, 16 | "User": { 17 | "dataSource": "db" 18 | }, 19 | "AccessToken": { 20 | "dataSource": "db", 21 | "public": false 22 | }, 23 | "ACL": { 24 | "dataSource": "db", 25 | "public": false 26 | }, 27 | "RoleMapping": { 28 | "dataSource": "db", 29 | "public": false 30 | }, 31 | "Role": { 32 | "dataSource": "db", 33 | "public": false 34 | }, 35 | "UserModel": { 36 | "dataSource": "elasticsearch-plain" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/find.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const log = require('debug')('loopback:connector:elasticsearch'); 3 | 4 | function find(modelName, id, done) { 5 | const self = this; 6 | log('ESConnector.prototype.find', 'model', modelName, 'id', id); 7 | 8 | if (id === undefined || id === null) { 9 | throw new Error('id not set!'); 10 | } 11 | const idName = self.idName(modelName); 12 | const defaults = self.addDefaults(modelName, 'find'); 13 | self.db.get(_.defaults({ 14 | id: self.getDocumentId(id) 15 | }, defaults)).then(({ body }) => { 16 | const totalCount = typeof body.hits.total === 'object' ? body.hits.total.value : body.hits.total; 17 | done(null, self.dataSourceToModel(modelName, body, idName, totalCount)); 18 | }).catch((error) => { 19 | log('ESConnector.prototype.find', error.message); 20 | done(error); 21 | }); 22 | } 23 | 24 | module.exports.find = find; 25 | -------------------------------------------------------------------------------- /examples/server/boot/explorer.js: -------------------------------------------------------------------------------- 1 | module.exports = function mountLoopBackExplorer(server) { 2 | var explorer; 3 | try { 4 | explorer = require('loopback-explorer'); 5 | } catch (err) { 6 | console.log( 7 | 'Run `npm install loopback-explorer` to enable the LoopBack explorer' 8 | ); 9 | return; 10 | } 11 | 12 | var restApiRoot = server.get('restApiRoot'); 13 | 14 | var explorerApp = explorer(server, { basePath: restApiRoot }); 15 | server.use('/explorer', explorerApp); 16 | server.once('started', function () { 17 | var baseUrl = server.get('url').replace(/\/$/, ''); 18 | // express 4.x (loopback 2.x) uses `mountpath` 19 | // express 3.x (loopback 1.x) uses `route` 20 | var explorerPath = explorerApp.mountpath || explorerApp.route; 21 | console.log('Browse your REST API at %s%s', baseUrl, explorerPath); 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT license 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /examples/common/models/user-model.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "UserModel", 3 | "base": "User", 4 | "idInjection": true, 5 | "properties": { 6 | "_search_after": { 7 | "type": ["any"] 8 | } 9 | }, 10 | "validations": [], 11 | "relations": { 12 | "globalConfigModels": { 13 | "type": "hasOne", 14 | "model": "GlobalConfigModel", 15 | "foreignKey": "userModelToGlobalConfigModelId" 16 | }, 17 | "storeConfigModels": { 18 | "type": "hasMany", 19 | "model": "StoreConfigModel", 20 | "foreignKey": "userModelToStoreConfigModelId" 21 | }, 22 | "storeModels": { 23 | "type": "hasMany", 24 | "model": "StoreModel", 25 | "foreignKey": "userModelToStoreModelId" 26 | } 27 | }, 28 | "acls": [ 29 | { 30 | "accessType": "*", 31 | "principalType": "ROLE", 32 | "principalId": "$owner", 33 | "permission": "ALLOW" 34 | }, 35 | { 36 | "accessType": "*", 37 | "principalType": "ROLE", 38 | "principalId": "admin", 39 | "permission": "ALLOW" 40 | } 41 | ], 42 | "methods": [] 43 | } 44 | -------------------------------------------------------------------------------- /lib/destroyAll.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const log = require('debug')('loopback:connector:elasticsearch'); 3 | 4 | function destroyAll(modelName, whereClause, cb) { 5 | const self = this; 6 | 7 | if ((!cb) && _.isFunction(whereClause)) { 8 | cb = whereClause; 9 | whereClause = {}; 10 | } 11 | log('ESConnector.prototype.destroyAll', 'modelName', modelName, 'whereClause', JSON.stringify(whereClause, null, 0)); 12 | 13 | const idName = self.idName(modelName); 14 | const filter = { 15 | query: self.buildWhere(modelName, idName, whereClause).query 16 | }; 17 | 18 | const defaults = self.addDefaults(modelName, 'destroyAll'); 19 | const options = _.defaults({ 20 | body: filter 21 | }, defaults); 22 | log('ESConnector.prototype.destroyAll', 'options:', JSON.stringify(options, null, 2)); 23 | self.db.deleteByQuery(options) 24 | .then(({ body }) => { 25 | cb(null, body); 26 | }) 27 | .catch((error) => { 28 | log('ESConnector.prototype.destroyAll', error.message); 29 | cb(error); 30 | }); 31 | } 32 | 33 | module.exports.destroyAll = destroyAll; 34 | -------------------------------------------------------------------------------- /lib/save.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const log = require('debug')('loopback:connector:elasticsearch'); 3 | // CONSTANTS 4 | const SEARCHAFTERKEY = '_search_after'; 5 | const TOTALCOUNTKEY = '_total_count'; 6 | 7 | // eslint-disable-next-line consistent-return 8 | function save(model, data, done) { 9 | const self = this; 10 | if (self.debug) { 11 | log('ESConnector.prototype.save ', 'model', model, 'data', data); 12 | } 13 | 14 | const idName = self.idName(model); 15 | const defaults = self.addDefaults(model, 'save'); 16 | const id = self.getDocumentId(data[idName]); 17 | 18 | if (id === undefined || id === null) { 19 | return done('Document id not setted!', null); 20 | } 21 | data.docType = model; 22 | if (data[SEARCHAFTERKEY] || data[TOTALCOUNTKEY]) { 23 | data[SEARCHAFTERKEY] = undefined; 24 | data[TOTALCOUNTKEY] = undefined; 25 | } 26 | self.db.update(_.defaults({ 27 | id, 28 | body: { 29 | doc: data, 30 | doc_as_upsert: false 31 | } 32 | }, defaults)).then(({ body }) => { 33 | done(null, body); 34 | }).catch((error) => { 35 | log('ESConnector.prototype.save', error.message); 36 | done(error); 37 | }); 38 | } 39 | 40 | module.exports.save = save; 41 | -------------------------------------------------------------------------------- /lib/all.js: -------------------------------------------------------------------------------- 1 | const log = require('debug')('loopback:connector:elasticsearch'); 2 | 3 | function all(model, filter, done) { 4 | const self = this; 5 | log('ESConnector.prototype.all', 'model', model, 'filter', JSON.stringify(filter, null, 0)); 6 | 7 | const idName = self.idName(model); 8 | log('ESConnector.prototype.all', 'idName', idName); 9 | 10 | self.db.search( 11 | self.buildFilter(model, idName, filter, self.defaultSize) 12 | ).then( 13 | ({ body }) => { 14 | const result = []; 15 | const totalCount = typeof body.hits.total === 'object' ? body.hits.total.value : body.hits.total; 16 | body.hits.hits.forEach((item) => { 17 | result.push(self.dataSourceToModel(model, item, idName, totalCount)); 18 | }); 19 | log('ESConnector.prototype.all', 'model', model, 'result', JSON.stringify(result, null, 2)); 20 | if (filter && filter.include) { 21 | // eslint-disable-next-line no-underscore-dangle 22 | self._models[model].model.include(result, filter.include, done); 23 | } else { 24 | done(null, result); 25 | } 26 | } 27 | ).catch((error) => { 28 | log('ESConnector.prototype.all', error.message); 29 | return done(error, null); 30 | }); 31 | } 32 | 33 | module.exports.all = all; 34 | -------------------------------------------------------------------------------- /lib/setupIndex.js: -------------------------------------------------------------------------------- 1 | const log = require('debug')('loopback:connector:elasticsearch'); 2 | 3 | async function setupIndex() { 4 | const self = this; 5 | const { 6 | db, 7 | version, 8 | index, 9 | settings: { 10 | mappingProperties, 11 | mappingType, 12 | indexSettings 13 | } 14 | } = self; 15 | const { body: exists } = await db.indices.exists({ 16 | index 17 | }); 18 | mappingProperties.docType = { 19 | type: 'keyword', 20 | index: true 21 | }; 22 | const mapping = { 23 | properties: mappingProperties 24 | }; 25 | if (!exists) { 26 | log('ESConnector.prototype.setupIndex', 'create index with mapping for', index); 27 | await db.indices.create({ 28 | index, 29 | body: { 30 | settings: indexSettings, 31 | mappings: version < 7 ? { 32 | [mappingType]: mapping 33 | } : mapping 34 | } 35 | }); 36 | return Promise.resolve(); 37 | } 38 | const updateMapping = { 39 | index, 40 | body: mapping 41 | }; 42 | log('ESConnector.prototype.setupIndex', 'update mapping for index', index); 43 | if (version < 7) { 44 | updateMapping.type = mappingType; 45 | } 46 | await db.indices.putMapping(updateMapping); 47 | return Promise.resolve(); 48 | } 49 | 50 | module.exports.setupIndex = setupIndex; 51 | -------------------------------------------------------------------------------- /examples/server/server.js: -------------------------------------------------------------------------------- 1 | var loopback = require('loopback'); 2 | var boot = require('loopback-boot'); 3 | 4 | var app = module.exports = loopback(); 5 | 6 | // Set up the /favicon.ico 7 | app.use(loopback.favicon()); 8 | 9 | // request pre-processing middleware 10 | app.use(loopback.compress()); 11 | 12 | // -- Add your pre-processing middleware here -- 13 | 14 | // boot scripts mount components like REST API 15 | boot(app, __dirname); 16 | 17 | // -- Mount static files here-- 18 | // All static middleware should be registered at the end, as all requests 19 | // passing the static middleware are hitting the file system 20 | // Example: 21 | // var path = require('path'); 22 | // app.use(loopback.static(path.resolve(__dirname, '../client'))); 23 | 24 | // Requests that get this far won't be handled 25 | // by any middleware. Convert them into a 404 error 26 | // that will be handled later down the chain. 27 | app.use(loopback.urlNotFound()); 28 | 29 | // The ultimate error handler. 30 | app.use(loopback.errorHandler()); 31 | 32 | app.start = function () { 33 | // start the web server 34 | return app.listen(function () { 35 | app.emit('started'); 36 | console.log('Web server listening at: %s', app.get('url')); 37 | }); 38 | }; 39 | 40 | // start the server if `$ node server.js` 41 | if (require.main === module) { 42 | app.start(); 43 | } 44 | -------------------------------------------------------------------------------- /test/es-v6/datasource-test-v6-plain.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elasticsearch-example-index-datasource", 3 | "connector": "esv6", 4 | "version": 6, 5 | "index": "example-index", 6 | "configuration": { 7 | "node": "http://localhost:9200", 8 | "requestTimeout": 30000, 9 | "pingTimeout": 3000 10 | }, 11 | "defaultSize": 50, 12 | "indexSettings": { 13 | "number_of_shards": 2, 14 | "number_of_replicas": 1 15 | }, 16 | "mappingType": "basedata", 17 | "mappingProperties": { 18 | "docType": { 19 | "type": "keyword", 20 | "index": true 21 | }, 22 | "id": { 23 | "type": "keyword", 24 | "index": true 25 | }, 26 | "seq": { 27 | "type": "integer", 28 | "index": true 29 | }, 30 | "name": { 31 | "type": "keyword", 32 | "index": true 33 | }, 34 | "email": { 35 | "type": "keyword", 36 | "index": true 37 | }, 38 | "birthday": { 39 | "type": "date", 40 | "index": true 41 | }, 42 | "role": { 43 | "type": "keyword", 44 | "index": true 45 | }, 46 | "order": { 47 | "type": "integer", 48 | "index": true 49 | }, 50 | "vip": { 51 | "type": "boolean", 52 | "index": true 53 | }, 54 | "objectId": { 55 | "type": "keyword", 56 | "index": true 57 | }, 58 | "ttl": { 59 | "type": "integer", 60 | "index": true 61 | }, 62 | "created": { 63 | "type": "date", 64 | "index": true 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /test/es-v7/datasource-test-v7-plain.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elasticsearch-example-index-datasource", 3 | "connector": "esv6", 4 | "version": 7, 5 | "index": "example-index", 6 | "configuration": { 7 | "node": "http://localhost:9200", 8 | "requestTimeout": 30000, 9 | "pingTimeout": 3000 10 | }, 11 | "defaultSize": 50, 12 | "indexSettings": { 13 | "number_of_shards": 2, 14 | "number_of_replicas": 1 15 | }, 16 | "mappingType": "basedata", 17 | "mappingProperties": { 18 | "docType": { 19 | "type": "keyword", 20 | "index": true 21 | }, 22 | "id": { 23 | "type": "keyword", 24 | "index": true 25 | }, 26 | "seq": { 27 | "type": "integer", 28 | "index": true 29 | }, 30 | "name": { 31 | "type": "keyword", 32 | "index": true 33 | }, 34 | "email": { 35 | "type": "keyword", 36 | "index": true 37 | }, 38 | "birthday": { 39 | "type": "date", 40 | "index": true 41 | }, 42 | "role": { 43 | "type": "keyword", 44 | "index": true 45 | }, 46 | "order": { 47 | "type": "integer", 48 | "index": true 49 | }, 50 | "vip": { 51 | "type": "boolean", 52 | "index": true 53 | }, 54 | "objectId": { 55 | "type": "keyword", 56 | "index": true 57 | }, 58 | "ttl": { 59 | "type": "integer", 60 | "index": true 61 | }, 62 | "created": { 63 | "type": "date", 64 | "index": true 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /test/es-v6/init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Why does this file exist? 5 | * 6 | * Individual tests can load the datasource, and avoid repetition, by adding: 7 | * `require('./init.js');` 8 | * of their source code. 9 | */ 10 | 11 | var chai = require('chai'); 12 | global.expect = chai.expect; 13 | global.assert = chai.assert; 14 | global.should = chai.should(); // Why is the function being executed? Because the "should" interface extends Object.prototype to provide a single getter as the starting point for your language assertions. 15 | 16 | global._ = require('lodash'); /*global _:true*/ 17 | 18 | var settings = require('./datasource-test-v6-plain.json'); 19 | global.getSettings = function() { /*global getSettings*/ 20 | return _.cloneDeep(settings); 21 | }; 22 | 23 | var DataSource = require('loopback-datasource-juggler').DataSource; 24 | global.getDataSource = global.getSchema = global.getConnector = function (customSettings) { 25 | (customSettings) /*eslint no-console: ["error", { allow: ["log"] }] */ 26 | ? console.log('\n\tcustomSettings will override global settings for datasource\n'/*, JSON.stringify(customSettings,null,2)*/) 27 | : console.log('\n\twill use global settings for datasource\n'); 28 | var settings = customSettings || getSettings(); 29 | // settings.log = { 30 | // type: 'file', 31 | // level: 'trace', 32 | // path: 'test/es-v5/elasticsearch-v5-'+Date.now()+'.log' 33 | // }; 34 | //console.log('\n\tsettings:\n', JSON.stringify(settings,null,2)); 35 | settings.connector = require('../../'); 36 | return new DataSource(settings); 37 | }; 38 | -------------------------------------------------------------------------------- /test/es-v7/init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Why does this file exist? 5 | * 6 | * Individual tests can load the datasource, and avoid repetition, by adding: 7 | * `require('./init.js');` 8 | * of their source code. 9 | */ 10 | 11 | var chai = require('chai'); 12 | global.expect = chai.expect; 13 | global.assert = chai.assert; 14 | global.should = chai.should(); // Why is the function being executed? Because the "should" interface extends Object.prototype to provide a single getter as the starting point for your language assertions. 15 | 16 | global._ = require('lodash'); /*global _:true*/ 17 | 18 | var settings = require('./datasource-test-v7-plain.json'); 19 | global.getSettings = function() { /*global getSettings*/ 20 | return _.cloneDeep(settings); 21 | }; 22 | 23 | var DataSource = require('loopback-datasource-juggler').DataSource; 24 | global.getDataSource = global.getSchema = global.getConnector = function (customSettings) { 25 | (customSettings) /*eslint no-console: ["error", { allow: ["log"] }] */ 26 | ? console.log('\n\tcustomSettings will override global settings for datasource\n'/*, JSON.stringify(customSettings,null,2)*/) 27 | : console.log('\n\twill use global settings for datasource\n'); 28 | var settings = customSettings || getSettings(); 29 | // settings.log = { 30 | // type: 'file', 31 | // level: 'trace', 32 | // path: 'test/es-v5/elasticsearch-v5-'+Date.now()+'.log' 33 | // }; 34 | //console.log('\n\tsettings:\n', JSON.stringify(settings,null,2)); 35 | settings.connector = require('../../'); 36 | return new DataSource(settings); 37 | }; 38 | -------------------------------------------------------------------------------- /lib/updateAll.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const log = require('debug')('loopback:connector:elasticsearch'); 3 | // CONSTANTS 4 | const SEARCHAFTERKEY = '_search_after'; 5 | const TOTALCOUNTKEY = '_total_count'; 6 | 7 | function updateAll(model, where, data, options, cb) { 8 | const self = this; 9 | if (self.debug) { 10 | log('ESConnector.prototype.updateAll', 'model', model, 'options', options, 'where', where, 'date', data); 11 | } 12 | const idName = self.idName(model); 13 | log('ESConnector.prototype.updateAll', 'idName', idName); 14 | 15 | const defaults = self.addDefaults(model, 'updateAll'); 16 | 17 | const reqBody = { 18 | query: self.buildWhere(model, idName, where).query 19 | }; 20 | 21 | reqBody.script = { 22 | inline: '', 23 | params: {} 24 | }; 25 | _.forEach(data, (value, key) => { 26 | if (key !== '_id' && key !== idName && key !== SEARCHAFTERKEY && key !== TOTALCOUNTKEY) { 27 | // default language for inline scripts is painless if ES 5, so this needs the extra params. 28 | reqBody.script.inline += `ctx._source.${key}=params.${key};`; 29 | reqBody.script.params[key] = value; 30 | if (key === 'docType') { 31 | reqBody.script.params[key] = model; 32 | } 33 | } 34 | }); 35 | 36 | const document = _.defaults({ 37 | body: reqBody 38 | }, defaults); 39 | log('ESConnector.prototype.updateAll', 'document to update', document); 40 | 41 | self.db.updateByQuery(document) 42 | .then(({ body }) => { 43 | log('ESConnector.prototype.updateAll', 'response', body); 44 | return cb(null, { 45 | updated: body.updated, 46 | total: body.total 47 | }); 48 | }).catch((error) => { 49 | log('ESConnector.prototype.updateAll', error.message); 50 | return cb(error); 51 | }); 52 | } 53 | 54 | module.exports.updateAll = updateAll; 55 | -------------------------------------------------------------------------------- /lib/replaceById.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const log = require('debug')('loopback:connector:elasticsearch'); 3 | // CONSTANTS 4 | const SEARCHAFTERKEY = '_search_after'; 5 | const TOTALCOUNTKEY = '_total_count'; 6 | 7 | function replaceById(modelName, id, data, options, callback) { 8 | const self = this; 9 | log('ESConnector.prototype.replaceById', 'modelName', modelName, 'id', id, 'data', data); 10 | 11 | const idName = self.idName(modelName); 12 | if (id === undefined || id === null) { 13 | throw new Error('id not set!'); 14 | } 15 | 16 | // eslint-disable-next-line no-underscore-dangle 17 | const modelProperties = this._models[modelName].properties; 18 | 19 | const document = self.addDefaults(modelName, 'replaceById'); 20 | document[self.idField] = self.getDocumentId(id); 21 | document.body = {}; 22 | _.assign(document.body, data); 23 | document.body.docType = modelName; 24 | if (Object.prototype.hasOwnProperty.call(modelProperties, idName)) { 25 | document.body[idName] = id; 26 | } 27 | if (document.body[SEARCHAFTERKEY] || document.body[TOTALCOUNTKEY]) { 28 | document.body[SEARCHAFTERKEY] = undefined; 29 | document.body[TOTALCOUNTKEY] = undefined; 30 | } 31 | log('ESConnector.prototype.replaceById', 'document', document); 32 | self.db.index( 33 | document 34 | ).then( 35 | ({ body }) => { 36 | log('ESConnector.prototype.replaceById', 'response', body); 37 | // eslint-disable-next-line no-underscore-dangle 38 | log('ESConnector.prototype.replaceById', 'will invoke callback with id:', body._id); 39 | // eslint-disable-next-line no-underscore-dangle 40 | callback(null, body._id); // the connector framework expects the id as a return value 41 | } 42 | ).catch((error) => { 43 | log('ESConnector.prototype.replaceById', error.message); 44 | callback(error); 45 | }); 46 | } 47 | 48 | module.exports.replaceById = replaceById; 49 | -------------------------------------------------------------------------------- /lib/buildWhere.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const log = require('debug')('loopback:connector:elasticsearch'); 3 | 4 | function buildWhere(model, idName, where) { 5 | const self = this; 6 | 7 | let nestedFields = _.map(self.settings.mappingProperties, (val, key) => (val.type === 'nested' ? key : null)); 8 | nestedFields = _.filter(nestedFields, (v) => v); 9 | log('ESConnector.prototype.buildWhere', 'model', model, 'idName', idName, 'where', JSON.stringify(where, null, 0)); 10 | 11 | const body = { 12 | query: { 13 | bool: { 14 | must: [], 15 | should: [], 16 | filter: [], 17 | must_not: [] 18 | } 19 | } 20 | }; 21 | 22 | self.buildNestedQueries(body, model, idName, where, nestedFields); 23 | if (body && body.query && body.query.bool 24 | && body.query.bool.must && body.query.bool.must.length === 0) { 25 | delete body.query.bool.must; 26 | } 27 | if (body && body.query && body.query.bool 28 | && body.query.bool.filter && body.query.bool.filter.length === 0) { 29 | delete body.query.bool.filter; 30 | } 31 | if (body && body.query && body.query.bool 32 | && body.query.bool.should && body.query.bool.should.length === 0) { 33 | delete body.query.bool.should; 34 | } 35 | if (body && body.query && body.query.bool 36 | && body.query.bool.must_not && body.query.bool.must_not.length === 0) { 37 | delete body.query.bool.must_not; 38 | } 39 | if (body && body.query && body.query.bool && _.isEmpty(body.query.bool)) { 40 | delete body.query.bool; 41 | } 42 | 43 | if (body && body.query && _.isEmpty(body.query)) { 44 | body.query = { 45 | bool: { 46 | must: { 47 | match_all: {} 48 | }, 49 | filter: [{ 50 | term: { 51 | docType: model 52 | } 53 | }] 54 | } 55 | }; 56 | } 57 | return body; 58 | } 59 | 60 | module.exports.buildWhere = buildWhere; 61 | -------------------------------------------------------------------------------- /lib/replaceOrCreate.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const log = require('debug')('loopback:connector:elasticsearch'); 3 | // CONSTANTS 4 | const SEARCHAFTERKEY = '_search_after'; 5 | const TOTALCOUNTKEY = '_total_count'; 6 | 7 | function replaceOrCreate(modelName, data, callback) { 8 | const self = this; 9 | log('ESConnector.prototype.replaceOrCreate', 'modelName', modelName, 'data', data); 10 | 11 | const idName = self.idName(modelName); 12 | const id = self.getDocumentId(data[idName]); 13 | if (id === undefined || id === null) { 14 | throw new Error('id not set!'); 15 | } 16 | 17 | const document = self.addDefaults(modelName, 'replaceOrCreate'); 18 | document[self.idField] = id; 19 | document.body = {}; 20 | _.assign(document.body, data); 21 | document.body.docType = modelName; 22 | if (document.body[SEARCHAFTERKEY] || document.body[TOTALCOUNTKEY]) { 23 | document.body[SEARCHAFTERKEY] = undefined; 24 | document.body[TOTALCOUNTKEY] = undefined; 25 | } 26 | log('ESConnector.prototype.replaceOrCreate', 'document', document); 27 | self.db.index( 28 | document 29 | ).then( 30 | ({ body }) => { 31 | log('ESConnector.prototype.replaceOrCreate', 'response', body); 32 | const options = { 33 | // eslint-disable-next-line no-underscore-dangle 34 | id: body._id, 35 | index: self.index 36 | }; 37 | if (self.mappingType) { 38 | options.type = self.mappingType; 39 | } 40 | return self.db.get(options); 41 | } 42 | ).then(({ body }) => { 43 | const totalCount = typeof body.hits.total === 'object' ? body.hits.total.value : body.hits.total; 44 | return callback(null, self.dataSourceToModel(modelName, body, idName, totalCount)); 45 | }).catch((error) => { 46 | log('ESConnector.prototype.replaceOrCreate', error.message); 47 | return callback(error, null); 48 | }); 49 | } 50 | 51 | module.exports.replaceOrCreate = replaceOrCreate; 52 | -------------------------------------------------------------------------------- /lib/updateAttributes.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const log = require('debug')('loopback:connector:elasticsearch'); 3 | // CONSTANTS 4 | const SEARCHAFTERKEY = '_search_after'; 5 | const TOTALCOUNTKEY = '_total_count'; 6 | 7 | function updateAttributes(modelName, id, data, callback) { 8 | const self = this; 9 | if (self.debug) { 10 | log('ESConnector.prototype.updateAttributes', 'modelName', modelName, 'id', id, 'data', data); 11 | } 12 | const idName = self.idName(modelName); 13 | log('ESConnector.prototype.updateAttributes', 'idName', idName); 14 | 15 | const defaults = self.addDefaults(modelName, 'updateAll'); 16 | 17 | const reqBody = { 18 | query: self.buildWhere(modelName, idName, { 19 | _id: id 20 | }).query 21 | }; 22 | 23 | reqBody.script = { 24 | inline: '', 25 | params: {} 26 | }; 27 | _.forEach(data, (value, key) => { 28 | if (key !== '_id' && key !== idName && key !== SEARCHAFTERKEY && key !== TOTALCOUNTKEY) { 29 | // default language for inline scripts is painless if ES 5, so this needs the extra params. 30 | reqBody.script.inline += `ctx._source.${key}=params.${key};`; 31 | reqBody.script.params[key] = value; 32 | if (key === 'docType') { 33 | reqBody.script.params[key] = modelName; 34 | } 35 | } 36 | }); 37 | 38 | const document = _.defaults({ 39 | body: reqBody 40 | }, defaults); 41 | log('ESConnector.prototype.updateAttributes', 'document to update', document); 42 | 43 | self.db.updateByQuery(document) 44 | .then(({ body }) => { 45 | log('ESConnector.prototype.updateAttributes', 'response', body); 46 | return callback(null, { 47 | updated: body.updated, 48 | total: body.total 49 | }); 50 | }).catch((error) => { 51 | log('ESConnector.prototype.updateAttributes', error.message); 52 | return callback(error); 53 | }); 54 | } 55 | 56 | module.exports.updateAttributes = updateAttributes; 57 | -------------------------------------------------------------------------------- /lib/buildNestedQueries.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const log = require('debug')('loopback:connector:elasticsearch'); 3 | 4 | function buildNestedQueries(body, model, idName, where, nestedFields) { 5 | /** 6 | * Return an empty match all object if no property is set in where filter 7 | * @example {where: {}} 8 | */ 9 | const self = this; 10 | if (_.keys(where).length === 0) { 11 | body = { 12 | query: { 13 | bool: { 14 | must: { 15 | match_all: {} 16 | }, 17 | filter: [{ 18 | term: { 19 | docType: model 20 | } 21 | }] 22 | } 23 | } 24 | }; 25 | log('ESConnector.prototype.buildNestedQueries', '\nbody', JSON.stringify(body, null, 0)); 26 | return body; 27 | } 28 | const rootPath = body.query; 29 | self.buildDeepNestedQueries(true, idName, where, 30 | body, rootPath, model, nestedFields); 31 | const docTypeQuery = _.find(rootPath.bool.filter, (v) => v.term && v.term.docType); 32 | let addedDocTypeToRootPath = false; 33 | if (typeof docTypeQuery !== 'undefined') { 34 | addedDocTypeToRootPath = true; 35 | docTypeQuery.term.docType = model; 36 | } else { 37 | addedDocTypeToRootPath = true; 38 | rootPath.bool.filter.push({ 39 | term: { 40 | docType: model 41 | } 42 | }); 43 | } 44 | 45 | if (addedDocTypeToRootPath) { 46 | if (!!rootPath && rootPath.bool && rootPath.bool.should && rootPath.bool.should.length !== 0) { 47 | rootPath.bool.must.push({ 48 | bool: { 49 | should: rootPath.bool.should 50 | } 51 | }); 52 | rootPath.bool.should = []; 53 | } 54 | 55 | if (!!rootPath && rootPath.bool 56 | && rootPath.bool.must_not && rootPath.bool.must_not.length !== 0) { 57 | rootPath.bool.must.push({ 58 | bool: { 59 | must_not: rootPath.bool.must_not 60 | } 61 | }); 62 | rootPath.bool.must_not = []; 63 | } 64 | } 65 | return true; 66 | } 67 | 68 | module.exports.buildNestedQueries = buildNestedQueries; 69 | -------------------------------------------------------------------------------- /examples/server/users/03-create-user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * To run: 3 | * DEBUG=loopback:connector:*,loopback:datasource,boot:test:* slc run 4 | * or 5 | * DEBUG=loopback:connector:*,loopback:datasource,boot:test:* node server/server.js 6 | */ 7 | 8 | var _ = require('lodash'); 9 | var Promise = require('bluebird'); 10 | 11 | var path = require('path'); 12 | var fileName = path.basename(__filename, '.js'); // gives the filename without the .js extension 13 | var debug = require('debug')('boot:test:'+fileName); 14 | 15 | module.exports = function(app) { 16 | var UserModel = app.models.UserModel; 17 | 18 | var userWithStringId4 = { 19 | id: '4', 20 | realm: 'portal', 21 | username: 'userWithStringId4@shoppinpal.com', 22 | email: 'userWithStringId4@shoppinpal.com', 23 | password: 'userWithStringId4' 24 | }; 25 | var userWithNumericId5 = { 26 | id: 5, 27 | realm: 'portal', 28 | username: 'userWithNumericId5@shoppinpal.com', 29 | email: 'userWithNumericId5@shoppinpal.com', 30 | password: 'userWithNumericId5' 31 | }; 32 | var userWithoutAnyId6 = { 33 | realm: 'portal', 34 | username: 'userWithoutAnyId6@shoppinpal.com', 35 | email: 'userWithoutAnyId6@shoppinpal.com', 36 | password: 'userWithoutAnyId6' 37 | }; 38 | 39 | var users = [userWithStringId4, userWithNumericId5, userWithoutAnyId6]; 40 | 41 | Promise.map( 42 | users, 43 | function (user) { 44 | return UserModel.createAsync(user) 45 | .then(function (resolvedData) { 46 | debug(JSON.stringify(resolvedData,null,2)); 47 | return Promise.resolve(); 48 | }, 49 | function (err) { 50 | console.error(err); 51 | return Promise.reject(); 52 | }); 53 | }, 54 | {concurrency: 1} 55 | ) 56 | .then(function () { 57 | debug('all work for UserModels finished'); 58 | }, 59 | function (err) { 60 | console.error(err); 61 | }); 62 | }; -------------------------------------------------------------------------------- /lib/create.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const log = require('debug')('loopback:connector:elasticsearch'); 3 | // CONSTANTS 4 | const SEARCHAFTERKEY = '_search_after'; 5 | const TOTALCOUNTKEY = '_total_count'; 6 | 7 | function create(model, data, done) { 8 | const self = this; 9 | if (self.debug) { 10 | log('ESConnector.prototype.create', model, data); 11 | } 12 | 13 | const idValue = self.getIdValue(model, data); 14 | const idName = self.idName(model); 15 | log('ESConnector.prototype.create', 'idName', idName, 'idValue', idValue); 16 | /* TODO: If model has custom id with generated false and 17 | if Id field is not prepopulated then create should fail. 18 | */ 19 | /* TODO: If model Id is not string and generated is true then 20 | create should fail because the auto generated es id is of type string and we cannot change it. 21 | */ 22 | const document = self.addDefaults(model, 'create'); 23 | document[self.idField] = self.getDocumentId(idValue); 24 | document.body = {}; 25 | _.assign(document.body, data); 26 | log('ESConnector.prototype.create', 'document', document); 27 | let method = 'create'; 28 | if (!document[self.idField]) { 29 | method = 'index'; // if there is no/empty id field, we must use the index method to create it (API 5.0) 30 | } 31 | document.body.docType = model; 32 | if (document.body[SEARCHAFTERKEY] || document.body[TOTALCOUNTKEY]) { 33 | document.body[SEARCHAFTERKEY] = undefined; 34 | document.body[TOTALCOUNTKEY] = undefined; 35 | } 36 | self.db[method]( 37 | document 38 | ).then( 39 | ({ body }) => { 40 | log('ESConnector.prototype.create', 'response', body); 41 | // eslint-disable-next-line no-underscore-dangle 42 | log('ESConnector.prototype.create', 'will invoke callback with id:', body._id); 43 | // eslint-disable-next-line no-underscore-dangle 44 | done(null, body._id); // the connector framework expects the id as a return value 45 | } 46 | ).catch((error) => { 47 | log('ESConnector.prototype.create', error.message); 48 | return done(error, null); 49 | }); 50 | } 51 | 52 | module.exports.create = create; 53 | -------------------------------------------------------------------------------- /examples/server/roles/01-create-role.js: -------------------------------------------------------------------------------- 1 | /** 2 | * To run: 3 | * DEBUG=loopback:connector:*,loopback:datasource,boot:test:* slc run 4 | * or 5 | * DEBUG=loopback:connector:*,loopback:datasource,boot:test:* node server/server.js 6 | */ 7 | 8 | var _ = require('lodash'); 9 | var Promise = require('bluebird'); 10 | 11 | var path = require('path'); 12 | var fileName = path.basename(__filename, '.js'); // gives the filename without the .js extension 13 | var debug = require('debug')('boot:test:'+fileName); 14 | 15 | module.exports = function(app) { 16 | var Role = app.models.Role; 17 | var RoleMapping = app.models.RoleMapping; 18 | 19 | var userWithStringId1 = { 20 | id: '1', 21 | realm: 'portal', 22 | username: 'userWithStringId1@shoppinpal.com', 23 | email: 'userWithStringId1@shoppinpal.com', 24 | password: 'userWithStringId1' 25 | }; 26 | var userWithNumericId2 = { 27 | id: 2, 28 | realm: 'portal', 29 | username: 'userWithNumericId2@shoppinpal.com', 30 | email: 'userWithNumericId2@shoppinpal.com', 31 | password: 'userWithNumericId2' 32 | }; 33 | var userWithoutAnyId3 = { 34 | realm: 'portal', 35 | username: 'userWithoutAnyId3@shoppinpal.com', 36 | email: 'userWithoutAnyId3@shoppinpal.com', 37 | password: 'userWithoutAnyId3' 38 | }; 39 | var users = [userWithStringId1, userWithNumericId2, userWithoutAnyId3]; 40 | 41 | Role.create( 42 | {name: 'admin'}, 43 | function(err, role) { 44 | if (err) { 45 | return debug(err); 46 | } 47 | debug(role); 48 | //make admin an admin 49 | role.principals.create({ 50 | principalType: RoleMapping.USER, 51 | principalId: userWithStringId1.id 52 | }, 53 | function (err, principal) { 54 | if (err) { 55 | return debug(err); 56 | } 57 | debug(principal); 58 | debug(userWithStringId1.username + ' now has role: ' + role.name); 59 | }); 60 | } 61 | ); 62 | }; -------------------------------------------------------------------------------- /examples/server/users/01-find-user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * To run: 3 | * DEBUG=loopback:connector:*,loopback:datasource,boot:test:* slc run 4 | * or 5 | * DEBUG=loopback:connector:*,loopback:datasource,boot:test:* node server/server.js 6 | */ 7 | 8 | var _ = require('lodash'); 9 | var Promise = require('bluebird'); 10 | 11 | var path = require('path'); 12 | var fileName = path.basename(__filename, '.js'); // gives the filename without the .js extension 13 | var debug = require('debug')('boot:test:'+fileName); 14 | 15 | module.exports = function(app) { 16 | var UserModel = app.models.UserModel; 17 | 18 | var userWithStringId1 = { 19 | id: '1', 20 | realm: 'portal', 21 | username: 'userWithStringId1@shoppinpal.com', 22 | email: 'userWithStringId1@shoppinpal.com', 23 | password: 'userWithStringId1' 24 | }; 25 | var userWithNumericId2 = { 26 | id: 2, 27 | realm: 'portal', 28 | username: 'userWithNumericId2@shoppinpal.com', 29 | email: 'userWithNumericId2@shoppinpal.com', 30 | password: 'userWithNumericId2' 31 | }; 32 | var userWithoutAnyId3 = { 33 | realm: 'portal', 34 | username: 'userWithoutAnyId3@shoppinpal.com', 35 | email: 'userWithoutAnyId3@shoppinpal.com', 36 | password: 'userWithoutAnyId3' 37 | }; 38 | 39 | var users = [userWithStringId1, userWithNumericId2, userWithoutAnyId3]; 40 | 41 | Promise.map( 42 | users, 43 | function (user) { 44 | return UserModel.findAsync({ 45 | where: {username: user.username} 46 | }) 47 | .then(function (resolvedData) { 48 | debug('findAsync', user.username, 'results:', JSON.stringify(resolvedData,null,2)); 49 | return Promise.resolve(); 50 | }, 51 | function (err) { 52 | console.error(err); 53 | return Promise.reject(); 54 | }); 55 | }, 56 | {concurrency: 1} 57 | ) 58 | .then(function () { 59 | debug('all work for UserModels finished'); 60 | }, 61 | function (err) { 62 | console.error(err); 63 | }); 64 | }; -------------------------------------------------------------------------------- /examples/server/roles/02-findOrCreate-role.js: -------------------------------------------------------------------------------- 1 | /** 2 | * To run: 3 | * DEBUG=loopback:connector:*,loopback:datasource,boot:test:* slc run 4 | * or 5 | * DEBUG=loopback:connector:*,loopback:datasource,boot:test:* node server/server.js 6 | */ 7 | 8 | var _ = require('lodash'); 9 | var Promise = require('bluebird'); 10 | 11 | var path = require('path'); 12 | var fileName = path.basename(__filename, '.js'); // gives the filename without the .js extension 13 | var debug = require('debug')('boot:test:'+fileName); 14 | 15 | module.exports = function(app) { 16 | var Role = app.models.Role; 17 | var RoleMapping = app.models.RoleMapping; 18 | 19 | var userWithStringId1 = { 20 | id: '1', 21 | realm: 'portal', 22 | username: 'userWithStringId1@shoppinpal.com', 23 | email: 'userWithStringId1@shoppinpal.com', 24 | password: 'userWithStringId1' 25 | }; 26 | var userWithNumericId2 = { 27 | id: 2, 28 | realm: 'portal', 29 | username: 'userWithNumericId2@shoppinpal.com', 30 | email: 'userWithNumericId2@shoppinpal.com', 31 | password: 'userWithNumericId2' 32 | }; 33 | var userWithoutAnyId3 = { 34 | realm: 'portal', 35 | username: 'userWithoutAnyId3@shoppinpal.com', 36 | email: 'userWithoutAnyId3@shoppinpal.com', 37 | password: 'userWithoutAnyId3' 38 | }; 39 | var users = [userWithStringId1, userWithNumericId2, userWithoutAnyId3]; 40 | 41 | Role.findOrCreate( 42 | {where: {name: 'admin'}}, // find 43 | {name: 'admin'}, // create 44 | function(err, role, created) { 45 | if (err) { 46 | return debug(err); 47 | } 48 | debug('created', created, 'role', role); 49 | //make admin an admin 50 | role.principals.create({ 51 | principalType: RoleMapping.USER, 52 | principalId: userWithStringId1.id 53 | }, 54 | function (err, principal) { 55 | if (err) { 56 | return debug(err); 57 | } 58 | debug(principal); 59 | debug(userWithStringId1.username + ' now has role: ' + role.name); 60 | }); 61 | } 62 | ); 63 | }; -------------------------------------------------------------------------------- /examples/server/users/02-findOrCreate-user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * To run: 3 | * DEBUG=loopback:connector:*,loopback:datasource,boot:test:* slc run 4 | * or 5 | * DEBUG=loopback:connector:*,loopback:datasource,boot:test:* node server/server.js 6 | */ 7 | 8 | var _ = require('lodash'); 9 | var Promise = require('bluebird'); 10 | 11 | var path = require('path'); 12 | var fileName = path.basename(__filename, '.js'); // gives the filename without the .js extension 13 | var debug = require('debug')('boot:test:'+fileName); 14 | 15 | module.exports = function(app) { 16 | var UserModel = app.models.UserModel; 17 | 18 | var userWithStringId1 = { 19 | id: '1', 20 | realm: 'portal', 21 | username: 'userWithStringId1@shoppinpal.com', 22 | email: 'userWithStringId1@shoppinpal.com', 23 | password: 'userWithStringId1' 24 | }; 25 | var userWithNumericId2 = { 26 | id: 2, 27 | realm: 'portal', 28 | username: 'userWithNumericId2@shoppinpal.com', 29 | email: 'userWithNumericId2@shoppinpal.com', 30 | password: 'userWithNumericId2' 31 | }; 32 | var userWithoutAnyId3 = { 33 | realm: 'portal', 34 | username: 'userWithoutAnyId3@shoppinpal.com', 35 | email: 'userWithoutAnyId3@shoppinpal.com', 36 | password: 'userWithoutAnyId3' 37 | }; 38 | 39 | var users = [userWithStringId1, userWithNumericId2, userWithoutAnyId3]; 40 | 41 | Promise.map( 42 | users, 43 | function (user) { 44 | return UserModel.findOrCreateAsync( 45 | {where: {username: user.username}}, // find 46 | user // create 47 | ) 48 | .spread(function (aUser, created) { 49 | // API changes: 2015-01-07, Version 2.13.0 50 | // add a flag to callback of findOrCreate to indicate find or create (Clark Wang) 51 | debug('findOrCreateAsync', user.username, 'created', created, 'results:', JSON.stringify(aUser,null,2)); 52 | return Promise.resolve(); 53 | }, 54 | function (err) { 55 | console.error(err); 56 | return Promise.reject(); 57 | }); 58 | }, 59 | {concurrency: 1} 60 | ) 61 | .then(function () { 62 | debug('all work for UserModels finished'); 63 | }, 64 | function (err) { 65 | console.error(err); 66 | }); 67 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback-connector-esv6", 3 | "version": "2.1.1", 4 | "description": "LoopBack Connector for Elasticsearch 6.x and 7.x", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "./node_modules/.bin/eslint .", 8 | "test": "mocha --recursive", 9 | "testv6": "mocha test/es-v6/**/*.js" 10 | }, 11 | "husky": { 12 | "hooks": { 13 | "pre-commit": "npm run lint", 14 | "pre-push": "npm run lint" 15 | } 16 | }, 17 | "keywords": [ 18 | "loopback", 19 | "elastic", 20 | "elasticsearch", 21 | "elasticsearchv6", 22 | "es", 23 | "esv6", 24 | "esv7", 25 | "loopback-connector", 26 | "connector" 27 | ], 28 | "author": "bharathkontham", 29 | "contributors": [ 30 | { 31 | "name": "Pulkit Singhal", 32 | "email": "pulkit@shoppinpal.com" 33 | }, 34 | { 35 | "name": "yagobski" 36 | }, 37 | { 38 | "name": "Aquid Shahwar", 39 | "email": "aquid.shahwar@gmail.com" 40 | }, 41 | { 42 | "name": "wolfgang-s" 43 | }, 44 | { 45 | "name": "Bharath Reddy Kontham", 46 | "email": "bharath2211@gmail.com" 47 | } 48 | ], 49 | "readmeFilename": "README.md", 50 | "repository": { 51 | "type": "git", 52 | "url": "https://github.com/strongloop-community/loopback-connector-elastic-search.git" 53 | }, 54 | "license": "MIT", 55 | "bugs": { 56 | "url": "https://github.com/strongloop-community/loopback-connector-elastic-search/issues" 57 | }, 58 | "homepage": "https://github.com/strongloop-community/loopback-connector-elastic-search", 59 | "dependencies": { 60 | "async": "3.1.0", 61 | "debug": "3.2.6", 62 | "elasticsearch": "15.2.0", 63 | "es6": "npm:@elastic/elasticsearch@6", 64 | "es7": "npm:@elastic/elasticsearch@7", 65 | "lodash": "4.17.11", 66 | "loopback-connector": "4.9.0", 67 | "ramda": "^0.26.1" 68 | }, 69 | "directories": { 70 | "example": "examples", 71 | "test": "test" 72 | }, 73 | "devDependencies": { 74 | "grunt": "^0.4.5", 75 | "grunt-mocha-test": "^0.12.1", 76 | "loopback-datasource-juggler": "^2.55.3", 77 | "should": "^5.2.0", 78 | "chai": "4.1.2", 79 | "eslint": "6.5.1", 80 | "eslint-config-airbnb": "18.0.1", 81 | "eslint-config-loopback": "8.0.0", 82 | "eslint-plugin-import": "2.18.2", 83 | "eslint-plugin-jsx-a11y": "6.2.3", 84 | "eslint-plugin-react": "7.15.0", 85 | "husky": "1.0.1", 86 | "mocha": "5.2.0", 87 | "sonarqube-scanner": "2.5.0", 88 | "supertest": "3.0.0" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /test/resource/datasource-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elasticsearch-ssl", 3 | "connector": "elasticsearch", 4 | "index": "shakespeare", 5 | "hosts": [ 6 | { 7 | "protocol": "https", 8 | "host": "hosted.foundcluster.com", 9 | "port": 9243, 10 | "auth": "username:password" 11 | } 12 | ], 13 | "apiVersion": "1.1", 14 | "log": "trace", 15 | "defaultSize": 50, 16 | "requestTimeout": 30000, 17 | "ssl": { 18 | "ca": "./../cacert.pem", 19 | "rejectUnauthorized": true 20 | }, 21 | "mappings": [ 22 | { 23 | "name": "User", 24 | "properties": { 25 | "id": {"type": "string", "index" : "not_analyzed"}, 26 | "seq": {"type": "integer"}, 27 | "name" : { 28 | "type" : "multi_field", 29 | "fields" : { 30 | "name" : {"type" : "string", "index" : "not_analyzed"}, 31 | "native" : {"type" : "string", "index" : "analyzed"} 32 | } 33 | }, 34 | "email": {"type": "string", "index" : "not_analyzed"}, 35 | "birthday": {"type": "date"}, 36 | "role": {"type": "string", "index" : "not_analyzed"}, 37 | "order": {"type": "integer"}, 38 | "vip": {"type": "boolean"} 39 | } 40 | }, 41 | { 42 | "name": "Customer", 43 | "properties": { 44 | "objectId": {"type": "string", "index" : "not_analyzed"}, 45 | "name" : { 46 | "type" : "multi_field", 47 | "fields" : { 48 | "name" : {"type" : "string", "index" : "not_analyzed"}, 49 | "native" : {"type" : "string", "index" : "analyzed"} 50 | } 51 | }, 52 | "email": {"type": "string", "index" : "not_analyzed"}, 53 | "birthday": {"type": "date"}, 54 | "role": {"type": "string", "index" : "not_analyzed"}, 55 | "order": {"type": "integer"}, 56 | "vip": {"type": "boolean"} 57 | } 58 | }, 59 | { 60 | "name": "AccessToken", 61 | "properties": { 62 | "id": { "type": "string", "index": "not_analyzed" }, 63 | "ttl": { "type": "integer" }, 64 | "created": { "type": "date" } 65 | } 66 | } 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /lib/automigrate.js: -------------------------------------------------------------------------------- 1 | let log = null; 2 | const _ = require('lodash'); 3 | 4 | /** 5 | * `Connector._models` are all known at the time `automigrate` is called 6 | * so it should be possible to work on all elasticsearch indicies and mappings at one time 7 | * unlike with `.connect()` when the models were still unknown so 8 | * initializing ES indicies and mappings in one go wasn't possible. 9 | * 10 | * @param models 11 | * @param cb 12 | */ 13 | const automigrate = (models, cb) => { 14 | log('ESConnector.prototype.automigrate', 'models:', models); 15 | const self = this; 16 | if (self.db) { 17 | if (!cb && (typeof models === 'function')) { 18 | cb = models; 19 | models = undefined; 20 | } 21 | // First argument is a model name 22 | if (typeof models === 'string') { 23 | models = [models]; 24 | } 25 | log('ESConnector.prototype.automigrate', 'models', models); 26 | 27 | // eslint-disable-next-line no-underscore-dangle 28 | models = models || Object.keys(self._models); 29 | 30 | let indices = []; 31 | let mappingTypes = []; 32 | 33 | _.forEach(models, (model) => { 34 | log('ESConnector.prototype.automigrate', 'model', model); 35 | const defaults = self.addDefaults(model); 36 | mappingTypes.push(defaults.type); 37 | indices.push(defaults.index); 38 | }); 39 | 40 | indices = _.uniq(indices); 41 | mappingTypes = _.uniq(mappingTypes); 42 | 43 | log('ESConnector.prototype.automigrate', 'calling self.db.indices.delete() for indices:', indices); 44 | cb(); 45 | // TODO: 46 | /* 47 | self.db.indices.delete({index: indices, ignore: 404}) 48 | .then(function(response) { 49 | log('ESConnector.prototype.automigrate', 'finished deleting all indices', response); 50 | return Promise.map( 51 | mappingTypes, 52 | function(mappingType){ 53 | return self.setupMapping(mappingType); 54 | }, 55 | {concurrency: 1} 56 | ) 57 | .then(function(){ 58 | log('ESConnector.prototype.automigrate', 'finished all mappings'); 59 | cb(); 60 | }); 61 | }) 62 | .catch(function(err){ 63 | log('ESConnector.prototype.automigrate', 'failed', err); 64 | cb(err); 65 | }); 66 | */ 67 | } else { 68 | log('ESConnector.prototype.automigrate', 'ERROR', 'Elasticsearch connector has not been initialized'); 69 | cb('Elasticsearch connector has not been initialized'); 70 | } 71 | }; 72 | 73 | module.exports = (dependencies) => { 74 | log = dependencies 75 | // eslint-disable-next-line no-console 76 | ? (dependencies.log || console.log) 77 | // eslint-disable-next-line no-console 78 | : console.log; 79 | return automigrate; 80 | }; 81 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v1.4.1 - Jun 6, 2017 2 | - Contributed by @AhsanAyaz 3 | - Fixed Bug: when `and` was nested inside `or`, the ES query was built incorrectly (#93, #99 and #100) 4 | - Many thanks to @aquid for jumping in multiple times and offering valuable advice during code reviews. 5 | 6 | ### v1.4.0 - Apr 23, 2017 7 | - contributed by @wolfgang-s 8 | - add support for ES 5.x (PR #87 and PR #89) 9 | 10 | ### v1.3.5 - March 7, 2017 11 | - Fixed Issue - Remove refresh option for unsupported methods 12 | - This issue was fixed in PR #86 13 | 14 | ### v1.3.4 - Feb 25, 2017 15 | - Fixed Issue #79 - Wait until the document is ready for searching 16 | - this issue was fixed in PR #81 17 | - Fixed Issue #85 - Update All for es2.3 and above 18 | - this issue was fixed in PR #84 19 | 20 | ### v1.3.3 - Feb 23, 2017 21 | - Fixed Issue #79 - Multiple where clause filter support without `and` filter 22 | - this issue was fixed in PR #78 23 | - Fixed Issue #73 - Nested and, or filter support added 24 | - this issue was fixed in PR #75 25 | - Fixed Issue #73 - `inq`,`nin`,`between`,`neq` filter support added 26 | - this issue was fixed in PR #75 27 | - Fixed Issue #28 - Include filter added 28 | - Minimum workflow to make include filter work until we optimize it for better performance. 29 | This issue was fixed in PR #71 30 | 31 | ### v1.3.2 - Jan 31, 2017 32 | - Fixed Issue #64 - Date strings were not returned as Javascript Data objects 33 | - this issue was fixed in PR #68 34 | - Fixed Issue #37 - MakeId refactoring applied for save method 35 | - Save method still had an old implementation of makeId method which was updated with 36 | getDocumentId method. This issue was fixed in PR #69 37 | - Fixed Issue - Objects inside an array were returned as strings 38 | - This issue was fixed in PR #70 39 | 40 | ### v1.3.1 - Dec 02, 2016 41 | - Fixed Regression - analyzers for index weren't being created anymore 42 | - originally this was added in #25 and got lost somewhere along the way 43 | 44 | ### v1.3.0 - Oct 17, 2016 45 | - contributed by @dtomasi 46 | - Issue #57: Fix for defaultSize setting 47 | - Issue #55: Support Signed Requests for AWS Elasticsearch-Service 48 | 49 | ### v1.2.0 - Sep 25, 2016 50 | - Add eslint infrastructure 51 | - `npm install --save-dev eslint@2.13.1` 52 | - ESLint v3.0.0 now requires Node.js 4 or higher. If you still need ESLint to run on Node.js < 4, then we recommend staying with ESLint v2.13.1 until you are ready to upgrade your Node.js version. 53 | - https://github.com/strongloop/loopback-contributor-docs/blob/master/eslint-guide.md 54 | - https://github.com/strongloop/eslint-config-loopback 55 | 56 | ### v1.1.0 - Sep 25, 2016 57 | - Fixed Issue #52 58 | - Multi Index usage 59 | - any model specific indices or mappings should be setup when the connector is initialized 60 | - mappings array in datasource..json may contain the index and type properties, which will be used to setup that model's index and mappings during connector initialization 61 | 62 | ### v1.0.8 - Sep 21, 2016 63 | - Fixed Issue #51 64 | 65 | ### v1.0.7 - Aug 11, 2016 66 | - Fixed Issue #45 67 | -------------------------------------------------------------------------------- /examples/client/client.js: -------------------------------------------------------------------------------- 1 | const dataSource = { 2 | "name": "elasticsearch-example-index-datasource", 3 | "connector": "esv6", 4 | "version": 6, 5 | "index": "example-index", 6 | "configuration": { 7 | "node": "http://localhost:9200", 8 | "requestTimeout": 30000, 9 | "pingTimeout": 3000 10 | }, 11 | "defaultSize": 50, 12 | "indexSettings": {}, 13 | "mappingType": "basedata", 14 | "mappingProperties": { 15 | "id": { 16 | "type": "keyword" 17 | }, 18 | "seq": { 19 | "type": "integer" 20 | }, 21 | "name": { 22 | "type": "keyword", 23 | "fields": { 24 | "native": { 25 | "type": "keyword" 26 | } 27 | } 28 | }, 29 | "email": { 30 | "type": "keyword" 31 | }, 32 | "birthday": { 33 | "type": "date" 34 | }, 35 | "role": { 36 | "type": "keyword" 37 | }, 38 | "order": { 39 | "type": "integer" 40 | }, 41 | "vip": { 42 | "type": "boolean" 43 | }, 44 | "objectId": { 45 | "type": "keyword" 46 | }, 47 | "ttl": { 48 | "type": "integer" 49 | }, 50 | "created": { 51 | "type": "date" 52 | } 53 | } 54 | }; 55 | 56 | const SupportedVersions = [6, 7]; // Supported elasticsearch versions 57 | // 'Client' will be assigned either Client6 or Client7 from below definitions based on version 58 | let Client = null; 59 | const { Client: Client6 } = require('es6'); 60 | const { Client: Client7 } = require('es7'); 61 | const version = 6; 62 | Client = version === 6 ? Client6 : Client7; 63 | const db = new Client(dataSource.configuration); 64 | 65 | db.ping().then(({ body }) => { 66 | console.log(body); 67 | }).catch((e) => { 68 | console.log(e); 69 | }); 70 | 71 | db.indices.create({ 72 | index: 'helloworld', 73 | body: { 74 | settings: {}, 75 | mappings: { 76 | basedata: { 77 | properties: { 78 | name: { 79 | type: 'keyword' 80 | } 81 | } 82 | } 83 | } 84 | } 85 | }).then((response) => { 86 | console.log(response); 87 | }).catch((e) => { 88 | console.log(e); 89 | }); 90 | 91 | /* db.indices.putMapping({ 92 | index: 'hello', 93 | type: 'basedata', 94 | body: { 95 | properties: { 96 | name: { 97 | type: 'keyword' 98 | } 99 | } 100 | } 101 | }).then(({ body }) => { 102 | console.log(body); 103 | }).catch((e) => { 104 | console.log(e); 105 | }); */ 106 | 107 | db.count({ 108 | index: 'hello' 109 | }).then(({ body }) => { 110 | console.log(body); 111 | }).catch((e) => { 112 | console.log(e); 113 | }); 114 | 115 | /* db.create({ 116 | index: 'hello', 117 | type: 'basedata', 118 | id: 'aasddd', 119 | body: { 120 | name: 'hello' 121 | } 122 | }).then(({ body }) => { 123 | console.log(body); 124 | }).catch((e) => { 125 | console.log(e); 126 | }); */ 127 | 128 | /* db.index({ 129 | index: 'hello', 130 | type: 'basedata', 131 | body: { 132 | name: 'hello2s' 133 | } 134 | }).then(({ body }) => { 135 | console.log(body, body._id); 136 | }).catch((e) => { 137 | console.log(e); 138 | }); */ 139 | -------------------------------------------------------------------------------- /lib/updateOrCreate.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const log = require('debug')('loopback:connector:elasticsearch'); 3 | // CONSTANTS 4 | const SEARCHAFTERKEY = '_search_after'; 5 | const TOTALCOUNTKEY = '_total_count'; 6 | 7 | function updateOrCreate(modelName, data, callback) { 8 | const self = this; 9 | log('ESConnector.prototype.updateOrCreate', 'modelName', modelName, 'data', data); 10 | 11 | const idName = self.idName(modelName); 12 | const id = self.getDocumentId(data[idName]); 13 | if (id === undefined || id === null) { 14 | throw new Error('id not set!'); 15 | } 16 | 17 | const defaults = self.addDefaults(modelName, 'updateOrCreate'); 18 | data.docType = modelName; 19 | if (data[SEARCHAFTERKEY] || data[TOTALCOUNTKEY]) { 20 | data[SEARCHAFTERKEY] = undefined; 21 | data[TOTALCOUNTKEY] = undefined; 22 | } 23 | self.db.update(_.defaults({ 24 | id, 25 | body: { 26 | doc: data, 27 | doc_as_upsert: true 28 | } 29 | }, defaults)).then(({ body }) => { 30 | /** 31 | * In the case of an update, elasticsearch only provides a confirmation that it worked 32 | * but does not provide any model data back. So what should be passed back in 33 | * the data object (second argument of callback)? 34 | * Q1) Should we just pass back the data that was meant to be updated 35 | * and came in as an argument to the updateOrCreate() call? This is what 36 | * the memory connector seems to do. 37 | * A: [Victor Law] Yes, that's fine to do. The reason why we are passing the data there 38 | * and back is to support databases that can add default values to undefined properties, 39 | * typically the id property is often generated by the backend. 40 | * Q2) OR, should we make an additional call to fetch the data for that id internally, 41 | * within updateOrCreate()? So we can make sure to pass back a data object? 42 | * A: [Victor Law] 43 | * - Most connectors don't fetch the inserted/updated data 44 | * and hope the data stored into DB 45 | * will be the same as the data sent to DB for create/update. 46 | * - It's true in most cases but not always. For example, the DB might have triggers 47 | * that change the value after the insert/update. 48 | * - We don't support that yet. 49 | * - In the future, that can be controlled via an options property, 50 | * such as fetchNewInstance = true. 51 | * 52 | * NOTE: Q1 based approach has been implemented for now. 53 | */ 54 | // eslint-disable-next-line no-underscore-dangle 55 | if (body._version === 1) { // distinguish if it was an update or create operation in ES 56 | // eslint-disable-next-line no-underscore-dangle 57 | data[idName] = body._id; 58 | // eslint-disable-next-line no-underscore-dangle 59 | log('ESConnector.prototype.updateOrCreate', 'assigned ID', idName, '=', body._id); 60 | } 61 | callback(null, data, { 62 | isNewInstance: body.created 63 | }); 64 | }).catch((error) => { 65 | log('ESConnector.prototype.updateOrCreate', error.message); 66 | return callback(error); 67 | }); 68 | } 69 | 70 | module.exports.updateOrCreate = updateOrCreate; 71 | -------------------------------------------------------------------------------- /examples/server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "name": "db", 4 | "connector": "memory" 5 | }, 6 | "elasticsearch-plain": { 7 | "name": "elasticsearch-plain", 8 | "connector": "esv6", 9 | "version": 6, 10 | "index": "example-index", 11 | "configuration": { 12 | "node": "http://localhost:9200", 13 | "requestTimeout": 30000, 14 | "pingTimeout": 3000 15 | }, 16 | "defaultSize": 50, 17 | "indexSettings": { 18 | "number_of_shards": 2, 19 | "number_of_replicas": 1 20 | }, 21 | "mappingType": "basedata", 22 | "mappingProperties": { 23 | "docType": { 24 | "type": "keyword", 25 | "index": true 26 | }, 27 | "id": { 28 | "type": "keyword", 29 | "index": true 30 | }, 31 | "seq": { 32 | "type": "integer", 33 | "index": true 34 | }, 35 | "name": { 36 | "type": "keyword", 37 | "index": true 38 | }, 39 | "email": { 40 | "type": "keyword", 41 | "index": true 42 | }, 43 | "birthday": { 44 | "type": "date", 45 | "index": true 46 | }, 47 | "role": { 48 | "type": "keyword", 49 | "index": true 50 | }, 51 | "order": { 52 | "type": "integer", 53 | "index": true 54 | }, 55 | "vip": { 56 | "type": "boolean", 57 | "index": true 58 | }, 59 | "objectId": { 60 | "type": "keyword", 61 | "index": true 62 | }, 63 | "ttl": { 64 | "type": "integer", 65 | "index": true 66 | }, 67 | "created": { 68 | "type": "date", 69 | "index": true 70 | } 71 | } 72 | }, 73 | "elasticsearch-ssl": { 74 | "name": "elasticsearch-ssl", 75 | "connector": "esv6", 76 | "version": 7, 77 | "index": "example-index", 78 | "configuration": { 79 | "node": "https://localhost:9200", 80 | "requestTimeout": 30000, 81 | "pingTimeout": 3000, 82 | "auth": { 83 | "username": "test", 84 | "password": "test" 85 | }, 86 | "ssl": { 87 | "rejectUnauthorized": true 88 | } 89 | }, 90 | "defaultSize": 50, 91 | "indexSettings": { 92 | "number_of_shards": 2, 93 | "number_of_replicas": 1 94 | }, 95 | "mappingProperties": { 96 | "docType": { 97 | "type": "keyword", 98 | "index": true 99 | }, 100 | "id": { 101 | "type": "keyword", 102 | "index": true 103 | }, 104 | "seq": { 105 | "type": "integer", 106 | "index": true 107 | }, 108 | "name": { 109 | "type": "keyword", 110 | "index": true 111 | }, 112 | "email": { 113 | "type": "keyword", 114 | "index": true 115 | }, 116 | "birthday": { 117 | "type": "date", 118 | "index": true 119 | }, 120 | "role": { 121 | "type": "keyword", 122 | "index": true 123 | }, 124 | "order": { 125 | "type": "integer", 126 | "index": true 127 | }, 128 | "vip": { 129 | "type": "boolean", 130 | "index": true 131 | }, 132 | "objectId": { 133 | "type": "keyword", 134 | "index": true 135 | }, 136 | "ttl": { 137 | "type": "integer", 138 | "index": true 139 | }, 140 | "created": { 141 | "type": "date", 142 | "index": true 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /lib/buildFilter.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const log = require('debug')('loopback:connector:elasticsearch'); 3 | 4 | function buildFilter(modelName, idName, criteria = {}, size = null, offset = null) { 5 | const self = this; 6 | log('ESConnector.prototype.buildFilter', 'model', modelName, 'idName', idName, 7 | 'criteria', JSON.stringify(criteria, null, 0)); 8 | 9 | if (idName === undefined || idName === null) { 10 | throw new Error('idName not set!'); 11 | } 12 | 13 | const filter = this.addDefaults(modelName, 'buildFilter'); 14 | filter.body = {}; 15 | 16 | if (size !== undefined && size !== null) { 17 | filter.size = size; 18 | } 19 | if (offset !== undefined && offset !== null) { 20 | filter.from = offset; 21 | } 22 | 23 | if (criteria) { 24 | // `criteria` is set by app-devs, therefore, it overrides any connector level arguments 25 | if (criteria.limit !== undefined && criteria.limit !== null) { 26 | filter.size = criteria.limit; 27 | } 28 | if (criteria.skip !== undefined && criteria.skip !== null) { 29 | filter.from = criteria.skip; 30 | } else if (criteria.offset !== undefined 31 | && criteria.offset !== null) { // use offset as an alias for skip 32 | filter.from = criteria.offset; 33 | } 34 | if (criteria.fields) { 35 | // Elasticsearch _source filtering code 36 | if (Array.isArray(criteria.fields) || typeof criteria.fields === 'string') { 37 | // eslint-disable-next-line no-underscore-dangle 38 | filter.body._source = criteria.fields; 39 | } else if (typeof criteria.fields === 'object' && Object.keys(criteria.fields).length > 0) { 40 | // eslint-disable-next-line no-underscore-dangle 41 | filter.body._source = { 42 | includes: _.map(_.pickBy(criteria.fields, (v) => v === true), (v, k) => k), 43 | excludes: _.map(_.pickBy(criteria.fields, (v) => v === false), (v, k) => k) 44 | }; 45 | } 46 | } 47 | if (criteria.searchafter && Array.isArray(criteria.searchafter) 48 | && criteria.searchafter.length) { 49 | filter.body.search_after = criteria.searchafter; 50 | filter.from = undefined; 51 | } 52 | if (criteria.order) { 53 | log('ESConnector.prototype.buildFilter', 'will delegate sorting to buildOrder()'); 54 | filter.body.sort = self.buildOrder(modelName, idName, criteria.order); 55 | } else { 56 | // eslint-disable-next-line no-underscore-dangle 57 | const modelProperties = this._models[modelName].properties; 58 | if (idName === 'id' && modelProperties.id.generated) { 59 | // filter.body.sort = ['_id']; // requires mapping to contain: ... 60 | // ...'_id' : {'index' : 'not_analyzed','store' : true} 61 | log('ESConnector.prototype.buildFilter', 'will sort on _id by default when IDs are meant to be auto-generated by elasticsearch'); 62 | filter.body.sort = ['_id']; 63 | } else { 64 | log('ESConnector.prototype.buildFilter', 'will sort on loopback specified IDs'); 65 | filter.body.sort = [idName]; // default sort should be based on fields marked as id 66 | } 67 | } 68 | if (criteria.where) { 69 | filter.body.query = self.buildWhere(modelName, idName, criteria.where).query; 70 | } else if (_.keys(criteria).length === 0) { 71 | filter.body = { 72 | query: { 73 | bool: { 74 | must: { 75 | match_all: {} 76 | }, 77 | filter: [{ 78 | term: { 79 | docType: modelName 80 | } 81 | }] 82 | } 83 | } 84 | }; 85 | } else if (!Object.prototype.hasOwnProperty.call(criteria, 'where')) { 86 | // For queries without 'where' filter, add docType filter 87 | filter.body.query = self.buildWhere(modelName, idName, criteria.where || {}).query; 88 | } 89 | // TODO: native query support must be secured to restrict data access to request model 90 | /* if (criteria.native) { 91 | filter.body = criteria.native; // assume that the developer has provided ES compatible DSL 92 | } */ 93 | } 94 | 95 | log('ESConnector.prototype.buildFilter', 'constructed', JSON.stringify(filter, null, 0)); 96 | return filter; 97 | } 98 | 99 | module.exports.buildFilter = buildFilter; 100 | -------------------------------------------------------------------------------- /test/es-v6/04.add-defaults-refresh-true.test.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: "off"*/ 2 | /*global getSchema should assert*/ 3 | describe('Add Defaults', function () { 4 | var testConnector, testConnector2, db, db2; 5 | 6 | before(function (done) { 7 | require('./init.js'); 8 | var settings = getSettings(); 9 | settings.log = 'error'; 10 | db = getDataSource(settings); 11 | 12 | var settings2 = getSettings(); 13 | settings2.refreshOn = ["save", "updateAttributes"]; 14 | db2 = getDataSource(settings2); 15 | 16 | var account = {real_name: {type: String, index: true, sort: true}}; 17 | db2.define("Account", account); 18 | db.define("Account", account); 19 | var bookProps = {real_name: {type: String, index: true, sort: true}}; 20 | var bookSettings = { 21 | "properties": { 22 | "real_name": { 23 | "type": "keyword" 24 | } 25 | }, 26 | "elasticsearch": { 27 | "create": { 28 | "refresh": false 29 | }, 30 | "destroy": { 31 | "refresh": false 32 | }, 33 | "destroyAll": { 34 | "refresh": "wait_for" 35 | } 36 | } 37 | }; 38 | db.define("Book", bookProps, bookSettings); 39 | db2.define("Book", bookProps, bookSettings); 40 | testConnector = db.connector; 41 | testConnector2 = db2.connector; 42 | db.automigrate(done); 43 | }); 44 | 45 | describe('Datasource specific settings', function () { 46 | 47 | it('modifying operations should have refresh true', function () { 48 | (typeof testConnector2.addDefaults('Account', 'create').refresh === 'undefined').should.be.true; 49 | (testConnector2.addDefaults('Account', 'save').refresh === true).should.be.true; 50 | (typeof testConnector2.addDefaults('Account', 'destroy').refresh === 'undefined').should.be.true; 51 | (typeof testConnector2.addDefaults('Account', 'destroyAll').refresh === 'undefined').should.be.true; 52 | (testConnector2.addDefaults('Account', 'updateAttributes').refresh === true).should.be.true; 53 | (typeof testConnector2.addDefaults('Account', 'updateOrCreate').refresh === 'undefined').should.be.true; 54 | }); 55 | 56 | it('create and destroy should have refresh false for model book', function () { 57 | (testConnector2.addDefaults('Book', 'destroy').refresh === false).should.be.true; 58 | (testConnector2.addDefaults('Book', 'create').refresh === false).should.be.true; 59 | (testConnector2.addDefaults('Book', 'save').refresh === true).should.be.true; 60 | (testConnector2.addDefaults('Book', 'destroyAll').refresh === 'wait_for').should.be.true; 61 | (testConnector2.addDefaults('Book', 'updateAttributes').refresh === true).should.be.true; 62 | (typeof testConnector2.addDefaults('Book', 'updateOrCreate').refresh === 'undefined').should.be.true; 63 | }); 64 | 65 | it('should never have a refresh attribute', function () { 66 | (typeof testConnector.addDefaults('Book', 'removeMappings').refresh === 'undefined').should.be.true; 67 | (typeof testConnector.addDefaults('Book', 'buildFilter').refresh === 'undefined').should.be.true; 68 | (typeof testConnector.addDefaults('Book', 'find').refresh === 'undefined').should.be.true; 69 | (typeof testConnector.addDefaults('Book', 'exists').refresh === 'undefined').should.be.true; 70 | (typeof testConnector.addDefaults('Book', 'count').refresh === 'undefined').should.be.true; 71 | }); 72 | }); 73 | 74 | describe('Model specific settings', function () { 75 | 76 | it('modifying operations should have refresh true', function () { 77 | (testConnector.addDefaults('Account', 'create').refresh === true).should.be.true; 78 | (testConnector.addDefaults('Account', 'save').refresh === true).should.be.true; 79 | (testConnector.addDefaults('Account', 'destroy').refresh === true).should.be.true; 80 | (testConnector.addDefaults('Account', 'destroyAll').refresh === true).should.be.true; 81 | (testConnector.addDefaults('Account', 'updateAttributes').refresh === true).should.be.true; 82 | (testConnector.addDefaults('Account', 'updateOrCreate').refresh === true).should.be.true; 83 | 84 | }); 85 | 86 | it('create and destroy should have refresh false for model book', function () { 87 | (testConnector.addDefaults('Book', 'destroy').refresh === false).should.be.true; 88 | (testConnector.addDefaults('Book', 'create').refresh === false).should.be.true; 89 | (testConnector.addDefaults('Book', 'save').refresh === true).should.be.true; 90 | (testConnector.addDefaults('Book', 'destroyAll').refresh === 'wait_for').should.be.true; 91 | (testConnector.addDefaults('Book', 'updateAttributes').refresh === true).should.be.true; 92 | (testConnector.addDefaults('Book', 'updateOrCreate').refresh === true).should.be.true; 93 | }); 94 | 95 | }); 96 | }); -------------------------------------------------------------------------------- /test/es-v7/04.add-defaults-refresh-true.test.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: "off"*/ 2 | /*global getSchema should assert*/ 3 | describe('Add Defaults', function () { 4 | var testConnector, testConnector2, db, db2; 5 | 6 | before(function (done) { 7 | require('./init.js'); 8 | var settings = getSettings(); 9 | settings.log = 'error'; 10 | db = getDataSource(settings); 11 | 12 | var settings2 = getSettings(); 13 | settings2.refreshOn = ["save", "updateAttributes"]; 14 | db2 = getDataSource(settings2); 15 | 16 | var account = {real_name: {type: String, index: true, sort: true}}; 17 | db2.define("Account", account); 18 | db.define("Account", account); 19 | var bookProps = {real_name: {type: String, index: true, sort: true}}; 20 | var bookSettings = { 21 | "properties": { 22 | "real_name": { 23 | "type": "keyword" 24 | } 25 | }, 26 | "elasticsearch": { 27 | "create": { 28 | "refresh": false 29 | }, 30 | "destroy": { 31 | "refresh": false 32 | }, 33 | "destroyAll": { 34 | "refresh": "wait_for" 35 | } 36 | } 37 | }; 38 | db.define("Book", bookProps, bookSettings); 39 | db2.define("Book", bookProps, bookSettings); 40 | testConnector = db.connector; 41 | testConnector2 = db2.connector; 42 | db.automigrate(done); 43 | }); 44 | 45 | describe('Datasource specific settings', function () { 46 | 47 | it('modifying operations should have refresh true', function () { 48 | (typeof testConnector2.addDefaults('Account', 'create').refresh === 'undefined').should.be.true; 49 | (testConnector2.addDefaults('Account', 'save').refresh === true).should.be.true; 50 | (typeof testConnector2.addDefaults('Account', 'destroy').refresh === 'undefined').should.be.true; 51 | (typeof testConnector2.addDefaults('Account', 'destroyAll').refresh === 'undefined').should.be.true; 52 | (testConnector2.addDefaults('Account', 'updateAttributes').refresh === true).should.be.true; 53 | (typeof testConnector2.addDefaults('Account', 'updateOrCreate').refresh === 'undefined').should.be.true; 54 | }); 55 | 56 | it('create and destroy should have refresh false for model book', function () { 57 | (testConnector2.addDefaults('Book', 'destroy').refresh === false).should.be.true; 58 | (testConnector2.addDefaults('Book', 'create').refresh === false).should.be.true; 59 | (testConnector2.addDefaults('Book', 'save').refresh === true).should.be.true; 60 | (testConnector2.addDefaults('Book', 'destroyAll').refresh === 'wait_for').should.be.true; 61 | (testConnector2.addDefaults('Book', 'updateAttributes').refresh === true).should.be.true; 62 | (typeof testConnector2.addDefaults('Book', 'updateOrCreate').refresh === 'undefined').should.be.true; 63 | }); 64 | 65 | it('should never have a refresh attribute', function () { 66 | (typeof testConnector.addDefaults('Book', 'removeMappings').refresh === 'undefined').should.be.true; 67 | (typeof testConnector.addDefaults('Book', 'buildFilter').refresh === 'undefined').should.be.true; 68 | (typeof testConnector.addDefaults('Book', 'find').refresh === 'undefined').should.be.true; 69 | (typeof testConnector.addDefaults('Book', 'exists').refresh === 'undefined').should.be.true; 70 | (typeof testConnector.addDefaults('Book', 'count').refresh === 'undefined').should.be.true; 71 | }); 72 | }); 73 | 74 | describe('Model specific settings', function () { 75 | 76 | it('modifying operations should have refresh true', function () { 77 | (testConnector.addDefaults('Account', 'create').refresh === true).should.be.true; 78 | (testConnector.addDefaults('Account', 'save').refresh === true).should.be.true; 79 | (testConnector.addDefaults('Account', 'destroy').refresh === true).should.be.true; 80 | (testConnector.addDefaults('Account', 'destroyAll').refresh === true).should.be.true; 81 | (testConnector.addDefaults('Account', 'updateAttributes').refresh === true).should.be.true; 82 | (testConnector.addDefaults('Account', 'updateOrCreate').refresh === true).should.be.true; 83 | 84 | }); 85 | 86 | it('create and destroy should have refresh false for model book', function () { 87 | (testConnector.addDefaults('Book', 'destroy').refresh === false).should.be.true; 88 | (testConnector.addDefaults('Book', 'create').refresh === false).should.be.true; 89 | (testConnector.addDefaults('Book', 'save').refresh === true).should.be.true; 90 | (testConnector.addDefaults('Book', 'destroyAll').refresh === 'wait_for').should.be.true; 91 | (testConnector.addDefaults('Book', 'updateAttributes').refresh === true).should.be.true; 92 | (testConnector.addDefaults('Book', 'updateOrCreate').refresh === true).should.be.true; 93 | }); 94 | 95 | }); 96 | }); -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | LoopBack, as member project of the OpenJS Foundation, use 4 | [Contributor Covenant v2.0](https://contributor-covenant.org/version/2/0/code_of_conduct) 5 | as their code of conduct. The full text is included 6 | [below](#contributor-covenant-code-of-conduct-v2.0) in English, and translations 7 | are available from the Contributor Covenant organisation: 8 | 9 | - [contributor-covenant.org/translations](https://www.contributor-covenant.org/translations) 10 | - [github.com/ContributorCovenant](https://github.com/ContributorCovenant/contributor_covenant/tree/release/content/version/2/0) 11 | 12 | Refer to the sections on reporting and escalation in this document for the 13 | specific emails that can be used to report and escalate issues. 14 | 15 | ## Reporting 16 | 17 | ### Project Spaces 18 | 19 | For reporting issues in spaces related to LoopBack, please use the email 20 | `tsc@loopback.io`. The LoopBack Technical Steering Committee (TSC) handles CoC issues related to the spaces that it 21 | maintains. The project TSC commits to: 22 | 23 | - maintain the confidentiality with regard to the reporter of an incident 24 | - to participate in the path for escalation as outlined in the section on 25 | Escalation when required. 26 | 27 | ### Foundation Spaces 28 | 29 | For reporting issues in spaces managed by the OpenJS Foundation, for example, 30 | repositories within the OpenJS organization, use the email 31 | `report@lists.openjsf.org`. The Cross Project Council (CPC) is responsible for 32 | managing these reports and commits to: 33 | 34 | - maintain the confidentiality with regard to the reporter of an incident 35 | - to participate in the path for escalation as outlined in the section on 36 | Escalation when required. 37 | 38 | ## Escalation 39 | 40 | The OpenJS Foundation maintains a Code of Conduct Panel (CoCP). This is a 41 | foundation-wide team established to manage escalation when a reporter believes 42 | that a report to a member project or the CPC has not been properly handled. In 43 | order to escalate to the CoCP send an email to 44 | `coc-escalation@lists.openjsf.org`. 45 | 46 | For more information, refer to the full 47 | [Code of Conduct governance document](https://github.com/openjs-foundation/cross-project-council/blob/HEAD/CODE_OF_CONDUCT.md). 48 | 49 | --- 50 | 51 | ## Contributor Covenant Code of Conduct v2.0 52 | 53 | ## Our Pledge 54 | 55 | We as members, contributors, and leaders pledge to make participation in our 56 | community a harassment-free experience for everyone, regardless of age, body 57 | size, visible or invisible disability, ethnicity, sex characteristics, gender 58 | identity and expression, level of experience, education, socio-economic status, 59 | nationality, personal appearance, race, religion, or sexual identity and 60 | orientation. 61 | 62 | We pledge to act and interact in ways that contribute to an open, welcoming, 63 | diverse, inclusive, and healthy community. 64 | 65 | ## Our Standards 66 | 67 | Examples of behavior that contributes to a positive environment for our 68 | community include: 69 | 70 | - Demonstrating empathy and kindness toward other people 71 | - Being respectful of differing opinions, viewpoints, and experiences 72 | - Giving and gracefully accepting constructive feedback 73 | - Accepting responsibility and apologizing to those affected by our mistakes, 74 | and learning from the experience 75 | - Focusing on what is best not just for us as individuals, but for the overall 76 | community 77 | 78 | Examples of unacceptable behavior include: 79 | 80 | - The use of sexualized language or imagery, and sexual attention or advances of 81 | any kind 82 | - Trolling, insulting or derogatory comments, and personal or political attacks 83 | - Public or private harassment 84 | - Publishing others' private information, such as a physical or email address, 85 | without their explicit permission 86 | - Other conduct which could reasonably be considered inappropriate in a 87 | professional setting 88 | 89 | ## Enforcement Responsibilities 90 | 91 | Community leaders are responsible for clarifying and enforcing our standards of 92 | acceptable behavior and will take appropriate and fair corrective action in 93 | response to any behavior that they deem inappropriate, threatening, offensive, 94 | or harmful. 95 | 96 | Community leaders have the right and responsibility to remove, edit, or reject 97 | comments, commits, code, wiki edits, issues, and other contributions that are 98 | not aligned to this Code of Conduct, and will communicate reasons for moderation 99 | decisions when appropriate. 100 | 101 | ## Scope 102 | 103 | This Code of Conduct applies within all community spaces, and also applies when 104 | an individual is officially representing the community in public spaces. 105 | Examples of representing our community include using an official e-mail address, 106 | posting via an official social media account, or acting as an appointed 107 | representative at an online or offline event. 108 | 109 | ## Enforcement 110 | 111 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 112 | reported to the community leaders responsible for enforcement at 113 | [tsc@loopback.io](mailto:tsc@loopback.io). All complaints will be reviewed and 114 | investigated promptly and fairly. 115 | 116 | All community leaders are obligated to respect the privacy and security of the 117 | reporter of any incident. 118 | 119 | ## Enforcement Guidelines 120 | 121 | Community leaders will follow these Community Impact Guidelines in determining 122 | the consequences for any action they deem in violation of this Code of Conduct: 123 | 124 | ### 1. Correction 125 | 126 | **Community Impact**: Use of inappropriate language or other behavior deemed 127 | unprofessional or unwelcome in the community. 128 | 129 | **Consequence**: A private, written warning from community leaders, providing 130 | clarity around the nature of the violation and an explanation of why the 131 | behavior was inappropriate. A public apology may be requested. 132 | 133 | ### 2. Warning 134 | 135 | **Community Impact**: A violation through a single incident or series of 136 | actions. 137 | 138 | **Consequence**: A warning with consequences for continued behavior. No 139 | interaction with the people involved, including unsolicited interaction with 140 | those enforcing the Code of Conduct, for a specified period of time. This 141 | includes avoiding interactions in community spaces as well as external channels 142 | like social media. Violating these terms may lead to a temporary or permanent 143 | ban. 144 | 145 | ### 3. Temporary Ban 146 | 147 | **Community Impact**: A serious violation of community standards, including 148 | sustained inappropriate behavior. 149 | 150 | **Consequence**: A temporary ban from any sort of interaction or public 151 | communication with the community for a specified period of time. No public or 152 | private interaction with the people involved, including unsolicited interaction 153 | with those enforcing the Code of Conduct, is allowed during this period. 154 | Violating these terms may lead to a permanent ban. 155 | 156 | ### 4. Permanent Ban 157 | 158 | **Community Impact**: Demonstrating a pattern of violation of community 159 | standards, including sustained inappropriate behavior, harassment of an 160 | individual, or aggression toward or disparagement of classes of individuals. 161 | 162 | **Consequence**: A permanent ban from any sort of public interaction within the 163 | community. 164 | 165 | ## Attribution 166 | 167 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 168 | version 2.0, available at 169 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 170 | 171 | Community Impact Guidelines were inspired by 172 | [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 173 | 174 | [homepage]: https://www.contributor-covenant.org 175 | 176 | For answers to common questions about this code of conduct, see the FAQ at 177 | https://www.contributor-covenant.org/faq. Translations are available at 178 | https://www.contributor-covenant.org/translations. 179 | -------------------------------------------------------------------------------- /lib/buildDeepNestedQueries.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const log = require('debug')('loopback:connector:elasticsearch'); 3 | 4 | function buildDeepNestedQueries( 5 | root, 6 | idName, 7 | where, 8 | body, 9 | queryPath, 10 | model, 11 | nestedFields 12 | ) { 13 | const self = this; 14 | _.forEach(where, (value, key) => { 15 | let cond = value; 16 | if (key === 'id' || key === idName) { 17 | key = '_id'; 18 | } 19 | const splitKey = key.split('.'); 20 | let isNestedKey = false; 21 | let nestedSuperKey = null; 22 | if (key.indexOf('.') > -1 && !!splitKey[0] && nestedFields.indexOf(splitKey[0]) > -1) { 23 | isNestedKey = true; 24 | // eslint-disable-next-line prefer-destructuring 25 | nestedSuperKey = splitKey[0]; 26 | } 27 | 28 | if (key === 'and' && Array.isArray(value)) { 29 | let andPath; 30 | if (root) { 31 | andPath = queryPath.bool.must; 32 | } else { 33 | const andObject = { 34 | bool: { 35 | must: [] 36 | } 37 | }; 38 | andPath = andObject.bool.must; 39 | queryPath.push(andObject); 40 | } 41 | cond.forEach((c) => { 42 | log('ESConnector.prototype.buildDeepNestedQueries', 'mapped', 'body', JSON.stringify(body, null, 0)); 43 | self.buildDeepNestedQueries(false, idName, c, body, andPath, model, nestedFields); 44 | }); 45 | } else if (key === 'or' && Array.isArray(value)) { 46 | let orPath; 47 | if (root) { 48 | orPath = queryPath.bool.should; 49 | } else { 50 | const orObject = { 51 | bool: { 52 | should: [] 53 | } 54 | }; 55 | orPath = orObject.bool.should; 56 | queryPath.push(orObject); 57 | } 58 | cond.forEach((c) => { 59 | log('ESConnector.prototype.buildDeepNestedQueries', 'mapped', 'body', JSON.stringify(body, null, 0)); 60 | self.buildDeepNestedQueries(false, idName, c, body, orPath, model, nestedFields); 61 | }); 62 | } else { 63 | let spec = false; 64 | let options = null; 65 | if (cond && cond.constructor.name === 'Object') { // need to understand 66 | options = cond.options; 67 | // eslint-disable-next-line prefer-destructuring 68 | spec = Object.keys(cond)[0]; 69 | cond = cond[spec]; 70 | } 71 | log('ESConnector.prototype.buildNestedQueries', 72 | 'spec', spec, 'key', key, 'cond', JSON.stringify(cond, null, 0), 'options', options); 73 | if (spec) { 74 | if (spec === 'gte' || spec === 'gt' || spec === 'lte' || spec === 'lt') { 75 | let rangeQuery = { 76 | range: {} 77 | }; 78 | const rangeQueryGuts = {}; 79 | rangeQueryGuts[spec] = cond; 80 | rangeQuery.range[key] = rangeQueryGuts; 81 | 82 | // Additional handling for nested Objects 83 | if (isNestedKey) { 84 | rangeQuery = { 85 | nested: { 86 | path: nestedSuperKey, 87 | score_mode: 'max', 88 | query: rangeQuery 89 | } 90 | }; 91 | } 92 | 93 | if (root) { 94 | queryPath.bool.must.push(rangeQuery); 95 | } else { 96 | queryPath.push(rangeQuery); 97 | } 98 | } 99 | 100 | /** 101 | * Logic for loopback `between` filter of where 102 | * @example {where: {size: {between: [0,7]}}} 103 | */ 104 | if (spec === 'between') { 105 | if (cond.length === 2 && (cond[0] <= cond[1])) { 106 | let betweenArray = { 107 | range: {} 108 | }; 109 | betweenArray.range[key] = { 110 | gte: cond[0], 111 | lte: cond[1] 112 | }; 113 | 114 | // Additional handling for nested Objects 115 | if (isNestedKey) { 116 | betweenArray = { 117 | nested: { 118 | path: nestedSuperKey, 119 | score_mode: 'max', 120 | query: betweenArray 121 | } 122 | }; 123 | } 124 | if (root) { 125 | queryPath.bool.must.push(betweenArray); 126 | } else { 127 | queryPath.push(betweenArray); 128 | } 129 | } 130 | } 131 | /** 132 | * Logic for loopback `inq`(include) filter of where 133 | * @example {where: { property: { inq: [val1, val2, ...]}}} 134 | */ 135 | if (spec === 'inq') { 136 | let inArray = { 137 | terms: {} 138 | }; 139 | inArray.terms[key] = cond; 140 | // Additional handling for nested Objects 141 | if (isNestedKey) { 142 | inArray = { 143 | nested: { 144 | path: nestedSuperKey, 145 | score_mode: 'max', 146 | query: inArray 147 | } 148 | }; 149 | } 150 | if (root) { 151 | queryPath.bool.must.push(inArray); 152 | } else { 153 | queryPath.push(inArray); 154 | } 155 | log('ESConnector.prototype.buildDeepNestedQueries', 156 | 'body', body, 157 | 'inArray', JSON.stringify(inArray, null, 0)); 158 | } 159 | 160 | /** 161 | * Logic for loopback `nin`(not include) filter of where 162 | * @example {where: { property: { nin: [val1, val2, ...]}}} 163 | */ 164 | if (spec === 'nin') { 165 | let notInArray = { 166 | terms: {} 167 | }; 168 | notInArray.terms[key] = cond; 169 | // Additional handling for nested Objects 170 | if (isNestedKey) { 171 | notInArray = { 172 | nested: { 173 | path: nestedSuperKey, 174 | score_mode: 'max', 175 | query: { 176 | bool: { 177 | must: [notInArray] 178 | } 179 | } 180 | } 181 | }; 182 | } 183 | if (root) { 184 | queryPath.bool.must_not.push(notInArray); 185 | } else { 186 | queryPath.push({ 187 | bool: { 188 | must_not: [notInArray] 189 | } 190 | }); 191 | } 192 | } 193 | 194 | /** 195 | * Logic for loopback `neq` (not equal) filter of where 196 | * @example {where: {role: {neq: 'lead' }}} 197 | */ 198 | if (spec === 'neq') { 199 | /** 200 | * First - filter the documents where the given property exists 201 | * @type {{exists: {field: *}}} 202 | */ 203 | // var missingFilter = {exists :{field : key}}; 204 | /** 205 | * Second - find the document where value not equals the given value 206 | * @type {{term: {}}} 207 | */ 208 | let notEqual = { 209 | term: {} 210 | }; 211 | notEqual.term[key] = cond; 212 | /** 213 | * Apply the given filter in the main filter(body) and on given path 214 | */ 215 | // Additional handling for nested Objects 216 | if (isNestedKey) { 217 | notEqual = { 218 | match: {} 219 | }; 220 | notEqual.match[key] = cond; 221 | notEqual = { 222 | nested: { 223 | path: nestedSuperKey, 224 | score_mode: 'max', 225 | query: { 226 | bool: { 227 | must: [notEqual] 228 | } 229 | } 230 | } 231 | }; 232 | } 233 | if (root) { 234 | queryPath.bool.must_not.push(notEqual); 235 | } else { 236 | queryPath.push({ 237 | bool: { 238 | must_not: [notEqual] 239 | } 240 | }); 241 | } 242 | 243 | 244 | // body.query.bool.must.push(missingFilter); 245 | } 246 | // TODO: near - For geolocations, return the closest points, 247 | // ...sorted in order of distance. Use with limit to return the n closest points. 248 | // TODO: like, nlike 249 | // TODO: ilike, inlike 250 | if (spec === 'like') { 251 | let likeQuery = { 252 | regexp: {} 253 | }; 254 | likeQuery.regexp[key] = cond; 255 | 256 | // Additional handling for nested Objects 257 | if (isNestedKey) { 258 | likeQuery = { 259 | nested: { 260 | path: nestedSuperKey, 261 | score_mode: 'max', 262 | query: { 263 | bool: { 264 | must: [likeQuery] 265 | } 266 | } 267 | } 268 | }; 269 | } 270 | if (root) { 271 | queryPath.bool.must.push(likeQuery); 272 | } else { 273 | queryPath.push(likeQuery); 274 | } 275 | } 276 | 277 | if (spec === 'nlike') { 278 | let nlikeQuery = { 279 | regexp: {} 280 | }; 281 | nlikeQuery.regexp[key] = cond; 282 | 283 | // Additional handling for nested Objects 284 | if (isNestedKey) { 285 | nlikeQuery = { 286 | nested: { 287 | path: nestedSuperKey, 288 | score_mode: 'max', 289 | query: { 290 | bool: { 291 | must_not: [nlikeQuery] 292 | } 293 | } 294 | } 295 | }; 296 | } 297 | if (root) { 298 | queryPath.bool.must_not.push(nlikeQuery); 299 | } else { 300 | queryPath.push({ 301 | bool: { 302 | must_not: [nlikeQuery] 303 | } 304 | }); 305 | } 306 | } 307 | // TODO: regex 308 | 309 | // geo_shape || geo_distance || geo_bounding_box 310 | if (spec === 'geo_shape' || spec === 'geo_distance' || spec === 'geo_bounding_box') { 311 | let geoQuery = { 312 | filter: {} 313 | }; 314 | geoQuery.filter[spec] = cond; 315 | 316 | if (isNestedKey) { 317 | geoQuery = { 318 | nested: { 319 | path: nestedSuperKey, 320 | score_mode: 'max', 321 | query: { 322 | bool: geoQuery 323 | } 324 | } 325 | }; 326 | if (root) { 327 | queryPath.bool.must.push(geoQuery); 328 | } else { 329 | queryPath.push(geoQuery); 330 | } 331 | } else if (root) { 332 | queryPath.bool.filter = geoQuery; 333 | } else { 334 | queryPath.push({ 335 | bool: geoQuery 336 | }); 337 | } 338 | } 339 | } else { 340 | let nestedQuery = {}; 341 | if (typeof value === 'string') { 342 | value = value.trim(); 343 | if (value.indexOf(' ') > -1) { 344 | nestedQuery.match_phrase = {}; 345 | nestedQuery.match_phrase[key] = value; 346 | } else { 347 | nestedQuery.match = {}; 348 | nestedQuery.match[key] = value; 349 | } 350 | } else { 351 | nestedQuery.match = {}; 352 | nestedQuery.match[key] = value; 353 | } 354 | // Additional handling for nested Objects 355 | if (isNestedKey) { 356 | nestedQuery = { 357 | nested: { 358 | path: nestedSuperKey, 359 | score_mode: 'max', 360 | query: { 361 | bool: { 362 | must: [nestedQuery] 363 | } 364 | } 365 | } 366 | }; 367 | } 368 | 369 | if (root) { 370 | queryPath.bool.must.push(nestedQuery); 371 | } else { 372 | queryPath.push(nestedQuery); 373 | } 374 | 375 | log('ESConnector.prototype.buildDeepNestedQueries', 376 | 'body', body, 377 | 'nestedQuery', JSON.stringify(nestedQuery, null, 0)); 378 | } 379 | } 380 | }); 381 | } 382 | 383 | module.exports.buildDeepNestedQueries = buildDeepNestedQueries; 384 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # loopback-connector-elastic-search 3 | 4 | [![Join the chat at https://gitter.im/strongloop-community/loopback-connector-elastic-search](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/strongloop-community/loopback-connector-elastic-search?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | Elasticsearch(versions 6.x and 7.x) datasource connector for [Loopback 3.x](https://loopback.io/). 7 | 8 | # Table of Contents 9 | 10 | - [Overview](#overview) 11 | - [Install this connector in your loopback app](#install-this-connector-in-your-loopback-app) 12 | - [Configuring connector](#configuring-connector) 13 | - [Required properties](#required) 14 | - [Recommended properties](#recommended) 15 | - [Optional properties](#optional) 16 | - [Sample for copy paste](#sample) 17 | 18 | - [Elasticsearch SearchAfter Support](#elasticsearch-searchafter-support) 19 | - [TotalCount Support for search](#totalcount-support-for-search) 20 | - [Example](#about-the-example-app) 21 | - [Troubleshooting](#troubleshooting) 22 | - [Contributing](#contributing) 23 | - [Frequently Asked Questions](#faqs) 24 | 25 | ## Overview 26 | 27 | 1. `lib` directory has the entire source code for this connector 28 | 1. this is what gets downloaded to your `node_modules` folder when you run `npm install loopback-connector-esv6 --save --save-exact` 29 | 1. `examples` directory has a loopback app which uses this connector 30 | 1. this is not published to NPM, it is only here for demo purposes 31 | 1. it will not be downloaded to your `node_modules` folder! 32 | 1. similarly the `examples/server/datasources.json` file is there for this demo app to use 33 | 1. you can copy their content over to `/server/datasources.json` or `/server/datasources..js` if you want and edit it there but don't start editing the files inside `examples/server` itself and expect changes to take place in your app! 34 | 1. `test` directory has unit tests 35 | 1. it does not reuse the loopback app from the `examples` folder 36 | 1. instead, loopback and ES/datasource are built and injected programatically 37 | 1. this directory is not published to NPM. 38 | 1. Refer to `.npmignore` if you're still confused about what's part of the *published* connector and what's not. 39 | 1. You will find the `datasources.json` files in this repo mention various configurations: 40 | 1. `elasticsearch-ssl` 41 | 2. `elasticsearch-plain` 42 | 3. `db` 43 | 4. You don't need them all! They are just examples to help you see the various ways in which you can configure a datasource. Delete the ones you don't need and keep the one you want. For example, most people will start off with `elasticsearch-plain` and then move on to configuring the additional properties that are exemplified in `elasticsearch-ssl`. You can mix & match if you'd like to have mongo and es and memory, all three! These are basics of the "connector" framework in loooback and not something we added. 44 | 1. Don't forget to edit your `model-config.json` file and point the models at the `dataSource` you want to use. 45 | 46 | ## Install this connector in your loopback app 47 | 48 | ```bash 49 | cd 50 | npm install loopback-connector-esv6 --save --save-exact 51 | ``` 52 | 53 | ## Configuring connector 54 | 55 | ### Important Note 56 | 57 | - **This connector will only connect to one index per datasource.** 58 | - This package is created to support ElasticSearch v6.x and 7.x only. 59 | - `docType` property is automatically added in mapping properties which is required to differentiate documents stored in index with loopback model data. It stores loopback modelName value. `docType: { type: "keyword", index: true }` 60 | 61 | ### Required 62 | 63 | - **name:** name of the connector. 64 | - **connector:** Elasticsearch driver **'esv6'**. 65 | - **configuration:** Elasticsearch client configuraiton object which includes nodes, authetication and ssl coonfiguration. Please refer this [official link](https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/client-configuration.html) for more information on configuraiton. 66 | - **index:** Name of the ElasticSearch index `eg: shakespeare`. 67 | - **version:** specify the major version of the Elasticsearch nodes you will be connecting to. Supported versions: [6, 7] `eg: version: 7` 68 | - **mappingType:** mapping type for provided index. defaults to `basedata`. Required only for version: 6 69 | - **mappingProperties:** An object with properties for above mentioned `mappingType` 70 | 71 | ### Optional 72 | 73 | - **indexSettings:** optional settings object for creating index. 74 | - **defaultSize:** Search size limit. Default is 50. 75 | 76 | ### Sample 77 | 78 | 1.Edit **datasources.json** and set: 79 | 80 | ```javascript 81 | 82 | "elastic-search-ssl": { 83 | "name": "elasticsearch-example-index-datasource", 84 | "connector": "esv6", 85 | "version": 7, 86 | "index": "example-index", 87 | "configuration": { // Elastic client configuration 88 | "node": "http://localhost:9200", 89 | "requestTimeout": 30000, 90 | "pingTimeout": 3000, 91 | "auth": { 92 | "username": "test", 93 | "password": "test" 94 | }, 95 | "ssl": { 96 | "rejectUnauthorized": true 97 | } 98 | }, 99 | "defaultSize": 50, 100 | "indexSettings": { // Elastic index settings 101 | "number_of_shards": 2, 102 | "number_of_replicas": 1 103 | }, 104 | "mappingType": "basedata", // not required for version: 7, will be ignored 105 | "mappingProperties": { 106 | "docType": { 107 | "type": "keyword", 108 | "index": true 109 | }, 110 | "id": { 111 | "type": "keyword", 112 | "index": true 113 | }, 114 | "seq": { 115 | "type": "integer", 116 | "index": true 117 | }, 118 | "name": { 119 | "type": "keyword", 120 | "index": true 121 | }, 122 | "email": { 123 | "type": "keyword", 124 | "index": true 125 | }, 126 | "birthday": { 127 | "type": "date", 128 | "index": true 129 | }, 130 | "role": { 131 | "type": "keyword", 132 | "index": true 133 | }, 134 | "order": { 135 | "type": "integer", 136 | "index": true 137 | }, 138 | "vip": { 139 | "type": "boolean", 140 | "index": true 141 | }, 142 | "objectId": { 143 | "type": "keyword", 144 | "index": true 145 | }, 146 | "ttl": { 147 | "type": "integer", 148 | "index": true 149 | }, 150 | "created": { 151 | "type": "date", 152 | "index": true 153 | } 154 | } 155 | } 156 | ``` 157 | 158 | 2.You can peek at `/examples/server/datasources.json` for more hints. 159 | 160 | ## Elasticsearch SearchAfter Support 161 | 162 | - ```search_after``` feature of elasticsearch is supported in loopback filter. 163 | - For this, you need to create a property in model called ```_search_after``` with loopback type ```["any"]```. This field cannot be updated using in connector. 164 | - Elasticsearch ```sort``` value will return in this field. 165 | - You need pass ```_search_after``` value in ```searchafter``` key of loopback filter. 166 | - Example filter query for ```find```. 167 | 168 | ```json 169 | { 170 | "where": { 171 | "username": "hello" 172 | }, 173 | "order": "created DESC", 174 | "searchafter": [ 175 | 1580902552905 176 | ], 177 | "limit": 4 178 | } 179 | ``` 180 | 181 | - Example result. 182 | 183 | ```json 184 | [ 185 | { 186 | "id": "1bb2dd63-c7b9-588e-a942-15ca4f891a80", 187 | "username": "test", 188 | "_search_after": [ 189 | 1580902552905 190 | ], 191 | "created": "2020-02-05T11:35:52.905Z" 192 | }, 193 | { 194 | "id": "fd5ea4df-f159-5816-9104-22147f2a740f", 195 | "username": "test3", 196 | "_search_after": [ 197 | 1580902552901 198 | ], 199 | "created": "2020-02-05T11:35:52.901Z" 200 | }, 201 | { 202 | "id": "047c0adb-13ea-5f80-a772-3d2a4691d47a", 203 | "username": "test4", 204 | "_search_after": [ 205 | 1580902552897 206 | ], 207 | "created": "2020-02-05T11:35:52.897Z" 208 | } 209 | ] 210 | ``` 211 | 212 | - This is useful for pagination. To go to previous page, change sorting order. 213 | 214 | ## TotalCount Support for search 215 | 216 | - ```total``` value from elasticsearch for search queries is now supported in loopback response. 217 | - For this, you need to create a property in model called ```_total_count``` with loopback type ```"number"```. This field cannot be updated using in connector. 218 | - Example response ```find```. 219 | 220 | ```json 221 | [ 222 | { 223 | "id": "1bb2dd63-c7b9-588e-a942-15ca4f891a80", 224 | "username": "test", 225 | "_search_after": [ 226 | 1580902552905 227 | ], 228 | "_total_count": 3, 229 | "created": "2020-02-05T11:35:52.905Z" 230 | }, 231 | { 232 | "id": "fd5ea4df-f159-5816-9104-22147f2a740f", 233 | "username": "test3", 234 | "_search_after": [ 235 | 1580902552901 236 | ], 237 | "_total_count": 3, 238 | "created": "2020-02-05T11:35:52.901Z" 239 | }, 240 | { 241 | "id": "047c0adb-13ea-5f80-a772-3d2a4691d47a", 242 | "username": "test4", 243 | "_search_after": [ 244 | 1580902552897 245 | ], 246 | "_total_count": 3, 247 | "created": "2020-02-05T11:35:52.897Z" 248 | } 249 | ] 250 | ``` 251 | 252 | ## About the example app 253 | 254 | 1. The `examples` directory contains a loopback app which uses this connector. 255 | 1. You can point this example at your own elasticsearch instance or use the quick instances provided via docker. 256 | 257 | ## Troubleshooting 258 | 259 | 1. Do you have both `elasticsearch-ssl` and `elasticsearch-plain` in your `datasources.json` file? You just need one of them (not both), based on how you've setup your ES instance. 260 | 1. Did you forget to set `model-config.json` to point at the datasource you configured? Maybe you are using a different or misspelled name than what you thought you had! 261 | 1. Make sure to configure major version of Elastic in `version` 262 | 1. Maybe the version of ES you are using isn't supported by the client that this project uses. Try removing the `elasticsearch` sub-dependency from `/node_modules/loopback-connector-esv6/node_modules` folder and then install the latest client: 263 | 1. `cd /node_modules/loopback-connector-esv6/node_modules` 264 | 1. then remove `es6` && `es7` folder 265 | 1. unix/mac quickie: `rm -rf es6 es7` 266 | 1. `npm install` 267 | 1. go back to yourApp's root directory 268 | 1. unix/mac quickie: `cd ` 269 | 1. And test that you can now use the connector without any issues! 270 | 1. These changes can easily get washed away for several reasons. So for a more permanent fix that adds the version you want to work on into a release of this connector, please look into [Contributing](#contributing). 271 | 272 | ## Contributing 273 | 274 | 1. Feel free to [contribute via PR](https://github.com/strongloop-community/loopback-connector-elastic-search/pulls) or [open an issue](https://github.com/strongloop-community/loopback-connector-elastic-search/issues) for discussion or jump into the [gitter chat room](https://gitter.im/strongloop-community/loopback-connector-elastic-search) if you have ideas. 275 | 1. I recommend that project contributors who are part of the team: 276 | 1. should merge `master` into `develop` ... if they are behind, before starting the `feature` branch 277 | 1. should create `feature` branches from the `develop` branch 278 | 1. should merge `feature` into `develop` then create a `release` branch to: 279 | 1. update the changelog 280 | 1. close related issues and mention release version 281 | 1. update the readme 282 | 1. fix any bugs from final testing 283 | 1. commit locally and run `npm-release x.x.x -m ""` 284 | 1. merge `release` into both `master` and `develop` 285 | 1. push `master` and `develop` to GitHub 286 | 1. For those who use forks: 287 | 1. please submit your PR against the `develop` branch, if possible 288 | 1. if you must submit your PR against the `master` branch ... I understand and I can't stop you. I only hope that there is a good reason like `develop` not being up-to-date with `master` for the work you want to build upon. 289 | 1. `npm-release -m ` may be used to publish. Pubilshing to NPM should happen from the `master` branch. It should ideally only happen when there is something release worthy. There's no point in publishing just because of changes to `test` or `examples` folder or any other such entities that aren't part of the "published module" (refer to `.npmignore`) to begin with. 290 | 291 | ## FAQs 292 | 293 | 1. How do we enable or disable the logs coming from the underlying elasticsearch client? There may be a need to debug/troubleshoot at times. 294 | 1. Use the env variable `DEBUG=elasticsearch` for elastic client logs. 295 | 1. How do we enable or disable the logs coming from this connector? 296 | 1. By default if you do not set the following env variable, they are disabled: `DEBUG=loopback:connector:elasticsearch` 297 | 1. What are the tests about? Can you provide a brief overview? 298 | 1. Tests are prefixed with `01` or `02` etc. in order to run them in that order by leveraging default alphabetical sorting. 299 | 1. The `02.basic-querying.test.js` file uses two models to test various CRUD operations that any connector must provide, like `find(), findById(), findByIds(), updateAttributes()` etc. 300 | 1. the two models are `User` and `Customer` 301 | 2. their ES *mappings* are laid out in `test/resource/datasource-test.json` 302 | 3. their loopback *definitions* can be found in the first `before` block that performs setup in `02.basic-querying.test.js` file ... these are the equivalent of a `MyModel.json` in your real loopback app. 303 | 1. naturally, this is also where we define which property serves as the `id` for the model and if its [generated](https://docs.strongloop.com/display/APIC/Model+definition+JSON+file#ModeldefinitionJSONfile-IDproperties) or not 304 | 1. How do we get elasticserch to take over ID generation? 305 | 1. An automatically generated id-like field that is maintained by ES is `_id`. Without some sort of es-field-level-scripting-on-index (if that is possible at all) ... I am not sure how we could ask elasticsearch to take over auto-generating an id-like value for any arbitrary field! So the connector is setup such that adding `id: {type: String, generated: true, id: true}` will tell it to use `_id` as the actual field backing the `id` ... you can keep using the doing `model.id` abstraction and in the background `_id` values are mapped to it. 306 | 1. Will this work for any field marked as with `generated: true` and `id: true`? 307 | 1. No! The connector isn't coded that way right now ... while it is an interesting idea to couple any such field with ES's `_id` field inside this connector ... I am not sure if this is the right thing to do. If you had `objectId: {type: String, generated: true, id: true}` then you won't find a real `objectId` field in your ES documents. Would that be ok? Wouldn't that confuse developers who want to write custom queries and run 3rd party app against their ES instance? Don't use `objectId`, use `_id` would have to be common knowledge. Is that ok? 308 | -------------------------------------------------------------------------------- /lib/esConnector.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names */ 2 | const util = require('util'); 3 | const fs = require('fs'); 4 | const _ = require('lodash'); 5 | const R = require('ramda'); 6 | 7 | const log = require('debug')('loopback:connector:elasticsearch'); 8 | 9 | const SupportedVersions = [6, 7]; // Supported elasticsearch versions 10 | // 'Client' will be assigned either Client6 or Client7 from below definitions based on version 11 | let Client = null; 12 | const { Client: Client6 } = require('es6'); 13 | const { Client: Client7 } = require('es7'); 14 | const { Connector } = require('loopback-connector'); 15 | const automigrate = require('./automigrate.js')({ 16 | log 17 | }); 18 | const { setupIndex } = require('./setupIndex'); 19 | const { all } = require('./all'); 20 | const { buildDeepNestedQueries } = require('./buildDeepNestedQueries'); 21 | const { buildNestedQueries } = require('./buildNestedQueries'); 22 | const { buildFilter } = require('./buildFilter'); 23 | const { buildOrder } = require('./buildOrder'); 24 | const { buildWhere } = require('./buildWhere'); 25 | const { count } = require('./count'); 26 | const { create } = require('./create'); 27 | const { destroy } = require('./destroy'); 28 | const { destroyAll } = require('./destroyAll'); 29 | const { exists } = require('./exists'); 30 | const { find } = require('./find'); 31 | const { replaceById } = require('./replaceById'); 32 | const { replaceOrCreate } = require('./replaceOrCreate'); 33 | const { save } = require('./save'); 34 | const { updateAll } = require('./updateAll'); 35 | const { updateAttributes } = require('./updateAttributes'); 36 | const { updateOrCreate } = require('./updateOrCreate'); 37 | 38 | // CONSTANTS 39 | const SEARCHAFTERKEY = '_search_after'; 40 | const TOTALCOUNTKEY = '_total_count'; 41 | 42 | /** 43 | * Connector constructor 44 | * @param {object} datasource settings 45 | * @param {object} dataSource 46 | * @constructor 47 | */ 48 | class ESConnector { 49 | constructor(settings, dataSource) { 50 | Connector.call(this, 'elasticsearch', settings); 51 | const defaultRefreshIndexAPIs = [ 52 | 'create', 53 | 'save', 54 | 'destroy', 55 | 'destroyAll', 56 | 'updateAttributes', 57 | 'updateOrCreate', 58 | 'updateAll', 59 | 'replaceOrCreate', 60 | 'replaceById' 61 | ]; 62 | this.configuration = settings.configuration || {}; 63 | this.version = settings.version; 64 | this.mappingType = settings.version < 7 ? settings.mappingType || 'basedata' : null; 65 | this.index = settings.index; 66 | this.indexSettings = settings.indexSettings || {}; 67 | this.defaultSize = (settings.defaultSize || 50); 68 | this.idField = 'id'; 69 | this.refreshOn = defaultRefreshIndexAPIs; 70 | 71 | this.debug = settings.debug || log.enabled; 72 | if (this.debug) { 73 | log('Settings: %j', settings); 74 | } 75 | 76 | this.dataSource = dataSource; 77 | } 78 | } 79 | 80 | /** 81 | * Initialize connector with datasource, configure settings and return 82 | * @param {object} dataSource 83 | * @param {function} done callback 84 | */ 85 | module.exports.initialize = (dataSource, callback) => { 86 | if (!R.has('settings', dataSource) || !R.has('version', dataSource.settings) || SupportedVersions.indexOf(dataSource.settings.version) === -1) { 87 | return; 88 | } 89 | const { version } = dataSource.settings; 90 | Client = version === 6 ? Client6 : Client7; 91 | const settings = dataSource.settings || {}; 92 | 93 | dataSource.connector = new ESConnector(settings, dataSource); 94 | 95 | if (callback) { 96 | dataSource.connector.connect(callback); 97 | } 98 | }; 99 | 100 | /** 101 | * Inherit the prototype methods 102 | */ 103 | util.inherits(ESConnector, Connector); 104 | 105 | /** 106 | * Generate a client configuration object based on settings. 107 | */ 108 | ESConnector.prototype.getClientConfig = function () { 109 | // https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/client-configuration.html 110 | const self = this; 111 | const config = self.settings.configuration; 112 | 113 | if (config.ssl) { 114 | const fskeys = ['ca', 'cert', 'key']; 115 | R.forEach((key) => { 116 | if (R.has(key, config.ssl)) { 117 | config.ssl[key] = fs.readFileSync(config.ssl[key]); 118 | } 119 | }, fskeys); 120 | } 121 | // Note: https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/client-configuration.html 122 | return config; 123 | }; 124 | 125 | /** 126 | * Connect to Elasticsearch client 127 | * @param {Function} [callback] The callback function 128 | * 129 | * @callback callback 130 | * @param {Error} err The error object 131 | * @param {Db} db The elasticsearch client 132 | */ 133 | ESConnector.prototype.connect = function (callback) { 134 | // TODO: throw error if callback isn't provided? 135 | // what are the corner-cases when the loopback framework does not provide callback 136 | // and we need to be able to live with that? 137 | const self = this; 138 | if (self.db) { 139 | process.nextTick(() => { 140 | callback(null, self.db); 141 | }); 142 | } else { 143 | self.db = new Client(self.getClientConfig()); 144 | self.ping(() => { 145 | // 146 | }); 147 | if (self.settings.mappingProperties) { 148 | self.setupIndex() 149 | .then(() => { 150 | log('ESConnector.prototype.connect', 'setupIndex', 'finished'); 151 | callback(null, self.db); 152 | }) 153 | .catch((err) => { 154 | log('ESConnector.prototype.connect', 'setupIndex', 'failed', err); 155 | callback(null, self.db); 156 | }); 157 | } else { 158 | process.nextTick(() => { 159 | callback(null, self.db); 160 | }); 161 | } 162 | } 163 | }; 164 | 165 | ESConnector.prototype.setupIndex = setupIndex; 166 | 167 | 168 | /** 169 | * Ping to test elastic connection 170 | * @returns {String} with ping result 171 | */ 172 | ESConnector.prototype.ping = function (cb) { 173 | const self = this; 174 | self.db.ping().then(() => { 175 | log('Pinged ES successfully.'); 176 | cb(); 177 | }).catch((error) => { 178 | log('Could not ping ES.'); 179 | cb(error); 180 | }); 181 | }; 182 | 183 | /** 184 | * Return connector type 185 | * @returns {String} type description 186 | */ 187 | ESConnector.prototype.getTypes = function () { 188 | return [this.name]; 189 | }; 190 | 191 | /** 192 | * Get value from property checking type 193 | * @param {object} property 194 | * @param {String} value 195 | * @returns {object} 196 | */ 197 | ESConnector.prototype.getValueFromProperty = function (property, value) { 198 | if (property.type instanceof Array) { 199 | if (!value || (value.length === 0)) { 200 | return []; 201 | } 202 | return value; 203 | } if (property.type === String) { 204 | return value.toString(); 205 | } if (property.type === Number) { 206 | return Number(value); 207 | } if (property.type === Date) { 208 | return new Date(value); 209 | } 210 | return value; 211 | }; 212 | 213 | /** 214 | * Match and transform data structure to modelName 215 | * @param {String} modelName name 216 | * @param {Object} data from DB 217 | * @returns {object} modeled document 218 | */ 219 | ESConnector.prototype.matchDataToModel = function (modelName, data, esId, idName, sort, docsCount) { 220 | /* 221 | log('ESConnector.prototype.matchDataToModel', 'modelName', 222 | modelName, 'data', JSON.stringify(data,null,0)); 223 | */ 224 | const self = this; 225 | if (!data) { 226 | return null; 227 | } 228 | try { 229 | const document = {}; 230 | 231 | // eslint-disable-next-line no-underscore-dangle 232 | const { properties } = this._models[modelName]; 233 | _.assign(document, data); // it can't be this easy, can it? 234 | document[idName] = esId; 235 | 236 | Object.keys(properties).forEach((propertyName) => { 237 | const propertyValue = data[propertyName]; 238 | // log('ESConnector.prototype.matchDataToModel', propertyName, propertyValue); 239 | if (propertyValue !== undefined && propertyValue !== null) { 240 | document[propertyName] = self.getValueFromProperty( 241 | properties[propertyName], 242 | propertyValue 243 | ); 244 | } 245 | }); 246 | document[SEARCHAFTERKEY] = sort; 247 | document[TOTALCOUNTKEY] = docsCount; 248 | log('ESConnector.prototype.matchDataToModel', 'document', JSON.stringify(document, null, 0)); 249 | return document; 250 | } catch (err) { 251 | log('ESConnector.prototype.matchDataToModel', err.message); 252 | return null; 253 | } 254 | }; 255 | 256 | /** 257 | * Convert data source to model 258 | * @param {String} model name 259 | * @param {Object} data object 260 | * @returns {object} modeled document 261 | */ 262 | ESConnector.prototype.dataSourceToModel = function (modelName, data, idName, totalCount) { 263 | log('ESConnector.prototype.dataSourceToModel', 'modelName', modelName, 'data', JSON.stringify(data, null, 0)); 264 | 265 | // return data._source; // TODO: super-simplify? 266 | // eslint-disable-next-line no-underscore-dangle 267 | return this.matchDataToModel( 268 | modelName, 269 | // eslint-disable-next-line no-underscore-dangle 270 | data._source, 271 | // eslint-disable-next-line no-underscore-dangle 272 | data._id, 273 | idName, 274 | data.sort || [], 275 | totalCount 276 | ); 277 | }; 278 | 279 | /** 280 | * Add defaults such as index name and type 281 | * 282 | * @param {String} modelName 283 | * @param {String} functionName The caller function name 284 | * @returns {object} Filter with index and type 285 | */ 286 | ESConnector.prototype.addDefaults = function (modelName, functionName) { 287 | const self = this; 288 | log('ESConnector.prototype.addDefaults', 'modelName:', modelName); 289 | const filter = { 290 | index: self.settings.index 291 | }; 292 | 293 | if (self.settings.version < 7) { 294 | filter.type = self.settings.mappingType; 295 | } 296 | 297 | // When changing data, wait until the change has been indexed... 298 | // ...so it is instantly available for search 299 | if (this.refreshOn.indexOf(functionName) !== -1) { 300 | filter.refresh = true; 301 | } 302 | 303 | log('ESConnector.prototype.addDefaults', 'filter:', filter); 304 | return filter; 305 | }; 306 | 307 | /** 308 | * Make filter from criteria, data index and type 309 | * Ex: 310 | * {'body': {'query': {'match': {'title': 'Futuro'}}}} 311 | * {'q' : 'Futuro'} 312 | * @param {String} modelName filter 313 | * @param {Object} criteria filter 314 | * @param {number} size of rows to return, if null then skip 315 | * @param {number} offset to return, if null then skip 316 | * @returns {object} filter 317 | */ 318 | ESConnector.prototype.buildFilter = buildFilter; 319 | 320 | /** 321 | * 1. Words of wisdom from @doublemarked: 322 | * > When writing a query without an order specified, 323 | the author should not assume any reliable order. 324 | * > So if we’re not assuming any order, 325 | there is not a compelling reason to potentially slow down 326 | * > the query by enforcing a default order. 327 | * 2. Yet, most connector implementations do enforce a default order ... what to do? 328 | * 329 | * @param model 330 | * @param idName 331 | * @param order 332 | * @returns {Array} 333 | */ 334 | ESConnector.prototype.buildOrder = buildOrder; 335 | 336 | ESConnector.prototype.buildWhere = buildWhere; 337 | 338 | ESConnector.prototype.buildNestedQueries = buildNestedQueries; 339 | 340 | ESConnector.prototype.buildDeepNestedQueries = buildDeepNestedQueries; 341 | 342 | /** 343 | * Get document Id validating data 344 | * @param {String} id 345 | * @returns {Number} Id 346 | * @constructor 347 | */ 348 | ESConnector.prototype.getDocumentId = function (id) { 349 | try { 350 | if (typeof id !== 'string') { 351 | return id.toString(); 352 | } 353 | return id; 354 | } catch (e) { 355 | return id; 356 | } 357 | }; 358 | 359 | /** 360 | * Implement CRUD Level I - Key methods to be implemented by a connector to support full CRUD 361 | * > Create a new model instance 362 | * > CRUDConnector.prototype.create = function (model, data, callback) {...}; 363 | * > Query model instances by filter 364 | * > CRUDConnector.prototype.all = function (model, filter, callback) {...}; 365 | * > Delete model instances by query 366 | * > CRUDConnector.prototype.destroyAll = function (model, where, callback) {...}; 367 | * > Update model instances by query 368 | * > CRUDConnector.prototype.updateAll = function (model, where, data, callback) {...}; 369 | * > Count model instances by query 370 | * > CRUDConnector.prototype.count = function (model, callback, where) {...}; 371 | * > getDefaultIdType 372 | * > very useful for setting a default type for IDs like 'string' rather than 'number' 373 | }; 374 | */ 375 | 376 | ESConnector.prototype.getDefaultIdType = function () { 377 | return String; 378 | }; 379 | /** 380 | * Create a new model instance 381 | * @param {String} model name 382 | * @param {object} data info 383 | * @param {Function} done - invoke the callback with the created model's id as an argument 384 | */ 385 | ESConnector.prototype.create = create; 386 | 387 | /** 388 | * Query model instances by filter 389 | * @param {String} model The model name 390 | * @param {Object} filter The filter 391 | * @param {Function} done callback function 392 | * 393 | * NOTE: UNLIKE create() where the ID is returned not as a part of the created content 394 | * but rather individually as an argument to the callback ... in the all() method 395 | * it makes sense to return the id with the content! So for a datasource like elasticsearch, 396 | * make sure to map '_id' into the content, just in case its an auto-generated one. 397 | */ 398 | ESConnector.prototype.all = all; 399 | 400 | /** 401 | * Delete model instances by query 402 | * @param {String} modelName name 403 | * @param {String} whereClause criteria 404 | * @param {Function} cb callback 405 | */ 406 | ESConnector.prototype.destroyAll = destroyAll; 407 | 408 | /** 409 | * Update model instances by query 410 | * 411 | * NOTES: 412 | * > Without an update by query plugin, this isn't supported by ES out-of-the-box 413 | * > To run updateAll these parameters should be enabled in elasticsearch config 414 | * -> script.inline: true 415 | * -> script.indexed: true 416 | * -> script.engine.groovy.inline.search: on 417 | * -> script.engine.groovy.inline.update: on 418 | * 419 | * @param {String} model The model name 420 | * @param {Object} where The search criteria 421 | * @param {Object} data The property/value pairs to be updated 422 | * @callback {Function} cb - should be invoked with a second callback argument 423 | * that provides the count of affected rows in the callback 424 | * such as cb(err, {count: affectedRows}). 425 | * Notice the second argument is an object with the count property 426 | * representing the number of rows that were updated. 427 | */ 428 | ESConnector.prototype.updateAll = updateAll; 429 | 430 | ESConnector.prototype.update = ESConnector.prototype.updateAll; 431 | 432 | /** 433 | * Count model instances by query 434 | * @param {String} model name 435 | * @param {String} where criteria 436 | * @param {Function} done callback 437 | */ 438 | ESConnector.prototype.count = count; 439 | 440 | /** 441 | * Implement CRUD Level II - A connector can choose to implement the following methods, 442 | * otherwise, they will be mapped to those from CRUD Level I. 443 | * > Find a model instance by id 444 | * > CRUDConnector.prototype.find = function (model, id, callback) {...}; 445 | * > Delete a model instance by id 446 | * > CRUDConnector.prototype.destroy = function (model, id, callback) {...}; 447 | * > Update a model instance by id 448 | * > CRUDConnector.prototype.updateAttributes = function (model, id, data, callback) {...}; 449 | * > Check existence of a model instance by id 450 | * > CRUDConnector.prototype.exists = function (model, id, callback) {...}; 451 | */ 452 | 453 | /** 454 | * Find a model instance by id 455 | * @param {String} model name 456 | * @param {String} id row identifier 457 | * @param {Function} done callback 458 | */ 459 | ESConnector.prototype.find = find; 460 | 461 | /** 462 | * Delete a model instance by id 463 | * @param {String} model name 464 | * @param {String} id row identifier 465 | * @param {Function} done callback 466 | */ 467 | ESConnector.prototype.destroy = destroy; 468 | 469 | /** 470 | * Update a model instance by id 471 | * 472 | */ 473 | 474 | ESConnector.prototype.updateAttributes = updateAttributes; 475 | 476 | /** 477 | * Check existence of a model instance by id 478 | * @param {String} model name 479 | * @param {String} id row identifier 480 | * @param {function} done callback 481 | */ 482 | ESConnector.prototype.exists = exists; 483 | 484 | /** 485 | * Implement CRUD Level III - A connector can also optimize certain methods 486 | * if the underlying database provides native/atomic 487 | * operations to avoid multiple calls. 488 | * > Save a model instance 489 | * > CRUDConnector.prototype.save = function (model, data, callback) {...}; 490 | * > Find or create a model instance 491 | * > CRUDConnector.prototype.findOrCreate = function (model, data, callback) {...}; 492 | * > Update or insert a model instance 493 | * > CRUDConnector.prototype.updateOrCreate = function (model, data, callback) {...}; 494 | */ 495 | 496 | /** 497 | * Update document data 498 | * @param {String} model name 499 | * @param {Object} data document 500 | * @param {Function} done callback 501 | */ 502 | ESConnector.prototype.save = save; 503 | 504 | /** 505 | * Find or create a model instance 506 | */ 507 | // ESConnector.prototype.findOrCreate = function (model, data, callback) {...}; 508 | 509 | /** 510 | * Update or insert a model instance 511 | * @param modelName 512 | * @param data 513 | * @param callback - should pass the following arguments to the callback: 514 | * err object (null on success) 515 | * data object containing the property values as found in the database 516 | * info object providing more details about the result of the operation. 517 | * At the moment, it should have a single property isNewInstance 518 | * with the value true if a new model was created 519 | * and the value false is an existing model was found & updated. 520 | */ 521 | ESConnector.prototype.updateOrCreate = updateOrCreate; 522 | 523 | /** 524 | * replace or insert a model instance 525 | * @param modelName 526 | * @param data 527 | * @param callback - should pass the following arguments to the callback: 528 | * err object (null on success) 529 | * data object containing the property values as found in the database 530 | * info object providing more details about the result of the operation. 531 | * At the moment, it should have a single property isNewInstance 532 | * with the value true if a new model was created 533 | * and the value false is an existing model was found & updated. 534 | */ 535 | ESConnector.prototype.replaceOrCreate = replaceOrCreate; 536 | 537 | ESConnector.prototype.replaceById = replaceById; 538 | 539 | /** 540 | * Migration 541 | * automigrate - Create/recreate DB objects (such as table/column/constraint/trigger/index) 542 | * to match the model definitions 543 | * autoupdate - Alter DB objects to match the model definitions 544 | */ 545 | 546 | /** 547 | * Perform automigrate for the given models. Create/recreate DB objects 548 | * (such as table/column/constraint/trigger/index) to match the model definitions 549 | * --> Drop the corresponding indices: both mappings and data are done away with 550 | * --> create/recreate mappings and indices 551 | * 552 | * @param {String[]} [models] A model name or an array of model names. 553 | * If not present, apply to all models 554 | * @param {Function} [cb] The callback function 555 | */ 556 | ESConnector.prototype.automigrate = automigrate; 557 | 558 | module.exports.name = ESConnector.name; 559 | module.exports.ESConnector = ESConnector; 560 | -------------------------------------------------------------------------------- /test/es-v6/01.filters.test.js: -------------------------------------------------------------------------------- 1 | /*global getSettings getDataSource expect*/ 2 | /*eslint no-console: ["error", { allow: ["trace","log"] }] */ 3 | describe('Connector', function() { 4 | var testConnector; 5 | 6 | before(function() { 7 | require('./init.js'); 8 | var settings = getSettings(); 9 | settings.log = 'error'; 10 | var datasource = getDataSource(settings); 11 | testConnector = datasource.connector; 12 | 13 | datasource.define('MockLoopbackModel', { 14 | // here we want to let elasticsearch auto-populate a field that will be mapped back to loopback as the `id` 15 | id: { type: String, generated: true, id: true } 16 | }); 17 | }); 18 | 19 | it('should configure defaults when building filters', function(done) { 20 | var modelName = 'MockLoopbackModel'; 21 | var defaults = testConnector.addDefaults(modelName); 22 | 23 | expect(defaults.index).to.be.a('string').to.have.length.above(1).to.match(/^[a-z0-9.-_]+$/i); 24 | expect(defaults.type).to.be.a('string').to.have.length.above(1).to.match(/^[a-z0-9.-_]+$/i); 25 | 26 | done(); 27 | }); 28 | 29 | it('should build a query for the WHERE filter', function(done) { 30 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 31 | criteria = { 32 | 'where': { 33 | 'title': 'Futuro' 34 | } 35 | }; 36 | size = 100; 37 | offset = 10; 38 | modelName = 'MockLoopbackModel'; 39 | modelIdName = 'id'; 40 | settings = getSettings(); 41 | mappingType = settings.mappingType; 42 | 43 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 44 | expect(filterCriteria).not.to.be.null; 45 | expect(filterCriteria).to.have.property('index') 46 | .that.is.a('string'); 47 | expect(filterCriteria).to.have.property('type') 48 | .that.is.a('string') 49 | .that.equals(mappingType); 50 | expect(filterCriteria).to.have.property('body') 51 | .that.is.an('object') 52 | .that.deep.equals({ // a. this is really 2 tests in one 53 | sort: [ 54 | // b. `_id` is an auto-generated field that ElasticSearch populates for us 55 | // when we want to let the backend/system/ES take care of id population 56 | // so if we want to sort by id, without specifying/controlling our own id field, 57 | // then ofcourse the sort must happen on `_id`, this part of the test, validates that! 58 | '_id' 59 | ], 60 | query: { // c. here we are testing the bigger picture `should build a query for the WHERE filter` 61 | bool: { 62 | must: [{ 63 | match: { 64 | title: 'Futuro' 65 | } 66 | }, { 67 | match: { 68 | docType: modelName 69 | } 70 | }] 71 | } 72 | 73 | } 74 | }); 75 | expect(filterCriteria).to.have.property('size') 76 | .that.is.a('number'); 77 | expect(filterCriteria).to.have.property('from') 78 | .that.is.a('number'); 79 | 80 | done(); 81 | }); 82 | 83 | it('should use a NATIVE filter query as-is', function(done) { 84 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 85 | criteria = { 86 | 'native': { 87 | query: { 88 | bool: { 89 | must: [{ 90 | match: { 91 | title: 'Futuro' 92 | } 93 | }] 94 | } 95 | } 96 | } 97 | }; 98 | size = 100; 99 | offset = 10; 100 | modelName = 'MockLoopbackModel'; 101 | modelIdName = 'id'; 102 | settings = getSettings(); 103 | mappingType = settings.mappingType; 104 | 105 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 106 | expect(filterCriteria).not.to.be.null; 107 | expect(filterCriteria).to.have.property('index') 108 | .that.is.a('string'); 109 | expect(filterCriteria).to.have.property('type') 110 | .that.is.a('string') 111 | .that.equals(mappingType); 112 | expect(filterCriteria).to.have.property('body') 113 | .that.is.an('object') 114 | .that.deep.equals({ 115 | query: { 116 | bool: { 117 | must: [{ 118 | match: { 119 | title: 'Futuro' 120 | } 121 | }] 122 | } 123 | } 124 | }); 125 | expect(filterCriteria).to.have.property('size') 126 | .that.is.a('number'); 127 | expect(filterCriteria).to.have.property('from') 128 | .that.is.a('number'); 129 | 130 | done(); 131 | }); 132 | 133 | it('should build a simple "and" query for the WHERE filter', function(done) { 134 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 135 | criteria = { 136 | where: { 137 | and: [ 138 | { name: 'John Lennon' }, 139 | { role: 'lead' } 140 | ] 141 | } 142 | }; 143 | size = 100; 144 | offset = 10; 145 | modelName = 'MockLoopbackModel'; 146 | modelIdName = 'id'; 147 | settings = getSettings(); 148 | mappingType = settings.mappingType; 149 | 150 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 151 | expect(filterCriteria).not.to.be.null; 152 | expect(filterCriteria).to.have.property('index').that.is.a('string'); 153 | expect(filterCriteria).to.have.property('type').that.is.a('string').that.equals(mappingType); 154 | expect(filterCriteria).to.have.property('body') 155 | .that.is.an('object') 156 | .that.deep.equals({ // a. this is really 2 tests in one 157 | sort: [ 158 | // b. `_id` is an auto-generated field that ElasticSearch populates for us 159 | // when we want to let the backend/system/ES take care of id population 160 | // so if we want to sort by id, without specifying/controlling our own id field, 161 | // then ofcourse the sort must happen on `_id`, this part of the test, validates that! 162 | '_id' 163 | ], 164 | query: { 165 | bool: { 166 | must: [{ 167 | match: { 168 | name: 'John Lennon' 169 | } 170 | }, 171 | { 172 | match: { 173 | role: 'lead' 174 | } 175 | }, 176 | { 177 | match: { 178 | docType: modelName 179 | } 180 | } 181 | ] 182 | } 183 | 184 | } 185 | }); 186 | expect(filterCriteria).to.have.property('size') 187 | .that.is.a('number'); 188 | expect(filterCriteria).to.have.property('from') 189 | .that.is.a('number'); 190 | 191 | done(); 192 | }); 193 | 194 | it('should build a complex "and" query with "inq" for the WHERE filter', function(done) { 195 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 196 | criteria = { 197 | where: { 198 | and: [ 199 | { id: { inq: [0, 1, 2] } }, 200 | { vip: true } 201 | ] 202 | } 203 | }; 204 | size = 100; 205 | offset = 10; 206 | modelName = 'MockLoopbackModel'; 207 | modelIdName = 'id'; 208 | settings = getSettings(); 209 | mappingType = settings.mappingType; 210 | 211 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 212 | expect(filterCriteria).not.to.be.null; 213 | expect(filterCriteria).to.have.property('index').that.is.a('string'); 214 | expect(filterCriteria).to.have.property('type').that.is.a('string').that.equals(mappingType); 215 | expect(filterCriteria).to.have.property('body') 216 | .that.is.an('object') 217 | .that.deep.equals({ // a. this is really 2 tests in one 218 | sort: [ 219 | // b. `_id` is an auto-generated field that ElasticSearch populates for us 220 | // when we want to let the backend/system/ES take care of id population 221 | // so if we want to sort by id, without specifying/controlling our own id field, 222 | // then ofcourse the sort must happen on `_id`, this part of the test, validates that! 223 | '_id' 224 | ], 225 | query: { 226 | bool: { 227 | must: [{ 228 | terms: { 229 | _id: [ 230 | 0, 231 | 1, 232 | 2 233 | ] 234 | } 235 | }, 236 | { 237 | match: { 238 | vip: true 239 | } 240 | }, 241 | { 242 | match: { 243 | docType: modelName 244 | } 245 | } 246 | ] 247 | } 248 | } 249 | }); 250 | expect(filterCriteria).to.have.property('size') 251 | .that.is.a('number'); 252 | expect(filterCriteria).to.have.property('from') 253 | .that.is.a('number'); 254 | 255 | done(); 256 | }); 257 | 258 | it('should build a nested "or" and "and" query for the WHERE filter', function(done) { 259 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 260 | criteria = { 261 | where: { 262 | or: [ 263 | { and: [{ id: { inq: [3, 4, 5] } }, { vip: true }] }, 264 | { role: 'lead' } 265 | ] 266 | } 267 | }; 268 | size = 100; 269 | offset = 10; 270 | modelName = 'MockLoopbackModel'; 271 | modelIdName = 'id'; 272 | settings = getSettings(); 273 | mappingType = settings.mappingType; 274 | 275 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 276 | expect(filterCriteria).not.to.be.null; 277 | expect(filterCriteria).to.have.property('index').that.is.a('string'); 278 | expect(filterCriteria).to.have.property('type').that.is.a('string').that.equals(mappingType); 279 | expect(filterCriteria).to.have.property('body') 280 | .that.is.an('object') 281 | .that.deep.equals({ // a. this is really 2 tests in one 282 | sort: [ 283 | // b. `_id` is an auto-generated field that ElasticSearch populates for us 284 | // when we want to let the backend/system/ES take care of id population 285 | // so if we want to sort by id, without specifying/controlling our own id field, 286 | // then ofcourse the sort must happen on `_id`, this part of the test, validates that! 287 | '_id' 288 | ], 289 | query: { 290 | bool: { 291 | must: [{ 292 | match: { 293 | docType: modelName 294 | } 295 | }, { 296 | bool: { 297 | should: [{ 298 | bool: { 299 | must: [{ 300 | terms: { 301 | _id: [ 302 | 3, 303 | 4, 304 | 5 305 | ] 306 | } 307 | }, 308 | { 309 | match: { 310 | vip: true 311 | } 312 | } 313 | ] 314 | } 315 | }, 316 | { 317 | match: { 318 | role: 'lead' 319 | } 320 | } 321 | ] 322 | } 323 | }] 324 | } 325 | } 326 | }); 327 | expect(filterCriteria).to.have.property('size') 328 | .that.is.a('number'); 329 | expect(filterCriteria).to.have.property('from') 330 | .that.is.a('number'); 331 | 332 | done(); 333 | }); 334 | 335 | it('should build a nested "and" and "or" query for the WHERE filter', function(done) { 336 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 337 | criteria = { 338 | where: { 339 | and: [ 340 | { or: [{ id: { inq: [3, 4, 5] } }, { vip: true }] }, 341 | { role: 'lead' } 342 | ] 343 | } 344 | }; 345 | size = 100; 346 | offset = 10; 347 | modelName = 'MockLoopbackModel'; 348 | modelIdName = 'id'; 349 | settings = getSettings(); 350 | mappingType = settings.mappingType; 351 | 352 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 353 | expect(filterCriteria).not.to.be.null; 354 | expect(filterCriteria).to.have.property('index').that.is.a('string'); 355 | expect(filterCriteria).to.have.property('type').that.is.a('string').that.equals(mappingType); 356 | expect(filterCriteria).to.have.property('body') 357 | .that.is.an('object') 358 | .that.deep.equals({ // a. this is really 2 tests in one 359 | sort: [ 360 | // b. `_id` is an auto-generated field that ElasticSearch populates for us 361 | // when we want to let the backend/system/ES take care of id population 362 | // so if we want to sort by id, without specifying/controlling our own id field, 363 | // then ofcourse the sort must happen on `_id`, this part of the test, validates that! 364 | '_id' 365 | ], 366 | query: { 367 | bool: { 368 | must: [{ 369 | bool: { 370 | should: [{ 371 | terms: { 372 | _id: [ 373 | 3, 374 | 4, 375 | 5 376 | ] 377 | } 378 | }, 379 | { 380 | match: { 381 | vip: true 382 | } 383 | } 384 | ] 385 | } 386 | }, 387 | { 388 | match: { 389 | role: 'lead' 390 | } 391 | }, 392 | { 393 | match: { 394 | docType: modelName 395 | } 396 | } 397 | ] 398 | } 399 | } 400 | }); 401 | expect(filterCriteria).to.have.property('size') 402 | .that.is.a('number'); 403 | expect(filterCriteria).to.have.property('from') 404 | .that.is.a('number'); 405 | 406 | done(); 407 | }); 408 | 409 | it('should build a "nin" query for the WHERE filter', function(done) { 410 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 411 | criteria = { 412 | 'where': { 413 | 'id': { 414 | 'nin': [0, 1, 2] 415 | } 416 | } 417 | }; 418 | size = 100; 419 | offset = 10; 420 | modelName = 'MockLoopbackModel'; 421 | modelIdName = 'id'; 422 | settings = getSettings(); 423 | mappingType = settings.mappingType; 424 | 425 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 426 | expect(filterCriteria).not.to.be.null; 427 | expect(filterCriteria).to.have.property('index').that.is.a('string'); 428 | expect(filterCriteria).to.have.property('type').that.is.a('string').that.equals(mappingType); 429 | expect(filterCriteria).to.have.property('body') 430 | .that.is.an('object') 431 | .that.deep.equals({ // a. this is really 2 tests in one 432 | sort: [ 433 | // b. `_id` is an auto-generated field that ElasticSearch populates for us 434 | // when we want to let the backend/system/ES take care of id population 435 | // so if we want to sort by id, without specifying/controlling our own id field, 436 | // then ofcourse the sort must happen on `_id`, this part of the test, validates that! 437 | '_id' 438 | ], 439 | query: { 440 | bool: { 441 | must: [{ 442 | match: { 443 | docType: modelName 444 | } 445 | }, { 446 | bool: { 447 | must_not: [{ 448 | terms: { 449 | _id: [ 450 | 0, 451 | 1, 452 | 2 453 | ] 454 | } 455 | }] 456 | } 457 | }] 458 | } 459 | } 460 | }); 461 | expect(filterCriteria).to.have.property('size') 462 | .that.is.a('number'); 463 | expect(filterCriteria).to.have.property('from') 464 | .that.is.a('number'); 465 | 466 | done(); 467 | }); 468 | 469 | it('should build a "and" and "nin" query for the WHERE filter', function(done) { 470 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 471 | criteria = { 472 | where: { 473 | and: [ 474 | { id: { nin: [0, 1, 2] } }, 475 | { vip: true } 476 | ] 477 | } 478 | }; 479 | size = 100; 480 | offset = 10; 481 | modelName = 'MockLoopbackModel'; 482 | modelIdName = 'id'; 483 | settings = getSettings(); 484 | mappingType = settings.mappingType; 485 | 486 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 487 | expect(filterCriteria).not.to.be.null; 488 | expect(filterCriteria).to.have.property('index').that.is.a('string'); 489 | expect(filterCriteria).to.have.property('type').that.is.a('string').that.equals(mappingType); 490 | expect(filterCriteria).to.have.property('body') 491 | .that.is.an('object') 492 | .that.deep.equals({ // a. this is really 2 tests in one 493 | sort: [ 494 | // b. `_id` is an auto-generated field that ElasticSearch populates for us 495 | // when we want to let the backend/system/ES take care of id population 496 | // so if we want to sort by id, without specifying/controlling our own id field, 497 | // then ofcourse the sort must happen on `_id`, this part of the test, validates that! 498 | '_id' 499 | ], 500 | query: { 501 | bool: { 502 | must: [{ 503 | bool: { 504 | must_not: [{ 505 | terms: { 506 | _id: [ 507 | 0, 508 | 1, 509 | 2 510 | ] 511 | } 512 | }] 513 | } 514 | }, 515 | { 516 | match: { 517 | vip: true 518 | } 519 | }, 520 | { 521 | match: { 522 | docType: modelName 523 | } 524 | } 525 | ] 526 | } 527 | } 528 | }); 529 | expect(filterCriteria).to.have.property('size') 530 | .that.is.a('number'); 531 | expect(filterCriteria).to.have.property('from') 532 | .that.is.a('number'); 533 | 534 | done(); 535 | }); 536 | 537 | it('should build a "between" query for the WHERE filter', function(done) { 538 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 539 | criteria = { 540 | where: { 541 | order: { 542 | between: [3, 6] 543 | } 544 | } 545 | }; 546 | size = 100; 547 | offset = 10; 548 | modelName = 'MockLoopbackModel'; 549 | modelIdName = 'id'; 550 | settings = getSettings(); 551 | mappingType = settings.mappingType; 552 | 553 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 554 | expect(filterCriteria).not.to.be.null; 555 | expect(filterCriteria).to.have.property('index').that.is.a('string'); 556 | expect(filterCriteria).to.have.property('type').that.is.a('string').that.equals(mappingType); 557 | expect(filterCriteria).to.have.property('body') 558 | .that.is.an('object') 559 | .that.deep.equals({ // a. this is really 2 tests in one 560 | sort: [ 561 | // b. `_id` is an auto-generated field that ElasticSearch populates for us 562 | // when we want to let the backend/system/ES take care of id population 563 | // so if we want to sort by id, without specifying/controlling our own id field, 564 | // then ofcourse the sort must happen on `_id`, this part of the test, validates that! 565 | '_id' 566 | ], 567 | query: { 568 | bool: { 569 | must: [{ 570 | range: { 571 | order: { 572 | gte: 3, 573 | lte: 6 574 | } 575 | } 576 | }, { 577 | match: { 578 | docType: modelName 579 | } 580 | }] 581 | } 582 | } 583 | }); 584 | expect(filterCriteria).to.have.property('size') 585 | .that.is.a('number'); 586 | expect(filterCriteria).to.have.property('from') 587 | .that.is.a('number'); 588 | 589 | done(); 590 | }); 591 | 592 | it('should build a "and" and "between" query for the WHERE filter', function(done) { 593 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 594 | criteria = { 595 | where: { 596 | and: [ 597 | { order: { between: [2, 6] } }, 598 | { vip: true } 599 | ] 600 | } 601 | }; 602 | size = 100; 603 | offset = 10; 604 | modelName = 'MockLoopbackModel'; 605 | modelIdName = 'id'; 606 | settings = getSettings(); 607 | mappingType = settings.mappingType; 608 | 609 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 610 | expect(filterCriteria).not.to.be.null; 611 | expect(filterCriteria).to.have.property('index').that.is.a('string'); 612 | expect(filterCriteria).to.have.property('type').that.is.a('string').that.equals(mappingType); 613 | expect(filterCriteria).to.have.property('body') 614 | .that.is.an('object') 615 | .that.deep.equals({ // a. this is really 2 tests in one 616 | sort: [ 617 | // b. `_id` is an auto-generated field that ElasticSearch populates for us 618 | // when we want to let the backend/system/ES take care of id population 619 | // so if we want to sort by id, without specifying/controlling our own id field, 620 | // then ofcourse the sort must happen on `_id`, this part of the test, validates that! 621 | '_id' 622 | ], 623 | query: { 624 | bool: { 625 | must: [{ 626 | range: { 627 | order: { 628 | gte: 2, 629 | lte: 6 630 | } 631 | } 632 | }, 633 | { 634 | match: { 635 | vip: true 636 | } 637 | }, { 638 | match: { 639 | docType: modelName 640 | } 641 | } 642 | ] 643 | } 644 | } 645 | }); 646 | expect(filterCriteria).to.have.property('size') 647 | .that.is.a('number'); 648 | expect(filterCriteria).to.have.property('from') 649 | .that.is.a('number'); 650 | 651 | done(); 652 | }); 653 | 654 | it('should build multiple normal where clause query without "and" for the WHERE filter', function(done) { 655 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 656 | criteria = { 657 | where: { 658 | role: 'lead', 659 | vip: true 660 | } 661 | }; 662 | size = 100; 663 | offset = 10; 664 | modelName = 'MockLoopbackModel'; 665 | modelIdName = 'id'; 666 | settings = getSettings(); 667 | mappingType = settings.mappingType; 668 | 669 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 670 | expect(filterCriteria).not.to.be.null; 671 | expect(filterCriteria).to.have.property('index').that.is.a('string'); 672 | expect(filterCriteria).to.have.property('type').that.is.a('string').that.equals(mappingType); 673 | expect(filterCriteria).to.have.property('body') 674 | .that.is.an('object') 675 | .that.deep.equals({ // a. this is really 2 tests in one 676 | sort: [ 677 | // b. `_id` is an auto-generated field that ElasticSearch populates for us 678 | // when we want to let the backend/system/ES take care of id population 679 | // so if we want to sort by id, without specifying/controlling our own id field, 680 | // then ofcourse the sort must happen on `_id`, this part of the test, validates that! 681 | '_id' 682 | ], 683 | query: { 684 | bool: { 685 | must: [{ 686 | match: { 687 | role: 'lead' 688 | } 689 | }, 690 | { 691 | match: { 692 | vip: true 693 | } 694 | }, { 695 | match: { 696 | docType: modelName 697 | } 698 | } 699 | ] 700 | } 701 | } 702 | }); 703 | expect(filterCriteria).to.have.property('size') 704 | .that.is.a('number'); 705 | expect(filterCriteria).to.have.property('from') 706 | .that.is.a('number'); 707 | 708 | done(); 709 | }); 710 | 711 | it('should build two "inq" and one "between" without "and" for the WHERE filter', function(done) { 712 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 713 | criteria = { 714 | where: { 715 | role: { inq: ['lead'] }, 716 | order: { between: [1, 6] }, 717 | id: { inq: [2, 3, 4, 5] } 718 | } 719 | }; 720 | size = 100; 721 | offset = 10; 722 | modelName = 'MockLoopbackModel'; 723 | modelIdName = 'id'; 724 | settings = getSettings(); 725 | mappingType = settings.mappingType; 726 | 727 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 728 | expect(filterCriteria).not.to.be.null; 729 | expect(filterCriteria).to.have.property('index').that.is.a('string'); 730 | expect(filterCriteria).to.have.property('type').that.is.a('string').that.equals(mappingType); 731 | expect(filterCriteria).to.have.property('body') 732 | .that.is.an('object') 733 | .that.deep.equals({ // a. this is really 2 tests in one 734 | sort: [ 735 | // b. `_id` is an auto-generated field that ElasticSearch populates for us 736 | // when we want to let the backend/system/ES take care of id population 737 | // so if we want to sort by id, without specifying/controlling our own id field, 738 | // then ofcourse the sort must happen on `_id`, this part of the test, validates that! 739 | '_id' 740 | ], 741 | query: { 742 | bool: { 743 | must: [{ 744 | terms: { 745 | role: [ 746 | 'lead' 747 | ] 748 | } 749 | }, 750 | { 751 | range: { 752 | order: { 753 | gte: 1, 754 | lte: 6 755 | } 756 | } 757 | }, 758 | { 759 | terms: { 760 | _id: [ 761 | 2, 762 | 3, 763 | 4, 764 | 5 765 | ] 766 | } 767 | }, { 768 | match: { 769 | docType: modelName 770 | } 771 | } 772 | ] 773 | } 774 | } 775 | }); 776 | expect(filterCriteria).to.have.property('size') 777 | .that.is.a('number'); 778 | expect(filterCriteria).to.have.property('from') 779 | .that.is.a('number'); 780 | 781 | done(); 782 | }); 783 | }); -------------------------------------------------------------------------------- /test/es-v7/01.filters.test.js: -------------------------------------------------------------------------------- 1 | /*global getSettings getDataSource expect*/ 2 | /*eslint no-console: ["error", { allow: ["trace","log"] }] */ 3 | describe('Connector', function() { 4 | var testConnector; 5 | 6 | before(function() { 7 | require('./init.js'); 8 | var settings = getSettings(); 9 | settings.log = 'error'; 10 | var datasource = getDataSource(settings); 11 | testConnector = datasource.connector; 12 | 13 | datasource.define('MockLoopbackModel', { 14 | // here we want to let elasticsearch auto-populate a field that will be mapped back to loopback as the `id` 15 | id: { type: String, generated: true, id: true } 16 | }); 17 | }); 18 | 19 | it('should configure defaults when building filters', function(done) { 20 | var modelName = 'MockLoopbackModel'; 21 | var defaults = testConnector.addDefaults(modelName); 22 | 23 | expect(defaults.index).to.be.a('string').to.have.length.above(1).to.match(/^[a-z0-9.-_]+$/i); 24 | expect(defaults.type).to.be.a('string').to.have.length.above(1).to.match(/^[a-z0-9.-_]+$/i); 25 | 26 | done(); 27 | }); 28 | 29 | it('should build a query for the WHERE filter', function(done) { 30 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 31 | criteria = { 32 | 'where': { 33 | 'title': 'Futuro' 34 | } 35 | }; 36 | size = 100; 37 | offset = 10; 38 | modelName = 'MockLoopbackModel'; 39 | modelIdName = 'id'; 40 | settings = getSettings(); 41 | mappingType = settings.mappingType; 42 | 43 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 44 | expect(filterCriteria).not.to.be.null; 45 | expect(filterCriteria).to.have.property('index') 46 | .that.is.a('string'); 47 | expect(filterCriteria).to.have.property('type') 48 | .that.is.a('string') 49 | .that.equals(mappingType); 50 | expect(filterCriteria).to.have.property('body') 51 | .that.is.an('object') 52 | .that.deep.equals({ // a. this is really 2 tests in one 53 | sort: [ 54 | // b. `_id` is an auto-generated field that ElasticSearch populates for us 55 | // when we want to let the backend/system/ES take care of id population 56 | // so if we want to sort by id, without specifying/controlling our own id field, 57 | // then ofcourse the sort must happen on `_id`, this part of the test, validates that! 58 | '_id' 59 | ], 60 | query: { // c. here we are testing the bigger picture `should build a query for the WHERE filter` 61 | bool: { 62 | must: [{ 63 | match: { 64 | title: 'Futuro' 65 | } 66 | }, { 67 | match: { 68 | docType: modelName 69 | } 70 | }] 71 | } 72 | 73 | } 74 | }); 75 | expect(filterCriteria).to.have.property('size') 76 | .that.is.a('number'); 77 | expect(filterCriteria).to.have.property('from') 78 | .that.is.a('number'); 79 | 80 | done(); 81 | }); 82 | 83 | it('should use a NATIVE filter query as-is', function(done) { 84 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 85 | criteria = { 86 | 'native': { 87 | query: { 88 | bool: { 89 | must: [{ 90 | match: { 91 | title: 'Futuro' 92 | } 93 | }] 94 | } 95 | } 96 | } 97 | }; 98 | size = 100; 99 | offset = 10; 100 | modelName = 'MockLoopbackModel'; 101 | modelIdName = 'id'; 102 | settings = getSettings(); 103 | mappingType = settings.mappingType; 104 | 105 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 106 | expect(filterCriteria).not.to.be.null; 107 | expect(filterCriteria).to.have.property('index') 108 | .that.is.a('string'); 109 | expect(filterCriteria).to.have.property('type') 110 | .that.is.a('string') 111 | .that.equals(mappingType); 112 | expect(filterCriteria).to.have.property('body') 113 | .that.is.an('object') 114 | .that.deep.equals({ 115 | query: { 116 | bool: { 117 | must: [{ 118 | match: { 119 | title: 'Futuro' 120 | } 121 | }] 122 | } 123 | } 124 | }); 125 | expect(filterCriteria).to.have.property('size') 126 | .that.is.a('number'); 127 | expect(filterCriteria).to.have.property('from') 128 | .that.is.a('number'); 129 | 130 | done(); 131 | }); 132 | 133 | it('should build a simple "and" query for the WHERE filter', function(done) { 134 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 135 | criteria = { 136 | where: { 137 | and: [ 138 | { name: 'John Lennon' }, 139 | { role: 'lead' } 140 | ] 141 | } 142 | }; 143 | size = 100; 144 | offset = 10; 145 | modelName = 'MockLoopbackModel'; 146 | modelIdName = 'id'; 147 | settings = getSettings(); 148 | mappingType = settings.mappingType; 149 | 150 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 151 | expect(filterCriteria).not.to.be.null; 152 | expect(filterCriteria).to.have.property('index').that.is.a('string'); 153 | expect(filterCriteria).to.have.property('type').that.is.a('string').that.equals(mappingType); 154 | expect(filterCriteria).to.have.property('body') 155 | .that.is.an('object') 156 | .that.deep.equals({ // a. this is really 2 tests in one 157 | sort: [ 158 | // b. `_id` is an auto-generated field that ElasticSearch populates for us 159 | // when we want to let the backend/system/ES take care of id population 160 | // so if we want to sort by id, without specifying/controlling our own id field, 161 | // then ofcourse the sort must happen on `_id`, this part of the test, validates that! 162 | '_id' 163 | ], 164 | query: { 165 | bool: { 166 | must: [{ 167 | match: { 168 | name: 'John Lennon' 169 | } 170 | }, 171 | { 172 | match: { 173 | role: 'lead' 174 | } 175 | }, 176 | { 177 | match: { 178 | docType: modelName 179 | } 180 | } 181 | ] 182 | } 183 | 184 | } 185 | }); 186 | expect(filterCriteria).to.have.property('size') 187 | .that.is.a('number'); 188 | expect(filterCriteria).to.have.property('from') 189 | .that.is.a('number'); 190 | 191 | done(); 192 | }); 193 | 194 | it('should build a complex "and" query with "inq" for the WHERE filter', function(done) { 195 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 196 | criteria = { 197 | where: { 198 | and: [ 199 | { id: { inq: [0, 1, 2] } }, 200 | { vip: true } 201 | ] 202 | } 203 | }; 204 | size = 100; 205 | offset = 10; 206 | modelName = 'MockLoopbackModel'; 207 | modelIdName = 'id'; 208 | settings = getSettings(); 209 | mappingType = settings.mappingType; 210 | 211 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 212 | expect(filterCriteria).not.to.be.null; 213 | expect(filterCriteria).to.have.property('index').that.is.a('string'); 214 | expect(filterCriteria).to.have.property('type').that.is.a('string').that.equals(mappingType); 215 | expect(filterCriteria).to.have.property('body') 216 | .that.is.an('object') 217 | .that.deep.equals({ // a. this is really 2 tests in one 218 | sort: [ 219 | // b. `_id` is an auto-generated field that ElasticSearch populates for us 220 | // when we want to let the backend/system/ES take care of id population 221 | // so if we want to sort by id, without specifying/controlling our own id field, 222 | // then ofcourse the sort must happen on `_id`, this part of the test, validates that! 223 | '_id' 224 | ], 225 | query: { 226 | bool: { 227 | must: [{ 228 | terms: { 229 | _id: [ 230 | 0, 231 | 1, 232 | 2 233 | ] 234 | } 235 | }, 236 | { 237 | match: { 238 | vip: true 239 | } 240 | }, 241 | { 242 | match: { 243 | docType: modelName 244 | } 245 | } 246 | ] 247 | } 248 | } 249 | }); 250 | expect(filterCriteria).to.have.property('size') 251 | .that.is.a('number'); 252 | expect(filterCriteria).to.have.property('from') 253 | .that.is.a('number'); 254 | 255 | done(); 256 | }); 257 | 258 | it('should build a nested "or" and "and" query for the WHERE filter', function(done) { 259 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 260 | criteria = { 261 | where: { 262 | or: [ 263 | { and: [{ id: { inq: [3, 4, 5] } }, { vip: true }] }, 264 | { role: 'lead' } 265 | ] 266 | } 267 | }; 268 | size = 100; 269 | offset = 10; 270 | modelName = 'MockLoopbackModel'; 271 | modelIdName = 'id'; 272 | settings = getSettings(); 273 | mappingType = settings.mappingType; 274 | 275 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 276 | expect(filterCriteria).not.to.be.null; 277 | expect(filterCriteria).to.have.property('index').that.is.a('string'); 278 | expect(filterCriteria).to.have.property('type').that.is.a('string').that.equals(mappingType); 279 | expect(filterCriteria).to.have.property('body') 280 | .that.is.an('object') 281 | .that.deep.equals({ // a. this is really 2 tests in one 282 | sort: [ 283 | // b. `_id` is an auto-generated field that ElasticSearch populates for us 284 | // when we want to let the backend/system/ES take care of id population 285 | // so if we want to sort by id, without specifying/controlling our own id field, 286 | // then ofcourse the sort must happen on `_id`, this part of the test, validates that! 287 | '_id' 288 | ], 289 | query: { 290 | bool: { 291 | must: [{ 292 | match: { 293 | docType: modelName 294 | } 295 | }, { 296 | bool: { 297 | should: [{ 298 | bool: { 299 | must: [{ 300 | terms: { 301 | _id: [ 302 | 3, 303 | 4, 304 | 5 305 | ] 306 | } 307 | }, 308 | { 309 | match: { 310 | vip: true 311 | } 312 | } 313 | ] 314 | } 315 | }, 316 | { 317 | match: { 318 | role: 'lead' 319 | } 320 | } 321 | ] 322 | } 323 | }] 324 | } 325 | } 326 | }); 327 | expect(filterCriteria).to.have.property('size') 328 | .that.is.a('number'); 329 | expect(filterCriteria).to.have.property('from') 330 | .that.is.a('number'); 331 | 332 | done(); 333 | }); 334 | 335 | it('should build a nested "and" and "or" query for the WHERE filter', function(done) { 336 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 337 | criteria = { 338 | where: { 339 | and: [ 340 | { or: [{ id: { inq: [3, 4, 5] } }, { vip: true }] }, 341 | { role: 'lead' } 342 | ] 343 | } 344 | }; 345 | size = 100; 346 | offset = 10; 347 | modelName = 'MockLoopbackModel'; 348 | modelIdName = 'id'; 349 | settings = getSettings(); 350 | mappingType = settings.mappingType; 351 | 352 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 353 | expect(filterCriteria).not.to.be.null; 354 | expect(filterCriteria).to.have.property('index').that.is.a('string'); 355 | expect(filterCriteria).to.have.property('type').that.is.a('string').that.equals(mappingType); 356 | expect(filterCriteria).to.have.property('body') 357 | .that.is.an('object') 358 | .that.deep.equals({ // a. this is really 2 tests in one 359 | sort: [ 360 | // b. `_id` is an auto-generated field that ElasticSearch populates for us 361 | // when we want to let the backend/system/ES take care of id population 362 | // so if we want to sort by id, without specifying/controlling our own id field, 363 | // then ofcourse the sort must happen on `_id`, this part of the test, validates that! 364 | '_id' 365 | ], 366 | query: { 367 | bool: { 368 | must: [{ 369 | bool: { 370 | should: [{ 371 | terms: { 372 | _id: [ 373 | 3, 374 | 4, 375 | 5 376 | ] 377 | } 378 | }, 379 | { 380 | match: { 381 | vip: true 382 | } 383 | } 384 | ] 385 | } 386 | }, 387 | { 388 | match: { 389 | role: 'lead' 390 | } 391 | }, 392 | { 393 | match: { 394 | docType: modelName 395 | } 396 | } 397 | ] 398 | } 399 | } 400 | }); 401 | expect(filterCriteria).to.have.property('size') 402 | .that.is.a('number'); 403 | expect(filterCriteria).to.have.property('from') 404 | .that.is.a('number'); 405 | 406 | done(); 407 | }); 408 | 409 | it('should build a "nin" query for the WHERE filter', function(done) { 410 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 411 | criteria = { 412 | 'where': { 413 | 'id': { 414 | 'nin': [0, 1, 2] 415 | } 416 | } 417 | }; 418 | size = 100; 419 | offset = 10; 420 | modelName = 'MockLoopbackModel'; 421 | modelIdName = 'id'; 422 | settings = getSettings(); 423 | mappingType = settings.mappingType; 424 | 425 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 426 | expect(filterCriteria).not.to.be.null; 427 | expect(filterCriteria).to.have.property('index').that.is.a('string'); 428 | expect(filterCriteria).to.have.property('type').that.is.a('string').that.equals(mappingType); 429 | expect(filterCriteria).to.have.property('body') 430 | .that.is.an('object') 431 | .that.deep.equals({ // a. this is really 2 tests in one 432 | sort: [ 433 | // b. `_id` is an auto-generated field that ElasticSearch populates for us 434 | // when we want to let the backend/system/ES take care of id population 435 | // so if we want to sort by id, without specifying/controlling our own id field, 436 | // then ofcourse the sort must happen on `_id`, this part of the test, validates that! 437 | '_id' 438 | ], 439 | query: { 440 | bool: { 441 | must: [{ 442 | match: { 443 | docType: modelName 444 | } 445 | }, { 446 | bool: { 447 | must_not: [{ 448 | terms: { 449 | _id: [ 450 | 0, 451 | 1, 452 | 2 453 | ] 454 | } 455 | }] 456 | } 457 | }] 458 | } 459 | } 460 | }); 461 | expect(filterCriteria).to.have.property('size') 462 | .that.is.a('number'); 463 | expect(filterCriteria).to.have.property('from') 464 | .that.is.a('number'); 465 | 466 | done(); 467 | }); 468 | 469 | it('should build a "and" and "nin" query for the WHERE filter', function(done) { 470 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 471 | criteria = { 472 | where: { 473 | and: [ 474 | { id: { nin: [0, 1, 2] } }, 475 | { vip: true } 476 | ] 477 | } 478 | }; 479 | size = 100; 480 | offset = 10; 481 | modelName = 'MockLoopbackModel'; 482 | modelIdName = 'id'; 483 | settings = getSettings(); 484 | mappingType = settings.mappingType; 485 | 486 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 487 | expect(filterCriteria).not.to.be.null; 488 | expect(filterCriteria).to.have.property('index').that.is.a('string'); 489 | expect(filterCriteria).to.have.property('type').that.is.a('string').that.equals(mappingType); 490 | expect(filterCriteria).to.have.property('body') 491 | .that.is.an('object') 492 | .that.deep.equals({ // a. this is really 2 tests in one 493 | sort: [ 494 | // b. `_id` is an auto-generated field that ElasticSearch populates for us 495 | // when we want to let the backend/system/ES take care of id population 496 | // so if we want to sort by id, without specifying/controlling our own id field, 497 | // then ofcourse the sort must happen on `_id`, this part of the test, validates that! 498 | '_id' 499 | ], 500 | query: { 501 | bool: { 502 | must: [{ 503 | bool: { 504 | must_not: [{ 505 | terms: { 506 | _id: [ 507 | 0, 508 | 1, 509 | 2 510 | ] 511 | } 512 | }] 513 | } 514 | }, 515 | { 516 | match: { 517 | vip: true 518 | } 519 | }, 520 | { 521 | match: { 522 | docType: modelName 523 | } 524 | } 525 | ] 526 | } 527 | } 528 | }); 529 | expect(filterCriteria).to.have.property('size') 530 | .that.is.a('number'); 531 | expect(filterCriteria).to.have.property('from') 532 | .that.is.a('number'); 533 | 534 | done(); 535 | }); 536 | 537 | it('should build a "between" query for the WHERE filter', function(done) { 538 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 539 | criteria = { 540 | where: { 541 | order: { 542 | between: [3, 6] 543 | } 544 | } 545 | }; 546 | size = 100; 547 | offset = 10; 548 | modelName = 'MockLoopbackModel'; 549 | modelIdName = 'id'; 550 | settings = getSettings(); 551 | mappingType = settings.mappingType; 552 | 553 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 554 | expect(filterCriteria).not.to.be.null; 555 | expect(filterCriteria).to.have.property('index').that.is.a('string'); 556 | expect(filterCriteria).to.have.property('type').that.is.a('string').that.equals(mappingType); 557 | expect(filterCriteria).to.have.property('body') 558 | .that.is.an('object') 559 | .that.deep.equals({ // a. this is really 2 tests in one 560 | sort: [ 561 | // b. `_id` is an auto-generated field that ElasticSearch populates for us 562 | // when we want to let the backend/system/ES take care of id population 563 | // so if we want to sort by id, without specifying/controlling our own id field, 564 | // then ofcourse the sort must happen on `_id`, this part of the test, validates that! 565 | '_id' 566 | ], 567 | query: { 568 | bool: { 569 | must: [{ 570 | range: { 571 | order: { 572 | gte: 3, 573 | lte: 6 574 | } 575 | } 576 | }, { 577 | match: { 578 | docType: modelName 579 | } 580 | }] 581 | } 582 | } 583 | }); 584 | expect(filterCriteria).to.have.property('size') 585 | .that.is.a('number'); 586 | expect(filterCriteria).to.have.property('from') 587 | .that.is.a('number'); 588 | 589 | done(); 590 | }); 591 | 592 | it('should build a "and" and "between" query for the WHERE filter', function(done) { 593 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 594 | criteria = { 595 | where: { 596 | and: [ 597 | { order: { between: [2, 6] } }, 598 | { vip: true } 599 | ] 600 | } 601 | }; 602 | size = 100; 603 | offset = 10; 604 | modelName = 'MockLoopbackModel'; 605 | modelIdName = 'id'; 606 | settings = getSettings(); 607 | mappingType = settings.mappingType; 608 | 609 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 610 | expect(filterCriteria).not.to.be.null; 611 | expect(filterCriteria).to.have.property('index').that.is.a('string'); 612 | expect(filterCriteria).to.have.property('type').that.is.a('string').that.equals(mappingType); 613 | expect(filterCriteria).to.have.property('body') 614 | .that.is.an('object') 615 | .that.deep.equals({ // a. this is really 2 tests in one 616 | sort: [ 617 | // b. `_id` is an auto-generated field that ElasticSearch populates for us 618 | // when we want to let the backend/system/ES take care of id population 619 | // so if we want to sort by id, without specifying/controlling our own id field, 620 | // then ofcourse the sort must happen on `_id`, this part of the test, validates that! 621 | '_id' 622 | ], 623 | query: { 624 | bool: { 625 | must: [{ 626 | range: { 627 | order: { 628 | gte: 2, 629 | lte: 6 630 | } 631 | } 632 | }, 633 | { 634 | match: { 635 | vip: true 636 | } 637 | }, { 638 | match: { 639 | docType: modelName 640 | } 641 | } 642 | ] 643 | } 644 | } 645 | }); 646 | expect(filterCriteria).to.have.property('size') 647 | .that.is.a('number'); 648 | expect(filterCriteria).to.have.property('from') 649 | .that.is.a('number'); 650 | 651 | done(); 652 | }); 653 | 654 | it('should build multiple normal where clause query without "and" for the WHERE filter', function(done) { 655 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 656 | criteria = { 657 | where: { 658 | role: 'lead', 659 | vip: true 660 | } 661 | }; 662 | size = 100; 663 | offset = 10; 664 | modelName = 'MockLoopbackModel'; 665 | modelIdName = 'id'; 666 | settings = getSettings(); 667 | mappingType = settings.mappingType; 668 | 669 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 670 | expect(filterCriteria).not.to.be.null; 671 | expect(filterCriteria).to.have.property('index').that.is.a('string'); 672 | expect(filterCriteria).to.have.property('type').that.is.a('string').that.equals(mappingType); 673 | expect(filterCriteria).to.have.property('body') 674 | .that.is.an('object') 675 | .that.deep.equals({ // a. this is really 2 tests in one 676 | sort: [ 677 | // b. `_id` is an auto-generated field that ElasticSearch populates for us 678 | // when we want to let the backend/system/ES take care of id population 679 | // so if we want to sort by id, without specifying/controlling our own id field, 680 | // then ofcourse the sort must happen on `_id`, this part of the test, validates that! 681 | '_id' 682 | ], 683 | query: { 684 | bool: { 685 | must: [{ 686 | match: { 687 | role: 'lead' 688 | } 689 | }, 690 | { 691 | match: { 692 | vip: true 693 | } 694 | }, { 695 | match: { 696 | docType: modelName 697 | } 698 | } 699 | ] 700 | } 701 | } 702 | }); 703 | expect(filterCriteria).to.have.property('size') 704 | .that.is.a('number'); 705 | expect(filterCriteria).to.have.property('from') 706 | .that.is.a('number'); 707 | 708 | done(); 709 | }); 710 | 711 | it('should build two "inq" and one "between" without "and" for the WHERE filter', function(done) { 712 | var criteria, size, offset, modelName, modelIdName, settings, mappingType; 713 | criteria = { 714 | where: { 715 | role: { inq: ['lead'] }, 716 | order: { between: [1, 6] }, 717 | id: { inq: [2, 3, 4, 5] } 718 | } 719 | }; 720 | size = 100; 721 | offset = 10; 722 | modelName = 'MockLoopbackModel'; 723 | modelIdName = 'id'; 724 | settings = getSettings(); 725 | mappingType = settings.mappingType; 726 | 727 | var filterCriteria = testConnector.buildFilter(modelName, modelIdName, criteria, size, offset); 728 | expect(filterCriteria).not.to.be.null; 729 | expect(filterCriteria).to.have.property('index').that.is.a('string'); 730 | expect(filterCriteria).to.have.property('type').that.is.a('string').that.equals(mappingType); 731 | expect(filterCriteria).to.have.property('body') 732 | .that.is.an('object') 733 | .that.deep.equals({ // a. this is really 2 tests in one 734 | sort: [ 735 | // b. `_id` is an auto-generated field that ElasticSearch populates for us 736 | // when we want to let the backend/system/ES take care of id population 737 | // so if we want to sort by id, without specifying/controlling our own id field, 738 | // then ofcourse the sort must happen on `_id`, this part of the test, validates that! 739 | '_id' 740 | ], 741 | query: { 742 | bool: { 743 | must: [{ 744 | terms: { 745 | role: [ 746 | 'lead' 747 | ] 748 | } 749 | }, 750 | { 751 | range: { 752 | order: { 753 | gte: 1, 754 | lte: 6 755 | } 756 | } 757 | }, 758 | { 759 | terms: { 760 | _id: [ 761 | 2, 762 | 3, 763 | 4, 764 | 5 765 | ] 766 | } 767 | }, { 768 | match: { 769 | docType: modelName 770 | } 771 | } 772 | ] 773 | } 774 | } 775 | }); 776 | expect(filterCriteria).to.have.property('size') 777 | .that.is.a('number'); 778 | expect(filterCriteria).to.have.property('from') 779 | .that.is.a('number'); 780 | 781 | done(); 782 | }); 783 | }); --------------------------------------------------------------------------------