├── .nvmrc ├── .npmrc ├── NOTICE.txt ├── logo-app-search.png ├── prettier.config.js ├── .gitignore ├── .eslintrc.json ├── .babelrc ├── src ├── query_cache.js ├── result_list.js ├── elastic_app_search.js ├── result_item.js ├── request.js ├── filters.js └── client.js ├── fixtures ├── invalid.api.swiftype.com-443 │ ├── search_404 │ └── click_404 ├── host-2376rb.api.swiftype.com-443 │ ├── search_missing_query │ ├── click_no_options │ ├── click_no_tags │ ├── click_simple │ ├── query_suggestion_bad_options │ ├── multi_search_error │ ├── query_suggestion_with_options │ ├── query_suggestion │ ├── disjunctive_license_with_override_tags │ ├── disjunctive_licence_override_tags │ ├── search_filter_and_facet │ ├── search_with_license_facet │ ├── disjunctive_license │ ├── disjunctive_deps_also_license │ ├── search_filter_and_multi_facet │ ├── search_multi_facet │ ├── search_filter_and_multi_facet_with_tags │ ├── disjunctive_license_also_deps │ ├── disjunctive_deps_also_license_no_array_syntax │ ├── search_multi_filter_multi_facet │ ├── search_multi_filter_multi_facet_no_array_syntax │ ├── additional_headers │ ├── search_simple │ ├── search_grouped │ └── multi_search └── localhost.swiftype.com-3002 │ └── localhost_search ├── .vscode └── launch.json ├── .circleci └── config.yml ├── tests ├── elastic_app_search.spec.js ├── result_item.spec.js ├── request.node.spec.js ├── request.spec.js ├── filters.spec.js └── client.spec.js ├── rollup.config.js ├── package.json ├── LICENSE.txt └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Elastic App Search JavaScript client. 2 | Copyright 2012-2019 Elasticsearch B.V. 3 | -------------------------------------------------------------------------------- /logo-app-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elastic/app-search-javascript/HEAD/logo-app-search.png -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "none", 3 | arrowParens: "avoid" 4 | }; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist 3 | node_modules 4 | npm-debug.log 5 | yarn-error.log 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:prettier/recommended"], 3 | "env": { 4 | "es6": true 5 | }, 6 | "parserOptions": { 7 | "sourceType": "module", 8 | "ecmaVersion": "2018" 9 | }, 10 | "rules": { 11 | "no-debugger": 2 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "dev": { 4 | "presets": [ 5 | [ 6 | "env", 7 | { 8 | "modules": false 9 | } 10 | ] 11 | ], 12 | "plugins": ["external-helpers", "transform-object-rest-spread"] 13 | }, 14 | "test": { 15 | "presets": ["env"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/query_cache.js: -------------------------------------------------------------------------------- 1 | export default class QueryCache { 2 | constructor() { 3 | this.cache = {}; 4 | } 5 | 6 | getKey(method, url, params) { 7 | return method + url + JSON.stringify(params); 8 | } 9 | 10 | store(key, response) { 11 | this.cache[key] = response; 12 | } 13 | 14 | retrieve(key) { 15 | return this.cache[key]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/result_list.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import ResultItem from "./result_item"; 4 | 5 | /** 6 | * A list of ResultItems and additional information returned by a search request 7 | */ 8 | export default class ResultList { 9 | constructor(rawResults, rawInfo) { 10 | this.rawResults = rawResults; 11 | this.rawInfo = rawInfo; 12 | 13 | const results = new Array(); 14 | rawResults.forEach(data => { 15 | results.push(new ResultItem(data)); 16 | }); 17 | 18 | this.results = results; 19 | this.info = rawInfo; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/elastic_app_search.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import Client from "./client"; 4 | 5 | export function createClient({ 6 | hostIdentifier, 7 | accountHostKey, 8 | apiKey, 9 | searchKey, 10 | engineName, 11 | endpointBase, 12 | cacheResponses, 13 | additionalHeaders 14 | }) { 15 | hostIdentifier = hostIdentifier || accountHostKey; // accountHostKey is deprecated 16 | searchKey = searchKey || apiKey; //apiKey is deprecated 17 | return new Client(hostIdentifier, searchKey, engineName, { 18 | endpointBase, 19 | cacheResponses, 20 | additionalHeaders 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /fixtures/invalid.api.swiftype.com-443/search_404: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/invalid/search.json 2 | authorization: Bearer invalid 3 | content-type: application/json 4 | accept: */* 5 | accept-encoding: gzip,deflate 6 | body: {} 7 | 8 | HTTP/1.1 404 Not Found 9 | date: Tue, 15 May 2018 17:14:52 GMT 10 | content-type: application/json 11 | transfer-encoding: chunked 12 | connection: close 13 | vary: Accept-Encoding, Origin 14 | status: 404 Not Found 15 | x-frame-options: SAMEORIGIN 16 | x-xss-protection: 1; mode=block 17 | x-content-type-options: nosniff 18 | cache-control: no-cache 19 | x-request-id: 6111bf0d61221b0c745d416fe1600d45 20 | x-runtime: 0.019072 21 | x-swiftype-datacenter: dal05 22 | x-swiftype-frontend-node: web02.dal05 23 | x-swiftype-edge-node: web02.dal05 24 | 25 | -------------------------------------------------------------------------------- /fixtures/invalid.api.swiftype.com-443/click_404: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/invalid/click.json 2 | authorization: Bearer invalid 3 | content-type: application/json 4 | accept: */* 5 | accept-encoding: gzip,deflate 6 | body: {\"tags\":[]} 7 | 8 | HTTP/1.1 404 Not Found 9 | date: Tue, 15 May 2018 17:14:54 GMT 10 | content-type: application/json 11 | transfer-encoding: chunked 12 | connection: close 13 | vary: Accept-Encoding, Origin 14 | status: 404 Not Found 15 | x-frame-options: SAMEORIGIN 16 | x-xss-protection: 1; mode=block 17 | x-content-type-options: nosniff 18 | cache-control: no-cache 19 | x-request-id: c97b1fbcb8b18bbf32235b9b0d09917c 20 | x-runtime: 0.013818 21 | x-swiftype-datacenter: dal05 22 | x-swiftype-frontend-node: web02.dal05 23 | x-swiftype-edge-node: web02.dal05 24 | 25 | -------------------------------------------------------------------------------- /.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 tests", 9 | "type": "node", 10 | "request": "launch", 11 | "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", 12 | "stopOnEntry": false, 13 | "args": ["--runInBand", "--forceExit", "--watch", "--verbose"], 14 | "cwd": "${workspaceRoot}", 15 | "preLaunchTask": null, 16 | "runtimeExecutable": null, 17 | "env": { 18 | "NODE_ENV": "test" 19 | }, 20 | "console": "integratedTerminal", 21 | "sourceMaps": true 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/search_missing_query: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/search.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {} 9 | 10 | HTTP/1.1 400 Bad Request 11 | date: Mon, 09 Jul 2018 14:14:39 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | status: 400 Bad Request 16 | x-frame-options: SAMEORIGIN 17 | x-xss-protection: 1; mode=block 18 | x-content-type-options: nosniff 19 | vary: Origin 20 | cache-control: no-cache 21 | x-request-id: d3a6f3b095dd09449afe25d3e30f7dff 22 | x-runtime: 0.063009 23 | x-swiftype-datacenter: dal05 24 | x-swiftype-frontend-node: web02.dal05 25 | x-swiftype-edge-node: web02.dal05 26 | 27 | {"errors":["Missing required parameter: query"]} 28 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/click_no_options: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/click.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"tags\":[]} 9 | 10 | HTTP/1.1 400 Bad Request 11 | date: Mon, 09 Jul 2018 14:14:40 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | status: 400 Bad Request 16 | x-frame-options: SAMEORIGIN 17 | x-xss-protection: 1; mode=block 18 | x-content-type-options: nosniff 19 | vary: Origin 20 | cache-control: no-cache 21 | x-request-id: 3fc40b48a8b58c21a254013511ce0e6b 22 | x-runtime: 0.079922 23 | x-swiftype-datacenter: dal05 24 | x-swiftype-frontend-node: web02.dal05 25 | x-swiftype-edge-node: web02.dal05 26 | 27 | {"errors":["Missing required parameter: query"]} 28 | -------------------------------------------------------------------------------- /src/result_item.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * An individual search result 5 | */ 6 | export default class ResultItem { 7 | constructor(data) { 8 | if (data._group && data._group.length > 0) { 9 | data = { 10 | ...data, 11 | _group: data._group.map(nestedData => new ResultItem(nestedData)) 12 | }; 13 | } 14 | this.data = data; 15 | } 16 | 17 | /** 18 | * Return the HTML-unsafe raw value for a field, if it exists 19 | * 20 | * @param {String} key - name of the field 21 | * 22 | * @returns {any} the raw value of the field 23 | */ 24 | getRaw(key) { 25 | return (this.data[key] || {}).raw; 26 | } 27 | 28 | /** 29 | * Return the HTML-safe snippet value for a field, if it exists 30 | * 31 | * @param {String} key - name of the field 32 | * 33 | * @returns {any} the snippet value of the field 34 | */ 35 | getSnippet(key) { 36 | return (this.data[key] || {}).snippet; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/click_no_tags: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/click.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"query\":\"Cat\",\"document_id\":\"rex-cli\",\"request_id\":\"8b55561954484f13d872728f849ffd22\",\"tags\":[]} 9 | 10 | HTTP/1.1 200 OK 11 | date: Mon, 09 Jul 2018 14:14:39 GMT 12 | content-type: application/json 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | cache-control: no-cache 21 | x-request-id: f78d4ec152dd57cddd4379a19e735941 22 | x-runtime: 0.058022 23 | x-swiftype-datacenter: dal05 24 | x-swiftype-frontend-node: web02.dal05 25 | x-swiftype-edge-node: web02.dal05 26 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/click_simple: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/click.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"query\":\"Cat\",\"document_id\":\"rex-cli\",\"request_id\":\"8b55561954484f13d872728f849ffd22\",\"tags\":[\"Cat\"]} 9 | 10 | HTTP/1.1 200 OK 11 | date: Mon, 09 Jul 2018 14:14:39 GMT 12 | content-type: application/json 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | cache-control: no-cache 21 | x-request-id: 46546cfbba463a9064c820b60caf3f1d 22 | x-runtime: 0.067884 23 | x-swiftype-datacenter: dal05 24 | x-swiftype-frontend-node: web02.dal05 25 | x-swiftype-edge-node: web02.dal05 26 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/query_suggestion_bad_options: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/query_suggestion 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"query\":\"cat\",\"bad\":\"option\"} 9 | 10 | HTTP/1.1 400 Bad Request 11 | date: Tue, 29 Jan 2019 18:11:16 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | status: 400 Bad Request 16 | x-frame-options: SAMEORIGIN 17 | x-xss-protection: 1; mode=block 18 | x-content-type-options: nosniff 19 | x-ratelimit-limit: 2400 20 | x-ratelimit-remaining: 2399 21 | vary: Origin 22 | cache-control: no-cache 23 | x-request-id: 3dc07facebb356d29cfb26e9e3d50ec0 24 | x-runtime: 0.127407 25 | x-swiftype-frontend-datacenter: dal05 26 | x-swiftype-frontend-node: web01.dal05 27 | x-swiftype-edge-datacenter: dal05 28 | x-swiftype-edge-node: web01.dal05 29 | 30 | {"errors":["Options contains invalid key: bad"]} 31 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:8 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: npm install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run tests! 37 | - run: npm run test 38 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/multi_search_error: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/multi_search.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"queries\":[{\"query\":\"cat\",\"invalid\":\"parameter\"},{\"query\":\"dog\",\"another\":\"parameter\"}]} 9 | 10 | HTTP/1.1 400 Bad Request 11 | date: Mon, 22 Oct 2018 16:53:33 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | status: 400 Bad Request 16 | x-frame-options: SAMEORIGIN 17 | x-xss-protection: 1; mode=block 18 | x-content-type-options: nosniff 19 | vary: Origin 20 | cache-control: no-cache 21 | x-request-id: 53292070947f02020641942bdc17d55f 22 | x-runtime: 0.083876 23 | x-swiftype-frontend-datacenter: dal05 24 | x-swiftype-frontend-node: web01.dal05 25 | x-swiftype-edge-datacenter: dal05 26 | x-swiftype-edge-node: web01.dal05 27 | 28 | [{"errors":["Options contains invalid key: invalid"]},{"errors":["Options contains invalid key: another"]}] 29 | -------------------------------------------------------------------------------- /tests/elastic_app_search.spec.js: -------------------------------------------------------------------------------- 1 | import { createClient } from "../src/elastic_app_search"; 2 | import Client from "../src/client"; 3 | 4 | const hostIdentifier = "host-2376rb"; 5 | const searchKey = "api-hean6g8dmxnm2shqqiag757a"; 6 | const engineName = "node-modules"; 7 | 8 | describe("ElasticAppSearch#createClient", () => { 9 | test("instantiates a new client", () => { 10 | var client = createClient({ 11 | hostIdentifier, 12 | searchKey, 13 | engineName 14 | }); 15 | 16 | expect(client).toBeInstanceOf(Client); 17 | }); 18 | 19 | test("instantiates a new client with deprecates accountHostKey parameter", () => { 20 | var client = createClient({ 21 | accountHostKey: hostIdentifier, 22 | searchKey, 23 | engineName 24 | }); 25 | 26 | expect(client).toBeInstanceOf(Client); 27 | }); 28 | 29 | test("instantiates a new client with options", () => { 30 | var client = createClient({ 31 | hostIdentifier, 32 | searchKey, 33 | engineName, 34 | endpointBase: "http://localhost:3002", 35 | cacheResponses: true 36 | }); 37 | 38 | expect(client).toBeInstanceOf(Client); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from "rollup-plugin-babel"; 2 | import { uglify } from "rollup-plugin-uglify"; 3 | import json from "rollup-plugin-json"; 4 | 5 | import pkg from "./package.json"; 6 | 7 | export default [ 8 | { 9 | input: "src/elastic_app_search.js", 10 | output: [ 11 | { 12 | // browser-friendly UMD build, for Browsers or Node 13 | name: "ElasticAppSearch", 14 | file: "dist/elastic_app_search.umd.js", 15 | format: "umd" 16 | }, 17 | { 18 | // ES6 module build, for things like Rollup 19 | file: pkg.module, 20 | format: "es" 21 | } 22 | ], 23 | plugins: [ 24 | json(), // So we can import thing like `package.json` as a module 25 | babel({ 26 | exclude: "node_modules/**" // only transpile our source code 27 | }) 28 | ] 29 | }, 30 | { 31 | input: "src/elastic_app_search.js", 32 | output: [ 33 | { 34 | // Minified UMD build 35 | name: "ElasticAppSearch", 36 | file: "dist/elastic_app_search.umd.min.js", 37 | format: "umd" 38 | } 39 | ], 40 | plugins: [ 41 | json(), 42 | babel({ 43 | exclude: "node_modules/**" 44 | }), 45 | uglify() 46 | ] 47 | } 48 | ]; 49 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/query_suggestion_with_options: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/query_suggestion 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"query\":\"cat\",\"size\":3,\"types\":{\"documents\":{\"fields\":[\"name\"]}}} 9 | 10 | HTTP/1.1 200 OK 11 | date: Tue, 29 Jan 2019 18:14:07 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | x-ratelimit-limit: 2400 21 | x-ratelimit-remaining: 2399 22 | etag: W/"b0c17abeee72f6e440978d1c979df21a" 23 | cache-control: max-age=0, private, must-revalidate 24 | x-request-id: 28aa2fbd5ee7e2cbcbb1535c9e85ecfb 25 | x-runtime: 0.314690 26 | x-swiftype-frontend-datacenter: dal05 27 | x-swiftype-frontend-node: web01.dal05 28 | x-swiftype-edge-datacenter: dal05 29 | x-swiftype-edge-node: web01.dal05 30 | 31 | {"results":{"documents":[{"suggestion":"catwalk"},{"suggestion":"cats"},{"suggestion":"cat4d"}]},"meta":{"request_id":"28aa2fbd5ee7e2cbcbb1535c9e85ecfb"}} 32 | -------------------------------------------------------------------------------- /tests/result_item.spec.js: -------------------------------------------------------------------------------- 1 | import ResultItem from "../src/result_item"; 2 | 3 | describe("ResultItem", () => { 4 | test("can be instantiated", () => { 5 | const resultItem = new ResultItem({}); 6 | expect(resultItem).toBeInstanceOf(ResultItem); 7 | }); 8 | 9 | describe("#getRaw", () => { 10 | test("returns a raw value for the specified field", () => { 11 | const resultItem = new ResultItem({ 12 | field: { 13 | raw: "value" 14 | } 15 | }); 16 | expect(resultItem.getRaw("field")).toEqual("value"); 17 | }); 18 | 19 | test("returns undefined if the field isn't contained within the response", () => { 20 | const resultItem = new ResultItem({}); 21 | expect(resultItem.getRaw("field")).toBeUndefined; 22 | }); 23 | }); 24 | 25 | describe("#getSnippet", () => { 26 | test("returns a raw value for the specified field", () => { 27 | const resultItem = new ResultItem({ 28 | field: { 29 | snippet: "value" 30 | } 31 | }); 32 | expect(resultItem.getSnippet("field")).toEqual("value"); 33 | }); 34 | 35 | test("returns undefined if the field isn't contained within the response", () => { 36 | const resultItem = new ResultItem({}); 37 | expect(resultItem.getSnippet("field")).toBeUndefined; 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/query_suggestion: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/query_suggestion 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"query\":\"cat\"} 9 | 10 | HTTP/1.1 200 OK 11 | date: Tue, 29 Jan 2019 18:08:51 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | x-ratelimit-limit: 2400 21 | x-ratelimit-remaining: 2399 22 | etag: W/"7bbbb718dc1cca4a9911412b186b6b3e" 23 | cache-control: max-age=0, private, must-revalidate 24 | x-request-id: 0750887ae5330b6c0234b6098f4459e8 25 | x-runtime: 0.353357 26 | x-swiftype-frontend-datacenter: dal05 27 | x-swiftype-frontend-node: web01.dal05 28 | x-swiftype-edge-datacenter: dal05 29 | x-swiftype-edge-node: web01.dal05 30 | 31 | {"results":{"documents":[{"suggestion":"catwalk"},{"suggestion":"catcher"},{"suggestion":"https://github.com/stuartpb/catcher"},{"suggestion":"categories"},{"suggestion":"categories for"},{"suggestion":"categories for javascript"},{"suggestion":"http://lcfrs.org/cats"},{"suggestion":"cats"},{"suggestion":"https://github.com/luciferous/cats"},{"suggestion":"cat4d"}]},"meta":{"request_id":"0750887ae5330b6c0234b6098f4459e8"}} 32 | -------------------------------------------------------------------------------- /tests/request.node.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | import { request } from "../src/request"; 6 | import { Headers } from "node-fetch"; 7 | import { version } from "../package.json"; 8 | const nodeVersion = process.version; 9 | 10 | describe("request - within node context", () => { 11 | const responseJson = {}; 12 | const response = { 13 | json: () => Promise.resolve(responseJson) 14 | }; 15 | 16 | const searchKey = "api-12345"; 17 | const endpoint = "http://www.example.com"; 18 | const path = "/v1/search"; 19 | const params = { 20 | a: "a" 21 | }; 22 | 23 | beforeEach(() => { 24 | global.Headers = Headers; 25 | jest.resetAllMocks(); 26 | global.fetch = jest 27 | .fn() 28 | .mockImplementation(() => Promise.resolve(response)); 29 | }); 30 | 31 | it("will have the correct node based meta headers when running in node context", async () => { 32 | expect(global.window).not.toBeDefined(); 33 | const res = await request(searchKey, endpoint, path, params, false); 34 | expect(res.response).toBe(response); 35 | expect(global.fetch.mock.calls.length).toBe(1); 36 | var [_, options] = global.fetch.mock.calls[0]; 37 | expect(options.headers.get("x-elastic-client-meta")).toEqual( 38 | `ent=${version}-legacy,js=${nodeVersion},t=${version}-legacy,ft=universal` 39 | ); 40 | const validHeaderRegex = /^[a-z]{1,}=[a-z0-9\.\-]{1,}(?:,[a-z]{1,}=[a-z0-9\.\-]+)*$/; 41 | expect(options.headers.get("x-elastic-client-meta")).toMatch( 42 | validHeaderRegex 43 | ); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/disjunctive_license_with_override_tags: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/search.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | x-elastic-client-meta: ent=8.13.0-legacy,js=browser,t=8.13.0-legacy,ft=universal 7 | accept: */* 8 | accept-encoding: gzip,deflate 9 | body: {\"query\":\"cat\",\"page\":{\"size\":0},\"filters\":{},\"facets\":{\"license\":[{\"type\":\"value\",\"size\":3}]},\"analytics\":{\"tags\":[\"Facet-Only\"]},\"record_analytics\":false} 10 | 11 | HTTP/1.1 200 OK 12 | date: Fri, 06 May 2022 15:25:51 GMT 13 | content-type: application/json; charset=utf-8 14 | transfer-encoding: chunked 15 | connection: close 16 | vary: Accept-Encoding, Origin 17 | status: 200 OK 18 | x-frame-options: SAMEORIGIN 19 | x-xss-protection: 1; mode=block 20 | x-content-type-options: nosniff 21 | x-swiftype-backend-region: dal 22 | x-swiftype-backend-datacenter: dal10 23 | x-swiftype-backend-node: app-api03a.dal10 24 | x-ratelimit-limit: 12000 25 | x-ratelimit-remaining: 11998 26 | etag: W/"680394011bb0efcb7a53ebd964a1127f" 27 | cache-control: max-age=0, private, must-revalidate 28 | x-request-id: 73ed894a358f45bc79fb829d2427d9d9 29 | x-runtime: 0.086157 30 | x-swiftype-frontend-datacenter: dal10 31 | x-swiftype-frontend-node: web02b.dal10 32 | x-swiftype-edge-datacenter: dal10 33 | x-swiftype-edge-node: web02b.dal10 34 | 35 | {"meta":{"alerts":[],"warnings":[],"page":{"current":1,"total_pages":1,"total_results":642,"size":0},"request_id":"73ed894a358f45bc79fb829d2427d9d9"},"results":[],"facets":{"license":[{"type":"value","data":[{"value":"MIT","count":101},{"value":"BSD","count":33},{"value":"MIT/X11","count":3}]}]}} 36 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/disjunctive_licence_override_tags: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/search.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | x-elastic-client-meta: ent=8.13.0-legacy,js=browser,t=8.13.0-legacy,ft=universal 7 | accept: */* 8 | accept-encoding: gzip,deflate 9 | body: {\"query\":\"cat\",\"page\":{\"size\":0},\"filters\":{},\"facets\":{\"license\":[{\"type\":\"value\",\"size\":3}]},\"analytics\":{\"tags\":[\"FromSERP\",\"Disjunctive\"]},\"record_analytics\":false} 10 | 11 | HTTP/1.1 200 OK 12 | date: Fri, 06 May 2022 19:35:53 GMT 13 | content-type: application/json; charset=utf-8 14 | transfer-encoding: chunked 15 | connection: close 16 | vary: Accept-Encoding, Origin 17 | status: 200 OK 18 | x-frame-options: SAMEORIGIN 19 | x-xss-protection: 1; mode=block 20 | x-content-type-options: nosniff 21 | x-swiftype-backend-region: dal 22 | x-swiftype-backend-datacenter: dal10 23 | x-swiftype-backend-node: app-api03a.dal10 24 | x-ratelimit-limit: 12000 25 | x-ratelimit-remaining: 11999 26 | etag: W/"10be0071db0ead6986f2ddf45b896207" 27 | cache-control: max-age=0, private, must-revalidate 28 | x-request-id: 78e10ad9edfafd32ff71faddaaaa6292 29 | x-runtime: 0.244323 30 | x-swiftype-frontend-datacenter: dal10 31 | x-swiftype-frontend-node: web02b.dal10 32 | x-swiftype-edge-datacenter: dal10 33 | x-swiftype-edge-node: web02b.dal10 34 | 35 | {"meta":{"alerts":[],"warnings":[],"page":{"current":1,"total_pages":1,"total_results":642,"size":0},"request_id":"78e10ad9edfafd32ff71faddaaaa6292"},"results":[],"facets":{"license":[{"type":"value","data":[{"value":"MIT","count":101},{"value":"BSD","count":33},{"value":"MIT/X11","count":3}]}]}} 36 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/search_filter_and_facet: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/search.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"query\":\"cat\",\"page\":{\"size\":1},\"filters\":{\"license\":[\"BSD\"]},\"facets\":{\"license\":[{\"type\":\"value\",\"size\":3}]}} 9 | 10 | HTTP/1.1 200 OK 11 | date: Thu, 27 Sep 2018 20:25:24 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | etag: W/"bc55d148b47669f6cf4081f8c4893888" 21 | cache-control: max-age=0, private, must-revalidate 22 | x-request-id: de13ba490e5bbc0be2a64f21d39d22c2 23 | x-runtime: 0.304407 24 | x-swiftype-frontend-datacenter: dal05 25 | x-swiftype-frontend-node: web02.dal05 26 | x-swiftype-edge-datacenter: dal05 27 | x-swiftype-edge-node: web02.dal05 28 | 29 | {"meta":{"warnings":[],"page":{"current":1,"total_pages":33,"total_results":33,"size":1},"request_id":"de13ba490e5bbc0be2a64f21d39d22c2"},"results":[{"license":{"raw":["BSD"]},"name":{"raw":"source-map-cat"},"repository":{"raw":null},"created":{"raw":"2013-01-23T04:51:39.995Z"},"dependencies":{"raw":["coffee-script","argparse","source-map"]},"keywords":{"raw":null},"description":{"raw":"WIP cat for JS source maps."},"modified":{"raw":"2013-01-23T04:51:42.782Z"},"id":{"raw":"source-map-cat"},"version":{"raw":"0.0.0"},"owners":{"raw":["gregg@aweber.com"]},"_meta":{"score":18.773478}}],"facets":{"license":[{"type":"value","data":[{"value":"BSD","count":33}]}]}} 30 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/search_with_license_facet: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/search.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"query\":\"cat\",\"page\":{\"size\":1},\"filters\":{},\"facets\":{\"license\":[{\"type\":\"value\",\"size\":3}]}} 9 | 10 | HTTP/1.1 200 OK 11 | date: Thu, 27 Sep 2018 20:36:08 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | etag: W/"d31aa222a68122cb8392577cf4a1a7f1" 21 | cache-control: max-age=0, private, must-revalidate 22 | x-request-id: 91197d45d104a3e2281a60eecec630ff 23 | x-runtime: 0.270027 24 | x-swiftype-frontend-datacenter: dal05 25 | x-swiftype-frontend-node: web01.dal05 26 | x-swiftype-edge-datacenter: dal05 27 | x-swiftype-edge-node: web01.dal05 28 | 29 | {"meta":{"warnings":[],"page":{"current":1,"total_pages":642,"total_results":642,"size":1},"request_id":"91197d45d104a3e2281a60eecec630ff"},"results":[{"license":{"raw":["BSD"]},"name":{"raw":"source-map-cat"},"repository":{"raw":null},"created":{"raw":"2013-01-23T04:51:39.995Z"},"dependencies":{"raw":["coffee-script","argparse","source-map"]},"keywords":{"raw":null},"description":{"raw":"WIP cat for JS source maps."},"modified":{"raw":"2013-01-23T04:51:42.782Z"},"id":{"raw":"source-map-cat"},"version":{"raw":"0.0.0"},"owners":{"raw":["gregg@aweber.com"]},"_meta":{"score":19.12111}}],"facets":{"license":[{"type":"value","data":[{"value":"MIT","count":101},{"value":"BSD","count":33},{"value":"MIT/X11","count":3}]}]}} 30 | -------------------------------------------------------------------------------- /src/request.js: -------------------------------------------------------------------------------- 1 | import { version } from "../package.json"; 2 | import QueryCache from "./query_cache"; 3 | const cache = new QueryCache(); 4 | 5 | export function request( 6 | searchKey, 7 | apiEndpoint, 8 | path, 9 | params, 10 | cacheResponses, 11 | { additionalHeaders } = {} 12 | ) { 13 | const method = "POST"; 14 | const key = cache.getKey(method, apiEndpoint + path, params); 15 | if (cacheResponses) { 16 | const cachedResult = cache.retrieve(key); 17 | if (cachedResult) { 18 | return Promise.resolve(cachedResult); 19 | } 20 | } 21 | 22 | return _request(method, searchKey, apiEndpoint, path, params, { 23 | additionalHeaders 24 | }).then(response => { 25 | return response 26 | .json() 27 | .then(json => { 28 | const result = { response: response, json: json }; 29 | if (cacheResponses) cache.store(key, result); 30 | return result; 31 | }) 32 | .catch(() => { 33 | return { response: response, json: {} }; 34 | }); 35 | }); 36 | } 37 | 38 | function _request( 39 | method, 40 | searchKey, 41 | apiEndpoint, 42 | path, 43 | params, 44 | { additionalHeaders } = {} 45 | ) { 46 | const jsVersion = typeof window !== "undefined" ? "browser" : process.version; 47 | const metaHeader = `ent=${version}-legacy,js=${jsVersion},t=${version}-legacy,ft=universal`; 48 | const headers = new Headers({ 49 | ...(searchKey && { Authorization: `Bearer ${searchKey}` }), 50 | "Content-Type": "application/json", 51 | "X-Swiftype-Client": "elastic-app-search-javascript", 52 | "X-Swiftype-Client-Version": version, 53 | "x-elastic-client-meta": metaHeader, 54 | ...additionalHeaders 55 | }); 56 | 57 | return fetch(`${apiEndpoint}${path}`, { 58 | method, 59 | headers, 60 | body: JSON.stringify(params), 61 | credentials: "include" 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/disjunctive_license: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/search.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"query\":\"cat\",\"page\":{\"size\":0},\"filters\":{},\"facets\":{\"license\":[{\"type\":\"value\",\"size\":3}]},\"record_analytics\":false,\"analytics\":{\"tags\":[\"Facet-Only\"]}} 9 | 10 | HTTP/1.1 200 OK 11 | date: Thu, 27 Sep 2018 20:36:08 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | etag: W/"d31aa222a68122cb8392577cf4a1a7f1" 21 | cache-control: max-age=0, private, must-revalidate 22 | x-request-id: 91197d45d104a3e2281a60eecec630ff 23 | x-runtime: 0.270027 24 | x-swiftype-frontend-datacenter: dal05 25 | x-swiftype-frontend-node: web01.dal05 26 | x-swiftype-edge-datacenter: dal05 27 | x-swiftype-edge-node: web01.dal05 28 | 29 | {"meta":{"warnings":[],"page":{"current":1,"total_pages":642,"total_results":642,"size":1},"request_id":"91197d45d104a3e2281a60eecec630ff"},"results":[{"license":{"raw":["BSD"]},"name":{"raw":"source-map-cat"},"repository":{"raw":null},"created":{"raw":"2013-01-23T04:51:39.995Z"},"dependencies":{"raw":["coffee-script","argparse","source-map"]},"keywords":{"raw":null},"description":{"raw":"WIP cat for JS source maps."},"modified":{"raw":"2013-01-23T04:51:42.782Z"},"id":{"raw":"source-map-cat"},"version":{"raw":"0.0.0"},"owners":{"raw":["gregg@aweber.com"]},"_meta":{"score":19.12111}}],"facets":{"license":[{"type":"value","data":[{"value":"MIT","count":101},{"value":"BSD","count":33},{"value":"MIT/X11","count":3}]}]}} 30 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/disjunctive_deps_also_license: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/search.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"query\":\"cat\",\"page\":{\"size\":0},\"filters\":{\"all\":[{\"license\":\"BSD\"}]},\"facets\":{\"dependencies\":[{\"type\":\"value\",\"size\":3}]},\"record_analytics\":false,\"analytics\":{\"tags\":[\"Facet-Only\"]}} 9 | 10 | HTTP/1.1 200 OK 11 | date: Thu, 27 Sep 2018 21:19:12 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | etag: W/"18ee65079f161780b7d9ce13afee0128" 21 | cache-control: max-age=0, private, must-revalidate 22 | x-request-id: 0dbfcb3f3379f524c0cb5530c9e9bc16 23 | x-runtime: 0.130653 24 | x-swiftype-frontend-datacenter: dal05 25 | x-swiftype-frontend-node: web02.dal05 26 | x-swiftype-edge-datacenter: dal05 27 | x-swiftype-edge-node: web02.dal05 28 | 29 | {"meta":{"warnings":[],"page":{"current":1,"total_pages":33,"total_results":33,"size":1},"request_id":"0dbfcb3f3379f524c0cb5530c9e9bc16"},"results":[{"license":{"raw":["BSD"]},"name":{"raw":"source-map-cat"},"repository":{"raw":null},"created":{"raw":"2013-01-23T04:51:39.995Z"},"dependencies":{"raw":["coffee-script","argparse","source-map"]},"keywords":{"raw":null},"description":{"raw":"WIP cat for JS source maps."},"modified":{"raw":"2013-01-23T04:51:42.782Z"},"id":{"raw":"source-map-cat"},"version":{"raw":"0.0.0"},"owners":{"raw":["gregg@aweber.com"]},"_meta":{"score":18.773478}}],"facets":{"dependencies":[{"type":"value","data":[{"value":"request","count":5},{"value":"socket.io","count":5},{"value":"express","count":4}]}]}} 30 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/search_filter_and_multi_facet: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/search.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"query\":\"cat\",\"page\":{\"size\":1},\"filters\":{\"license\":\"BSD\"},\"facets\":{\"license\":[{\"type\":\"value\",\"size\":3}],\"dependencies\":[{\"type\":\"value\",\"size\":3}]}} 9 | 10 | HTTP/1.1 200 OK 11 | date: Thu, 27 Sep 2018 20:53:56 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | etag: W/"5010d52221547ddec5baceeb9f8a4931" 21 | cache-control: max-age=0, private, must-revalidate 22 | x-request-id: 8369bdda4d5c018ddbd85fc2859fbadb 23 | x-runtime: 0.212519 24 | x-swiftype-frontend-datacenter: dal05 25 | x-swiftype-frontend-node: web01.dal05 26 | x-swiftype-edge-datacenter: dal05 27 | x-swiftype-edge-node: web01.dal05 28 | 29 | {"meta":{"warnings":[],"page":{"current":1,"total_pages":33,"total_results":33,"size":1},"request_id":"8369bdda4d5c018ddbd85fc2859fbadb"},"results":[{"license":{"raw":["BSD"]},"name":{"raw":"source-map-cat"},"repository":{"raw":null},"created":{"raw":"2013-01-23T04:51:39.995Z"},"dependencies":{"raw":["coffee-script","argparse","source-map"]},"keywords":{"raw":null},"description":{"raw":"WIP cat for JS source maps."},"modified":{"raw":"2013-01-23T04:51:42.782Z"},"id":{"raw":"source-map-cat"},"version":{"raw":"0.0.0"},"owners":{"raw":["gregg@aweber.com"]},"_meta":{"score":18.773478}}],"facets":{"dependencies":[{"type":"value","data":[{"value":"request","count":5},{"value":"socket.io","count":5},{"value":"express","count":4}]}],"license":[{"type":"value","data":[{"value":"BSD","count":33}]}]}} 30 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/search_multi_facet: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/search.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"query\":\"cat\",\"page\":{\"size\":1},\"facets\":{\"license\":[{\"type\":\"value\",\"size\":3}],\"dependencies\":[{\"type\":\"value\",\"size\":3}]}} 9 | 10 | HTTP/1.1 200 OK 11 | date: Thu, 27 Sep 2018 20:45:39 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | etag: W/"f001ab18053ede3b2cfb60267569ed87" 21 | cache-control: max-age=0, private, must-revalidate 22 | x-request-id: 45219b36fbd0c360acf96bd1d0c46c50 23 | x-runtime: 0.157922 24 | x-swiftype-frontend-datacenter: dal05 25 | x-swiftype-frontend-node: web02.dal05 26 | x-swiftype-edge-datacenter: dal05 27 | x-swiftype-edge-node: web02.dal05 28 | 29 | {"meta":{"warnings":[],"page":{"current":1,"total_pages":642,"total_results":642,"size":1},"request_id":"45219b36fbd0c360acf96bd1d0c46c50"},"results":[{"license":{"raw":["BSD"]},"name":{"raw":"source-map-cat"},"repository":{"raw":null},"created":{"raw":"2013-01-23T04:51:39.995Z"},"dependencies":{"raw":["coffee-script","argparse","source-map"]},"keywords":{"raw":null},"description":{"raw":"WIP cat for JS source maps."},"modified":{"raw":"2013-01-23T04:51:42.782Z"},"id":{"raw":"source-map-cat"},"version":{"raw":"0.0.0"},"owners":{"raw":["gregg@aweber.com"]},"_meta":{"score":19.12111}}],"facets":{"dependencies":[{"type":"value","data":[{"value":"underscore","count":67},{"value":"pkginfo","count":49},{"value":"express","count":48}]}],"license":[{"type":"value","data":[{"value":"MIT","count":101},{"value":"BSD","count":33},{"value":"MIT/X11","count":3}]}]}} 30 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/search_filter_and_multi_facet_with_tags: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/search.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"query\":\"cat\",\"page\":{\"size\":1},\"filters\":{\"license\":\"BSD\"},\"facets\":{\"license\":[{\"type\":\"value\",\"size\":3}],\"dependencies\":[{\"type\":\"value\",\"size\":3}]},\"analytics\":{\"tags\":[\"SERP\"]}} 9 | 10 | HTTP/1.1 200 OK 11 | date: Thu, 27 Sep 2018 20:53:56 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | etag: W/"5010d52221547ddec5baceeb9f8a4931" 21 | cache-control: max-age=0, private, must-revalidate 22 | x-request-id: 8369bdda4d5c018ddbd85fc2859fbadb 23 | x-runtime: 0.212519 24 | x-swiftype-frontend-datacenter: dal05 25 | x-swiftype-frontend-node: web01.dal05 26 | x-swiftype-edge-datacenter: dal05 27 | x-swiftype-edge-node: web01.dal05 28 | 29 | {"meta":{"warnings":[],"page":{"current":1,"total_pages":33,"total_results":33,"size":1},"request_id":"8369bdda4d5c018ddbd85fc2859fbadb"},"results":[{"license":{"raw":["BSD"]},"name":{"raw":"source-map-cat"},"repository":{"raw":null},"created":{"raw":"2013-01-23T04:51:39.995Z"},"dependencies":{"raw":["coffee-script","argparse","source-map"]},"keywords":{"raw":null},"description":{"raw":"WIP cat for JS source maps."},"modified":{"raw":"2013-01-23T04:51:42.782Z"},"id":{"raw":"source-map-cat"},"version":{"raw":"0.0.0"},"owners":{"raw":["gregg@aweber.com"]},"_meta":{"score":18.773478}}],"facets":{"dependencies":[{"type":"value","data":[{"value":"request","count":5},{"value":"socket.io","count":5},{"value":"express","count":4}]}],"license":[{"type":"value","data":[{"value":"BSD","count":33}]}]}} 30 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/disjunctive_license_also_deps: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/search.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"query\":\"cat\",\"page\":{\"size\":0},\"filters\":{\"all\":[{\"dependencies\":\"socket.io\"}]},\"facets\":{\"license\":[{\"type\":\"value\",\"size\":3}]},\"record_analytics\":false,\"analytics\":{\"tags\":[\"Facet-Only\"]}} 9 | 10 | HTTP/1.1 200 OK 11 | date: Thu, 27 Sep 2018 21:04:37 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | etag: W/"3d0a9854946dc61b9a424fa156f6c6dd" 21 | cache-control: max-age=0, private, must-revalidate 22 | x-request-id: 97feb1c3696b7ee64633e7ce00af384f 23 | x-runtime: 0.173476 24 | x-swiftype-frontend-datacenter: dal05 25 | x-swiftype-frontend-node: web02.dal05 26 | x-swiftype-edge-datacenter: dal05 27 | x-swiftype-edge-node: web02.dal05 28 | 29 | {"meta":{"warnings":[],"page":{"current":1,"total_pages":28,"total_results":28,"size":1},"request_id":"97feb1c3696b7ee64633e7ce00af384f"},"results":[{"license":{"raw":null},"name":{"raw":"brainy-server"},"repository":{"raw":["https://github.com/brainyio/brainy-server"]},"created":{"raw":"2013-02-18T07:37:10.852Z"},"dependencies":{"raw":["backbone","brainy-sync-api","brainy-sync","express","file","nconf","requirejs","socket.io","underscore"]},"keywords":{"raw":["brainy","backbone","client","framework"]},"description":{"raw":"a server side and client side framework for Backbone"},"modified":{"raw":"2013-03-02T20:46:27.738Z"},"id":{"raw":"brainy-server"},"version":{"raw":"0.0.5"},"owners":{"raw":["caden@catshirt.net"]},"_meta":{"score":0.45596027}}],"facets":{"license":[{"type":"value","data":[{"value":"BSD","count":5},{"value":"MIT","count":3},{"value":"GPL","count":1}]}]}} 30 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/disjunctive_deps_also_license_no_array_syntax: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/search.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"query\":\"cat\",\"page\":{\"size\":0},\"filters\":{\"all\":[{\"dependencies\":\"socket.io\"}]},\"facets\":{\"license\":{\"type\":\"value\",\"size\":3}},\"record_analytics\":false,\"analytics\":{\"tags\":[\"Facet-Only\"]}} 9 | 10 | HTTP/1.1 200 OK 11 | date: Fri, 28 Sep 2018 20:07:24 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | etag: W/"db276752f14b2467c16355c019ef8301" 21 | cache-control: max-age=0, private, must-revalidate 22 | x-request-id: d5982bf521684718ed764208e3a817b6 23 | x-runtime: 0.187982 24 | x-swiftype-frontend-datacenter: dal05 25 | x-swiftype-frontend-node: web02.dal05 26 | x-swiftype-edge-datacenter: dal05 27 | x-swiftype-edge-node: web02.dal05 28 | 29 | {"meta":{"warnings":[],"page":{"current":1,"total_pages":28,"total_results":28,"size":1},"request_id":"d5982bf521684718ed764208e3a817b6"},"results":[{"license":{"raw":null},"name":{"raw":"brainy-server"},"repository":{"raw":["https://github.com/brainyio/brainy-server"]},"created":{"raw":"2013-02-18T07:37:10.852Z"},"dependencies":{"raw":["backbone","brainy-sync-api","brainy-sync","express","file","nconf","requirejs","socket.io","underscore"]},"keywords":{"raw":["brainy","backbone","client","framework"]},"description":{"raw":"a server side and client side framework for Backbone"},"modified":{"raw":"2013-03-02T20:46:27.738Z"},"id":{"raw":"brainy-server"},"version":{"raw":"0.0.5"},"owners":{"raw":["caden@catshirt.net"]},"_meta":{"score":0.45596027}}],"facets":{"license":[{"type":"value","data":[{"value":"BSD","count":5},{"value":"MIT","count":3},{"value":"GPL","count":1}]}]}} 30 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/search_multi_filter_multi_facet: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/search.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"query\":\"cat\",\"page\":{\"size\":1},\"filters\":{\"all\":[{\"license\":\"BSD\"},{\"dependencies\":\"socket.io\"}]},\"facets\":{\"license\":[{\"type\":\"value\",\"size\":3}],\"dependencies\":[{\"type\":\"value\",\"size\":3}]}} 9 | 10 | HTTP/1.1 200 OK 11 | date: Thu, 27 Sep 2018 21:04:37 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | etag: W/"d685c28ab443372e8e830a960e7de602" 21 | cache-control: max-age=0, private, must-revalidate 22 | x-request-id: c0a394d7343c0363e3a1e0b9a2a89447 23 | x-runtime: 0.194632 24 | x-swiftype-frontend-datacenter: dal05 25 | x-swiftype-frontend-node: web02.dal05 26 | x-swiftype-edge-datacenter: dal05 27 | x-swiftype-edge-node: web02.dal05 28 | 29 | {"meta":{"warnings":[],"page":{"current":1,"total_pages":5,"total_results":5,"size":1},"request_id":"c0a394d7343c0363e3a1e0b9a2a89447"},"results":[{"license":{"raw":["BSD"]},"name":{"raw":"battlefield-tanks"},"repository":{"raw":null},"created":{"raw":"2012-11-17T18:37:08.507Z"},"dependencies":{"raw":["express","socket.io","mongoose","caterpillar","jade","connect","underscore"]},"keywords":{"raw":["game","websocket","tank"]},"description":{"raw":"Realtime game with easeljs, node.js, mongodb and socket.io"},"modified":{"raw":"2012-11-19T17:59:43.452Z"},"id":{"raw":"battlefield-tanks"},"version":{"raw":"0.1.9"},"owners":{"raw":["tim@visualappeal.de"]},"_meta":{"score":0.17728129}}],"facets":{"dependencies":[{"type":"value","data":[{"value":"socket.io","count":5},{"value":"express","count":2},{"value":"async","count":1}]}],"license":[{"type":"value","data":[{"value":"BSD","count":5}]}]}} 30 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/search_multi_filter_multi_facet_no_array_syntax: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/search.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"query\":\"cat\",\"page\":{\"size\":1},\"filters\":{\"all\":[{\"license\":\"BSD\"},{\"dependencies\":\"socket.io\"}]},\"facets\":{\"license\":{\"type\":\"value\",\"size\":3},\"dependencies\":[{\"type\":\"value\",\"size\":3}]}} 9 | 10 | HTTP/1.1 200 OK 11 | date: Fri, 28 Sep 2018 20:07:24 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | etag: W/"2bba9fb440214c4932629a0f695c5bec" 21 | cache-control: max-age=0, private, must-revalidate 22 | x-request-id: 93b521c3dba5a4b9e05d77006e3b4786 23 | x-runtime: 0.126792 24 | x-swiftype-frontend-datacenter: dal05 25 | x-swiftype-frontend-node: web02.dal05 26 | x-swiftype-edge-datacenter: dal05 27 | x-swiftype-edge-node: web02.dal05 28 | 29 | {"meta":{"warnings":[],"page":{"current":1,"total_pages":5,"total_results":5,"size":1},"request_id":"93b521c3dba5a4b9e05d77006e3b4786"},"results":[{"license":{"raw":["BSD"]},"name":{"raw":"battlefield-tanks"},"repository":{"raw":null},"created":{"raw":"2012-11-17T18:37:08.507Z"},"dependencies":{"raw":["express","socket.io","mongoose","caterpillar","jade","connect","underscore"]},"keywords":{"raw":["game","websocket","tank"]},"description":{"raw":"Realtime game with easeljs, node.js, mongodb and socket.io"},"modified":{"raw":"2012-11-19T17:59:43.452Z"},"id":{"raw":"battlefield-tanks"},"version":{"raw":"0.1.9"},"owners":{"raw":["tim@visualappeal.de"]},"_meta":{"score":0.17728129}}],"facets":{"dependencies":[{"type":"value","data":[{"value":"socket.io","count":5},{"value":"express","count":2},{"value":"async","count":1}]}],"license":[{"type":"value","data":[{"value":"BSD","count":5}]}]}} 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@elastic/app-search-javascript", 3 | "version": "8.13.0", 4 | "description": "Javascript client for the Elastic App Search Api", 5 | "browser": "dist/elastic_app_search.umd.js", 6 | "main": "dist/elastic_app_search.umd.js", 7 | "module": "dist/elastic_app_search.es.js", 8 | "scripts": { 9 | "test": "BABEL_ENV=test jest", 10 | "format": "prettier --write \"**/*.js\"", 11 | "build": "BABEL_ENV=dev rollup -c", 12 | "watch": "BABEL_ENV=dev rollup -c --watch", 13 | "predev": "npm run watch &", 14 | "dev": "http-server dist", 15 | "prepare": "npm run build", 16 | "precommit": "lint-staged" 17 | }, 18 | "jest": { 19 | "testURL": "http://localhost/", 20 | "testEnvironment": "jsdom" 21 | }, 22 | "lint-staged": { 23 | "*.js": [ 24 | "eslint --fix", 25 | "git add" 26 | ], 27 | "*.{json,css,md}": [ 28 | "prettier --write", 29 | "git add" 30 | ] 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/elastic/app-search-javascript.git" 35 | }, 36 | "author": "Elastic", 37 | "license": "Apache-2.0", 38 | "bugs": { 39 | "url": "https://github.com/elastic/app-search-javascript/issues" 40 | }, 41 | "homepage": "https://github.com/elastic/app-search-javascript", 42 | "dependencies": { 43 | "object-hash": "^1.3.0" 44 | }, 45 | "devDependencies": { 46 | "babel-cli": "^6.24.1", 47 | "babel-core": "^6.26.3", 48 | "babel-jest": "^22.4.3", 49 | "babel-plugin-external-helpers": "^6.22.0", 50 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 51 | "babel-preset-env": "^1.6.1", 52 | "babel-runtime": "^6.9.2", 53 | "eslint": "^5.1.0", 54 | "eslint-config-prettier": "^2.9.0", 55 | "eslint-plugin-prettier": "^2.6.2", 56 | "http-server": "^0.11.1", 57 | "husky": "^0.14.3", 58 | "jest": "^22.4.3", 59 | "lint-staged": "^7.2.0", 60 | "node-fetch": "2.1.2", 61 | "prettier": "1.13.7", 62 | "regenerator-runtime": "^0.11.1", 63 | "replay": "^2.3.0", 64 | "rollup": "^0.62.0", 65 | "rollup-plugin-babel": "^3.0.7", 66 | "rollup-plugin-json": "^3.0.0", 67 | "rollup-plugin-uglify": "^4.0.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/filters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A helper for working with the JSON structure which represent 3 | * filters in API requests. 4 | */ 5 | export default class Filters { 6 | constructor(filtersJSON = {}) { 7 | this.filtersJSON = filtersJSON; 8 | } 9 | 10 | removeFilter(filterKey, filtersMap = this.filtersJSON) { 11 | function go(filterKey, filtersMap) { 12 | const filtered = Object.entries(filtersMap).reduce( 13 | (acc, [filterName, filterValue]) => { 14 | if (filterName === filterKey) { 15 | return acc; 16 | } 17 | 18 | if (["all", "any", "none"].includes(filterName)) { 19 | const nestedFiltersArray = filterValue; 20 | filterValue = nestedFiltersArray.reduce((acc, nestedFiltersMap) => { 21 | const updatedNestedFiltersMap = go(filterKey, nestedFiltersMap); 22 | if (updatedNestedFiltersMap) { 23 | return acc.concat(updatedNestedFiltersMap); 24 | } else { 25 | return acc; 26 | } 27 | }, []); 28 | } 29 | 30 | return { 31 | ...acc, 32 | [filterName]: filterValue 33 | }; 34 | }, 35 | {} 36 | ); 37 | 38 | if (Object.keys(filtered).length === 0) { 39 | return; 40 | } 41 | return filtered; 42 | } 43 | 44 | const filtered = go(filterKey, filtersMap); 45 | return new Filters(filtered); 46 | } 47 | 48 | getListOfAppliedFilters(filters = this.filtersJSON) { 49 | const set = Object.entries(filters).reduce((acc, [key, value]) => { 50 | if (!["all", "any", "none"].includes(key)) { 51 | acc.add(key); 52 | } else { 53 | value.forEach(nestedValue => { 54 | Object.keys(nestedValue).forEach(nestedKey => { 55 | if (!["all", "any", "none"].includes(nestedKey)) { 56 | acc.add(nestedKey); 57 | } else { 58 | acc = new Set([ 59 | ...acc, 60 | ...this.getListOfAppliedFilters(nestedValue) 61 | ]); 62 | } 63 | }); 64 | }); 65 | } 66 | return acc; 67 | }, new Set()); 68 | 69 | return Array.from(set.values()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/request.spec.js: -------------------------------------------------------------------------------- 1 | import { request } from "../src/request"; 2 | import { Headers } from "node-fetch"; 3 | import { version } from "../package.json"; 4 | 5 | describe("request", () => { 6 | const responseJson = {}; 7 | const response = { 8 | json: () => Promise.resolve(responseJson) 9 | }; 10 | const responseWithParsingError = { 11 | json: () => Promise.reject() 12 | }; 13 | 14 | const searchKey = "api-12345"; 15 | const endpoint = "http://www.example.com"; 16 | const path = "/v1/search"; 17 | const params = { 18 | a: "a" 19 | }; 20 | 21 | beforeEach(() => { 22 | global.Headers = Headers; 23 | jest.resetAllMocks(); 24 | global.fetch = jest 25 | .fn() 26 | .mockImplementation(() => Promise.resolve(response)); 27 | }); 28 | 29 | it("can send a fetch request, authenticated by the provided search key", async () => { 30 | const res = await request(searchKey, endpoint, path, params, false); 31 | expect(res.response).toBe(response); 32 | expect(global.fetch.mock.calls.length).toBe(1); 33 | var [_, options] = global.fetch.mock.calls[0]; 34 | expect(options.headers.get("Authorization")).toEqual("Bearer api-12345"); 35 | }); 36 | 37 | // The use case for this is mostly internal to Elastic, where we rely on the logged in user session (via cookies) to authenticate 38 | it("can send an authenticated fetch request, when no search key is provided", async () => { 39 | const res = await request(undefined, endpoint, path, params, false); 40 | expect(global.fetch.mock.calls.length).toBe(1); 41 | var [_, options] = global.fetch.mock.calls[0]; 42 | expect(options.headers.has("Authorization")).toBe(false); 43 | }); 44 | 45 | it("will return a cached response if already called once", async () => { 46 | const res = await request(searchKey, endpoint, path, params, true); 47 | await request(searchKey, endpoint, path, params, true); 48 | await request(searchKey, endpoint, path, params, true); 49 | expect(res.response).toBe(response); 50 | expect(global.fetch.mock.calls.length).toBe(1); 51 | }); 52 | 53 | it("will not return the cached response if endpoint changes", async () => { 54 | const res = await request( 55 | searchKey, 56 | "http://www.jira.com", 57 | path, 58 | params, 59 | true 60 | ); 61 | expect(res.response).toBe(response); 62 | expect(global.fetch.mock.calls.length).toBe(1); 63 | }); 64 | 65 | it("will not return the cached response if path changes", async () => { 66 | const res = await request(searchKey, endpoint, "/new/path", params, true); 67 | expect(res.response).toBe(response); 68 | expect(global.fetch.mock.calls.length).toBe(1); 69 | }); 70 | 71 | it("will not return the cached response if path params change", async () => { 72 | const res = await request( 73 | searchKey, 74 | endpoint, 75 | path, 76 | { 77 | a: "a", 78 | b: "b" 79 | }, 80 | true 81 | ); 82 | expect(res.response).toBe(response); 83 | expect(global.fetch.mock.calls.length).toBe(1); 84 | }); 85 | 86 | it("will return another cached response", async () => { 87 | const res = await request( 88 | searchKey, 89 | endpoint, 90 | path, 91 | { 92 | a: "a", 93 | b: "b" 94 | }, 95 | true 96 | ); 97 | expect(res.response).toBe(response); 98 | expect(global.fetch.mock.calls.length).toBe(0); 99 | }); 100 | 101 | it("will not cache an error response", async () => { 102 | global.fetch = jest 103 | .fn() 104 | .mockImplementation(() => Promise.resolve(responseWithParsingError)); 105 | 106 | let res = await request(searchKey, "bad/endpoint", path, params, true); 107 | expect(res.json).toEqual({}); 108 | expect(global.fetch.mock.calls.length).toBe(1); 109 | 110 | res = await request(searchKey, "bad/endpoint", path, params, true); 111 | expect(res.json).toEqual({}); 112 | expect(global.fetch.mock.calls.length).toBe(2); 113 | }); 114 | 115 | it("will ignore cache if cacheResponses is false", async () => { 116 | const res = await request(searchKey, endpoint, path, params, false); 117 | expect(res.response).toBe(response); 118 | expect(global.fetch.mock.calls.length).toBe(1); 119 | }); 120 | 121 | it("will have the correct browser based meta headers when running in browser context", async () => { 122 | expect(global.window).toBeDefined(); 123 | const res = await request(searchKey, endpoint, path, params, false); 124 | expect(res.response).toBe(response); 125 | expect(global.fetch.mock.calls.length).toBe(1); 126 | var [_, options] = global.fetch.mock.calls[0]; 127 | expect(options.headers.get("x-elastic-client-meta")).toEqual( 128 | `ent=${version}-legacy,js=browser,t=${version}-legacy,ft=universal` 129 | ); 130 | const validHeaderRegex = /^[a-z]{1,}=[a-z0-9\.\-]{1,}(?:,[a-z]{1,}=[a-z0-9\.\-]+)*$/; 131 | expect(options.headers.get("x-elastic-client-meta")).toMatch( 132 | validHeaderRegex 133 | ); 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/additional_headers: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/search.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: bogus/format 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"query\":\"cat\"} 9 | 10 | HTTP/1.1 200 OK 11 | date: Mon, 09 Jul 2018 14:14:38 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | etag: W/"a4200d33b39a6f91d97871fd280d0e97" 21 | cache-control: max-age=0, private, must-revalidate 22 | x-request-id: 8bedbf97e0a9ed0e9335665938502c27 23 | x-runtime: 0.274941 24 | x-swiftype-datacenter: dal05 25 | x-swiftype-frontend-node: web02.dal05 26 | x-swiftype-edge-node: web02.dal05 27 | 28 | {"meta":{"warnings":[],"page":{"current":1,"total_pages":65,"total_results":642,"size":10},"request_id":"8bedbf97e0a9ed0e9335665938502c27"},"results":[{"license":{"raw":["BSD"]},"name":{"raw":"source-map-cat"},"repository":{"raw":null},"created":{"raw":"2013-01-23T04:51:39.995Z"},"dependencies":{"raw":["coffee-script","argparse","source-map"]},"keywords":{"raw":null},"description":{"raw":"WIP cat for JS source maps."},"modified":{"raw":"2013-01-23T04:51:42.782Z"},"id":{"raw":"source-map-cat"},"version":{"raw":"0.0.0"},"owners":{"raw":["gregg@aweber.com"]},"_meta":{"score":19.10181}},{"license":{"raw":null},"name":{"raw":"Cat4D"},"repository":{"raw":["https://github.com/Cat4D/Cat4D"]},"homepage":{"raw":"http://www.dumb.cat"},"created":{"raw":"2012-04-12T06:27:43.515Z"},"dependencies":{"raw":null},"keywords":{"raw":["Cat4D","dumb","cat"]},"description":{"raw":"Cat4D Framework Implementation"},"modified":{"raw":"2012-04-12T06:39:23.334Z"},"id":{"raw":"Cat4D"},"version":{"raw":"0.0.2"},"owners":{"raw":["Cat@Dumb.Cat"]},"_meta":{"score":18.906376}},{"license":{"raw":null},"name":{"raw":"response.require"},"repository":{"raw":["https://github.com/kapetan/response.require"]},"created":{"raw":"2013-01-06T13:43:21.012Z"},"dependencies":{"raw":["rex","cat","htmlparser"]},"keywords":{"raw":["root","rex","require"]},"description":{"raw":"Rex Javascript compiling for root"},"modified":{"raw":"2013-01-06T13:43:23.680Z"},"id":{"raw":"response.require"},"version":{"raw":"0.1.0"},"owners":{"raw":["mirza.kapetanovic@gmail.com"]},"_meta":{"score":17.527254}},{"license":{"raw":null},"name":{"raw":"rex-cli"},"repository":{"raw":["https://github.com/gett/rex"]},"created":{"raw":"2012-05-01T21:32:09.544Z"},"dependencies":{"raw":["common","rex","optimist","cat"]},"keywords":{"raw":["browser","commonjs","cli","require"]},"description":{"raw":"rex-cli is the cli equivivalent of rex"},"modified":{"raw":"2013-01-02T12:48:38.182Z"},"id":{"raw":"rex-cli"},"version":{"raw":"0.3.2"},"owners":{"raw":["mathiasbuus@gmail.com"]},"_meta":{"score":15.753918}},{"license":{"raw":["BSD"]},"name":{"raw":"cats"},"repository":{"raw":["https://github.com/luciferous/cats"]},"homepage":{"raw":"http://lcfrs.org/cats"},"created":{"raw":"2012-10-15T23:26:48.468Z"},"dependencies":{"raw":null},"keywords":{"raw":["monad","functor","monoid","async"]},"description":{"raw":"Categories for Javascript"},"modified":{"raw":"2012-10-18T16:14:10.680Z"},"id":{"raw":"cats"},"version":{"raw":"0.0.2"},"owners":{"raw":["neuman.vong@gmail.com"]},"_meta":{"score":12.959391}},{"license":{"raw":null},"name":{"raw":"cts"},"repository":{"raw":["https://github.com/cts/cts-js"]},"homepage":{"raw":"http://www.treesheets.org"},"created":{"raw":"2013-01-28T16:50:08.471Z"},"dependencies":{"raw":null},"keywords":{"raw":["treesheets","cts","cats","structure","cascading treesheets","tree"]},"description":{"raw":"Declarative structural remapping for the web."},"modified":{"raw":"2013-01-28T16:50:09.891Z"},"id":{"raw":"cts"},"version":{"raw":"0.5.0"},"owners":{"raw":["eob@csail.mit.edu"]},"_meta":{"score":7.3216166}},{"license":{"raw":null},"name":{"raw":"redis-user"},"repository":{"raw":null},"homepage":{"raw":"https://github.com/CatChen/redis-user"},"created":{"raw":"2011-06-24T15:38:51.745Z"},"dependencies":{"raw":["jshelpers"]},"keywords":{"raw":null},"description":{"raw":"A simple user and role system for Node.js and Redis."},"modified":{"raw":"2011-07-20T04:00:12.987Z"},"id":{"raw":"redis-user"},"version":{"raw":"0.2.4"},"owners":{"raw":["cathsfz@gmail.com"]},"_meta":{"score":3.2000966}},{"license":{"raw":null},"name":{"raw":"traceurl"},"repository":{"raw":null},"homepage":{"raw":"https://github.com/CatChen/traceurl"},"created":{"raw":"2011-07-16T16:31:00.287Z"},"dependencies":{"raw":["jshelpers"]},"keywords":{"raw":null},"description":{"raw":"A JavaScript utility to trace the original url of a shortened url."},"modified":{"raw":"2011-12-16T16:38:49.255Z"},"id":{"raw":"traceurl"},"version":{"raw":"0.2.8"},"owners":{"raw":["cathsfz@gmail.com"]},"_meta":{"score":2.8856053}},{"license":{"raw":["MIT"]},"name":{"raw":"catcher"},"repository":{"raw":["https://github.com/stuartpb/catcher"]},"created":{"raw":"2013-01-10T01:48:15.743Z"},"dependencies":{"raw":null},"keywords":{"raw":["error","handling"]},"description":{"raw":"microscopic Node error helpers"},"modified":{"raw":"2013-01-10T01:48:16.381Z"},"id":{"raw":"catcher"},"version":{"raw":"0.1.0"},"owners":{"raw":["stuart@testtrack4.com"]},"_meta":{"score":0.73055637}},{"license":{"raw":null},"name":{"raw":"catwalk"},"repository":{"raw":null},"created":{"raw":"2012-12-20T21:05:36.757Z"},"dependencies":{"raw":null},"keywords":{"raw":null},"description":{"raw":"models++"},"modified":{"raw":"2012-12-20T21:05:38.476Z"},"id":{"raw":"catwalk"},"version":{"raw":"0.0.1"},"owners":{"raw":["a@so.hn"]},"_meta":{"score":0.7191726}}]} 29 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/search_simple: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/search.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"query\":\"cat\"} 9 | 10 | HTTP/1.1 200 OK 11 | date: Mon, 09 Jul 2018 14:14:38 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | etag: W/"a4200d33b39a6f91d97871fd280d0e97" 21 | cache-control: max-age=0, private, must-revalidate 22 | x-request-id: 8bedbf97e0a9ed0e9335665938502c27 23 | x-runtime: 0.274941 24 | x-swiftype-datacenter: dal05 25 | x-swiftype-frontend-node: web02.dal05 26 | x-swiftype-edge-node: web02.dal05 27 | 28 | {"meta":{"warnings":[],"page":{"current":1,"total_pages":65,"total_results":642,"size":10},"request_id":"8bedbf97e0a9ed0e9335665938502c27"},"results":[{"license":{"raw":["BSD"]},"name":{"raw":"source-map-cat"},"repository":{"raw":null},"created":{"raw":"2013-01-23T04:51:39.995Z"},"dependencies":{"raw":["coffee-script","argparse","source-map"]},"keywords":{"raw":null},"description":{"raw":"WIP cat for JS source maps."},"modified":{"raw":"2013-01-23T04:51:42.782Z"},"id":{"raw":"source-map-cat"},"version":{"raw":"0.0.0"},"owners":{"raw":["gregg@aweber.com"]},"_meta":{"score":19.10181}},{"license":{"raw":null},"name":{"raw":"Cat4D"},"repository":{"raw":["https://github.com/Cat4D/Cat4D"]},"homepage":{"raw":"http://www.dumb.cat"},"created":{"raw":"2012-04-12T06:27:43.515Z"},"dependencies":{"raw":null},"keywords":{"raw":["Cat4D","dumb","cat"]},"description":{"raw":"Cat4D Framework Implementation"},"modified":{"raw":"2012-04-12T06:39:23.334Z"},"id":{"raw":"Cat4D"},"version":{"raw":"0.0.2"},"owners":{"raw":["Cat@Dumb.Cat"]},"_meta":{"score":18.906376}},{"license":{"raw":null},"name":{"raw":"response.require"},"repository":{"raw":["https://github.com/kapetan/response.require"]},"created":{"raw":"2013-01-06T13:43:21.012Z"},"dependencies":{"raw":["rex","cat","htmlparser"]},"keywords":{"raw":["root","rex","require"]},"description":{"raw":"Rex Javascript compiling for root"},"modified":{"raw":"2013-01-06T13:43:23.680Z"},"id":{"raw":"response.require"},"version":{"raw":"0.1.0"},"owners":{"raw":["mirza.kapetanovic@gmail.com"]},"_meta":{"score":17.527254}},{"license":{"raw":null},"name":{"raw":"rex-cli"},"repository":{"raw":["https://github.com/gett/rex"]},"created":{"raw":"2012-05-01T21:32:09.544Z"},"dependencies":{"raw":["common","rex","optimist","cat"]},"keywords":{"raw":["browser","commonjs","cli","require"]},"description":{"raw":"rex-cli is the cli equivivalent of rex"},"modified":{"raw":"2013-01-02T12:48:38.182Z"},"id":{"raw":"rex-cli"},"version":{"raw":"0.3.2"},"owners":{"raw":["mathiasbuus@gmail.com"]},"_meta":{"score":15.753918}},{"license":{"raw":["BSD"]},"name":{"raw":"cats"},"repository":{"raw":["https://github.com/luciferous/cats"]},"homepage":{"raw":"http://lcfrs.org/cats"},"created":{"raw":"2012-10-15T23:26:48.468Z"},"dependencies":{"raw":null},"keywords":{"raw":["monad","functor","monoid","async"]},"description":{"raw":"Categories for Javascript"},"modified":{"raw":"2012-10-18T16:14:10.680Z"},"id":{"raw":"cats"},"version":{"raw":"0.0.2"},"owners":{"raw":["neuman.vong@gmail.com"]},"_meta":{"score":12.959391}},{"license":{"raw":null},"name":{"raw":"cts"},"repository":{"raw":["https://github.com/cts/cts-js"]},"homepage":{"raw":"http://www.treesheets.org"},"created":{"raw":"2013-01-28T16:50:08.471Z"},"dependencies":{"raw":null},"keywords":{"raw":["treesheets","cts","cats","structure","cascading treesheets","tree"]},"description":{"raw":"Declarative structural remapping for the web."},"modified":{"raw":"2013-01-28T16:50:09.891Z"},"id":{"raw":"cts"},"version":{"raw":"0.5.0"},"owners":{"raw":["eob@csail.mit.edu"]},"_meta":{"score":7.3216166}},{"license":{"raw":null},"name":{"raw":"redis-user"},"repository":{"raw":null},"homepage":{"raw":"https://github.com/CatChen/redis-user"},"created":{"raw":"2011-06-24T15:38:51.745Z"},"dependencies":{"raw":["jshelpers"]},"keywords":{"raw":null},"description":{"raw":"A simple user and role system for Node.js and Redis."},"modified":{"raw":"2011-07-20T04:00:12.987Z"},"id":{"raw":"redis-user"},"version":{"raw":"0.2.4"},"owners":{"raw":["cathsfz@gmail.com"]},"_meta":{"score":3.2000966}},{"license":{"raw":null},"name":{"raw":"traceurl"},"repository":{"raw":null},"homepage":{"raw":"https://github.com/CatChen/traceurl"},"created":{"raw":"2011-07-16T16:31:00.287Z"},"dependencies":{"raw":["jshelpers"]},"keywords":{"raw":null},"description":{"raw":"A JavaScript utility to trace the original url of a shortened url."},"modified":{"raw":"2011-12-16T16:38:49.255Z"},"id":{"raw":"traceurl"},"version":{"raw":"0.2.8"},"owners":{"raw":["cathsfz@gmail.com"]},"_meta":{"score":2.8856053}},{"license":{"raw":["MIT"]},"name":{"raw":"catcher"},"repository":{"raw":["https://github.com/stuartpb/catcher"]},"created":{"raw":"2013-01-10T01:48:15.743Z"},"dependencies":{"raw":null},"keywords":{"raw":["error","handling"]},"description":{"raw":"microscopic Node error helpers"},"modified":{"raw":"2013-01-10T01:48:16.381Z"},"id":{"raw":"catcher"},"version":{"raw":"0.1.0"},"owners":{"raw":["stuart@testtrack4.com"]},"_meta":{"score":0.73055637}},{"license":{"raw":null},"name":{"raw":"catwalk"},"repository":{"raw":null},"created":{"raw":"2012-12-20T21:05:36.757Z"},"dependencies":{"raw":null},"keywords":{"raw":null},"description":{"raw":"models++"},"modified":{"raw":"2012-12-20T21:05:38.476Z"},"id":{"raw":"catwalk"},"version":{"raw":"0.0.1"},"owners":{"raw":["a@so.hn"]},"_meta":{"score":0.7191726}}]} 29 | -------------------------------------------------------------------------------- /fixtures/localhost.swiftype.com-3002/localhost_search: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/search.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"query\":\"cat\"} 9 | 10 | HTTP/1.1 200 OK 11 | date: Wed, 26 Sep 2018 13:20:13 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | etag: W/"152562ba357115883a713ed7bfc2f851" 21 | cache-control: max-age=0, private, must-revalidate 22 | x-request-id: ad345570a3193f91ab91aece286ede1d 23 | x-runtime: 0.302035 24 | x-swiftype-frontend-datacenter: dal05 25 | x-swiftype-frontend-node: web02.dal05 26 | x-swiftype-edge-datacenter: dal05 27 | x-swiftype-edge-node: web02.dal05 28 | 29 | {"meta":{"warnings":[],"page":{"current":1,"total_pages":65,"total_results":642,"size":10},"request_id":"ad345570a3193f91ab91aece286ede1d"},"results":[{"license":{"raw":["BSD"]},"name":{"raw":"source-map-cat"},"repository":{"raw":null},"created":{"raw":"2013-01-23T04:51:39.995Z"},"dependencies":{"raw":["coffee-script","argparse","source-map"]},"keywords":{"raw":null},"description":{"raw":"WIP cat for JS source maps."},"modified":{"raw":"2013-01-23T04:51:42.782Z"},"id":{"raw":"source-map-cat"},"version":{"raw":"0.0.0"},"owners":{"raw":["gregg@aweber.com"]},"_meta":{"score":18.773478}},{"license":{"raw":null},"name":{"raw":"Cat4D"},"repository":{"raw":["https://github.com/Cat4D/Cat4D"]},"homepage":{"raw":"http://www.dumb.cat"},"created":{"raw":"2012-04-12T06:27:43.515Z"},"dependencies":{"raw":null},"keywords":{"raw":["Cat4D","dumb","cat"]},"description":{"raw":"Cat4D Framework Implementation"},"modified":{"raw":"2012-04-12T06:39:23.334Z"},"id":{"raw":"Cat4D"},"version":{"raw":"0.0.2"},"owners":{"raw":["Cat@Dumb.Cat"]},"_meta":{"score":18.477566}},{"license":{"raw":null},"name":{"raw":"response.require"},"repository":{"raw":["https://github.com/kapetan/response.require"]},"created":{"raw":"2013-01-06T13:43:21.012Z"},"dependencies":{"raw":["rex","cat","htmlparser"]},"keywords":{"raw":["root","rex","require"]},"description":{"raw":"Rex Javascript compiling for root"},"modified":{"raw":"2013-01-06T13:43:23.680Z"},"id":{"raw":"response.require"},"version":{"raw":"0.1.0"},"owners":{"raw":["mirza.kapetanovic@gmail.com"]},"_meta":{"score":17.458853}},{"license":{"raw":null},"name":{"raw":"rex-cli"},"repository":{"raw":["https://github.com/gett/rex"]},"created":{"raw":"2012-05-01T21:32:09.544Z"},"dependencies":{"raw":["common","rex","optimist","cat"]},"keywords":{"raw":["browser","commonjs","cli","require"]},"description":{"raw":"rex-cli is the cli equivivalent of rex"},"modified":{"raw":"2013-01-02T12:48:38.182Z"},"id":{"raw":"rex-cli"},"version":{"raw":"0.3.2"},"owners":{"raw":["mathiasbuus@gmail.com"]},"_meta":{"score":15.531057}},{"license":{"raw":["BSD"]},"name":{"raw":"cats"},"repository":{"raw":["https://github.com/luciferous/cats"]},"homepage":{"raw":"http://lcfrs.org/cats"},"created":{"raw":"2012-10-15T23:26:48.468Z"},"dependencies":{"raw":null},"keywords":{"raw":["monad","functor","monoid","async"]},"description":{"raw":"Categories for Javascript"},"modified":{"raw":"2012-10-18T16:14:10.680Z"},"id":{"raw":"cats"},"version":{"raw":"0.0.2"},"owners":{"raw":["neuman.vong@gmail.com"]},"_meta":{"score":12.659987}},{"license":{"raw":null},"name":{"raw":"cts"},"repository":{"raw":["https://github.com/cts/cts-js"]},"homepage":{"raw":"http://www.treesheets.org"},"created":{"raw":"2013-01-28T16:50:08.471Z"},"dependencies":{"raw":null},"keywords":{"raw":["treesheets","cts","cats","structure","cascading treesheets","tree"]},"description":{"raw":"Declarative structural remapping for the web."},"modified":{"raw":"2013-01-28T16:50:09.891Z"},"id":{"raw":"cts"},"version":{"raw":"0.5.0"},"owners":{"raw":["eob@csail.mit.edu"]},"_meta":{"score":6.8988786}},{"license":{"raw":null},"name":{"raw":"redis-user"},"repository":{"raw":null},"homepage":{"raw":"https://github.com/CatChen/redis-user"},"created":{"raw":"2011-06-24T15:38:51.745Z"},"dependencies":{"raw":["jshelpers"]},"keywords":{"raw":null},"description":{"raw":"A simple user and role system for Node.js and Redis."},"modified":{"raw":"2011-07-20T04:00:12.987Z"},"id":{"raw":"redis-user"},"version":{"raw":"0.2.4"},"owners":{"raw":["cathsfz@gmail.com"]},"_meta":{"score":3.1969237}},{"license":{"raw":null},"name":{"raw":"traceurl"},"repository":{"raw":null},"homepage":{"raw":"https://github.com/CatChen/traceurl"},"created":{"raw":"2011-07-16T16:31:00.287Z"},"dependencies":{"raw":["jshelpers"]},"keywords":{"raw":null},"description":{"raw":"A JavaScript utility to trace the original url of a shortened url."},"modified":{"raw":"2011-12-16T16:38:49.255Z"},"id":{"raw":"traceurl"},"version":{"raw":"0.2.8"},"owners":{"raw":["cathsfz@gmail.com"]},"_meta":{"score":2.837085}},{"license":{"raw":["MIT"]},"name":{"raw":"catcher"},"repository":{"raw":["https://github.com/stuartpb/catcher"]},"created":{"raw":"2013-01-10T01:48:15.743Z"},"dependencies":{"raw":null},"keywords":{"raw":["error","handling"]},"description":{"raw":"microscopic Node error helpers"},"modified":{"raw":"2013-01-10T01:48:16.381Z"},"id":{"raw":"catcher"},"version":{"raw":"0.1.0"},"owners":{"raw":["stuart@testtrack4.com"]},"_meta":{"score":0.7402164}},{"license":{"raw":null},"name":{"raw":"catwalk"},"repository":{"raw":null},"created":{"raw":"2012-12-20T21:05:36.757Z"},"dependencies":{"raw":null},"keywords":{"raw":null},"description":{"raw":"models++"},"modified":{"raw":"2012-12-20T21:05:38.476Z"},"id":{"raw":"catwalk"},"version":{"raw":"0.0.1"},"owners":{"raw":["a@so.hn"]},"_meta":{"score":0.7177082}}]} 30 | -------------------------------------------------------------------------------- /tests/filters.spec.js: -------------------------------------------------------------------------------- 1 | import Filters from "../src/filters"; 2 | 3 | describe("Filters", () => { 4 | test("can be instantiated", () => { 5 | const filters = new Filters({}); 6 | expect(filters).toBeInstanceOf(Filters); 7 | }); 8 | 9 | describe("#removeFilter", () => { 10 | // At a top level, there can only ever be one filter applied. In other words 11 | // the top level hash can have any combo of 'all', 'any', or 'none', and 12 | // if they have none of those, there can be one key for one top level filter 13 | test("can remove a filter at the top level", () => { 14 | const filters = new Filters({ 15 | a: "a" 16 | }); 17 | 18 | expect(filters.removeFilter("a").filtersJSON).toEqual({}); 19 | }); 20 | 21 | test("can be chained", () => { 22 | const filters = new Filters({ 23 | all: [{ c: "c" }, { a: "a" }, { b: "b" }, { d: "d" }] 24 | }); 25 | 26 | expect(filters.removeFilter("a").removeFilter("b").filtersJSON).toEqual({ 27 | all: [{ c: "c" }, { d: "d" }] 28 | }); 29 | }); 30 | 31 | test("can remove a filter from any, all, or none", () => { 32 | const filters = new Filters({ 33 | all: [{ c: "c" }, { d: "d" }], 34 | none: [{ e: "e" }, { f: "f" }], 35 | any: [{ g: "g" }] 36 | }); 37 | 38 | expect( 39 | filters 40 | .removeFilter("c") 41 | .removeFilter("f") 42 | .removeFilter("g").filtersJSON 43 | ).toEqual({ 44 | all: [{ d: "d" }], 45 | none: [{ e: "e" }], 46 | any: [] 47 | }); 48 | }); 49 | 50 | test("can remove nested filters", () => { 51 | const filters = new Filters({ 52 | all: [ 53 | { 54 | all: [{ c: "c" }, { d: "d" }], 55 | none: [{ e: "e" }, { e: "e1" }, { f: "f" }], 56 | any: [ 57 | { 58 | g: "g" 59 | }, 60 | { 61 | all: [ 62 | { 63 | h: "h" 64 | }, 65 | { 66 | any: [ 67 | { 68 | i: "i" 69 | } 70 | ] 71 | } 72 | ] 73 | } 74 | ] 75 | }, 76 | { j: "j" } 77 | ], 78 | any: [ 79 | { 80 | all: [{ k: "k" }], 81 | any: [{ l: "l" }] 82 | } 83 | ] 84 | }); 85 | 86 | expect( 87 | filters 88 | .removeFilter("k") 89 | .removeFilter("i") 90 | .removeFilter("e").filtersJSON 91 | ).toEqual({ 92 | all: [ 93 | { 94 | all: [{ c: "c" }, { d: "d" }], 95 | none: [{ f: "f" }], 96 | any: [ 97 | { 98 | g: "g" 99 | }, 100 | { 101 | all: [ 102 | { 103 | h: "h" 104 | }, 105 | { 106 | any: [] 107 | } 108 | ] 109 | } 110 | ] 111 | }, 112 | { j: "j" } 113 | ], 114 | any: [ 115 | { 116 | all: [], 117 | any: [{ l: "l" }] 118 | } 119 | ] 120 | }); 121 | }); 122 | }); 123 | 124 | describe("#getListOfAppliedFilters", () => { 125 | test("it should return a single top level value filter", () => { 126 | const filters = new Filters({ 127 | b: "b" 128 | }); 129 | 130 | expect(filters.getListOfAppliedFilters()).toEqual(["b"]); 131 | }); 132 | 133 | test("it should return a list of top level filters", () => { 134 | const filters = new Filters({ 135 | b: ["b", "b1"] 136 | }); 137 | 138 | expect(filters.getListOfAppliedFilters()).toEqual(["b"]); 139 | }); 140 | 141 | test("it include filters nested in all, any, or not", () => { 142 | const filters = new Filters({ 143 | all: [{ c: "c" }, { d: "d" }], 144 | none: [{ e: "e" }, { e: "e1" }, { f: "f" }], 145 | any: [{ g: "g" }] 146 | }); 147 | 148 | expect(filters.getListOfAppliedFilters()).toEqual([ 149 | "c", 150 | "d", 151 | "e", 152 | "f", 153 | "g" 154 | ]); 155 | }); 156 | 157 | test("it should return nested filters", () => { 158 | const filters = new Filters({ 159 | all: [ 160 | { 161 | all: [{ c: "c" }, { d: "d" }], 162 | none: [{ e: "e" }, { e: "e1" }, { f: "f" }], 163 | any: [ 164 | { 165 | g: "g" 166 | }, 167 | { 168 | all: [ 169 | { 170 | h: "h" 171 | }, 172 | { 173 | any: [ 174 | { 175 | i: "i" 176 | } 177 | ] 178 | } 179 | ] 180 | } 181 | ] 182 | }, 183 | { j: "j" } 184 | ], 185 | any: [ 186 | { 187 | all: [{ k: "k" }], 188 | any: [{ l: "l" }] 189 | } 190 | ] 191 | }); 192 | 193 | expect(filters.getListOfAppliedFilters()).toEqual([ 194 | "c", 195 | "d", 196 | "e", 197 | "f", 198 | "g", 199 | "h", 200 | "i", 201 | "j", 202 | "k", 203 | "l" 204 | ]); 205 | }); 206 | }); 207 | }); 208 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/search_grouped: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/search.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"query\":\"cat\",\"page\":{\"size\":1},\"group\":{\"field\":\"license\"}} 9 | 10 | HTTP/1.1 200 OK 11 | date: Thu, 04 Oct 2018 17:22:59 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | etag: W/"695793907b99dbfef43c8a952c877648" 21 | cache-control: max-age=0, private, must-revalidate 22 | x-request-id: 6fd3267d6e128a1eee55afb02cc954e7 23 | x-runtime: 0.283013 24 | x-swiftype-frontend-datacenter: dal05 25 | x-swiftype-frontend-node: web01.dal05 26 | x-swiftype-edge-datacenter: dal05 27 | x-swiftype-edge-node: web01.dal05 28 | 29 | {"meta":{"warnings":[],"page":{"current":1,"total_pages":14,"total_results":14,"size":1},"request_id":"6fd3267d6e128a1eee55afb02cc954e7"},"results":[{"name":{"raw":"source-map-cat"},"version":{"raw":"0.0.0"},"license":{"raw":["BSD"]},"description":{"raw":"WIP cat for JS source maps."},"created":{"raw":"2013-01-23T04:51:39.995Z"},"modified":{"raw":"2013-01-23T04:51:42.782Z"},"keywords":{"raw":null},"repository":{"raw":null},"owners":{"raw":["gregg@aweber.com"]},"dependencies":{"raw":["coffee-script","argparse","source-map"]},"id":{"raw":"source-map-cat"},"_meta":{"score":18.773478},"_group":[{"name":{"raw":"cats"},"version":{"raw":"0.0.2"},"license":{"raw":["BSD"]},"description":{"raw":"Categories for Javascript"},"homepage":{"raw":"http://lcfrs.org/cats"},"created":{"raw":"2012-10-15T23:26:48.468Z"},"modified":{"raw":"2012-10-18T16:14:10.680Z"},"keywords":{"raw":["monad","functor","monoid","async"]},"repository":{"raw":["https://github.com/luciferous/cats"]},"owners":{"raw":["neuman.vong@gmail.com"]},"dependencies":{"raw":null},"id":{"raw":"cats"},"_meta":{"score":12.659987}},{"name":{"raw":"battlefield-tanks"},"version":{"raw":"0.1.9"},"license":{"raw":["BSD"]},"description":{"raw":"Realtime game with easeljs, node.js, mongodb and socket.io"},"created":{"raw":"2012-11-17T18:37:08.507Z"},"modified":{"raw":"2012-11-19T17:59:43.452Z"},"keywords":{"raw":["game","websocket","tank"]},"repository":{"raw":null},"owners":{"raw":["tim@visualappeal.de"]},"dependencies":{"raw":["express","socket.io","mongoose","caterpillar","jade","connect","underscore"]},"id":{"raw":"battlefield-tanks"},"_meta":{"score":0.17317744}},{"name":{"raw":"throwandtell"},"version":{"raw":"0.1.0"},"license":{"raw":["BSD"]},"description":{"raw":"ThrowAndTell Error Reporter Client for Node.js"},"created":{"raw":"2012-11-13T13:02:03.828Z"},"modified":{"raw":"2012-11-13T13:02:38.128Z"},"keywords":{"raw":["error","err","report","reporting","throw","tell","github","catch","auto"]},"repository":{"raw":null},"owners":{"raw":["ben@bensbit.co.uk"]},"dependencies":{"raw":["request"]},"id":{"raw":"throwandtell"},"_meta":{"score":0.13406895}},{"name":{"raw":"bark-notifications"},"version":{"raw":"0.0.3"},"license":{"raw":["BSD"]},"description":{"raw":"- jQuery - [transit](http://ricostacruz.com/jquery.transit/)"},"created":{"raw":"2012-12-04T00:05:56.292Z"},"modified":{"raw":"2012-12-09T23:12:33.385Z"},"keywords":{"raw":null},"repository":{"raw":["https://github.com/crcn/bark.js"]},"owners":{"raw":["craig.j.condon@gmail.com"]},"dependencies":{"raw":["structr"]},"id":{"raw":"bark-notifications"},"_meta":{"score":0.040876213}},{"name":{"raw":"nodeapp"},"version":{"raw":"1.0.0"},"license":{"raw":["BSD"]},"description":{"raw":"Writing desktop applications with all node.js technologies!"},"created":{"raw":"2012-12-27T13:27:26.583Z"},"modified":{"raw":"2012-12-27T13:27:34.670Z"},"keywords":{"raw":["html","application"]},"repository":{"raw":["https://github.com/derek-chen/NodeApp"]},"owners":{"raw":["derek.amz@gmail.com"]},"dependencies":{"raw":null},"id":{"raw":"nodeapp"},"_meta":{"score":0.03494464}},{"name":{"raw":"tokenauthentication"},"version":{"raw":"0.0.3"},"license":{"raw":["BSD"]},"description":{"raw":"This module associates an authentication procedure with a token generation."},"created":{"raw":"2013-02-13T04:10:44.316Z"},"modified":{"raw":"2013-03-05T01:21:43.724Z"},"keywords":{"raw":["authentication","token"]},"repository":{"raw":null},"owners":{"raw":["stephan.donin@gmail.com"]},"dependencies":{"raw":["config","js-yaml"]},"id":{"raw":"tokenauthentication"},"_meta":{"score":0.028970243}},{"name":{"raw":"application-dashboard"},"version":{"raw":"0.1.0"},"license":{"raw":["BSD"]},"description":{"raw":"Collect custom application stats and broadcast them via socket.io to html dashboard. "},"homepage":{"raw":"https://github.com/HeikoR/ApplicationDashboard"},"created":{"raw":"2013-02-22T07:22:07.694Z"},"modified":{"raw":"2013-02-22T07:22:12.080Z"},"keywords":{"raw":["stats","dashboard"]},"repository":{"raw":["https://github.com/HeikoR/ApplicationDashboard"]},"owners":{"raw":["h.risser@gmail.com"]},"dependencies":{"raw":["socket.io","express"]},"id":{"raw":"application-dashboard"},"_meta":{"score":0.02881171}},{"name":{"raw":"tokauth"},"version":{"raw":"0.0.1"},"license":{"raw":["BSD"]},"description":{"raw":"This module links an authentication function with a token generation"},"created":{"raw":"2013-03-05T02:28:31.091Z"},"modified":{"raw":"2013-03-05T02:28:33.648Z"},"keywords":{"raw":["authentication","token"]},"repository":{"raw":["https://github.com/stouf/node-tokauth"]},"owners":{"raw":["stephan.donin@gmail.com"]},"dependencies":{"raw":null},"id":{"raw":"tokauth"},"_meta":{"score":0.026198026}},{"name":{"raw":"bld"},"version":{"raw":"1.1.0"},"license":{"raw":["BSD"]},"description":{"raw":"manifest-based builds"},"created":{"raw":"2013-01-23T18:21:08.227Z"},"modified":{"raw":"2013-01-23T18:26:12.154Z"},"keywords":{"raw":["build","concat","manifest"]},"repository":{"raw":["https://github.com/itsjoesullivan/bld"]},"owners":{"raw":["itsjoesullivan@gmail.com"]},"dependencies":{"raw":null},"id":{"raw":"bld"},"_meta":{"score":0.026121652}}],"_group_key":"BSD"}]} 30 | -------------------------------------------------------------------------------- /fixtures/host-2376rb.api.swiftype.com-443/multi_search: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/node-modules/multi_search.json 2 | authorization: Bearer api-hean6g8dmxnm2shqqiag757a 3 | content-type: application/json 4 | x-swiftype-client: elastic-app-search-javascript 5 | x-swiftype-client-version: 8.13.0 6 | accept: */* 7 | accept-encoding: gzip,deflate 8 | body: {\"queries\":[{\"query\":\"cat\",\"page\":{\"size\":1}},{\"query\":\"dog\"}]} 9 | 10 | HTTP/1.1 200 OK 11 | date: Mon, 22 Oct 2018 16:53:33 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | etag: W/"95fc8821dc0f0d88f414ba8cda6e41e2" 21 | cache-control: max-age=0, private, must-revalidate 22 | x-request-id: 5da20f6cc47205df9ddce06c6efb1a18 23 | x-runtime: 0.141974 24 | x-swiftype-frontend-datacenter: dal05 25 | x-swiftype-frontend-node: web01.dal05 26 | x-swiftype-edge-datacenter: dal05 27 | x-swiftype-edge-node: web01.dal05 28 | 29 | [{"meta":{"warnings":[],"page":{"current":1,"total_pages":642,"total_results":642,"size":1},"request_id":"5da20f6cc47205df9ddce06c6efb1a18"},"results":[{"license":{"raw":["BSD"]},"name":{"raw":"source-map-cat"},"repository":{"raw":null},"created":{"raw":"2013-01-23T04:51:39.995Z"},"dependencies":{"raw":["coffee-script","argparse","source-map"]},"keywords":{"raw":null},"description":{"raw":"WIP cat for JS source maps."},"modified":{"raw":"2013-01-23T04:51:42.782Z"},"id":{"raw":"source-map-cat"},"version":{"raw":"0.0.0"},"owners":{"raw":["gregg@aweber.com"]},"_meta":{"score":18.773478}}]},{"meta":{"warnings":[],"page":{"current":1,"total_pages":1,"total_results":9,"size":10},"request_id":"5da20f6cc47205df9ddce06c6efb1a18"},"results":[{"license":{"raw":["MIT"]},"name":{"raw":"bulldog"},"repository":{"raw":["https://github.com/fernetjs/bulldogjs"]},"created":{"raw":"2012-10-09T01:10:48.905Z"},"dependencies":{"raw":["request","cheerio"]},"keywords":{"raw":["bulldog","bulldogjs","watcher","web watcher","site watcher"]},"description":{"raw":"Release some Bulldogs to watch internet for you"},"modified":{"raw":"2012-10-09T01:10:56.277Z"},"id":{"raw":"bulldog"},"version":{"raw":"0.1.0"},"owners":{"raw":["social@fernetjs.com"]},"_meta":{"score":0.17133085}},{"license":{"raw":null},"name":{"raw":"Squirrel"},"repository":{"raw":["https://github.com/pomelo-modules/data-sync"]},"created":{"raw":"2012-02-15T09:48:45.804Z"},"dependencies":{"raw":null},"keywords":{"raw":null},"description":{"raw":"synchronize data between logic layer and persistence layer"},"modified":{"raw":"2012-02-15T09:48:53.668Z"},"id":{"raw":"Squirrel"},"version":{"raw":"0.0.1"},"owners":{"raw":["dubodog@gmail.com"]},"_meta":{"score":0.09943935}},{"license":{"raw":null},"name":{"raw":"odata-cli"},"repository":{"raw":["https://github.com/machadogj/node-odata-cli"]},"created":{"raw":"2012-07-27T18:47:51.181Z"},"dependencies":{"raw":["request","node.extend"]},"keywords":{"raw":null},"description":{"raw":"odata client for node.js"},"modified":{"raw":"2012-11-08T16:05:41.225Z"},"id":{"raw":"odata-cli"},"version":{"raw":"0.0.4"},"owners":{"raw":["machadogj@gmail.com"]},"_meta":{"score":0.082790785}},{"license":{"raw":["BSD"]},"name":{"raw":"winston-mailer"},"repository":{"raw":["https://github.com/machadogj/node-winston-mailer"]},"created":{"raw":"2013-01-10T20:10:30.401Z"},"dependencies":{"raw":null},"keywords":{"raw":["winston","email","mail","mailer"]},"description":{"raw":"Winston transport based on mailer. It buffers errors, and packs them in one email if necessary."},"modified":{"raw":"2013-01-10T20:10:32.828Z"},"id":{"raw":"winston-mailer"},"version":{"raw":"0.0.1"},"owners":{"raw":["machadogj@gmail.com"]},"_meta":{"score":0.077640004}},{"license":{"raw":null},"name":{"raw":"acs-cli"},"repository":{"raw":["{\"type\":\"git\",\"url\":\"https://bitbucket.org/tellagostudios/node-acs-cli.git\"}"]},"created":{"raw":"2012-07-31T18:52:42.198Z"},"dependencies":{"raw":["request","odata-cli"]},"keywords":{"raw":null},"description":{"raw":"node.js client for azure acs management service."},"modified":{"raw":"2012-08-02T13:43:42.049Z"},"id":{"raw":"acs-cli"},"version":{"raw":"0.0.1"},"owners":{"raw":["machadogj@gmail.com"]},"_meta":{"score":0.077640004}},{"license":{"raw":null},"name":{"raw":"console.io-client"},"repository":{"raw":["https://github.com/TellagoDevLabs/node-console.io-client"]},"created":{"raw":"2012-10-01T22:18:02.952Z"},"dependencies":{"raw":["socket.io-client"]},"keywords":{"raw":null},"description":{"raw":"Client package for console.io (https://github.com/TellagoDevLabs/node-console.io)."},"modified":{"raw":"2012-10-26T19:29:57.456Z"},"id":{"raw":"console.io-client"},"version":{"raw":"0.0.4"},"owners":{"raw":["machadogj@gmail.com"]},"_meta":{"score":0.077640004}},{"license":{"raw":null},"name":{"raw":"couch-ar"},"repository":{"raw":null},"homepage":{"raw":"https://github.com/scottburch/couch-ar"},"created":{"raw":"2011-02-03T01:29:24.057Z"},"dependencies":{"raw":["cradle"]},"keywords":{"raw":null},"description":{"raw":"active record for CouchDB"},"modified":{"raw":"2012-03-10T20:12:54.478Z"},"id":{"raw":"couch-ar"},"version":{"raw":"0.3.4"},"owners":{"raw":["scott@bulldoginfo.com"]},"_meta":{"score":0.06188738}},{"license":{"raw":null},"name":{"raw":"jalapeno"},"repository":{"raw":["https://github.com/gekitz/jalapeno"]},"created":{"raw":"2012-08-02T20:47:29.279Z"},"dependencies":{"raw":["watch","mkdirp"]},"keywords":{"raw":["jalapeno","watchdog","file copy"]},"description":{"raw":"Checkout http://github/gekitz/jalapeno for further details"},"modified":{"raw":"2012-08-02T20:47:31.161Z"},"id":{"raw":"jalapeno"},"version":{"raw":"0.1.1"},"owners":{"raw":["georgkitz@gmail.com"]},"_meta":{"score":0.05774788}},{"license":{"raw":null},"name":{"raw":"prng-parkmiller-js"},"repository":{"raw":["{\"type\":\"git\",\"url\":\"git://github.com:odogono/prng-parkmiller-js.git\"}"]},"homepage":{"raw":"https://github.com/odogono/prng-parkmiller-js/"},"created":{"raw":"2012-05-08T11:03:10.224Z"},"dependencies":{"raw":null},"keywords":{"raw":["random","number","generator","park-miller","prng"]},"description":{"raw":"Park-Miller-Carta pseudo-random number generator library"},"modified":{"raw":"2012-05-08T11:07:48.042Z"},"id":{"raw":"prng-parkmiller-js"},"version":{"raw":"0.1.1"},"owners":{"raw":["alex@opendoorgonorth.com"]},"_meta":{"score":0.040761493}}]}] 30 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import ResultList from "./result_list"; 4 | import Filters from "./filters"; 5 | import { request } from "./request.js"; 6 | 7 | const SEARCH_TYPES = { 8 | SEARCH: "SEARCH", 9 | MULTI_SEARCH: "MULTI_SEARCH" 10 | }; 11 | 12 | /** 13 | * Omit a single key from an object 14 | */ 15 | function omit(obj, keyToOmit) { 16 | if (!obj) return; 17 | const { [keyToOmit]: _, ...rest } = obj; 18 | return rest; 19 | } 20 | 21 | function flatten(arrayOfArrays) { 22 | return [].concat.apply([], arrayOfArrays); 23 | } 24 | 25 | function formatResultsJSON(json) { 26 | return new ResultList(json.results, omit(json, "results")); 27 | } 28 | 29 | function handleErrorResponse({ response, json }) { 30 | if (!response.ok) { 31 | const message = Array.isArray(json) 32 | ? ` ${flatten(json.map(response => response.errors)).join(", ")}` 33 | : `${json.errors ? " " + json.errors : ""}`; 34 | throw new Error(`[${response.status}]${message}`); 35 | } 36 | return json; 37 | } 38 | 39 | export default class Client { 40 | constructor( 41 | hostIdentifier, 42 | searchKey, 43 | engineName, 44 | { endpointBase = "", cacheResponses = true, additionalHeaders } = {} 45 | ) { 46 | this.additionalHeaders = additionalHeaders; 47 | this.searchKey = searchKey; 48 | this.cacheResponses = cacheResponses; 49 | this.engineName = engineName; 50 | this.apiEndpoint = endpointBase 51 | ? `${endpointBase}/api/as/v1/` 52 | : `https://${hostIdentifier}.api.swiftype.com/api/as/v1/`; 53 | this.searchPath = `engines/${this.engineName}/search`; 54 | this.multiSearchPath = `engines/${this.engineName}/multi_search`; 55 | this.querySuggestionPath = `engines/${this.engineName}/query_suggestion`; 56 | this.clickPath = `engines/${this.engineName}/click`; 57 | } 58 | 59 | /** 60 | * Sends a query suggestion request to the Elastic App Search Api 61 | * 62 | * @param {String} query String that is used to perform a query suggest. 63 | * @param {Object} options Object used for configuring the query suggest, like 'types' or 'size' 64 | * @returns {Promise} a Promise that returns results, otherwise throws an Error. 65 | */ 66 | querySuggestion(query, options = {}) { 67 | const params = Object.assign({ query: query }, options); 68 | 69 | return request( 70 | this.searchKey, 71 | this.apiEndpoint, 72 | this.querySuggestionPath, 73 | params, 74 | this.cacheResponses, 75 | { additionalHeaders: this.additionalHeaders } 76 | ).then(handleErrorResponse); 77 | } 78 | 79 | /** 80 | * Sends a search request to the Elastic App Search Api 81 | * 82 | * @param {String} query String, Query, or Object that is used to perform a search request. 83 | * @param {Object} options Object used for configuring the search like search_fields and result_fields 84 | * @returns {Promise} a Promise that returns a {ResultList} when resolved, otherwise throws an Error. 85 | */ 86 | search(query, options = {}) { 87 | const { 88 | disjunctiveFacets, 89 | disjunctiveFacetsAnalyticsTags, 90 | ...validOptions 91 | } = options; 92 | 93 | const params = Object.assign({ query: query }, validOptions); 94 | 95 | if (disjunctiveFacets && disjunctiveFacets.length > 0) { 96 | return this._performDisjunctiveSearch( 97 | params, 98 | disjunctiveFacets, 99 | disjunctiveFacetsAnalyticsTags 100 | ).then(formatResultsJSON); 101 | } 102 | return this._performSearch(params).then(formatResultsJSON); 103 | } 104 | 105 | /** 106 | * Sends multiple search requests to the Elastic App Search Api, using the 107 | * "multi_search" endpoint 108 | * 109 | * @param {Array[Object]} searches searches to send, valid keys are: 110 | * - query: String 111 | * - options: Object (optional) 112 | * @returns {Promise<[ResultList]>} a Promise that returns an array of {ResultList} when resolved, otherwise throws an Error. 113 | */ 114 | multiSearch(searches) { 115 | const params = searches.map(search => ({ 116 | query: search.query, 117 | ...(search.options || {}) 118 | })); 119 | 120 | return this._performSearch( 121 | { queries: params }, 122 | SEARCH_TYPES.MULTI_SEARCH 123 | ).then(responses => responses.map(formatResultsJSON)); 124 | } 125 | 126 | /* 127 | * A disjunctive search, as opposed to a regular search is used any time 128 | * a `disjunctiveFacet` option is provided to the `search` method. A 129 | * a disjunctive facet requires multiple API calls. 130 | * 131 | * Typically: 132 | * 133 | * 1 API call to get the base results 134 | * 1 additional API call to get the "disjunctive" facet counts for each 135 | * facet configured as "disjunctive". 136 | * 137 | * The additional API calls are required, because a "disjunctive" facet 138 | * is one where we want the counts for a facet as if there is no filter applied 139 | * to a particular field. 140 | * 141 | * After all queries are performed, we merge the facet values on the 142 | * additional requests into the facet values of the original request, thus 143 | * creating a single response with the disjunctive facet values. 144 | */ 145 | _performDisjunctiveSearch( 146 | params, 147 | disjunctiveFacets, 148 | disjunctiveFacetsAnalyticsTags = ["Facet-Only"] 149 | ) { 150 | const baseQueryPromise = this._performSearch(params); 151 | 152 | const filters = new Filters(params.filters); 153 | const appliedFilers = filters.getListOfAppliedFilters(); 154 | const listOfAppliedDisjunctiveFilters = appliedFilers.filter(filter => { 155 | return disjunctiveFacets.includes(filter); 156 | }); 157 | 158 | if (!listOfAppliedDisjunctiveFilters.length) { 159 | return baseQueryPromise; 160 | } 161 | 162 | const page = params.page || {}; 163 | 164 | // We intentionally drop passed analytics tags here so that we don't get 165 | // double counted search analytics in the dashboard from disjunctive 166 | // calls 167 | const analytics = params.analytics || {}; 168 | analytics.tags = disjunctiveFacetsAnalyticsTags; 169 | 170 | const disjunctiveQueriesPromises = listOfAppliedDisjunctiveFilters.map( 171 | appliedDisjunctiveFilter => { 172 | return this._performSearch({ 173 | ...params, 174 | filters: filters.removeFilter(appliedDisjunctiveFilter).filtersJSON, 175 | record_analytics: false, 176 | page: { 177 | ...page, 178 | // Set this to 0 for performance, since disjunctive queries 179 | // don't need results 180 | size: 0 181 | }, 182 | analytics, 183 | facets: { 184 | [appliedDisjunctiveFilter]: params.facets[appliedDisjunctiveFilter] 185 | } 186 | }); 187 | } 188 | ); 189 | 190 | return Promise.all([baseQueryPromise, ...disjunctiveQueriesPromises]).then( 191 | ([baseQueryResults, ...disjunctiveQueries]) => { 192 | disjunctiveQueries.forEach(disjunctiveQueryResults => { 193 | const [facetName, facetValue] = Object.entries( 194 | disjunctiveQueryResults.facets 195 | )[0]; 196 | baseQueryResults.facets[facetName] = facetValue; 197 | }); 198 | return baseQueryResults; 199 | } 200 | ); 201 | } 202 | 203 | _performSearch(params, searchType = SEARCH_TYPES.SEARCH) { 204 | const searchPath = 205 | searchType === SEARCH_TYPES.MULTI_SEARCH 206 | ? this.multiSearchPath 207 | : this.searchPath; 208 | return request( 209 | this.searchKey, 210 | this.apiEndpoint, 211 | `${searchPath}.json`, 212 | params, 213 | this.cacheResponses, 214 | { additionalHeaders: this.additionalHeaders } 215 | ).then(handleErrorResponse); 216 | } 217 | 218 | /** 219 | * Sends a click event to the Elastic App Search Api, to track a click-through event 220 | * 221 | * @param {String} query Query that was used to perform the search request 222 | * @param {String} documentId ID of the document that was clicked 223 | * @param {String} requestId Request_id from search response 224 | * @param {String[]} tags Tags to categorize this request in the Dashboard 225 | * @returns {Promise} An empty Promise, otherwise throws an Error. 226 | */ 227 | click({ query, documentId, requestId, tags = [] }) { 228 | const params = { 229 | query, 230 | document_id: documentId, 231 | request_id: requestId, 232 | tags 233 | }; 234 | 235 | return request( 236 | this.searchKey, 237 | this.apiEndpoint, 238 | `${this.clickPath}.json`, 239 | params, 240 | this.cacheResponses, 241 | { additionalHeaders: this.additionalHeaders } 242 | ).then(handleErrorResponse); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2019 Elasticsearch B.V. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /tests/client.spec.js: -------------------------------------------------------------------------------- 1 | import Client from "../src/client"; 2 | import fetch, { Headers } from "node-fetch"; 3 | import ResultItem from "../src/result_item"; 4 | import Replay from "replay"; 5 | 6 | const hostIdentifier = "host-2376rb"; 7 | const searchKey = "api-hean6g8dmxnm2shqqiag757a"; 8 | const engineName = "node-modules"; 9 | 10 | describe("Client", () => { 11 | beforeAll(() => { 12 | global.Headers = Headers; 13 | global.fetch = fetch; 14 | }); 15 | 16 | const client = new Client(hostIdentifier, searchKey, engineName); 17 | 18 | it("can be instantiated", () => { 19 | expect(client).toBeInstanceOf(Client); 20 | }); 21 | 22 | // localhost_search 23 | it("can be instantiated with options", async () => { 24 | const client = new Client(hostIdentifier, searchKey, engineName, { 25 | endpointBase: "http://localhost.swiftype.com:3002", 26 | cacheResponses: true 27 | }); 28 | 29 | const result = await client.search("cat", {}); 30 | expect(result).toMatchSnapshot(); 31 | }); 32 | 33 | describe("#querySuggestion", () => { 34 | // Fixture: query_suggestion 35 | it("should suggest queries", async () => { 36 | const result = await client.querySuggestion("cat"); 37 | expect(result).toMatchSnapshot(); 38 | }); 39 | 40 | // Fixture: query_suggestion_with_options 41 | it("should suggest with valid options", async () => { 42 | const result = await client.querySuggestion("cat", { 43 | size: 3, 44 | types: { 45 | documents: { 46 | fields: ["name"] 47 | } 48 | } 49 | }); 50 | expect(result).toMatchSnapshot(); 51 | }); 52 | 53 | // Fixture: query_suggestion_bad_options 54 | it("should reject on a bad option", async () => { 55 | try { 56 | await client.querySuggestion("cat", { bad: "option" }); 57 | } catch (e) { 58 | expect(e).toEqual(new Error("[400] Options contains invalid key: bad")); 59 | } 60 | }); 61 | }); 62 | 63 | describe("#search", () => { 64 | // Fixture: search_simple 65 | it("should query", async () => { 66 | const result = await client.search("cat", {}); 67 | expect(result).toMatchSnapshot(); 68 | }); 69 | 70 | // Fixture: search_missing_query 71 | it("should should reject when given invalid options", async () => { 72 | try { 73 | await client.search(); 74 | } catch (e) { 75 | expect(e).toEqual(new Error("[400] Missing required parameter: query")); 76 | } 77 | }); 78 | 79 | // search_404 80 | it("should reject on a 404", async () => { 81 | const badClient = new Client("invalid", "invalid", "invalid"); 82 | try { 83 | await badClient.search(); 84 | } catch (e) { 85 | expect(e).toEqual(new Error("[404]")); 86 | } 87 | }); 88 | 89 | // Fixture: search_grouped 90 | it("should wrap grouped results in ResultItem", async () => { 91 | const result = await client.search("cat", { 92 | page: { 93 | size: 1 94 | }, 95 | group: { 96 | field: "license" 97 | } 98 | }); 99 | expect(result.results[0].data._group[0]).toBeInstanceOf(ResultItem); 100 | }); 101 | 102 | describe("disjunctive facets", () => { 103 | const config = { 104 | page: { 105 | size: 1 //To make the response fixture manageable 106 | }, 107 | filters: { 108 | license: ["BSD"] 109 | }, 110 | facets: { 111 | license: [{ type: "value", size: 3 }] 112 | } 113 | }; 114 | 115 | const licenseFacetWithFullCounts = [ 116 | { count: 101, value: "MIT" }, 117 | { count: 33, value: "BSD" }, 118 | { count: 3, value: "MIT/X11" } 119 | ]; 120 | 121 | const licenseFacetWithFilteredCounts = [ 122 | { 123 | value: "BSD", // Only BSD values are returned, since we've filtered to BSD 124 | count: 33 125 | } 126 | ]; 127 | 128 | const licenseFacetWithFilteredCountsByDependency = [ 129 | { count: 5, value: "BSD" }, 130 | { count: 3, value: "MIT" }, 131 | { count: 1, value: "GPL" } 132 | ]; 133 | 134 | const dependenciesFacetWithFullCounts = [ 135 | { count: 67, value: "underscore" }, 136 | { count: 49, value: "pkginfo" }, 137 | { count: 48, value: "express" } 138 | ]; 139 | 140 | const dependenciesFacetWithFilteredCounts = [ 141 | { count: 5, value: "request" }, 142 | { count: 5, value: "socket.io" }, 143 | { count: 4, value: "express" } 144 | ]; 145 | 146 | const dependenciesFacetsWithFilteredCountsByLicense = [ 147 | { count: 5, value: "request" }, 148 | { count: 5, value: "socket.io" }, 149 | { count: 4, value: "express" } 150 | ]; 151 | 152 | // Fixture: search_filter_and_facet 153 | it("returns filtered facet values when facet is not disjunctive", async () => { 154 | const result = await client.search("cat", config); 155 | expect(result.info.facets.license[0].data).toEqual( 156 | licenseFacetWithFilteredCounts 157 | ); 158 | }); 159 | 160 | // Fixture: search_filter_and_facet 161 | // Fixture: search_with_license_facet 162 | it("returns facet counts as if filter is not applied and facet is disjunctive", async () => { 163 | const result = await client.search("cat", { 164 | ...config, 165 | disjunctiveFacets: ["license"] 166 | }); 167 | 168 | expect(result.info.facets.license[0].data).toEqual( 169 | licenseFacetWithFullCounts 170 | ); 171 | }); 172 | 173 | // Fixture: disjunctive_license 174 | it("returns filtered facet values if facet is disjunctive, but no corresponding filter is applied", async () => { 175 | const result = await client.search("cat", { 176 | ...config, 177 | filters: {}, 178 | disjunctiveFacets: ["license"] 179 | }); 180 | 181 | expect(result.info.facets.license[0].data).toEqual( 182 | licenseFacetWithFullCounts 183 | ); 184 | }); 185 | 186 | // Fixture: search_multi_facet 187 | it("will return full results when multiple disjunctive facets, but no filters", async () => { 188 | const result = await client.search("cat", { 189 | page: { size: 1 }, 190 | facets: { 191 | license: [{ type: "value", size: 3 }], 192 | dependencies: [{ type: "value", size: 3 }] 193 | }, 194 | disjunctiveFacets: ["license", "dependencies"] 195 | }); 196 | 197 | expect(result.info.facets.license[0].data).toEqual( 198 | licenseFacetWithFullCounts 199 | ); 200 | expect(result.info.facets.dependencies[0].data).toEqual( 201 | dependenciesFacetWithFullCounts 202 | ); 203 | }); 204 | 205 | // Fixture: disjunctive_license 206 | // Fixture: search_filter_and_multi_facet 207 | it("will return only one set of filtered facet counts when multiple disjunctive facets, with only one filter", async () => { 208 | const result = await client.search("cat", { 209 | ...config, 210 | filters: { 211 | license: "BSD" 212 | }, 213 | facets: { 214 | license: [{ type: "value", size: 3 }], 215 | dependencies: [{ type: "value", size: 3 }] 216 | }, 217 | disjunctiveFacets: ["license", "dependencies"] 218 | }); 219 | 220 | expect(result.info.facets.license[0].data).toEqual( 221 | licenseFacetWithFullCounts 222 | ); 223 | expect(result.info.facets.dependencies[0].data).toEqual( 224 | dependenciesFacetWithFilteredCounts 225 | ); 226 | }); 227 | 228 | // Fixture: disjunctive_license_override_tags 229 | // Fixture: search_filter_and_multi_facet_with_tags 230 | it("will not pass tags through on disjunctive queries", async () => { 231 | // Note, this is tested implicitly by using the same disjunctive fixture as the previous test. This 232 | // ensures that tags are not passed through. If they were, this test would fail as no 233 | // fixture would match. 234 | 235 | await client.search("cat", { 236 | ...config, 237 | analytics: { 238 | tags: ["SERP"] 239 | }, 240 | filters: { 241 | license: "BSD" 242 | }, 243 | facets: { 244 | license: [{ type: "value", size: 3 }], 245 | dependencies: [{ type: "value", size: 3 }] 246 | }, 247 | disjunctiveFacets: ["license", "dependencies"] 248 | }); 249 | }); 250 | 251 | // Fixture: disjunctive_license_with_override_tags 252 | // Fixture: Fixture: search_filter_and_multi_facet_with_tags 253 | it("will accept an alternative analytics tag for disjunctive queries", async () => { 254 | await client.search("cat", { 255 | ...config, 256 | analytics: { 257 | tags: ["SERP"] 258 | }, 259 | filters: { 260 | license: "BSD" 261 | }, 262 | facets: { 263 | license: [{ type: "value", size: 3 }], 264 | dependencies: [{ type: "value", size: 3 }] 265 | }, 266 | disjunctiveFacetsAnalyticsTags: ["FromSERP", "Disjunctive"], 267 | disjunctiveFacets: ["license", "dependencies"] 268 | }); 269 | }); 270 | 271 | // Fixture: disjunctive_license_also_deps 272 | // Fixture: disjunctive_deps_also_license 273 | // Fixture: search_multi_filter_multi_facet 274 | it("will return both sets of filtered facet counts when multiple disjunctive facets and both are filtered", async () => { 275 | const result = await client.search("cat", { 276 | ...config, 277 | filters: { 278 | all: [{ license: "BSD" }, { dependencies: "socket.io" }] 279 | }, 280 | facets: { 281 | license: [{ type: "value", size: 3 }], 282 | dependencies: [{ type: "value", size: 3 }] 283 | }, 284 | disjunctiveFacets: ["license", "dependencies"] 285 | }); 286 | 287 | expect(result.info.facets.license[0].data).toEqual( 288 | licenseFacetWithFilteredCountsByDependency 289 | ); 290 | expect(result.info.facets.dependencies[0].data).toEqual( 291 | dependenciesFacetsWithFilteredCountsByLicense 292 | ); 293 | }); 294 | 295 | // Fixture: disjunctive_deps_also_license_no_array_syntax 296 | // Fixture: disjunctive_license_also_deps 297 | // Fixture: search_multi_filter_multi_facet_no_array_syntax 298 | it("works when facets don't use array syntax", async () => { 299 | const result = await client.search("cat", { 300 | ...config, 301 | filters: { 302 | all: [{ license: "BSD" }, { dependencies: "socket.io" }] 303 | }, 304 | facets: { 305 | license: { type: "value", size: 3 }, 306 | dependencies: [{ type: "value", size: 3 }] 307 | }, 308 | disjunctiveFacets: ["license", "dependencies"] 309 | }); 310 | 311 | expect(result.info.facets.license[0].data).toEqual( 312 | licenseFacetWithFilteredCountsByDependency 313 | ); 314 | expect(result.info.facets.dependencies[0].data).toEqual( 315 | dependenciesFacetsWithFilteredCountsByLicense 316 | ); 317 | }); 318 | }); 319 | }); 320 | 321 | describe("#multiSearch", () => { 322 | function subject({ 323 | firstOptions = { page: { size: 1 } }, 324 | secondOptions = {} 325 | } = {}) { 326 | return client.multiSearch([ 327 | { 328 | query: "cat", 329 | options: firstOptions 330 | }, 331 | { 332 | query: "dog", 333 | options: secondOptions 334 | } 335 | ]); 336 | } 337 | 338 | // Fixture: multi_search 339 | it("should perform multi search", async () => { 340 | expect(await subject()).toMatchSnapshot(); 341 | }); 342 | 343 | // Fixture: multi_search_error 344 | it("should pass through error messages", async () => { 345 | let error; 346 | try { 347 | await subject({ 348 | firstOptions: { invalid: "parameter" }, 349 | secondOptions: { another: "parameter" } 350 | }); 351 | } catch (e) { 352 | error = e; 353 | } 354 | expect(error).toEqual( 355 | new Error( 356 | "[400] Options contains invalid key: invalid, Options contains invalid key: another" 357 | ) 358 | ); 359 | }); 360 | }); 361 | 362 | describe("#click", () => { 363 | // Fixture: click_ok 364 | it("should resolve", async () => { 365 | const result = await client.click({ 366 | query: "Cat", 367 | documentId: "rex-cli", 368 | requestId: "8b55561954484f13d872728f849ffd22", 369 | tags: ["Cat"] 370 | }); 371 | expect(result).toMatchSnapshot(); 372 | }); 373 | 374 | // Fixture: click_no_tags 375 | it("should resolve if no tags are provided", async () => { 376 | const result = await client.click({ 377 | query: "Cat", 378 | documentId: "rex-cli", 379 | requestId: "8b55561954484f13d872728f849ffd22" 380 | }); 381 | expect(result).toMatchSnapshot(); 382 | }); 383 | 384 | // Fixture: click_no_options 385 | it("should should reject when given invalid options", async () => { 386 | try { 387 | await client.click({}); 388 | } catch (e) { 389 | expect(e).toEqual(new Error("[400] Missing required parameter: query")); 390 | } 391 | }); 392 | 393 | // Fixture: click_404 394 | it("should reject on a 404", async () => { 395 | const badClient = new Client("invalid", "invalid", "invalid"); 396 | try { 397 | await badClient.click({}); 398 | } catch (e) { 399 | expect(e).toEqual(new Error("[404]")); 400 | } 401 | }); 402 | 403 | // Fixture: additional_headers 404 | it("should pass along additional headers", async () => { 405 | const headerClient = new Client(hostIdentifier, searchKey, engineName, { 406 | additionalHeaders: { "Content-Type": "bogus/format" } 407 | }); 408 | await headerClient.search("cat", {}); 409 | // REPLAY will fail this spec if the additional helper is not sent 410 | expect(true).toBe(true); 411 | }); 412 | }); 413 | }); 414 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Elastic App Search Logo

2 | 3 |

CircleCI buidl

4 | 5 | > A first-party JavaScript client for building excellent, relevant search experiences with [Elastic App Search](https://www.elastic.co/products/app-search). 6 | 7 | ## Contents 8 | 9 | - [Getting started](#getting-started-) 10 | - [Versioning](#versioning) 11 | - [Browser support](#browser-support) 12 | - [Usage](#usage) 13 | - [Running tests](#running-tests) 14 | - [Development](#development) 15 | - [FAQ](#faq-) 16 | - [Contribute](#contribute-) 17 | - [License](#license-) 18 | 19 | --- 20 | 21 | ## Getting started 🐣 22 | 23 | ### Install from a CDN 24 | 25 | The easiest way to install this client is to simply include the built distribution from the [jsDelivr](https://www.jsdelivr.com/) CDN. 26 | 27 | ```html 28 | 29 | ``` 30 | 31 | This will make the client available globally at: 32 | 33 | ```javascript 34 | window.ElasticAppSearch; 35 | ``` 36 | 37 | ### Install from NPM 38 | 39 | This package can also be installed with `npm` or `yarn`. 40 | 41 | ``` 42 | npm install --save @elastic/app-search-javascript 43 | ``` 44 | 45 | The client could then be included into your project like follows: 46 | 47 | ```javascript 48 | // CommonJS 49 | var ElasticAppSearch = require("@elastic/app-search-javascript"); 50 | 51 | // ES 52 | import * as ElasticAppSearch from "@elastic/app-search-javascript"; 53 | ``` 54 | 55 | ## Versioning 56 | 57 | This client is versioned and released alongside App Search. 58 | 59 | To guarantee compatibility, use the most recent version of this library within the major version of the corresponding App Search implementation. 60 | 61 | For example, for App Search `7.3`, use `7.3` of this library or above, but not `8.0`. 62 | 63 | If you are using the [SaaS version available on swiftype.com](https://app.swiftype.com/as) of App Search, you should use the version 7.5.x of the client. 64 | 65 | ## Browser support 66 | 67 | The client is compatible with all modern browsers. 68 | 69 | Note that this library depends on the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). 70 | 71 | This is not supported by Internet Explorer. If you need backwards compatibility 72 | for Internet Explorer, you'll need to polyfill the Fetch API with something 73 | like https://github.com/github/fetch. 74 | 75 | ## Usage 76 | 77 | ### Setup: Configuring the client and authentication 78 | 79 | Using this client assumes that you have already an instance of [Elastic App Search](https://www.elastic.co/products/app-search) up and running. 80 | 81 | The client is configured using the `searchKey`, `endpointBase`, and `engineName` parameters. 82 | 83 | ```javascript 84 | var client = ElasticAppSearch.createClient({ 85 | searchKey: "search-mu75psc5egt9ppzuycnc2mc3", 86 | endpointBase: "http://127.0.0.1:3002", 87 | engineName: "favorite-videos" 88 | }); 89 | ``` 90 | 91 | \* Please note that you should only ever use a **Public Search Key** within Javascript code on the browser. By default, your account should have a Key prefixed with `search-` that is read-only. More information can be found in the [documentation](https://swiftype.com/documentation/app-search/authentication). 92 | 93 | ### Swiftype.com App Search users: 94 | 95 | When using the [SaaS version available on swiftype.com](https://app.swiftype.com/as) of App Search, you can configure the client using your `hostIdentifier` instead of the `endpointBase` parameter. 96 | The `hostIdentifier` can be found within the [Credentials](https://app.swiftype.com/as#/credentials) menu. 97 | 98 | ```javascript 99 | var client = ElasticAppSearch.createClient({ 100 | hostIdentifier: "host-c5s2mj", 101 | searchKey: "search-mu75psc5egt9ppzuycnc2mc3", 102 | engineName: "favorite-videos" 103 | }); 104 | ``` 105 | 106 | ### List of configuration options: 107 | 108 | | Option | Required | Description | 109 | | ----------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 110 | | hostIdentifier | No | Your **Host Identifier**, should start with `host-`. Required unless explicitly setting `endpointBase` | 111 | | searchKey | No | Your **Public Search Key**. It should start with `search-`.

NOTE: This is not _technically_ required, but in 99% of cases it should be provided. There is a small edge case for not providing this, mainly useful for internal App Search usage, where this can be ommited in order to leverage App Search's session based authentication. | 112 | | engineName | Yes | | 113 | | endpointBase | No | Overrides the base of the App Search API endpoint completely. Useful when proxying the App Search API, developing against a local server, or a Self-Managed or Cloud Deployment. Ex. "http://localhost:3002" | 114 | | cacheResponses | No | Whether or not API responses should be cached. Default: `true`. | 115 | | additionalHeaders | No | An Object with keys and values that will be converted to header names and values on all API requests | 116 | 117 | ### API Methods 118 | 119 | This client is a thin interface to the Elastic App Search API. Additional details for requests and responses can be 120 | found in the [documentation](https://swiftype.com/documentation/app-search). 121 | 122 | #### Searching 123 | 124 | For the query term `lion`, a search call is constructed as follows: 125 | 126 | ```javascript 127 | var options = { 128 | search_fields: { title: {} }, 129 | result_fields: { id: { raw: {} }, title: { raw: {} } } 130 | }; 131 | 132 | client 133 | .search("lion", options) 134 | .then(resultList => { 135 | resultList.results.forEach(result => { 136 | console.log(`id: ${result.getRaw("id")} raw: ${result.getRaw("title")}`); 137 | }); 138 | }) 139 | .catch(error => { 140 | console.log(`error: ${error}`); 141 | }); 142 | ``` 143 | 144 | Note that `options` supports all options listed here: https://swiftype.com/documentation/app-search/guides/search. 145 | 146 | In addition to the supported options above, we also support the following fields: 147 | 148 | | Name | Type | Description | 149 | | ------------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 150 | | disjunctiveFacets | Array[String] | An array of field names. Every field listed here must also be provided as a facet in the `facet` field. It denotes that a facet should be considered disjunctive. When returning counts for disjunctive facets, the counts will be returned as if no filter is applied on this field, even if one is applied. | 151 | | disjunctiveFacetsAnalyticsTags | Array[String] | Used in conjunction with the `disjunctiveFacets` parameter. Queries will be tagged with "Facet-Only" in the Analytics Dashboard unless specified here. | 152 | 153 | _Response_ 154 | 155 | The search method returns the response wrapped in a `ResultList` type: 156 | 157 | ```javascript 158 | ResultList { 159 | rawResults: [], // List of raw `results` from JSON response 160 | rawInfo: { // Object wrapping the raw `meta` property from JSON response 161 | meta: {} 162 | }, 163 | results: [ResultItem], // List of `results` wrapped in `ResultItem` type 164 | info: { // Currently the same as `rawInfo` 165 | meta: {} 166 | } 167 | } 168 | 169 | ResultItem { 170 | getRaw(fieldName), // Returns the HTML-unsafe raw value for a field, if it exists 171 | getSnippet(fieldName) // Returns the HTML-safe snippet value for a field, if it exists 172 | } 173 | ``` 174 | 175 | #### Query Suggestion 176 | 177 | ```javascript 178 | var options = { 179 | size: 3, 180 | types: { 181 | documents: { 182 | fields: ["name"] 183 | } 184 | } 185 | }; 186 | 187 | client 188 | .querySuggestion("cat", options) 189 | .then(response => { 190 | response.results.documents.forEach(document => { 191 | console.log(document.suggestion); 192 | }); 193 | }) 194 | .catch(error => { 195 | console.log(`error: ${error}`); 196 | }); 197 | ``` 198 | 199 | #### Multi Search 200 | 201 | It is possible to run multiple queries at once using the `multiSearch` method. 202 | 203 | To search for the term `lion` and `tiger`, a search call is constructed as follows: 204 | 205 | ```javascript 206 | var options = { 207 | search_fields: { name: {} }, 208 | result_fields: { id: { raw: {} }, title: { raw: {} } } 209 | }; 210 | 211 | client 212 | .multiSearch([{ query: "node", options }, { query: "java", options }]) 213 | .then(allResults => { 214 | allResults.forEach(resultList => { 215 | resultList.results.forEach(result => { 216 | console.log( 217 | `id: ${result.getRaw("id")} raw: ${result.getRaw("title")}` 218 | ); 219 | }); 220 | }); 221 | }) 222 | .catch(error => { 223 | console.log(`error: ${error}`); 224 | }); 225 | ``` 226 | 227 | #### Clickthrough Tracking 228 | 229 | ```javascript 230 | client 231 | .click({ 232 | query: "lion", 233 | documentId: "1234567", 234 | requestId: "8b55561954484f13d872728f849ffd22", 235 | tags: ["Animal"] 236 | }) 237 | .catch(error => { 238 | console.log(`error: ${error}`); 239 | }); 240 | ``` 241 | 242 | Clickthroughs can be tracked by binding `client.click` calls to click events on individual search result links. 243 | 244 | The following example shows how this can be implemented declaratively by annotating links with class and data attributes. 245 | 246 | ```javascript 247 | document.addEventListener("click", function(e) { 248 | const el = e.target; 249 | if (!el.classList.contains("track-click")) return; 250 | 251 | client.click({ 252 | query: el.getAttribute("data-query"), 253 | documentId: el.getAttribute("data-document-id"), 254 | requestId: el.getAttribute("data-request-id"), 255 | tags: [el.getAttribute("data-tag")] 256 | }); 257 | }); 258 | ``` 259 | 260 | ```html 261 | 269 | Item 1 270 | 271 | ``` 272 | 273 | ## Running tests 274 | 275 | The specs in this project use [node-replay](https://github.com/assaf/node-replay) to capture responses. 276 | 277 | The responses are then checked against Jest snapshots. 278 | 279 | To capture new responses and update snapshots, run: 280 | 281 | ``` 282 | nvm use 283 | REPLAY=record npm run test -u 284 | ``` 285 | 286 | To run tests: 287 | 288 | ``` 289 | nvm use 290 | npm run test 291 | ``` 292 | 293 | ## Development 294 | 295 | ### Node 296 | 297 | You will probably want to install a node version manager, like nvm. 298 | 299 | We depend upon the version of node defined in [.nvmrc](.nvmrc). 300 | 301 | To install and use the correct node version with nvm: 302 | 303 | ``` 304 | nvm install 305 | ``` 306 | 307 | ### Dev Server 308 | 309 | Install dependencies: 310 | 311 | ```bash 312 | nvm use 313 | npm install 314 | ``` 315 | 316 | Build artifacts in `dist` directory: 317 | 318 | ```bash 319 | # This will create files in the `dist` directory 320 | npm run build 321 | ``` 322 | 323 | Add an `index.html` file to your `dist` directory 324 | 325 | ```html 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | ``` 335 | 336 | Run dev server: 337 | 338 | ```bash 339 | # This will serve files in the `dist` directory 340 | npm run dev 341 | ``` 342 | 343 | Navigate to http://127.0.0.1:8080 and execute JavaScript commands through the browser Dev Console. 344 | 345 | ### Build 346 | 347 | ``` 348 | nvm use 349 | npm run build 350 | ``` 351 | 352 | ### Publish 353 | 354 | ``` 355 | nvm use 356 | npm run publish 357 | ``` 358 | 359 | ## FAQ 🔮 360 | 361 | ### What if I need write operations? 362 | 363 | App Search has a first-party [Node.js](https://github.com/elastic/app-search-node) client which supports write operations like indexing. 364 | 365 | ### Where do I report issues with the client? 366 | 367 | If something is not working as expected, please open an [issue](https://github.com/elastic/app-search-javascript/issues/new). 368 | 369 | ### Where can I learn more about App Search? 370 | 371 | Your best bet is to read the [documentation](https://swiftype.com/documentation/app-search). 372 | 373 | ### Where else can I go to get help? 374 | 375 | You can checkout the [Elastic App Search community discuss forums](https://discuss.elastic.co/c/app-search). 376 | 377 | ## Contribute 🚀 378 | 379 | We welcome contributors to the project. Before you begin, a couple notes... 380 | 381 | - Prior to opening a pull request, please create an issue to [discuss the scope of your proposal](https://github.com/elastic/app-search-javascript/issues). 382 | - Please write simple code and concise documentation, when appropriate. 383 | 384 | ## License 📗 385 | 386 | [Apache 2.0](https://github.com/elastic/app-search-javascript/blob/master/LICENSE.txt) © [Elastic](https://github.com/elastic) 387 | 388 | Thank you to all the [contributors](https://github.com/elastic/app-search-javascript/graphs/contributors)! 389 | --------------------------------------------------------------------------------