├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── contributing.md ├── issue_template.md ├── pull_request_template.md └── stale.yml ├── .gitignore ├── .npmignore ├── .nycrc.yml ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── adapter.js ├── declarations.ts ├── error-handler.js ├── index.js ├── methods │ ├── create-bulk.js │ ├── create.js │ ├── find.js │ ├── get-bulk.js │ ├── get.js │ ├── index.js │ ├── patch-bulk.js │ ├── patch.js │ ├── raw.js │ ├── remove-bulk.js │ ├── remove.js │ └── update.js └── utils │ ├── core.js │ ├── index.js │ └── parse-query.js ├── test-utils ├── schema-5.0.js ├── schema-6.0.js ├── schema-7.0.js └── test-db.js ├── test ├── .eslintrc.js ├── core │ ├── create.js │ ├── find.js │ ├── get.js │ ├── index.js │ ├── patch.js │ ├── raw.js │ ├── remove.js │ └── update.js ├── index.js └── utils │ ├── core.js │ ├── index.js │ └── parse-query.js └── types ├── index.test.ts ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | !.eslintrc.js 2 | /coverage/ 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'semistandard' 3 | }; 4 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Feathers 2 | 3 | Thank you for contributing to Feathers! :heart: :tada: 4 | 5 | This repo is the main core and where most issues are reported. Feathers embraces modularity and is broken up across many repos. To make this easier to manage we currently use [Zenhub](https://www.zenhub.com/) for issue triage and visibility. They have a free browser plugin you can install so that you can see what is in flight at any time, but of course you also always see current issues in Github. 6 | 7 | ## Report a bug 8 | 9 | Before creating an issue please make sure you have checked out the docs, specifically the [FAQ](https://docs.feathersjs.com/help/faq.html) section. You might want to also try searching Github. It's pretty likely someone has already asked a similar question. 10 | 11 | If you haven't found your answer please feel free to join our [slack channel](http://slack.feathersjs.com), create an issue on Github, or post on [Stackoverflow](http://stackoverflow.com) using the `feathers` or `feathersjs` tag. We try our best to monitor Stackoverflow but you're likely to get more immediate responses in Slack and Github. 12 | 13 | Issues can be reported in the [issue tracker](https://github.com/feathersjs/feathers/issues). Since feathers combines many modules it can be hard for us to assess the root cause without knowing which modules are being used and what your configuration looks like, so **it helps us immensely if you can link to a simple example that reproduces your issue**. 14 | 15 | ## Report a Security Concern 16 | 17 | We take security very seriously at Feathers. We welcome any peer review of our 100% open source code to ensure nobody's Feathers app is ever compromised or hacked. As a web application developer you are responsible for any security breaches. We do our very best to make sure Feathers is as secure as possible by default. 18 | 19 | In order to give the community time to respond and upgrade we strongly urge you report all security issues to us. Send one of the core team members a PM in [Slack](http://slack.feathersjs.com) or email us at hello@feathersjs.com with details and we will respond ASAP. 20 | 21 | For full details refer to our [Security docs](https://docs.feathersjs.com/SECURITY.html). 22 | 23 | ## Pull Requests 24 | 25 | We :heart: pull requests and we're continually working to make it as easy as possible for people to contribute, including a [Plugin Generator](https://github.com/feathersjs/generator-feathers-plugin) and a [common test suite](https://github.com/feathersjs/feathers-service-tests) for database adapters. 26 | 27 | We prefer small pull requests with minimal code changes. The smaller they are the easier they are to review and merge. A core team member will pick up your PR and review it as soon as they can. They may ask for changes or reject your pull request. This is not a reflection of you as an engineer or a person. Please accept feedback graciously as we will also try to be sensitive when providing it. 28 | 29 | Although we generally accept many PRs they can be rejected for many reasons. We will be as transparent as possible but it may simply be that you do not have the same context or information regarding the roadmap that the core team members have. We value the time you take to put together any contributions so we pledge to always be respectful of that time and will try to be as open as possible so that you don't waste it. :smile: 30 | 31 | **All PRs (except documentation) should be accompanied with tests and pass the linting rules.** 32 | 33 | ### Code style 34 | 35 | Before running the tests from the `test/` folder `npm test` will run ESlint. You can check your code changes individually by running `npm run lint`. 36 | 37 | ### ES6 compilation 38 | 39 | Feathers uses [Babel](https://babeljs.io/) to leverage the latest developments of the JavaScript language. All code and samples are currently written in ES2015. To transpile the code in this repository run 40 | 41 | > npm run compile 42 | 43 | __Note:__ `npm test` will run the compilation automatically before the tests. 44 | 45 | ### Tests 46 | 47 | [Mocha](http://mochajs.org/) tests are located in the `test/` folder and can be run using the `npm run mocha` or `npm test` (with ESLint and code coverage) command. 48 | 49 | ### Documentation 50 | 51 | Feathers documentation is contained in Markdown files in the [feathers-docs](https://github.com/feathersjs/feathers-docs) repository. To change the documentation submit a pull request to that repo, referencing any other PR if applicable, and the docs will be updated with the next release. 52 | 53 | ## External Modules 54 | 55 | If you're written something awesome for Feathers, the Feathers ecosystem, or using Feathers please add it to the [showcase](https://docs.feathersjs.com/why/showcase.html). You also might want to check out the [Plugin Generator](https://github.com/feathersjs/generator-feathers-plugin) that can be used to scaffold plugins to be Feathers compliant from the start. 56 | 57 | If you think it would be a good core module then please contact one of the Feathers core team members in [Slack](http://slack.feathersjs.com) and we can discuss whether it belongs in core and how to get it there. :beers: 58 | 59 | ## Contributor Code of Conduct 60 | 61 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 62 | 63 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 64 | 65 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 66 | 67 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 68 | 69 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 72 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Steps to reproduce 2 | 3 | (First please check that this issue is not already solved as [described 4 | here](https://github.com/feathersjs/feathers/blob/master/.github/contributing.md#report-a-bug)) 5 | 6 | - [ ] Tell us what broke. The more detailed the better. 7 | - [ ] If you can, please create a simple example that reproduces the issue and link to a gist, jsbin, repo, etc. 8 | 9 | ### Expected behavior 10 | Tell us what should happen 11 | 12 | ### Actual behavior 13 | Tell us what happens instead 14 | 15 | ### System configuration 16 | 17 | Tell us about the applicable parts of your setup. 18 | 19 | **Module versions** (especially the part that's not working): 20 | 21 | **NodeJS version**: 22 | 23 | **Operating System**: 24 | 25 | **Browser Version**: 26 | 27 | **React Native Version**: 28 | 29 | **Module Loader**: -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | (If you have not already please refer to the contributing guideline as [described 4 | here](https://github.com/feathersjs/feathers/blob/master/.github/contributing.md#pull-requests)) 5 | 6 | - [ ] Tell us about the problem your pull request is solving. 7 | - [ ] Are there any open issues that are related to this? 8 | - [ ] Is this PR dependent on PRs in other repos? 9 | 10 | If so, please mention them to keep the conversations linked together. 11 | 12 | ### Other Information 13 | 14 | If there's anything else that's important and relevant to your pull 15 | request, mention that information here. This could include 16 | benchmarks, or other information. 17 | 18 | Your PR will be reviewed by a core team member and they will work with you to get your changes merged in a timely manner. If merged your PR will automatically be added to the changelog in the next release. 19 | 20 | If your changes involve documentation updates please mention that and link the appropriate PR in [feathers-docs](https://github.com/feathersjs/feathers-docs). 21 | 22 | Thanks for contributing to Feathers! :heart: -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 84 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - greenkeeper 8 | - bug 9 | - security 10 | - enhancement 11 | # Label to use when marking an issue as stale 12 | staleLabel: wontfix 13 | # Comment to post when marking an issue as stale. Set to `false` to disable 14 | markComment: > 15 | This issue has been automatically marked as stale because it has not had 16 | recent activity. It will be closed if no further activity occurs. 17 | Apologies if the issue could not be resolved. FeathersJS ecosystem 18 | modules are community maintained so there may be a chance that there isn't anybody 19 | available to address the issue at the moment. 20 | For other ways to get help [see here](https://docs.feathersjs.com/help/readme.html). 21 | # Comment to post when closing a stale issue. Set to `false` to disable 22 | closeComment: false 23 | # Only close stale issues 24 | only: issues 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # Compiled binary addons (http://nodejs.org/api/addons.html) 22 | build/Release 23 | 24 | # Dependency directory 25 | # Commenting this out is preferred by some people, see 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 27 | node_modules 28 | 29 | # Users Environment Variables 30 | .lock-wscript 31 | 32 | dist/ 33 | .nyc_output/ 34 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .jshintrc 3 | .travis.yml 4 | .istanbul.yml 5 | .babelrc 6 | .idea/ 7 | .vscode/ 8 | test/ 9 | coverage/ 10 | .github/ 11 | -------------------------------------------------------------------------------- /.nycrc.yml: -------------------------------------------------------------------------------- 1 | all: false 2 | exclude: 3 | - test/**/* 4 | - test-utils/**/* 5 | reporter: 6 | - html 7 | - text 8 | - lcov 9 | watermarks: 10 | statements: [50, 80] 11 | lines: [50, 80] 12 | functions: [50, 80] 13 | branches: [50, 80] 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | language: node_js 4 | node_js: node 5 | services: 6 | - docker 7 | env: 8 | - ES_VERSION=5.0.2 9 | - ES_VERSION=5.6.7 10 | - ES_VERSION=6.8.0 11 | - ES_VERSION=7.0.1 12 | - ES_VERSION=7.1.1 13 | addons: 14 | code_climate: 15 | repo_token: 'f7898d1d1ca2b76715bc35cc3ba880b35e3fbdc07c3aeb27ca98eecb5e5c064d' 16 | notifications: 17 | email: false 18 | before_install: 19 | - sudo sysctl vm.max_map_count=262144 20 | - docker pull elasticsearch:${ES_VERSION} 21 | - docker run -d -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:${ES_VERSION} 22 | install: 23 | - npm install 24 | before_script: 25 | - sleep 10 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v3.1.0](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v3.1.0) (2019-10-07) 4 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v3.0.0...v3.1.0) 5 | 6 | **Closed issues:** 7 | 8 | - An in-range update of @feathersjs/adapter-tests is breaking the build 🚨 [\#96](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues/96) 9 | - An in-range update of @feathersjs/commons is breaking the build 🚨 [\#95](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues/95) 10 | - An in-range update of dtslint is breaking the build 🚨 [\#93](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues/93) 11 | 12 | **Merged pull requests:** 13 | 14 | - Update all dependencies [\#97](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/97) ([daffl](https://github.com/daffl)) 15 | - Update dtslint to the latest version 🚀 [\#92](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/92) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 16 | - Drop support for elasticsearch 2.4 [\#91](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/91) ([jciolek](https://github.com/jciolek)) 17 | 18 | ## [v3.0.0](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v3.0.0) (2019-07-06) 19 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v2.1.0...v3.0.0) 20 | 21 | **Merged pull requests:** 22 | 23 | - Add TypeScript definitions and upgrade tests to Feathers 4 [\#90](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/90) ([daffl](https://github.com/daffl)) 24 | 25 | ## [v2.1.0](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v2.1.0) (2019-06-27) 26 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v2.0.2...v2.1.0) 27 | 28 | **Merged pull requests:** 29 | 30 | - Add support for elasticsearch 7+ [\#88](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/88) ([jciolek](https://github.com/jciolek)) 31 | - Update eslint to the latest version 🚀 [\#87](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/87) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 32 | 33 | ## [v2.0.2](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v2.0.2) (2019-05-14) 34 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v2.0.1...v2.0.2) 35 | 36 | **Closed issues:** 37 | 38 | - $exists + $missing operators not working [\#81](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues/81) 39 | - An in-range update of elasticsearch is breaking the build 🚨 [\#79](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues/79) 40 | 41 | **Merged pull requests:** 42 | 43 | - Remove deprecated version tests from CI and update dependencies [\#83](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/83) ([daffl](https://github.com/daffl)) 44 | - added $exists + $missing to whitelist [\#82](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/82) ([orgalaf](https://github.com/orgalaf)) 45 | 46 | ## [v2.0.1](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v2.0.1) (2019-05-02) 47 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v2.0.0...v2.0.1) 48 | 49 | **Closed issues:** 50 | 51 | - How to use whitelist [\#77](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues/77) 52 | - Upsert doesn't work for bulkCreate [\#75](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues/75) 53 | 54 | **Merged pull requests:** 55 | 56 | - Consider upsert param when setting the method in create-bulk [\#78](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/78) ([othersideofphase](https://github.com/othersideofphase)) 57 | 58 | ## [v2.0.0](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v2.0.0) (2019-04-23) 59 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v1.4.0...v2.0.0) 60 | 61 | **Closed issues:** 62 | 63 | - An in-range update of elasticsearch is breaking the build 🚨 [\#72](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues/72) 64 | - An in-range update of @feathersjs/express is breaking the build 🚨 [\#70](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues/70) 65 | - An in-range update of @feathersjs/errors is breaking the build 🚨 [\#69](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues/69) 66 | - An in-range update of @feathersjs/errors is breaking the build 🚨 [\#67](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues/67) 67 | 68 | **Merged pull requests:** 69 | 70 | - Update nyc to the latest version 🚀 [\#76](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/76) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 71 | - Update mocha to the latest version 🚀 [\#74](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/74) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 72 | - Update sinon to the latest version 🚀 [\#73](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/73) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 73 | - Update nyc to the latest version 🚀 [\#71](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/71) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 74 | - Upgrade to @feathersjs/adapter-commons and latest common service features [\#68](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/68) ([daffl](https://github.com/daffl)) 75 | 76 | ## [v1.4.0](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v1.4.0) (2018-12-16) 77 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v1.3.1...v1.4.0) 78 | 79 | **Closed issues:** 80 | 81 | - I could also use upsert support [\#65](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues/65) 82 | - Would it be possible to implement $wildcard and $regexp [\#63](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues/63) 83 | - Issue with .create [\#60](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues/60) 84 | - Create { \_meta : { \_index: 'myindex-MM-YYY' } } is ignored [\#58](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues/58) 85 | 86 | **Merged pull requests:** 87 | 88 | - WIldcard, regexp \#63 and upsert \#65 support [\#66](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/66) ([penngrove](https://github.com/penngrove)) 89 | - Update semistandard to the latest version 🚀 [\#62](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/62) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 90 | - Update debug to the latest version 🚀 [\#59](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/59) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 91 | - Update eslint to the latest version 🚀 [\#55](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/55) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 92 | - Update sinon to the latest version 🚀 [\#54](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/54) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 93 | 94 | ## [v1.3.1](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v1.3.1) (2018-06-03) 95 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v1.3.0...v1.3.1) 96 | 97 | **Closed issues:** 98 | 99 | - Travis horror [\#53](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues/53) 100 | 101 | **Merged pull requests:** 102 | 103 | - Update uberproto to the latest version 🚀 [\#52](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/52) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 104 | - Update sinon to the latest version 🚀 [\#50](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/50) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 105 | 106 | ## [v1.3.0](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v1.3.0) (2018-05-16) 107 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v1.2.0...v1.3.0) 108 | 109 | **Merged pull requests:** 110 | 111 | - Add support for Elasticsearch 6.0+ [\#49](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/49) ([jciolek](https://github.com/jciolek)) 112 | - Update elasticsearch to the latest version 🚀 [\#48](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/48) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 113 | 114 | ## [v1.2.0](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v1.2.0) (2018-04-18) 115 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v1.1.1...v1.2.0) 116 | 117 | **Merged pull requests:** 118 | 119 | - Add support for $exists and $missing queries [\#46](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/46) ([DesignByOnyx](https://github.com/DesignByOnyx)) 120 | 121 | ## [v1.1.1](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v1.1.1) (2018-04-15) 122 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v1.1.0...v1.1.1) 123 | 124 | **Merged pull requests:** 125 | 126 | - General maintenance [\#45](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/45) ([jciolek](https://github.com/jciolek)) 127 | 128 | ## [v1.1.0](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v1.1.0) (2018-03-07) 129 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v1.0.0...v1.1.0) 130 | 131 | **Merged pull requests:** 132 | 133 | - Updates [\#44](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/44) ([jciolek](https://github.com/jciolek)) 134 | - Update mocha to the latest version 🚀 [\#43](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/43) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 135 | - Update semistandard to the latest version 🚀 [\#42](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/42) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 136 | 137 | ## [v1.0.0](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v1.0.0) (2017-12-01) 138 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v0.4.3...v1.0.0) 139 | 140 | **Merged pull requests:** 141 | 142 | - Update to Feathers Buzzard \(v3\) [\#40](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/40) ([daffl](https://github.com/daffl)) 143 | - Update to new plugin infrastructure [\#39](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/39) ([daffl](https://github.com/daffl)) 144 | - Add logic control to raw method and tests for corresponding codes [\#34](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/34) ([xwa130](https://github.com/xwa130)) 145 | 146 | ## [v0.4.3](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v0.4.3) (2017-11-24) 147 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v0.4.2...v0.4.3) 148 | 149 | **Merged pull requests:** 150 | 151 | - Added nested query param option [\#38](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/38) ([Mattchewone](https://github.com/Mattchewone)) 152 | - Update elasticsearch to the latest version 🚀 [\#37](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/37) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 153 | - Update mocha to the latest version 🚀 [\#36](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/36) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 154 | - Fixed a typo! [\#35](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/35) ([martineboh](https://github.com/martineboh)) 155 | 156 | ## [v0.4.2](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v0.4.2) (2017-08-14) 157 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v0.4.1...v0.4.2) 158 | 159 | **Merged pull requests:** 160 | 161 | - make raw method robuster [\#31](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/31) ([xwa130](https://github.com/xwa130)) 162 | 163 | ## [v0.4.1](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v0.4.1) (2017-08-11) 164 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v0.4.0...v0.4.1) 165 | 166 | **Merged pull requests:** 167 | 168 | - test\(elasticsearch\) add support for elasticsearch 5.4 and 5.5 [\#33](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/33) ([jciolek](https://github.com/jciolek)) 169 | - Update debug to the latest version 🚀 [\#32](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/32) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 170 | 171 | ## [v0.4.0](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v0.4.0) (2017-07-21) 172 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v0.3.1...v0.4.0) 173 | 174 | **Closed issues:** 175 | 176 | - An in-range update of elasticsearch is breaking the build 🚨 [\#29](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues/29) 177 | 178 | **Merged pull requests:** 179 | 180 | - add raw method [\#30](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/30) ([xwa130](https://github.com/xwa130)) 181 | 182 | ## [v0.3.1](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v0.3.1) (2017-06-07) 183 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v0.3.0...v0.3.1) 184 | 185 | **Merged pull requests:** 186 | 187 | - Updated tests and operator [\#27](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/27) ([Mattchewone](https://github.com/Mattchewone)) 188 | - export Service [\#25](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/25) ([christopherjbaker](https://github.com/christopherjbaker)) 189 | 190 | ## [v0.3.0](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v0.3.0) (2017-06-03) 191 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v0.2.3...v0.3.0) 192 | 193 | **Closed issues:** 194 | 195 | - Simple Query String / Aggregations \[Feature\] [\#22](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues/22) 196 | - Using $and in query string [\#20](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues/20) 197 | 198 | **Merged pull requests:** 199 | 200 | - feat\(query\) add $sqs simple\_query\_string query [\#24](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/24) ([Mattchewone](https://github.com/Mattchewone)) 201 | - Update chai to the latest version 🚀 [\#21](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/21) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 202 | - Type for validateType [\#18](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/18) ([Mattchewone](https://github.com/Mattchewone)) 203 | - Update feathers-socketio to the latest version 🚀 [\#17](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/17) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 204 | 205 | ## [v0.2.3](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v0.2.3) (2017-05-06) 206 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v0.2.2...v0.2.3) 207 | 208 | **Implemented enhancements:** 209 | 210 | - Multi term search [\#14](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues/14) 211 | 212 | **Merged pull requests:** 213 | 214 | - feat\(query\) add $all query \(es: array datatype\) [\#16](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/16) ([jciolek](https://github.com/jciolek)) 215 | - Update feathers-service-tests to the latest version 🚀 [\#15](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/15) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 216 | - Update elasticsearch to the latest version 🚀 [\#13](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/13) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 217 | - Update semistandard to the latest version 🚀 [\#12](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/12) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 218 | 219 | ## [v0.2.2](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v0.2.2) (2017-04-15) 220 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v0.2.1...v0.2.2) 221 | 222 | **Closed issues:** 223 | 224 | - How to use with existing datastore question? [\#9](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues/9) 225 | 226 | **Merged pull requests:** 227 | 228 | - feat\(query\) add $child \(es: has\_child\) and $parent \(es: has\_parent\) [\#11](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/11) ([jciolek](https://github.com/jciolek)) 229 | - Add Greenkeeper badge 🌴 [\#10](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/10) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 230 | 231 | ## [v0.2.1](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v0.2.1) (2017-03-19) 232 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v0.2.0...v0.2.1) 233 | 234 | **Merged pull requests:** 235 | 236 | - fix\(query\) add minimum\_should\_match = 1 to the "should" query [\#8](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/8) ([jciolek](https://github.com/jciolek)) 237 | - fix\(eslint\) minor changes to satisfy new version of semistandard [\#7](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/7) ([jciolek](https://github.com/jciolek)) 238 | - Update all dependencies 🌴 [\#6](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/6) ([greenkeeperio-bot](https://github.com/greenkeeperio-bot)) 239 | 240 | ## [v0.2.0](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v0.2.0) (2017-03-15) 241 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/compare/v0.1.0...v0.2.0) 242 | 243 | **Closed issues:** 244 | 245 | - Support full text search [\#1](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues/1) 246 | 247 | **Merged pull requests:** 248 | 249 | - Add full-text and term level queries specific to Elasticsearch [\#5](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/5) ([jciolek](https://github.com/jciolek)) 250 | - Merged master to es-5.1-tests [\#4](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/4) ([jciolek](https://github.com/jciolek)) 251 | - Merged master to es-5.0-tests [\#3](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/3) ([jciolek](https://github.com/jciolek)) 252 | - Update repo links in package.json. [\#2](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/pull/2) ([jciolek](https://github.com/jciolek)) 253 | 254 | ## [v0.1.0](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/tree/v0.1.0) (2017-01-20) 255 | 256 | 257 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Webnicer Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # feathers-elasticsearch 2 | 3 | [![Greenkeeper badge](https://badges.greenkeeper.io/feathersjs-ecosystem/feathers-elasticsearch.svg)](https://greenkeeper.io/) 4 | 5 | [![Build Status](https://travis-ci.org/feathersjs-ecosystem/feathers-elasticsearch.svg?branch=master)](https://travis-ci.org/feathersjs-ecosystem/feathers-elasticsearch) 6 | [![Dependency Status](https://david-dm.org/feathersjs-ecosystem/feathers-elasticsearch/status.svg)](https://david-dm.org/feathersjs-ecosystem/feathers-elasticsearch) 7 | [![Download Status](https://img.shields.io/npm/dm/feathers-elasticsearch.svg?style=flat-square)](https://www.npmjs.com/package/feathers-elasticsearch) 8 | 9 | [feathers-elasticsearch](https://github.com/feathersjs-ecosystem/feathers-elasticsearch/) is a database adapter for [Elasticsearch](https://www.elastic.co/products/elasticsearch). This adapter is not using any ORM, it is dealing with the database directly through the [elasticsearch.js client](https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/quick-start.html). 10 | 11 | 12 | ```bash 13 | $ npm install --save elasticsearch feathers-elasticsearch 14 | ``` 15 | 16 | > __Important:__ `feathers-elasticsearch` implements the [Feathers Common database adapter API](https://docs.feathersjs.com/api/databases/common.html) and [querying syntax](https://docs.feathersjs.com/api/databases/querying.html). 17 | 18 | ## Getting Started 19 | 20 | The following bare-bones example will create a `messages` endpoint and connect to a local `messages` type in the `test` index in your Elasticsearch database: 21 | 22 | ```js 23 | const feathers = require('@feathersjs/feathers'); 24 | const elasticsearch = require('elasticsearch'); 25 | const service = require('feathers-elasticsearch'); 26 | 27 | app.use('/messages', service({ 28 | Model: new elasticsearch.Client({ 29 | host: 'localhost:9200', 30 | apiVersion: '5.0' 31 | }), 32 | elasticsearch: { 33 | index: 'test', 34 | type: 'messages' 35 | } 36 | })); 37 | ``` 38 | 39 | ## Options 40 | 41 | The following options can be passed when creating a new Elasticsearch service: 42 | 43 | - `Model` (**required**) - The Elasticsearch client instance. 44 | - `elasticsearch` (**required**) - Configuration object for elasticsearch requests. The required properties are `index` and `type`. Apart from that you can specify anything that should be passed to **all** requests going to Elasticsearch. Another recognised property is [`refresh`](https://www.elastic.co/guide/en/elasticsearch/guide/2.x/near-real-time.html#refresh-api) which is set to `false` by default. Anything else use at your own risk. 45 | - `paginate` [optional] - A pagination object containing a `default` and `max` page size (see the [Pagination documentation](https://docs.feathersjs.com/api/databases/common.html#pagination)). 46 | - `esVersion` (default: '5.0') [optional] - A string indicating which version of Elasticsearch the service is supposed to be talking to. Based on this setting the service will choose compatible API. If you plan on using Elasticsearch 6.0+ features (e.g. join fields) it's quite important to have it set, as there were breaking changes in Elasticsearch 6.0. 47 | - `id` (default: '_id') [optional] - The id property of your documents in this service. 48 | - `parent` (default: '_parent') [optional] - The parent property, which is used to pass document's parent id. 49 | - `routing` (default: '_routing') [optional] - The routing property, which is used to pass document's routing parameter. 50 | - `join` (default: undefined) [optional] - Elasticsearch 6.0+ specific. The name of the [join field](https://www.elastic.co/guide/en/elasticsearch/reference/6.0/parent-join.html) defined in the mapping type used by the service. It is required for parent-child relationship features (e.g. setting a parent, `$child` and `$parent` queries) to work. 51 | - `meta` (default: '_meta') [optional] - The meta property of your documents in this service. The meta field is an object containing elasticsearch specific information, e.g. `_score`, `_type`, `_index`, `_parent`, `_routing` and so forth. It will be stripped off from the documents passed to the service. 52 | - `whitelist` (default: `['$prefix', '$wildcard', '$regexp', '$exists', '$missing', '$all', '$match', '$phrase', '$phrase_prefix', '$and', '$sqs', '$child', '$parent', '$nested', '$fields', '$path', '$type', '$query', '$operator']`) [optional] - The list of additional non-standard query parameters to allow, by default populated with all Elasticsearch specific ones. You can override, for example in order to restrict access to some queries (see the [options documentation](https://docs.feathersjs.com/api/databases/common.html#serviceoptions)). 53 | 54 | ## Complete Example 55 | 56 | Here's an example of a Feathers server that uses `feathers-elasticsearch`. 57 | 58 | ```js 59 | const feathers = require('@feathersjs/feathers'); 60 | const rest = require('@feathersjs/express/rest'); 61 | const express = require('@feathersjs/express'); 62 | 63 | const service = require('feathers-elasticsearch'); 64 | const elasticsearch = require('elasticsearch'); 65 | 66 | const messageService = service({ 67 | Model: new elasticsearch.Client({ 68 | host: 'localhost:9200', 69 | apiVersion: '6.0' 70 | }), 71 | paginate: { 72 | default: 10, 73 | max: 50 74 | }, 75 | elasticsearch: { 76 | index: 'test', 77 | type: 'messages' 78 | }, 79 | esVersion: '6.0' 80 | }); 81 | 82 | // Initialize the application 83 | const app = express(feathers()); 84 | 85 | // Needed for parsing bodies (login) 86 | app.use(express.json()); 87 | app.use(express.urlencoded({ extended: true })); 88 | // Enable REST services 89 | app.configure(express.rest()); 90 | // Initialize your feathers plugin 91 | app.use('/messages', messageService); 92 | app.use(express.errorHandler());; 93 | 94 | app.listen(3030); 95 | 96 | console.log('Feathers app started on 127.0.0.1:3030'); 97 | ``` 98 | 99 | You can run this example by using `npm start` and going to [localhost:3030/messages](http://localhost:3030/messages). 100 | You should see an empty array. That's because you don't have any messages yet but you now have full CRUD for your new message service! 101 | 102 | ## Supported Elasticsearch specific queries 103 | 104 | On top of the standard, cross-adapter [queries](querying.md), feathers-elasticsearch also supports Elasticsearch specific queries. 105 | 106 | ### $all 107 | 108 | [The simplest query `match_all`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-all-query.html). Find all documents. 109 | 110 | ```js 111 | query: { 112 | $all: true 113 | } 114 | ``` 115 | 116 | ### $prefix 117 | 118 | [Term level query `prefix`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-prefix-query.html). Find all documents which have given field containing terms with a specified prefix (not analyzed). 119 | 120 | ```js 121 | query: { 122 | user: { 123 | $prefix: 'bo' 124 | } 125 | } 126 | ``` 127 | 128 | ### $wildcard 129 | 130 | [Term level query `wildcard`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-wildcard-query.html). Find all documents which have given field containing terms matching a wildcard expression (not analyzed). 131 | 132 | ```js 133 | query: { 134 | user: { 135 | $wildcard: 'B*b' 136 | } 137 | } 138 | ``` 139 | 140 | ### $regexp 141 | 142 | [Term level query `regexp`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html). Find all documents which have given field containing terms matching a regular expression (not analyzed). 143 | 144 | ```js 145 | query: { 146 | user: { 147 | $regexp: 'Bo[xb]' 148 | } 149 | } 150 | ``` 151 | 152 | ### $exists 153 | 154 | [Term level query `exists`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-exists-query.html). Find all documents that have at least one non-null value in the original field (not analyzed). 155 | 156 | ```js 157 | query: { 158 | $exists: ['phone', 'address'] 159 | } 160 | ``` 161 | 162 | ### $missing 163 | 164 | The inverse of [`exists`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-exists-query.html). Find all documents missing the specified field (not analyzed). 165 | 166 | ```js 167 | query: { 168 | $missing: ['phone', 'address'] 169 | } 170 | ``` 171 | 172 | ### $match 173 | 174 | [Full text query `match`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html). Find all documents which have given given fields matching the specified value (analysed). 175 | 176 | ```js 177 | query: { 178 | bio: { 179 | $match: 'javascript' 180 | } 181 | } 182 | ``` 183 | 184 | ### $phrase 185 | 186 | [Full text query `match_phrase`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase.html). Find all documents which have given given fields matching the specified phrase (analysed). 187 | 188 | ```js 189 | query: { 190 | bio: { 191 | $phrase: 'I like JavaScript' 192 | } 193 | } 194 | ``` 195 | 196 | ### $phrase_prefix 197 | 198 | [Full text query `match_phrase_prefix`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase-prefix.html). Find all documents which have given given fields matching the specified phrase prefix (analysed). 199 | 200 | ```js 201 | query: { 202 | bio: { 203 | $phrase_prefix: 'I like JavaS' 204 | } 205 | } 206 | ``` 207 | 208 | ### $child 209 | 210 | [Joining query `has_child`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-has-child-query.html). 211 | Find all documents which have children matching the query. The `$child` query is essentially a full-blown query of its own. The `$child` query requires `$type` property. 212 | 213 | **Elasticsearch 6.0 change** 214 | 215 | Prior to Elasticsearch 6.0, the `$type` parameter represents the child document type in the index. As of Elasticsearch 6.0, the `$type` parameter represents the child relationship name, as defined in the [join field](https://www.elastic.co/guide/en/elasticsearch/reference/6.0/parent-join.html). 216 | 217 | 218 | ```js 219 | query: { 220 | $child: { 221 | $type: 'blog_tag', 222 | tag: 'something' 223 | } 224 | } 225 | ``` 226 | 227 | ### $parent 228 | 229 | [Joining query `has_parent`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-has-parent-query.html). 230 | Find all documents which have parent matching the query. The `$parent` query is essentially a full-blown query of its own. The `$parent` query requires `$type` property. 231 | 232 | **Elasticsearch 6.0 change** 233 | 234 | Prior to Elasticsearch 6.0, the `$type` parameter represents the parent document type in the index. As of Elasticsearch 6.0, the `$type` parameter represents the parent relationship name, as defined in the [join field](https://www.elastic.co/guide/en/elasticsearch/reference/6.0/parent-join.html). 235 | 236 | ```js 237 | query: { 238 | $parent: { 239 | $type: 'blog', 240 | title: { 241 | $match: 'javascript' 242 | } 243 | } 244 | } 245 | ``` 246 | 247 | ### $and 248 | 249 | This operator does not translate directly to any Elasticsearch query, but it provides support for [Elasticsearch array datatype](https://www.elastic.co/guide/en/elasticsearch/reference/current/array.html). 250 | Find all documents which match all of the given criteria. As any field in Elasticsearch can contain an array, therefore sometimes it is important to match more than one value per field. 251 | 252 | 253 | ```js 254 | query: { 255 | $and: [ 256 | { notes: { $match: 'javascript' } }, 257 | { notes: { $match: 'project' } } 258 | ] 259 | } 260 | ``` 261 | 262 | There is also a shorthand version of `$and` for equality. For instance: 263 | 264 | ```js 265 | query: { 266 | $and: [ 267 | { tags: 'javascript' }, 268 | { tags: 'react' } 269 | ] 270 | } 271 | ``` 272 | 273 | Can be also expressed as: 274 | 275 | ```js 276 | query: { 277 | tags: ['javascript', 'react'] 278 | } 279 | ``` 280 | 281 | ### $sqs 282 | 283 | [simple_query_string](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html). A query that uses the SimpleQueryParser to parse its context. Optional `$operator` which is set to `or` by default but can be set to `and` if required. 284 | 285 | ```js 286 | query: { 287 | $sqs: { 288 | $fields: [ 289 | 'title^5', 290 | 'description' 291 | ], 292 | $query: '+like +javascript', 293 | $operator: 'and' 294 | } 295 | } 296 | ``` 297 | This can also be expressed in an URL as the following: 298 | ```http 299 | http://localhost:3030/users?$sqs[$fields][]=title^5&$sqs[$fields][]=description&$sqs[$query]=+like +javascript&$sqs[$operator]=and 300 | ``` 301 | 302 | ## Parent-child relationship 303 | 304 | Elasticsearch supports parent-child relationship however it is not exactly the same as in relational databases. To make things even more interesting, the relationship principles were slightly different up to (version 5.6)[https://www.elastic.co/guide/en/elasticsearch/reference/5.6/mapping-parent-field.html] and from (version 6.0+)[https://www.elastic.co/guide/en/elasticsearch/reference/6.0/parent-join.html] onwards. 305 | 306 | Even though Elasticsearch's API changed in that matter, feathers-elasticsearch offers consistent API across those changes. That is actually the main reason why the `esVersion` and `join` service options have been introduced (see the "Options" section of this manual). Having said that, it is important to notice that there are but subtle differences, which are outline below and in the description of `$parent` and `$child` queries. 307 | 308 | ### Overview 309 | 310 | feathers-elasticsearch supports all CRUD operations for Elasticsearch types with parent mapping, and does that with the Elasticsearch constrains. Therefore: 311 | 312 | - each operation concering a single document (create, get, patch, update, remove) is required to provide parent id 313 | - creating documents in bulk (providing a list of documents) is the same as many single document operations, so parent id is required as well 314 | - to avoid any doubts, none of the query based operations (find, bulk patch, bulk remove) can use the parent id 315 | 316 | 317 | #### Elasticsearch <= 5.6 318 | 319 | Parent id should be provided as part of the data for the create operations (single and bulk): 320 | 321 | ```javascript 322 | postService.create({ 323 | _id: 123, 324 | text: 'JavaScript may be flawed, but it\'s better than Java anyway.' 325 | }); 326 | 327 | commentService.create({ 328 | _id: 1000, 329 | _parent: 123, 330 | text: 'You cannot be serious.' 331 | }) 332 | ``` 333 | Please note, that name of the parent property (`_parent` by default) is configurable through the service options, so that you can set it to whatever suits you. 334 | 335 | For all other operations (get, patch, update, remove), the parent id should be provided as part of the query: 336 | 337 | ```javascript 338 | childService.remove( 339 | 1000, 340 | { query: { _parent: 123 } } 341 | ); 342 | ``` 343 | 344 | #### Elasticsearch >= 6.0 345 | 346 | As the parent-child relationship changed in Elasticsearch 6.0, it is now expressed by the [join datatype](https://www.elastic.co/guide/en/elasticsearch/reference/6.0/parent-join.html). Everything said above about the parent id holds true, although there is one more detail to be taken into account - the relationship name. 347 | 348 | Let's consider the following mapping: 349 | 350 | ```javascript 351 | { 352 | mappings: { 353 | doc: { 354 | properties: { 355 | text: { 356 | type: 'text' 357 | }, 358 | my_join_field: { 359 | type: 'join', 360 | relations: { 361 | post: 'comment' 362 | } 363 | } 364 | } 365 | } 366 | } 367 | } 368 | ``` 369 | 370 | Parent id (for children) and relationship name (for children and parents) should be provided for as part of the data for the create operations (single and bulk): 371 | 372 | ```javascript 373 | docService.create({ 374 | _id: 123, 375 | text: 'JavaScript may be flawed, but it\'s better than Java anyway.', 376 | my_join_field: 'post' 377 | }); 378 | 379 | docService.create({ 380 | _id: 1000, 381 | _parent: 123, 382 | text: 'You cannot be serious.', 383 | my_join_field: 'comment' 384 | }) 385 | ``` 386 | 387 | Please note, that name of the parent property ('_parent' by default) and the join property (`undefined` by default) are configurable through the service options, so that you can set it to whatever suits you. 388 | 389 | For all other operations (get, patch, update, remove), the parent id should be provided as part of the query: 390 | 391 | ```javascript 392 | docService.remove( 393 | 1000, 394 | { query: { _parent: 123 } } 395 | ); 396 | ``` 397 | 398 | ## Supported Elasticsearch versions 399 | 400 | feathers-elasticsearch is currently tested on Elasticsearch 5.0, 5.6, 6.6, 6.7, 6.8, 7.0 and 7.1 Please note, we have recently dropped support for version 2.4, as its life ended quite a while back. If you are still running Elasticsearch 2.4 and want to take advantage of feathers-elasticsearch, please use version 2.x of this package. 401 | 402 | ## Quirks 403 | 404 | ### Updating and deleting by query 405 | 406 | Elasticsearch is special in many ways. For example, the ["update by query"](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update-by-query.html) API is still considered experimental and so is the ["delete by query"](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete-by-query.html) API introduced in Elasticsearch 5.0. 407 | 408 | Just to clarify - update in Elasticsearch is an equivalent to `patch` in feathers. I will use `patch` from now on, to set focus on the feathers side of the fence. 409 | 410 | Considering the above, our implementation of path / remove by query uses combo of find and bulk patch / remove, which in turn means for you: 411 | 412 | - Standard pagination is taken into account for patching / removing by query, so you have no guarantee that all existing documents matching your query will be patched / removed. 413 | - The operation is a bit slower than it could potentially be, because of the two-step process involved. 414 | 415 | Considering, however that elasticsearch is mainly used to dump data in it and search through it, I presume that should not be a great problem. 416 | 417 | ### Search visibility 418 | 419 | Please be aware that search visibility of the changes (creates, updates, patches, removals) is going to be delayed due to Elasticsearch [`index.refresh_interval`](https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html) setting. You may force refresh after each operation by setting the service option `elasticsearch.refresh` as decribed above but it is highly discouraged due to Elasticsearch performance implications. 420 | 421 | ### Full-text search 422 | 423 | Currently feathers-elasticsearch supports most important full-text queries in their default form. Elasticsearch search allows additional parameters to be passed to each of those queries for fine-tuning. Those parameters can change behaviour and affect peformance of the queries therefore I believe they should not be exposed to the client. I am considering ways of adding them safely to the queries while retaining flexibility. 424 | 425 | ### Performance considerations 426 | 427 | Most of the data mutating operations in Elasticsearch v5.0 (create, update, remove) do not return the full resulting document, therefore I had to resolve to using get as well in order to return complete data. This solution is of course adding a bit of an overhead, although it is also compliant with the standard behaviour expected of a feathers database adapter. 428 | 429 | The conceptual solution for that is quite simple. This behaviour will be configurable through a `lean` switch allowing to get rid of those additional gets should they be not needed for your application. This feature will be added soon as well. 430 | 431 | ### Upsert capability 432 | 433 | An `upsert` parameter is available for the `create` operation that will update the document if it exists already instead of throwing an error. 434 | 435 | ```javascript 436 | postService.create({ 437 | _id: 123, 438 | text: 'JavaScript may be flawed, but it\'s better than Ruby.' 439 | }, 440 | { 441 | upsert: true 442 | }) 443 | 444 | ``` 445 | 446 | Additionally, an `upsert` parameter is also available for the `update` operation that will create the document if it doesn't exist instead of throwing an error. 447 | 448 | ```javascript 449 | postService.update(123, { 450 | _id: 123, 451 | text: 'JavaScript may be flawed, but Feathers makes it fly.' 452 | }, 453 | { 454 | upsert: true 455 | }) 456 | 457 | ``` 458 | 459 | ## Contributing 460 | 461 | If you find a bug or something to improve we will be happy to see your PR! 462 | 463 | When adding a new feature, please make sure you write tests for it with decent coverage as well. 464 | 465 | ### Running tests locally 466 | 467 | When you run the test locally, you need to set the Elasticsearch version you are testing against in an environmental variable `ES_VERSION` to tell the tests which schema it should set up. The value from this variable will be also used to determine the API version for the Elasticsearch client and the tested service. 468 | 469 | If you want to all tests: 470 | 471 | ```bash 472 | ES_VERSION=6.7.2 npm t 473 | ``` 474 | 475 | When you just want to run coverage: 476 | 477 | ```bash 478 | ES_VERSION=6.7.2 npm run coverage 479 | ``` 480 | 481 | ## Born out of need 482 | 483 | feathers-elasticsearch was born out of need. When I was building [Hacker Search](https://hacker-search.net) (a real time search engine for Hacker News), I chose Elasticsearch for the database and Feathers for the application framework. All well and good, the only snag was a missing adapter, which would marry the two together. I decided to take a detour from the main project and create the missing piece. Three weeks had passed and the result was... another project (typical, isn't it). Everything went to plan however, and Hacker Search has been happily using feathers-elasticsearch since its first release. 484 | 485 | If you want to see the adapter in action, jump on Hacker Search and watch the queries sent from the client. Feel free to play around with the API. 486 | 487 | ## License 488 | 489 | Copyright (c) 2018 490 | 491 | Licensed under the [MIT license](LICENSE). 492 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feathers-elasticsearch", 3 | "description": "Elasticsearch adapter for FeathersJs", 4 | "version": "3.1.0", 5 | "homepage": "https://github.com/feathersjs-ecosystem/feathers-elasticsearch", 6 | "main": "lib/", 7 | "types": "types", 8 | "keywords": [ 9 | "feathers", 10 | "feathers-plugin" 11 | ], 12 | "license": "MIT", 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/feathersjs-ecosystem/feathers-elasticsearch.git" 16 | }, 17 | "author": { 18 | "name": "Feathers contributors", 19 | "email": "hello@feathersjs.com", 20 | "url": "https://feathersjs.com" 21 | }, 22 | "contributors": [], 23 | "bugs": { 24 | "url": "https://github.com/feathersjs-ecosystem/feathers-elasticsearch/issues" 25 | }, 26 | "engines": { 27 | "node": ">= 6" 28 | }, 29 | "scripts": { 30 | "publish": "git push origin --tags && npm run changelog && git push origin", 31 | "release:patch": "npm version patch && npm publish", 32 | "release:minor": "npm version minor && npm publish", 33 | "release:major": "npm version major && npm publish", 34 | "changelog": "github_changelog_generator && git add CHANGELOG.md && git commit -am \"Updating changelog\"", 35 | "lint": "eslint --fix .", 36 | "dtslint": "dtslint types", 37 | "mocha": "mocha --recursive test/", 38 | "coverage": "nyc npm run mocha", 39 | "test": "npm run lint && npm run dtslint && npm run coverage" 40 | }, 41 | "directories": { 42 | "lib": "lib" 43 | }, 44 | "dependencies": { 45 | "@feathersjs/adapter-commons": "^5.0.0-pre.31", 46 | "@feathersjs/commons": "^5.0.0-pre.31", 47 | "@feathersjs/errors": "^5.0.0-pre.31", 48 | "@feathersjs/feathers": "^5.0.0-pre.31", 49 | "debug": "^4.3.4" 50 | }, 51 | "peerDependencies": { 52 | "@elastic/elasticsearch": "^8.4.0" 53 | }, 54 | "devDependencies": { 55 | "@feathersjs/adapter-tests": "^5.0.0-pre.31", 56 | "@types/mocha": "^10.0.0", 57 | "@types/node": "^18.11.9", 58 | "chai": "^4.3.7", 59 | "mocha": "^10.1.0", 60 | "pg": "^8.8.0", 61 | "shx": "^0.3.4", 62 | "sqlite3": "^5.1.2", 63 | "typescript": "^4.8.4" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/adapter.js: -------------------------------------------------------------------------------- 1 | // import { _ } from "@feathersjs/commons"; 2 | import { AdapterBase, filterQuery } from '@feathersjs/adapter-commons'; 3 | 4 | import { errors } from '@feathersjs/errors'; 5 | import { errorHandler } from './error-handler.js'; 6 | // const errors = require('@feathersjs/errors'); 7 | // const debug = makeDebug('feathers-elasticsearch'); 8 | 9 | import * as methods from './methods/index.js'; 10 | 11 | export class ElasticAdapter extends AdapterBase { 12 | constructor(options) { 13 | if (typeof options !== 'object') { 14 | throw new Error('Elasticsearch options have to be provided'); 15 | } 16 | 17 | if (!options || !options.Model) { 18 | throw new Error('Elasticsearch `Model` (client) needs to be provided'); 19 | } 20 | 21 | super({ 22 | id: '_id', 23 | parent: '_parent', 24 | routing: '_routing', 25 | meta: '_meta', 26 | esParams: Object.assign({ refresh: false }, options.elasticsearch), 27 | ...options, 28 | filters: { 29 | ...options.filters, 30 | $routing: (val) => val, 31 | }, 32 | operators: [ 33 | ...(options.operators || []), 34 | '$prefix', 35 | '$wildcard', 36 | '$regexp', 37 | '$exists', 38 | '$missing', 39 | '$all', 40 | '$match', 41 | '$phrase', 42 | '$phrase_prefix', 43 | '$and', 44 | '$sqs', 45 | '$child', 46 | '$parent', 47 | '$nested', 48 | '$fields', 49 | '$path', 50 | '$type', 51 | '$query', 52 | '$operator', 53 | '$index', 54 | ], 55 | }); 56 | 57 | // Alias getters for options 58 | ['Model', 'index', 'parent', 'meta', 'join', 'esVersion', 'esParams'].forEach((name) => 59 | Object.defineProperty(this, name, { 60 | get() { 61 | return this.options[name]; 62 | }, 63 | }) 64 | ); 65 | } 66 | 67 | filterQuery(params = {}) { 68 | const options = this.getOptions(params); 69 | const { filters, query } = filterQuery(params?.query || {}, options); 70 | 71 | if (!filters.$skip || isNaN(filters.$skip)) { 72 | filters.$skip = 0; 73 | } 74 | 75 | if (typeof filters.$sort === 'object') { 76 | filters.$sort = Object.entries(filters.$sort).map(([key, val]) => ({ 77 | [key]: val > 0 ? 'asc' : 'desc', 78 | })); 79 | } 80 | 81 | return { filters, query, paginate: options.paginate }; 82 | } 83 | 84 | // GET 85 | _find(params = {}) { 86 | return methods.find(this, params).catch(errorHandler); 87 | } 88 | 89 | // GET 90 | _get(id, params = {}) { 91 | return methods.get(this, id, params).catch((error) => errorHandler(error, id)); 92 | } 93 | 94 | // POST 95 | // Supports single and bulk creation, with or without id specified. 96 | _create(data, params = {}) { 97 | // Check if we are creating single item. 98 | if (!Array.isArray(data)) { 99 | return methods 100 | .create(this, data, params) 101 | .catch((error) => errorHandler(error, data[this.id])); 102 | } 103 | 104 | return methods.createBulk(this, data, params).catch(errorHandler); 105 | } 106 | 107 | // PUT 108 | // Supports single item update. 109 | _update(id, data, params = {}) { 110 | return methods.update(this, id, data, params).catch((error) => errorHandler(error, id)); 111 | } 112 | 113 | // PATCH 114 | // Supports single and bulk patching. 115 | _patch(id, data, params = {}) { 116 | // Check if we are patching single item. 117 | if (id !== null) { 118 | return methods.patch(this, id, data, params).catch((error) => errorHandler(error, id)); 119 | } 120 | 121 | return methods.patchBulk(this, data, params).catch(errorHandler); 122 | } 123 | 124 | // DELETE 125 | // Supports single and bulk removal. 126 | _remove(id, params = {}) { 127 | if (id !== null) { 128 | return methods.remove(this, id, params).catch((error) => errorHandler(error, id)); 129 | } 130 | 131 | return methods.removeBulk(this, params).catch(errorHandler); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/declarations.ts: -------------------------------------------------------------------------------- 1 | import { Params, Paginated, Id, NullableId, Query, Hook } from '@feathersjs/feathers'; 2 | import { AdapterServiceOptions, AdapterParams, AdapterQuery } from '@feathersjs/adapter-commons' 3 | import { Client } from '@elastic/elasticsearch' 4 | export { estypes } from '@elastic/elasticsearch' 5 | 6 | export interface ElasticAdapterServiceOptions extends AdapterServiceOptions { 7 | Model: Client; 8 | index?: string; 9 | elasticsearch?: any; 10 | parent?: string; 11 | routing?: string; 12 | join?: string; 13 | meta?: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/error-handler.js: -------------------------------------------------------------------------------- 1 | import { errors } from "@feathersjs/errors"; 2 | 3 | export function errorHandler(error, id) { 4 | if (error instanceof errors.FeathersError) { 5 | throw error; 6 | } 7 | const statusCode = error.statusCode; 8 | 9 | if (statusCode === 404 && id !== undefined) { 10 | throw new errors.NotFound(`No record found for id '${id}'`); 11 | } 12 | 13 | if (errors[statusCode]) { 14 | throw new errors[statusCode](error.message, error); 15 | } 16 | 17 | throw new errors.GeneralError(error.message, error); 18 | } 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { ElasticAdapter } from "./adapter.js"; 2 | 3 | export * from "./error-handler.js"; 4 | export * from "./adapter.js"; 5 | 6 | export class ElasticService extends ElasticAdapter { 7 | async find(params) { 8 | return this._find(params); 9 | } 10 | 11 | async get(id, params) { 12 | return this._get(id, params); 13 | } 14 | 15 | async create(data, params) { 16 | return this._create(data, params); 17 | } 18 | 19 | async update(id, data, params) { 20 | return this._update(id, data, params); 21 | } 22 | 23 | async patch(id, data, params) { 24 | return this._patch(id, data, params); 25 | } 26 | 27 | async remove(id, params) { 28 | return this._remove(id, params); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/methods/create-bulk.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { mapBulk, getDocDescriptor } from '../utils/index.js'; 4 | import { getBulk } from './get-bulk.js'; 5 | 6 | function getBulkCreateParams(service, data, params) { 7 | return Object.assign( 8 | { 9 | body: data.reduce((result, item) => { 10 | const { id, parent, routing, join, doc } = getDocDescriptor(service, item); 11 | const method = id !== undefined && !params.upsert ? 'create' : 'index'; 12 | 13 | if (join) { 14 | doc[service.join] = { 15 | name: join, 16 | parent, 17 | }; 18 | } 19 | 20 | result.push({ [method]: { _id: id, routing } }); 21 | result.push(doc); 22 | 23 | return result; 24 | }, []), 25 | }, 26 | service.esParams 27 | ); 28 | } 29 | 30 | export function createBulk(service, data, params) { 31 | const bulkCreateParams = getBulkCreateParams(service, data, params); 32 | 33 | return service.Model.bulk(bulkCreateParams).then((results) => { 34 | const created = mapBulk(results.items, service.id, service.meta, service.join); 35 | // We are fetching only items which have been correctly created. 36 | const docs = created 37 | .map((item, index) => 38 | Object.assign( 39 | { 40 | [service.routing]: data[index][service.routing] || data[index][service.parent], 41 | }, 42 | item 43 | ) 44 | ) 45 | .filter((item) => item[service.meta].status === 201) 46 | .map((item) => ({ 47 | _id: item[service.meta]._id, 48 | routing: item[service.routing], 49 | })); 50 | 51 | if (!docs.length) { 52 | return created; 53 | } 54 | 55 | return getBulk(service, docs, params).then((fetched) => { 56 | let fetchedIndex = 0; 57 | 58 | // We need to return responses for all items, either success or failure, 59 | // in the same order as the request. 60 | return created.map((createdItem) => { 61 | if (createdItem[service.meta].status === 201) { 62 | const fetchedItem = fetched[fetchedIndex]; 63 | 64 | fetchedIndex += 1; 65 | 66 | return fetchedItem; 67 | } 68 | 69 | return createdItem; 70 | }); 71 | }); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /src/methods/create.js: -------------------------------------------------------------------------------- 1 | import { removeProps, getDocDescriptor } from '../utils/index.js'; 2 | import { get } from './get.js'; 3 | 4 | function getCreateParams(service, docDescriptor) { 5 | let { id, parent, routing, join, doc } = docDescriptor; 6 | 7 | if (join) { 8 | doc = Object.assign( 9 | { 10 | [service.join]: { 11 | name: join, 12 | parent, 13 | }, 14 | }, 15 | doc 16 | ); 17 | } 18 | 19 | return Object.assign({ id, routing, body: doc }, service.esParams); 20 | } 21 | 22 | export function create(service, data, params) { 23 | const docDescriptor = getDocDescriptor(service, data); 24 | const { id, routing } = docDescriptor; 25 | const createParams = getCreateParams(service, docDescriptor); 26 | const getParams = Object.assign(removeProps(params, 'query', 'upsert'), { 27 | query: Object.assign({ [service.routing]: routing }, params.query), 28 | }); 29 | // Elasticsearch `create` expects _id, whereas index does not. 30 | // Our `create` supports both forms. 31 | const method = id !== undefined && !params.upsert ? 'create' : 'index'; 32 | 33 | return service.Model[method](createParams).then((result) => get(service, result._id, getParams)); 34 | } 35 | -------------------------------------------------------------------------------- /src/methods/find.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { parseQuery, mapFind } from '../utils/index.js'; 4 | 5 | export function find(service, params) { 6 | const { filters, query, paginate } = service.filterQuery(params); 7 | const esQuery = parseQuery(query, service.id); 8 | const findParams = { 9 | index: filters.$index ?? service.index, 10 | from: filters.$skip, 11 | size: filters.$limit, 12 | sort: filters.$sort, 13 | routing: filters.$routing, 14 | query: esQuery ? { bool: esQuery } : undefined, 15 | ...service.esParams, 16 | }; 17 | 18 | console.dir(findParams, { depth: null }); 19 | 20 | // The `refresh` param is not recognised for search in Es. 21 | delete findParams.refresh; 22 | 23 | return service.Model.search(findParams).then((result) => 24 | mapFind( 25 | result, 26 | service.id, 27 | service.meta, 28 | service.join, 29 | filters, 30 | !!(paginate && paginate.default) 31 | ) 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/methods/get-bulk.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { mapGet } from '../utils/index.js'; 4 | 5 | export function getBulk(service, docs, params) { 6 | const { filters } = service.filterQuery(params); 7 | const bulkGetParams = Object.assign( 8 | { 9 | _source: filters.$select, 10 | body: { docs }, 11 | }, 12 | service.esParams 13 | ); 14 | 15 | return service.Model.mget(bulkGetParams).then((fetched) => 16 | fetched.docs.map((item) => mapGet(item, service.id, service.meta, service.join)) 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/methods/get.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { errors } from '@feathersjs/errors'; 4 | import { mapGet, getDocDescriptor, getQueryLength } from '../utils/index.js'; 5 | 6 | export function get(service, id, params) { 7 | const { filters, query } = service.filterQuery(params); 8 | const queryLength = getQueryLength(service, query); 9 | 10 | if (queryLength >= 1) { 11 | return service.core 12 | .find(service, { 13 | ...params, 14 | query: { 15 | $and: [params.query, { [service.id]: id }], 16 | }, 17 | paginate: false, 18 | }) 19 | .then(([result]) => { 20 | if (!result) { 21 | throw new errors.NotFound(`No record found for id ${id}`); 22 | } 23 | 24 | return result; 25 | }); 26 | } 27 | 28 | const { routing } = getDocDescriptor(service, query); 29 | const getParams = Object.assign( 30 | { 31 | _source: filters.$select, 32 | id: String(id), 33 | routing, 34 | }, 35 | service.esParams 36 | ); 37 | 38 | return service.Model.get(getParams).then((result) => 39 | mapGet(result, service.id, service.meta, service.join) 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/methods/index.js: -------------------------------------------------------------------------------- 1 | export { find } from './find.js'; 2 | export { get } from './get.js'; 3 | export { getBulk } from './get-bulk.js'; 4 | export { create } from './create.js'; 5 | export { createBulk } from './create-bulk.js'; 6 | export { patch } from './patch.js'; 7 | export { patchBulk } from './patch-bulk.js'; 8 | export { remove } from './remove.js'; 9 | export { removeBulk } from './remove-bulk.js'; 10 | export { update } from './update.js'; 11 | export { raw } from './raw.js'; 12 | -------------------------------------------------------------------------------- /src/methods/patch-bulk.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { mapBulk, removeProps, getDocDescriptor } from '../utils/index.js'; 4 | 5 | export function patchBulk(service, data, params) { 6 | const { find } = service.core; 7 | const { filters } = service.filterQuery(params); 8 | 9 | // Poor man's semi-deep object extension. We only want to override params.query.$select here. 10 | const findParams = Object.assign(removeProps(params, 'query'), { 11 | query: Object.assign({}, params.query, { $select: false }), 12 | }); 13 | 14 | // Elasticsearch provides update by query, which is quite sadly somewhat unfit for our purpose here. 15 | // Hence the find / bulk-update duo. We need to be aware, that the pagination rules apply here, 16 | // therefore the update will be perform on max items at any time (Es default is 5). 17 | return find(service, findParams).then((results) => { 18 | // The results might be paginated. 19 | const found = Array.isArray(results) ? results : results.data; 20 | 21 | if (!found.length) { 22 | return found; 23 | } 24 | 25 | const bulkUpdateParams = Object.assign( 26 | { 27 | _source: filters.$select, 28 | body: found.reduce((result, item) => { 29 | const { _id, _parent: parent, _routing: routing } = item[service.meta]; 30 | const { doc } = getDocDescriptor(service, data); 31 | 32 | result.push({ update: { _id, routing: routing || parent } }); 33 | result.push({ doc }); 34 | 35 | return result; 36 | }, []), 37 | }, 38 | service.esParams 39 | ); 40 | 41 | return service.Model.bulk(bulkUpdateParams).then((result) => 42 | mapBulk(result.items, service.id, service.meta, service.join) 43 | ); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /src/methods/patch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { getDocDescriptor, getQueryLength, mapPatch } from '../utils/index.js'; 4 | 5 | export function patch(service, id, data, params) { 6 | const { get } = service.core; 7 | const { filters, query } = service.filterQuery(params); 8 | const { routing } = getDocDescriptor(service, query); 9 | const { doc } = getDocDescriptor(service, data); 10 | const updateParams = { 11 | id: String(id), 12 | routing, 13 | body: { doc }, 14 | _source: filters.$select, 15 | ...service.esParams, 16 | }; 17 | 18 | const queryPromise = 19 | getQueryLength(service, query) >= 1 ? get(service, updateParams.id, params) : Promise.resolve(); 20 | 21 | return queryPromise 22 | .then(() => service.Model.update(updateParams)) 23 | .then((result) => mapPatch(result, service.id, service.meta, service.join)); 24 | } 25 | -------------------------------------------------------------------------------- /src/methods/raw.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { errors } from "@feathersjs/errors"; 4 | 5 | export function raw(service, method, params) { 6 | // handle client methods like indices.create 7 | const [primaryMethod, secondaryMethod] = method.split("."); 8 | 9 | if (typeof service.Model[primaryMethod] === "undefined") { 10 | return Promise.reject( 11 | errors.MethodNotAllowed(`There is no query method ${primaryMethod}.`) 12 | ); 13 | } 14 | 15 | if ( 16 | secondaryMethod && 17 | typeof service.Model[primaryMethod][secondaryMethod] === "undefined" 18 | ) { 19 | return Promise.reject( 20 | errors.MethodNotAllowed( 21 | `There is no query method ${primaryMethod}.${secondaryMethod}.` 22 | ) 23 | ); 24 | } 25 | 26 | return typeof service.Model[primaryMethod][secondaryMethod] === "function" 27 | ? service.Model[primaryMethod][secondaryMethod](params) 28 | : service.Model[primaryMethod](params); 29 | } 30 | -------------------------------------------------------------------------------- /src/methods/remove-bulk.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export function removeBulk(service, params) { 4 | const { find } = service.core; 5 | 6 | return find(service, params).then((results) => { 7 | const found = Array.isArray(results) ? results : results.data; 8 | 9 | if (!found.length) { 10 | return found; 11 | } 12 | 13 | const bulkRemoveParams = Object.assign( 14 | { 15 | body: found.map((item) => { 16 | const { 17 | _id, 18 | _parent: parent, 19 | _routing: routing, 20 | } = item[service.meta]; 21 | 22 | return { delete: { _id, routing: routing || parent } }; 23 | }), 24 | }, 25 | service.esParams 26 | ); 27 | 28 | return service.Model.bulk(bulkRemoveParams).then((results) => 29 | results.items 30 | .map((item, index) => 31 | item.delete.status === 200 ? found[index] : false 32 | ) 33 | .filter((item) => !!item) 34 | ); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /src/methods/remove.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { getDocDescriptor } from '../utils/index.js'; 4 | 5 | export function remove(service, id, params) { 6 | const { get } = service.core; 7 | const { query } = service.filterQuery(params); 8 | const { routing } = getDocDescriptor(service, query); 9 | const removeParams = Object.assign({ id: String(id), routing }, service.esParams); 10 | 11 | return get(service, id, params).then((result) => 12 | service.Model.delete(removeParams).then(() => result) 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/methods/update.js: -------------------------------------------------------------------------------- 1 | import { removeProps, getDocDescriptor } from '../utils/index.js'; 2 | 3 | function getUpdateParams(service, docDescriptor) { 4 | const { id, routing, doc } = docDescriptor; 5 | 6 | return { 7 | id: String(id), 8 | routing, 9 | body: doc, 10 | ...service.esParams, 11 | }; 12 | } 13 | 14 | export function update(service, id, data, params) { 15 | const { get } = service.core; 16 | const { query } = service.filterQuery(params); 17 | const docDescriptor = getDocDescriptor(service, data, query, { 18 | [service.id]: id, 19 | }); 20 | const updateParams = getUpdateParams(service, docDescriptor); 21 | 22 | if (params.upsert) { 23 | return service.Model.index(updateParams).then((result) => 24 | get(service, result._id, removeProps(params, 'upsert')) 25 | ); 26 | } 27 | 28 | const getParams = Object.assign(removeProps(params, 'query'), { 29 | query: Object.assign({ $select: false }, params.query), 30 | }); 31 | 32 | // The first get is a bit of an overhead, as per the spec we want to update only existing elements. 33 | return get(service, id, getParams) 34 | .then(() => service.Model.index(updateParams)) 35 | .then((result) => get(service, result._id, params)); 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/core.js: -------------------------------------------------------------------------------- 1 | import { errors } from '@feathersjs/errors'; 2 | 3 | export function getType(value) { 4 | const type = (Array.isArray(value) && 'array') || (value === null && 'null') || typeof value; 5 | 6 | return (type === 'number' && isNaN(value) && 'NaN') || type; 7 | } 8 | 9 | export function validateType(value, name, validators) { 10 | const type = getType(value); 11 | 12 | if (typeof validators === 'string') { 13 | validators = [validators]; 14 | } 15 | 16 | if (validators.indexOf(type) === -1) { 17 | throw new errors.BadRequest(`${name} should be one of ${validators.join(', ')}`); 18 | } 19 | 20 | return type; 21 | } 22 | 23 | export function removeProps(object, ...props) { 24 | const result = Object.assign({}, object); 25 | 26 | props.forEach((prop) => prop !== undefined && delete result[prop]); 27 | 28 | return result; 29 | } 30 | 31 | export function getDocDescriptor(service, data, ...supplementaryData) { 32 | const mergedData = supplementaryData.reduce((acc, dataObject) => Object.assign(acc, dataObject), { 33 | ...data, 34 | }); 35 | 36 | const id = mergedData[service.id] !== undefined ? String(mergedData[service.id]) : undefined; 37 | const parent = mergedData[service.parent] ? String(mergedData[service.parent]) : undefined; 38 | const routing = mergedData[service.routing] ? String(mergedData[service.routing]) : parent; 39 | const join = service.join && mergedData[service.join]; 40 | const doc = removeProps( 41 | data, 42 | service.meta, 43 | service.id, 44 | service.parent, 45 | service.routing, 46 | service.join 47 | ); 48 | 49 | return { id, parent, routing, join, doc }; 50 | } 51 | 52 | export function getCompatVersion(allVersions, curVersion, defVersion = '5.0') { 53 | const curVersionNum = Number(curVersion); 54 | const prevVersionsNum = allVersions 55 | .map((version) => Number(version)) 56 | .filter((version) => version <= curVersionNum); 57 | 58 | if (!prevVersionsNum.length) { 59 | return defVersion; 60 | } 61 | 62 | return Math.max(...prevVersionsNum).toFixed(1); 63 | } 64 | 65 | export function getCompatProp(versionMap, curVersion) { 66 | return versionMap[getCompatVersion(Object.keys(versionMap), curVersion)]; 67 | } 68 | 69 | export function getQueryLength(service, query) { 70 | return Object.keys(removeProps(query, service.routing, service.parent)).length; 71 | } 72 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { removeProps } from './core.js' 4 | 5 | export * from './core.js' 6 | export * from './parse-query.js' 7 | 8 | export function mapFind(results, idProp, metaProp, joinProp, filters, hasPagination) { 9 | const data = results.hits.hits.map((result) => mapGet(result, idProp, metaProp, joinProp)) 10 | 11 | if (hasPagination) { 12 | const total = typeof results.hits.total === 'object' ? results.hits.total.value : results.hits.total 13 | 14 | return { 15 | total, 16 | skip: filters.$skip, 17 | limit: filters.$limit, 18 | data 19 | } 20 | } 21 | 22 | return data 23 | } 24 | 25 | export function mapGet(item, idProp, metaProp, joinProp) { 26 | return mapItem(item, idProp, metaProp, joinProp) 27 | } 28 | 29 | export function mapPatch(item, idProp, metaProp, joinProp) { 30 | const normalizedItem = removeProps(item, 'get') 31 | 32 | normalizedItem._source = item.get && item.get._source 33 | 34 | return mapItem(normalizedItem, idProp, metaProp, joinProp) 35 | } 36 | 37 | export function mapBulk(items, idProp, metaProp, joinProp) { 38 | return items.map((item) => { 39 | if (item.update) { 40 | return mapPatch(item.update, idProp, metaProp, joinProp) 41 | } 42 | 43 | return mapItem(item.create || item.index || item.delete, idProp, metaProp, joinProp) 44 | }) 45 | } 46 | 47 | export function mapItem(item, idProp, metaProp, joinProp) { 48 | const meta = removeProps(item, '_source') 49 | const result = Object.assign({ [metaProp]: meta }, item._source) 50 | 51 | if (meta._id !== undefined) { 52 | result[idProp] = meta._id 53 | } 54 | 55 | if (joinProp && result[joinProp] && typeof result[joinProp] === 'object') { 56 | const { parent, name } = result[joinProp] 57 | 58 | result[metaProp]._parent = parent 59 | result[joinProp] = name 60 | } 61 | 62 | return result 63 | } 64 | -------------------------------------------------------------------------------- /src/utils/parse-query.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { removeProps, getType, validateType } from './core.js'; 4 | 5 | const queryCriteriaMap = { 6 | $nin: 'must_not.terms', 7 | $in: 'filter.terms', 8 | $gt: 'filter.range.gt', 9 | $gte: 'filter.range.gte', 10 | $lt: 'filter.range.lt', 11 | $lte: 'filter.range.lte', 12 | $ne: 'must_not.term', 13 | $prefix: 'filter.prefix', 14 | $wildcard: 'filter.wildcard', 15 | $regexp: 'filter.regexp', 16 | $match: 'must.match', 17 | $phrase: 'must.match_phrase', 18 | $phrase_prefix: 'must.match_phrase_prefix', 19 | }; 20 | 21 | const specialQueryHandlers = { 22 | $or, 23 | $and, 24 | $all, 25 | $sqs, 26 | $nested, 27 | $exists: (...args) => $existsOr$missing('must', ...args), 28 | $missing: (...args) => $existsOr$missing('must_not', ...args), 29 | $child: (...args) => $childOr$parent('$child', ...args), 30 | $parent: (...args) => $childOr$parent('$parent', ...args), 31 | }; 32 | 33 | function $or(value, esQuery, idProp) { 34 | validateType(value, '$or', 'array'); 35 | 36 | esQuery.should = esQuery.should || []; 37 | esQuery.should.push( 38 | ...value 39 | .map((subQuery) => parseQuery(subQuery, idProp)) 40 | .filter((parsed) => !!parsed) 41 | .map((parsed) => ({ bool: parsed })) 42 | ); 43 | esQuery.minimum_should_match = 1; 44 | 45 | return esQuery; 46 | } 47 | 48 | function $all(value, esQuery) { 49 | if (!value) { 50 | return esQuery; 51 | } 52 | 53 | esQuery.must = esQuery.must || []; 54 | esQuery.must.push({ match_all: {} }); 55 | 56 | return esQuery; 57 | } 58 | 59 | function $and(value, esQuery, idProp) { 60 | validateType(value, '$and', 'array'); 61 | 62 | value 63 | .map((subQuery) => parseQuery(subQuery, idProp)) 64 | .filter((parsed) => !!parsed) 65 | .forEach((parsed) => { 66 | Object.keys(parsed).forEach((section) => { 67 | esQuery[section] = esQuery[section] || []; 68 | esQuery[section].push(...parsed[section]); 69 | }); 70 | }); 71 | 72 | return esQuery; 73 | } 74 | 75 | function $sqs(value, esQuery) { 76 | if (value === null || value === undefined) { 77 | return esQuery; 78 | } 79 | 80 | validateType(value, '$sqs', 'object'); 81 | validateType(value.$fields, '$sqs.$fields', 'array'); 82 | validateType(value.$query, '$sqs.$query', 'string'); 83 | 84 | if (value.$operator) { 85 | validateType(value.$operator, '$sqs.$operator', 'string'); 86 | } 87 | 88 | esQuery.must = esQuery.must || []; 89 | esQuery.must.push({ 90 | simple_query_string: { 91 | fields: value.$fields, 92 | query: value.$query, 93 | default_operator: value.$operator || 'or', 94 | }, 95 | }); 96 | 97 | return esQuery; 98 | } 99 | 100 | function $childOr$parent(queryType, value, esQuery) { 101 | const queryName = queryType === '$child' ? 'has_child' : 'has_parent'; 102 | const typeName = queryType === '$child' ? 'type' : 'parent_type'; 103 | 104 | if (value === null || value === undefined) { 105 | return esQuery; 106 | } 107 | 108 | validateType(value, queryType, 'object'); 109 | validateType(value.$type, `${queryType}.$type`, 'string'); 110 | 111 | const subQuery = parseQuery(removeProps(value, '$type')); 112 | 113 | if (!subQuery) { 114 | return esQuery; 115 | } 116 | 117 | esQuery.must = esQuery.must || []; 118 | esQuery.must.push({ 119 | [queryName]: { 120 | [typeName]: value.$type, 121 | query: { 122 | bool: subQuery, 123 | }, 124 | }, 125 | }); 126 | 127 | return esQuery; 128 | } 129 | 130 | function $nested(value, esQuery) { 131 | if (value === null || value === undefined) { 132 | return esQuery; 133 | } 134 | 135 | validateType(value, '$nested', 'object'); 136 | validateType(value.$path, '$nested.$path', 'string'); 137 | 138 | const subQuery = parseQuery(removeProps(value, '$path')); 139 | 140 | if (!subQuery) { 141 | return esQuery; 142 | } 143 | 144 | esQuery.must = esQuery.must || []; 145 | esQuery.must.push({ 146 | nested: { 147 | path: value.$path, 148 | query: { 149 | bool: subQuery, 150 | }, 151 | }, 152 | }); 153 | 154 | return esQuery; 155 | } 156 | 157 | function $existsOr$missing(clause, value, esQuery) { 158 | if (value === null || value === undefined) { 159 | return esQuery; 160 | } 161 | 162 | validateType(value, `${clause}.exists`, 'array'); 163 | 164 | const values = value.map((val, i) => { 165 | validateType(val, `${clause}.exists[${i}]`, 'string'); 166 | return { exists: { field: val } }; 167 | }); 168 | 169 | esQuery[clause] = (esQuery[clause] || []).concat(values); 170 | 171 | return esQuery; 172 | } 173 | 174 | export function parseQuery(query, idProp) { 175 | validateType(query, 'query', ['object', 'null', 'undefined']); 176 | 177 | if (query === null || query === undefined) { 178 | return null; 179 | } 180 | 181 | const bool = Object.entries(query).reduce((result, [key, value]) => { 182 | const type = getType(value); 183 | 184 | // The search can be done by ids as well. 185 | // We need to translate the id prop used by the app to the id prop used by Es. 186 | if (key === idProp) { 187 | key = '_id'; 188 | } 189 | 190 | if (specialQueryHandlers[key]) { 191 | return specialQueryHandlers[key](value, result, idProp); 192 | } 193 | 194 | validateType(value, key, ['number', 'string', 'boolean', 'undefined', 'object', 'array']); 195 | // The value is not an object, which means it's supposed to be a primitive or an array. 196 | // We need add simple filter[{term: {}}] query. 197 | if (type !== 'object') { 198 | result.filter = result.filter || []; 199 | if (type === 'array') { 200 | value.forEach((value) => result.filter.push({ term: { [key]: value } })); 201 | } else { 202 | result.filter.push({ term: { [key]: value } }); 203 | } 204 | 205 | return result; 206 | } 207 | 208 | // In this case the key is not $or and value is an object, 209 | // so we are most probably dealing with criteria. 210 | Object.keys(value) 211 | .filter((criterion) => queryCriteriaMap[criterion]) 212 | .forEach((criterion) => { 213 | const [section, term, operand] = queryCriteriaMap[criterion].split('.'); 214 | 215 | result[section] = result[section] || []; 216 | result[section].push({ 217 | [term]: { 218 | [key]: operand ? { [operand]: value[criterion] } : value[criterion], 219 | }, 220 | }); 221 | }); 222 | 223 | return result; 224 | }, {}); 225 | 226 | if (!Object.keys(bool).length) { 227 | return null; 228 | } 229 | 230 | return bool; 231 | } 232 | -------------------------------------------------------------------------------- /test-utils/schema-5.0.js: -------------------------------------------------------------------------------- 1 | const schema = [ 2 | { 3 | index: 'test', 4 | body: { 5 | mappings: { 6 | people: { 7 | properties: { 8 | name: { type: 'keyword' }, 9 | tags: { type: 'keyword' }, 10 | addresses: { 11 | type: 'nested', 12 | properties: { 13 | street: { type: 'keyword' } 14 | } 15 | }, 16 | phone: { type: 'keyword' } 17 | } 18 | }, 19 | aka: { 20 | _parent: { 21 | type: 'people' 22 | } 23 | }, 24 | todos: { 25 | properties: { 26 | text: { type: 'keyword' } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | ]; 33 | 34 | module.exports = schema; 35 | -------------------------------------------------------------------------------- /test-utils/schema-6.0.js: -------------------------------------------------------------------------------- 1 | const schema = [ 2 | { 3 | index: 'test-people', 4 | body: { 5 | mappings: { 6 | doc: { 7 | properties: { 8 | name: { type: 'keyword' }, 9 | tags: { type: 'keyword' }, 10 | addresses: { 11 | type: 'nested', 12 | properties: { 13 | street: { type: 'keyword' } 14 | } 15 | }, 16 | phone: { type: 'keyword' }, 17 | aka: { 18 | type: 'join', 19 | relations: { 20 | real: 'alias' 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | }, 28 | { 29 | index: 'test-todos', 30 | body: { 31 | mappings: { 32 | doc: { 33 | properties: { 34 | text: { type: 'keyword' } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | ]; 41 | 42 | module.exports = schema; 43 | -------------------------------------------------------------------------------- /test-utils/schema-7.0.js: -------------------------------------------------------------------------------- 1 | const schema = [ 2 | { 3 | index: 'test-people', 4 | body: { 5 | mappings: { 6 | properties: { 7 | name: { type: 'keyword' }, 8 | tags: { type: 'keyword' }, 9 | addresses: { 10 | type: 'nested', 11 | properties: { 12 | street: { type: 'keyword' } 13 | } 14 | }, 15 | phone: { type: 'keyword' }, 16 | aka: { 17 | type: 'join', 18 | relations: { 19 | real: 'alias' 20 | } 21 | } 22 | } 23 | } 24 | } 25 | }, 26 | { 27 | index: 'test-todos', 28 | body: { 29 | mappings: { 30 | properties: { 31 | text: { type: 'keyword' } 32 | } 33 | } 34 | } 35 | } 36 | ]; 37 | 38 | module.exports = schema; 39 | -------------------------------------------------------------------------------- /test-utils/test-db.js: -------------------------------------------------------------------------------- 1 | const elasticsearch = require("elasticsearch"); 2 | const { getCompatVersion, getCompatProp } = require("../src/utils/core"); 3 | 4 | let apiVersion = null; 5 | let client = null; 6 | const schemaVersions = ["5.0", "6.0", "7.0"]; 7 | 8 | const compatVersion = getCompatVersion(schemaVersions, getApiVersion()); 9 | const compatSchema = require(`./schema-${compatVersion}`); 10 | 11 | function getServiceConfig(serviceName) { 12 | const configs = { 13 | "5.0": { 14 | index: "test", 15 | type: serviceName, 16 | }, 17 | "6.0": { 18 | index: serviceName === "aka" ? "test-people" : `test-${serviceName}`, 19 | type: "doc", 20 | }, 21 | "7.0": { 22 | index: serviceName === "aka" ? "test-people" : `test-${serviceName}`, 23 | type: "_doc", 24 | }, 25 | }; 26 | 27 | return Object.assign( 28 | { refresh: true }, 29 | getCompatProp(configs, getApiVersion()) 30 | ); 31 | } 32 | 33 | function getApiVersion() { 34 | if (!apiVersion) { 35 | const esVersion = process.env.ES_VERSION || "5.0.0"; 36 | const [major, minor] = esVersion.split(".").slice(0, 2); 37 | 38 | // elasticsearch client 15.5 does not support api 5.0 - 5.5 39 | apiVersion = +major === 5 && +minor < 6 ? "5.6" : `${major}.${minor}`; 40 | } 41 | 42 | return apiVersion; 43 | } 44 | 45 | function getClient() { 46 | if (!client) { 47 | client = new elasticsearch.Client({ 48 | host: "localhost:9200", 49 | apiVersion: getApiVersion(), 50 | }); 51 | } 52 | 53 | return client; 54 | } 55 | 56 | function deleteSchema() { 57 | const index = compatSchema.map((indexSetup) => indexSetup.index); 58 | 59 | return getClient() 60 | .indices.delete({ index }) 61 | .catch((err) => err.status !== 404 && Promise.reject(err)); 62 | } 63 | 64 | function createSchema() { 65 | return compatSchema.reduce( 66 | (result, indexSetup) => 67 | result.then(() => getClient().indices.create(indexSetup)), 68 | Promise.resolve() 69 | ); 70 | } 71 | 72 | function resetSchema() { 73 | return deleteSchema().then(createSchema); 74 | } 75 | 76 | module.exports = { 77 | getApiVersion, 78 | getClient, 79 | getServiceConfig, 80 | resetSchema, 81 | deleteSchema, 82 | createSchema, 83 | }; 84 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | mocha: true 4 | }, 5 | rules: { 6 | 'no-unused-expressions': 'off' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /test/core/create.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const errors = require('@feathersjs/errors'); 3 | 4 | function create (app, serviceName) { 5 | describe('create()', () => { 6 | it('should create an item with provided id', () => { 7 | return app.service(serviceName) 8 | .create({ name: 'Bob', id: 'BobId' }) 9 | .then(result => { 10 | expect(result.name).to.equal('Bob'); 11 | expect(result.id).to.equal('BobId'); 12 | 13 | return app.service(serviceName).get('BobId'); 14 | }) 15 | .then(result => { 16 | expect(result.name).to.equal('Bob'); 17 | 18 | return app.service(serviceName).remove('BobId'); 19 | }); 20 | }); 21 | 22 | it('should throw Conflict when trying to create an element with existing id', () => { 23 | return app.service(serviceName) 24 | .create({ name: 'Bob', id: 'BobId' }) 25 | .then(() => app.service(serviceName).create({ name: 'Bob', id: 'BobId' })) 26 | .then(() => { throw new Error('Should never get here'); }) 27 | .catch(error => { 28 | expect(error instanceof errors.Conflict).to.be.true; 29 | 30 | return app.service(serviceName).remove('BobId'); 31 | }); 32 | }); 33 | 34 | it('should update when trying to create an element with existing id using upsert', () => { 35 | const service = app.service(serviceName); 36 | 37 | return service 38 | .create({ name: 'Bob', id: 'BobId' }) 39 | .then(() => service.create({ name: 'Box', id: 'BobId' }, { upsert: true })) 40 | .then(result => { 41 | expect(result.name).to.equal('Box'); 42 | expect(result.id).to.equal('BobId'); 43 | 44 | return service.get('BobId'); 45 | }) 46 | .then(result => { 47 | expect(result.name).to.equal('Box'); 48 | 49 | return service.remove('BobId'); 50 | }); 51 | }); 52 | 53 | it('should create items with provided ids (bulk)', () => { 54 | return app.service(serviceName) 55 | .create([ 56 | { name: 'Cal', id: 'CalId' }, 57 | { name: 'Max', id: 'MaxId' } 58 | ]) 59 | .then(results => { 60 | expect(results[0].name).to.equal('Cal'); 61 | expect(results[1].name).to.equal('Max'); 62 | 63 | return app.service(serviceName).find({ 64 | query: { 65 | id: { $in: ['CalId', 'MaxId'] } 66 | } 67 | }); 68 | }) 69 | .then(results => { 70 | expect(results[0].name).to.equal('Cal'); 71 | expect(results[1].name).to.equal('Max'); 72 | 73 | return app.service(serviceName).remove( 74 | null, 75 | { query: { id: { $in: ['CalId', 'MaxId'] } } } 76 | ); 77 | }); 78 | }); 79 | 80 | it('should return created items in the same order as requested ones along with the errors (bulk)', () => { 81 | return app.service(serviceName) 82 | .create([ 83 | { name: 'Catnis', id: 'CatnisId' }, 84 | { name: 'Catnis', id: 'CatnisId' }, 85 | { name: 'Mark', id: 'MarkId' } 86 | ]) 87 | .then(results => { 88 | expect(results[0].name).to.equal('Catnis'); 89 | expect(results[1]._meta.status).to.equal(409); 90 | expect(results[2].name).to.equal('Mark'); 91 | 92 | return app.service(serviceName).remove( 93 | null, 94 | { query: { id: { $in: ['CatnisId', 'MarkId'] } } } 95 | ); 96 | }); 97 | }); 98 | 99 | it('should create an item with provided parent', () => { 100 | return app.service('aka') 101 | .create({ name: 'Bobster McBobface', parent: 'bob', aka: 'alias' }) 102 | .then(result => { 103 | expect(result.name).to.equal('Bobster McBobface'); 104 | expect(result._meta._parent).to.equal('bob'); 105 | return app.service('aka').remove( 106 | result.id, 107 | { query: { parent: 'bob' } } 108 | ); 109 | }); 110 | }); 111 | 112 | it('should create items with provided parents (bulk)', () => { 113 | return app.service('aka') 114 | .create([ 115 | { name: 'Bobster', parent: 'bob', id: 'bobAka', aka: 'alias' }, 116 | { name: 'Sunshine', parent: 'moody', aka: 'alias' } 117 | ]) 118 | .then(results => { 119 | const [bobAka, moodyAka] = results; 120 | 121 | expect(results.length).to.equal(2); 122 | expect(bobAka.name).to.equal('Bobster'); 123 | expect(bobAka._meta._parent).to.equal('bob'); 124 | expect(moodyAka.name).to.equal('Sunshine'); 125 | expect(moodyAka._meta._parent).to.equal('moody'); 126 | 127 | return app.service('aka').remove( 128 | null, 129 | { query: { id: { $in: [bobAka.id, moodyAka.id] } } } 130 | ); 131 | }); 132 | }); 133 | 134 | it('should return only raw response if no items were created (bulk)', () => { 135 | return app.service(serviceName) 136 | .create([ 137 | { name: { first: 'Douglas' }, id: 'wrongDouglas' }, 138 | { name: { first: 'Bob' }, id: 'wrongBob' } 139 | ]) 140 | .then(results => { 141 | expect(results).to.have.lengthOf(2); 142 | expect(results).to.have.nested.property('[0].id', 'wrongDouglas'); 143 | expect(results).to.have.nested.property('[0]._meta.error'); 144 | expect(results).to.have.nested.property('[0]._meta.status', 400); 145 | expect(results).to.have.nested.property('[1].id', 'wrongBob'); 146 | expect(results).to.have.nested.property('[1]._meta.error'); 147 | expect(results).to.have.nested.property('[1]._meta.status', 400); 148 | }); 149 | }); 150 | }); 151 | } 152 | 153 | module.exports = create; 154 | -------------------------------------------------------------------------------- /test/core/find.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const { getCompatProp } = require("../../src/utils"); 3 | 4 | function find(app, serviceName, esVersion) { 5 | describe("find()", () => { 6 | it("should return empty array if no results found", () => { 7 | return app 8 | .service(serviceName) 9 | .find({ query: { id: "better-luck-next-time" } }) 10 | .then((results) => { 11 | expect(results).to.be.an("array").and.be.empty; 12 | }); 13 | }); 14 | 15 | it("should return empty paginated results if no results found", () => { 16 | return app 17 | .service(serviceName) 18 | .find({ 19 | query: { id: "better-luck-next-time" }, 20 | paginate: { default: 10 }, 21 | }) 22 | .then((results) => { 23 | expect(results.total).to.equal(0); 24 | expect(results.data).to.be.an("array").and.be.empty; 25 | }); 26 | }); 27 | 28 | it("should filter results by array parameter", () => { 29 | return app 30 | .service(serviceName) 31 | .find({ 32 | query: { tags: ["legend", "javascript"] }, 33 | }) 34 | .then((results) => { 35 | expect(results.length).to.equal(1); 36 | expect(results[0].name).to.equal("Douglas"); 37 | }); 38 | }); 39 | 40 | describe("special filters", () => { 41 | it("can $prefix", () => { 42 | return app 43 | .service(serviceName) 44 | .find({ 45 | query: { name: { $prefix: "B" } }, 46 | }) 47 | .then((results) => { 48 | expect(results.length).to.equal(1); 49 | expect(results[0].name).to.equal("Bob"); 50 | }); 51 | }); 52 | 53 | it("can $wildcard", () => { 54 | return app 55 | .service(serviceName) 56 | .find({ 57 | query: { name: { $wildcard: "B*b" } }, 58 | }) 59 | .then((results) => { 60 | expect(results.length).to.equal(1); 61 | expect(results[0].name).to.equal("Bob"); 62 | }); 63 | }); 64 | 65 | it("can $regexp", () => { 66 | return app 67 | .service(serviceName) 68 | .find({ 69 | query: { name: { $regexp: "Bo[xb]" } }, 70 | }) 71 | .then((results) => { 72 | expect(results.length).to.equal(1); 73 | expect(results[0].name).to.equal("Bob"); 74 | }); 75 | }); 76 | 77 | it("can $all", () => { 78 | const expectedLength = getCompatProp( 79 | { 80 | "5.0": 3, 81 | "6.0": 6, 82 | }, 83 | esVersion 84 | ); 85 | 86 | return app 87 | .service(serviceName) 88 | .find({ 89 | query: { $all: true }, 90 | }) 91 | .then((results) => { 92 | expect(results.length).to.equal(expectedLength); 93 | }); 94 | }); 95 | 96 | it("can $match", () => { 97 | return app 98 | .service(serviceName) 99 | .find({ 100 | query: { bio: { $match: "I like JavaScript" } }, 101 | }) 102 | .then((results) => { 103 | expect(results.length).to.equal(2); 104 | }); 105 | }); 106 | 107 | it("can $phrase", () => { 108 | return app 109 | .service(serviceName) 110 | .find({ 111 | query: { bio: { $phrase: "I like JavaScript" } }, 112 | }) 113 | .then((results) => { 114 | expect(results.length).to.equal(1); 115 | expect(results[0].name).to.equal("Bob"); 116 | }); 117 | }); 118 | 119 | it("can $phrase_prefix", () => { 120 | return app 121 | .service(serviceName) 122 | .find({ 123 | query: { bio: { $phrase_prefix: "I like JavaS" } }, 124 | }) 125 | .then((results) => { 126 | expect(results.length).to.equal(1); 127 | expect(results[0].name).to.equal("Bob"); 128 | }); 129 | }); 130 | 131 | it("can $or correctly with other filters", () => { 132 | return app 133 | .service(serviceName) 134 | .find({ 135 | query: { 136 | $or: [{ name: "Moody" }, { name: "Douglas" }], 137 | bio: { $match: "JavaScript legend" }, 138 | }, 139 | }) 140 | .then((results) => { 141 | expect(results.length).to.equal(1); 142 | expect(results[0].name).to.equal("Douglas"); 143 | }); 144 | }); 145 | 146 | it("can $and", () => { 147 | return app 148 | .service(serviceName) 149 | .find({ 150 | query: { 151 | $sort: { name: 1 }, 152 | $and: [{ tags: "javascript" }, { tags: "programmer" }], 153 | }, 154 | }) 155 | .then((results) => { 156 | expect(results.length).to.equal(2); 157 | expect(results[0].name).to.equal("Bob"); 158 | expect(results[1].name).to.equal("Douglas"); 159 | }); 160 | }); 161 | 162 | it("can $sqs (simple_query_string)", () => { 163 | return app 164 | .service(serviceName) 165 | .find({ 166 | query: { 167 | $sort: { name: 1 }, 168 | $sqs: { 169 | $fields: ["bio", "name^5"], 170 | $query: "+like -javascript", 171 | $operator: "and", 172 | }, 173 | }, 174 | }) 175 | .then((results) => { 176 | expect(results.length).to.equal(1); 177 | expect(results[0].name).to.equal("Moody"); 178 | }); 179 | }); 180 | 181 | it("can $sqs (simple_query_string) with other filters", () => { 182 | return app 183 | .service(serviceName) 184 | .find({ 185 | query: { 186 | $sort: { name: 1 }, 187 | $and: [{ tags: "javascript" }], 188 | $sqs: { 189 | $fields: ["bio"], 190 | $query: "-legend", 191 | }, 192 | }, 193 | }) 194 | .then((results) => { 195 | expect(results.length).to.equal(1); 196 | expect(results[0].name).to.equal("Bob"); 197 | }); 198 | }); 199 | 200 | it("can $child", () => { 201 | const types = { 202 | "5.0": "aka", 203 | "6.0": "alias", 204 | }; 205 | 206 | return app 207 | .service(serviceName) 208 | .find({ 209 | query: { 210 | $sort: { name: 1 }, 211 | $child: { 212 | $type: getCompatProp(types, esVersion), 213 | name: "Teacher", 214 | }, 215 | }, 216 | }) 217 | .then((results) => { 218 | expect(results.length).to.equal(2); 219 | expect(results[0].name).to.equal("Douglas"); 220 | expect(results[1].name).to.equal("Moody"); 221 | }); 222 | }); 223 | 224 | it("can $parent", () => { 225 | const types = { 226 | "5.0": "people", 227 | "6.0": "real", 228 | }; 229 | 230 | return app 231 | .service("aka") 232 | .find({ 233 | query: { 234 | $sort: { name: 1 }, 235 | $parent: { 236 | $type: getCompatProp(types, esVersion), 237 | name: "Douglas", 238 | }, 239 | }, 240 | }) 241 | .then((results) => { 242 | expect(results.length).to.equal(2); 243 | expect(results[0].name).to.equal("Teacher"); 244 | expect(results[1].name).to.equal("The Master"); 245 | }); 246 | }); 247 | 248 | it("can $nested", () => { 249 | return app 250 | .service(serviceName) 251 | .find({ 252 | query: { 253 | $nested: { 254 | $path: "addresses", 255 | "addresses.street": "1 The Road", 256 | }, 257 | }, 258 | }) 259 | .then((results) => { 260 | expect(results.length).to.equal(1); 261 | expect(results[0].name).to.equal("Bob"); 262 | }); 263 | }); 264 | 265 | it("can $exists", () => { 266 | return app 267 | .service(serviceName) 268 | .find({ 269 | query: { 270 | $exists: ["phone"], 271 | }, 272 | }) 273 | .then((results) => { 274 | expect(results.length).to.equal(1); 275 | expect(results[0].name).to.equal("Douglas"); 276 | }); 277 | }); 278 | 279 | it("can $missing", () => { 280 | const expectedLength = getCompatProp( 281 | { 282 | "5.0": 2, 283 | "6.0": 5, 284 | }, 285 | esVersion 286 | ); 287 | 288 | return app 289 | .service(serviceName) 290 | .find({ 291 | query: { 292 | $sort: { name: 1 }, 293 | $missing: ["phone"], 294 | }, 295 | }) 296 | .then((results) => { 297 | expect(results.length).to.equal(expectedLength); 298 | expect(results[0].name).to.equal("Bob"); 299 | expect(results[1].name).to.equal("Moody"); 300 | }); 301 | }); 302 | }); 303 | }); 304 | } 305 | 306 | module.exports = find; 307 | -------------------------------------------------------------------------------- /test/core/get.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | 3 | function get (app, serviceName) { 4 | describe('get()', () => { 5 | it('should get an item with specified parent', () => { 6 | return app.service('aka') 7 | .get('douglasAka', { query: { parent: 'douglas' } }) 8 | .then(result => { 9 | expect(result.name).to.equal('The Master'); 10 | }); 11 | }); 12 | }); 13 | } 14 | 15 | module.exports = get; 16 | -------------------------------------------------------------------------------- /test/core/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const find = require('./find'); 4 | const get = require('./get'); 5 | const create = require('./create'); 6 | const patch = require('./patch'); 7 | const remove = require('./remove'); 8 | const update = require('./update'); 9 | const raw = require('./raw'); 10 | 11 | module.exports = { 12 | find, 13 | get, 14 | create, 15 | patch, 16 | remove, 17 | update, 18 | raw 19 | }; 20 | -------------------------------------------------------------------------------- /test/core/patch.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const sinon = require("sinon"); 3 | const { getCompatProp } = require("../../src/utils"); 4 | 5 | function patch(app, serviceName, esVersion) { 6 | describe("patch()", () => { 7 | it("should return empty array if no items have been found (bulk)", () => { 8 | return app 9 | .service(serviceName) 10 | .patch( 11 | null, 12 | { name: "John" }, 13 | { query: { id: "better-luck-next-time" } } 14 | ) 15 | .then((results) => { 16 | expect(results).to.be.an("array").and.be.empty; 17 | }); 18 | }); 19 | 20 | it("should return only raw response if no items were patched (bulk)", () => { 21 | const queries = { 22 | "5.0": { $all: true, $sort: { name: 1 } }, 23 | "6.0": { aka: "real", $sort: { name: 1 } }, 24 | }; 25 | 26 | return app 27 | .service(serviceName) 28 | .patch( 29 | null, 30 | { name: { first: "Douglas" } }, 31 | { query: getCompatProp(queries, esVersion) } 32 | ) 33 | .then((results) => { 34 | expect(results).to.have.lengthOf(3); 35 | expect(results).to.have.nested.property("[0].id", "bob"); 36 | expect(results).to.have.nested.property("[0]._meta.error"); 37 | expect(results).to.have.nested.property("[0]._meta.status", 400); 38 | expect(results).to.have.nested.property("[1].id", "douglas"); 39 | expect(results).to.have.nested.property("[1]._meta.error"); 40 | expect(results).to.have.nested.property("[1]._meta.status", 400); 41 | expect(results).to.have.nested.property("[2].id", "moody"); 42 | expect(results).to.have.nested.property("[2]._meta.error"); 43 | expect(results).to.have.nested.property("[2]._meta.status", 400); 44 | }); 45 | }); 46 | 47 | it("should return raw responses for items which were not patched (bulk)", () => { 48 | // It's easier to stub `bulk` then to try and make ES not to update selected item. 49 | const bulk = sinon.stub(app.service(serviceName).Model, "bulk").returns( 50 | Promise.resolve({ 51 | errors: true, 52 | items: [ 53 | { 54 | update: { 55 | _id: "bob", 56 | status: 200, 57 | get: { _source: { name: "Whatever" } }, 58 | }, 59 | }, 60 | { update: { _id: "douglas", status: 400, error: {} } }, 61 | { 62 | update: { 63 | _id: "moody", 64 | status: 200, 65 | get: { _source: { name: "Whatever" } }, 66 | }, 67 | }, 68 | ], 69 | }) 70 | ); 71 | 72 | return app 73 | .service(serviceName) 74 | .patch( 75 | null, 76 | { name: "Whatever" }, 77 | { query: { $all: true, $sort: { name: 1 } } } 78 | ) 79 | .then((results) => { 80 | expect(results).to.have.lengthOf(3); 81 | expect(results[0]).to.include({ name: "Whatever", id: "bob" }); 82 | expect(results[1]).to.have.property("id", "douglas"); 83 | expect(results[1]).to.have.nested.property("_meta.error"); 84 | expect(results[1]).to.have.nested.property("_meta.status", 400); 85 | expect(results[2]).to.include({ name: "Whatever", id: "moody" }); 86 | }) 87 | .catch() 88 | .then(() => bulk.restore()); 89 | }); 90 | 91 | it("should patch items selected with pagination (bulk)", () => { 92 | return app 93 | .service(serviceName) 94 | .create([ 95 | { name: "Patch me a", id: "patchMeA" }, 96 | { name: "Patch me b", id: "patchMeB" }, 97 | ]) 98 | .then(() => 99 | app.service(serviceName).patch( 100 | null, 101 | { name: "Patched" }, 102 | { 103 | query: { 104 | id: { $in: ["patchMeA", "patchMeB"] }, 105 | $sort: { name: 1 }, 106 | }, 107 | paginate: { default: 10, max: 10 }, 108 | } 109 | ) 110 | ) 111 | .then((results) => { 112 | expect(results).to.have.lengthOf(2); 113 | expect(results[0]).to.include({ name: "Patched", id: "patchMeA" }); 114 | expect(results[1]).to.include({ name: "Patched", id: "patchMeB" }); 115 | }) 116 | .then(() => 117 | app 118 | .service(serviceName) 119 | .remove(null, { query: { id: { $in: ["patchMeA", "patchMeB"] } } }) 120 | ); 121 | }); 122 | 123 | it("should patch an item with a specified parent", () => { 124 | return app 125 | .service("aka") 126 | .create({ 127 | name: "Bobby McBobface", 128 | parent: "bob", 129 | id: "bobAka", 130 | aka: "alias", 131 | }) 132 | .then(() => { 133 | return app 134 | .service("aka") 135 | .patch("bobAka", { name: "Bobster" }, { query: { parent: "bob" } }); 136 | }) 137 | .then((result) => { 138 | expect(result.name).to.equal("Bobster"); 139 | 140 | return app 141 | .service("aka") 142 | .remove("bobAka", { query: { parent: "bob" } }); 143 | }); 144 | }); 145 | 146 | it("should patch items which have parents (bulk)", () => { 147 | return app 148 | .service("aka") 149 | .create([ 150 | { name: "patchme", parent: "bob", aka: "alias" }, 151 | { name: "patchme", parent: "moody", aka: "alias" }, 152 | ]) 153 | .then(() => 154 | app 155 | .service("aka") 156 | .patch(null, { name: "patched" }, { query: { name: "patchme" } }) 157 | ) 158 | .then((results) => { 159 | expect(results.length).to.equal(2); 160 | expect(results[0].name).to.equal("patched"); 161 | expect(results[1].name).to.equal("patched"); 162 | 163 | return app 164 | .service("aka") 165 | .remove(null, { query: { name: "patched" } }); 166 | }); 167 | }); 168 | 169 | it("should patch with $select (bulk)", () => { 170 | return app 171 | .service(serviceName) 172 | .create([ 173 | { name: "patchme", age: 20, tags: ["uninteresting"], id: "patchMeA" }, 174 | { name: "patchme", age: 30, tags: ["boring"], id: "patchMeB" }, 175 | ]) 176 | .then(() => 177 | app.service(serviceName).patch( 178 | null, 179 | { name: "Patched" }, 180 | { 181 | query: { 182 | name: "patchme", 183 | $sort: { age: 1 }, 184 | $select: ["age"], 185 | }, 186 | paginate: { default: 10, max: 10 }, 187 | } 188 | ) 189 | ) 190 | .then((results) => { 191 | const [{ _meta: meta1, ...result1 }, { _meta: meta2, ...result2 }] = 192 | results; 193 | 194 | expect(results).to.have.lengthOf(2); 195 | expect(result1).to.deep.equal({ age: 20, id: "patchMeA" }); 196 | expect(result2).to.deep.equal({ age: 30, id: "patchMeB" }); 197 | }) 198 | .then(() => 199 | app 200 | .service(serviceName) 201 | .remove(null, { query: { id: { $in: ["patchMeA", "patchMeB"] } } }) 202 | ); 203 | }); 204 | }); 205 | } 206 | 207 | module.exports = patch; 208 | -------------------------------------------------------------------------------- /test/core/raw.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const { getCompatProp } = require("../../src/utils"); 3 | 4 | function raw(app, serviceName, esVersion) { 5 | describe("raw()", () => { 6 | it("should search documents in index with syntax term", () => { 7 | return app 8 | .service(serviceName) 9 | .raw("search", { 10 | size: 50, 11 | body: { 12 | query: { 13 | term: { 14 | name: "Bob", 15 | }, 16 | }, 17 | }, 18 | }) 19 | .then((results) => { 20 | expect(results.hits.hits.length).to.equal(1); 21 | }); 22 | }); 23 | 24 | it("should search documents in index with syntax match", () => { 25 | return app 26 | .service(serviceName) 27 | .raw("search", { 28 | size: 50, 29 | body: { 30 | query: { 31 | match: { 32 | bio: "javascript", 33 | }, 34 | }, 35 | }, 36 | }) 37 | .then((results) => { 38 | expect(results.hits.hits.length).to.equal(1); 39 | }); 40 | }); 41 | 42 | it("should show the mapping of index test", () => { 43 | const mappings = { 44 | "5.0": ["test.mappings.aka._parent.type", "people"], 45 | "6.0": ["test-people.mappings.doc.properties.aka.type", "join"], 46 | "7.0": ["test-people.mappings.properties.aka.type", "join"], 47 | }; 48 | 49 | return app 50 | .service("aka") 51 | .raw("indices.getMapping", {}) 52 | .then((results) => { 53 | expect(results).to.have.nested.property( 54 | ...getCompatProp(mappings, esVersion) 55 | ); 56 | }); 57 | }); 58 | 59 | it("should return a promise when the passed in method is not defined", () => { 60 | app 61 | .service(serviceName) 62 | .raw(undefined, {}) 63 | .catch((err) => { 64 | expect(err.message === "params.method must be defined."); 65 | }); 66 | }); 67 | 68 | it("should return a promise when service.method is not a function", () => { 69 | app 70 | .service(serviceName) 71 | .raw("notafunction", {}) 72 | .catch((err) => { 73 | expect(err.message === "There is no query method notafunction."); 74 | }); 75 | }); 76 | 77 | it("should return a promise when service.method.extention is not a function", () => { 78 | app 79 | .service(serviceName) 80 | .raw("indices.notafunction", {}) 81 | .catch((err) => { 82 | expect( 83 | err.message === "There is no query method indices.notafunction." 84 | ); 85 | }); 86 | }); 87 | }); 88 | } 89 | 90 | module.exports = raw; 91 | -------------------------------------------------------------------------------- /test/core/remove.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const sinon = require('sinon'); 3 | 4 | function remove (app, serviceName) { 5 | describe('remove()', () => { 6 | it('should return empty array if no items have been removed (bulk)', () => { 7 | return app.service(serviceName) 8 | .remove( 9 | null, 10 | { query: { id: 'better-luck-next-time' } } 11 | ) 12 | .then(results => { 13 | expect(results).to.be.an('array').and.be.empty; 14 | }); 15 | }); 16 | 17 | it('should remove an item with a specified parent', () => { 18 | return app.service('aka') 19 | .create({ name: 'Bobster', parent: 'bob', id: 'bobAka' }) 20 | .then(() => { 21 | return app.service('aka').remove( 22 | 'bobAka', 23 | { query: { parent: 'bob' } } 24 | ); 25 | }) 26 | .then(result => { 27 | expect(result.name).to.equal('Bobster'); 28 | }); 29 | }); 30 | 31 | it('should remove items which have a parent (bulk)', () => { 32 | return app.service('aka') 33 | .create([ 34 | { name: 'remove me', no: 1, parent: 'bob', aka: 'alias' }, 35 | { name: 'remove me', no: 2, parent: 'moody', aka: 'alias' } 36 | ]) 37 | .then(() => app.service('aka') 38 | .remove( 39 | null, 40 | { query: { name: 'remove me', $sort: { no: 1 } } } 41 | ) 42 | ) 43 | .then(results => { 44 | expect(results.length).to.equal(2); 45 | expect(results[0].name).to.equal('remove me'); 46 | expect(results[0]._meta._parent).to.equal('bob'); 47 | expect(results[1].name).to.equal('remove me'); 48 | expect(results[1]._meta._parent).to.equal('moody'); 49 | }); 50 | }); 51 | 52 | it('should remove items selected with pagination (bulk)', () => { 53 | return app.service(serviceName) 54 | .create([ 55 | { name: 'remove me', no: 1 }, 56 | { name: 'remove me', no: 2 } 57 | ]) 58 | .then(() => app.service(serviceName) 59 | .remove( 60 | null, 61 | { 62 | query: { name: 'remove me', $sort: { no: 1 } }, 63 | paginate: { default: 10, max: 10 } 64 | } 65 | ) 66 | ) 67 | .then(results => { 68 | expect(results).to.have.lengthOf(2); 69 | expect(results[0]).to.include({ name: 'remove me', no: 1 }); 70 | expect(results[1]).to.include({ name: 'remove me', no: 2 }); 71 | }); 72 | }); 73 | 74 | it('should return only removed items (bulk)', () => { 75 | // It's easier to stub `bulk` then to try and make ES not to delete selected item. 76 | const bulk = sinon.stub(app.service(serviceName).Model, 'bulk') 77 | .returns(Promise.resolve({ 78 | errors: true, 79 | items: [ 80 | { delete: { _id: 'bob', status: 200 } }, 81 | { delete: { _id: 'douglas', status: 400 } } 82 | ] 83 | })); 84 | 85 | return app.service(serviceName) 86 | .remove( 87 | null, 88 | { query: { $all: 1 } } 89 | ) 90 | .then(results => { 91 | expect(results).to.have.lengthOf(1); 92 | }) 93 | .catch().then(() => bulk.restore()); 94 | }); 95 | }); 96 | } 97 | 98 | module.exports = remove; 99 | -------------------------------------------------------------------------------- /test/core/update.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const errors = require('@feathersjs/errors'); 3 | 4 | function update (app, serviceName) { 5 | describe('update()', () => { 6 | it('should update an item with provided id', () => { 7 | const service = app.service(serviceName); 8 | 9 | return service 10 | .create({ name: 'Bob', id: 'BobId' }) 11 | .then(value => service.update('BobId', { name: 'Box', id: 'BobId' })) 12 | .then(result => { 13 | expect(result.name).to.equal('Box'); 14 | expect(result.id).to.equal('BobId'); 15 | 16 | return service.get('BobId'); 17 | }) 18 | .then(result => { 19 | expect(result.name).to.equal('Box'); 20 | 21 | return service.remove('BobId'); 22 | }); 23 | }); 24 | 25 | it('should throw NotFound when trying to update a non-existing element', () => { 26 | const service = app.service(serviceName); 27 | 28 | return service 29 | .update('BobId', { name: 'Bob', id: 'BobId' }) 30 | .then(() => { throw new Error('Should never get here'); }) 31 | .catch(error => { 32 | expect(error instanceof errors.NotFound).to.be.true; 33 | }); 34 | }); 35 | 36 | it('should create document when trying to update a non-existing element using upsert', () => { 37 | const service = app.service(serviceName); 38 | 39 | return service 40 | .update('BobId', { name: 'Bob', id: 'BobId' }, { upsert: true }) 41 | .then(result => { 42 | expect(result.name).to.equal('Bob'); 43 | expect(result.id).to.equal('BobId'); 44 | 45 | return service.get('BobId'); 46 | }) 47 | .then(result => { 48 | expect(result.name).to.equal('Bob'); 49 | 50 | return service.remove('BobId'); 51 | }); 52 | }); 53 | 54 | it('should update an item with specified parent', () => { 55 | return app.service('aka') 56 | .create({ name: 'Bobster', parent: 'bob', id: 'bobAka', aka: 'alias' }) 57 | .then(() => { 58 | return app.service('aka').update( 59 | 'bobAka', 60 | { name: 'Boberson' }, 61 | { query: { parent: 'bob' } } 62 | ); 63 | }) 64 | .then(result => { 65 | expect(result.name).to.equal('Boberson'); 66 | 67 | return app.service('aka').remove( 68 | 'bobAka', 69 | { query: { parent: 'bob' } } 70 | ); 71 | }); 72 | }); 73 | }); 74 | } 75 | 76 | module.exports = update; 77 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const adapterTests = require("@feathersjs/adapter-tests"); 3 | 4 | const feathers = require("@feathersjs/feathers"); 5 | const errors = require("@feathersjs/errors"); 6 | const service = require("../src"); 7 | const db = require("../test-utils/test-db"); 8 | const coreTests = require("./core"); 9 | const { getCompatProp } = require("../src/utils/core"); 10 | const testSuite = adapterTests([ 11 | ".options", 12 | ".events", 13 | "._get", 14 | "._find", 15 | "._create", 16 | "._update", 17 | "._patch", 18 | "._remove", 19 | ".get", 20 | ".get + $select", 21 | ".get + id + query", 22 | ".get + id + query id", 23 | ".get + NotFound", 24 | ".find", 25 | ".remove", 26 | ".remove + $select", 27 | ".remove + id + query", 28 | ".remove + multi", 29 | ".remove + id + query id", 30 | ".update", 31 | ".update + $select", 32 | ".update + id + query", 33 | ".update + NotFound", 34 | ".patch", 35 | ".patch + $select", 36 | ".patch + id + query", 37 | ".patch multiple", 38 | ".patch multi query", 39 | ".patch + NotFound", 40 | ".create", 41 | ".create + $select", 42 | ".create multi", 43 | "internal .find", 44 | "internal .get", 45 | "internal .create", 46 | "internal .update", 47 | "internal .patch", 48 | "internal .remove", 49 | ".find + equal", 50 | ".find + equal multiple", 51 | ".find + $sort", 52 | ".find + $sort + string", 53 | ".find + $limit", 54 | ".find + $limit 0", 55 | ".find + $skip", 56 | ".find + $select", 57 | ".find + $or", 58 | ".find + $in", 59 | ".find + $nin", 60 | ".find + $lt", 61 | ".find + $lte", 62 | ".find + $gt", 63 | ".find + $gte", 64 | ".find + $ne", 65 | ".find + $gt + $lt + $sort", 66 | ".find + $or nested + $sort", 67 | ".find + paginate", 68 | ".find + paginate + $limit + $skip", 69 | ".find + paginate + $limit 0", 70 | ".find + paginate + params", 71 | ".remove + id + query id", 72 | ".update + id + query id", 73 | ".patch + id + query id", 74 | ]); 75 | 76 | describe("Elasticsearch Service", () => { 77 | const app = feathers(); 78 | const serviceName = "people"; 79 | const esVersion = db.getApiVersion(); 80 | 81 | before(() => { 82 | return db.resetSchema().then(() => { 83 | app.use( 84 | `/${serviceName}`, 85 | service({ 86 | Model: db.getClient(), 87 | events: ["testing"], 88 | id: "id", 89 | esVersion, 90 | elasticsearch: db.getServiceConfig(serviceName), 91 | }) 92 | ); 93 | app.use( 94 | "/aka", 95 | service({ 96 | Model: db.getClient(), 97 | id: "id", 98 | parent: "parent", 99 | esVersion, 100 | elasticsearch: db.getServiceConfig("aka"), 101 | join: getCompatProp({ "6.0": "aka" }, esVersion), 102 | }) 103 | ); 104 | }); 105 | }); 106 | 107 | after(() => db.deleteSchema()); 108 | 109 | it("is CommonJS compatible", () => { 110 | expect(typeof require("../src")).to.equal("function"); 111 | }); 112 | 113 | describe("Initialization", () => { 114 | it("throws an error when missing options", () => { 115 | expect(service.bind(null)).to.throw( 116 | "Elasticsearch options have to be provided" 117 | ); 118 | }); 119 | 120 | it("throws an error when missing `options.Model`", () => { 121 | expect(service.bind(null, {})).to.throw( 122 | "Elasticsearch `Model` (client) needs to be provided" 123 | ); 124 | }); 125 | }); 126 | 127 | testSuite(app, errors, "people", "id"); 128 | 129 | describe("Specific Elasticsearch tests", () => { 130 | before(async () => { 131 | const service = app.service(serviceName); 132 | 133 | service.options.multi = true; 134 | app.service("aka").options.multi = true; 135 | 136 | await service.remove(null, { query: { $limit: 1000 } }); 137 | await service.create([ 138 | { 139 | id: "bob", 140 | name: "Bob", 141 | bio: "I like JavaScript.", 142 | tags: ["javascript", "programmer"], 143 | addresses: [{ street: "1 The Road" }, { street: "Programmer Lane" }], 144 | aka: "real", 145 | }, 146 | { 147 | id: "moody", 148 | name: "Moody", 149 | bio: "I don't like .NET.", 150 | tags: ["programmer"], 151 | addresses: [{ street: "2 The Road" }, { street: "Developer Lane" }], 152 | aka: "real", 153 | }, 154 | { 155 | id: "douglas", 156 | name: "Douglas", 157 | bio: "A legend", 158 | tags: ["javascript", "legend", "programmer"], 159 | addresses: [{ street: "3 The Road" }, { street: "Coder Alley" }], 160 | phone: "0123455567", 161 | aka: "real", 162 | }, 163 | ]); 164 | 165 | await app.service("aka").create([ 166 | { 167 | name: "The Master", 168 | parent: "douglas", 169 | id: "douglasAka", 170 | aka: "alias", 171 | }, 172 | { name: "Teacher", parent: "douglas", aka: "alias" }, 173 | { name: "Teacher", parent: "moody", aka: "alias" }, 174 | ]); 175 | }); 176 | 177 | after(async () => { 178 | await app.service(serviceName).remove(null, { query: { $limit: 1000 } }); 179 | }); 180 | 181 | coreTests.find(app, serviceName, esVersion); 182 | coreTests.get(app, serviceName); 183 | coreTests.create(app, serviceName); 184 | coreTests.patch(app, serviceName, esVersion); 185 | coreTests.remove(app, serviceName); 186 | coreTests.update(app, serviceName); 187 | coreTests.raw(app, serviceName, esVersion); 188 | }); 189 | }); 190 | -------------------------------------------------------------------------------- /test/utils/core.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const errors = require("@feathersjs/errors"); 3 | 4 | const { 5 | getType, 6 | validateType, 7 | removeProps, 8 | getDocDescriptor, 9 | getCompatVersion, 10 | getCompatProp, 11 | } = require("../../src/utils/core"); 12 | 13 | module.exports = function utilsCoreTests() { 14 | describe("getType", () => { 15 | it("should recognize number", () => { 16 | expect(getType(1)).to.equal("number"); 17 | }); 18 | 19 | it("should recognize string", () => { 20 | expect(getType("1")).to.equal("string"); 21 | }); 22 | 23 | it("should recognize boolean", () => { 24 | expect(getType(true)).to.equal("boolean"); 25 | expect(getType(false)).to.equal("boolean"); 26 | }); 27 | 28 | it("should recognize undefined", () => { 29 | expect(getType(undefined)).to.equal("undefined"); 30 | }); 31 | 32 | it("should recognize null", () => { 33 | expect(getType(null)).to.equal("null"); 34 | }); 35 | 36 | it("should recognize NaN", () => { 37 | expect(getType(NaN)).to.equal("NaN"); 38 | }); 39 | 40 | it("should recognize object", () => { 41 | expect(getType({})).to.equal("object"); 42 | }); 43 | 44 | it("should recognize array", () => { 45 | expect(getType([])).to.equal("array"); 46 | }); 47 | }); 48 | 49 | describe("validateType", () => { 50 | it("should accept one validator as a string", () => { 51 | expect(validateType(1, "val", "number")).to.be.ok; 52 | }); 53 | 54 | it("should accept multiple validators as an array", () => { 55 | expect(validateType(1, "val", ["number", "object"])).to.be.ok; 56 | }); 57 | 58 | it("should return the type", () => { 59 | expect(validateType(1, "val", "number")).to.equal("number"); 60 | expect( 61 | validateType("abc", "val", ["number", "array", "string"]) 62 | ).to.equal("string"); 63 | expect( 64 | validateType(true, "val", ["number", "array", "boolean"]) 65 | ).to.equal("boolean"); 66 | expect( 67 | validateType(false, "val", ["number", "array", "boolean"]) 68 | ).to.equal("boolean"); 69 | expect(validateType(null, "val", ["number", "object", "null"])).to.equal( 70 | "null" 71 | ); 72 | expect( 73 | validateType(undefined, "val", ["number", "object", "undefined"]) 74 | ).to.equal("undefined"); 75 | expect(validateType(NaN, "val", ["number", "object", "NaN"])).to.equal( 76 | "NaN" 77 | ); 78 | expect( 79 | validateType([], "val", ["number", "array", "undefined"]) 80 | ).to.equal("array"); 81 | expect( 82 | validateType({}, "val", ["number", "object", "undefined"]) 83 | ).to.equal("object"); 84 | }); 85 | 86 | it("should throw if none of the validators match", () => { 87 | expect(() => validateType(1, "val", "null")).to.throw(errors.BadRequest); 88 | expect(() => 89 | validateType(1, "val", ["null", "object", "array"]) 90 | ).to.throw(errors.BadRequest); 91 | expect(() => 92 | validateType("abc", "val", ["number", "object", "undefined"]) 93 | ).to.throw(errors.BadRequest); 94 | expect(() => 95 | validateType(true, "val", ["number", "object", "array"]) 96 | ).to.throw(errors.BadRequest); 97 | expect(() => 98 | validateType(null, "val", ["number", "object", "string"]) 99 | ).to.throw(errors.BadRequest); 100 | expect(() => 101 | validateType([], "val", ["number", "object", "null"]) 102 | ).to.throw(errors.BadRequest); 103 | }); 104 | }); 105 | 106 | describe("removeProps", () => { 107 | let object; 108 | 109 | beforeEach(() => { 110 | object = { 111 | _id: 12, 112 | _meta: { 113 | _index: "test", 114 | }, 115 | age: 13, 116 | }; 117 | }); 118 | 119 | it("should remove all properties from given list", () => { 120 | expect(removeProps(object, "_id", "_meta")).to.deep.equal({ age: 13 }); 121 | }); 122 | 123 | it("should not change the original object", () => { 124 | const objectSnapshot = JSON.stringify(object); 125 | 126 | removeProps(object); 127 | expect(JSON.stringify(object)).to.equal(objectSnapshot); 128 | }); 129 | 130 | it("should work if some properties are not defined on the object", () => { 131 | expect(removeProps(object, "_meta", "not_there")).to.deep.equal({ 132 | _id: 12, 133 | age: 13, 134 | }); 135 | }); 136 | 137 | it("should work if there are no props to remove", () => { 138 | expect(removeProps(object)).to.deep.equal(object); 139 | }); 140 | }); 141 | 142 | describe("getDocDescriptor", () => { 143 | let service; 144 | let doc; 145 | 146 | beforeEach(() => { 147 | service = { 148 | id: "id", 149 | parent: "parent", 150 | routing: "routing", 151 | join: "aka", 152 | meta: "meta", 153 | }; 154 | 155 | doc = { 156 | id: 13, 157 | parent: 1, 158 | routing: 2, 159 | name: "John", 160 | aka: "alias", 161 | meta: { _id: 13 }, 162 | }; 163 | }); 164 | 165 | it("should return doc descriptor", () => { 166 | expect(getDocDescriptor(service, doc)).to.deep.equal({ 167 | id: "13", 168 | parent: "1", 169 | routing: "2", 170 | join: "alias", 171 | doc: { name: "John" }, 172 | }); 173 | }); 174 | 175 | it("should use parent for routing if no routing specified", () => { 176 | delete doc.routing; 177 | 178 | expect(getDocDescriptor(service, doc)).to.deep.equal({ 179 | id: "13", 180 | parent: "1", 181 | routing: "1", 182 | join: "alias", 183 | doc: { name: "John" }, 184 | }); 185 | }); 186 | 187 | it("should not interpret the join field if join not configured", () => { 188 | delete service.join; 189 | 190 | expect(getDocDescriptor(service, doc)).to.deep.equal({ 191 | id: "13", 192 | parent: "1", 193 | routing: "2", 194 | join: undefined, 195 | doc: { name: "John", aka: "alias" }, 196 | }); 197 | }); 198 | 199 | it("should take overrides from the third parameter", () => { 200 | delete doc.parent; 201 | delete doc.routing; 202 | 203 | expect(getDocDescriptor(service, doc, { parent: 10 })).to.deep.equal({ 204 | id: "13", 205 | parent: "10", 206 | routing: "10", 207 | join: "alias", 208 | doc: { name: "John" }, 209 | }); 210 | }); 211 | }); 212 | 213 | describe("getCompatVersion", () => { 214 | it("should return biggest version from the list, which is smaller than provided current", () => { 215 | const allVersions = ["1.2", "2.3", "2.4", "2.5", "5.0"]; 216 | 217 | expect(getCompatVersion(allVersions, "2.4")).to.equal("2.4"); 218 | expect(getCompatVersion(allVersions, "2.6")).to.equal("2.5"); 219 | expect(getCompatVersion(allVersions, "2.0")).to.equal("1.2"); 220 | expect(getCompatVersion(allVersions, "6.0")).to.equal("5.0"); 221 | }); 222 | 223 | it("should return default version if no compatible version found", () => { 224 | expect(getCompatVersion([], "0.9", "1.0")).to.equal("1.0"); 225 | expect(getCompatVersion(["1.2", "5.3"], "0.9", "1.0")).to.equal("1.0"); 226 | }); 227 | 228 | it("should set default value for default version to '5.0'", () => { 229 | expect(getCompatVersion([], "0.9")).to.equal("5.0"); 230 | }); 231 | }); 232 | 233 | describe("getCompatProp", () => { 234 | it("should return the value identified by compatible version key", () => { 235 | const compatMap = { 236 | 2.4: "version 2.4", 237 | 2.6: "version 2.6", 238 | "6.0": "version 6.0", 239 | }; 240 | 241 | expect(getCompatProp(compatMap, "2.4")).to.equal("version 2.4"); 242 | expect(getCompatProp(compatMap, "2.5")).to.equal("version 2.4"); 243 | expect(getCompatProp(compatMap, "5.9")).to.equal("version 2.6"); 244 | expect(getCompatProp(compatMap, "10.0")).to.equal("version 6.0"); 245 | }); 246 | }); 247 | }; 248 | -------------------------------------------------------------------------------- /test/utils/index.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | 3 | const { mapFind, mapGet, mapPatch, mapBulk } = require("../../src/utils"); 4 | 5 | const parseQueryTests = require("./parse-query.js"); 6 | const coreUtilsTests = require("./core.js"); 7 | 8 | describe("Elasticsearch utils", () => { 9 | describe("mapFind", () => { 10 | let sourceResults; 11 | let mappedResults; 12 | 13 | beforeEach(() => { 14 | sourceResults = { 15 | hits: { 16 | max_score: 0.677, 17 | total: 2, 18 | hits: [ 19 | { 20 | _id: 12, 21 | _type: "people", 22 | _source: { 23 | name: "Andy", 24 | }, 25 | }, 26 | { 27 | _id: 15, 28 | _type: "people", 29 | _source: { 30 | name: "Duke", 31 | }, 32 | }, 33 | ], 34 | }, 35 | }; 36 | mappedResults = [ 37 | { 38 | _id: 12, 39 | name: "Andy", 40 | _meta: { 41 | _id: 12, 42 | _type: "people", 43 | }, 44 | }, 45 | { 46 | _id: 15, 47 | name: "Duke", 48 | _meta: { 49 | _id: 15, 50 | _type: "people", 51 | }, 52 | }, 53 | ]; 54 | }); 55 | 56 | it("should swap around meta and the docs", () => { 57 | const expectedResult = mappedResults; 58 | 59 | expect(mapFind(sourceResults, "_id", "_meta")).to.deep.equal( 60 | expectedResult 61 | ); 62 | }); 63 | 64 | it("should returned paginated results when hasPagination is true", () => { 65 | const filters = { 66 | $skip: 10, 67 | $limit: 25, 68 | }; 69 | const expectedResult = { 70 | total: 2, 71 | skip: filters.$skip, 72 | limit: filters.$limit, 73 | data: mappedResults, 74 | }; 75 | 76 | expect( 77 | mapFind(sourceResults, "_id", "_meta", undefined, filters, true) 78 | ).to.deep.equal(expectedResult); 79 | }); 80 | 81 | it("should support `hits.total` as an object in the response", () => { 82 | const filters = { 83 | $skip: 10, 84 | $limit: 25, 85 | }; 86 | const expectedResult = { 87 | total: 2, 88 | skip: filters.$skip, 89 | limit: filters.$limit, 90 | data: mappedResults, 91 | }; 92 | const { total } = sourceResults.hits; 93 | 94 | sourceResults.hits.total = { value: total }; 95 | 96 | expect( 97 | mapFind(sourceResults, "_id", "_meta", undefined, filters, true) 98 | ).to.deep.equal(expectedResult); 99 | }); 100 | }); 101 | 102 | describe("mapGet", () => { 103 | let item; 104 | 105 | beforeEach(() => { 106 | item = { 107 | _id: 12, 108 | _type: "people", 109 | _index: "test", 110 | _source: { 111 | name: "John", 112 | age: 13, 113 | aka: { 114 | name: "alias", 115 | parent: 1, 116 | }, 117 | }, 118 | found: true, 119 | }; 120 | }); 121 | 122 | it("should swap around meta and the doc", () => { 123 | const expectedResult = { 124 | name: "John", 125 | age: 13, 126 | aka: { 127 | name: "alias", 128 | parent: 1, 129 | }, 130 | _id: 12, 131 | _meta: { 132 | _id: 12, 133 | _type: "people", 134 | _index: "test", 135 | found: true, 136 | }, 137 | }; 138 | 139 | expect(mapGet(item, "_id", "_meta")).to.deep.equal(expectedResult); 140 | }); 141 | 142 | it("should extract parent from join field when join prop provided", () => { 143 | const expectedResult = { 144 | name: "John", 145 | age: 13, 146 | aka: "alias", 147 | _id: 12, 148 | _meta: { 149 | _id: 12, 150 | _type: "people", 151 | _index: "test", 152 | found: true, 153 | _parent: 1, 154 | }, 155 | }; 156 | 157 | expect(mapGet(item, "_id", "_meta", "aka")).to.deep.equal(expectedResult); 158 | }); 159 | 160 | it("should not change the original item", () => { 161 | const itemSnapshot = JSON.stringify(item); 162 | 163 | mapGet(item, "_id", "_meta"); 164 | expect(item).to.deep.equal(JSON.parse(itemSnapshot)); 165 | }); 166 | }); 167 | 168 | describe("mapPatch", () => { 169 | let item; 170 | 171 | beforeEach(() => { 172 | item = { 173 | _id: 12, 174 | _type: "people", 175 | _index: "test", 176 | get: { 177 | _source: { 178 | name: "John", 179 | age: 13, 180 | }, 181 | found: true, 182 | }, 183 | result: "updated", 184 | }; 185 | }); 186 | 187 | it("should swap around meta and the doc", () => { 188 | const expectedResult = { 189 | _id: 12, 190 | name: "John", 191 | age: 13, 192 | _meta: { 193 | _id: 12, 194 | _type: "people", 195 | _index: "test", 196 | result: "updated", 197 | }, 198 | }; 199 | 200 | expect(mapPatch(item, "_id", "_meta")).to.deep.equal(expectedResult); 201 | }); 202 | 203 | it("should return just meta if patched document not present", () => { 204 | delete item.get; 205 | const expectedResult = { 206 | _id: 12, 207 | _meta: { 208 | _id: 12, 209 | _type: "people", 210 | _index: "test", 211 | result: "updated", 212 | }, 213 | }; 214 | 215 | expect(mapPatch(item, "_id", "_meta")).to.deep.equal(expectedResult); 216 | }); 217 | 218 | it("should not change the original item", () => { 219 | const itemSnapshot = JSON.stringify(item); 220 | 221 | mapPatch(item, "_id", "_meta"); 222 | expect(item).to.deep.equal(JSON.parse(itemSnapshot)); 223 | }); 224 | }); 225 | 226 | describe("mapBulk", () => { 227 | it("should get rid of action name property swap around meta and the doc", () => { 228 | const items = [ 229 | { create: { status: 409, _id: "12" } }, 230 | { index: { result: "created", _id: "13" } }, 231 | { delete: { result: "deleted" } }, 232 | { update: { result: "updated", get: { _source: { name: "Bob" } } } }, 233 | { 234 | update: { 235 | result: "updated", 236 | get: { 237 | _source: { 238 | name: "Sunshine", 239 | aka: { name: "alias", parent: "12" }, 240 | }, 241 | }, 242 | }, 243 | }, 244 | ]; 245 | const expectedResult = [ 246 | { id: "12", _meta: { status: 409, _id: "12" } }, 247 | { id: "13", _meta: { result: "created", _id: "13" } }, 248 | { _meta: { result: "deleted" } }, 249 | { _meta: { result: "updated" }, name: "Bob" }, 250 | { 251 | _meta: { result: "updated", _parent: "12" }, 252 | name: "Sunshine", 253 | aka: "alias", 254 | }, 255 | ]; 256 | 257 | expect(mapBulk(items, "id", "_meta", "aka")).to.deep.equal( 258 | expectedResult 259 | ); 260 | }); 261 | 262 | it("should not change original items", () => { 263 | const items = [{ create: { status: 409, _id: "12" } }]; 264 | const itemsSnapshot = JSON.stringify(items); 265 | 266 | mapBulk(items, "id", "_meta"); 267 | expect(items).to.deep.equal(JSON.parse(itemsSnapshot)); 268 | }); 269 | }); 270 | 271 | parseQueryTests(); 272 | coreUtilsTests(); 273 | }); 274 | -------------------------------------------------------------------------------- /test/utils/parse-query.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const errors = require("@feathersjs/errors"); 3 | 4 | const { parseQuery } = require("../../src/utils"); 5 | 6 | module.exports = function parseQueryTests() { 7 | describe("parseQuery", () => { 8 | it("should return null if query is null or undefined", () => { 9 | expect(parseQuery(null, "_id")).to.be.null; 10 | expect(parseQuery()).to.be.null; 11 | }); 12 | 13 | it("should return null if query has no own properties", () => { 14 | const query = Object.create({ hello: "world" }); 15 | 16 | expect(parseQuery({}, "_id")).to.be.null; 17 | expect(parseQuery(query, "_id")).to.be.null; 18 | }); 19 | 20 | it("should throw BadRequest if query is not an object, null or undefined", () => { 21 | expect(() => parseQuery(12, "_id")).to.throw(errors.BadRequest); 22 | expect(() => parseQuery(true, "_id")).to.throw(errors.BadRequest); 23 | expect(() => parseQuery("abc", "_id")).to.throw(errors.BadRequest); 24 | expect(() => parseQuery([], "_id")).to.throw(errors.BadRequest); 25 | }); 26 | 27 | it("should throw BadRequest if $or is not an array", () => { 28 | expect(() => parseQuery({ $or: 12 }, "_id")).to.throw(errors.BadRequest); 29 | expect(() => parseQuery({ $or: true }, "_id")).to.throw( 30 | errors.BadRequest 31 | ); 32 | expect(() => parseQuery({ $or: "abc" }, "_id")).to.throw( 33 | errors.BadRequest 34 | ); 35 | expect(() => parseQuery({ $or: {} }, "_id")).to.throw(errors.BadRequest); 36 | }); 37 | 38 | it("should throw BadRequest if $and is not an array", () => { 39 | expect(() => parseQuery({ $and: 12 }, "_id")).to.throw(errors.BadRequest); 40 | expect(() => parseQuery({ $and: true }, "_id")).to.throw( 41 | errors.BadRequest 42 | ); 43 | expect(() => parseQuery({ $and: "abc" }, "_id")).to.throw( 44 | errors.BadRequest 45 | ); 46 | expect(() => parseQuery({ $and: {} }, "_id")).to.throw(errors.BadRequest); 47 | }); 48 | 49 | it("should throw BadRequest if $sqs is not an object, null or undefined", () => { 50 | expect(() => parseQuery({ $sqs: 12 }, "_id")).to.throw(errors.BadRequest); 51 | expect(() => parseQuery({ $sqs: true }, "_id")).to.throw( 52 | errors.BadRequest 53 | ); 54 | expect(() => parseQuery({ $sqs: "abc" }, "_id")).to.throw( 55 | errors.BadRequest 56 | ); 57 | expect(() => parseQuery({ $sqs: {} }, "_id")).to.throw(errors.BadRequest); 58 | }); 59 | 60 | it("should return null if $sqs is null or undefined", () => { 61 | expect(parseQuery({ $sqs: null }, "_id")).to.be.null; 62 | expect(parseQuery({ $sqs: undefined }, "_id")).to.be.null; 63 | }); 64 | 65 | it("should throw BadRequest if $sqs does not have (array)$fields property", () => { 66 | expect(() => parseQuery({ $sqs: { $query: "" } })).to.throw( 67 | errors.BadRequest 68 | ); 69 | expect(() => parseQuery({ $sqs: { $query: "", $fields: 123 } })).to.throw( 70 | errors.BadRequest 71 | ); 72 | expect(() => 73 | parseQuery({ $sqs: { $query: "", $fields: true } }) 74 | ).to.throw(errors.BadRequest); 75 | expect(() => parseQuery({ $sqs: { $query: "", $fields: {} } })).to.throw( 76 | errors.BadRequest 77 | ); 78 | }); 79 | 80 | it("should throw BadRequest if $sqs does not have (string)$query property", () => { 81 | expect(() => parseQuery({ $sqs: { $fields: [] } })).to.throw( 82 | errors.BadRequest 83 | ); 84 | expect(() => parseQuery({ $sqs: { $fields: [], $query: 123 } })).to.throw( 85 | errors.BadRequest 86 | ); 87 | expect(() => 88 | parseQuery({ $sqs: { $fields: [], $query: true } }) 89 | ).to.throw(errors.BadRequest); 90 | expect(() => parseQuery({ $sqs: { $fields: [], $query: {} } })).to.throw( 91 | errors.BadRequest 92 | ); 93 | }); 94 | 95 | it("should throw BadRequest if $sqs has non-string $operator property", () => { 96 | expect(() => 97 | parseQuery({ $sqs: { $fields: [], $query: "", $operator: [] } }) 98 | ).to.throw(errors.BadRequest); 99 | expect(() => 100 | parseQuery({ $sqs: { $fields: [], $query: "", $operator: 123 } }) 101 | ).to.throw(errors.BadRequest); 102 | expect(() => 103 | parseQuery({ $sqs: { $fields: [], $query: "", $operator: true } }) 104 | ).to.throw(errors.BadRequest); 105 | expect(() => 106 | parseQuery({ $sqs: { $fields: [], $query: "", $operator: {} } }) 107 | ).to.throw(errors.BadRequest); 108 | }); 109 | 110 | it("should throw BadRequest if $child is not an object, null or undefined", () => { 111 | expect(() => parseQuery({ $child: 12 })).to.throw(errors.BadRequest); 112 | expect(() => parseQuery({ $child: true })).to.throw(errors.BadRequest); 113 | expect(() => parseQuery({ $child: "abc" })).to.throw(errors.BadRequest); 114 | expect(() => parseQuery({ $child: [] })).to.throw(errors.BadRequest); 115 | }); 116 | 117 | it("should return null if $child is null or undefined", () => { 118 | expect(parseQuery({ $child: null }, "_id")).to.be.null; 119 | expect(parseQuery({ $child: undefined }, "_id")).to.be.null; 120 | }); 121 | 122 | it("should return null if $child has no criteria", () => { 123 | expect(parseQuery({ $child: { $type: "hello" } })).to.be.null; 124 | }); 125 | 126 | it("should throw BadRequest if $parent is not an object, null or undefined", () => { 127 | expect(() => parseQuery({ $parent: 12 })).to.throw(errors.BadRequest); 128 | expect(() => parseQuery({ $parent: true })).to.throw(errors.BadRequest); 129 | expect(() => parseQuery({ $parent: "abc" })).to.throw(errors.BadRequest); 130 | expect(() => parseQuery({ $parent: [] })).to.throw(errors.BadRequest); 131 | }); 132 | 133 | it("should return null if $parent is null or undefined", () => { 134 | expect(parseQuery({ $parent: null }, "_id")).to.be.null; 135 | expect(parseQuery({ $parent: undefined }, "_id")).to.be.null; 136 | }); 137 | 138 | it("should return null if $parent has no criteria", () => { 139 | expect(parseQuery({ $parent: { $type: "hello" } })).to.be.null; 140 | }); 141 | 142 | it("should throw BadRequest if $parent does not have (string)$type property", () => { 143 | expect(() => parseQuery({ $parent: {} })).to.throw(errors.BadRequest); 144 | expect(() => parseQuery({ $parent: { $type: 123 } })).to.throw( 145 | errors.BadRequest 146 | ); 147 | expect(() => parseQuery({ $parent: { $type: true } })).to.throw( 148 | errors.BadRequest 149 | ); 150 | expect(() => parseQuery({ $parent: { $type: {} } })).to.throw( 151 | errors.BadRequest 152 | ); 153 | }); 154 | 155 | it("should throw BadRequest if $nested is not an object, null or undefined", () => { 156 | expect(() => parseQuery({ $nested: 12 })).to.throw(errors.BadRequest); 157 | expect(() => parseQuery({ $nested: true })).to.throw(errors.BadRequest); 158 | expect(() => parseQuery({ $nested: "abc" })).to.throw(errors.BadRequest); 159 | expect(() => parseQuery({ $nested: [] })).to.throw(errors.BadRequest); 160 | }); 161 | 162 | it("should return null if $nested is null or undefined", () => { 163 | expect(parseQuery({ $nested: null })).to.be.null; 164 | expect(parseQuery({ $nested: undefined })).to.be.null; 165 | }); 166 | 167 | it("should throw BadRequest if $nested does not have (string)$path property", () => { 168 | expect(() => parseQuery({ $nested: {} })).to.throw(errors.BadRequest); 169 | expect(() => parseQuery({ $nested: { $path: 12 } })).to.throw( 170 | errors.BadRequest 171 | ); 172 | expect(() => parseQuery({ $nested: { $path: true } })).to.throw( 173 | errors.BadRequest 174 | ); 175 | expect(() => parseQuery({ $nested: { $path: {} } })).to.throw( 176 | errors.BadRequest 177 | ); 178 | }); 179 | 180 | it("should return null if $nested has no critera", () => { 181 | expect(parseQuery({ $nested: { $path: "hello" } })).to.be.null; 182 | }); 183 | 184 | it("should throw BadRequest if criteria is not a valid primitive, array or an object", () => { 185 | expect(() => parseQuery({ age: null }, "_id")).to.throw( 186 | errors.BadRequest 187 | ); 188 | expect(() => parseQuery({ age: NaN }, "_id")).to.throw(errors.BadRequest); 189 | expect(() => parseQuery({ age: () => {} }, "_id")).to.throw( 190 | errors.BadRequest 191 | ); 192 | }); 193 | 194 | ["$exists", "$missing"].forEach((query) => { 195 | it(`should throw BadRequest if ${query} values are not arrays with (string)field property`, () => { 196 | expect(() => parseQuery({ [query]: "foo" }, "_id")).to.throw( 197 | errors.BadRequest 198 | ); 199 | expect(() => parseQuery({ [query]: [1234] }, "_id")).to.throw( 200 | errors.BadRequest 201 | ); 202 | expect(() => parseQuery({ [query]: { foo: "bar" } }, "_id")).to.throw( 203 | errors.BadRequest 204 | ); 205 | expect(() => parseQuery({ [query]: [{ foo: "bar" }] }, "_id")).to.throw( 206 | errors.BadRequest 207 | ); 208 | }); 209 | }); 210 | 211 | it("should return term query for each primitive param", () => { 212 | const query = { 213 | user: "doug", 214 | age: 23, 215 | active: true, 216 | }; 217 | const expectedResult = { 218 | filter: [ 219 | { term: { user: "doug" } }, 220 | { term: { age: 23 } }, 221 | { term: { active: true } }, 222 | ], 223 | }; 224 | 225 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 226 | }); 227 | 228 | it("should return term query for each value from an array", () => { 229 | const query = { 230 | tags: ["javascript", "nodejs"], 231 | user: "doug", 232 | }; 233 | const expectedResult = { 234 | filter: [ 235 | { term: { tags: "javascript" } }, 236 | { term: { tags: "nodejs" } }, 237 | { term: { user: "doug" } }, 238 | ], 239 | }; 240 | 241 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 242 | }); 243 | 244 | it("should convert provided id property name to _id", () => { 245 | const query = { id: 12 }; 246 | const expectedResult = { 247 | filter: [{ term: { _id: 12 } }], 248 | }; 249 | expect(parseQuery(query, "id")).to.deep.equal(expectedResult); 250 | }); 251 | 252 | it("should return terms query for each $in param", () => { 253 | const query = { 254 | user: { $in: ["doug", "bob"] }, 255 | age: { $in: [23, 24, 50] }, 256 | }; 257 | const expectedResult = { 258 | filter: [ 259 | { terms: { user: ["doug", "bob"] } }, 260 | { terms: { age: [23, 24, 50] } }, 261 | ], 262 | }; 263 | 264 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 265 | }); 266 | 267 | it("should return term and terms query together", () => { 268 | const query = { 269 | user: "doug", 270 | age: { $in: [23, 24] }, 271 | }; 272 | const expectedResult = { 273 | filter: [{ term: { user: "doug" } }, { terms: { age: [23, 24] } }], 274 | }; 275 | 276 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 277 | }); 278 | 279 | it("should return must_not terms query for each $nin param", () => { 280 | const query = { 281 | user: { $nin: ["doug", "bob"] }, 282 | age: { $nin: [23, 24, 50] }, 283 | }; 284 | const expectedResult = { 285 | must_not: [ 286 | { terms: { user: ["doug", "bob"] } }, 287 | { terms: { age: [23, 24, 50] } }, 288 | ], 289 | }; 290 | 291 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 292 | }); 293 | 294 | it("should return range query for $lt, $lte, $gt, $gte", () => { 295 | const query = { 296 | age: { $gt: 30, $lt: 40 }, 297 | likes: { $lte: 100 }, 298 | cars: { $gte: 2, $lt: 5 }, 299 | }; 300 | const expectedResult = { 301 | filter: [ 302 | { range: { age: { gt: 30 } } }, 303 | { range: { age: { lt: 40 } } }, 304 | { range: { likes: { lte: 100 } } }, 305 | { range: { cars: { gte: 2 } } }, 306 | { range: { cars: { lt: 5 } } }, 307 | ], 308 | }; 309 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 310 | }); 311 | 312 | it('should return "should" subquery for $or', () => { 313 | const query = { 314 | $or: [{ user: "Adam", age: { $gt: 40 } }, { age: { $gt: 40 } }], 315 | }; 316 | const expectedResult = { 317 | should: [ 318 | { 319 | bool: { 320 | filter: [ 321 | { term: { user: "Adam" } }, 322 | { range: { age: { gt: 40 } } }, 323 | ], 324 | }, 325 | }, 326 | { 327 | bool: { 328 | filter: [{ range: { age: { gt: 40 } } }], 329 | }, 330 | }, 331 | ], 332 | minimum_should_match: 1, 333 | }; 334 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 335 | }); 336 | 337 | it("should return all queries for $and", () => { 338 | const query = { 339 | $and: [ 340 | { tags: "javascript" }, 341 | { tags: { $ne: "legend" } }, 342 | { age: { $nin: [23, 24] } }, 343 | { age: { $in: [25, 26] } }, 344 | ], 345 | name: "Doug", 346 | }; 347 | const expectedResult = { 348 | filter: [ 349 | { term: { tags: "javascript" } }, 350 | { terms: { age: [25, 26] } }, 351 | { term: { name: "Doug" } }, 352 | ], 353 | must_not: [{ term: { tags: "legend" } }, { terms: { age: [23, 24] } }], 354 | }; 355 | 356 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 357 | }); 358 | 359 | it('should return "simple_query_string" for $sqs with default_operator "or" by default', () => { 360 | const query = { 361 | $sqs: { 362 | $fields: ["description", "title^5"], 363 | $query: "-(track another)", 364 | }, 365 | }; 366 | const expectedResult = { 367 | must: [ 368 | { 369 | simple_query_string: { 370 | fields: ["description", "title^5"], 371 | query: "-(track another)", 372 | default_operator: "or", 373 | }, 374 | }, 375 | ], 376 | }; 377 | 378 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 379 | }); 380 | 381 | it('should return "simple_query_string" for $sqs with specified default_operator', () => { 382 | const query = { 383 | $sqs: { 384 | $fields: ["description"], 385 | $query: "-(track another)", 386 | $operator: "and", 387 | }, 388 | }; 389 | const expectedResult = { 390 | must: [ 391 | { 392 | simple_query_string: { 393 | fields: ["description"], 394 | query: "-(track another)", 395 | default_operator: "and", 396 | }, 397 | }, 398 | ], 399 | }; 400 | 401 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 402 | }); 403 | 404 | it('should return "prefix" query for $prefix', () => { 405 | const query = { 406 | user: { $prefix: "ada" }, 407 | }; 408 | const expectedResult = { 409 | filter: [{ prefix: { user: "ada" } }], 410 | }; 411 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 412 | }); 413 | 414 | it('should return "wildcard" query for $wildcard', () => { 415 | const query = { 416 | user: { $wildcard: "ada" }, 417 | }; 418 | const expectedResult = { 419 | filter: [{ wildcard: { user: "ada" } }], 420 | }; 421 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 422 | }); 423 | 424 | it('should return "regexp" query for $regexp', () => { 425 | const query = { 426 | user: { $regexp: "ada" }, 427 | }; 428 | const expectedResult = { 429 | filter: [{ regexp: { user: "ada" } }], 430 | }; 431 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 432 | }); 433 | 434 | it('should return "match_all" query for $all: true', () => { 435 | const query = { 436 | $all: true, 437 | }; 438 | const expectedResult = { 439 | must: [{ match_all: {} }], 440 | }; 441 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 442 | }); 443 | 444 | it('should not return "match_all" query for $all: false', () => { 445 | const query = { 446 | $all: false, 447 | }; 448 | const expectedResult = null; 449 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 450 | }); 451 | 452 | it('should return "match" query for $match', () => { 453 | const query = { 454 | text: { $match: "javascript" }, 455 | }; 456 | const expectedResult = { 457 | must: [{ match: { text: "javascript" } }], 458 | }; 459 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 460 | }); 461 | 462 | it('should return "match_phrase" query for $phrase', () => { 463 | const query = { 464 | text: { $phrase: "javascript" }, 465 | }; 466 | const expectedResult = { 467 | must: [{ match_phrase: { text: "javascript" } }], 468 | }; 469 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 470 | }); 471 | 472 | it('should return "match_phrase_prefix" query for $phrase_prefix', () => { 473 | const query = { 474 | text: { $phrase_prefix: "javasc" }, 475 | }; 476 | const expectedResult = { 477 | must: [{ match_phrase_prefix: { text: "javasc" } }], 478 | }; 479 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 480 | }); 481 | 482 | it('should return "has_child" query for $child', () => { 483 | const query = { 484 | $child: { 485 | $type: "address", 486 | city: "Ashford", 487 | }, 488 | }; 489 | const expectedResult = { 490 | must: [ 491 | { 492 | has_child: { 493 | type: "address", 494 | query: { 495 | bool: { 496 | filter: [{ term: { city: "Ashford" } }], 497 | }, 498 | }, 499 | }, 500 | }, 501 | ], 502 | }; 503 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 504 | }); 505 | 506 | it('should return "has_parent" query for $parent', () => { 507 | const query = { 508 | $parent: { 509 | $type: "people", 510 | name: "Douglas", 511 | }, 512 | }; 513 | const expectedResult = { 514 | must: [ 515 | { 516 | has_parent: { 517 | parent_type: "people", 518 | query: { 519 | bool: { 520 | filter: [{ term: { name: "Douglas" } }], 521 | }, 522 | }, 523 | }, 524 | }, 525 | ], 526 | }; 527 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 528 | }); 529 | 530 | it('should return "nested" query for $nested', () => { 531 | const query = { 532 | $nested: { 533 | $path: "legend", 534 | "legend.name": "Douglas", 535 | }, 536 | }; 537 | const expectedResult = { 538 | must: [ 539 | { 540 | nested: { 541 | path: "legend", 542 | query: { 543 | bool: { 544 | filter: [{ term: { "legend.name": "Douglas" } }], 545 | }, 546 | }, 547 | }, 548 | }, 549 | ], 550 | }; 551 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 552 | }); 553 | 554 | [ 555 | ["$exists", "must"], 556 | ["$missing", "must_not"], 557 | ].forEach(([q, clause]) => { 558 | it(`should return "${clause}" query for ${q}`, () => { 559 | const query = { 560 | [q]: ["phone", "address"], 561 | }; 562 | const expectedResult = { 563 | [clause]: [ 564 | { 565 | exists: { field: "phone" }, 566 | }, 567 | { 568 | exists: { field: "address" }, 569 | }, 570 | ], 571 | }; 572 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 573 | }); 574 | }); 575 | 576 | it("should return all types of queries together", () => { 577 | const query = { 578 | $or: [ 579 | { likes: { $gt: 9, $lt: 12 }, age: { $ne: 10 } }, 580 | { user: { $nin: ["Anakin", "Luke"] } }, 581 | { user: { $prefix: "ada" } }, 582 | { $all: true }, 583 | ], 584 | age: { $in: [12, 13] }, 585 | user: "Obi Wan", 586 | country: { $nin: ["us", "pl", "ae"] }, 587 | bio: { $match: "javascript", $phrase: "the good parts" }, 588 | $child: { $type: "address", city: "Ashford" }, 589 | $parent: { $type: "people", name: "Douglas" }, 590 | $nested: { $path: "legend", "legend.name": { $match: "Douglas" } }, 591 | $and: [{ tags: "javascript" }, { tags: "legend" }], 592 | $exists: ["phone"], 593 | $missing: ["address"], 594 | }; 595 | const expectedResult = { 596 | should: [ 597 | { 598 | bool: { 599 | filter: [ 600 | { range: { likes: { gt: 9 } } }, 601 | { range: { likes: { lt: 12 } } }, 602 | ], 603 | must_not: [{ term: { age: 10 } }], 604 | }, 605 | }, 606 | { 607 | bool: { 608 | must_not: [{ terms: { user: ["Anakin", "Luke"] } }], 609 | }, 610 | }, 611 | { 612 | bool: { 613 | filter: [{ prefix: { user: "ada" } }], 614 | }, 615 | }, 616 | { 617 | bool: { 618 | must: [{ match_all: {} }], 619 | }, 620 | }, 621 | ], 622 | minimum_should_match: 1, 623 | filter: [ 624 | { terms: { age: [12, 13] } }, 625 | { term: { user: "Obi Wan" } }, 626 | { term: { tags: "javascript" } }, 627 | { term: { tags: "legend" } }, 628 | ], 629 | must_not: [ 630 | { terms: { country: ["us", "pl", "ae"] } }, 631 | { exists: { field: "address" } }, 632 | ], 633 | must: [ 634 | { match: { bio: "javascript" } }, 635 | { match_phrase: { bio: "the good parts" } }, 636 | { 637 | has_child: { 638 | type: "address", 639 | query: { 640 | bool: { 641 | filter: [{ term: { city: "Ashford" } }], 642 | }, 643 | }, 644 | }, 645 | }, 646 | { 647 | has_parent: { 648 | parent_type: "people", 649 | query: { 650 | bool: { 651 | filter: [{ term: { name: "Douglas" } }], 652 | }, 653 | }, 654 | }, 655 | }, 656 | { 657 | nested: { 658 | path: "legend", 659 | query: { 660 | bool: { 661 | must: [{ match: { "legend.name": "Douglas" } }], 662 | }, 663 | }, 664 | }, 665 | }, 666 | { exists: { field: "phone" } }, 667 | ], 668 | }; 669 | 670 | expect(parseQuery(query, "_id")).to.deep.equal(expectedResult); 671 | }); 672 | }); 673 | }; 674 | -------------------------------------------------------------------------------- /types/index.test.ts: -------------------------------------------------------------------------------- 1 | import { default as service } from 'feathers-elasticsearch'; 2 | import * as elasticsearch from 'elasticsearch'; 3 | 4 | const messageService = service({ 5 | Model: new elasticsearch.Client({ 6 | host: 'localhost:9200', 7 | apiVersion: '6.0' 8 | }), 9 | paginate: { 10 | default: 10, 11 | max: 50 12 | }, 13 | elasticsearch: { 14 | index: 'test', 15 | type: 'messages' 16 | }, 17 | esVersion: '6.0' 18 | }); 19 | -------------------------------------------------------------------------------- /types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es6"], 5 | "target": "es6", 6 | "noImplicitAny": true, 7 | "noImplicitThis": true, 8 | "strictNullChecks": true, 9 | "strictFunctionTypes": true, 10 | "noEmit": true, 11 | 12 | // If the library is an external module (uses `export`), this allows your test file to import "mylib" instead of "./index". 13 | // If the library is global (cannot be imported via `import` or `require`), leave this out. 14 | "baseUrl": ".", 15 | "paths": { "feathers-elasticsearch": ["."] } 16 | } 17 | } -------------------------------------------------------------------------------- /types/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "dtslint/dtslint.json", // Or "dtslint/dt.json" if on DefinitelyTyped 3 | "rules": { 4 | "indent": [true, "spaces"] 5 | } 6 | } --------------------------------------------------------------------------------