├── .eslintignore ├── .eslintrc.js ├── .github ├── FUNDING.yml └── workflows │ └── nodejs.yml ├── .gitignore ├── .markdownlint.json ├── .npmignore ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── examples ├── differentVersions │ └── index.ts └── elastic56 │ ├── index.ts │ ├── schema.ts │ ├── seedData.json │ └── seedData.ts ├── jest.config.js ├── package.json ├── scripts └── docker │ ├── es2 │ ├── Dockerfile │ └── elasticsearch.yml │ ├── es5 │ ├── Dockerfile │ └── elasticsearch.yml │ └── start.js ├── src ├── ElasticApiParser.ts ├── __mocks__ │ ├── apiPartial.js │ ├── cv.ts │ ├── cvMapping.ts │ └── elasticClient.ts ├── __tests__ │ ├── ElasticApiParser-test.ts │ ├── __snapshots__ │ │ └── ElasticApiParser-test.ts.snap │ ├── github_issues │ │ ├── 32-test.ts │ │ └── 37-test.ts │ └── mappingConverter-test.ts ├── composeWithElastic.ts ├── elasticApiFieldConfig.ts ├── elasticDSL │ ├── Aggs │ │ ├── AggBlock.ts │ │ ├── AggRules.ts │ │ ├── Aggs.ts │ │ ├── Bucket │ │ │ ├── Children.ts │ │ │ ├── DateHistogram.ts │ │ │ ├── DateRange.ts │ │ │ ├── DiversifiedSampler.ts │ │ │ ├── Filter.ts │ │ │ ├── Filters.ts │ │ │ ├── GeoDistance.ts │ │ │ ├── GeohashGrid.ts │ │ │ ├── Global.ts │ │ │ ├── Histogram.ts │ │ │ ├── IpRange.ts │ │ │ ├── Missing.ts │ │ │ ├── Nested.ts │ │ │ ├── Range.ts │ │ │ ├── ReverseNested.ts │ │ │ ├── Sampler.ts │ │ │ ├── SignificantTerms.ts │ │ │ └── Terms.ts │ │ ├── Metrics │ │ │ ├── Avg.ts │ │ │ ├── Cardinality.ts │ │ │ ├── ExtendedStats.ts │ │ │ ├── GeoBounds.ts │ │ │ ├── GeoCentroid.ts │ │ │ ├── Max.ts │ │ │ ├── Min.ts │ │ │ ├── PercentileRanks.ts │ │ │ ├── Percentiles.ts │ │ │ ├── ScriptedMetric.ts │ │ │ ├── Stats.ts │ │ │ ├── Sum.ts │ │ │ ├── TopHits.ts │ │ │ └── ValueCount.ts │ │ ├── Pipeline │ │ │ ├── AvgBucket.ts │ │ │ ├── BucketScript.ts │ │ │ ├── BucketSelector.ts │ │ │ ├── CumulativeSum.ts │ │ │ ├── Derivative.ts │ │ │ ├── ExtendedStatsBucket.ts │ │ │ ├── MaxBucket.ts │ │ │ ├── MinBucket.ts │ │ │ ├── MovingAverage.ts │ │ │ ├── PercentilesBucket.ts │ │ │ ├── SerialDifferencing.ts │ │ │ ├── StatsBucket.ts │ │ │ └── SumBucket.ts │ │ └── __tests__ │ │ │ ├── Aggs-test.ts │ │ │ └── __snapshots__ │ │ │ └── Aggs-test.ts.snap │ ├── Commons │ │ ├── Date.ts │ │ ├── FieldNames.ts │ │ ├── Float.ts │ │ ├── Geo.ts │ │ ├── HDR.ts │ │ ├── Ip.ts │ │ ├── Script.ts │ │ └── __tests__ │ │ │ └── FieldNames-test.ts │ ├── Query │ │ ├── Compound │ │ │ ├── Bool.ts │ │ │ ├── Boosting.ts │ │ │ ├── ConstantScore.ts │ │ │ ├── DisMax.ts │ │ │ └── FunctionScore.ts │ │ ├── FullText │ │ │ ├── Common.ts │ │ │ ├── Match.ts │ │ │ ├── MatchPhrase.ts │ │ │ ├── MatchPhrasePrefix.ts │ │ │ ├── MultiMatch.ts │ │ │ ├── QueryString.ts │ │ │ └── SimpleQueryString.ts │ │ ├── Geo │ │ │ ├── GeoBoundingBox.ts │ │ │ ├── GeoDistance.ts │ │ │ ├── GeoPolygon.ts │ │ │ └── GeoShape.ts │ │ ├── Joining │ │ │ ├── HasChild.ts │ │ │ ├── HasParent.ts │ │ │ ├── Nested.ts │ │ │ └── ParentId.ts │ │ ├── MatchAll.ts │ │ ├── Query.ts │ │ ├── Specialized │ │ │ ├── MoreLikeThis.ts │ │ │ ├── Percolate.ts │ │ │ └── Script.ts │ │ ├── TermLevel │ │ │ ├── Exists.ts │ │ │ ├── Fuzzy.ts │ │ │ ├── Ids.ts │ │ │ ├── Prefix.ts │ │ │ ├── Range.ts │ │ │ ├── Regexp.ts │ │ │ ├── Term.ts │ │ │ ├── Terms.ts │ │ │ ├── Type.ts │ │ │ └── Wildcard.ts │ │ └── __tests__ │ │ │ ├── Query-test.ts │ │ │ └── __snapshots__ │ │ │ └── Query-test.ts.snap │ ├── SearchBody.ts │ └── Sort.ts ├── index.ts ├── mappingConverter.ts ├── resolvers │ ├── __tests__ │ │ ├── findById-test.ts │ │ └── search-test.ts │ ├── findById.ts │ ├── search.ts │ ├── searchConnection.ts │ ├── searchPagination.ts │ ├── suggest.ts │ └── updateById.ts ├── types.d.ts ├── types │ ├── FindByIdOutput.ts │ ├── SearchHitItem.ts │ ├── SearchOutput.ts │ ├── Shards.ts │ └── UpdateByIdOutput.ts └── utils.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | flow-typed 2 | lib 3 | mjs 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | parser: '@typescript-eslint/parser', 5 | plugins: ['@typescript-eslint', 'prettier'], 6 | extends: ['plugin:@typescript-eslint/recommended', 'prettier', 'plugin:prettier/recommended'], 7 | parserOptions: { 8 | sourceType: 'module', 9 | useJSXTextNode: true, 10 | project: [path.resolve(__dirname, 'tsconfig.json')], 11 | }, 12 | rules: { 13 | 'no-underscore-dangle': 0, 14 | 'arrow-body-style': 0, 15 | 'no-unused-expressions': 0, 16 | 'no-plusplus': 0, 17 | 'no-console': 0, 18 | 'func-names': 0, 19 | 'comma-dangle': [ 20 | 'error', 21 | { 22 | arrays: 'always-multiline', 23 | objects: 'always-multiline', 24 | imports: 'always-multiline', 25 | exports: 'always-multiline', 26 | functions: 'ignore', 27 | }, 28 | ], 29 | 'no-prototype-builtins': 0, 30 | 'prefer-destructuring': 0, 31 | 'no-else-return': 0, 32 | 'lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }], 33 | '@typescript-eslint/explicit-member-accessibility': 0, 34 | '@typescript-eslint/no-explicit-any': 0, 35 | '@typescript-eslint/no-inferrable-types': 0, 36 | '@typescript-eslint/explicit-function-return-type': 0, 37 | '@typescript-eslint/no-use-before-define': 0, 38 | '@typescript-eslint/no-empty-function': 0, 39 | '@typescript-eslint/camelcase': 0, 40 | '@typescript-eslint/ban-ts-comment': 0, 41 | '@typescript-eslint/triple-slash-reference': 0, 42 | }, 43 | env: { 44 | jasmine: true, 45 | jest: true, 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [nodkz] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: graphql-compose 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | tests: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node-version: [14.x, 16.x] 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: Install node_modules 21 | run: yarn 22 | - name: Test & lint 23 | run: yarn test 24 | env: 25 | CI: true 26 | - name: Send codecov.io stats 27 | if: matrix.node-version == '14.x' 28 | run: bash <(curl -s https://codecov.io/bash) || echo '' 29 | 30 | publish: 31 | if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/alpha' || github.ref == 'refs/heads/beta' 32 | needs: [tests] 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v2 36 | - name: Use Node.js 14 37 | uses: actions/setup-node@v1 38 | with: 39 | node-version: 14.x 40 | - name: Install node_modules 41 | run: yarn install 42 | - name: Build 43 | run: yarn build 44 | - name: Semantic Release (publish to npm) 45 | run: yarn semantic-release 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | 15 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 16 | .grunt 17 | 18 | # node-waf configuration 19 | .lock-wscript 20 | 21 | # Compiled binary addons (http://nodejs.org/api/addons.html) 22 | build/Release 23 | 24 | # IntelliJ Files 25 | *.iml 26 | *.ipr 27 | *.iws 28 | /out/ 29 | .idea/ 30 | .idea_modules/ 31 | 32 | # Dependency directory 33 | node_modules 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | 41 | # Transpiled code 42 | /es 43 | /lib 44 | /mjs 45 | /__fixtures__ 46 | 47 | coverage 48 | .nyc_output 49 | package-lock.json 50 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "line-length": false, 3 | "no-trailing-punctuation": { 4 | "punctuation": ",;" 5 | }, 6 | "no-inline-html": false, 7 | "ol-prefix": false, 8 | "first-line-h1": false, 9 | "first-heading-h1": false 10 | } 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | src 4 | example 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "printWidth": 100, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Jest", 9 | "type": "node", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/node_modules/.bin/jest", 12 | "args": ["--runInBand", "--watch"], 13 | "cwd": "${workspaceFolder}", 14 | "console": "integratedTerminal", 15 | "internalConsoleOptions": "neverOpen", 16 | "disableOptimisticBPs": true 17 | }, 18 | { 19 | "name": "Jest Current File", 20 | "type": "node", 21 | "request": "launch", 22 | "program": "${workspaceFolder}/node_modules/.bin/jest", 23 | "args": [ 24 | "${fileBasenameNoExtension}", 25 | "--config", 26 | "jest.config.js" 27 | ], 28 | "console": "integratedTerminal", 29 | "internalConsoleOptions": "neverOpen", 30 | "disableOptimisticBPs": true 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": ["javascript"], 3 | "javascript.validate.enable": false, 4 | "javascript.autoClosingTags": false, 5 | "eslint.autoFixOnSave": true, 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.0-semantically-released (March 04, 2017) 2 | This package publishing automated by [semantic-release](https://github.com/semantic-release/semantic-release). 3 | [Changelog](https://github.com/nodkz/graphql-compose-elasticsearch/releases) is generated automatically and can be found here: https://github.com/nodkz/graphql-compose-elasticsearch/releases 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present Pavel Chertorogov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /examples/differentVersions/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import graphqlHTTP from 'express-graphql'; 3 | import { graphql } from 'graphql-compose'; 4 | import { elasticApiFieldConfig } from '../../src'; // from 'graphql-compose-elasticsearch'; 5 | 6 | const { GraphQLSchema, GraphQLObjectType } = graphql; 7 | 8 | const expressPort = process.env.port || process.env.PORT || 9201; 9 | 10 | const generatedSchema = new GraphQLSchema({ 11 | query: new GraphQLObjectType({ 12 | name: 'Query', 13 | fields: { 14 | elastic56: elasticApiFieldConfig({ 15 | host: 'http://user:pass@localhost:9200', 16 | apiVersion: '5.6', 17 | // log: 'trace', 18 | }), 19 | 20 | elastic68: elasticApiFieldConfig({ 21 | host: 'http://user:pass@localhost:9200', 22 | apiVersion: '6.8', 23 | // log: 'trace', 24 | }), 25 | 26 | elastic77: elasticApiFieldConfig({ 27 | host: 'http://user:pass@localhost:9200', 28 | apiVersion: '7.7', 29 | // log: 'trace', 30 | }), 31 | }, 32 | }), 33 | }); 34 | 35 | const server = express(); 36 | server.use( 37 | '/', 38 | graphqlHTTP({ 39 | schema: generatedSchema, 40 | graphiql: true, 41 | }) 42 | ); 43 | 44 | server.listen(expressPort, () => { 45 | console.log(`The server is running at http://localhost:${expressPort}/`); 46 | }); 47 | -------------------------------------------------------------------------------- /examples/elastic56/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import express from 'express'; 4 | import graphqlHTTP from 'express-graphql'; 5 | import schema from './schema'; 6 | 7 | const expressPort = process.env.port || process.env.PORT || 9201; 8 | 9 | const server = express(); 10 | server.use( 11 | '/', 12 | graphqlHTTP({ 13 | schema, 14 | graphiql: true, 15 | customFormatErrorFn: (error: any) => ({ 16 | message: error.message, 17 | stack: error.stack.split('\n'), 18 | }), 19 | }) 20 | ); 21 | 22 | server.listen(expressPort, () => { 23 | console.log(`The server is running at http://localhost:${expressPort}/`); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/elastic56/schema.ts: -------------------------------------------------------------------------------- 1 | import elasticsearch from 'elasticsearch'; 2 | import { graphql, ObjectTypeComposer } from 'graphql-compose'; 3 | import { composeWithElastic, elasticApiFieldConfig } from '../../src'; // from 'graphql-compose-elasticsearch'; 4 | 5 | const { GraphQLSchema, GraphQLObjectType } = graphql; 6 | 7 | // Mapping obtained from ElasticSearch server 8 | // If you have existed index in ES you may load mapping via 9 | // GET http://user:pass@localhost:9200/demo_user/_mapping 10 | // and then get subtree of returned document which contains 11 | // properties definitions (which looks like following data): 12 | const demoUserMapping = { 13 | properties: { 14 | name: { 15 | type: 'text', 16 | fields: { 17 | keyword: { 18 | type: 'keyword', 19 | }, 20 | }, 21 | }, 22 | gender: { 23 | type: 'text', 24 | }, 25 | birthday: { 26 | type: 'date', 27 | }, 28 | position: { 29 | type: 'text', 30 | }, 31 | relocation: { 32 | type: 'boolean', 33 | }, 34 | salary: { 35 | properties: { 36 | currency: { 37 | type: 'text', 38 | }, 39 | total: { 40 | type: 'double', 41 | }, 42 | }, 43 | }, 44 | skills: { 45 | type: 'text', 46 | }, 47 | languages: { 48 | type: 'keyword', 49 | }, 50 | location: { 51 | properties: { 52 | name: { 53 | type: 'text', 54 | }, 55 | point: { 56 | type: 'geo_point', 57 | }, 58 | }, 59 | }, 60 | experience: { 61 | properties: { 62 | company: { 63 | type: 'text', 64 | }, 65 | description: { 66 | type: 'text', 67 | }, 68 | end: { 69 | type: 'date', 70 | }, 71 | position: { 72 | type: 'text', 73 | }, 74 | start: { 75 | type: 'date', 76 | }, 77 | tillNow: { 78 | type: 'boolean', 79 | }, 80 | }, 81 | }, 82 | createdAt: { 83 | type: 'date', 84 | }, 85 | }, 86 | }; 87 | 88 | const UserEsTC = composeWithElastic({ 89 | graphqlTypeName: 'UserES', 90 | elasticIndex: 'demo_user', 91 | elasticType: 'demo_user', 92 | elasticMapping: demoUserMapping, 93 | elasticClient: new elasticsearch.Client({ 94 | host: 'http://localhost:9200', 95 | apiVersion: '5.6', 96 | log: 'trace', 97 | }), 98 | // elastic mapping does not contain information about is fields are arrays or not 99 | // so provide this information explicitly for obtaining correct types in GraphQL 100 | pluralFields: ['skills', 'languages'], 101 | }); 102 | 103 | const ProxyTC = ObjectTypeComposer.createTemp(`type ProxyDebugType { source: JSON }`); 104 | ProxyTC.addResolver({ 105 | name: 'showArgs', 106 | kind: 'query', 107 | args: { 108 | source: 'JSON', 109 | }, 110 | type: 'ProxyDebugType', 111 | resolve: ({ args }: any) => args, 112 | }); 113 | 114 | UserEsTC.addRelation('showRelationArguments', { 115 | resolver: () => ProxyTC.getResolver('showArgs'), 116 | prepareArgs: { 117 | source: (source) => source, 118 | }, 119 | projection: { 120 | name: true, 121 | salary: true, 122 | }, 123 | }); 124 | 125 | const schema = new GraphQLSchema({ 126 | query: new GraphQLObjectType({ 127 | name: 'Query', 128 | fields: { 129 | userSearch: UserEsTC.getResolver('search').getFieldConfig(), 130 | userSearchConnection: UserEsTC.getResolver('searchConnection').getFieldConfig(), 131 | elastic50: elasticApiFieldConfig({ 132 | host: 'http://user:pass@localhost:9200', 133 | apiVersion: '5.6', 134 | log: 'trace', 135 | }), 136 | }, 137 | }), 138 | }); 139 | 140 | export default schema; 141 | -------------------------------------------------------------------------------- /examples/elastic56/seedData.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "name": "Pavel", 5 | "gender": "male", 6 | "birthday": "1985-07-15", 7 | "position": "CTO", 8 | "relocation": true, 9 | "salary": { 10 | "total": 333, 11 | "currency": "BTC" 12 | }, 13 | "skills": [ "nodejs", "graphql", "babel", "webpack", "oss contributor", "devOps" ], 14 | "languages": [ "ru", "en" ], 15 | "location": { 16 | "name": "Almaty", 17 | "point": [43.238949, 76.889709] 18 | }, 19 | "createdAt": "2017-10-19T04:29:08Z" 20 | }, 21 | { 22 | "id": 2, 23 | "name": "Helen", 24 | "gender": "female", 25 | "birthday": "1986-03-06", 26 | "position": "sales manager", 27 | "relocation": true, 28 | "salary": { 29 | "total": 12345, 30 | "currency": "USD" 31 | }, 32 | "skills": [ "Communication", "Ability to Attain Targets", "Analytical Ability", "Judgment"], 33 | "languages": [ "ru", "en" ], 34 | "location": { 35 | "name": "Almaty", 36 | "point": [43.238949, 76.889709] 37 | }, 38 | "createdAt": "2017-10-19T04:29:09Z" 39 | }, 40 | { 41 | "id": 3, 42 | "name": "Andrew", 43 | "gender": "male", 44 | "birthday": "1952-02-19", 45 | "position": "Vice President Sales", 46 | "relocation": false, 47 | "salary": { 48 | "total": 23456, 49 | "currency": "USD" 50 | }, 51 | "skills": [ "BTS commercial", "international marketing"], 52 | "languages": [ "en", "fr", "ge" ], 53 | "location": { 54 | "name": "Tacoma" 55 | }, 56 | "createdAt": "2017-10-19T04:29:10Z" 57 | }, 58 | { 59 | "id": 4, 60 | "name": "Steven", 61 | "gender": "male", 62 | "birthday": "1955-03-04", 63 | "position": "Sales Representative", 64 | "relocation": false, 65 | "salary": { 66 | "total": 12222, 67 | "currency": "USD" 68 | }, 69 | "skills": [ "BA in psychology", "Art of the Cold Call"], 70 | "languages": [ "en" ], 71 | "location": { 72 | "name": "Seattle" 73 | }, 74 | "createdAt": "2017-10-19T04:29:11Z" 75 | } 76 | ] 77 | -------------------------------------------------------------------------------- /examples/elastic56/seedData.ts: -------------------------------------------------------------------------------- 1 | import elasticsearch from 'elasticsearch'; 2 | import seedData from './seedData.json'; 3 | 4 | const client = new elasticsearch.Client({ 5 | host: 'localhost:9200', 6 | log: 'trace', 7 | }); 8 | 9 | const body = [] as any[]; 10 | seedData.forEach((row) => { 11 | const { id, ...restData } = row; 12 | body.push({ index: { _index: 'demo_user', _type: 'demo_user', _id: id } }, restData); 13 | }); 14 | 15 | client 16 | .bulk({ 17 | index: 'demo_user', 18 | type: 'demo_user', 19 | body, 20 | }) 21 | .then(() => { 22 | console.log('Data successfully seeded!'); 23 | }); 24 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: '/tsconfig.json', 7 | isolatedModules: true, 8 | diagnostics: false, 9 | }, 10 | }, 11 | moduleFileExtensions: ['ts', 'js'], 12 | transform: { 13 | '^.+\\.ts$': 'ts-jest', 14 | '^.+\\.js$': 'babel-jest', 15 | }, 16 | roots: ['/src'], 17 | testPathIgnorePatterns: ['/node_modules/', '/lib/'], 18 | }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-compose-elasticsearch", 3 | "version": "0.0.0-semantically-released", 4 | "description": "Elastic search via GraphQL", 5 | "files": [ 6 | "lib" 7 | ], 8 | "main": "lib/index.js", 9 | "module": "mjs/index.mjs", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/nodkz/graphql-compose-elasticsearch.git" 13 | }, 14 | "keywords": [ 15 | "graphql", 16 | "elastic", 17 | "elasticsearch", 18 | "graphql-compose" 19 | ], 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/nodkz/graphql-compose-elasticsearch/issues" 23 | }, 24 | "homepage": "https://github.com/nodkz/graphql-compose-elasticsearch#readme", 25 | "dependencies": { 26 | "dox": "^0.9.0" 27 | }, 28 | "peerDependencies": { 29 | "elasticsearch": "^16.0.0 || ^15.0.0 || ^14.0.0 || ^13.0.0 || ^12.0.0", 30 | "graphql-compose": "^7.0.4 || ^8.0.0 || ^9.0.0" 31 | }, 32 | "devDependencies": { 33 | "@types/elasticsearch": "5.0.38", 34 | "@types/express": "4.17.13", 35 | "@types/jest": "27.0.2", 36 | "@typescript-eslint/eslint-plugin": "4.33.0", 37 | "@typescript-eslint/parser": "4.33.0", 38 | "elasticsearch": "16.7.2", 39 | "eslint": "7.32.0", 40 | "eslint-config-airbnb-base": "14.2.1", 41 | "eslint-config-prettier": "8.3.0", 42 | "eslint-plugin-import": "2.24.2", 43 | "eslint-plugin-prettier": "4.0.0", 44 | "express": "^4.17.1", 45 | "express-graphql": "0.12.0", 46 | "graphql": "15.6.1", 47 | "graphql-compose": "9.0.3", 48 | "jest": "27.2.4", 49 | "nodemon": "2.0.13", 50 | "npm-run-all": "^4.1.5", 51 | "prettier": "2.4.1", 52 | "rimraf": "3.0.2", 53 | "semantic-release": "17.4.7", 54 | "ts-jest": "27.0.5", 55 | "ts-node": "10.2.1", 56 | "ts-node-dev": "1.1.8", 57 | "typescript": "4.4.3" 58 | }, 59 | "scripts": { 60 | "build": "rimraf lib && tsc -p ./tsconfig.build.json", 61 | "dev": "npm run demo1", 62 | "demo1": "yarn demo1:seed && ts-node-dev ./examples/elastic56/index.ts", 63 | "demo1:seed": "ts-node ./examples/elastic56/seedData.ts", 64 | "demo2": "ts-node-dev ./examples/differentVersions/index.ts", 65 | "watch": "jest --watch", 66 | "coverage": "jest --coverage", 67 | "lint": "eslint --ext .ts ./src", 68 | "test": "npm run coverage && npm run lint", 69 | "semantic-release": "semantic-release", 70 | "docker": "npm run docker:v5", 71 | "docker:v2": "node ./scripts/docker/start 2 & wait", 72 | "docker:v5": "node ./scripts/docker/start 5 & wait" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /scripts/docker/es2/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM elasticsearch:2-alpine 2 | 3 | COPY elasticsearch.yml /usr/share/elasticsearch/config/elasticsearch.yml 4 | ENV ES_JAVA_OPTS="-Xms750m -Xmx750m" -------------------------------------------------------------------------------- /scripts/docker/es2/elasticsearch.yml: -------------------------------------------------------------------------------- 1 | script.inline: true 2 | script.indexed: true 3 | network.host: 0.0.0.0 4 | http.cors.enabled: true 5 | http.cors.allow-origin: "*" 6 | cluster.name: elasticsearch_nodkz 7 | -------------------------------------------------------------------------------- /scripts/docker/es5/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.elastic.co/elasticsearch/elasticsearch:5.2.1 2 | 3 | COPY elasticsearch.yml /usr/share/elasticsearch/config/elasticsearch.yml 4 | ENV ES_JAVA_OPTS="-Xms750m -Xmx750m" -------------------------------------------------------------------------------- /scripts/docker/es5/elasticsearch.yml: -------------------------------------------------------------------------------- 1 | script.inline: true 2 | network.host: 0.0.0.0 3 | http.cors.enabled: true 4 | http.cors.allow-origin: "*" 5 | cluster.name: elasticsearch_nodkz 6 | xpack.security.enabled: false 7 | -------------------------------------------------------------------------------- /scripts/docker/start.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const cp = require('child_process'); 4 | const path = require('path'); 5 | 6 | const dockerImageName = 'elasticsearch-nodkz'; 7 | const version = process.argv[2] || 5; 8 | 9 | function isDockerImageExists(imageNameWithTag) { 10 | const imageId = cp 11 | .execSync(`docker images -q ${imageNameWithTag}`, { cwd: '.' }) 12 | .toString(); 13 | return imageId && imageId.length > 0; 14 | } 15 | 16 | function buildDockerContainer(v) { 17 | const imageNameWithTag = `${dockerImageName}:${v}`; 18 | const dockerContextFolder = path.resolve(__dirname, `./es${v}`); 19 | console.log( 20 | `Building docker container ${imageNameWithTag} from ${dockerContextFolder}/Dockerfile ...` 21 | ); 22 | cp.execSync( 23 | `docker build \ 24 | -t ${imageNameWithTag} \ 25 | ${dockerContextFolder}`, 26 | { 27 | cwd: dockerContextFolder, 28 | stdio: [0, 1, 2], 29 | } 30 | ); 31 | } 32 | 33 | function runDockerContainer(v) { 34 | const imageNameWithTag = `${dockerImageName}:${v}`; 35 | if (!isDockerImageExists(imageNameWithTag)) { 36 | buildDockerContainer(v); 37 | } 38 | 39 | console.log(`Starting docker container ${imageNameWithTag} ...`); 40 | cp.execSync(`docker run -i --rm -p 9200:9200 ${imageNameWithTag}`, { 41 | stdio: [0, 1, 2], 42 | }); 43 | } 44 | 45 | function removeDockerContainer(v) { 46 | const imageNameWithTag = `${dockerImageName}:${v}`; 47 | console.log(`Removing docker image ${imageNameWithTag} ...`); 48 | cp.execSync(`docker rmi ${imageNameWithTag}`, { stdio: [0, 1, 2] }); 49 | } 50 | 51 | function onExit() { 52 | removeDockerContainer(version); 53 | process.exit(0); 54 | } 55 | process.on('SIGINT', onExit); // catch ctrl-c 56 | process.on('SIGTERM', onExit); // catch kill 57 | runDockerContainer(version); 58 | -------------------------------------------------------------------------------- /src/__mocks__/cv.ts: -------------------------------------------------------------------------------- 1 | import { schemaComposer } from 'graphql-compose'; 2 | import { inputPropertiesToGraphQLTypes, convertToSourceTC } from '../mappingConverter'; 3 | import cvMapping from './cvMapping'; 4 | 5 | export const CvFieldMap = inputPropertiesToGraphQLTypes(cvMapping); 6 | export const CvTC = convertToSourceTC(schemaComposer, cvMapping, 'Cv'); 7 | -------------------------------------------------------------------------------- /src/__mocks__/elasticClient.ts: -------------------------------------------------------------------------------- 1 | import elasticsearch from 'elasticsearch'; 2 | 3 | const elasticClient = new elasticsearch.Client({ 4 | host: 'http://localhost:9200', 5 | apiVersion: '6.8', 6 | // log: 'trace', 7 | }); 8 | 9 | export default elasticClient; 10 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/32-test.ts: -------------------------------------------------------------------------------- 1 | import elasticsearch from 'elasticsearch'; 2 | import { schemaComposer, ObjectTypeComposer, Resolver } from 'graphql-compose'; 3 | import { GraphQLSchema, GraphQLObjectType } from 'graphql-compose/lib/graphql'; 4 | import { composeWithElastic } from '../..'; 5 | 6 | const ELASTICSEARCH_HOST = ''; 7 | const ELASTICSEARCH_API_VERSION = '6.8'; 8 | const mapping = { 9 | properties: { 10 | id: { 11 | type: 'keyword', 12 | }, 13 | title: { 14 | type: 'text', 15 | }, 16 | description: { 17 | type: 'text', 18 | }, 19 | }, 20 | }; 21 | 22 | const ActivitiesEsTC = composeWithElastic({ 23 | graphqlTypeName: 'SearchActivities', 24 | elasticIndex: 'myindex', 25 | elasticType: 'activities', 26 | elasticMapping: mapping, 27 | elasticClient: new elasticsearch.Client({ 28 | host: ELASTICSEARCH_HOST, 29 | apiVersion: ELASTICSEARCH_API_VERSION, 30 | log: 'info', 31 | }), 32 | }); 33 | 34 | describe('github issue #32 - hits returns me the found id, score, type...', () => { 35 | beforeEach(() => { 36 | schemaComposer.clear(); 37 | }); 38 | 39 | it('test `search` resolver', () => { 40 | expect(ActivitiesEsTC).toBeInstanceOf(ObjectTypeComposer); 41 | 42 | const resolver = ActivitiesEsTC.getResolver('search'); 43 | expect(resolver).toBeInstanceOf(Resolver); 44 | 45 | const OutputType: any = resolver.getType(); 46 | const OutputTC = schemaComposer.createObjectTC(OutputType); 47 | expect(OutputTC.getFieldNames()).toEqual([ 48 | 'hits', 49 | 'count', 50 | 'aggregations', 51 | 'max_score', 52 | 'took', 53 | 'timed_out', 54 | '_shards', 55 | ]); 56 | 57 | const HitsTC = OutputTC.getFieldOTC('hits'); 58 | expect(HitsTC).toBeInstanceOf(ObjectTypeComposer); 59 | expect(HitsTC.getFieldNames()).toEqual([ 60 | '_index', 61 | '_type', 62 | '_id', 63 | '_score', 64 | '_source', 65 | '_shard', 66 | '_node', 67 | '_explanation', 68 | '_version', 69 | 'inner_hits', 70 | 'highlight', 71 | 'sort', 72 | 'fields', 73 | ]); 74 | 75 | const SourceTC = HitsTC.getFieldOTC('_source'); 76 | expect(SourceTC.getTypeName()).toBe('SearchActivitiesSearchActivities'); 77 | expect(SourceTC).toBeInstanceOf(ObjectTypeComposer); 78 | expect(SourceTC.getFieldNames()).toEqual(['id', 'title', 'description']); 79 | }); 80 | 81 | it('test schema', () => { 82 | const schema: any = new GraphQLSchema({ 83 | query: new GraphQLObjectType({ 84 | name: 'Query', 85 | fields: { 86 | searchActivities: ActivitiesEsTC.getResolver('search').getFieldConfig(), 87 | }, 88 | }), 89 | }); 90 | 91 | const fc: any = schema._queryType.getFields().searchActivities; 92 | 93 | const OutputTC = schemaComposer.createObjectTC(fc.type); 94 | expect(OutputTC.getFieldNames()).toEqual([ 95 | 'hits', 96 | 'count', 97 | 'aggregations', 98 | 'max_score', 99 | 'took', 100 | 'timed_out', 101 | '_shards', 102 | ]); 103 | 104 | const HitsTC = OutputTC.getFieldOTC('hits'); 105 | expect(HitsTC).toBeInstanceOf(ObjectTypeComposer); 106 | expect(HitsTC.getFieldNames()).toEqual([ 107 | '_index', 108 | '_type', 109 | '_id', 110 | '_score', 111 | '_source', 112 | '_shard', 113 | '_node', 114 | '_explanation', 115 | '_version', 116 | 'inner_hits', 117 | 'highlight', 118 | 'sort', 119 | 'fields', 120 | ]); 121 | 122 | const SourceTC = HitsTC.getFieldOTC('_source'); 123 | expect(SourceTC.getTypeName()).toBe('SearchActivitiesSearchActivities'); 124 | expect(SourceTC).toBeInstanceOf(ObjectTypeComposer); 125 | expect(SourceTC.getFieldNames()).toEqual(['id', 'title', 'description']); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/37-test.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------- 2 | // You should have running elasticsearch instance for passing this test 3 | // you may run (this package has built-in docker container for tests) 4 | // yarn docker:v5 5 | // ------------------------------------------------------------------- 6 | 7 | import elasticsearch from 'elasticsearch'; 8 | import { ObjectTypeComposer, schemaComposer } from 'graphql-compose'; 9 | import { graphql } from 'graphql'; 10 | import { composeWithElastic } from '../..'; 11 | 12 | const ELASTICSEARCH_HOST = ''; 13 | const ELASTICSEARCH_API_VERSION = '5.6'; 14 | const mapping = { 15 | properties: { 16 | id: { 17 | type: 'keyword', 18 | }, 19 | title: { 20 | type: 'text', 21 | }, 22 | tags: { 23 | type: 'text', 24 | }, 25 | }, 26 | }; 27 | 28 | const elasticClient = new elasticsearch.Client({ 29 | host: ELASTICSEARCH_HOST, 30 | apiVersion: ELASTICSEARCH_API_VERSION, 31 | log: 'info', // FOR DETAILED DEBUG USE - 'trace' 32 | }); 33 | const elasticIndex = 'github37'; 34 | const elasticType = 'activities'; 35 | 36 | const ActivitiesEsTC = composeWithElastic({ 37 | graphqlTypeName: 'SearchActivities', 38 | elasticMapping: mapping, 39 | pluralFields: ['tags'], 40 | elasticIndex, 41 | elasticType, 42 | elasticClient, 43 | }); 44 | 45 | describe.skip('github issue #37 - Mutations via updateById overwrite arrays instead of appending to them', () => { 46 | beforeAll(async () => { 47 | const indexExists = await elasticClient.indices.exists({ index: elasticIndex }); 48 | if (indexExists) { 49 | await elasticClient.indices.delete({ index: elasticIndex }); 50 | } 51 | 52 | // create demo record directly in elastic 53 | await elasticClient.create({ 54 | index: elasticIndex, 55 | type: elasticType, 56 | id: '333', 57 | body: { 58 | title: 'Test 1', 59 | tags: ['y', 'z'], 60 | }, 61 | }); 62 | }); 63 | 64 | it('create custom resolver', async () => { 65 | expect(ActivitiesEsTC).toBeInstanceOf(ObjectTypeComposer); 66 | 67 | ActivitiesEsTC.addResolver({ 68 | name: 'addTag', 69 | kind: 'mutation', 70 | type: 'JSON', 71 | args: { 72 | id: 'String!', 73 | tag: 'String!', 74 | }, 75 | resolve: ({ args }: any) => { 76 | return elasticClient.update({ 77 | index: elasticIndex, 78 | type: elasticType, 79 | id: args.id, 80 | body: { 81 | script: { 82 | inline: 'ctx._source.tags.add(params.tag)', 83 | params: { tag: args.tag }, 84 | }, 85 | }, 86 | }); 87 | }, 88 | }); 89 | 90 | // create simple Schema for EE test 91 | schemaComposer.Query.addFields({ noop: 'String' }); // by spec query MUST be always present 92 | schemaComposer.Mutation.addFields({ 93 | activitiesAddTag: ActivitiesEsTC.getResolver('addTag'), 94 | }); 95 | const schema = schemaComposer.buildSchema(); 96 | 97 | // update record via graphql 98 | const graphqlResponse = await graphql({ 99 | schema, 100 | source: ` 101 | mutation { 102 | activitiesAddTag(id: "333", tag: "x") 103 | } 104 | `, 105 | }); 106 | 107 | // check graphql response 108 | expect(graphqlResponse).toEqual({ 109 | data: { 110 | activitiesAddTag: { 111 | _id: '333', 112 | _index: 'github37', 113 | _shards: { failed: 0, successful: 1, total: 2 }, 114 | _type: 'activities', 115 | _version: 2, 116 | result: 'updated', 117 | }, 118 | }, 119 | }); 120 | 121 | // check demo record directly in elastic 122 | const elasticData = await elasticClient.get({ 123 | index: elasticIndex, 124 | type: elasticType, 125 | id: '333', 126 | _source: true, 127 | }); 128 | 129 | expect(elasticData).toEqual({ 130 | _id: '333', 131 | _index: 'github37', 132 | _source: { tags: ['y', 'z', 'x'], title: 'Test 1' }, 133 | _type: 'activities', 134 | _version: 2, 135 | found: true, 136 | }); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /src/composeWithElastic.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ObjectTypeComposer, 3 | SchemaComposer, 4 | schemaComposer as globalSchemaComposer, 5 | } from 'graphql-compose'; 6 | import { convertToSourceTC, inputPropertiesToGraphQLTypes } from './mappingConverter'; 7 | import createSearchResolver from './resolvers/search'; 8 | import createSearchConnectionResolver from './resolvers/searchConnection'; 9 | import createSearchPaginationResolver from './resolvers/searchPagination'; 10 | import createFindByIdResolver from './resolvers/findById'; 11 | import createUpdateByIdResolver from './resolvers/updateById'; 12 | import { prepareCommonOpts } from './utils'; 13 | 14 | import type { ElasticMappingT } from './mappingConverter'; 15 | 16 | export type composeWithElasticOptsT = { 17 | graphqlTypeName: string; 18 | elasticIndex: string; 19 | elasticType: string; 20 | elasticMapping: ElasticMappingT; 21 | elasticClient: any; 22 | pluralFields?: string[]; 23 | prefix?: string; 24 | postfix?: string; 25 | schemaComposer?: SchemaComposer; 26 | }; 27 | 28 | export function composeWithElastic( 29 | opts: composeWithElasticOptsT 30 | ): ObjectTypeComposer { 31 | if (!opts) { 32 | throw new Error('Opts is required argument for composeWithElastic()'); 33 | } 34 | 35 | if (!opts.elasticMapping || !opts.elasticMapping.properties) { 36 | throw new Error( 37 | 'You provide incorrect elasticMapping property. It should be an object `{ properties: {} }`' 38 | ); 39 | } 40 | 41 | if (!opts.elasticIndex || typeof opts.elasticIndex !== 'string') { 42 | throw new Error( 43 | 'Third arg for Resolver search() should contain `elasticIndex` string property from your Elastic server.' 44 | ); 45 | } 46 | 47 | if (!opts.elasticType || typeof opts.elasticType !== 'string') { 48 | throw new Error( 49 | 'Third arg for Resolver search() should contain `elasticType` string property from your Elastic server.' 50 | ); 51 | } 52 | 53 | if (typeof opts.graphqlTypeName !== 'string' || !opts.graphqlTypeName) { 54 | throw new Error( 55 | 'Opts.graphqlTypeName is required property for generated GraphQL Type name in composeWithElastic()' 56 | ); 57 | } 58 | 59 | if (!opts.prefix) { 60 | opts.prefix = opts.graphqlTypeName; // eslint-disable-line 61 | } 62 | 63 | if (opts.pluralFields && !Array.isArray(opts.pluralFields)) { 64 | throw new Error( 65 | 'Opts.pluralFields should be an Array of strings with field names ' + 66 | 'which are plural (you may use dot notation for nested fields).' 67 | ); 68 | } 69 | 70 | if (opts.schemaComposer && !(opts.schemaComposer instanceof SchemaComposer)) { 71 | throw new Error( 72 | 'Opts.schemaComposer should be an SchemaComposer instance from graphql-compose package.' 73 | ); 74 | } 75 | 76 | const schemaComposer = opts.schemaComposer || globalSchemaComposer; 77 | 78 | const fieldMap = inputPropertiesToGraphQLTypes(opts.elasticMapping); 79 | const sourceTC = convertToSourceTC( 80 | schemaComposer, 81 | opts.elasticMapping, 82 | opts.graphqlTypeName, 83 | opts 84 | ); 85 | const commonOpts = prepareCommonOpts(schemaComposer, { 86 | ...opts, 87 | prefix: opts.prefix || 'Es', 88 | fieldMap, 89 | sourceTC, 90 | schemaComposer, 91 | }); 92 | 93 | const searchR = createSearchResolver(commonOpts); 94 | const searchConnectionR = createSearchConnectionResolver(commonOpts, searchR); 95 | const searchPaginationR = createSearchPaginationResolver(commonOpts, searchR); 96 | const findByIdR = createFindByIdResolver(commonOpts); 97 | const updateByIdR = createUpdateByIdResolver(commonOpts); 98 | 99 | sourceTC.addResolver(searchR); 100 | sourceTC.addResolver(searchConnectionR); 101 | sourceTC.addResolver(searchPaginationR); 102 | sourceTC.addResolver(findByIdR); 103 | sourceTC.addResolver(updateByIdR); 104 | 105 | return sourceTC; 106 | } 107 | -------------------------------------------------------------------------------- /src/elasticApiFieldConfig.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | 3 | import { ObjectTypeComposer, graphql } from 'graphql-compose'; 4 | import type { GraphQLFieldConfig } from 'graphql'; 5 | import elasticsearch from 'elasticsearch'; 6 | import ElasticApiParser from './ElasticApiParser'; 7 | 8 | const DEFAULT_ELASTIC_API_VERSION = '_default'; 9 | const { GraphQLString } = graphql; 10 | 11 | export function elasticApiFieldConfig(esClientOrOpts: any): GraphQLFieldConfig { 12 | if (!esClientOrOpts || typeof esClientOrOpts !== 'object') { 13 | throw new Error( 14 | 'You should provide ElasticClient instance or ElasticClientConfig in first argument.' 15 | ); 16 | } 17 | 18 | if (isElasticClient(esClientOrOpts)) { 19 | return instanceElasticClient(esClientOrOpts); 20 | } else { 21 | return contextElasticClient(esClientOrOpts); 22 | } 23 | } 24 | 25 | function instanceElasticClient(elasticClient: Record): GraphQLFieldConfig { 26 | const apiVersion = elasticClient.transport._config.apiVersion || DEFAULT_ELASTIC_API_VERSION; 27 | const prefix = `ElasticAPI${apiVersion.replace('.', '')}`; 28 | 29 | const apiParser = new ElasticApiParser({ 30 | elasticClient, 31 | prefix, 32 | }); 33 | 34 | return { 35 | description: `Elastic API v${apiVersion}`, 36 | type: ObjectTypeComposer.createTemp({ 37 | name: prefix, 38 | fields: apiParser.generateFieldMap(), 39 | }).getType(), 40 | resolve: () => ({}), 41 | }; 42 | } 43 | 44 | function contextElasticClient(elasticConfig: Record): GraphQLFieldConfig { 45 | if (!elasticConfig.apiVersion) { 46 | elasticConfig.apiVersion = DEFAULT_ELASTIC_API_VERSION; 47 | } 48 | const { apiVersion } = elasticConfig; 49 | const prefix = `ElasticAPI${apiVersion.replace('.', '')}`; 50 | 51 | const apiParser = new ElasticApiParser({ 52 | apiVersion, 53 | prefix, 54 | }); 55 | 56 | return { 57 | description: `Elastic API v${apiVersion}`, 58 | type: ObjectTypeComposer.createTemp({ 59 | name: prefix, 60 | fields: apiParser.generateFieldMap(), 61 | }).getType(), 62 | args: { 63 | host: { 64 | type: GraphQLString, 65 | defaultValue: elasticConfig.host || 'http://user:pass@localhost:9200', 66 | }, 67 | }, 68 | resolve: (_: any, args: Record, context: Record) => { 69 | if (typeof context === 'object') { 70 | const opts = args.host 71 | ? { 72 | ...elasticConfig, 73 | host: args.host, 74 | } 75 | : elasticConfig; 76 | context.elasticClient = new elasticsearch.Client(opts); 77 | } 78 | return {}; 79 | }, 80 | }; 81 | } 82 | 83 | function isElasticClient(obj: any) { 84 | if (obj instanceof elasticsearch.Client) { 85 | return true; 86 | } 87 | 88 | if (obj && obj.transport && obj.transport._config && obj.transport._config.__reused) { 89 | return true; 90 | } 91 | 92 | return false; 93 | } 94 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/AggBlock.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../utils'; 3 | import { getAggRulesITC } from './AggRules'; 4 | 5 | export function getAggBlockITC(opts: CommonOpts): InputTypeComposer { 6 | const name = getTypeName('AggBlock', opts); 7 | const description = desc( 8 | ` 9 | The aggregations framework helps provide aggregated data based on 10 | a search query. It is based on simple building blocks called aggregations, 11 | that can be composed in order to build complex summaries of the data. 12 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html) 13 | ` 14 | ); 15 | 16 | return opts.getOrCreateITC(name, () => ({ 17 | name, 18 | description, 19 | fields: { 20 | key: { 21 | type: 'String', 22 | description: 'FieldName in response for aggregation result', 23 | }, 24 | value: { 25 | type: () => getAggRulesITC(opts), 26 | description: 'Aggregation rules', 27 | }, 28 | }, 29 | })); 30 | } 31 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/AggRules.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../utils'; 3 | import { getAggBlockITC } from './AggBlock'; 4 | 5 | import { getAvgITC } from './Metrics/Avg'; 6 | import { getCardinalityITC } from './Metrics/Cardinality'; 7 | import { getExtendedStatsITC } from './Metrics/ExtendedStats'; 8 | import { getGeoBoundsITC } from './Metrics/GeoBounds'; 9 | import { getGeoCentroidITC } from './Metrics/GeoCentroid'; 10 | import { getMaxITC } from './Metrics/Max'; 11 | import { getMinITC } from './Metrics/Min'; 12 | import { getPercentileRanksITC } from './Metrics/PercentileRanks'; 13 | import { getPercentilesITC } from './Metrics/Percentiles'; 14 | import { getScriptedMetricITC } from './Metrics/ScriptedMetric'; 15 | import { getStatsITC } from './Metrics/Stats'; 16 | import { getSumITC } from './Metrics/Sum'; 17 | import { getTopHitsITC } from './Metrics/TopHits'; 18 | import { getValueCountITC } from './Metrics/ValueCount'; 19 | 20 | import { getChildrenITC } from './Bucket/Children'; 21 | import { getDateHistogramITC } from './Bucket/DateHistogram'; 22 | import { getAggsDateRangeITC } from './Bucket/DateRange'; 23 | import { getDiversifiedSamplerITC } from './Bucket/DiversifiedSampler'; 24 | import { getFilterITC } from './Bucket/Filter'; 25 | import { getFiltersITC } from './Bucket/Filters'; 26 | import { getGeoDistanceITC } from './Bucket/GeoDistance'; 27 | import { getGeohashGridITC } from './Bucket/GeohashGrid'; 28 | import { getGlobalITC } from './Bucket/Global'; 29 | import { getHistogramITC } from './Bucket/Histogram'; 30 | import { getIpRangeITC } from './Bucket/IpRange'; 31 | import { getMissingITC } from './Bucket/Missing'; 32 | import { getNestedITC } from './Bucket/Nested'; 33 | import { getRangeITC } from './Bucket/Range'; 34 | import { getReverseNestedITC } from './Bucket/ReverseNested'; 35 | import { getSamplerITC } from './Bucket/Sampler'; 36 | import { getSignificantTermsITC } from './Bucket/SignificantTerms'; 37 | import { getTermsITC } from './Bucket/Terms'; 38 | 39 | import { getAvgBucketITC } from './Pipeline/AvgBucket'; 40 | import { getBucketScriptITC } from './Pipeline/BucketScript'; 41 | import { getBucketSelectorITC } from './Pipeline/BucketSelector'; 42 | import { getCumulativeSumITC } from './Pipeline/CumulativeSum'; 43 | import { getDerivativeITC } from './Pipeline/Derivative'; 44 | import { getExtendedStatsBucketITC } from './Pipeline/ExtendedStatsBucket'; 45 | import { getMaxBucketITC } from './Pipeline/MaxBucket'; 46 | import { getMinBucketITC } from './Pipeline/MinBucket'; 47 | import { getMovingAverageITC } from './Pipeline/MovingAverage'; 48 | import { getPercentilesBucketITC } from './Pipeline/PercentilesBucket'; 49 | import { getSerialDifferencingITC } from './Pipeline/SerialDifferencing'; 50 | import { getStatsBucketITC } from './Pipeline/StatsBucket'; 51 | import { getSumBucketITC } from './Pipeline/SumBucket'; 52 | 53 | export function getAggRulesITC(opts: CommonOpts): InputTypeComposer { 54 | const name = getTypeName('AggRules', opts); 55 | const description = desc( 56 | ` 57 | The aggregations framework helps provide aggregated data based on 58 | a search query. It is based on simple building blocks called aggregations, 59 | that can be composed in order to build complex summaries of the data. 60 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html) 61 | ` 62 | ); 63 | 64 | return opts.getOrCreateITC(name, () => ({ 65 | name, 66 | description, 67 | // $FlowFixMe 68 | fields: { 69 | avg: () => getAvgITC(opts), 70 | cardinality: () => getCardinalityITC(opts), 71 | extended_stats: () => getExtendedStatsITC(opts), 72 | geo_bounds: () => getGeoBoundsITC(opts), 73 | geo_centroid: () => getGeoCentroidITC(opts), 74 | max: () => getMaxITC(opts), 75 | min: () => getMinITC(opts), 76 | percentile_ranks: () => getPercentileRanksITC(opts), 77 | percentiles: () => getPercentilesITC(opts), 78 | scripted_metric: () => getScriptedMetricITC(opts), 79 | stats: () => getStatsITC(opts), 80 | sum: () => getSumITC(opts), 81 | top_hits: () => getTopHitsITC(opts), 82 | value_count: () => getValueCountITC(opts), 83 | 84 | children: () => getChildrenITC(opts), 85 | date_histogram: () => getDateHistogramITC(opts), 86 | date_range: () => getAggsDateRangeITC(opts), 87 | diversified_sampler: () => getDiversifiedSamplerITC(opts), 88 | filter: () => getFilterITC(opts), 89 | filters: () => getFiltersITC(opts), 90 | geo_distance: () => getGeoDistanceITC(opts), 91 | geohash_grid: () => getGeohashGridITC(opts), 92 | global: () => getGlobalITC(opts), 93 | histogram: () => getHistogramITC(opts), 94 | ip_range: () => getIpRangeITC(opts), 95 | missing: () => getMissingITC(opts), 96 | nested: () => getNestedITC(opts), 97 | range: () => getRangeITC(opts), 98 | reverse_nested: () => getReverseNestedITC(opts), 99 | sampler: () => getSamplerITC(opts), 100 | significant_terms: () => getSignificantTermsITC(opts), 101 | terms: () => getTermsITC(opts), 102 | 103 | avg_bucket: () => getAvgBucketITC(opts), 104 | bucket_script: () => getBucketScriptITC(opts), 105 | bucket_selector: () => getBucketSelectorITC(opts), 106 | cumulative_sum: () => getCumulativeSumITC(opts), 107 | derivative: () => getDerivativeITC(opts), 108 | extended_stats_bucket: () => getExtendedStatsBucketITC(opts), 109 | max_bucket: () => getMaxBucketITC(opts), 110 | min_bucket: () => getMinBucketITC(opts), 111 | moving_average: () => getMovingAverageITC(opts), 112 | percentiles_bucket: () => getPercentilesBucketITC(opts), 113 | serial_differencing: () => getSerialDifferencingITC(opts), 114 | stats_bucket: () => getStatsBucketITC(opts), 115 | sum_bucket: () => getSumBucketITC(opts), 116 | 117 | aggs: { 118 | type: () => [getAggBlockITC(opts)], 119 | description: 'Aggregation block', 120 | }, 121 | }, 122 | })); 123 | } 124 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Aggs.ts: -------------------------------------------------------------------------------- 1 | import type { InputTypeComposer } from 'graphql-compose'; 2 | import { getAggBlockITC } from './AggBlock'; 3 | import { CommonOpts } from '../../utils'; 4 | 5 | export function getAggsITC( 6 | opts: CommonOpts 7 | ): Array> { 8 | return [getAggBlockITC(opts)]; 9 | } 10 | 11 | export type ElasticAggsT = { 12 | [outputFieldName: string]: ElasticAggsRulesT; 13 | }; 14 | 15 | export type ElasticAggsRulesT = { 16 | [aggOperationName: string]: any; 17 | aggs?: ElasticAggsT; 18 | }; 19 | 20 | export type GqlAggBlock = { 21 | key: string; 22 | value: GqlAggRules; 23 | }; 24 | 25 | export type GqlAggRules = { 26 | [aggOperationName: string]: any; 27 | aggs?: GqlAggBlock[]; 28 | }; 29 | 30 | export function prepareAggsInResolve( 31 | aggs: GqlAggBlock[], 32 | _fieldMap: any 33 | ): { [argName: string]: any } { 34 | if (Array.isArray(aggs)) { 35 | return convertAggsBlocks(aggs); 36 | } 37 | return aggs; 38 | } 39 | 40 | export function convertAggsBlocks(blockList: GqlAggBlock[]): ElasticAggsT { 41 | const result = {} as ElasticAggsT; 42 | blockList.forEach((block) => { 43 | if (block.key && block.value) { 44 | result[block.key] = convertAggsRules(block.value); 45 | } 46 | }); 47 | return result; 48 | } 49 | 50 | export function convertAggsRules(rules: GqlAggRules): ElasticAggsRulesT { 51 | if (typeof rules === 'string') return rules; 52 | const result = {} as ElasticAggsRulesT; 53 | Object.keys(rules).forEach((key) => { 54 | if (key === 'aggs' && rules.aggs) { 55 | result.aggs = convertAggsBlocks(rules.aggs); 56 | } else if (Array.isArray(rules[key])) { 57 | result[key.replace(/__/g, '.')] = rules[key].map((rule: any) => convertAggsRules(rule)); 58 | } else if (typeof rules[key] === 'object') { 59 | result[key.replace(/__/g, '.')] = convertAggsRules(rules[key]); 60 | } else { 61 | result[key.replace(/__/g, '.')] = rules[key]; 62 | } 63 | }); 64 | return result; 65 | } 66 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Bucket/Children.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getChildrenITC(opts: CommonOpts): InputTypeComposer { 5 | const name = getTypeName('AggsChildren', opts); 6 | const description = desc( 7 | ` 8 | A special single bucket aggregation that enables aggregating from buckets 9 | on parent document types to buckets on child documents. 10 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-children-aggregation.html) 11 | ` 12 | ); 13 | 14 | return opts.getOrCreateITC(name, () => ({ 15 | name, 16 | description, 17 | fields: { 18 | type: 'String', 19 | }, 20 | })); 21 | } 22 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Bucket/DateHistogram.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getCommonsScriptITC } from '../../Commons/Script'; 4 | import { getDateIntervalFC, getDateFormatFC, getDateTimeZoneFC } from '../../Commons/Date'; 5 | import { getDateFields } from '../../Commons/FieldNames'; 6 | 7 | export function getDateHistogramITC( 8 | opts: CommonOpts 9 | ): InputTypeComposer { 10 | const name = getTypeName('AggsDateHistogram', opts); 11 | const description = desc( 12 | ` 13 | A multi-bucket aggregation similar to the histogram except it can only 14 | be applied on date values. 15 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-datehistogram-aggregation.html) 16 | ` 17 | ); 18 | 19 | return opts.getOrCreateITC(name, () => ({ 20 | name, 21 | description, 22 | fields: { 23 | field: getDateFields(opts), 24 | interval: getDateIntervalFC(opts), 25 | time_zone: getDateTimeZoneFC(opts), 26 | offset: getDateIntervalFC(opts), 27 | format: getDateFormatFC(opts), 28 | missing: 'String', 29 | script: (): InputTypeComposer => getCommonsScriptITC(opts), 30 | }, 31 | })); 32 | } 33 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Bucket/DateRange.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getDateFormatFC, getDateTimeZoneFC, getDateRangeITC } from '../../Commons/Date'; 4 | import { getDateFields } from '../../Commons/FieldNames'; 5 | 6 | export function getAggsDateRangeITC( 7 | opts: CommonOpts 8 | ): InputTypeComposer { 9 | const name = getTypeName('AggsDateRange', opts); 10 | const description = desc( 11 | ` 12 | A range aggregation that is dedicated for date values. 13 | The \`from\` and \`to\` values can be expressed in Date Math expressions, 14 | and it is also possible to specify a date format by which the from 15 | and to response fields will be returned. 16 | Note that this aggregation includes the \`from\` value and excludes the 17 | \`to\` value for each range. 18 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-daterange-aggregation.html) 19 | ` 20 | ); 21 | 22 | return opts.getOrCreateITC(name, () => ({ 23 | name, 24 | description, 25 | fields: { 26 | field: getDateFields(opts), 27 | format: getDateFormatFC(opts), 28 | ranges: (): InputTypeComposer[] => [getDateRangeITC(opts)], 29 | time_zone: getDateTimeZoneFC(opts), 30 | }, 31 | })); 32 | } 33 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Bucket/DiversifiedSampler.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getCommonsScriptITC } from '../../Commons/Script'; 4 | import { getAllFields } from '../../Commons/FieldNames'; 5 | 6 | export function getDiversifiedSamplerITC( 7 | opts: CommonOpts 8 | ): InputTypeComposer { 9 | const name = getTypeName('AggsDiversifiedSampler', opts); 10 | const description = desc( 11 | ` 12 | A filtering aggregation used to limit any sub aggregations' processing to 13 | a sample of the top-scoring documents. Diversity settings are used to 14 | limit the number of matches that share a common value such as an "author". 15 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-diversified-sampler-aggregation.html) 16 | ` 17 | ); 18 | 19 | return opts.getOrCreateITC(name, () => ({ 20 | name, 21 | description, 22 | fields: { 23 | shard_size: { 24 | type: 'String', 25 | defaultValue: 100, 26 | }, 27 | field: getAllFields(opts), 28 | max_docs_per_value: 'Int', 29 | script: (): InputTypeComposer => getCommonsScriptITC(opts), 30 | execution_hint: 'String', 31 | }, 32 | })); 33 | } 34 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Bucket/Filter.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { InputTypeComposer } from 'graphql-compose'; 4 | import { CommonOpts /* getTypeName, desc */ } from '../../../utils'; 5 | import { getQueryITC } from '../../Query/Query'; 6 | 7 | export function getFilterITC(opts: CommonOpts): InputTypeComposer { 8 | // const name = getTypeName('AggsFilter', opts); 9 | // const description = desc( 10 | // ` 11 | // Defines a single bucket of all the documents in the current document set 12 | // context that match a specified filter. Often this will be used to narrow 13 | // down the current aggregation context to a specific set of documents. 14 | // [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-filter-aggregation.html) 15 | // ` 16 | // ); 17 | 18 | return getQueryITC(opts); 19 | } 20 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Bucket/Filters.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getFiltersITC(opts: CommonOpts): InputTypeComposer { 5 | const name = getTypeName('AggsFilters', opts); 6 | const description = desc( 7 | ` 8 | Defines a multi bucket aggregation where each bucket is associated 9 | with a filter. Each bucket will collect all documents that match 10 | its associated filter. 11 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-filters-aggregation.html) 12 | ` 13 | ); 14 | 15 | return opts.getOrCreateITC(name, () => ({ 16 | name, 17 | description, 18 | fields: { 19 | filters: 'JSON', 20 | other_bucket: 'Boolean', 21 | other_bucket_key: 'String', 22 | }, 23 | })); 24 | } 25 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Bucket/GeoDistance.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getGeoPointFC, getDistanceUnitFC, getDistanceCalculationModeFC } from '../../Commons/Geo'; 4 | import { getFloatRangeITC } from '../../Commons/Float'; 5 | import { getGeoPointFields } from '../../Commons/FieldNames'; 6 | 7 | export function getGeoDistanceITC( 8 | opts: CommonOpts 9 | ): InputTypeComposer { 10 | const name = getTypeName('AggsGeoDistance', opts); 11 | const description = desc( 12 | ` 13 | A multi-bucket aggregation that works on geo_point fields. The user can 14 | define a point of origin and a set of distance range buckets. 15 | The aggregation evaluate the distance of each document value from the 16 | origin point and determines the buckets it belongs to based on the ranges 17 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geodistance-aggregation.html) 18 | ` 19 | ); 20 | 21 | return opts.getOrCreateITC(name, () => ({ 22 | name, 23 | description, 24 | fields: { 25 | field: getGeoPointFields(opts), 26 | origin: getGeoPointFC(opts), 27 | ranges: [getFloatRangeITC(opts)], 28 | unit: getDistanceUnitFC(opts), 29 | distance_type: getDistanceCalculationModeFC(opts), 30 | }, 31 | })); 32 | } 33 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Bucket/GeohashGrid.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getGeoPointFields } from '../../Commons/FieldNames'; 4 | 5 | export function getGeohashGridITC( 6 | opts: CommonOpts 7 | ): InputTypeComposer { 8 | const name = getTypeName('AggsGeohashGrid', opts); 9 | const description = desc( 10 | ` 11 | A multi-bucket aggregation that works on geo_point fields and groups points 12 | into buckets that represent cells in a grid. Each cell is labeled using 13 | a geohash which is of user-definable precision. Geohashes can have a choice 14 | of precision between 1 and 12. 15 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geohashgrid-aggregation.html) 16 | ` 17 | ); 18 | 19 | return opts.getOrCreateITC(name, () => ({ 20 | name, 21 | description, 22 | fields: { 23 | field: getGeoPointFields(opts), 24 | precision: 'Int', 25 | size: { 26 | type: 'Int', 27 | defaultValue: 10000, 28 | }, 29 | shard_size: 'Int', 30 | }, 31 | })); 32 | } 33 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Bucket/Global.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getGlobalITC(opts: CommonOpts): InputTypeComposer { 5 | const name = getTypeName('AggsGlobal', opts); 6 | const description = desc( 7 | ` 8 | Defines a single bucket of all the documents within the search execution 9 | context. This context is defined by the indices and the document types 10 | you’re searching on, but is not influenced by the search query itself. 11 | Should have empty body, without fields, eg. \`global: {}\` 12 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-global-aggregation.html) 13 | ` 14 | ); 15 | 16 | return opts.getOrCreateITC(name, () => ({ 17 | name, 18 | description, 19 | fields: { 20 | _without_fields_: 'JSON', 21 | }, 22 | })); 23 | } 24 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Bucket/Histogram.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getNumericFields } from '../../Commons/FieldNames'; 4 | 5 | export function getHistogramITC(opts: CommonOpts): InputTypeComposer { 6 | const name = getTypeName('AggsHistogram', opts); 7 | const description = desc( 8 | ` 9 | A multi-bucket values source based aggregation that can be applied on 10 | numeric values extracted from the documents. It dynamically builds fixed 11 | size (a.k.a. interval) buckets over the values. 12 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-histogram-aggregation.html) 13 | ` 14 | ); 15 | 16 | return opts.getOrCreateITC(name, () => ({ 17 | name, 18 | description, 19 | fields: { 20 | field: getNumericFields(opts), 21 | interval: 'Float', 22 | missing: 'Float', 23 | min_doc_count: 'Int', 24 | extended_bounds: `input ${getTypeName('AggsHistogramBounds', opts)} { 25 | min: Float 26 | max: Float 27 | }`, 28 | order: 'JSON', 29 | offset: 'Int', 30 | keyed: 'Boolean', 31 | }, 32 | })); 33 | } 34 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Bucket/IpRange.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getIpRangeTypeITC } from '../../Commons/Ip'; 4 | import { getIpFields } from '../../Commons/FieldNames'; 5 | 6 | export function getIpRangeITC(opts: CommonOpts): InputTypeComposer { 7 | const name = getTypeName('AggsIpRange', opts); 8 | const description = desc( 9 | ` 10 | A range aggregation that is dedicated for IP values. 11 | Note that this aggregation includes the \`from\` value and excludes the 12 | \`to\` value for each range. 13 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-iprange-aggregation.html) 14 | ` 15 | ); 16 | 17 | return opts.getOrCreateITC(name, () => ({ 18 | name, 19 | description, 20 | fields: { 21 | field: getIpFields(opts), 22 | ranges: (): InputTypeComposer[] => [getIpRangeTypeITC(opts)], 23 | }, 24 | })); 25 | } 26 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Bucket/Missing.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getAllFields } from '../../Commons/FieldNames'; 4 | 5 | export function getMissingITC(opts: CommonOpts): InputTypeComposer { 6 | const name = getTypeName('AggsMissing', opts); 7 | const description = desc( 8 | ` 9 | A field data based single bucket aggregation, that creates a bucket of all 10 | documents in the current document set context that are missing a field 11 | value (effectively, missing a field or having the configured NULL value set). 12 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-missing-aggregation.html) 13 | ` 14 | ); 15 | 16 | return opts.getOrCreateITC(name, () => ({ 17 | name, 18 | description, 19 | fields: { 20 | field: getAllFields(opts), 21 | }, 22 | })); 23 | } 24 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Bucket/Nested.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getNestedITC(opts: CommonOpts): InputTypeComposer { 5 | const name = getTypeName('AggsNested', opts); 6 | const description = desc( 7 | ` 8 | A special single bucket aggregation that enables aggregating nested documents. 9 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-nested-aggregation.html) 10 | ` 11 | ); 12 | 13 | return opts.getOrCreateITC(name, () => ({ 14 | name, 15 | description, 16 | fields: { 17 | path: 'String', 18 | }, 19 | })); 20 | } 21 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Bucket/Range.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getFloatRangeKeyedITC } from '../../Commons/Float'; 4 | import { getCommonsScriptITC } from '../../Commons/Script'; 5 | import { getNumericFields } from '../../Commons/FieldNames'; 6 | 7 | export function getRangeITC(opts: CommonOpts): InputTypeComposer { 8 | const name = getTypeName('AggsRange', opts); 9 | const description = desc( 10 | ` 11 | A multi-bucket value source based aggregation that enables the user 12 | to define a set of ranges - each representing a bucket. 13 | Note that this aggregation includes the \`from\` value and excludes the 14 | \`to\` value for each range. 15 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-range-aggregation.html) 16 | ` 17 | ); 18 | 19 | return opts.getOrCreateITC(name, () => ({ 20 | name, 21 | description, 22 | fields: { 23 | field: getNumericFields(opts), 24 | ranges: (): InputTypeComposer[] => [getFloatRangeKeyedITC(opts)], 25 | keyed: 'Boolean', 26 | script: (): InputTypeComposer => getCommonsScriptITC(opts), 27 | }, 28 | })); 29 | } 30 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Bucket/ReverseNested.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getReverseNestedITC( 5 | opts: CommonOpts 6 | ): InputTypeComposer { 7 | const name = getTypeName('AggsReverseNested', opts); 8 | const description = desc( 9 | ` 10 | A special single bucket aggregation that enables aggregating on parent docs 11 | from nested documents. 12 | The \`reverse_nested\` aggregation must be defined inside a \`nested\` aggregation. 13 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-reverse-nested-aggregation.html) 14 | ` 15 | ); 16 | 17 | return opts.getOrCreateITC(name, () => ({ 18 | name, 19 | description, 20 | fields: { 21 | path: 'String', 22 | }, 23 | })); 24 | } 25 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Bucket/Sampler.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getSamplerITC(opts: CommonOpts): InputTypeComposer { 5 | const name = getTypeName('AggsSampler', opts); 6 | const description = desc( 7 | ` 8 | A filtering aggregation used to limit any sub aggregations' processing 9 | to a sample of the top-scoring documents. 10 | Tightening the focus of analytics to high-relevance matches rather than 11 | the potentially very long tail of low-quality matches. 12 | This functionality is experimental and may be changed or removed completely. 13 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-sampler-aggregation.html) 14 | ` 15 | ); 16 | 17 | return opts.getOrCreateITC(name, () => ({ 18 | name, 19 | description, 20 | fields: { 21 | shard_size: 'Int', 22 | }, 23 | })); 24 | } 25 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Bucket/SignificantTerms.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getAllFields } from '../../Commons/FieldNames'; 4 | 5 | export function getSignificantTermsITC( 6 | opts: CommonOpts 7 | ): InputTypeComposer { 8 | const name = getTypeName('AggsSignificantTerms', opts); 9 | const description = desc( 10 | ` 11 | An aggregation that returns interesting or unusual occurrences of terms in a set. 12 | The significant_terms aggregation can be very heavy when run on large indices. 13 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-significantterms-aggregation.html) 14 | ` 15 | ); 16 | 17 | return opts.getOrCreateITC(name, () => ({ 18 | name, 19 | description, 20 | fields: { 21 | field: getAllFields(opts), 22 | min_doc_count: 'Int', 23 | background_filter: 'JSON', 24 | execution_hint: 'String', 25 | }, 26 | })); 27 | } 28 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Bucket/Terms.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getCommonsScriptITC } from '../../Commons/Script'; 4 | import { getTermFields } from '../../Commons/FieldNames'; 5 | 6 | export function getTermsITC(opts: CommonOpts): InputTypeComposer { 7 | const name = getTypeName('AggsTerms', opts); 8 | const description = desc( 9 | ` 10 | A multi-bucket value source based aggregation where buckets 11 | are dynamically built - one per unique value. 12 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html) 13 | ` 14 | ); 15 | 16 | return opts.getOrCreateITC(name, () => ({ 17 | name, 18 | description, 19 | fields: { 20 | field: (): any => getTermFields(opts), 21 | size: { 22 | type: 'Int', 23 | defaultValue: 10, 24 | }, 25 | shard_size: 'Int', 26 | order: 'JSON', 27 | include: 'JSON', 28 | exclude: 'JSON', 29 | script: (): InputTypeComposer => getCommonsScriptITC(opts), 30 | execution_hint: 'String', 31 | missing: 'JSON', 32 | }, 33 | })); 34 | } 35 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Metrics/Avg.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getCommonsScriptITC } from '../../Commons/Script'; 4 | import { getNumericFields } from '../../Commons/FieldNames'; 5 | 6 | export function getAvgITC(opts: CommonOpts): InputTypeComposer { 7 | const name = getTypeName('AggsAvg', opts); 8 | const description = desc( 9 | ` 10 | A single-value metrics aggregation that computes the average 11 | of numeric values that are extracted from the aggregated documents. 12 | These values can be extracted either from specific numeric fields 13 | in the documents, or be generated by a provided script. 14 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-avg-aggregation.html) 15 | ` 16 | ); 17 | 18 | return opts.getOrCreateITC(name, () => ({ 19 | name, 20 | description, 21 | fields: { 22 | field: getNumericFields(opts), 23 | missing: 'Float', 24 | script: (): InputTypeComposer => getCommonsScriptITC(opts), 25 | }, 26 | })); 27 | } 28 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Metrics/Cardinality.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getCommonsScriptITC } from '../../Commons/Script'; 4 | import { getAllFields } from '../../Commons/FieldNames'; 5 | 6 | export function getCardinalityITC( 7 | opts: CommonOpts 8 | ): InputTypeComposer { 9 | const name = getTypeName('AggsCardinality', opts); 10 | const description = desc( 11 | ` 12 | A single-value metrics aggregation that calculates an approximate count 13 | of distinct values. Values can be extracted either from specific fields 14 | in the document or generated by a script. 15 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-cardinality-aggregation.html) 16 | ` 17 | ); 18 | 19 | return opts.getOrCreateITC(name, () => ({ 20 | name, 21 | description, 22 | fields: { 23 | field: getAllFields(opts), 24 | precision_threshold: { 25 | type: 'Int', 26 | defaultValue: 3000, 27 | description: desc( 28 | ` 29 | Allows to trade memory for accuracy, and defines a unique count 30 | below which counts are expected to be close to accurate. 31 | ` 32 | ), 33 | }, 34 | missing: 'String', 35 | script: (): InputTypeComposer => getCommonsScriptITC(opts), 36 | }, 37 | })); 38 | } 39 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Metrics/ExtendedStats.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getCommonsScriptITC } from '../../Commons/Script'; 4 | import { getNumericFields } from '../../Commons/FieldNames'; 5 | 6 | export function getExtendedStatsITC( 7 | opts: CommonOpts 8 | ): InputTypeComposer { 9 | const name = getTypeName('AggsExtendedStats', opts); 10 | const description = desc( 11 | ` 12 | A multi-value metrics aggregation that computes stats over numeric values 13 | extracted from the aggregated documents. These values can be extracted 14 | either from specific numeric fields in the documents, or be generated 15 | by a provided script. 16 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-extendedstats-aggregation.html) 17 | ` 18 | ); 19 | 20 | return opts.getOrCreateITC(name, () => ({ 21 | name, 22 | description, 23 | fields: { 24 | field: getNumericFields(opts), 25 | sigma: 'Float', 26 | missing: 'Float', 27 | script: (): InputTypeComposer => getCommonsScriptITC(opts), 28 | }, 29 | })); 30 | } 31 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Metrics/GeoBounds.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getGeoPointFields } from '../../Commons/FieldNames'; 4 | 5 | export function getGeoBoundsITC(opts: CommonOpts): InputTypeComposer { 6 | const name = getTypeName('AggsGeoBounds', opts); 7 | const description = desc( 8 | ` 9 | A metric aggregation that computes the bounding box containing 10 | all geo_point values for a field. 11 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-geobounds-aggregation.html) 12 | ` 13 | ); 14 | 15 | return opts.getOrCreateITC(name, () => ({ 16 | name, 17 | description, 18 | fields: { 19 | field: getGeoPointFields(opts), 20 | wrap_longitude: 'Boolean', 21 | }, 22 | })); 23 | } 24 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Metrics/GeoCentroid.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getGeoPointFields } from '../../Commons/FieldNames'; 4 | 5 | export function getGeoCentroidITC( 6 | opts: CommonOpts 7 | ): InputTypeComposer { 8 | const name = getTypeName('AggsGeoCentroid', opts); 9 | const description = desc( 10 | ` 11 | A metric aggregation that computes the weighted centroid from all coordinate 12 | values for a Geo-point datatype field. 13 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-geocentroid-aggregation.html) 14 | ` 15 | ); 16 | 17 | return opts.getOrCreateITC(name, () => ({ 18 | name, 19 | description, 20 | fields: { 21 | field: getGeoPointFields(opts), 22 | }, 23 | })); 24 | } 25 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Metrics/Max.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getCommonsScriptITC } from '../../Commons/Script'; 4 | import { getNumericFields } from '../../Commons/FieldNames'; 5 | 6 | export function getMaxITC(opts: CommonOpts): InputTypeComposer { 7 | const name = getTypeName('AggsMax', opts); 8 | const description = desc( 9 | ` 10 | A single-value metrics aggregation that keeps track and returns the maximum 11 | value among the numeric values extracted from the aggregated documents. 12 | These values can be extracted either from specific numeric fields 13 | in the documents, or be generated by a provided script. 14 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-max-aggregation.html) 15 | ` 16 | ); 17 | 18 | return opts.getOrCreateITC(name, () => ({ 19 | name, 20 | description, 21 | fields: { 22 | field: getNumericFields(opts), 23 | missing: 'Float', 24 | script: (): InputTypeComposer => getCommonsScriptITC(opts), 25 | }, 26 | })); 27 | } 28 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Metrics/Min.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getCommonsScriptITC } from '../../Commons/Script'; 4 | import { getNumericFields } from '../../Commons/FieldNames'; 5 | 6 | export function getMinITC(opts: CommonOpts): InputTypeComposer { 7 | const name = getTypeName('AggsMin', opts); 8 | const description = desc( 9 | ` 10 | A single-value metrics aggregation that keeps track and returns the minimum 11 | value among the numeric values extracted from the aggregated documents. 12 | These values can be extracted either from specific numeric fields 13 | in the documents, or be generated by a provided script. 14 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-min-aggregation.html) 15 | ` 16 | ); 17 | 18 | return opts.getOrCreateITC(name, () => ({ 19 | name, 20 | description, 21 | fields: { 22 | field: getNumericFields(opts), 23 | missing: 'Float', 24 | script: (): InputTypeComposer => getCommonsScriptITC(opts), 25 | }, 26 | })); 27 | } 28 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Metrics/PercentileRanks.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getCommonsScriptITC } from '../../Commons/Script'; 4 | import { getCommonsHdrITC } from '../../Commons/HDR'; 5 | import { getNumericFields } from '../../Commons/FieldNames'; 6 | 7 | export function getPercentileRanksITC( 8 | opts: CommonOpts 9 | ): InputTypeComposer { 10 | const name = getTypeName('AggsPercentileRanks', opts); 11 | const description = desc( 12 | ` 13 | A multi-value metrics aggregation that calculates one or more percentile 14 | ranks over numeric values extracted from the aggregated documents. 15 | These values can be extracted either from specific numeric fields 16 | in the documents, or be generated by a provided script. 17 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-percentile-rank-aggregation.html) 18 | ` 19 | ); 20 | 21 | return opts.getOrCreateITC(name, () => ({ 22 | name, 23 | description, 24 | fields: { 25 | field: getNumericFields(opts), 26 | values: '[Float]', 27 | hdr: (): InputTypeComposer => getCommonsHdrITC(opts), 28 | missing: 'Float', 29 | script: (): InputTypeComposer => getCommonsScriptITC(opts), 30 | }, 31 | })); 32 | } 33 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Metrics/Percentiles.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getCommonsScriptITC } from '../../Commons/Script'; 4 | import { getCommonsHdrITC } from '../../Commons/HDR'; 5 | import { getNumericFields } from '../../Commons/FieldNames'; 6 | 7 | export function getPercentilesITC( 8 | opts: CommonOpts 9 | ): InputTypeComposer { 10 | const name = getTypeName('AggsPercentiles', opts); 11 | const description = desc( 12 | ` 13 | A multi-value metrics aggregation that calculates one or more percentiles 14 | over numeric values extracted from the aggregated documents. 15 | These values can be extracted either from specific numeric fields 16 | in the documents, or be generated by a provided script. 17 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-percentile-aggregation.html) 18 | ` 19 | ); 20 | 21 | return opts.getOrCreateITC(name, () => ({ 22 | name, 23 | description, 24 | fields: { 25 | field: getNumericFields(opts), 26 | percents: '[Float]', 27 | tdigest: `input ${getTypeName('AggsPercentilesTDigest', opts)} { 28 | compression: Int, 29 | }`, 30 | hdr: (): InputTypeComposer => getCommonsHdrITC(opts), 31 | missing: 'Float', 32 | script: (): InputTypeComposer => getCommonsScriptITC(opts), 33 | }, 34 | })); 35 | } 36 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Metrics/ScriptedMetric.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getCommonsScriptITC } from '../../Commons/Script'; 4 | 5 | export function getScriptedMetricITC( 6 | opts: CommonOpts 7 | ): InputTypeComposer { 8 | const name = getTypeName('AggsScriptedMetric', opts); 9 | const description = desc( 10 | ` 11 | A metric aggregation that executes using scripts to provide a metric output. 12 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-scripted-metric-aggregation.html) 13 | ` 14 | ); 15 | 16 | return opts.getOrCreateITC(name, () => ({ 17 | name, 18 | description, 19 | // $FlowFixMe 20 | fields: { 21 | init_script: () => getCommonsScriptITC(opts), 22 | map_script: () => getCommonsScriptITC(opts), 23 | combine_script: () => getCommonsScriptITC(opts), 24 | reduce_script: () => getCommonsScriptITC(opts), 25 | params: `input ${getTypeName('AggsScriptedMetricParams', opts)} { 26 | field: String 27 | _agg: JSON! 28 | }`, 29 | }, 30 | })); 31 | } 32 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Metrics/Stats.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getCommonsScriptITC } from '../../Commons/Script'; 4 | import { getNumericFields } from '../../Commons/FieldNames'; 5 | 6 | export function getStatsITC(opts: CommonOpts): InputTypeComposer { 7 | const name = getTypeName('AggsStats', opts); 8 | const description = desc( 9 | ` 10 | A multi-value metrics aggregation that computes stats over numeric values 11 | extracted from the aggregated documents. These values can be extracted 12 | either from specific numeric fields in the documents, or be generated 13 | by a provided script. 14 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-stats-aggregation.html) 15 | ` 16 | ); 17 | 18 | return opts.getOrCreateITC(name, () => ({ 19 | name, 20 | description, 21 | fields: { 22 | field: getNumericFields(opts), 23 | missing: 'Float', 24 | script: (): InputTypeComposer => getCommonsScriptITC(opts), 25 | }, 26 | })); 27 | } 28 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Metrics/Sum.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getCommonsScriptITC } from '../../Commons/Script'; 4 | import { getNumericFields } from '../../Commons/FieldNames'; 5 | 6 | export function getSumITC(opts: CommonOpts): InputTypeComposer { 7 | const name = getTypeName('AggsSum', opts); 8 | const description = desc( 9 | ` 10 | A single-value metrics aggregation that sums up numeric values that are 11 | extracted from the aggregated documents. These values can be extracted 12 | either from specific numeric fields in the documents, or be generated 13 | by a provided script. 14 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-sum-aggregation.html) 15 | ` 16 | ); 17 | 18 | return opts.getOrCreateITC(name, () => ({ 19 | name, 20 | description, 21 | fields: { 22 | field: getNumericFields(opts), 23 | missing: 'Float', 24 | script: (): InputTypeComposer => getCommonsScriptITC(opts), 25 | }, 26 | })); 27 | } 28 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Metrics/TopHits.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getTopHitsITC(opts: CommonOpts): InputTypeComposer { 5 | const name = getTypeName('AggsTopHits', opts); 6 | const description = desc( 7 | ` 8 | A top_hits metric aggregator keeps track of the most relevant document being 9 | aggregated. This aggregator is intended to be used as a sub aggregator, 10 | so that the top matching documents can be aggregated per bucket. 11 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-top-hits-aggregation.html#search-aggregations-metrics-top-hits-aggregation) 12 | ` 13 | ); 14 | 15 | return opts.getOrCreateITC(name, () => ({ 16 | name, 17 | description, 18 | fields: { 19 | from: 'Int', 20 | size: 'Int', 21 | sort: 'JSON', 22 | _source: 'JSON', 23 | }, 24 | })); 25 | } 26 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Metrics/ValueCount.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getCommonsScriptITC } from '../../Commons/Script'; 4 | import { getAllFields } from '../../Commons/FieldNames'; 5 | 6 | export function getValueCountITC( 7 | opts: CommonOpts 8 | ): InputTypeComposer { 9 | const name = getTypeName('AggsValueCount', opts); 10 | const description = desc( 11 | ` 12 | A single-value metrics aggregation that counts the number of values that 13 | are extracted from the aggregated documents. 14 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-valuecount-aggregation.html) 15 | ` 16 | ); 17 | 18 | return opts.getOrCreateITC(name, () => ({ 19 | name, 20 | description, 21 | fields: { 22 | field: getAllFields(opts), 23 | script: (): InputTypeComposer => getCommonsScriptITC(opts), 24 | }, 25 | })); 26 | } 27 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Pipeline/AvgBucket.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getAvgBucketITC(opts: CommonOpts): InputTypeComposer { 5 | const name = getTypeName('AggsAvgBucket', opts); 6 | const description = desc( 7 | ` 8 | A sibling pipeline aggregation which calculates the (mean) average value 9 | of a specified metric in a sibling aggregation. The specified metric must 10 | be numeric and the sibling aggregation must be a multi-bucket aggregation. 11 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-avg-bucket-aggregation.html) 12 | ` 13 | ); 14 | 15 | return opts.getOrCreateITC(name, () => ({ 16 | name, 17 | description, 18 | fields: { 19 | buckets_path: 'String!', 20 | gap_policy: 'String', 21 | format: 'String', 22 | }, 23 | })); 24 | } 25 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Pipeline/BucketScript.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getBucketScriptITC( 5 | opts: CommonOpts 6 | ): InputTypeComposer { 7 | const name = getTypeName('AggsBucketScript', opts); 8 | const description = desc( 9 | ` 10 | A parent pipeline aggregation which executes a script which can perform 11 | per bucket computations on specified metrics in the parent multi-bucket 12 | aggregation. The specified metric must be numeric and the script must 13 | return a numeric value. 14 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-bucket-script-aggregation.html) 15 | ` 16 | ); 17 | 18 | return opts.getOrCreateITC(name, () => ({ 19 | name, 20 | description, 21 | fields: { 22 | buckets_path: 'JSON!', 23 | script: 'String!', 24 | format: 'String', 25 | gap_policy: 'String', 26 | }, 27 | })); 28 | } 29 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Pipeline/BucketSelector.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getBucketSelectorITC( 5 | opts: CommonOpts 6 | ): InputTypeComposer { 7 | const name = getTypeName('AggsBucketSelector', opts); 8 | const description = desc( 9 | ` 10 | A parent pipeline aggregation which executes a script which determines 11 | whether the current bucket will be retained in the parent multi-bucket 12 | aggregation. The specified metric must be numeric and the script must 13 | return a boolean value. If the script language is expression then a numeric 14 | return value is permitted. In this case 0.0 will be evaluated as false 15 | and all other values will evaluate to true. 16 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-bucket-selector-aggregation.html) 17 | ` 18 | ); 19 | 20 | return opts.getOrCreateITC(name, () => ({ 21 | name, 22 | description, 23 | fields: { 24 | buckets_path: 'JSON!', 25 | script: 'String!', 26 | gap_policy: 'String', 27 | }, 28 | })); 29 | } 30 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Pipeline/CumulativeSum.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getCumulativeSumITC( 5 | opts: CommonOpts 6 | ): InputTypeComposer { 7 | const name = getTypeName('AggsCumulativeSum', opts); 8 | const description = desc( 9 | ` 10 | A parent pipeline aggregation which calculates the cumulative sum of a 11 | specified metric in a parent histogram (or date_histogram) aggregation. 12 | The specified metric must be numeric and the enclosing histogram must 13 | have min_doc_count set to 0 (default for histogram aggregations). 14 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-cumulative-sum-aggregation.html) 15 | ` 16 | ); 17 | 18 | return opts.getOrCreateITC(name, () => ({ 19 | name, 20 | description, 21 | fields: { 22 | buckets_path: 'String!', 23 | format: 'String', 24 | }, 25 | })); 26 | } 27 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Pipeline/Derivative.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getDerivativeITC( 5 | opts: CommonOpts 6 | ): InputTypeComposer { 7 | const name = getTypeName('AggsDerivative', opts); 8 | const description = desc( 9 | ` 10 | A parent pipeline aggregation which calculates the derivative of a specified 11 | metric in a parent histogram (or date_histogram) aggregation. The specified 12 | metric must be numeric and the enclosing histogram must have min_doc_count 13 | set to 0 (default for histogram aggregations). 14 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-derivative-aggregation.html) 15 | ` 16 | ); 17 | 18 | return opts.getOrCreateITC(name, () => ({ 19 | name, 20 | description, 21 | fields: { 22 | buckets_path: 'String!', 23 | gap_policy: 'String', 24 | format: 'String', 25 | unit: 'String', 26 | }, 27 | })); 28 | } 29 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Pipeline/ExtendedStatsBucket.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getExtendedStatsBucketITC( 5 | opts: CommonOpts 6 | ): InputTypeComposer { 7 | const name = getTypeName('AggsExtendedStatsBucket', opts); 8 | const description = desc( 9 | ` 10 | A sibling pipeline aggregation which calculates a variety of stats across 11 | all bucket of a specified metric in a sibling aggregation. The specified 12 | metric must be numeric and the sibling aggregation must be a multi-bucket 13 | aggregation. 14 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-extended-stats-bucket-aggregation.html) 15 | ` 16 | ); 17 | 18 | return opts.getOrCreateITC(name, () => ({ 19 | name, 20 | description, 21 | fields: { 22 | buckets_path: 'String!', 23 | gap_policy: 'String', 24 | format: 'String', 25 | sigma: 'Float', 26 | }, 27 | })); 28 | } 29 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Pipeline/MaxBucket.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getMaxBucketITC(opts: CommonOpts): InputTypeComposer { 5 | const name = getTypeName('AggsMaxBucket', opts); 6 | const description = desc( 7 | ` 8 | A sibling pipeline aggregation which identifies the bucket(s) with 9 | the maximum value of a specified metric in a sibling aggregation and 10 | outputs both the value and the key(s) of the bucket(s). The specified 11 | metric must be numeric and the sibling aggregation must be a multi-bucket 12 | aggregation. 13 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-max-bucket-aggregation.html) 14 | ` 15 | ); 16 | 17 | return opts.getOrCreateITC(name, () => ({ 18 | name, 19 | description, 20 | fields: { 21 | buckets_path: 'String!', 22 | gap_policy: 'String', 23 | format: 'String', 24 | }, 25 | })); 26 | } 27 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Pipeline/MinBucket.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getMinBucketITC(opts: CommonOpts): InputTypeComposer { 5 | const name = getTypeName('AggsMinBucket', opts); 6 | const description = desc( 7 | ` 8 | A sibling pipeline aggregation which identifies the bucket(s) with 9 | the minimum value of a specified metric in a sibling aggregation and 10 | outputs both the value and the key(s) of the bucket(s). The specified 11 | metric must be numeric and the sibling aggregation must be a multi-bucket 12 | aggregation. 13 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-min-bucket-aggregation.html) 14 | ` 15 | ); 16 | 17 | return opts.getOrCreateITC(name, () => ({ 18 | name, 19 | description, 20 | fields: { 21 | buckets_path: 'String!', 22 | gap_policy: 'String', 23 | format: 'String', 24 | }, 25 | })); 26 | } 27 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Pipeline/MovingAverage.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getMovingAverageITC( 5 | opts: CommonOpts 6 | ): InputTypeComposer { 7 | const name = getTypeName('AggsMovingAverage', opts); 8 | const description = desc( 9 | ` 10 | Given an ordered series of data, the Moving Average aggregation will slide 11 | a window across the data and emit the average value of that window. 12 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-movavg-aggregation.html) 13 | ` 14 | ); 15 | 16 | return opts.getOrCreateITC(name, () => ({ 17 | name, 18 | description, 19 | fields: { 20 | buckets_path: 'String!', 21 | format: 'String', 22 | window: 'Int', 23 | gap_policy: 'String', 24 | model: 'String', 25 | settings: 'JSON', 26 | }, 27 | })); 28 | } 29 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Pipeline/PercentilesBucket.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getPercentilesBucketITC( 5 | opts: CommonOpts 6 | ): InputTypeComposer { 7 | const name = getTypeName('AggsPercentilesBucket', opts); 8 | const description = desc( 9 | ` 10 | A sibling pipeline aggregation which calculates percentiles across all 11 | bucket of a specified metric in a sibling aggregation. The specified metric 12 | must be numeric and the sibling aggregation must be a multi-bucket aggregation. 13 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-percentiles-bucket-aggregation.html) 14 | ` 15 | ); 16 | 17 | return opts.getOrCreateITC(name, () => ({ 18 | name, 19 | description, 20 | fields: { 21 | buckets_path: 'String!', 22 | gap_policy: 'String', 23 | format: 'String', 24 | percents: '[Float]', 25 | }, 26 | })); 27 | } 28 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Pipeline/SerialDifferencing.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getSerialDifferencingITC( 5 | opts: CommonOpts 6 | ): InputTypeComposer { 7 | const name = getTypeName('AggsSerialDifferencing', opts); 8 | const description = desc( 9 | ` 10 | Serial differencing is a technique where values in a time series are 11 | subtracted from itself at different time lags or periods. 12 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-serialdiff-aggregation.html) 13 | ` 14 | ); 15 | 16 | return opts.getOrCreateITC(name, () => ({ 17 | name, 18 | description, 19 | fields: { 20 | buckets_path: 'String!', 21 | lag: 'String', 22 | gap_policy: 'String', 23 | format: 'String', 24 | }, 25 | })); 26 | } 27 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Pipeline/StatsBucket.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getStatsBucketITC( 5 | opts: CommonOpts 6 | ): InputTypeComposer { 7 | const name = getTypeName('AggsStatsBucket', opts); 8 | const description = desc( 9 | ` 10 | A sibling pipeline aggregation which calculates a variety of stats across 11 | all bucket of a specified metric in a sibling aggregation. The specified 12 | metric must be numeric and the sibling aggregation must be a multi-bucket 13 | aggregation. 14 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-stats-bucket-aggregation.html) 15 | ` 16 | ); 17 | 18 | return opts.getOrCreateITC(name, () => ({ 19 | name, 20 | description, 21 | fields: { 22 | buckets_path: 'String!', 23 | gap_policy: 'String', 24 | format: 'String', 25 | }, 26 | })); 27 | } 28 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/Pipeline/SumBucket.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getSumBucketITC(opts: CommonOpts): InputTypeComposer { 5 | const name = getTypeName('AggsSumBucket', opts); 6 | const description = desc( 7 | ` 8 | A sibling pipeline aggregation which calculates the sum across all bucket 9 | of a specified metric in a sibling aggregation. The specified metric must 10 | be numeric and the sibling aggregation must be a multi-bucket aggregation. 11 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-sum-bucket-aggregation.html) 12 | ` 13 | ); 14 | 15 | return opts.getOrCreateITC(name, () => ({ 16 | name, 17 | description, 18 | fields: { 19 | buckets_path: 'String!', 20 | gap_policy: 'String', 21 | format: 'String', 22 | }, 23 | })); 24 | } 25 | -------------------------------------------------------------------------------- /src/elasticDSL/Aggs/__tests__/Aggs-test.ts: -------------------------------------------------------------------------------- 1 | import { schemaComposer, graphql } from 'graphql-compose'; 2 | import { getAggsITC, prepareAggsInResolve, convertAggsBlocks, convertAggsRules } from '../Aggs'; 3 | import { prepareCommonOpts } from '../../../utils'; 4 | 5 | const { printSchema } = graphql; 6 | 7 | beforeEach(() => { 8 | schemaComposer.clear(); 9 | }); 10 | 11 | describe('AGGS args converter', () => { 12 | it('Aggs DSL', () => { 13 | schemaComposer.Query.addFields({ 14 | search: { 15 | args: { 16 | body: { 17 | type: getAggsITC( 18 | prepareCommonOpts(schemaComposer, { 19 | prefix: 'Elastic_', 20 | postfix: '_50', 21 | }) 22 | ), 23 | }, 24 | }, 25 | type: 'Int', 26 | }, 27 | }); 28 | const schema = schemaComposer.buildSchema(); 29 | expect(printSchema(schema)).toMatchSnapshot(); 30 | }); 31 | 32 | it('convertAggsRules()', () => { 33 | expect(convertAggsRules({ some: { field: 1 } })).toEqual({ 34 | some: { field: 1 }, 35 | }); 36 | }); 37 | 38 | it('convertAggsBlocks()', () => { 39 | expect( 40 | convertAggsBlocks([ 41 | { key: 'field1', value: {} }, 42 | { key: 'field2', value: {} }, 43 | ]) 44 | ).toEqual({ 45 | field1: {}, 46 | field2: {}, 47 | }); 48 | }); 49 | 50 | it('should convert recursively aggs', () => { 51 | expect( 52 | convertAggsBlocks([{ key: 'field1', value: { aggs: [{ key: 'field2', value: {} }] } }]) 53 | ).toEqual({ field1: { aggs: { field2: {} } } }); 54 | }); 55 | 56 | it('prepareAggsInResolve()', () => { 57 | expect( 58 | prepareAggsInResolve([ 59 | { key: 'field1', value: { term: 'a' } }, 60 | { key: 'field2', value: { term: 'b' } }, 61 | ]) 62 | ).toEqual({ 63 | field1: { term: 'a' }, 64 | field2: { term: 'b' }, 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /src/elasticDSL/Commons/Date.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { InputTypeComposer, InputTypeComposerFieldConfigAsObjectDefinition } from 'graphql-compose'; 4 | import { getTypeName, CommonOpts, desc } from '../../utils'; 5 | 6 | export function getDateRangeITC(opts: CommonOpts): InputTypeComposer { 7 | const name = getTypeName('DateRange', opts); 8 | const description = desc(`Date range where \`from\` value includes and \`to\` value excludes.`); 9 | 10 | return opts.getOrCreateITC(name, () => ({ 11 | name, 12 | description, 13 | fields: { 14 | from: getDateMathFC(opts), 15 | to: getDateMathFC(opts), 16 | }, 17 | })); 18 | } 19 | 20 | export function getDateFormatFC( 21 | _opts: CommonOpts 22 | ): InputTypeComposerFieldConfigAsObjectDefinition { 23 | return { 24 | type: 'String', 25 | description: desc( 26 | ` 27 | Date Format/Patter. Eg MM-yyy returns 08-2012. 28 | [JodaDate](http://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html) 29 | ` 30 | ), 31 | }; 32 | } 33 | 34 | export function getDateIntervalFC( 35 | _opts: CommonOpts 36 | ): InputTypeComposerFieldConfigAsObjectDefinition { 37 | return { 38 | type: 'String', 39 | description: desc( 40 | ` 41 | Available expressions for interval: year, quarter, month, week, day, hour, 42 | minute, second. 43 | Or time units, like 2d for 2 days. h, m, s, ms, micros, nanos. 44 | ` 45 | ), 46 | }; 47 | } 48 | 49 | export function getDateMathFC( 50 | _opts: CommonOpts 51 | ): InputTypeComposerFieldConfigAsObjectDefinition { 52 | return { 53 | type: 'String', 54 | description: desc( 55 | ` 56 | The expression starts with an anchor date, which can either be now, 57 | or a date string ending with ||. Eg \`2015-01-01||+1M/d\` means 2015-01-01 58 | plus one month, rounded down to the nearest day. Or \`now+1h+1m\`. 59 | The supported time units: y, M, w, d, h, m, s 60 | ` 61 | ), 62 | }; 63 | } 64 | 65 | export function getDateTimeZoneFC( 66 | _opts: CommonOpts 67 | ): InputTypeComposerFieldConfigAsObjectDefinition { 68 | return { 69 | type: 'String', 70 | description: desc( 71 | ` 72 | Time zones may either be specified as an ISO 8601 UTC offset 73 | (e.g. +01:00 or -08:00) or as one of the time zone ids from the 74 | [TZ database](http://www.joda.org/joda-time/timezones.html). 75 | ` 76 | ), 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /src/elasticDSL/Commons/FieldNames.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | 3 | import { 4 | upperFirst, 5 | EnumTypeComposer, 6 | InputTypeComposerFieldConfigMapDefinition, 7 | ScalarTypeComposer, 8 | } from 'graphql-compose'; 9 | import type { EnumTypeComposerValueConfigMapDefinition } from 'graphql-compose/lib/EnumTypeComposer'; 10 | import { getTypeName, CommonOpts, desc } from '../../utils'; 11 | 12 | export type ElasticDataType = string; 13 | 14 | export function getStringFields(opts: CommonOpts) { 15 | return getFieldNamesType(opts, ['text', 'keyword', 'string'], 'String'); 16 | } 17 | 18 | export function getStringAsFieldConfigMap(opts: CommonOpts, fc: any) { 19 | return getFieldConfigMap(opts, ['text', 'keyword', 'string'], fc); 20 | } 21 | 22 | export function getAnalyzedFields(opts: CommonOpts) { 23 | return getFieldNamesType(opts, ['text', 'string'], 'String', true); 24 | } 25 | 26 | export function getAnalyzedAsFieldConfigMap(opts: CommonOpts, fc: any) { 27 | return getFieldConfigMap(opts, ['text', 'string'], fc, true); 28 | } 29 | 30 | export function getKeywordAsFieldConfigMap(opts: CommonOpts, fc: any) { 31 | return getFieldConfigMap(opts, ['keyword'], fc); 32 | } 33 | 34 | export function getNumericFields(opts: CommonOpts) { 35 | return getFieldNamesType( 36 | opts, 37 | [ 38 | 'byte', 39 | 'short', 40 | 'integer', 41 | 'long', 42 | 'double', 43 | 'float', 44 | 'half_float', 45 | 'scaled_float', 46 | 'token_count', 47 | ], 48 | 'Numeric' 49 | ); 50 | } 51 | 52 | export function getDateFields(opts: CommonOpts) { 53 | return getFieldNamesType(opts, ['date'], 'Date'); 54 | } 55 | 56 | export function getBooleanFields(opts: CommonOpts) { 57 | return getFieldNamesType(opts, ['boolean'], 'Boolean'); 58 | } 59 | 60 | export function getGeoPointFields(opts: CommonOpts) { 61 | return getFieldNamesType(opts, ['geo_point'], 'GeoPoint'); 62 | } 63 | 64 | export function getGeoPointAsFieldConfigMap(opts: CommonOpts, fc: any) { 65 | return getFieldConfigMap(opts, ['geo_point'], fc); 66 | } 67 | 68 | export function getGeoShapeAsFieldConfigMap(opts: CommonOpts, fc: any) { 69 | return getFieldConfigMap(opts, ['geo_shape'], fc); 70 | } 71 | 72 | export function getNestedFields(opts: CommonOpts) { 73 | return getFieldNamesType(opts, ['nested'], 'Nested'); 74 | } 75 | 76 | export function getIpFields(opts: CommonOpts) { 77 | return getFieldNamesType(opts, ['ip'], 'Ip'); 78 | } 79 | 80 | export function getPercolatorFields(opts: CommonOpts) { 81 | return getFieldNamesType(opts, ['percolator'], 'Percolator'); 82 | } 83 | 84 | export function getTermFields(opts: CommonOpts) { 85 | return getFieldNamesType( 86 | opts, 87 | [ 88 | 'keyword', 89 | 'date', 90 | 'boolean', 91 | 'ip', 92 | 'byte', 93 | 'short', 94 | 'integer', 95 | 'long', 96 | 'double', 97 | 'float', 98 | 'half_float', 99 | 'scaled_float', 100 | 'token_count', 101 | ], 102 | 'Term' 103 | ); 104 | } 105 | 106 | export function getAllFields(opts: CommonOpts) { 107 | return getFieldNamesType(opts, ['_all'], 'All'); 108 | } 109 | 110 | export function getAllAsFieldConfigMap(opts: CommonOpts, fc: any) { 111 | return getFieldConfigMap(opts, ['_all'], fc); 112 | } 113 | 114 | export function getFieldNamesByElasticType(fieldMap: any, types: ElasticDataType[]): string[] { 115 | const fieldNames = [] as string[]; 116 | types.forEach((type) => { 117 | if (typeof fieldMap[type] === 'object') { 118 | Object.keys(fieldMap[type]).forEach((fieldName) => { 119 | fieldNames.push(fieldName); 120 | }); 121 | } 122 | }); 123 | return fieldNames; 124 | } 125 | 126 | export function getFieldNamesType( 127 | opts: CommonOpts, 128 | types: ElasticDataType[], 129 | typePrefix: string, 130 | addAll: boolean = false 131 | ): EnumTypeComposer | ScalarTypeComposer | string { 132 | if (!opts || !opts.fieldMap) { 133 | return 'String'; 134 | } 135 | 136 | if (!types) { 137 | types = ['_all']; 138 | typePrefix = 'All'; 139 | } 140 | if (!typePrefix) { 141 | types.sort(); 142 | typePrefix = types.map((t) => upperFirst(t)).join(''); 143 | } 144 | const name = getTypeName(`${typePrefix}Fields`, opts); 145 | const description = desc(`Available fields from mapping.`); 146 | 147 | return opts.schemaComposer.getOrSet(name, () => { 148 | if (!opts || !opts.fieldMap) { 149 | return opts.schemaComposer.getSTC('String'); 150 | } 151 | const values = getEnumValues(opts.fieldMap, types, addAll); 152 | 153 | if (Object.keys(values).length === 0) { 154 | return opts.schemaComposer.getSTC('String'); 155 | } 156 | 157 | return opts.schemaComposer.createEnumTC({ 158 | name, 159 | description, 160 | values, 161 | }); 162 | }) as any; 163 | } 164 | 165 | function getEnumValues( 166 | fieldMap: any, 167 | types: ElasticDataType[], 168 | addAll: boolean = false 169 | ): EnumTypeComposerValueConfigMapDefinition { 170 | const values = {} as Record; 171 | if (addAll) { 172 | values._all = { 173 | value: '_all', 174 | }; 175 | } 176 | getFieldNamesByElasticType(fieldMap, types).forEach((fieldName) => { 177 | values[fieldName] = { 178 | value: fieldName.replace(/__/g, '.'), 179 | }; 180 | }); 181 | return values; 182 | } 183 | 184 | // FieldsMap generated by this function, contain underscored field names 185 | // So you should manually reassemble args before sending query to ElasticSearch 186 | // for renaming { "field__subField": 123 } to { "field.subField": 123 } 187 | // Eg. see elasticDSL/Query/Query.js method prepareQueryInResolve() 188 | export function getFieldConfigMap( 189 | opts: any, 190 | types: ElasticDataType[], 191 | fc: any, 192 | addAll: boolean = false 193 | ): InputTypeComposerFieldConfigMapDefinition | string { 194 | if (!fc) fc = 'JSON'; 195 | if (!opts || !opts.fieldMap) { 196 | return 'JSON'; 197 | } 198 | 199 | if (!types) { 200 | types = ['_all']; 201 | } 202 | 203 | const fcMap = {} as Record; 204 | if (addAll) { 205 | fcMap._all = fc; 206 | } 207 | getFieldNamesByElasticType(opts.fieldMap, types).forEach((fieldName) => { 208 | fcMap[fieldName] = fc; 209 | }); 210 | 211 | if (Object.keys(fcMap).length === 0) { 212 | return 'JSON'; 213 | } 214 | 215 | return fcMap; 216 | } 217 | -------------------------------------------------------------------------------- /src/elasticDSL/Commons/Float.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { InputTypeComposer } from 'graphql-compose'; 4 | import { getTypeName, CommonOpts, desc } from '../../utils'; 5 | 6 | export function getFloatRangeITC( 7 | opts: CommonOpts 8 | ): InputTypeComposer { 9 | const name = getTypeName('FloatRange', opts); 10 | const description = desc(`Float range where \`from\` value includes and \`to\` value excludes.`); 11 | 12 | return opts.getOrCreateITC(name, () => ({ 13 | name, 14 | description, 15 | fields: { 16 | from: 'Float', 17 | to: 'Float', 18 | }, 19 | })); 20 | } 21 | 22 | export function getFloatRangeKeyedITC( 23 | opts: CommonOpts 24 | ): InputTypeComposer { 25 | const name = getTypeName('FloatRangeKeyed', opts); 26 | const description = desc( 27 | ` 28 | Float range where \`from\` value includes and \`to\` value excludes and 29 | may have a key for aggregation. 30 | ` 31 | ); 32 | 33 | return opts.getOrCreateITC(name, () => ({ 34 | name, 35 | description, 36 | fields: { 37 | from: 'Float', 38 | to: 'Float', 39 | key: 'String', 40 | }, 41 | })); 42 | } 43 | -------------------------------------------------------------------------------- /src/elasticDSL/Commons/Geo.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { GraphQLScalarType, Kind } from 'graphql'; 4 | import { CommonOpts, desc } from '../../utils'; 5 | 6 | export const ElasticGeoPointType = new GraphQLScalarType({ 7 | name: 'ElasticGeoPointType', 8 | description: desc( 9 | ` 10 | Elastic Search GeoPoint Type. 11 | Object format: { "lat" : 52.3760, "lon" : 4.894 }. 12 | String format (lat, lon): "52.3760, 4.894". 13 | Array GeoJson format (lat, lon): [4.894, 52.3760] 14 | ` 15 | ), 16 | serialize: (v) => v, 17 | parseValue: (v) => v, 18 | parseLiteral(ast) { 19 | switch (ast.kind) { 20 | case Kind.STRING: 21 | return ast.value; 22 | case Kind.OBJECT: { 23 | let lat; 24 | let lon; 25 | ast.fields.forEach((field) => { 26 | if (field.name.value === 'lat') { 27 | if (field.value.kind === 'StringValue') { 28 | lat = parseFloat(field.value.value); 29 | } else if (field.value.kind === 'FloatValue') { 30 | lat = field.value.value; 31 | } 32 | } 33 | if (field.name.value === 'lon') { 34 | if (field.value.kind === 'StringValue') { 35 | lon = parseFloat(field.value.value); 36 | } else if (field.value.kind === 'FloatValue') { 37 | lon = field.value.value; 38 | } 39 | } 40 | }); 41 | return { lat, lon }; 42 | } 43 | case Kind.LIST: 44 | if (ast.values.length === 2) { 45 | if (ast.values[0].kind === 'StringValue' && ast.values[1].kind === 'StringValue') { 46 | return [parseFloat(ast.values[0].value || '0'), parseFloat(ast.values[1].value || '0')]; 47 | } 48 | } 49 | return null; 50 | default: 51 | return null; 52 | } 53 | }, 54 | }); 55 | 56 | export function getGeoPointFC(_opts: CommonOpts): GraphQLScalarType { 57 | return ElasticGeoPointType; 58 | } 59 | 60 | export function getDistanceUnitFC(_opts: CommonOpts): { type: string; description: string } { 61 | return { 62 | type: 'String', 63 | description: desc( 64 | ` 65 | By default, the distance unit is m (metres) but it can also accept: 66 | mi (miles), in (inches), yd (yards), km (kilometers), cm (centimeters), 67 | mm (millimeters). 68 | ` 69 | ), 70 | }; 71 | } 72 | 73 | export function getDistanceCalculationModeFC(_opts: CommonOpts): { 74 | type: string; 75 | description: string; 76 | } { 77 | return { 78 | type: 'String', 79 | description: desc( 80 | ` 81 | \`sloppy_arc\` (the default) 82 | \`arc\` (most accurate) for very large areas like cross continent search. 83 | \`plane\` (fastest) for smaller geographical areas like cities or even countries. 84 | ` 85 | ), 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /src/elasticDSL/Commons/HDR.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../utils'; 3 | 4 | export function getCommonsHdrITC( 5 | opts: CommonOpts 6 | ): InputTypeComposer { 7 | const name = getTypeName('CommonsHDR', opts); 8 | const description = desc( 9 | ` 10 | A High Dynamic Range (HDR) Histogram. 11 | [Documentation](https://github.com/HdrHistogram/HdrHistogram) 12 | ` 13 | ); 14 | 15 | return opts.getOrCreateITC(name, () => ({ 16 | name, 17 | description, 18 | fields: { 19 | number_of_significant_value_digits: 'Int', 20 | }, 21 | })); 22 | } 23 | -------------------------------------------------------------------------------- /src/elasticDSL/Commons/Ip.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { InputTypeComposer } from 'graphql-compose'; 4 | import { getTypeName, CommonOpts, desc } from '../../utils'; 5 | 6 | export function getIpRangeTypeITC( 7 | opts: CommonOpts 8 | ): InputTypeComposer { 9 | const name = getTypeName('IpRangeType', opts); 10 | const description = desc(`Ip range where \`from\` value includes and \`to\` value excludes.`); 11 | 12 | return opts.getOrCreateITC(name, () => ({ 13 | name, 14 | description, 15 | fields: { 16 | from: 'String', 17 | to: 'String', 18 | mask: { 19 | type: 'String', 20 | description: 'IP ranges can also be defined as CIDR masks 10.0.0.127/25', 21 | }, 22 | }, 23 | })); 24 | } 25 | -------------------------------------------------------------------------------- /src/elasticDSL/Commons/Script.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../utils'; 3 | 4 | export function getCommonsScriptITC( 5 | opts: CommonOpts 6 | ): InputTypeComposer { 7 | const name = getTypeName('CommonsScript', opts); 8 | const description = desc( 9 | ` 10 | The scripting module enables you to use scripts to evaluate custom expressions. 11 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html) 12 | ` 13 | ); 14 | 15 | return opts.getOrCreateITC(name, () => ({ 16 | name, 17 | description, 18 | fields: { 19 | lang: 'String!', 20 | inline: 'String!', 21 | params: 'JSON', 22 | file: 'String', 23 | }, 24 | })); 25 | } 26 | -------------------------------------------------------------------------------- /src/elasticDSL/Commons/__tests__/FieldNames-test.ts: -------------------------------------------------------------------------------- 1 | import { schemaComposer, EnumTypeComposer } from 'graphql-compose'; 2 | import * as FieldNames from '../FieldNames'; 3 | import { prepareCommonOpts } from '../../../utils'; 4 | 5 | beforeEach(() => { 6 | schemaComposer.clear(); 7 | }); 8 | 9 | const fieldMap = { 10 | _all: { 11 | text1: {}, 12 | text2: {}, 13 | keyword1: {}, 14 | int1: {}, 15 | float1: {}, 16 | double1: {}, 17 | date1: {}, 18 | boolean1: {}, 19 | geo1: {}, 20 | nested1: {}, 21 | ip1: {}, 22 | }, 23 | text: { 24 | text1: {}, 25 | text2: {}, 26 | }, 27 | keyword: { 28 | keyword1: {}, 29 | }, 30 | integer: { 31 | int1: {}, 32 | }, 33 | float: { 34 | float1: {}, 35 | }, 36 | double: { 37 | double1: {}, 38 | }, 39 | date: { 40 | date1: {}, 41 | }, 42 | boolean: { 43 | boolean1: {}, 44 | }, 45 | geo_point: { 46 | geo1: {}, 47 | }, 48 | nested: { 49 | nested1: {}, 50 | }, 51 | ip: { 52 | ip1: {}, 53 | }, 54 | }; 55 | const opts = { fieldMap }; 56 | 57 | describe('FieldNames', () => { 58 | it('getStringFields()', () => { 59 | const type = FieldNames.getStringFields( 60 | prepareCommonOpts(schemaComposer, opts) 61 | ) as EnumTypeComposer; 62 | expect(type).toBeInstanceOf(EnumTypeComposer); 63 | expect(type.getDescription()).toBe('Available fields from mapping.'); 64 | expect(type.getTypeName()).toBe('ElasticStringFields'); 65 | expect(type.getFields()).toMatchObject({ 66 | keyword1: { value: 'keyword1' }, 67 | text1: { value: 'text1' }, 68 | text2: { value: 'text2' }, 69 | }); 70 | }); 71 | 72 | it('getNumericFields()', () => { 73 | const type = FieldNames.getNumericFields( 74 | prepareCommonOpts(schemaComposer, opts) 75 | ) as EnumTypeComposer; 76 | expect(type).toBeInstanceOf(EnumTypeComposer); 77 | expect(type.getDescription()).toBe('Available fields from mapping.'); 78 | expect(type.getTypeName()).toBe('ElasticNumericFields'); 79 | expect(type.getFields()).toMatchObject({ 80 | double1: { value: 'double1' }, 81 | float1: { value: 'float1' }, 82 | int1: { value: 'int1' }, 83 | }); 84 | }); 85 | 86 | it('getDateFields()', () => { 87 | const type = FieldNames.getDateFields( 88 | prepareCommonOpts(schemaComposer, opts) 89 | ) as EnumTypeComposer; 90 | expect(type).toBeInstanceOf(EnumTypeComposer); 91 | expect(type.getDescription()).toBe('Available fields from mapping.'); 92 | expect(type.getTypeName()).toBe('ElasticDateFields'); 93 | expect(type.getFields()).toMatchObject({ 94 | date1: { value: 'date1' }, 95 | }); 96 | }); 97 | 98 | it('getBooleanFields()', () => { 99 | const type = FieldNames.getBooleanFields( 100 | prepareCommonOpts(schemaComposer, opts) 101 | ) as EnumTypeComposer; 102 | expect(type).toBeInstanceOf(EnumTypeComposer); 103 | expect(type.getDescription()).toBe('Available fields from mapping.'); 104 | expect(type.getTypeName()).toBe('ElasticBooleanFields'); 105 | expect(type.getFields()).toMatchObject({ 106 | boolean1: { value: 'boolean1' }, 107 | }); 108 | }); 109 | 110 | it('getGeoPointFields()', () => { 111 | const type = FieldNames.getGeoPointFields( 112 | prepareCommonOpts(schemaComposer, opts) 113 | ) as EnumTypeComposer; 114 | expect(type).toBeInstanceOf(EnumTypeComposer); 115 | expect(type.getDescription()).toBe('Available fields from mapping.'); 116 | expect(type.getTypeName()).toBe('ElasticGeoPointFields'); 117 | expect(type.getFields()).toMatchObject({ 118 | geo1: { value: 'geo1' }, 119 | }); 120 | }); 121 | 122 | it('getNestedFields()', () => { 123 | const type = FieldNames.getNestedFields( 124 | prepareCommonOpts(schemaComposer, opts) 125 | ) as EnumTypeComposer; 126 | expect(type).toBeInstanceOf(EnumTypeComposer); 127 | expect(type.getDescription()).toBe('Available fields from mapping.'); 128 | expect(type.getTypeName()).toBe('ElasticNestedFields'); 129 | expect(type.getFields()).toMatchObject({ 130 | nested1: { value: 'nested1' }, 131 | }); 132 | }); 133 | 134 | it('getIpFields()', () => { 135 | const type = FieldNames.getIpFields( 136 | prepareCommonOpts(schemaComposer, opts) 137 | ) as EnumTypeComposer; 138 | expect(type).toBeInstanceOf(EnumTypeComposer); 139 | expect(type.getDescription()).toBe('Available fields from mapping.'); 140 | expect(type.getTypeName()).toBe('ElasticIpFields'); 141 | expect(type.getFields()).toMatchObject({ 142 | ip1: { value: 'ip1' }, 143 | }); 144 | }); 145 | 146 | it('getAllFields()', () => { 147 | const type = FieldNames.getAllFields( 148 | prepareCommonOpts(schemaComposer, opts) 149 | ) as EnumTypeComposer; 150 | expect(type).toBeInstanceOf(EnumTypeComposer); 151 | expect(type.getDescription()).toBe('Available fields from mapping.'); 152 | expect(type.getTypeName()).toBe('ElasticAllFields'); 153 | expect(type.getFields()).toMatchObject({ 154 | boolean1: { value: 'boolean1' }, 155 | date1: { value: 'date1' }, 156 | double1: { value: 'double1' }, 157 | float1: { value: 'float1' }, 158 | geo1: { value: 'geo1' }, 159 | int1: { value: 'int1' }, 160 | ip1: { value: 'ip1' }, 161 | keyword1: { value: 'keyword1' }, 162 | nested1: { value: 'nested1' }, 163 | text1: { value: 'text1' }, 164 | text2: { value: 'text2' }, 165 | }); 166 | }); 167 | 168 | it('should return string if mapping not provided', () => { 169 | expect(FieldNames.getStringFields({} as any)).toEqual('String'); 170 | expect(FieldNames.getNumericFields({} as any)).toEqual('String'); 171 | expect(FieldNames.getDateFields({} as any)).toEqual('String'); 172 | expect(FieldNames.getBooleanFields({} as any)).toEqual('String'); 173 | expect(FieldNames.getGeoPointFields({} as any)).toEqual('String'); 174 | expect(FieldNames.getNestedFields({} as any)).toEqual('String'); 175 | expect(FieldNames.getIpFields({} as any)).toEqual('String'); 176 | expect(FieldNames.getAllFields({} as any)).toEqual('String'); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/Compound/Bool.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getQueryITC, prepareQueryInResolve } from '../Query'; 3 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 4 | 5 | export function getBoolITC(opts: CommonOpts): InputTypeComposer { 6 | const name = getTypeName('QueryBool', opts); 7 | const description = desc( 8 | ` 9 | A query that matches documents matching boolean combinations 10 | of other queries. The bool query maps to Lucene BooleanQuery. 11 | It is built using one or more boolean clauses, each clause 12 | with a typed occurrence. 13 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html) 14 | ` 15 | ); 16 | 17 | return opts.getOrCreateITC(name, () => ({ 18 | name, 19 | description, 20 | fields: { 21 | must: { 22 | type: () => [getQueryITC(opts)], 23 | description: desc( 24 | ` 25 | The clause (query) must appear in matching documents 26 | and will contribute to the score. 27 | ` 28 | ), 29 | }, 30 | filter: { 31 | type: () => [getQueryITC(opts)], 32 | description: desc( 33 | ` 34 | The clause (query) must appear in matching documents. 35 | However unlike must the score of the query will be ignored. 36 | Filter clauses are executed in filter context, meaning 37 | that scoring is ignored and clauses are considered for caching. 38 | ` 39 | ), 40 | }, 41 | should: { 42 | type: () => [getQueryITC(opts)], 43 | description: desc( 44 | ` 45 | The clause (query) should appear in the matching document. 46 | In a boolean query with no must or filter clauses, 47 | one or more should clauses must match a document. 48 | The minimum number of should clauses to match can be set 49 | using the minimum_should_match parameter. 50 | ` 51 | ), 52 | }, 53 | minimum_should_match: { 54 | type: 'String', 55 | description: desc( 56 | ` 57 | The minimum number of should clauses to match. 58 | ` 59 | ), 60 | }, 61 | must_not: { 62 | type: () => [getQueryITC(opts)], 63 | description: desc( 64 | ` 65 | The clause (query) must not appear in the matching documents. 66 | Clauses are executed in filter context meaning that scoring 67 | is ignored and clauses are considered for caching. 68 | Because scoring is ignored, a score of 0 for all documents 69 | is returned. 70 | ` 71 | ), 72 | }, 73 | boost: 'Float', 74 | }, 75 | })); 76 | } 77 | 78 | function prepareQueryMayBeArray(vals: any, fieldMap: any) { 79 | if (Array.isArray(vals)) { 80 | return vals.map((val) => prepareQueryInResolve(val, fieldMap)); 81 | } 82 | return prepareQueryInResolve(vals, fieldMap); 83 | } 84 | 85 | export function prepareBoolInResolve(bool: any, fieldMap: any): { [argName: string]: any } { 86 | /* eslint-disable no-param-reassign */ 87 | if (bool.must) { 88 | bool.must = prepareQueryMayBeArray(bool.must, fieldMap); 89 | } 90 | if (bool.filter) { 91 | bool.filter = prepareQueryMayBeArray(bool.filter, fieldMap); 92 | } 93 | if (bool.should) { 94 | bool.should = prepareQueryMayBeArray(bool.should, fieldMap); 95 | } 96 | if (bool.must_not) { 97 | bool.must_not = prepareQueryMayBeArray(bool.must_not, fieldMap); 98 | } 99 | 100 | return bool; 101 | } 102 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/Compound/Boosting.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getQueryITC, prepareQueryInResolve } from '../Query'; 3 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 4 | 5 | export function getBoostingITC(opts: CommonOpts): InputTypeComposer { 6 | const name = getTypeName('QueryBoosting', opts); 7 | const description = desc( 8 | ` 9 | The boosting query can be used to effectively demote results that match a given query. 10 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-boosting-query.html) 11 | ` 12 | ); 13 | 14 | return opts.getOrCreateITC(name, () => ({ 15 | name, 16 | description, 17 | fields: { 18 | positive: (): InputTypeComposer => getQueryITC(opts), 19 | negative: (): InputTypeComposer => getQueryITC(opts), 20 | negative_boost: 'Float', 21 | }, 22 | })); 23 | } 24 | 25 | export function prepareBoostingInResolve(boosting: any, fieldMap: any): { [argName: string]: any } { 26 | /* eslint-disable no-param-reassign */ 27 | if (boosting.positive) { 28 | boosting.positive = prepareQueryInResolve(boosting.positive, fieldMap); 29 | } 30 | if (boosting.negative) { 31 | boosting.negative = prepareQueryInResolve(boosting.negative, fieldMap); 32 | } 33 | 34 | return boosting; 35 | } 36 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/Compound/ConstantScore.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getQueryITC, prepareQueryInResolve } from '../Query'; 3 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 4 | 5 | export function getConstantScoreITC( 6 | opts: CommonOpts 7 | ): InputTypeComposer { 8 | const name = getTypeName('QueryConstantScore', opts); 9 | const description = desc( 10 | ` 11 | A query that wraps another query and simply returns a constant score equal 12 | to the query boost for every document in the filter. 13 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-constant-score-query.html) 14 | ` 15 | ); 16 | 17 | return opts.getOrCreateITC(name, () => ({ 18 | name, 19 | description, 20 | fields: { 21 | filter: () => getQueryITC(opts).getTypeNonNull(), 22 | boost: 'Float!', 23 | }, 24 | })); 25 | } 26 | 27 | /* eslint-disable no-param-reassign, camelcase */ 28 | export function prepareConstantScoreInResolve( 29 | constant_score: any, 30 | fieldMap: any 31 | ): { [argName: string]: any } { 32 | if (constant_score.filter) { 33 | constant_score.filter = prepareQueryInResolve(constant_score.filter, fieldMap); 34 | } 35 | 36 | return constant_score; 37 | } 38 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/Compound/DisMax.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getQueryITC, prepareQueryInResolve } from '../Query'; 3 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 4 | 5 | export function getDisMaxITC(opts: CommonOpts): InputTypeComposer { 6 | const name = getTypeName('QueryDisMax', opts); 7 | const description = desc( 8 | ` 9 | A query that generates the union of documents produced by its subqueries, 10 | and that scores each document with the maximum score for that document 11 | as produced by any subquery, plus a tie breaking increment 12 | for any additional matching subqueries. 13 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-dis-max-query.html) 14 | ` 15 | ); 16 | 17 | return opts.getOrCreateITC(name, () => ({ 18 | name, 19 | description, 20 | fields: { 21 | queries: (): InputTypeComposer[] => [getQueryITC(opts)], 22 | boost: 'Float', 23 | tie_breaker: 'Float', 24 | }, 25 | })); 26 | } 27 | 28 | /* eslint-disable no-param-reassign, camelcase */ 29 | export function prepareDisMaxResolve(dis_max: any, fieldMap: any): { [argName: string]: any } { 30 | if (Array.isArray(dis_max.queries)) { 31 | dis_max.queries = dis_max.queries.map((query: any) => prepareQueryInResolve(query, fieldMap)); 32 | } 33 | 34 | return dis_max; 35 | } 36 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/Compound/FunctionScore.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getQueryITC, prepareQueryInResolve } from '../Query'; 3 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 4 | 5 | export function getFunctionScoreITC( 6 | opts: CommonOpts 7 | ): InputTypeComposer { 8 | const name = getTypeName('QueryFunctionScore', opts); 9 | const description = desc( 10 | ` 11 | The function_score allows you to modify the score of documents that 12 | are retrieved by a query. This can be useful if, for example, 13 | a score function is computationally expensive and it is sufficient 14 | to compute the score on a filtered set of documents. 15 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html) 16 | ` 17 | ); 18 | 19 | const RandomScoreType = opts.schemaComposer.createInputTC({ 20 | name: getTypeName('QueryFunctionScoreRandom', opts), 21 | fields: { 22 | seed: 'Float', 23 | }, 24 | }); 25 | 26 | return opts.getOrCreateITC(name, () => ({ 27 | name, 28 | description, 29 | fields: { 30 | query: (): InputTypeComposer => getQueryITC(opts), 31 | boost: 'String', 32 | boost_mode: { 33 | type: 'String', 34 | description: 'Can be: `multiply`, `replace`, `sum`, `avg`, `max`, `min`.', 35 | }, 36 | random_score: RandomScoreType, 37 | functions: [ 38 | opts.schemaComposer.createInputTC({ 39 | name: getTypeName('QueryFunctionScoreFunction', opts), 40 | fields: { 41 | filter: (): InputTypeComposer => getQueryITC(opts), 42 | random_score: RandomScoreType, 43 | weight: 'Float', 44 | script_score: 'JSON', 45 | field_value_factor: 'JSON', 46 | gauss: 'JSON', 47 | linear: 'JSON', 48 | exp: 'JSON', 49 | }, 50 | }), 51 | ], 52 | max_boost: 'Float', 53 | score_mode: { 54 | type: 'String', 55 | description: 'Can be: `multiply`, `sum`, `avg`, `first`, `max`, `min`.', 56 | }, 57 | min_score: 'Float', 58 | }, 59 | })); 60 | } 61 | 62 | /* eslint-disable no-param-reassign, camelcase */ 63 | export function prepareFunctionScoreInResolve( 64 | function_score: any, 65 | fieldMap: any 66 | ): { [argName: string]: any } { 67 | if (function_score.query) { 68 | function_score.query = prepareQueryInResolve(function_score.query, fieldMap); 69 | } 70 | 71 | if (Array.isArray(function_score.functions)) { 72 | function_score.functions = function_score.functions.map((func: any) => { 73 | if (func.filter) { 74 | func.filter = prepareQueryInResolve(func.filter, fieldMap); 75 | } 76 | return func; 77 | }); 78 | } 79 | 80 | return function_score; 81 | } 82 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/FullText/Common.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer, InputTypeComposerFieldConfigAsObjectDefinition } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getAnalyzedAsFieldConfigMap } from '../../Commons/FieldNames'; 4 | 5 | export function getCommonITC( 6 | opts: CommonOpts 7 | ): InputTypeComposer | InputTypeComposerFieldConfigAsObjectDefinition { 8 | const name = getTypeName('QueryCommon', opts); 9 | const description = desc( 10 | ` 11 | The common terms query is a modern alternative to stopwords which improves 12 | the precision and recall of search results (by taking stopwords into account), 13 | without sacrificing performance. 14 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-common-terms-query.html) 15 | ` 16 | ); 17 | 18 | const subName = getTypeName('QueryCommonSettings', opts); 19 | const fields = getAnalyzedAsFieldConfigMap( 20 | opts, 21 | opts.getOrCreateITC(subName, () => ({ 22 | name: subName, 23 | fields: { 24 | query: 'String', 25 | cutoff_frequency: 'Float', 26 | minimum_should_match: 'JSON', 27 | low_freq_operator: 'String', 28 | high_freq_operator: 'String', 29 | boost: 'Float', 30 | }, 31 | })) 32 | ); 33 | 34 | if (typeof fields === 'object') { 35 | return opts.getOrCreateITC(name, () => ({ 36 | name, 37 | description, 38 | fields, 39 | })); 40 | } 41 | 42 | return { 43 | type: 'JSON', 44 | description, 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/FullText/Match.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer, InputTypeComposerFieldConfigAsObjectDefinition } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getAnalyzedAsFieldConfigMap } from '../../Commons/FieldNames'; 4 | 5 | export function getMatchITC( 6 | opts: CommonOpts 7 | ): InputTypeComposer | InputTypeComposerFieldConfigAsObjectDefinition { 8 | const name = getTypeName('QueryMatch', opts); 9 | const description = desc( 10 | ` 11 | Match Query accept text/numerics/dates, analyzes them, and constructs a query. 12 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html) 13 | ` 14 | ); 15 | 16 | const subName = getTypeName('QueryMatchSettings', opts); 17 | const fields = getAnalyzedAsFieldConfigMap( 18 | opts, 19 | opts.getOrCreateITC(subName, () => ({ 20 | name: subName, 21 | fields: { 22 | query: 'String', 23 | operator: 'String', 24 | zero_terms_query: 'String', 25 | cutoff_frequency: 'Float', 26 | boost: 'Float', 27 | }, 28 | })) 29 | ); 30 | 31 | if (typeof fields === 'object') { 32 | return opts.getOrCreateITC(name, () => ({ 33 | name, 34 | description, 35 | fields, 36 | })); 37 | } 38 | 39 | return { 40 | type: 'JSON', 41 | description, 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/FullText/MatchPhrase.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer, InputTypeComposerFieldConfigAsObjectDefinition } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getAnalyzedAsFieldConfigMap } from '../../Commons/FieldNames'; 4 | 5 | export function getMatchPhraseITC( 6 | opts: CommonOpts 7 | ): InputTypeComposer | InputTypeComposerFieldConfigAsObjectDefinition { 8 | const name = getTypeName('QueryMatchPhrase', opts); 9 | const description = desc( 10 | ` 11 | The match_phrase query analyzes the text and creates a phrase query out 12 | of the analyzed text. 13 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase.html) 14 | ` 15 | ); 16 | 17 | const subName = getTypeName('QueryMatchPhraseSettings', opts); 18 | const fields = getAnalyzedAsFieldConfigMap( 19 | opts, 20 | opts.getOrCreateITC(subName, () => ({ 21 | name: subName, 22 | fields: { 23 | query: 'String', 24 | analyzer: 'String', 25 | boost: 'Float', 26 | }, 27 | })) 28 | ); 29 | 30 | if (typeof fields === 'object') { 31 | return opts.getOrCreateITC(name, () => ({ 32 | name, 33 | description, 34 | fields, 35 | })); 36 | } 37 | 38 | return { 39 | type: 'JSON', 40 | description, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/FullText/MatchPhrasePrefix.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer, InputTypeComposerFieldConfigAsObjectDefinition } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getAnalyzedAsFieldConfigMap } from '../../Commons/FieldNames'; 4 | 5 | export function getMatchPhrasePrefixITC( 6 | opts: CommonOpts 7 | ): InputTypeComposer | InputTypeComposerFieldConfigAsObjectDefinition { 8 | const name = getTypeName('QueryMatchPhrasePrefix', opts); 9 | const description = desc( 10 | ` 11 | The match_phrase_prefix is the same as match_phrase, except that it allows 12 | for prefix matches on the last term in the text. Eg "quick brown f" 13 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase-prefix.html) 14 | ` 15 | ); 16 | 17 | const subName = getTypeName('QueryMatchPhrasePrefixSettings', opts); 18 | const fields = getAnalyzedAsFieldConfigMap( 19 | opts, 20 | opts.getOrCreateITC(subName, () => ({ 21 | name: subName, 22 | fields: { 23 | query: 'String', 24 | max_expansions: 'Int', 25 | boost: 'Float', 26 | }, 27 | })) 28 | ); 29 | 30 | if (typeof fields === 'object') { 31 | return opts.getOrCreateITC(name, () => ({ 32 | name, 33 | description, 34 | fields, 35 | })); 36 | } 37 | 38 | return { 39 | type: 'JSON', 40 | description, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/FullText/MultiMatch.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getMultiMatchITC( 5 | opts: CommonOpts 6 | ): InputTypeComposer { 7 | const name = getTypeName('QueryMultiMatch', opts); 8 | const description = desc( 9 | ` 10 | The multi_match query builds on the match query to allow multi-field queries. 11 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html) 12 | ` 13 | ); 14 | 15 | return opts.getOrCreateITC(name, () => ({ 16 | name, 17 | description, 18 | fields: { 19 | query: 'String!', 20 | fields: { 21 | type: '[String]!', 22 | description: desc( 23 | ` 24 | Array of fields [ title, *_name, subject^3 ]. 25 | You may use wildcards and boosting field. 26 | ` 27 | ), 28 | }, 29 | type: `enum ${getTypeName('QueryMultiMatchTypeEnum', opts)} { 30 | best_fields 31 | most_fields 32 | cross_fields 33 | phrase 34 | phrase_prefix 35 | }`, 36 | operator: `enum ${getTypeName('QueryMultiMatchOperatorEnum', opts)} { 37 | and 38 | or 39 | }`, 40 | minimum_should_match: 'String', 41 | analyzer: 'String', 42 | slop: 'Int', 43 | boost: 'Float', 44 | fuzziness: 'JSON', 45 | prefix_length: 'Int', 46 | max_expansions: 'Int', 47 | rewrite: 'String', 48 | zero_terms_query: 'JSON', 49 | cutoff_frequency: 'Float', 50 | // lenient: 'JSON', // depricated from ES 5.3 51 | }, 52 | })); 53 | } 54 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/FullText/QueryString.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getQueryStringITC( 5 | opts: CommonOpts 6 | ): InputTypeComposer { 7 | const name = getTypeName('QueryQueryString', opts); 8 | const description = desc( 9 | ` 10 | A query that uses a query parser in order to parse its content. 11 | Eg. "this AND that OR thus" or "(content:this OR name:this) AND (content:that OR name:that)" 12 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html) 13 | ` 14 | ); 15 | 16 | return opts.getOrCreateITC(name, () => ({ 17 | name, 18 | description, 19 | fields: { 20 | query: 'String!', 21 | fields: '[String]', 22 | default_field: 'String', 23 | default_operator: `enum ${getTypeName('QueryQueryStringOperatorEnum', opts)} { 24 | and 25 | or 26 | }`, 27 | analyzer: 'String', 28 | allow_leading_wildcard: 'Boolean', 29 | enable_position_increments: 'Boolean', 30 | fuzzy_max_expansions: 'Int', 31 | fuzziness: 'String', 32 | fuzzy_prefix_length: 'Int', 33 | phrase_slop: 'Int', 34 | boost: 'Float', 35 | auto_generate_phrase_queries: 'Boolean', 36 | analyze_wildcard: 'Boolean', 37 | max_determinized_states: 'Int', 38 | minimum_should_match: 'String', 39 | lenient: 'Boolean', 40 | time_zone: 'String', 41 | quote_field_suffix: 'String', 42 | split_on_whitespace: 'Boolean', 43 | use_dis_max: 'Boolean', 44 | tie_breaker: 'Int', 45 | }, 46 | })); 47 | } 48 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/FullText/SimpleQueryString.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getSimpleQueryStringITC( 5 | opts: CommonOpts 6 | ): InputTypeComposer { 7 | const name = getTypeName('QuerySimpleQueryString', opts); 8 | const description = desc( 9 | ` 10 | A query that uses the SimpleQueryParser to parse its context. 11 | Unlike the regular query_string query, the simple_query_string query 12 | will never throw an exception, and discards invalid parts of the query. 13 | Eg. "this AND that OR thus" or "(content:this OR name:this) AND (content:that OR name:that)" 14 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html) 15 | ` 16 | ); 17 | 18 | return opts.getOrCreateITC(name, () => ({ 19 | name, 20 | description, 21 | fields: { 22 | query: 'String!', 23 | fields: '[String]', 24 | default_operator: `enum ${getTypeName('QuerySimpleQueryStringOperatorEnum', opts)} { 25 | and 26 | or 27 | }`, 28 | analyzer: 'String', 29 | flags: { 30 | type: 'String', 31 | description: desc( 32 | ` 33 | Can provided several flags, eg "OR|AND|PREFIX". 34 | The available flags are: ALL, NONE, AND, OR, NOT, PREFIX, PHRASE, 35 | PRECEDENCE, ESCAPE, WHITESPACE, FUZZY, NEAR, and SLOP. 36 | ` 37 | ), 38 | }, 39 | analyze_wildcard: 'Boolean', 40 | lenient: 'Boolean', 41 | minimum_should_match: 'String', 42 | quote_field_suffix: 'String', 43 | boost: 'Float', 44 | }, 45 | })); 46 | } 47 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/Geo/GeoBoundingBox.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer, InputTypeComposerFieldConfigAsObjectDefinition } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getGeoPointAsFieldConfigMap } from '../../Commons/FieldNames'; 4 | import { getGeoPointFC } from '../../Commons/Geo'; 5 | 6 | export function getGeoBoundingBoxITC( 7 | opts: CommonOpts 8 | ): InputTypeComposer | InputTypeComposerFieldConfigAsObjectDefinition { 9 | const name = getTypeName('QueryGeoBoundingBox', opts); 10 | const description = desc( 11 | ` 12 | A query allowing to filter hits based on a point location using a bounding box. 13 | Requires the geo_point Mapping. 14 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-bounding-box-query.html) 15 | ` 16 | ); 17 | 18 | const subName = getTypeName('QueryGeoBoundingBoxSettings', opts); 19 | const fields = getGeoPointAsFieldConfigMap( 20 | opts, 21 | opts.getOrCreateITC(subName, () => ({ 22 | name: subName, 23 | fields: { 24 | top_left: getGeoPointFC(opts), 25 | bottom_right: getGeoPointFC(opts), 26 | }, 27 | })) 28 | ); 29 | 30 | if (typeof fields === 'object') { 31 | return opts.getOrCreateITC(name, () => ({ 32 | name, 33 | description, 34 | fields, 35 | })); 36 | } 37 | 38 | return { 39 | type: 'JSON', 40 | description, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/Geo/GeoDistance.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getGeoPointAsFieldConfigMap } from '../../Commons/FieldNames'; 4 | import { getGeoPointFC, getDistanceCalculationModeFC } from '../../Commons/Geo'; 5 | 6 | export function getGeoDistanceITC( 7 | opts: CommonOpts 8 | ): InputTypeComposer | { type: string; description: string } { 9 | const name = getTypeName('QueryGeoDistance', opts); 10 | const description = desc( 11 | ` 12 | Filters documents that include only hits that exists within 13 | a specific distance from a geo point. 14 | Requires the geo_point Mapping. 15 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-distance-query.html) 16 | ` 17 | ); 18 | 19 | const subName = getTypeName('QueryGeoDistanceSettings', opts); 20 | const fields = getGeoPointAsFieldConfigMap( 21 | opts, 22 | opts.getOrCreateITC(subName, () => ({ 23 | name: subName, 24 | fields: { 25 | top_left: getGeoPointFC(opts), 26 | bottom_right: getGeoPointFC(opts), 27 | }, 28 | })) 29 | ); 30 | 31 | if (typeof fields === 'object') { 32 | return opts.getOrCreateITC(name, () => ({ 33 | name, 34 | description, 35 | fields: { 36 | distance: { 37 | type: 'String!', 38 | description: 'Eg. 12km', 39 | }, 40 | distance_type: getDistanceCalculationModeFC(opts), 41 | ...fields, 42 | validation_method: 'String', 43 | }, 44 | })); 45 | } 46 | 47 | return { 48 | type: 'JSON', 49 | description, 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/Geo/GeoPolygon.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer, InputTypeComposerFieldConfigAsObjectDefinition } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getGeoPointAsFieldConfigMap } from '../../Commons/FieldNames'; 4 | import { getGeoPointFC } from '../../Commons/Geo'; 5 | 6 | export function getGeoPolygonITC( 7 | opts: CommonOpts 8 | ): InputTypeComposer | InputTypeComposerFieldConfigAsObjectDefinition { 9 | const name = getTypeName('QueryGeoPolygon', opts); 10 | const description = desc( 11 | ` 12 | A query allowing to include hits that only fall within a polygon of points. 13 | Requires the geo_point Mapping. 14 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-polygon-query.html) 15 | ` 16 | ); 17 | 18 | const subName = getTypeName('QueryGeoPolygonSettings', opts); 19 | const fields = getGeoPointAsFieldConfigMap( 20 | opts, 21 | opts.getOrCreateITC(subName, () => ({ 22 | name: subName, 23 | fields: { 24 | points: [getGeoPointFC(opts)], 25 | validation_method: 'String', 26 | }, 27 | })) 28 | ); 29 | 30 | if (typeof fields === 'object') { 31 | return opts.getOrCreateITC(name, () => ({ 32 | name, 33 | description, 34 | fields, 35 | })); 36 | } 37 | 38 | return { 39 | type: 'JSON', 40 | description, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/Geo/GeoShape.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer, InputTypeComposerFieldConfigAsObjectDefinition } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getGeoShapeAsFieldConfigMap } from '../../Commons/FieldNames'; 4 | 5 | export function getGeoShapeITC( 6 | opts: CommonOpts 7 | ): InputTypeComposer | InputTypeComposerFieldConfigAsObjectDefinition { 8 | const name = getTypeName('QueryGeoShape', opts); 9 | const description = desc( 10 | ` 11 | Filter documents indexed using the geo_shape type. 12 | Requires the geo_shape Mapping. 13 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-shape-query.html) 14 | ` 15 | ); 16 | 17 | const subName = getTypeName('QueryGeoShapeSettings', opts); 18 | const fields = getGeoShapeAsFieldConfigMap( 19 | opts, 20 | opts.getOrCreateITC(subName, () => ({ 21 | name: subName, 22 | fields: { 23 | shape: 'JSON', 24 | relation: 'JSON', 25 | indexed_shape: 'JSON', 26 | }, 27 | })) 28 | ); 29 | 30 | if (typeof fields === 'object') { 31 | return opts.getOrCreateITC(name, () => ({ 32 | name, 33 | description, 34 | fields, 35 | })); 36 | } 37 | 38 | return { 39 | type: 'JSON', 40 | description, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/Joining/HasChild.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getQueryITC } from '../Query'; 3 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 4 | 5 | export function getHasChildITC(opts: CommonOpts): InputTypeComposer { 6 | const name = getTypeName('QueryHasChild', opts); 7 | const description = desc( 8 | ` 9 | The has_child filter accepts a query and the child type to run against, 10 | and results in parent documents that have child docs matching the query. 11 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-has-child-query.html) 12 | ` 13 | ); 14 | 15 | return opts.getOrCreateITC(name, () => ({ 16 | name, 17 | description, 18 | fields: { 19 | type: 'String', 20 | query: (): InputTypeComposer => getQueryITC(opts), 21 | score_mode: { 22 | type: 'String', 23 | description: 'Can be: `avg`, `sum`, `max`, `min`, `none`.', 24 | }, 25 | min_children: 'Int', 26 | max_children: 'Int', 27 | }, 28 | })); 29 | } 30 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/Joining/HasParent.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getQueryITC } from '../Query'; 3 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 4 | 5 | export function getHasParentITC(opts: CommonOpts): InputTypeComposer { 6 | const name = getTypeName('QueryHasParent', opts); 7 | const description = desc( 8 | ` 9 | The has_parent query accepts a query and a parent type. The query is executed 10 | in the parent document space, which is specified by the parent type 11 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-has-parent-query.html) 12 | ` 13 | ); 14 | 15 | return opts.getOrCreateITC(name, () => ({ 16 | name, 17 | description, 18 | fields: { 19 | parent_type: 'String', 20 | query: (): InputTypeComposer => getQueryITC(opts), 21 | score: 'Boolean', 22 | ignore_unmapped: 'Boolean', 23 | }, 24 | })); 25 | } 26 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/Joining/Nested.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getQueryITC } from '../Query'; 3 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 4 | 5 | export function getNestedITC(opts: CommonOpts): InputTypeComposer { 6 | const name = getTypeName('QueryNested', opts); 7 | const description = desc( 8 | ` 9 | Nested query allows to query nested objects / docs. The query is executed 10 | against the nested objects / docs as if they were indexed as separate docs. 11 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-nested-query.html) 12 | ` 13 | ); 14 | 15 | return opts.getOrCreateITC(name, () => ({ 16 | name, 17 | description, 18 | fields: { 19 | path: 'String', 20 | score_mode: { 21 | type: 'String', 22 | description: 'Can be: `avg`, `sum`, `max`, `min`, `none`.', 23 | }, 24 | query: (): InputTypeComposer => getQueryITC(opts), 25 | }, 26 | })); 27 | } 28 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/Joining/ParentId.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getParentIdITC(opts: CommonOpts): InputTypeComposer { 5 | const name = getTypeName('QueryParentId', opts); 6 | const description = desc( 7 | ` 8 | The parent_id query can be used to find child documents 9 | which belong to a particular parent. 10 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-parent-id-query.html) 11 | ` 12 | ); 13 | 14 | return opts.getOrCreateITC(name, () => ({ 15 | name, 16 | description, 17 | fields: { 18 | type: 'String', 19 | id: 'String', 20 | ignore_unmapped: 'Boolean', 21 | }, 22 | })); 23 | } 24 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/MatchAll.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../utils'; 3 | 4 | export function getMatchAllITC(opts: CommonOpts): InputTypeComposer { 5 | const name = getTypeName('QueryMatchAll', opts); 6 | const description = desc( 7 | ` 8 | The most simple query, which matches all documents, 9 | giving them all a _score of 1.0. 10 | ` 11 | ); 12 | 13 | return opts.getOrCreateITC(name, () => ({ 14 | name, 15 | description, 16 | fields: { 17 | boost: { 18 | type: 'Float', 19 | }, 20 | }, 21 | })); 22 | } 23 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/Query.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getMatchAllITC } from './MatchAll'; 3 | 4 | import { getBoolITC, prepareBoolInResolve } from './Compound/Bool'; 5 | import { getConstantScoreITC, prepareConstantScoreInResolve } from './Compound/ConstantScore'; 6 | import { getDisMaxITC, prepareDisMaxResolve } from './Compound/DisMax'; 7 | import { getBoostingITC, prepareBoostingInResolve } from './Compound/Boosting'; 8 | import { getFunctionScoreITC, prepareFunctionScoreInResolve } from './Compound/FunctionScore'; 9 | 10 | import { getExistsITC } from './TermLevel/Exists'; 11 | import { getFuzzyITC } from './TermLevel/Fuzzy'; 12 | import { getIdsITC } from './TermLevel/Ids'; 13 | import { getPrefixITC } from './TermLevel/Prefix'; 14 | import { getRangeITC } from './TermLevel/Range'; 15 | import { getRegexpITC } from './TermLevel/Regexp'; 16 | import { getTypeITC } from './TermLevel/Type'; 17 | import { getTermITC } from './TermLevel/Term'; 18 | import { getTermsITC } from './TermLevel/Terms'; 19 | import { getWildcardITC } from './TermLevel/Wildcard'; 20 | 21 | import { getMatchITC } from './FullText/Match'; 22 | import { getMatchPhraseITC } from './FullText/MatchPhrase'; 23 | import { getMatchPhrasePrefixITC } from './FullText/MatchPhrasePrefix'; 24 | import { getMultiMatchITC } from './FullText/MultiMatch'; 25 | import { getCommonITC } from './FullText/Common'; 26 | import { getQueryStringITC } from './FullText/QueryString'; 27 | import { getSimpleQueryStringITC } from './FullText/SimpleQueryString'; 28 | 29 | import { getGeoBoundingBoxITC } from './Geo/GeoBoundingBox'; 30 | import { getGeoDistanceITC } from './Geo/GeoDistance'; 31 | import { getGeoPolygonITC } from './Geo/GeoPolygon'; 32 | import { getGeoShapeITC } from './Geo/GeoShape'; 33 | 34 | import { getMoreLikeThisITC } from './Specialized/MoreLikeThis'; 35 | import { getPercolateITC } from './Specialized/Percolate'; 36 | import { getScriptITC } from './Specialized/Script'; 37 | 38 | import { getHasChildITC } from './Joining/HasChild'; 39 | import { getHasParentITC } from './Joining/HasParent'; 40 | import { getNestedITC } from './Joining/Nested'; 41 | import { getParentIdITC } from './Joining/ParentId'; 42 | 43 | import { getTypeName, CommonOpts, desc } from '../../utils'; 44 | 45 | export function getQueryITC(opts: CommonOpts): InputTypeComposer { 46 | const name = getTypeName('Query', opts); 47 | const description = desc( 48 | ` 49 | Elasticsearch provides a full Query DSL based on JSON to define queries. 50 | [Query DSL](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html) 51 | ` 52 | ); 53 | 54 | return opts.getOrCreateITC(name, () => ({ 55 | name, 56 | description, 57 | fields: { 58 | match_all: getMatchAllITC(opts), 59 | 60 | // Compound queries 61 | bool: getBoolITC(opts), 62 | constant_score: getConstantScoreITC(opts), 63 | dis_max: getDisMaxITC(opts), 64 | boosting: getBoostingITC(opts), 65 | function_score: getFunctionScoreITC(opts), 66 | 67 | // FullText queries 68 | match: getMatchITC(opts), 69 | match_phrase: getMatchPhraseITC(opts), 70 | match_phrase_prefix: getMatchPhrasePrefixITC(opts), 71 | multi_match: getMultiMatchITC(opts), 72 | common: getCommonITC(opts), 73 | query_string: getQueryStringITC(opts), 74 | simple_query_string: getSimpleQueryStringITC(opts), 75 | 76 | // Term queries 77 | exists: getExistsITC(opts), 78 | fuzzy: getFuzzyITC(opts), 79 | ids: getIdsITC(opts), 80 | prefix: getPrefixITC(opts), 81 | range: getRangeITC(opts), 82 | regexp: getRegexpITC(opts), 83 | type: getTypeITC(opts), 84 | term: getTermITC(opts), 85 | terms: getTermsITC(opts), 86 | wildcard: getWildcardITC(opts), 87 | 88 | // Geo queries 89 | geo_bounding_box: getGeoBoundingBoxITC(opts), 90 | geo_distance: getGeoDistanceITC(opts), 91 | geo_polygon: getGeoPolygonITC(opts), 92 | geo_shape: getGeoShapeITC(opts), 93 | 94 | // Specialized queries 95 | more_like_this: getMoreLikeThisITC(opts), 96 | percolate: getPercolateITC(opts), 97 | script: getScriptITC(opts), 98 | 99 | // Joining queries 100 | has_child: getHasChildITC(opts), 101 | has_parent: getHasParentITC(opts), 102 | nested: getNestedITC(opts), 103 | parent_id: getParentIdITC(opts), 104 | }, 105 | })); 106 | } 107 | 108 | /* eslint-disable no-param-reassign */ 109 | export function prepareQueryInResolve( 110 | query: { [argName: string]: any }, 111 | fieldMap: any 112 | ): { [argName: string]: any } { 113 | // 114 | // Compound queries 115 | // 116 | if (query.bool) { 117 | query.bool = prepareBoolInResolve(query.bool, fieldMap); 118 | } 119 | if (query.nested && query.nested.query && query.nested.path) { 120 | query.nested.path = query.nested.path.replace(/__/g, '.'); 121 | query.nested.query = prepareQueryInResolve(query.nested.query, fieldMap); 122 | } 123 | if (query.constant_score) { 124 | query.constant_score = prepareConstantScoreInResolve(query.constant_score, fieldMap); 125 | } 126 | if (query.dis_max) { 127 | query.dis_max = prepareDisMaxResolve(query.dis_max, fieldMap); 128 | } 129 | if (query.boosting) { 130 | query.boosting = prepareBoostingInResolve(query.boosting, fieldMap); 131 | } 132 | if (query.function_score) { 133 | query.function_score = prepareFunctionScoreInResolve(query.function_score, fieldMap); 134 | } 135 | 136 | // 137 | // FullText queries 138 | // 139 | if (query.match) { 140 | query.match = renameUnderscoredToDots(query.match, fieldMap); 141 | } 142 | if (query.match_phrase) { 143 | query.match_phrase = renameUnderscoredToDots(query.match_phrase, fieldMap); 144 | } 145 | if (query.match_phrase_prefix) { 146 | query.match_phrase_prefix = renameUnderscoredToDots(query.match_phrase_prefix, fieldMap); 147 | } 148 | if (query.common) { 149 | query.common = renameUnderscoredToDots(query.common, fieldMap); 150 | } 151 | 152 | // 153 | // Geo queries 154 | // 155 | if (query.geo_bounding_box) { 156 | query.geo_bounding_box = renameUnderscoredToDots(query.geo_bounding_box, fieldMap); 157 | } 158 | if (query.geo_distance) { 159 | query.geo_distance = renameUnderscoredToDots(query.geo_distance, fieldMap); 160 | } 161 | if (query.geo_polygon) { 162 | query.geo_polygon = renameUnderscoredToDots(query.geo_polygon, fieldMap); 163 | } 164 | if (query.geo_shape) { 165 | query.geo_shape = renameUnderscoredToDots(query.geo_shape, fieldMap); 166 | } 167 | 168 | // 169 | // TermLevel queries 170 | // 171 | if (query.fuzzy) { 172 | query.fuzzy = renameUnderscoredToDots(query.fuzzy, fieldMap); 173 | } 174 | if (query.prefix) { 175 | query.prefix = renameUnderscoredToDots(query.prefix, fieldMap); 176 | } 177 | if (query.range) { 178 | query.range = renameUnderscoredToDots(query.range, fieldMap); 179 | } 180 | if (query.regexp) { 181 | query.regexp = renameUnderscoredToDots(query.regexp, fieldMap); 182 | } 183 | if (query.term) { 184 | query.term = renameUnderscoredToDots(query.term, fieldMap); 185 | } 186 | if (query.terms) { 187 | query.terms = renameUnderscoredToDots(query.terms, fieldMap); 188 | } 189 | if (query.wildcard) { 190 | query.wildcard = renameUnderscoredToDots(query.wildcard, fieldMap); 191 | } 192 | 193 | return query; 194 | } 195 | 196 | export function renameUnderscoredToDots(obj: any, _fieldMap: any) { 197 | const result = {} as Record; 198 | Object.keys(obj).forEach((o) => { 199 | result[o.replace(/__/g, '.')] = obj[o]; 200 | }); 201 | return result; 202 | } 203 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/Specialized/MoreLikeThis.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getAnalyzedFields } from '../../Commons/FieldNames'; 4 | 5 | export function getMoreLikeThisITC( 6 | opts: CommonOpts 7 | ): InputTypeComposer { 8 | const name = getTypeName('QueryMoreLikeThis', opts); 9 | const description = desc( 10 | ` 11 | The More Like This Query (MLT Query) finds documents that are like a given set of documents. 12 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-mlt-query.html) 13 | ` 14 | ); 15 | 16 | return opts.getOrCreateITC(name, () => ({ 17 | name, 18 | description, 19 | fields: { 20 | fields: [getAnalyzedFields(opts)], 21 | like: 'JSON', 22 | unlike: 'JSON', 23 | min_term_freq: 'Int', 24 | max_query_terms: 'Int', 25 | boost: 'Float', 26 | }, 27 | })); 28 | } 29 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/Specialized/Percolate.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getPercolatorFields } from '../../Commons/FieldNames'; 4 | 5 | export function getPercolateITC(opts: CommonOpts): InputTypeComposer { 6 | const name = getTypeName('QueryPercolate', opts); 7 | const description = desc( 8 | ` 9 | The percolate query can be used to match queries stored in an index. 10 | The percolate query itself contains the document that will be used 11 | as query to match with the stored queries. 12 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-percolate-query.html) 13 | ` 14 | ); 15 | 16 | return opts.getOrCreateITC(name, () => ({ 17 | name, 18 | description, 19 | fields: { 20 | field: getPercolatorFields(opts), 21 | document_type: 'String!', 22 | document: 'JSON!', 23 | }, 24 | })); 25 | } 26 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/Specialized/Script.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getCommonsScriptITC } from '../../Commons/Script'; 4 | 5 | export function getScriptITC(opts: CommonOpts): InputTypeComposer { 6 | const name = getTypeName('QueryScript', opts); 7 | const description = desc( 8 | ` 9 | A query allowing to define scripts as queries. They are typically used in a filter context. 10 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-script-query.html) 11 | ` 12 | ); 13 | 14 | return opts.getOrCreateITC(name, () => ({ 15 | name, 16 | description, 17 | fields: { 18 | script: { type: () => getCommonsScriptITC(opts) }, 19 | }, 20 | })); 21 | } 22 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/TermLevel/Exists.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getAllFields } from '../../Commons/FieldNames'; 4 | 5 | export function getExistsITC(opts: CommonOpts): InputTypeComposer { 6 | const name = getTypeName('QueryExists', opts); 7 | const description = desc( 8 | ` 9 | Returns documents that have at least one non-null value in the original field. 10 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-exists-query.html) 11 | ` 12 | ); 13 | 14 | return opts.getOrCreateITC(name, () => ({ 15 | name, 16 | description, 17 | fields: { 18 | field: getAllFields(opts), 19 | }, 20 | })); 21 | } 22 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/TermLevel/Fuzzy.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer, InputTypeComposerFieldConfigAsObjectDefinition } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getAnalyzedAsFieldConfigMap } from '../../Commons/FieldNames'; 4 | 5 | export function getFuzzyITC( 6 | opts: CommonOpts 7 | ): InputTypeComposer | InputTypeComposerFieldConfigAsObjectDefinition { 8 | const name = getTypeName('QueryFuzzy', opts); 9 | const description = desc( 10 | ` 11 | The fuzzy query uses similarity based on Levenshtein edit distance. 12 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-fuzzy-query.html) 13 | ` 14 | ); 15 | 16 | const subName = getTypeName('QueryFuzzySettings', opts); 17 | const fields = getAnalyzedAsFieldConfigMap( 18 | opts, 19 | opts.getOrCreateITC(subName, () => ({ 20 | name: subName, 21 | fields: { 22 | value: 'String!', 23 | boost: 'Float', 24 | fuzziness: 'Int', 25 | prefix_length: 'Int', 26 | max_expansions: 'Int', 27 | }, 28 | })) 29 | ); 30 | 31 | if (typeof fields === 'object') { 32 | return opts.getOrCreateITC(name, () => ({ 33 | name, 34 | description, 35 | fields, 36 | })); 37 | } 38 | 39 | return { 40 | type: 'JSON', 41 | description, 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/TermLevel/Ids.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getIdsITC(opts: CommonOpts): InputTypeComposer { 5 | const name = getTypeName('QueryIds', opts); 6 | const description = desc( 7 | ` 8 | Filters documents that only have the provided ids. 9 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-ids-query.html) 10 | ` 11 | ); 12 | 13 | return opts.getOrCreateITC(name, () => ({ 14 | name, 15 | description, 16 | fields: { 17 | type: 'String!', 18 | values: '[String]!', 19 | }, 20 | })); 21 | } 22 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/TermLevel/Prefix.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer, InputTypeComposerFieldConfigAsObjectDefinition } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getKeywordAsFieldConfigMap } from '../../Commons/FieldNames'; 4 | 5 | export function getPrefixITC( 6 | opts: CommonOpts 7 | ): InputTypeComposer | InputTypeComposerFieldConfigAsObjectDefinition { 8 | const name = getTypeName('QueryPrefix', opts); 9 | const description = desc( 10 | ` 11 | Matches documents that have fields containing terms with a specified prefix (not analyzed). 12 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-prefix-query.html) 13 | ` 14 | ); 15 | 16 | const subName = getTypeName('QueryPrefixSettings', opts); 17 | const fields = getKeywordAsFieldConfigMap( 18 | opts, 19 | opts.getOrCreateITC(subName, () => ({ 20 | name: subName, 21 | fields: { 22 | value: 'String!', 23 | boost: 'Float', 24 | }, 25 | })) 26 | ); 27 | 28 | if (typeof fields === 'object') { 29 | return opts.getOrCreateITC(name, () => ({ 30 | name, 31 | description, 32 | fields, 33 | })); 34 | } 35 | 36 | return { 37 | type: 'JSON', 38 | description, 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/TermLevel/Range.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer, InputTypeComposerFieldConfigAsObjectDefinition } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getAllAsFieldConfigMap } from '../../Commons/FieldNames'; 4 | 5 | export function getRangeITC( 6 | opts: CommonOpts 7 | ): InputTypeComposer | InputTypeComposerFieldConfigAsObjectDefinition { 8 | const name = getTypeName('QueryRange', opts); 9 | const description = desc( 10 | ` 11 | Matches documents with fields that have terms within a certain range. 12 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html) 13 | ` 14 | ); 15 | 16 | const subName = getTypeName('QueryRangeSettings', opts); 17 | const fields = getAllAsFieldConfigMap( 18 | opts, 19 | opts.getOrCreateITC(subName, () => ({ 20 | name: subName, 21 | fields: { 22 | gt: 'JSON', 23 | gte: 'JSON', 24 | lt: 'JSON', 25 | lte: 'JSON', 26 | boost: 'Float', 27 | relation: 'String', 28 | }, 29 | })) 30 | ); 31 | 32 | if (typeof fields === 'object') { 33 | return opts.getOrCreateITC(name, () => ({ 34 | name, 35 | description, 36 | fields, 37 | })); 38 | } 39 | 40 | return { 41 | type: 'JSON', 42 | description, 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/TermLevel/Regexp.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer, InputTypeComposerFieldConfigAsObjectDefinition } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getStringAsFieldConfigMap } from '../../Commons/FieldNames'; 4 | 5 | export function getRegexpITC( 6 | opts: CommonOpts 7 | ): InputTypeComposer | InputTypeComposerFieldConfigAsObjectDefinition { 8 | const name = getTypeName('QueryRegexp', opts); 9 | const description = desc( 10 | ` 11 | The regexp query allows you to use regular expression term queries. 12 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html) 13 | ` 14 | ); 15 | 16 | const subName = getTypeName('QueryRegexpSettings', opts); 17 | const fields = getStringAsFieldConfigMap( 18 | opts, 19 | opts.getOrCreateITC(subName, () => ({ 20 | name: subName, 21 | fields: { 22 | value: 'String!', 23 | boost: 'Float', 24 | flags: 'String', 25 | max_determinized_states: 'Int', 26 | }, 27 | })) 28 | ); 29 | 30 | if (typeof fields === 'object') { 31 | return opts.getOrCreateITC(name, () => ({ 32 | name, 33 | description, 34 | fields, 35 | })); 36 | } 37 | 38 | return { 39 | type: 'JSON', 40 | description, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/TermLevel/Term.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer, InputTypeComposerFieldConfigAsObjectDefinition } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getAllAsFieldConfigMap } from '../../Commons/FieldNames'; 4 | 5 | export function getTermITC( 6 | opts: CommonOpts 7 | ): InputTypeComposer | InputTypeComposerFieldConfigAsObjectDefinition { 8 | const name = getTypeName('QueryTerm', opts); 9 | const description = desc( 10 | ` 11 | Find documents which contain the exact term specified 12 | in the field specified. 13 | { fieldName: value } or { fieldName: { value: value, boost: 2.0 } } 14 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html) 15 | ` 16 | ); 17 | 18 | const subName = getTypeName('QueryTermSettings', opts); 19 | const fields = getAllAsFieldConfigMap( 20 | opts, 21 | opts.getOrCreateITC(subName, () => ({ 22 | name: subName, 23 | fields: { 24 | value: 'JSON!', 25 | boost: 'Float', 26 | fuzziness: 'Int', 27 | prefix_length: 'Int', 28 | max_expansions: 'Int', 29 | }, 30 | })) 31 | ); 32 | 33 | if (typeof fields === 'object') { 34 | return opts.getOrCreateITC(name, () => ({ 35 | name, 36 | description, 37 | fields, 38 | })); 39 | } 40 | 41 | return { 42 | type: 'JSON', 43 | description, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/TermLevel/Terms.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer, InputTypeComposerFieldConfigAsObjectDefinition } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getAllAsFieldConfigMap } from '../../Commons/FieldNames'; 4 | 5 | export function getTermsITC( 6 | opts: CommonOpts 7 | ): InputTypeComposer | InputTypeComposerFieldConfigAsObjectDefinition { 8 | const name = getTypeName('QueryTerms', opts); 9 | const description = desc( 10 | ` 11 | Filters documents that have fields that match any of 12 | the provided terms (not analyzed). { fieldName: [values] } 13 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-query.html) 14 | ` 15 | ); 16 | 17 | const fields = getAllAsFieldConfigMap(opts, '[JSON]'); 18 | 19 | if (typeof fields === 'object') { 20 | return opts.getOrCreateITC(name, () => ({ 21 | name, 22 | description, 23 | fields, 24 | })); 25 | } 26 | 27 | return { 28 | type: 'JSON', 29 | description, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/TermLevel/Type.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | 4 | export function getTypeITC(opts: CommonOpts): InputTypeComposer { 5 | const name = getTypeName('QueryType', opts); 6 | const description = desc( 7 | ` 8 | Filters documents matching the provided document / mapping type. 9 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-type-query.html) 10 | ` 11 | ); 12 | 13 | return opts.getOrCreateITC(name, () => ({ 14 | name, 15 | description, 16 | fields: { 17 | value: 'String', 18 | }, 19 | })); 20 | } 21 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/TermLevel/Wildcard.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer, InputTypeComposerFieldConfigAsObjectDefinition } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../../../utils'; 3 | import { getKeywordAsFieldConfigMap } from '../../Commons/FieldNames'; 4 | 5 | export function getWildcardITC( 6 | opts: CommonOpts 7 | ): InputTypeComposer | InputTypeComposerFieldConfigAsObjectDefinition { 8 | const name = getTypeName('QueryWildcard', opts); 9 | const description = desc( 10 | ` 11 | Matches documents that have fields matching a wildcard expression (not analyzed). 12 | In order to prevent extremely SLOW wildcard queries, term should not start 13 | from * or ?. 14 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-wildcard-query.html) 15 | ` 16 | ); 17 | 18 | const subName = getTypeName('QueryWildcardSettings', opts); 19 | const fields = getKeywordAsFieldConfigMap( 20 | opts, 21 | opts.getOrCreateITC(subName, () => ({ 22 | name: subName, 23 | fields: { 24 | value: 'String!', 25 | boost: 'Float', 26 | }, 27 | })) 28 | ); 29 | 30 | if (typeof fields === 'object') { 31 | return opts.getOrCreateITC(name, () => ({ 32 | name, 33 | description, 34 | fields, 35 | })); 36 | } 37 | 38 | return { 39 | type: 'JSON', 40 | description, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/elasticDSL/Query/__tests__/Query-test.ts: -------------------------------------------------------------------------------- 1 | import { schemaComposer, graphql } from 'graphql-compose'; 2 | import { getQueryITC } from '../Query'; 3 | import { prepareCommonOpts } from '../../../utils'; 4 | 5 | const { printSchema } = graphql; 6 | 7 | beforeEach(() => { 8 | schemaComposer.clear(); 9 | }); 10 | 11 | describe('AGGS args converter', () => { 12 | it('Query DSL', () => { 13 | schemaComposer.Query.addFields({ 14 | search: { 15 | args: { 16 | body: { 17 | type: getQueryITC( 18 | prepareCommonOpts(schemaComposer, { 19 | prefix: 'Elastic_', 20 | postfix: '_50', 21 | }) 22 | ), 23 | }, 24 | }, 25 | type: 'Int', 26 | }, 27 | }); 28 | const schema = schemaComposer.buildSchema(); 29 | expect(printSchema(schema)).toMatchSnapshot(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/elasticDSL/SearchBody.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer } from 'graphql-compose'; 2 | import { getQueryITC, prepareQueryInResolve } from './Query/Query'; 3 | import { getAggsITC, prepareAggsInResolve } from './Aggs/Aggs'; 4 | import { getSortITC } from './Sort'; 5 | import { getTypeName, CommonOpts, desc } from '../utils'; 6 | 7 | export function getSearchBodyITC( 8 | opts: CommonOpts 9 | ): InputTypeComposer { 10 | const name = getTypeName('SearchBody', opts); 11 | const description = desc( 12 | ` 13 | Request Body Search 14 | [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html) 15 | ` 16 | ); 17 | 18 | return opts.getOrCreateITC(name, () => ({ 19 | name, 20 | description, 21 | fields: { 22 | query: { type: () => getQueryITC(opts) }, 23 | collapse: 'JSON', 24 | aggs: { type: () => getAggsITC(opts) }, 25 | size: 'Int', 26 | from: 'Int', 27 | sort: { type: () => [getSortITC(opts)] }, 28 | _source: 'JSON', 29 | script_fields: 'JSON', 30 | post_filter: { type: () => getQueryITC(opts) }, 31 | highlight: 'JSON', 32 | search_after: 'JSON', 33 | 34 | explain: 'Boolean', 35 | version: 'Boolean', 36 | indices_boost: 'JSON', 37 | min_score: 'Float', 38 | 39 | search_type: 'String', 40 | rescore: 'JSON', 41 | docvalue_fields: '[String]', 42 | stored_fields: '[String]', 43 | }, 44 | })); 45 | } 46 | 47 | export function prepareBodyInResolve(body: any, fieldMap: any): { [argName: string]: any } { 48 | /* eslint-disable no-param-reassign */ 49 | if (body.query) { 50 | body.query = prepareQueryInResolve(body.query, fieldMap); 51 | } 52 | if (body.aggs) { 53 | body.aggs = prepareAggsInResolve(body.aggs, fieldMap); 54 | } 55 | return body; 56 | } 57 | -------------------------------------------------------------------------------- /src/elasticDSL/Sort.ts: -------------------------------------------------------------------------------- 1 | import type { ComposeInputType } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts } from '../utils'; 3 | import { getFieldNamesByElasticType } from './Commons/FieldNames'; 4 | 5 | const sortableTypes = [ 6 | 'byte', 7 | 'short', 8 | 'integer', 9 | 'long', 10 | 'double', 11 | 'float', 12 | 'half_float', 13 | 'scaled_float', 14 | 'token_count', 15 | 'date', 16 | 'boolean', 17 | 'ip', 18 | 'keyword', 19 | ]; 20 | 21 | export function getSortITC(opts: CommonOpts): ComposeInputType { 22 | const name = getTypeName('SortEnum', opts); 23 | const description = 'Sortable fields from mapping'; 24 | 25 | if (!opts.fieldMap) { 26 | return opts.schemaComposer.getSTC('JSON'); 27 | } 28 | 29 | return opts.schemaComposer.getOrSet(name, () => { 30 | const sortableFields = getFieldNamesByElasticType(opts.fieldMap, sortableTypes); 31 | if (sortableFields.length === 0) { 32 | return opts.schemaComposer.getSTC('JSON'); 33 | } 34 | 35 | const values = { 36 | _score: { 37 | value: '_score', 38 | }, 39 | } as Record; 40 | sortableFields.forEach((fieldName) => { 41 | const dottedName = fieldName.replace(/__/g, '.'); 42 | values[`${fieldName}__asc`] = { 43 | value: { [dottedName]: 'asc' }, 44 | }; 45 | values[`${fieldName}__desc`] = { 46 | value: { [dottedName]: 'desc' }, 47 | }; 48 | }); 49 | 50 | return opts.schemaComposer.createEnumTC({ 51 | name, 52 | description, 53 | values, 54 | }); 55 | }) as any; 56 | } 57 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { composeWithElastic } from './composeWithElastic'; 2 | export { convertToSourceTC, inputPropertiesToGraphQLTypes } from './mappingConverter'; 3 | export { default as ElasticApiParser } from './ElasticApiParser'; 4 | export { elasticApiFieldConfig } from './elasticApiFieldConfig'; 5 | export { fetchElasticMapping } from './utils'; 6 | -------------------------------------------------------------------------------- /src/mappingConverter.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define, no-param-reassign */ 2 | 3 | import { 4 | ObjectTypeComposer, 5 | SchemaComposer, 6 | upperFirst, 7 | isObject, 8 | ComposeInputTypeDefinition, 9 | ComposeOutputTypeDefinition, 10 | } from 'graphql-compose'; 11 | import { ElasticGeoPointType } from './elasticDSL/Commons/Geo'; 12 | 13 | export type ElasticMappingT = { 14 | properties: ElasticMappingPropertiesT; 15 | }; 16 | 17 | export type ElasticMappingPropertiesT = { 18 | [propertyName: string]: ElasticPropertyT; 19 | }; 20 | 21 | export type ElasticPropertyT = { 22 | type?: string; 23 | fields?: ElasticMappingPropertiesT; 24 | properties?: ElasticMappingPropertiesT; 25 | index?: any; 26 | }; 27 | 28 | export type InputFieldsMap = { 29 | [field: string]: ComposeInputTypeDefinition; 30 | }; 31 | 32 | export type FieldsMapByElasticType = { 33 | [elasticType: string]: InputFieldsMap; 34 | _all: InputFieldsMap; 35 | }; 36 | 37 | export const typeMap = { 38 | text: 'String', 39 | keyword: 'String', 40 | string: 'String', 41 | byte: 'Int', // 8-bit integer 42 | short: 'Int', // 16-bit integer 43 | integer: 'Int', // 32-bit integer 44 | long: 'Int', // 64-bit (should changed in future for 64 GraphQL type) 45 | double: 'Float', // 64-bit (should changed in future for 64 GraphQL type) 46 | float: 'Float', // 32-bit 47 | half_float: 'Float', // 16-bit 48 | scaled_float: 'Float', 49 | date: 'Date', 50 | boolean: 'Boolean', 51 | binary: 'Buffer', 52 | token_count: 'Int', 53 | ip: 'String', 54 | geo_point: ElasticGeoPointType, // 'JSON' 55 | geo_shape: 'JSON', 56 | object: 'JSON', 57 | nested: '[JSON]', 58 | completion: 'String', 59 | percolator: 'JSON', 60 | }; 61 | 62 | export type ConvertOptsT = { 63 | prefix?: string | null; 64 | postfix?: string | null; 65 | pluralFields?: string[]; 66 | }; 67 | 68 | export function convertToSourceTC( 69 | schemaComposer: SchemaComposer, 70 | mapping: ElasticMappingT | ElasticPropertyT, 71 | typeName: string, 72 | opts: ConvertOptsT = {} 73 | ): ObjectTypeComposer { 74 | if (!mapping || !mapping.properties) { 75 | throw new Error('You provide incorrect mapping. It should be an object `{ properties: {} }`'); 76 | } 77 | if (!typeName || typeof typeName !== 'string') { 78 | throw new Error( 79 | 'You provide empty name for type. Second argument `typeName` should be non-empty string.' 80 | ); 81 | } 82 | 83 | const tc = schemaComposer.createObjectTC({ 84 | name: `${opts.prefix || ''}${typeName}${opts.postfix || ''}`, 85 | description: 86 | 'Elasticsearch mapping does not contains info about ' + 87 | 'is field plural or not. So `propName` is singular and returns value ' + 88 | 'or first value from array. ' + 89 | '`propNameA` is plural and returns array of values.', 90 | }); 91 | 92 | const { properties = {} } = mapping; 93 | const fields = {} as Record; 94 | const pluralFields = opts.pluralFields || []; 95 | 96 | Object.keys(properties).forEach((sourceName) => { 97 | const fieldName = sourceName.replace(/[^_a-zA-Z0-9]/g, '_'); 98 | const gqType = propertyToSourceGraphQLType( 99 | schemaComposer, 100 | properties[sourceName], 101 | `${typeName}${upperFirst(fieldName)}`, 102 | { 103 | ...opts, 104 | pluralFields: getSubFields(sourceName, pluralFields), 105 | } 106 | ); 107 | if (gqType) { 108 | if (pluralFields.indexOf(sourceName) >= 0) { 109 | fields[fieldName] = { 110 | type: [gqType], 111 | resolve: (source: any) => { 112 | if (Array.isArray(source[sourceName])) { 113 | return source[sourceName]; 114 | } 115 | return [source[sourceName]]; 116 | }, 117 | }; 118 | } else { 119 | fields[fieldName] = { 120 | type: gqType, 121 | resolve: (source: any) => { 122 | if (Array.isArray(source[sourceName])) { 123 | return source[sourceName][0]; 124 | } 125 | return source[sourceName]; 126 | }, 127 | }; 128 | } 129 | } 130 | }); 131 | 132 | tc.addFields(fields); 133 | 134 | return tc; 135 | } 136 | 137 | export function propertyToSourceGraphQLType( 138 | schemaComposer: SchemaComposer, 139 | prop: ElasticPropertyT, 140 | typeName?: string, 141 | opts?: ConvertOptsT 142 | ): ComposeOutputTypeDefinition { 143 | if (!prop || (typeof prop.type !== 'string' && !prop.properties)) { 144 | throw new Error('You provide incorrect Elastic property config.'); 145 | } 146 | 147 | if (prop.properties) { 148 | // object type with subfields 149 | return convertToSourceTC(schemaComposer, prop, typeName || '', opts); 150 | } 151 | 152 | const type = prop.type as keyof typeof typeMap; 153 | if (type && typeMap[type]) { 154 | return typeMap[type]; 155 | } 156 | 157 | return 'JSON'; 158 | } 159 | 160 | export function inputPropertiesToGraphQLTypes( 161 | prop: ElasticPropertyT | ElasticMappingT, 162 | fieldName?: string, 163 | result: FieldsMapByElasticType = { _all: {} } 164 | ): FieldsMapByElasticType { 165 | if ( 166 | !prop || 167 | (typeof (prop as ElasticPropertyT).type !== 'string' && !(prop as ElasticMappingT).properties) 168 | ) { 169 | throw new Error('You provide incorrect Elastic property config.'); 170 | } 171 | 172 | // mapping 173 | const { properties } = prop as ElasticMappingT; 174 | if (properties && isObject(properties)) { 175 | Object.keys(properties).forEach((subFieldName) => { 176 | inputPropertiesToGraphQLTypes( 177 | properties[subFieldName], 178 | [fieldName, subFieldName].filter((o) => !!o).join('__'), 179 | result 180 | ); 181 | }); 182 | return result; 183 | } 184 | 185 | // object type with subfields 186 | const { fields } = prop as ElasticPropertyT; 187 | if (fields && isObject(fields)) { 188 | Object.keys(fields).forEach((subFieldName) => { 189 | inputPropertiesToGraphQLTypes( 190 | fields[subFieldName], 191 | [fieldName, subFieldName].filter((o) => !!o).join('__'), 192 | result 193 | ); 194 | }); 195 | } 196 | 197 | // skip no index fields 198 | if (prop.hasOwnProperty('index') && !(prop as ElasticPropertyT).index) { 199 | return result; 200 | } 201 | 202 | const type = (prop as ElasticPropertyT).type; 203 | if (typeof type === 'string' && fieldName) { 204 | if (!result[type]) { 205 | const newMap: InputFieldsMap = {}; 206 | result[type] = newMap; 207 | } 208 | 209 | const graphqlType = (typeMap as any)[type] || 'JSON'; 210 | result[type][fieldName] = graphqlType; 211 | result._all[fieldName] = graphqlType; 212 | } 213 | 214 | return result; 215 | } 216 | 217 | export function getSubFields(fieldName: string, pluralFields?: string[]): string[] { 218 | const st = `${fieldName}.`; 219 | return (pluralFields || []) 220 | .filter((o) => typeof o === 'string' && o.startsWith(st)) 221 | .map((v) => v.slice(st.length)); 222 | } 223 | -------------------------------------------------------------------------------- /src/resolvers/__tests__/findById-test.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, schemaComposer } from 'graphql-compose'; 2 | import createFindByIdResolver from '../findById'; 3 | import elasticClient from '../../__mocks__/elasticClient'; 4 | import { CvTC, CvFieldMap } from '../../__mocks__/cv'; 5 | import { prepareCommonOpts } from '../../utils'; 6 | 7 | const findByIdResolver = createFindByIdResolver( 8 | prepareCommonOpts(schemaComposer, { 9 | sourceTC: CvTC, 10 | fieldMap: CvFieldMap, 11 | elasticClient, 12 | elasticIndex: 'cv', 13 | elasticType: 'cv', 14 | }) 15 | ); 16 | 17 | beforeEach(() => { 18 | schemaComposer.clear(); 19 | }); 20 | 21 | describe('findById', () => { 22 | it('return instance of Resolver', () => { 23 | expect(findByIdResolver).toBeInstanceOf(Resolver); 24 | }); 25 | 26 | it('check args', () => { 27 | expect(findByIdResolver.hasArg('id')).toBeTruthy(); 28 | }); 29 | 30 | it('resolve', async () => { 31 | await findByIdResolver 32 | .resolve({ args: { id: '4554' }, context: { elasticClient } }) 33 | .then((res: any) => { 34 | console.log(res); // eslint-disable-line 35 | }) 36 | .catch((e: Error) => { 37 | expect(e).toMatchObject({ message: /unknown error/ }); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/resolvers/__tests__/search-test.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, schemaComposer } from 'graphql-compose'; 2 | import createSearchResolver, * as Search from '../search'; 3 | import elasticClient from '../../__mocks__/elasticClient'; 4 | import { CvTC, CvFieldMap } from '../../__mocks__/cv'; 5 | import { prepareCommonOpts } from '../../utils'; 6 | 7 | const opts = prepareCommonOpts(schemaComposer, { 8 | sourceTC: CvTC, 9 | fieldMap: CvFieldMap, 10 | elasticClient, 11 | elasticIndex: 'cv', 12 | elasticType: 'cv', 13 | }); 14 | 15 | beforeEach(() => { 16 | schemaComposer.clear(); 17 | }); 18 | 19 | describe('search resolver', () => { 20 | it('should return Resolver', () => { 21 | expect(createSearchResolver(opts)).toBeInstanceOf(Resolver); 22 | }); 23 | 24 | describe('helper methods', () => { 25 | it('toDottedList()', () => { 26 | expect(Search.toDottedList({ a: { b: true, c: { e: true } }, d: true })).toEqual([ 27 | 'a.b', 28 | 'a.c.e', 29 | 'd', 30 | ]); 31 | expect(Search.toDottedList({})).toEqual(true); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/resolvers/findById.ts: -------------------------------------------------------------------------------- 1 | import { Resolver } from 'graphql-compose'; 2 | import type { ResolverResolveParams } from 'graphql-compose'; 3 | import ElasticApiParser from '../ElasticApiParser'; 4 | import { getFindByIdOutputTC } from '../types/FindByIdOutput'; 5 | import type { CommonOpts } from '../utils'; 6 | 7 | export default function createFindByIdResolver( 8 | opts: CommonOpts 9 | ): Resolver { 10 | const { fieldMap, sourceTC, schemaComposer } = opts; 11 | 12 | if (!fieldMap || !fieldMap._all) { 13 | throw new Error( 14 | 'opts.fieldMap for Resolver findById() should be fieldMap of FieldsMapByElasticType type.' 15 | ); 16 | } 17 | 18 | if (!sourceTC || sourceTC.constructor.name !== 'ObjectTypeComposer') { 19 | throw new Error( 20 | 'opts.sourceTC for Resolver findById() should be instance of ObjectTypeComposer.' 21 | ); 22 | } 23 | 24 | const parser = new ElasticApiParser({ 25 | elasticClient: opts.elasticClient, 26 | prefix: opts.prefix, 27 | }); 28 | 29 | const findByIdFC = parser.generateFieldConfig('get', { 30 | index: opts.elasticIndex, 31 | type: opts.elasticType, 32 | }); 33 | 34 | const type = getFindByIdOutputTC(opts); 35 | 36 | return schemaComposer.createResolver({ 37 | type, 38 | name: 'findById', 39 | kind: 'query', 40 | args: { 41 | id: 'String!', 42 | }, 43 | resolve: async (rp: ResolverResolveParams) => { 44 | const { source, args, context, info } = rp; 45 | 46 | if (!args.id) { 47 | throw new Error(`Missed 'id' argument!`); 48 | } 49 | 50 | const res = await findByIdFC.resolve(source, args, context, info); 51 | const { _index, _type, _id, _version, _source } = res || {}; 52 | 53 | return { 54 | _index, 55 | _type, 56 | _id, 57 | _version, 58 | ..._source, 59 | }; 60 | }, 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /src/resolvers/searchConnection.ts: -------------------------------------------------------------------------------- 1 | import type { Resolver, ObjectTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts } from '../utils'; 3 | 4 | export default function createSearchConnectionResolver( 5 | opts: CommonOpts, 6 | searchResolver: Resolver 7 | ): Resolver { 8 | const resolver = searchResolver.clone({ 9 | name: `searchConnection`, 10 | }) as Resolver; 11 | 12 | resolver 13 | .addArgs({ 14 | first: 'Int', 15 | after: 'String', 16 | last: 'Int', 17 | before: 'String', 18 | }) 19 | .removeArg(['limit', 'skip']) 20 | .reorderArgs(['q', 'query', 'sort', 'aggs', 'first', 'after', 'last', 'before']); 21 | 22 | const searchTC = searchResolver.getOTC(); 23 | if (!searchTC) { 24 | throw new Error('Cannot get ObjectTypeComposer from resolver. Maybe resolver return Scalar?!'); 25 | } 26 | 27 | const typeName = searchTC.getTypeName(); 28 | resolver.setType( 29 | searchTC 30 | .clone(`${typeName}Connection`) 31 | .addFields({ 32 | pageInfo: getPageInfoTC(opts), 33 | edges: [ 34 | opts.schemaComposer.createObjectTC({ 35 | name: `${typeName}Edge`, 36 | fields: { 37 | node: searchTC.getFieldTC('hits'), 38 | cursor: 'String!', 39 | }, 40 | }), 41 | ], 42 | }) 43 | .removeField('hits') 44 | .reorderFields(['count', 'pageInfo', 'edges', 'aggregations']) 45 | ); 46 | 47 | resolver.resolve = async (rp) => { 48 | const { args = {}, projection = {} } = rp; 49 | 50 | if (!args.sort || !Array.isArray(args.sort) || args.sort.length === 0) { 51 | throw new Error( 52 | 'Argument `sort` is required for the Relay Connection. According to ' + 53 | 'the fields in `sort` will be constructed `cursor`s for every edge. ' + 54 | 'Values of fields which used in `sort` should be unique in compound.' 55 | ); 56 | } 57 | 58 | const first = parseInt(args.first, 10) || 0; 59 | if (first < 0) { 60 | throw new Error('Argument `first` should be non-negative number.'); 61 | } 62 | const last = parseInt(args.last, 10) || 0; 63 | if (last < 0) { 64 | throw new Error('Argument `last` should be non-negative number.'); 65 | } 66 | const { before, after } = args; 67 | delete args.before; 68 | delete args.after; 69 | if (before !== undefined) { 70 | throw new Error('Elastic does not support before cursor.'); 71 | } 72 | if (after) { 73 | if (!args.body) args.body = {}; 74 | const tmp = cursorToData(after); 75 | if (Array.isArray(tmp)) { 76 | args.body.search_after = tmp; 77 | } 78 | } 79 | 80 | const limit = last || first || 20; 81 | const skip = last > 0 ? first - last : 0; 82 | 83 | delete args.last; 84 | delete args.first; 85 | args.limit = limit + 1; // +1 document, to check next page presence 86 | args.skip = skip; 87 | 88 | if (projection.edges) { 89 | projection.hits = projection.edges.node; 90 | delete projection.edges; 91 | } 92 | 93 | const res = await searchResolver.resolve(rp); 94 | 95 | let list = res.hits || []; 96 | 97 | const hasExtraRecords = list.length > limit; 98 | if (hasExtraRecords) list = list.slice(0, limit); 99 | const cursorMap = new Map(); 100 | const edges = list.map((node: any) => { 101 | const cursor = dataToCursor(node.sort); 102 | if (cursorMap.has(cursor)) { 103 | throw new Error( 104 | `Argument \`sort {${args.sort.join(', ')}}\` must be more complex! ` + 105 | 'Values from record which are used in `sort` will be used for `cursor` fields. ' + 106 | 'According to connection spec `cursor` must be unique for every node.' + 107 | 'Detected that two nodes have ' + 108 | `the same cursors '${cursor}' with data '${unbase64(cursor)}'. ` + 109 | 'You must add more `sort` fields, which provide unique data ' + 110 | 'for all cursors in the result set.' 111 | ); 112 | } 113 | cursorMap.set(cursor, node); 114 | return { node, cursor }; 115 | }); 116 | const result = { 117 | ...res, 118 | pageInfo: { 119 | hasNextPage: limit > 0 && hasExtraRecords, 120 | hasPreviousPage: false, // Elastic does not support before cursor 121 | startCursor: edges.length > 0 ? edges[0].cursor : null, 122 | endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null, 123 | }, 124 | edges, 125 | }; 126 | 127 | return result; 128 | }; 129 | 130 | return resolver; 131 | } 132 | 133 | function getPageInfoTC(opts: CommonOpts): ObjectTypeComposer { 134 | const name = getTypeName('PageInfo', opts); 135 | 136 | if (opts.schemaComposer.has(name)) { 137 | return opts.schemaComposer.getOTC(name); 138 | } 139 | 140 | return opts.schemaComposer.createObjectTC(` 141 | # Information about pagination in a connection. 142 | type ${name} { 143 | # When paginating forwards, are there more items? 144 | hasNextPage: Boolean! 145 | 146 | # When paginating backwards, are there more items? 147 | hasPreviousPage: Boolean! 148 | 149 | # When paginating backwards, the cursor to continue. 150 | startCursor: String 151 | 152 | # When paginating forwards, the cursor to continue. 153 | endCursor: String 154 | } 155 | `); 156 | } 157 | 158 | export function base64(i: string): string { 159 | return Buffer.from(i, 'ascii').toString('base64'); 160 | } 161 | 162 | export function unbase64(i: string): string { 163 | return Buffer.from(i, 'base64').toString('ascii'); 164 | } 165 | 166 | export function cursorToData(cursor: string): any { 167 | if (typeof cursor === 'string') { 168 | try { 169 | return JSON.parse(unbase64(cursor)) || null; 170 | } catch (err) { 171 | return null; 172 | } 173 | } 174 | return null; 175 | } 176 | 177 | export function dataToCursor(data: any): string { 178 | if (!data) return ''; 179 | return base64(JSON.stringify(data)); 180 | } 181 | -------------------------------------------------------------------------------- /src/resolvers/searchPagination.ts: -------------------------------------------------------------------------------- 1 | import type { Resolver, ObjectTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts } from '../utils'; 3 | 4 | export default function createSearchPaginationResolver( 5 | opts: CommonOpts, 6 | searchResolver: Resolver 7 | ): Resolver { 8 | const resolver = searchResolver.clone({ 9 | name: `searchPagination`, 10 | }) as Resolver; 11 | 12 | resolver 13 | .addArgs({ 14 | page: 'Int', 15 | perPage: { 16 | type: 'Int', 17 | defaultValue: 20, 18 | }, 19 | }) 20 | .removeArg(['limit', 'skip']) 21 | .reorderArgs(['q', 'query', 'sort', 'aggs', 'page', 'perPage']); 22 | 23 | const searchTC = searchResolver.getOTC(); 24 | if (!searchTC) { 25 | throw new Error('Cannot get ObjectTypeComposer from resolver. Maybe resolver return Scalar?!'); 26 | } 27 | 28 | const typeName = searchTC.getTypeName(); 29 | resolver.setType( 30 | searchTC 31 | .clone(`${typeName}Pagination`) 32 | .addFields({ 33 | pageInfo: getPageInfoTC(opts), 34 | items: searchTC.getFieldTC('hits').List, 35 | }) 36 | .removeField('hits') 37 | .reorderFields(['items', 'count', 'pageInfo', 'aggregations']) 38 | ); 39 | 40 | resolver.resolve = async (rp) => { 41 | const { args = {}, projection = {} } = rp; 42 | 43 | const page = args.page || 1; 44 | if (page <= 0) { 45 | throw new Error('Argument `page` should be positive number.'); 46 | } 47 | const perPage = args.perPage || 20; 48 | if (perPage <= 0) { 49 | throw new Error('Argument `perPage` should be positive number.'); 50 | } 51 | delete args.page; 52 | delete args.perPage; 53 | args.limit = perPage; 54 | args.skip = (page - 1) * perPage; 55 | 56 | if (projection.items) { 57 | projection.hits = projection.items; 58 | delete projection.items; 59 | } 60 | 61 | const res = await searchResolver.resolve(rp); 62 | 63 | const items = res.hits || []; 64 | const itemCount = res.count || 0; 65 | 66 | const result = { 67 | ...res, 68 | pageInfo: { 69 | hasNextPage: itemCount > page * perPage, 70 | hasPreviousPage: page > 1, 71 | currentPage: page, 72 | perPage, 73 | pageCount: Math.ceil(itemCount / perPage), 74 | itemCount, 75 | }, 76 | items, 77 | }; 78 | 79 | return result; 80 | }; 81 | 82 | return resolver; 83 | } 84 | 85 | function getPageInfoTC(opts: CommonOpts): ObjectTypeComposer { 86 | const name = getTypeName('PaginationInfo', opts); 87 | 88 | if (opts.schemaComposer.has(name)) { 89 | return opts.schemaComposer.getOTC(name); 90 | } 91 | 92 | return opts.schemaComposer.createObjectTC(` 93 | # Information about pagination. 94 | type ${name} { 95 | # Current page number 96 | currentPage: Int! 97 | 98 | # Number of items per page 99 | perPage: Int! 100 | 101 | # Total number of pages 102 | pageCount: Int 103 | 104 | # Total number of items 105 | itemCount: Int 106 | 107 | # When paginating forwards, are there more items? 108 | hasNextPage: Boolean 109 | 110 | # When paginating backwards, are there more items? 111 | hasPreviousPage: Boolean 112 | } 113 | `); 114 | } 115 | -------------------------------------------------------------------------------- /src/resolvers/suggest.ts: -------------------------------------------------------------------------------- 1 | // https://www.elastic.co/blog/you-complete-me 2 | // https://qbox.io/blog/multi-field-partial-word-autocomplete-in-elasticsearch-using-ngrams 3 | // https://qbox.io/blog/quick-and-dirty-autocomplete-with-elasticsearch-completion-suggest 4 | // https://engineering.skroutz.gr/blog/implementing-a-fuzzy-suggestion-mechanism/ 5 | // https://gist.github.com/justinvw/5025854 6 | -------------------------------------------------------------------------------- /src/resolvers/updateById.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, InputTypeComposer, ResolverResolveParams } from 'graphql-compose'; 2 | import ElasticApiParser from '../ElasticApiParser'; 3 | import { getUpdateByIdOutputTC } from '../types/UpdateByIdOutput'; 4 | import { getTypeName, CommonOpts, desc } from '../utils'; 5 | 6 | export default function createUpdateByIdResolver( 7 | opts: CommonOpts 8 | ): Resolver { 9 | const { fieldMap, sourceTC, schemaComposer } = opts; 10 | 11 | if (!fieldMap || !fieldMap._all) { 12 | throw new Error( 13 | 'opts.fieldMap for Resolver updateById() should be fieldMap of FieldsMapByElasticType type.' 14 | ); 15 | } 16 | 17 | if (!sourceTC || sourceTC.constructor.name !== 'ObjectTypeComposer') { 18 | throw new Error( 19 | 'opts.sourceTC for Resolver updateById() should be instance of ObjectTypeComposer.' 20 | ); 21 | } 22 | 23 | const parser = new ElasticApiParser({ 24 | elasticClient: opts.elasticClient, 25 | prefix: opts.prefix, 26 | }); 27 | 28 | const updateByIdFC = parser.generateFieldConfig('update', { 29 | index: opts.elasticIndex, 30 | type: opts.elasticType, 31 | _source: true, 32 | }); 33 | 34 | const argsConfigMap = { 35 | id: 'String!', 36 | record: getRecordITC(opts).getTypeNonNull(), 37 | }; 38 | 39 | const type = getUpdateByIdOutputTC(opts); 40 | 41 | return schemaComposer.createResolver({ 42 | type, 43 | name: 'updateById', 44 | kind: 'mutation', 45 | args: argsConfigMap, 46 | resolve: async (rp: ResolverResolveParams) => { 47 | const { source, args, context, info } = rp; 48 | 49 | args.body = { 50 | doc: { 51 | ...args.record, 52 | }, 53 | }; 54 | delete args.record; 55 | 56 | const res = await updateByIdFC.resolve(source, args, context, info); 57 | 58 | const { _index, _type, _id, _version, result, get } = res || {}; 59 | const { _source } = get || {}; 60 | 61 | return { 62 | _id, 63 | _index, 64 | _type, 65 | _version, 66 | result, 67 | ..._source, 68 | }; 69 | }, 70 | }); 71 | } 72 | 73 | export function getRecordITC(opts: CommonOpts): InputTypeComposer { 74 | const name = getTypeName('Record', {}); 75 | const description = desc(`The record from Elastic Search`); 76 | return opts.getOrCreateITC(name, () => ({ 77 | name, 78 | description, 79 | fields: { ...opts.fieldMap._all }, 80 | })); 81 | } 82 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'dox'; 2 | -------------------------------------------------------------------------------- /src/types/FindByIdOutput.ts: -------------------------------------------------------------------------------- 1 | import { ObjectTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts } from '../utils'; 3 | 4 | export function getFindByIdOutputTC( 5 | opts: CommonOpts 6 | ): ObjectTypeComposer { 7 | const name = getTypeName('FindByIdOutput', opts); 8 | const { sourceTC } = opts; 9 | return opts.getOrCreateOTC(name, () => ({ 10 | name, 11 | fields: { 12 | _id: 'String', 13 | _index: 'String', 14 | _type: 'String', 15 | _version: 'Int', 16 | ...sourceTC.getFields(), 17 | }, 18 | })); 19 | } 20 | -------------------------------------------------------------------------------- /src/types/SearchHitItem.ts: -------------------------------------------------------------------------------- 1 | import { ObjectTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts, desc } from '../utils'; 3 | 4 | export function getSearchHitItemTC( 5 | opts: CommonOpts 6 | ): ObjectTypeComposer { 7 | const name = getTypeName('SearchHitItem', opts); 8 | 9 | return opts.getOrCreateOTC(name, () => ({ 10 | name, 11 | fields: { 12 | _index: 'String', 13 | _type: 'String', 14 | _id: 'String', 15 | _score: 'Float', 16 | _source: opts.sourceTC || 'JSON', 17 | 18 | // if arg.explain = true 19 | _shard: { 20 | type: 'String', 21 | description: desc(`Use explain API on query`), 22 | }, 23 | _node: { 24 | type: 'String', 25 | description: desc(`Use explain API on query`), 26 | }, 27 | _explanation: { 28 | type: 'JSON', 29 | description: desc(`Use explain API on query`), 30 | }, 31 | 32 | _version: 'Int', 33 | 34 | inner_hits: { 35 | type: 'JSON', 36 | description: 'Returns data only if `args.collapse` is provided', 37 | }, 38 | 39 | highlight: { 40 | type: 'JSON', 41 | description: 'Returns data only if `args.highlight` is provided', 42 | }, 43 | 44 | // return sort values for search_after 45 | sort: 'JSON', 46 | 47 | fields: { 48 | type: 'JSON', 49 | description: 'Returns result from `args.opts.body.script_fields`', 50 | }, 51 | }, 52 | })); 53 | } 54 | -------------------------------------------------------------------------------- /src/types/SearchOutput.ts: -------------------------------------------------------------------------------- 1 | import { ObjectTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts } from '../utils'; 3 | import getShardsTC from './Shards'; 4 | import { getSearchHitItemTC } from './SearchHitItem'; 5 | 6 | export function getSearchOutputTC( 7 | opts: CommonOpts 8 | ): ObjectTypeComposer { 9 | const name = getTypeName('SearchOutput', opts); 10 | const nameHits = getTypeName('SearchHits', opts); 11 | 12 | return opts.getOrCreateOTC(name, () => ({ 13 | name, 14 | fields: { 15 | took: 'Int', 16 | timed_out: 'Boolean', 17 | _shards: getShardsTC(opts), 18 | hits: opts.getOrCreateOTC(nameHits, () => ({ 19 | name: nameHits, 20 | fields: { 21 | total: 'Int', 22 | max_score: 'Float', 23 | hits: [getSearchHitItemTC(opts)], 24 | }, 25 | })), 26 | aggregations: 'JSON', 27 | }, 28 | })); 29 | } 30 | -------------------------------------------------------------------------------- /src/types/Shards.ts: -------------------------------------------------------------------------------- 1 | import { ObjectTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts } from '../utils'; 3 | 4 | export default function getShardsTC( 5 | opts: CommonOpts 6 | ): ObjectTypeComposer { 7 | const name = getTypeName('MetaShards', opts); 8 | 9 | return opts.getOrCreateOTC(name, () => ({ 10 | name, 11 | fields: { 12 | total: 'Int', 13 | successful: 'Int', 14 | failed: 'Int', 15 | }, 16 | })); 17 | } 18 | -------------------------------------------------------------------------------- /src/types/UpdateByIdOutput.ts: -------------------------------------------------------------------------------- 1 | import { ObjectTypeComposer } from 'graphql-compose'; 2 | import { getTypeName, CommonOpts } from '../utils'; 3 | 4 | export function getUpdateByIdOutputTC( 5 | opts: CommonOpts 6 | ): ObjectTypeComposer { 7 | const name = getTypeName('UpdateByIdOutput', opts); 8 | const { sourceTC } = opts; 9 | return opts.getOrCreateOTC(name, () => ({ 10 | name, 11 | fields: { 12 | _id: 'String', 13 | _index: 'String', 14 | _type: 'String', 15 | _version: 'Int', 16 | result: 'String', 17 | ...sourceTC.getFields(), 18 | }, 19 | })); 20 | } 21 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | SchemaComposer, 3 | ObjectTypeComposer, 4 | InputTypeComposer, 5 | EnumTypeComposer, 6 | InputTypeComposerAsObjectDefinition, 7 | EnumTypeComposerAsObjectDefinition, 8 | ObjectTypeComposerAsObjectDefinition, 9 | } from 'graphql-compose'; 10 | import { isFunction } from 'graphql-compose'; 11 | import type { ElasticMappingT, FieldsMapByElasticType } from './mappingConverter'; 12 | 13 | export type CommonOpts = { 14 | prefix?: string; 15 | postfix?: string; 16 | pluralFields?: string[]; 17 | elasticIndex: string; 18 | elasticType: string; 19 | elasticClient: any; 20 | fieldMap: FieldsMapByElasticType; 21 | sourceTC: ObjectTypeComposer; 22 | schemaComposer: SchemaComposer; 23 | getOrCreateOTC: ( 24 | name: string, 25 | onCreate: () => ObjectTypeComposerAsObjectDefinition 26 | ) => ObjectTypeComposer; 27 | getOrCreateITC: ( 28 | name: string, 29 | onCreate: () => InputTypeComposerAsObjectDefinition 30 | ) => InputTypeComposer; 31 | getOrCreateETC: ( 32 | name: string, 33 | onCreate: () => EnumTypeComposerAsObjectDefinition 34 | ) => EnumTypeComposer; 35 | }; 36 | 37 | export function prepareCommonOpts( 38 | schemaComposer: SchemaComposer, 39 | opts: any = {} 40 | ): CommonOpts { 41 | return { 42 | schemaComposer, 43 | getOrCreateOTC: (typeName, cfgOrThunk) => { 44 | return schemaComposer.getOrSet(typeName, () => { 45 | const tc = schemaComposer.createObjectTC( 46 | isFunction(cfgOrThunk) ? (cfgOrThunk as any)() : cfgOrThunk 47 | ); 48 | return tc; 49 | }); 50 | }, 51 | getOrCreateITC: (typeName, cfgOrThunk) => { 52 | return schemaComposer.getOrSet(typeName, () => { 53 | const tc = schemaComposer.createInputTC( 54 | isFunction(cfgOrThunk) ? (cfgOrThunk as any)() : cfgOrThunk 55 | ); 56 | return tc; 57 | }); 58 | }, 59 | getOrCreateETC: (typeName, cfgOrThunk) => { 60 | return schemaComposer.getOrSet(typeName, () => { 61 | const tc = schemaComposer.createEnumTC( 62 | isFunction(cfgOrThunk) ? (cfgOrThunk as any)() : cfgOrThunk 63 | ); 64 | return tc; 65 | }); 66 | }, 67 | ...opts, 68 | }; 69 | } 70 | 71 | export function getTypeName(name: string, opts: any): string { 72 | return `${(opts && opts.prefix) || 'Elastic'}${name}${(opts && opts.postfix) || ''}`; 73 | } 74 | 75 | // Remove newline multiline in descriptions 76 | export function desc(str: string): string { 77 | return str.replace(/\n\s+/gi, ' ').replace(/^\s+/, ''); 78 | } 79 | 80 | export function reorderKeys>(obj: T, names: string[]): T { 81 | const orderedFields = {} as Record; 82 | const fields = { ...obj }; 83 | names.forEach((name) => { 84 | if (fields[name]) { 85 | orderedFields[name] = fields[name]; 86 | delete fields[name]; 87 | } 88 | }); 89 | return { ...orderedFields, ...fields }; 90 | } 91 | 92 | export type fetchElasticMappingOptsT = { 93 | elasticIndex: string; 94 | elasticType: string; 95 | elasticMapping: ElasticMappingT; 96 | elasticClient: any; 97 | }; 98 | 99 | export async function fetchElasticMapping( 100 | opts: fetchElasticMappingOptsT 101 | ): Promise { 102 | if (!opts.elasticIndex || typeof opts.elasticIndex !== 'string') { 103 | throw new Error('Must provide `elasticIndex` string parameter from your Elastic server.'); 104 | } 105 | 106 | if (!opts.elasticType || typeof opts.elasticType !== 'string') { 107 | throw new Error('Must provide `elasticType` string parameter from your Elastic server.'); 108 | } 109 | 110 | if (!opts.elasticClient) { 111 | throw new Error( 112 | 'Must provide `elasticClient` Object parameter connected to your Elastic server.' 113 | ); 114 | } 115 | 116 | const elasticMapping = await opts.elasticClient.indices.getMapping({ 117 | index: opts.elasticIndex, 118 | type: opts.elasticType, 119 | }); 120 | 121 | // Try index name, else resort to Value in case Index name is an alias 122 | const baseIndex = elasticMapping[opts.elasticIndex] || Object.values(elasticMapping)[0]; 123 | 124 | return baseIndex.mappings[opts.elasticType]; 125 | } 126 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["node"], 5 | "outDir": "./lib", 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["**/__tests__", "**/__mocks__"] 9 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "resolveJsonModule": true, 8 | "sourceMap": true, 9 | "declaration": true, 10 | "declarationMap": true, 11 | "removeComments": true, 12 | "strict": true, 13 | "noImplicitAny": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "noUnusedParameters": true, 17 | "noUnusedLocals": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "lib": ["es2017", "esnext.asynciterable"], 20 | "types": ["node", "jest"], 21 | "baseUrl": ".", 22 | "rootDir": "./src", 23 | }, 24 | "include": ["src/**/*", "examples/**/*"], 25 | "exclude": [ 26 | "./node_modules" 27 | ] 28 | } 29 | --------------------------------------------------------------------------------