├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── __mocks__ ├── defaultConfig.js └── index.js ├── __tests__ ├── AppBaseClientTest.js ├── ReactiveSearchTest.js ├── ReactiveSearchV3Test.js ├── bulkTest.js ├── getMappingsTest.js ├── getTest.js ├── msearchTest.js ├── searchTest.js └── updateTest.js ├── jest.config.js ├── package-scripts.js ├── package.json ├── rollup.config.js ├── src ├── core │ ├── api │ │ ├── bulk.js │ │ ├── delete.js │ │ ├── get.js │ │ ├── getMappings.js │ │ ├── getSuggestionsv3Api.js │ │ ├── index.js │ │ ├── msearch.js │ │ ├── reactiveSearchApi.js │ │ ├── reactiveSearchv3Api.js │ │ ├── search.js │ │ └── update.js │ ├── index.js │ └── request │ │ └── fetch.js ├── index.d.ts ├── index.js └── utils │ ├── index.js │ └── schema │ ├── elasticsearch.js │ ├── index.js │ └── mongodb.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "modules": "commonjs" 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test 2 | __tests__ 3 | node_modules 4 | coverage 5 | dist 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'airbnb/base', 3 | env: { 4 | browser: true, 5 | es6: true, 6 | node: true, 7 | }, 8 | parser: 'babel-eslint', 9 | rules: { 10 | indent: 0, 11 | 'no-tabs': 0, 12 | 'eol-last': ['error', 'always'], 13 | 'no-underscore-dangle': 0, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | coverage 4 | dist 5 | lib 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '9' 4 | - '8' 5 | - '6' 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.trimTrailingWhitespace": true, 3 | "files.insertFinalNewline": true, 4 | "files.trimFinalNewlines": true, 5 | // to resolve flow issues - Refer: https://github.com/flowtype/flow-for-vscode#setup 6 | "javascript.validate.enable": false, 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.tslint": true, 9 | "source.fixAll.eslint": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kuldeep Saxena 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ![Build Status Image](https://img.shields.io/badge/build-passing-brightgreen.svg) 2 | 3 |

4 | appbase-js 5 |
6 | appbase-js 7 |
8 |

9 | 10 | [appbase-js](https://github.com/appbaseio/appbase-js) is a universal JavaScript client library for working with the appbase.io database, for Node.JS and Javascript (browser UMD build is in the [dist/](https://github.com/appbaseio/appbase-js/tree/master/dist) directory); compatible with [elasticsearch.js](https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html). 11 | 12 | An up-to-date documentation for Node.JS API is available at http://docs.appbase.io/javascript/quickstart. 13 | 14 | ## TOC 15 | 16 | 1. **[appbase-js: Intro](#1-appbase-js-intro)** 17 | 2. **[Features](#2-features)** 18 | 3. **[Live Examples](#3-live-examples)** 19 | 4. **[Installation](#4-installation)** 20 | 5. **[Docs Manual](#5-docs-manual)** 21 | 6. **[Other Projects You Might Like](#6-other-projects-you-might-like)** 22 | 23 |
24 | ## 1. appbase-js: Intro 25 | 26 | [appbase-js](https://github.com/appbaseio/appbase-js) is a universal JavaScript client library for working with the appbase.io database. 27 | 28 | ## 2. Features 29 | 30 | It can: 31 | 32 | - Index new documents or update / delete existing ones. 33 | - Work universally with Node.JS, Browser, and React Native. 34 | 35 | It can't: 36 | 37 | - Configure mappings, change analyzers, or capture snapshots. All these are provided by [elasticsearch.js](https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html) - the official Elasticsearch JS client library. 38 | 39 | [Appbase.io - the database service](https://appbase.io) is opinionated about cluster setup and hence doesn't support the Elasticsearch devops APIs. See [rest.appbase.io](https://rest.appbase.io) for a full reference on the supported APIs. 40 | 41 | ## 3. Live Examples 42 | 43 |
44 |

Check out the Live interactive Examples at reactiveapps.io.

46 |
47 | 48 | [![image](https://user-images.githubusercontent.com/57627350/128456523-7c964efc-8940-43bc-b3b5-142fc40bdf11.png)](https://docs.appbase.io/api/examples/rest/) 49 | 50 | ## 4. Installation 51 | 52 | We will fetch and install the **appbase-js** lib using npm. `4.0.0-beta` is the most current version. 53 | 54 | ```js 55 | npm install appbase-js 56 | ``` 57 | 58 | Adding it in the browser should be a one line script addition. 59 | 60 | ```html 61 | 65 | ``` 66 | 67 | Alternatively, a UMD build of the library can be used directly from [jsDelivr](https://cdn.jsdelivr.net/npm/appbase-js/dist/). 68 | 69 | To write data to [appbase.io](https://appbase.io), we need to first create a reference object. We do this by passing the appbase.io API URL, app name, and credentials into the `Appbase` constructor: 70 | 71 | ```js 72 | var appbaseRef = Appbase({ 73 | url: "https://appbase-demo-ansible-abxiydt-arc.searchbase.io", 74 | app: "good-books-demo", 75 | credentials: "c84fb24cbe08:db2a25b5-1267-404f-b8e6-cf0754953c68", 76 | }); 77 | ``` 78 | 79 | **OR** 80 | 81 | ```js 82 | var appbaseRef = Appbase({ 83 | url: "https://c84fb24cbe08:db2a25b5-1267-404f-b8e6-cf0754953c68@appbase-demo-ansible-abxiydt-arc.searchbase.io", 84 | app: "good-books-demo", 85 | }); 86 | ``` 87 | 88 | Credentials can also be directly passed as a part of the API URL. 89 | 90 | ## 5. Docs Manual 91 | 92 | For a complete API reference, check out [JS API Ref doc](http://docs.appbase.io/javascript/apireference). 93 | 94 | ## 6. Other Projects You Might Like 95 | 96 | - [**arc**](https://github.com/appbaseio/arc) API Gateway for ElasticSearch (Out of the box Security, Rate Limit Features, Record Analytics and Request Logs). 97 | 98 | - [**searchbox**](https://github.com/appbaseio/searchox) A lightweight and performance focused searchbox UI libraries to query and display results from your ElasticSearch app (aka index). 99 | 100 | - **Vanilla JS** - (~16kB Minified + Gzipped) 101 | - **React** - (~30kB Minified + Gzipped) 102 | - **Vue** - (~22kB Minified + Gzipped) 103 | 104 | - [**dejavu**](https://github.com/appbaseio/dejavu) allows viewing raw data within an appbase.io (or Elasticsearch) app. **Soon to be released feature:** An ability to import custom data from CSV and JSON files, along with a guided walkthrough on applying data mappings. 105 | 106 | - [**mirage**](https://github.com/appbaseio/mirage) ReactiveSearch components can be extended using custom Elasticsearch queries. For those new to Elasticsearch, Mirage provides an intuitive GUI for composing queries. 107 | 108 | - [**ReactiveMaps**](https://github.com/appbaseio/reactivesearch/tree/next/packages/maps) is a similar project to Reactive Search that allows building realtime maps easily. 109 | 110 | - [**reactivesearch**](https://github.com/appbaseio/reactivesearch) UI components library for Elasticsearch: Available for React and Vue. 111 | 112 | [⬆ Back to Top](#top) 113 | 114 | 115 | -------------------------------------------------------------------------------- /__mocks__/defaultConfig.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | URL: 'https://appbase-demo-ansible-abxiydt-arc.searchbase.io', 3 | APPNAME: 'appbase-js-test-cases', 4 | CREDENTIALS: 'f1a7b4562098:35fed6ff-4a19-4387-a188-7cdfe759c40f', 5 | }; 6 | -------------------------------------------------------------------------------- /__mocks__/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | uuidv4() { 3 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { 4 | const r = (Math.random() * 16) | 0; // eslint-disable-line no-bitwise 5 | 6 | const v = c === 'x' ? r : (r & 0x3) | 0x8; // eslint-disable-line no-bitwise 7 | return v.toString(16); 8 | }); 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /__tests__/AppBaseClientTest.js: -------------------------------------------------------------------------------- 1 | const appbase = require(".."); 2 | 3 | const { uuidv4 } = require("../__mocks__"); 4 | 5 | describe("#AppBase_Client", () => { 6 | test("should create appbase client", () => { 7 | const client = appbase({ 8 | url: "https://appbase-demo-ansible-abxiydt-arc.searchbase.io", 9 | app: "appbasejs-test-app", 10 | credentials: "f1a7b4562098:35fed6ff-4a19-4387-a188-7cdfe759c40f", 11 | }); 12 | expect(client).toEqual({ 13 | url: "appbase-demo-ansible-abxiydt-arc.searchbase.io", 14 | protocol: "https", 15 | app: "appbasejs-test-app", 16 | credentials: "f1a7b4562098:35fed6ff-4a19-4387-a188-7cdfe759c40f", 17 | }); 18 | }); 19 | test("should throw url missing error", () => { 20 | try { 21 | appbase({ 22 | app: "appbasejs-test-app", 23 | credentials: "f1a7b4562098:35fed6ff-4a19-4387-a188-7cdfe759c40f", 24 | }); 25 | expect(true).toBe(false); 26 | } catch (e) { 27 | expect(e.message).toEqual( 28 | "url is required when using the elasticsearch Search backend." 29 | ); 30 | } 31 | }); 32 | test("should throw app missing error", () => { 33 | try { 34 | appbase({ 35 | url: "https://appbase-demo-ansible-abxiydt-arc.searchbase.io", 36 | credentials: "f1a7b4562098:35fed6ff-4a19-4387-a188-7cdfe759c40f", 37 | }); 38 | expect(true).toBe(false); 39 | } catch (e) { 40 | expect(e.message).toEqual( 41 | "app is required when using the elasticsearch Search backend." 42 | ); 43 | } 44 | }); 45 | test("should throw protocol missing error", () => { 46 | try { 47 | appbase({ 48 | url: "appbase-demo-ansible-abxiydt-arc.searchbase.io", 49 | app: "appbasejs-test-app", 50 | credentials: "f1a7b4562098:35fed6ff-4a19-4387-a188-7cdfe759c40f", 51 | }); 52 | expect(true).toBe(false); 53 | } catch (e) { 54 | expect(e.message).toEqual( 55 | "Protocol is not present in url. URL should be of the form https://appbase-demo-ansible-abxiydt-arc.searchbase.io" 56 | ); 57 | } 58 | }); 59 | test("should throw credentials missing error", () => { 60 | try { 61 | appbase({ 62 | url: "https://scalr.api.appbase.io", 63 | app: "appbasejs-test-app", 64 | }); 65 | expect(true).toBe(false); 66 | } catch (e) { 67 | expect(e.message).toEqual( 68 | "Authentication information is not present. Did you add credentials?" 69 | ); 70 | } 71 | }); 72 | test("should set headers", () => { 73 | const client = appbase({ 74 | url: "https://appbase-demo-ansible-abxiydt-arc.searchbase.io", 75 | app: "appbasejs-test-app", 76 | credentials: "f1a7b4562098:35fed6ff-4a19-4387-a188-7cdfe759c40f", 77 | }); 78 | client.setHeaders({ 79 | authorization: "test-authorize", 80 | "x-search-key": "美女", 81 | }); 82 | expect(client).toEqual({ 83 | url: "appbase-demo-ansible-abxiydt-arc.searchbase.io", 84 | protocol: "https", 85 | app: "appbasejs-test-app", 86 | credentials: "f1a7b4562098:35fed6ff-4a19-4387-a188-7cdfe759c40f", 87 | headers: { 88 | authorization: "test-authorize", 89 | "x-search-key": "美女", 90 | }, 91 | }); 92 | }); 93 | test("should set X-Enable-Telemetry header", () => { 94 | const client = appbase({ 95 | url: "https://appbase-demo-ansible-abxiydt-arc.searchbase.io", 96 | app: "appbasejs-test-app", 97 | credentials: "f1a7b4562098:35fed6ff-4a19-4387-a188-7cdfe759c40f", 98 | enableTelemetry: false, 99 | }); 100 | expect(client).toEqual({ 101 | url: "appbase-demo-ansible-abxiydt-arc.searchbase.io", 102 | protocol: "https", 103 | app: "appbasejs-test-app", 104 | credentials: "f1a7b4562098:35fed6ff-4a19-4387-a188-7cdfe759c40f", 105 | enableTelemetry: false, 106 | }); 107 | }); 108 | test("should call transformRequest before fetch", async () => { 109 | const client = appbase({ 110 | url: "https://appbase-demo-ansible-abxiydt-arc.searchbase.io", 111 | app: "appbasejs-test-app", 112 | credentials: "f1a7b4562098:35fed6ff-4a19-4387-a188-7cdfe759c40f", 113 | }); 114 | client.transformRequest = (request) => { 115 | expect(true).toBe(true); 116 | return request; 117 | }; 118 | const id = uuidv4(); 119 | const tweet = { 120 | user: "olivere", 121 | message: "Welcome to Golang and Elasticsearch.", 122 | }; 123 | client.index({ 124 | type: "tweet_transform_error", 125 | id, 126 | body: tweet, 127 | }); 128 | }); 129 | test("should create appbase client with mongodb", () => { 130 | const client = appbase({ 131 | url: "https://us-east-1.aws.webhooks.mongodb-realm.com/api/client/v2.0/app/public-demo-skxjb/service/http_endpoint/incoming_webhook/reactivesearch", 132 | app: "appbasejs-test-app", 133 | mongodb: { 134 | db: "test_db", 135 | collection: "test_collection", 136 | }, 137 | }); 138 | expect(client).toEqual({ 139 | url: "us-east-1.aws.webhooks.mongodb-realm.com/api/client/v2.0/app/public-demo-skxjb/service/http_endpoint/incoming_webhook/reactivesearch", 140 | protocol: "https", 141 | credentials: null, 142 | app: "appbasejs-test-app", 143 | mongodb: { 144 | db: "test_db", 145 | collection: "test_collection", 146 | }, 147 | }); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /__tests__/ReactiveSearchTest.js: -------------------------------------------------------------------------------- 1 | const appbase = require(".."); 2 | const { uuidv4 } = require("../__mocks__"); 3 | const config = require("../__mocks__/defaultConfig"); 4 | 5 | describe("#Search", function () { 6 | let client; 7 | beforeAll(() => { 8 | client = appbase({ 9 | url: config.URL, 10 | app: config.APPNAME, 11 | credentials: config.CREDENTIALS, 12 | }); 13 | }); 14 | 15 | test("should return results", async () => { 16 | var tweet = { 17 | user: "olivere", 18 | message: "Welcome to Golang and Elasticsearch.", 19 | }; 20 | await client.index({ 21 | id: uuidv4(), 22 | body: tweet, 23 | }); 24 | const res = await client.reactiveSearch([ 25 | { 26 | id: "tweet_search", 27 | dataField: ["user"], 28 | size: 10, 29 | value: "olivere", 30 | }, 31 | ]); 32 | expect(res.tweet_search.hits.total.value).toBeGreaterThan(0); 33 | }); 34 | test("Elastic error", async () => { 35 | try { 36 | const res = await client.reactiveSearch([ 37 | { 38 | id: "tweet_search", 39 | dataField: ["user"], 40 | type: "term", 41 | size: 10, 42 | }, 43 | ]); 44 | } catch (e) { 45 | expect(e.tweet_search.status).toEqual(400); 46 | } 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /__tests__/ReactiveSearchV3Test.js: -------------------------------------------------------------------------------- 1 | const appbase = require(".."); 2 | const { uuidv4 } = require("../__mocks__"); 3 | const config = require("../__mocks__/defaultConfig"); 4 | 5 | describe("#Search", function () { 6 | let client; 7 | beforeAll(() => { 8 | client = appbase({ 9 | url: config.URL, 10 | app: config.APPNAME, 11 | credentials: config.CREDENTIALS, 12 | }); 13 | }); 14 | 15 | test("should return results", async () => { 16 | var tweet = { 17 | user: "olivere", 18 | message: "Welcome to Golang and Elasticsearch.", 19 | }; 20 | await client.index({ 21 | id: uuidv4(), 22 | body: tweet, 23 | }); 24 | const res = await client.reactiveSearchv3([ 25 | { 26 | id: "tweet_search", 27 | dataField: ["user"], 28 | size: 10, 29 | value: "olivere", 30 | }, 31 | ]); 32 | expect(res.tweet_search.hits.total.value).toBeGreaterThan(0); 33 | }); 34 | test("Elastic error", async () => { 35 | try { 36 | const res = await client.reactiveSearchv3([ 37 | { 38 | id: "tweet_search", 39 | dataField: ["user"], 40 | type: "term", 41 | size: 10, 42 | }, 43 | ]); 44 | } catch (e) { 45 | expect(e.tweet_search.status).toEqual(400); 46 | } 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /__tests__/bulkTest.js: -------------------------------------------------------------------------------- 1 | const appbase = require(".."); 2 | const { uuidv4 } = require("../__mocks__"); 3 | const config = require("../__mocks__/defaultConfig"); 4 | 5 | describe("#Bulk", function () { 6 | let client; 7 | beforeAll(() => { 8 | client = appbase({ 9 | url: config.URL, 10 | app: config.APPNAME, 11 | credentials: config.CREDENTIALS, 12 | }); 13 | }); 14 | 15 | test("should bulk index one document", async (done) => { 16 | var tweet = { 17 | user: "olivere", 18 | message: "Welcome to Golang and Elasticsearch.", 19 | }; 20 | const id = uuidv4(); 21 | try { 22 | await client.bulk({ 23 | body: [ 24 | { 25 | index: { 26 | _id: id, 27 | }, 28 | }, 29 | tweet, 30 | ], 31 | }); 32 | } catch (e) { 33 | console.error(e); 34 | } 35 | try { 36 | const res = await client.get({ 37 | id: id, 38 | }); 39 | try { 40 | expect(res).toMatchObject({ 41 | _id: id, 42 | found: true, 43 | _source: tweet, 44 | }); 45 | } catch (e) { 46 | return done(e); 47 | } 48 | } catch (e) { 49 | console.error(e); 50 | } 51 | 52 | const deleteRes = await client.delete({ 53 | id: id, 54 | }); 55 | if (deleteRes && deleteRes.result === 'deleted') { 56 | done(); 57 | } else { 58 | done(new Error("Unable to delete data because it was not found")); 59 | } 60 | }); 61 | test("should throw error if body is missing", function () { 62 | try { 63 | client.bulk({}); 64 | } catch (e) { 65 | expect(true).toEqual(true); 66 | } 67 | }); 68 | 69 | test("should throw error when no arguments is passed", function () { 70 | try { 71 | client.bulk(); 72 | } catch (e) { 73 | expect(true).toEqual(true); 74 | } 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /__tests__/getMappingsTest.js: -------------------------------------------------------------------------------- 1 | const appbase = require('..'); 2 | const config = require('../__mocks__/defaultConfig'); 3 | 4 | describe('#Get_Mappings', function() { 5 | let client; 6 | beforeAll(() => { 7 | client = appbase({ 8 | url: config.URL, 9 | app: config.APPNAME, 10 | credentials: config.CREDENTIALS, 11 | }); 12 | }); 13 | 14 | test('should get mappings object', async done => { 15 | const mappings = await client.getMappings(); 16 | if (mappings[client.app].mappings) { 17 | done(); 18 | } else { 19 | done(new Error('Mappings are not present')); 20 | } 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /__tests__/getTest.js: -------------------------------------------------------------------------------- 1 | const appbase = require('..'); 2 | const { uuidv4 } = require('../__mocks__'); 3 | const config = require('../__mocks__/defaultConfig'); 4 | 5 | describe('#Get', function() { 6 | let client; 7 | beforeAll(() => { 8 | client = appbase({ 9 | url: config.URL, 10 | app: config.APPNAME, 11 | credentials: config.CREDENTIALS, 12 | }); 13 | }); 14 | 15 | test('should get one document', async () => { 16 | const tweet = { user: 'olivere', message: 'Welcome to Golang and Elasticsearch.' }; 17 | const id = uuidv4(); 18 | await client.index({ 19 | id, 20 | body: tweet, 21 | }); 22 | const getRes = await client.get({ 23 | id, 24 | }); 25 | expect(getRes.found).toEqual(true); 26 | }); 27 | 28 | test('should get one document: id as number', async () => { 29 | const tweet = { user: 'olivere', message: 'Welcome to Golang and Elasticsearch.' }; 30 | const id = new Date().getTime(); 31 | await client.index({ 32 | id, 33 | body: tweet, 34 | }); 35 | const getRes = await client.get({ 36 | id, 37 | }); 38 | expect(getRes.found).toEqual(true); 39 | }); 40 | 41 | test('should throw error: id as object', async () => { 42 | const tweet = { user: 'olivere', message: 'Welcome to Golang and Elasticsearch.' }; 43 | const id = { key: 'ded' }; 44 | try { 45 | const res = await client.get({ 46 | id, 47 | body: tweet, 48 | }); 49 | expect(true).toBe(false) 50 | } catch(e) { 51 | expect(e.message).toEqual("fields missing: id, ") 52 | } 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /__tests__/msearchTest.js: -------------------------------------------------------------------------------- 1 | const appbase = require('..'); 2 | const { uuidv4 } = require('../__mocks__'); 3 | const config = require('../__mocks__/defaultConfig'); 4 | 5 | describe('#Msearch', function() { 6 | let client; 7 | beforeAll(() => { 8 | client = appbase({ 9 | url: config.URL, 10 | app: config.APPNAME, 11 | credentials: config.CREDENTIALS, 12 | }); 13 | }); 14 | 15 | test('should return results', async () => { 16 | var tweet = { user: 'olivere', message: 'Welcome to Golang and Elasticsearch.' }; 17 | await client.index({ 18 | id: uuidv4(), 19 | body: tweet, 20 | }); 21 | // index second tweet 22 | var penguinTweet = { user: 'penguin', message: 'woot woot!' }; 23 | await client.index({ 24 | id: uuidv4(), 25 | body: penguinTweet, 26 | }); 27 | const res = await client.msearch({ 28 | body: [{}, { query: { match_all: {} } }, {}, { query: { match: { _id: 1 } } }], 29 | }); 30 | expect(res.responses.length).toEqual(2); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /__tests__/searchTest.js: -------------------------------------------------------------------------------- 1 | const appbase = require('..'); 2 | const { uuidv4 } = require('../__mocks__'); 3 | const config = require('../__mocks__/defaultConfig'); 4 | 5 | describe('#Search', function() { 6 | let client; 7 | beforeAll(() => { 8 | client = appbase({ 9 | url: config.URL, 10 | app: config.APPNAME, 11 | credentials: config.CREDENTIALS, 12 | }); 13 | }); 14 | 15 | test('should return results', async () => { 16 | var tweet = { user: 'olivere', message: 'Welcome to Golang and Elasticsearch.' }; 17 | await client.index({ 18 | id: uuidv4(), 19 | body: tweet, 20 | }); 21 | const res = await client.search({ 22 | body: { 23 | query: { 24 | match_all: {}, 25 | }, 26 | }, 27 | }); 28 | expect(res.hits.total.value).toBeGreaterThan(0); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /__tests__/updateTest.js: -------------------------------------------------------------------------------- 1 | const appbase = require('..'); 2 | const { uuidv4 } = require('../__mocks__'); 3 | const config = require('../__mocks__/defaultConfig'); 4 | 5 | describe('#Update', function() { 6 | let client; 7 | beforeAll(() => { 8 | client = appbase({ 9 | url: config.URL, 10 | app: config.APPNAME, 11 | credentials: config.CREDENTIALS, 12 | }); 13 | }); 14 | 15 | test('should update one document', async () => { 16 | const id = uuidv4(); 17 | var tweet = { 18 | user: 'olivere', 19 | message: 'Welcome to Golang and Elasticsearch.', 20 | }; 21 | var tweet2 = { 22 | user: 'olivere', 23 | message: 'This is a new tweet.', 24 | }; 25 | await client.index({ 26 | id, 27 | body: tweet, 28 | }); 29 | const updateRes = await client.update({ 30 | id, 31 | body: { 32 | doc: tweet2 33 | }, 34 | }) 35 | expect(updateRes.result).toEqual('updated'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverageFrom: ['src/**/*.{js}'], 3 | testEnvironment: 'node', 4 | }; 5 | -------------------------------------------------------------------------------- /package-scripts.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | const npsUtils = require("nps-utils"); 3 | 4 | const { 5 | series, concurrent, rimraf, crossEnv, 6 | } = npsUtils; 7 | 8 | module.exports = { 9 | scripts: { 10 | test: { 11 | default: crossEnv('NODE_ENV=test jest --coverage'), 12 | update: crossEnv('NODE_ENV=test jest --coverage'), 13 | watch: crossEnv('NODE_ENV=test jest --watch'), 14 | codeCov: crossEnv( 15 | 'cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js', 16 | ), 17 | }, 18 | build: { 19 | description: 'delete the dist directory and run all builds', 20 | default: series( 21 | rimraf('dist'), 22 | concurrent.nps( 23 | 'compileToes5', 24 | 'build.es', 25 | 'build.cjs', 26 | 'build.umd.main', 27 | 'build.umd.min', 28 | ), 29 | ), 30 | es: { 31 | description: 'run the build with rollup (uses rollup.config.js)', 32 | script: 'rollup --config --environment FORMAT:es', 33 | }, 34 | cjs: { 35 | description: 'run rollup build with CommonJS format', 36 | script: 'rollup --config --environment FORMAT:cjs', 37 | }, 38 | umd: { 39 | min: { 40 | description: 'run the rollup build with sourcemaps', 41 | script: 'rollup --config --sourcemap --environment MINIFY,FORMAT:umd', 42 | }, 43 | main: { 44 | description: 'builds the cjs and umd files', 45 | script: 'rollup --config --sourcemap --environment FORMAT:umd', 46 | }, 47 | }, 48 | andTest: series.nps('build'), 49 | }, 50 | copyTypes: series(npsUtils.copy('dist')), 51 | compileToes5: series(rimraf('lib')), 52 | lint: { 53 | description: 'lint the entire project', 54 | script: 'eslint .', 55 | }, 56 | validate: { 57 | description: 58 | 'This runs several scripts to make sure things look good before committing or on clean install', 59 | default: concurrent.nps('compileToes5', 'lint', 'build.andTest'), 60 | }, 61 | }, 62 | options: { 63 | silent: false, 64 | }, 65 | }; 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appbase-js", 3 | "version": "5.3.4", 4 | "main": "dist/appbase-js.cjs.js", 5 | "jsnext:main": "dist/appbase-js.es.js", 6 | "module": "dist/appbase-js.es.js", 7 | "description": "Appbase.io streaming client lib for Javascript", 8 | "types": "dist/index.d.ts", 9 | "keywords": [ 10 | "appbase", 11 | "elasticsearch", 12 | "search" 13 | ], 14 | "author": "Kuldeep Saxena ", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "babel-cli": "^6.26.0", 18 | "babel-core": "^6.26.3", 19 | "babel-eslint": "^8.2.6", 20 | "babel-jest": "^23.4.2", 21 | "babel-plugin-external-helpers": "^6.22.0", 22 | "babel-preset-env": "^1.7.0", 23 | "eslint": "^5.3.0", 24 | "eslint-config-airbnb": "^17.1.0", 25 | "eslint-plugin-import": "^2.14.0", 26 | "husky": "^0.14.3", 27 | "jest": "^23.5.0", 28 | "lint-staged": "^7.2.2", 29 | "nps": "^5.9.3", 30 | "nps-utils": "^1.7.0", 31 | "rollup": "^0.64.0", 32 | "rollup-plugin-babel": "^3.0.7", 33 | "rollup-plugin-commonjs": "^9.1.0", 34 | "rollup-plugin-copy": "^3.4.0", 35 | "rollup-plugin-node-builtins": "^2.1.2", 36 | "rollup-plugin-node-resolve": "^3.3.0", 37 | "rollup-plugin-replace": "^2.0.0", 38 | "rollup-plugin-terser": "^1.0.1" 39 | }, 40 | "scripts": { 41 | "start": "nps", 42 | "pretest": "nps build", 43 | "test": "nps test", 44 | "precommit": "lint-staged", 45 | "prepare": "npm start validate" 46 | }, 47 | "lint-staged": { 48 | "*.js": [ 49 | "eslint" 50 | ] 51 | }, 52 | "files": [ 53 | "dist", 54 | "lib" 55 | ], 56 | "dependencies": { 57 | "cross-fetch": "^3.1.5", 58 | "querystring": "^0.2.0", 59 | "url-parser-lite": "^0.1.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import { terser } from 'rollup-plugin-terser'; 4 | import replace from 'rollup-plugin-replace'; 5 | import builtins from 'rollup-plugin-node-builtins'; 6 | import babel from 'rollup-plugin-babel'; 7 | import copy from 'rollup-plugin-copy'; 8 | import pkg from './package.json'; 9 | 10 | const minify = process.env.MINIFY; 11 | const format = process.env.FORMAT; 12 | const es = format === 'es'; 13 | const umd = format === 'umd'; 14 | const cjs = format === 'cjs'; 15 | 16 | let output; 17 | 18 | if (es) { 19 | output = { file: 'dist/appbase-js.es.js', format: 'es' }; 20 | } else if (umd) { 21 | if (minify) { 22 | output = { 23 | file: 'dist/appbase-js.umd.min.js', 24 | format: 'umd', 25 | }; 26 | } else { 27 | output = { file: 'dist/appbase-js.umd.js', format: 'umd' }; 28 | } 29 | } else if (cjs) { 30 | output = { file: 'dist/appbase-js.cjs.js', format: 'cjs' }; 31 | } else if (format) { 32 | throw new Error(`invalid format specified: "${format}".`); 33 | } else { 34 | throw new Error('no format specified. --environment FORMAT:xxx'); 35 | } 36 | 37 | export default { 38 | input: 'src/index.js', 39 | output: Object.assign( 40 | { 41 | name: umd ? 'Appbase' : 'appbase-js', 42 | }, 43 | output, 44 | ), 45 | external: umd 46 | ? Object.keys(pkg.peerDependencies || {}) 47 | : [ 48 | ...Object.keys(pkg.dependencies || {}), 49 | ...Object.keys(pkg.peerDependencies || {}), 50 | ], 51 | plugins: [ 52 | umd 53 | ? resolve({ 54 | jsnext: true, 55 | main: true, 56 | preferBuiltins: false, 57 | browser: true, 58 | }) 59 | : {}, 60 | umd ? commonjs({ include: 'node_modules/**' }) : {}, 61 | babel({ 62 | exclude: 'node_modules/**', 63 | babelrc: false, 64 | presets: [['env', { loose: true, modules: false }]], 65 | plugins: ['external-helpers'], 66 | }), 67 | umd ? builtins() : {}, 68 | umd 69 | ? replace({ 70 | 'process.env.NODE_ENV': JSON.stringify( 71 | minify ? 'production' : 'development', 72 | ), 73 | }) 74 | : null, 75 | minify ? terser() : null, 76 | copy({ 77 | targets: [{ src: 'src/index.d.ts', dest: 'dist' }], 78 | }), 79 | ].filter(Boolean), 80 | }; 81 | -------------------------------------------------------------------------------- /src/core/api/bulk.js: -------------------------------------------------------------------------------- 1 | import { removeUndefined, validate } from '../../utils/index'; 2 | 3 | /** 4 | * Bulk Service 5 | * @param {Object} args 6 | * @param {String} args.type 7 | * @param {Object} args.body 8 | */ 9 | function bulkApi(args) { 10 | const parsedArgs = removeUndefined(args); 11 | // Validate arguments 12 | const valid = validate(parsedArgs, { 13 | body: 'object', 14 | }); 15 | if (valid !== true) { 16 | throw valid; 17 | } 18 | 19 | const { type, body } = parsedArgs; 20 | 21 | delete parsedArgs.type; 22 | delete parsedArgs.body; 23 | 24 | let path; 25 | if (type) { 26 | path = `${type}/_bulk`; 27 | } else { 28 | path = '_bulk'; 29 | } 30 | 31 | return this.performFetchRequest({ 32 | method: 'POST', 33 | path, 34 | params: parsedArgs, 35 | body, 36 | }); 37 | } 38 | export default bulkApi; 39 | -------------------------------------------------------------------------------- /src/core/api/delete.js: -------------------------------------------------------------------------------- 1 | import { removeUndefined, validate } from '../../utils/index'; 2 | 3 | /** 4 | * Delete Service 5 | * @param {Object} args 6 | * @param {String} args.type 7 | * @param {String} args.id 8 | */ 9 | function deleteApi(args) { 10 | const parsedArgs = removeUndefined(args); 11 | // Validate arguments 12 | const valid = validate(parsedArgs, { 13 | id: 'string|number', 14 | }); 15 | if (valid !== true) { 16 | throw valid; 17 | } 18 | 19 | const { type = '_doc', id } = parsedArgs; 20 | delete parsedArgs.type; 21 | delete parsedArgs.id; 22 | 23 | const path = `${type}/${encodeURIComponent(id)}`; 24 | 25 | return this.performFetchRequest({ 26 | method: 'DELETE', 27 | path, 28 | params: parsedArgs, 29 | }); 30 | } 31 | export default deleteApi; 32 | -------------------------------------------------------------------------------- /src/core/api/get.js: -------------------------------------------------------------------------------- 1 | import { removeUndefined, validate } from '../../utils/index'; 2 | 3 | /** 4 | * Get Service 5 | * @param {Object} args 6 | * @param {String} args.type 7 | * @param {String} args.id 8 | */ 9 | function getApi(args) { 10 | const parsedArgs = removeUndefined(args); 11 | // Validate arguments 12 | const valid = validate(parsedArgs, { 13 | id: 'string|number', 14 | }); 15 | 16 | if (valid !== true) { 17 | throw valid; 18 | } 19 | 20 | const { type = '_doc', id } = parsedArgs; 21 | 22 | delete parsedArgs.type; 23 | delete parsedArgs.id; 24 | 25 | const path = `${type}/${encodeURIComponent(id)}`; 26 | 27 | return this.performFetchRequest({ 28 | method: 'GET', 29 | path, 30 | params: parsedArgs, 31 | }); 32 | } 33 | export default getApi; 34 | -------------------------------------------------------------------------------- /src/core/api/getMappings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * To get mappings 3 | */ 4 | function getMappings() { 5 | return this.performFetchRequest({ 6 | method: 'GET', 7 | path: '_mapping', 8 | }); 9 | } 10 | export default getMappings; 11 | -------------------------------------------------------------------------------- /src/core/api/getSuggestionsv3Api.js: -------------------------------------------------------------------------------- 1 | import { 2 | getMongoRequest, 3 | getTelemetryHeaders, 4 | removeUndefined, 5 | validateRSQuery, 6 | } from '../../utils/index'; 7 | 8 | /** 9 | * ReactiveSearch suggestions API for v3 10 | * @param {Array} query 11 | * @param {Object} settings 12 | * @param {boolean} settings.recordAnalytics 13 | * @param {boolean} settings.userId 14 | * @param {boolean} settings.enableQueryRules 15 | * @param {boolean} settings.customEvents 16 | */ 17 | function getSuggestionsv3Api(query, settings) { 18 | const parsedSettings = removeUndefined(settings); 19 | 20 | // Validate query 21 | const valid = validateRSQuery(query); 22 | 23 | if (valid !== true) { 24 | throw valid; 25 | } 26 | 27 | const body = { 28 | settings: parsedSettings, 29 | query, 30 | }; 31 | 32 | if (this.mongodb) { 33 | Object.assign(body, { mongodb: getMongoRequest(this.app, this.mongodb) }); 34 | } 35 | return this.performFetchRequest({ 36 | method: 'POST', 37 | path: '_reactivesearch.v3', 38 | body, 39 | headers: getTelemetryHeaders(this.enableTelemetry), 40 | isRSAPI: true, 41 | isSuggestionsAPI: true, 42 | isMongoRequest: !!this.mongodb, 43 | }); 44 | } 45 | 46 | export default getSuggestionsv3Api; 47 | -------------------------------------------------------------------------------- /src/core/api/index.js: -------------------------------------------------------------------------------- 1 | import { removeUndefined, validate } from '../../utils/index'; 2 | 3 | /** 4 | * Index Service 5 | * @param {Object} args 6 | * @param {String} args.type 7 | * @param {Object} args.body 8 | * @param {String} args.id 9 | */ 10 | function indexApi(args) { 11 | const parsedArgs = removeUndefined(args); 12 | // Validate arguments 13 | const valid = validate(parsedArgs, { 14 | body: 'object', 15 | }); 16 | if (valid !== true) { 17 | throw valid; 18 | } 19 | const { type = '_doc', id, body } = parsedArgs; 20 | 21 | delete parsedArgs.type; 22 | delete parsedArgs.body; 23 | delete parsedArgs.id; 24 | 25 | let path; 26 | if (id) { 27 | path = type ? `${type}/${encodeURIComponent(id)}` : encodeURIComponent(id); 28 | } else { 29 | path = type; 30 | } 31 | return this.performFetchRequest({ 32 | method: 'POST', 33 | path, 34 | params: parsedArgs, 35 | body, 36 | }); 37 | } 38 | export default indexApi; 39 | -------------------------------------------------------------------------------- /src/core/api/msearch.js: -------------------------------------------------------------------------------- 1 | import { removeUndefined, validate } from '../../utils/index'; 2 | 3 | /** 4 | * Msearch Service 5 | * @param {Object} args 6 | * @param {String} args.type 7 | * @param {Object} args.body 8 | */ 9 | function msearchApi(args) { 10 | const parsedArgs = removeUndefined(args); 11 | // Validate arguments 12 | const valid = validate(parsedArgs, { 13 | body: 'object', 14 | }); 15 | if (valid !== true) { 16 | throw valid; 17 | } 18 | 19 | let type; 20 | if (Array.isArray(parsedArgs.type)) { 21 | type = parsedArgs.type.join(); 22 | } else { 23 | ({ type } = parsedArgs); 24 | } 25 | 26 | const { body } = parsedArgs; 27 | 28 | delete parsedArgs.type; 29 | delete parsedArgs.body; 30 | 31 | let path; 32 | if (type) { 33 | path = `${type}/_msearch`; 34 | } else { 35 | path = '_msearch'; 36 | } 37 | 38 | return this.performFetchRequest({ 39 | method: 'POST', 40 | path, 41 | params: parsedArgs, 42 | body, 43 | }); 44 | } 45 | export default msearchApi; 46 | -------------------------------------------------------------------------------- /src/core/api/reactiveSearchApi.js: -------------------------------------------------------------------------------- 1 | import { 2 | getTelemetryHeaders, 3 | getMongoRequest, 4 | removeUndefined, 5 | validateRSQuery, 6 | } from '../../utils/index'; 7 | 8 | /** 9 | * ReactiveSearch API Service for v3 10 | * @param {Array} query 11 | * @param {Object} settings 12 | * @param {boolean} settings.recordAnalytics 13 | * @param {boolean} settings.userId 14 | * @param {boolean} settings.enableQueryRules 15 | * @param {boolean} settings.customEvents 16 | */ 17 | function reactiveSearchApi(query, settings, params) { 18 | const parsedSettings = removeUndefined(settings); 19 | 20 | // Validate query 21 | const valid = validateRSQuery(query); 22 | 23 | if (valid !== true) { 24 | throw valid; 25 | } 26 | 27 | const body = { 28 | settings: parsedSettings, 29 | query, 30 | }; 31 | 32 | if (this.mongodb) { 33 | Object.assign(body, { mongodb: getMongoRequest(this.app, this.mongodb) }); 34 | } 35 | return this.performFetchRequest({ 36 | method: 'POST', 37 | path: '_reactivesearch', 38 | body, 39 | headers: getTelemetryHeaders(this.enableTelemetry, !this.mongodb), 40 | isRSAPI: true, 41 | isMongoRequest: !!this.mongodb, 42 | params, 43 | httpRequestTimeout: this.httpRequestTimeout || 0, 44 | }); 45 | } 46 | export default reactiveSearchApi; 47 | -------------------------------------------------------------------------------- /src/core/api/reactiveSearchv3Api.js: -------------------------------------------------------------------------------- 1 | import { 2 | removeUndefined, 3 | validateRSQuery, 4 | getMongoRequest, 5 | getTelemetryHeaders, 6 | } from '../../utils/index'; 7 | 8 | /** 9 | * ReactiveSearch API Service for v3 10 | * @param {Array} query 11 | * @param {Object} settings 12 | * @param {boolean} settings.recordAnalytics 13 | * @param {boolean} settings.userId 14 | * @param {boolean} settings.enableQueryRules 15 | * @param {boolean} settings.customEvents 16 | */ 17 | function reactiveSearchv3Api(query, settings, params) { 18 | const parsedSettings = removeUndefined(settings); 19 | 20 | // Validate query 21 | const valid = validateRSQuery(query); 22 | 23 | if (valid !== true) { 24 | throw valid; 25 | } 26 | 27 | const body = { 28 | settings: parsedSettings, 29 | query, 30 | }; 31 | if (this.mongodb) { 32 | Object.assign(body, { mongodb: getMongoRequest(this.app, this.mongodb) }); 33 | } 34 | return this.performFetchRequest({ 35 | method: 'POST', 36 | path: '_reactivesearch.v3', 37 | body, 38 | headers: getTelemetryHeaders(this.enableTelemetry, !this.mongodb), 39 | isRSAPI: true, 40 | isMongoRequest: !!this.mongodb, 41 | params, 42 | httpRequestTimeout: this.httpRequestTimeout || 0, 43 | }); 44 | } 45 | export default reactiveSearchv3Api; 46 | -------------------------------------------------------------------------------- /src/core/api/search.js: -------------------------------------------------------------------------------- 1 | import { removeUndefined, validate } from '../../utils/index'; 2 | 3 | /** 4 | * Search Service 5 | * @param {Object} args 6 | * @param {String} args.type 7 | * @param {Object} args.body 8 | */ 9 | function searchApi(args) { 10 | const parsedArgs = removeUndefined(args); 11 | // Validate arguments 12 | const valid = validate(parsedArgs, { 13 | body: 'object', 14 | }); 15 | if (valid !== true) { 16 | throw valid; 17 | } 18 | 19 | let type; 20 | if (Array.isArray(parsedArgs.type)) { 21 | type = parsedArgs.type.join(); 22 | } else { 23 | // eslint-disable-next-line 24 | type = parsedArgs.type; 25 | } 26 | 27 | const { body } = parsedArgs; 28 | 29 | delete parsedArgs.type; 30 | delete parsedArgs.body; 31 | 32 | let path; 33 | if (type) { 34 | path = `${type}/_search`; 35 | } else { 36 | path = '_search'; 37 | } 38 | 39 | return this.performFetchRequest({ 40 | method: 'POST', 41 | path, 42 | params: parsedArgs, 43 | body, 44 | }); 45 | } 46 | export default searchApi; 47 | -------------------------------------------------------------------------------- /src/core/api/update.js: -------------------------------------------------------------------------------- 1 | import { removeUndefined, validate } from '../../utils/index'; 2 | 3 | /** 4 | * Update Service 5 | * @param {Object} args 6 | * @param {String} args.type 7 | * @param {Object} args.body 8 | * @param {String} args.id 9 | */ 10 | function updateApi(args) { 11 | const parsedArgs = removeUndefined(args); 12 | // Validate arguments 13 | const valid = validate(parsedArgs, { 14 | id: 'string|number', 15 | body: 'object', 16 | }); 17 | if (valid !== true) { 18 | throw valid; 19 | } 20 | 21 | const { type = '_doc', id, body } = parsedArgs; 22 | delete parsedArgs.type; 23 | delete parsedArgs.id; 24 | delete parsedArgs.body; 25 | const path = `${type}/${encodeURIComponent(id)}/_update`; 26 | 27 | return this.performFetchRequest({ 28 | method: 'POST', 29 | path, 30 | params: parsedArgs, 31 | body, 32 | }); 33 | } 34 | export default updateApi; 35 | -------------------------------------------------------------------------------- /src/core/index.js: -------------------------------------------------------------------------------- 1 | import URL from 'url-parser-lite'; 2 | import { 3 | backendAlias, 4 | isAppbase, 5 | isValidHttpUrl, 6 | validateSchema, 7 | } from '../utils/index'; 8 | import SCHEMA from '../utils/schema/index'; 9 | /** 10 | * Returns an instance of Appbase client 11 | * @param {Object} config To configure properties 12 | * @param {String} config.url 13 | * @param {String} config.app 14 | * @param {String} config.credentials 15 | * @param {String} config.username 16 | * @param {String} config.password 17 | * @param {Boolean} config.enableTelemetry 18 | * @param {Object} config.mongodb 19 | * @param {Object} config.endpoint 20 | * @param {Object} config.httpRequestTimeout 21 | * A callback function which will be invoked before a fetch request made 22 | */ 23 | function AppBase(config) { 24 | const { 25 | auth = null, 26 | host = '', 27 | path = '', 28 | protocol = '', 29 | } = URL((config.endpoint ? config.endpoint.url : config.url) || ''); 30 | let { url } = config; 31 | url = host + path; 32 | // Parse url 33 | if (url.slice(-1) === '/') { 34 | url = url.slice(0, -1); 35 | } 36 | const backendName = backendAlias[config.mongodb ? 'MONGODB' : 'ELASTICSEARCH']; 37 | // eslint-disable-next-line 38 | const schema = SCHEMA[backendName]; 39 | 40 | if (config.endpoint && isValidHttpUrl(config.endpoint.url)) { 41 | schema.url.required = false; 42 | schema.app.required = false; 43 | schema.credentials.required = false; 44 | } 45 | 46 | validateSchema( 47 | { 48 | url: config.url, 49 | app: config.app, 50 | credentials: config.credentials, 51 | username: config.username, 52 | password: config.password, 53 | enableTelemetry: config.enableTelemetry, 54 | mongodb: config.mongodb, 55 | }, 56 | schema, 57 | backendName, 58 | ); 59 | 60 | if (typeof protocol !== 'string' || protocol === '') { 61 | throw new Error( 62 | 'Protocol is not present in url. URL should be of the form https://appbase-demo-ansible-abxiydt-arc.searchbase.io', 63 | ); 64 | } 65 | 66 | let credentials = auth || null; 67 | /** 68 | * Credentials can be provided as a part of the URL, 69 | * as username, password args or as a credentials argument directly */ 70 | if (typeof config.credentials === 'string' && config.credentials !== '') { 71 | // eslint-disable-next-line 72 | credentials = config.credentials; 73 | } else if ( 74 | typeof config.username === 'string' 75 | && config.username !== '' 76 | && typeof config.password === 'string' 77 | && config.password !== '' 78 | ) { 79 | credentials = `${config.username}:${config.password}`; 80 | } 81 | if (!config.mongodb) { 82 | if (isAppbase(url) && credentials === null) { 83 | throw new Error( 84 | 'Authentication information is not present. Did you add credentials?', 85 | ); 86 | } 87 | } 88 | 89 | this.url = url; 90 | this.protocol = protocol; 91 | this.app = config.app; 92 | this.credentials = credentials; 93 | if (config.mongodb) { 94 | this.mongodb = config.mongodb; 95 | } 96 | 97 | if (config.httpRequestTimeout) { 98 | this.httpRequestTimeout = config.httpRequestTimeout; 99 | } 100 | 101 | if (typeof config.enableTelemetry === 'boolean') { 102 | this.enableTelemetry = config.enableTelemetry; 103 | } 104 | } 105 | export default AppBase; 106 | -------------------------------------------------------------------------------- /src/core/request/fetch.js: -------------------------------------------------------------------------------- 1 | import querystring from 'querystring'; 2 | import fetch from 'cross-fetch'; 3 | import { btoa, removeUndefined } from '../../utils/index'; 4 | 5 | /** 6 | * To perform fetch request 7 | * @param {Object} args 8 | * @param {String} args.method 9 | * @param {String} args.path 10 | * @param {Object} args.params 11 | * @param {Object} args.body 12 | * @param {Object} args.headers 13 | * @param {boolean} args.isSuggestionsAPI 14 | * @param {number} args.httpRequestTimeout - Timeout duration in milliseconds 15 | */ 16 | function fetchRequest(args) { 17 | return new Promise((resolve, reject) => { 18 | const parsedArgs = removeUndefined(args); 19 | try { 20 | const { 21 | method, 22 | path, 23 | params, 24 | body, 25 | isRSAPI, 26 | isSuggestionsAPI, 27 | isMongoRequest = false, 28 | httpRequestTimeout = 0, // Default timeout is 0 (no timeout) 29 | } = parsedArgs; 30 | const app = isSuggestionsAPI ? '.suggestions' : this.app; 31 | let bodyCopy = body; 32 | const contentType = path.endsWith('msearch') || path.endsWith('bulk') 33 | ? 'application/x-ndjson' 34 | : 'application/json'; 35 | const headers = Object.assign( 36 | {}, 37 | { 38 | Accept: 'application/json', 39 | 'Content-Type': contentType, 40 | }, 41 | args.headers, 42 | this.headers, 43 | ); 44 | const timestamp = Date.now(); 45 | if (this.credentials) { 46 | headers.Authorization = `Basic ${btoa(this.credentials)}`; 47 | } 48 | const requestOptions = { 49 | method, 50 | headers, 51 | }; 52 | if (Array.isArray(bodyCopy)) { 53 | let arrayBody = ''; 54 | bodyCopy.forEach((item) => { 55 | arrayBody += JSON.stringify(item); 56 | arrayBody += '\n'; 57 | }); 58 | 59 | bodyCopy = arrayBody; 60 | } else { 61 | bodyCopy = JSON.stringify(bodyCopy) || {}; 62 | } 63 | 64 | if (Object.keys(bodyCopy).length !== 0) { 65 | requestOptions.body = bodyCopy; 66 | } 67 | 68 | const handleTransformRequest = (res) => { 69 | if ( 70 | this.transformRequest 71 | && typeof this.transformRequest === 'function' 72 | ) { 73 | const transformRequestPromise = this.transformRequest(res); 74 | return transformRequestPromise instanceof Promise 75 | ? transformRequestPromise 76 | : Promise.resolve(transformRequestPromise); 77 | } 78 | return Promise.resolve(res); 79 | }; 80 | 81 | let responseHeaders = {}; 82 | 83 | let paramsString = ''; 84 | if (params) { 85 | paramsString = `?${querystring.stringify(params)}`; 86 | } 87 | const finalURL = isMongoRequest 88 | ? `${this.protocol}://${this.url}` 89 | : `${this.protocol}://${this.url}/${app}/${path}${paramsString}`; 90 | 91 | return handleTransformRequest( 92 | Object.assign( 93 | {}, 94 | { 95 | url: finalURL, 96 | }, 97 | requestOptions, 98 | ), 99 | ) 100 | .then((ts) => { 101 | const transformedRequest = Object.assign({}, ts); 102 | const { url } = transformedRequest; 103 | delete transformedRequest.url; 104 | 105 | const controller = new AbortController(); 106 | const { signal } = controller; 107 | 108 | const fetchPromise = fetch( 109 | url || finalURL, 110 | Object.assign({}, transformedRequest, { 111 | // apply timestamp header for RS API 112 | headers: 113 | isRSAPI && !isMongoRequest 114 | ? Object.assign({}, transformedRequest.headers, { 115 | 'x-timestamp': new Date().getTime(), 116 | }) 117 | : transformedRequest.headers, 118 | signal, // Attach the abort signal to the fetch request 119 | }), 120 | ); 121 | 122 | const timeoutPromise = new Promise((_, rejectTP) => { 123 | if (httpRequestTimeout > 0) { 124 | setTimeout(() => { 125 | rejectTP(new Error('Request timeout')); 126 | controller.abort(); 127 | }, httpRequestTimeout); 128 | } 129 | }); 130 | 131 | return Promise.race([fetchPromise, timeoutPromise]) 132 | .then((res) => { 133 | if (res.status >= 500) { 134 | return reject(res); 135 | } 136 | responseHeaders = res.headers; 137 | return res 138 | .json() 139 | .then((data) => { 140 | if (res.status >= 400) { 141 | return reject(res); 142 | } 143 | if (data && data.error) { 144 | return reject(data); 145 | } 146 | // Handle error from RS API RESPONSE 147 | if ( 148 | isRSAPI 149 | && data 150 | && Object.prototype.toString.call(data) === '[object Object]' 151 | ) { 152 | if (body && body.query && body.query instanceof Array) { 153 | let errorResponses = 0; 154 | const allResponses = body.query.filter( 155 | q => q.execute || q.execute === undefined, 156 | ).length; 157 | 158 | if (data) { 159 | Object.keys(data).forEach((key) => { 160 | if ( 161 | data[key] 162 | && Object.prototype.hasOwnProperty.call( 163 | data[key], 164 | 'error', 165 | ) 166 | && !!data[key].error 167 | ) { 168 | errorResponses += 1; 169 | } 170 | }); 171 | } 172 | // reject only when all responses have an error 173 | if ( 174 | errorResponses > 0 175 | && allResponses === errorResponses 176 | ) { 177 | return reject(data); 178 | } 179 | } 180 | } 181 | 182 | // Handle error from _msearch response 183 | if (data && data.responses instanceof Array) { 184 | const allResponses = data.responses.length; 185 | const errorResponses = data.responses.filter(entry => Object.prototype.hasOwnProperty.call(entry, 'error')).length; 186 | // reject only when all responses have an error 187 | if (allResponses === errorResponses) { 188 | return reject(data); 189 | } 190 | } 191 | const response = Object.assign({}, data, { 192 | _timestamp: timestamp, 193 | _headers: responseHeaders, 194 | }); 195 | return resolve(response); 196 | }) 197 | .catch(e => reject(e)); 198 | }) 199 | .catch(e => reject(e)); 200 | }) 201 | .catch(err => reject(err)); 202 | } catch (e) { 203 | return reject(e); 204 | } 205 | }); 206 | } 207 | 208 | export default fetchRequest; 209 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | type BulkApiArgs = { 2 | type?: string; 3 | body: Object; 4 | }; 5 | type DeleteApiArgs = { 6 | type?: string; 7 | id: string | number; 8 | }; 9 | type GetApiArgs = DeleteApiArgs; 10 | type AppbaseSettings = { 11 | recordAnalytics?: boolean; 12 | enableQueryRules?: boolean; 13 | userId?: string; 14 | customEvents?: Object; 15 | }; 16 | type IndexApiArgs = { 17 | type?: string; 18 | id?: string; 19 | body: Object; 20 | }; 21 | type MsearchApiArgs = BulkApiArgs; 22 | type SearchApiArgs = BulkApiArgs; 23 | type UpdateApiArgs = { 24 | type?: string; 25 | id: string | number; 26 | body: Object; 27 | }; 28 | type FetchApiArgs = { 29 | method: string; 30 | path: string; 31 | body: Object | Array; 32 | params?: Object; 33 | isSuggestionsAPI?: Boolean; 34 | }; 35 | 36 | /* eslint-disable */ 37 | 38 | interface MongoDbObject{ 39 | db: string; 40 | collection: string; 41 | } 42 | interface AppbaseConfig { 43 | url?: string; 44 | app?: string; 45 | credentials?: string; 46 | username?: string; 47 | password?: string; 48 | enableTelemetry?: boolean; 49 | mongodb?: MongoDbObject; 50 | } 51 | 52 | interface AppbaseInstanceObject { 53 | performFetchRequest: (args: FetchApiArgs) => Object; 54 | index: (args: IndexApiArgs) => Object; 55 | get: (args: GetApiArgs) => Object; 56 | update: (args: UpdateApiArgs) => Object; 57 | delete: (args: DeleteApiArgs) => Object; 58 | bulk: (args: BulkApiArgs) => Object; 59 | search: (args: SearchApiArgs) => Object; 60 | msearch: (args: MsearchApiArgs) => Object; 61 | reactiveSearch: (query: Array, settings?: AppbaseSettings) => Object; 62 | reactiveSearchv3: ( 63 | query: Array, 64 | settings?: AppbaseSettings 65 | ) => Object; 66 | getQuerySuggestions: ( 67 | query: Array, 68 | settings?: AppbaseSettings 69 | ) => Object; 70 | getMappings: () => Object; 71 | setHeaders: (headers?: Object, shouldEncode?: Boolean) => void; 72 | } 73 | 74 | /* eslint-enable */ 75 | declare function appbasejs(config: AppbaseConfig): AppbaseInstanceObject; 76 | export default appbasejs; 77 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import AppBaseClient from './core/index'; 2 | import fetchRequest from './core/request/fetch'; 3 | import indexApi from './core/api/index'; 4 | import getApi from './core/api/get'; 5 | import updateApi from './core/api/update'; 6 | import deleteApi from './core/api/delete'; 7 | import bulkApi from './core/api/bulk'; 8 | import searchApi from './core/api/search'; 9 | import msearchApi from './core/api/msearch'; 10 | import reactiveSearchApi from './core/api/reactiveSearchApi'; 11 | import reactiveSearchv3Api from './core/api/reactiveSearchv3Api'; 12 | import getMappingsApi from './core/api/getMappings'; 13 | import { encodeHeaders } from './utils/index'; 14 | import getSuggestionsv3Api from './core/api/getSuggestionsv3Api'; 15 | 16 | export default function appbasejs(config) { 17 | const client = new AppBaseClient(config); 18 | 19 | AppBaseClient.prototype.performFetchRequest = fetchRequest; 20 | 21 | AppBaseClient.prototype.index = indexApi; 22 | 23 | AppBaseClient.prototype.get = getApi; 24 | 25 | AppBaseClient.prototype.update = updateApi; 26 | 27 | AppBaseClient.prototype.delete = deleteApi; 28 | 29 | AppBaseClient.prototype.bulk = bulkApi; 30 | 31 | AppBaseClient.prototype.search = searchApi; 32 | 33 | AppBaseClient.prototype.msearch = msearchApi; 34 | 35 | AppBaseClient.prototype.reactiveSearch = reactiveSearchApi; 36 | 37 | AppBaseClient.prototype.reactiveSearchv3 = reactiveSearchv3Api; 38 | 39 | AppBaseClient.prototype.getQuerySuggestions = getSuggestionsv3Api; 40 | 41 | AppBaseClient.prototype.getMappings = getMappingsApi; 42 | 43 | AppBaseClient.prototype.setHeaders = function setHeaders( 44 | headers = {}, 45 | shouldEncode = false, 46 | ) { 47 | // Encode headers 48 | if (shouldEncode) { 49 | this.headers = encodeHeaders(headers); 50 | } else { 51 | this.headers = headers; 52 | } 53 | }; 54 | 55 | if (typeof window !== 'undefined') { 56 | window.Appbase = client; 57 | } 58 | return client; 59 | } 60 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export function contains(string, substring) { 2 | return string.indexOf(substring) !== -1; 3 | } 4 | export function isAppbase(url) { 5 | return contains(url, 'scalr.api.appbase.io'); 6 | } 7 | export function btoa(input = '') { 8 | const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; 9 | const str = input; 10 | let output = ''; 11 | 12 | // eslint-disable-next-line 13 | for ( 14 | let block = 0, charCode, i = 0, map = chars; 15 | str.charAt(i | 0) || ((map = '='), i % 1); // eslint-disable-line no-bitwise 16 | output += map.charAt(63 & (block >> (8 - (i % 1) * 8))) // eslint-disable-line no-bitwise 17 | ) { 18 | charCode = str.charCodeAt((i += 3 / 4)); 19 | 20 | if (charCode > 0xff) { 21 | throw new Error( 22 | '"btoa" failed: The string to be encoded contains characters outside of the Latin1 range.', 23 | ); 24 | } 25 | 26 | block = (block << 8) | charCode; // eslint-disable-line no-bitwise 27 | } 28 | 29 | return output; 30 | } 31 | export function uuidv4() { 32 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { 33 | const r = (Math.random() * 16) | 0; // eslint-disable-line no-bitwise 34 | 35 | const v = c === 'x' ? r : (r & 0x3) | 0x8; // eslint-disable-line no-bitwise 36 | return v.toString(16); 37 | }); 38 | } 39 | 40 | export function validateRSQuery(query) { 41 | if (query && Object.prototype.toString.call(query) === '[object Array]') { 42 | for (let i = 0; i < query.length; i += 1) { 43 | const q = query[i]; 44 | if (q) { 45 | if (!q.id) { 46 | return new Error("'id' field must be present in query object"); 47 | } 48 | } else { 49 | return new Error('query object can not have an empty value'); 50 | } 51 | } 52 | return true; 53 | } 54 | return new Error("invalid query value, 'query' value must be an array"); 55 | } 56 | 57 | export function validate(object, fields) { 58 | const invalid = []; 59 | const emptyFor = { 60 | object: null, 61 | string: '', 62 | number: 0, 63 | }; 64 | const keys = Object.keys(fields); 65 | keys.forEach((key) => { 66 | const types = fields[key].split('|'); 67 | const matchedType = types.find( 68 | type => 69 | // eslint-disable-next-line 70 | typeof object[key] === type 71 | ); 72 | if (!matchedType || object[key] === emptyFor[matchedType]) { 73 | invalid.push(key); 74 | } 75 | }); 76 | let missing = ''; 77 | for (let i = 0; i < invalid.length; i += 1) { 78 | missing += `${invalid[i]}, `; 79 | } 80 | if (invalid.length > 0) { 81 | return new Error(`fields missing: ${missing}`); 82 | } 83 | 84 | return true; 85 | } 86 | 87 | export function removeUndefined(value = {}) { 88 | if ( 89 | value 90 | || !(Object.keys(value).length === 0 && value.constructor === Object) 91 | ) { 92 | return JSON.parse(JSON.stringify(value)); 93 | } 94 | return null; 95 | } 96 | 97 | /** 98 | * Send only when a connection is opened 99 | * @param {Object} socket 100 | * @param {Function} callback 101 | */ 102 | export function waitForSocketConnection(socket, callback) { 103 | setTimeout(() => { 104 | if (socket.readyState === 1) { 105 | if (callback != null) { 106 | callback(); 107 | } 108 | } else { 109 | waitForSocketConnection(socket, callback); 110 | } 111 | }, 5); // wait 5 ms for the connection... 112 | } 113 | 114 | export function encodeHeaders(headers = {}, shouldEncode = true) { 115 | // Encode headers 116 | let encodedHeaders = {}; 117 | if (shouldEncode) { 118 | Object.keys(headers).forEach((header) => { 119 | encodedHeaders[header] = encodeURI(headers[header]); 120 | }); 121 | } else { 122 | encodedHeaders = headers; 123 | } 124 | return encodedHeaders; 125 | } 126 | export function getMongoRequest(app, mongo) { 127 | const mongodb = {}; 128 | if (app) { 129 | mongodb.index = app; 130 | } 131 | if (mongo) { 132 | if (mongo.db) { 133 | mongodb.db = mongo.db; 134 | } 135 | if (mongo.collection) { 136 | mongodb.collection = mongo.collection; 137 | } 138 | } 139 | return mongodb; 140 | } 141 | 142 | export function getTelemetryHeaders(enableTelemetry, shouldSetHeaders) { 143 | const headers = {}; 144 | if (!shouldSetHeaders) { 145 | return headers; 146 | } 147 | Object.assign(headers, { 148 | 'X-Search-Client': 'Appbase JS', 149 | }); 150 | 151 | if (enableTelemetry === false) { 152 | Object.assign(headers, { 153 | 'X-Enable-Telemetry': enableTelemetry, 154 | }); 155 | } 156 | 157 | return headers; 158 | } 159 | 160 | 161 | export const backendAlias = { 162 | MONGODB: 'mongodb', // mongodb 163 | ELASTICSEARCH: 'elasticsearch', // elasticsearch 164 | }; 165 | export const dataTypes = { 166 | ARRAY: 'array', 167 | FUNCTION: 'function', 168 | OBJECT: 'object', 169 | NUMBER: 'number', 170 | BOOLEAN: 'boolean', 171 | STRING: 'string', 172 | }; 173 | const checkDataType = (temp) => { 174 | // eslint-disable-next-line 175 | if (typeof temp === dataTypes.OBJECT) { 176 | if (Array.isArray(temp)) { 177 | return dataTypes.ARRAY; 178 | } 179 | 180 | return dataTypes.OBJECT; 181 | } 182 | return typeof temp; 183 | }; 184 | 185 | export function validateSchema( 186 | passedProperties = {}, 187 | schema = {}, 188 | backendName = '', 189 | ) { 190 | const passedPropertiesKeys = Object.keys(passedProperties).filter( 191 | propertyKey => !!passedProperties[propertyKey], 192 | ); 193 | const acceptedProperties = Object.keys(schema); 194 | const requiredProperties = []; 195 | // fetch required properties 196 | acceptedProperties.forEach((propName) => { 197 | const currentProperty = schema[propName]; 198 | if (currentProperty.required) { 199 | requiredProperties.push(propName); 200 | } 201 | }); 202 | // check for required properties 203 | requiredProperties.forEach((requiredProperty) => { 204 | if (!passedPropertiesKeys.includes(requiredProperty)) { 205 | throw new Error( 206 | `${requiredProperty} is required when using the ${backendName} Search backend.`, 207 | ); 208 | } 209 | }); 210 | 211 | // check for accepted properties 212 | passedPropertiesKeys.forEach((passedPropertyKey) => { 213 | if (!acceptedProperties.includes(passedPropertyKey)) { 214 | throw new Error( 215 | `${passedPropertyKey} property isn't accepted property by ${backendName} backend.`, 216 | ); 217 | } 218 | 219 | const acceptedTypes = Array.isArray(schema[passedPropertyKey].type) 220 | ? schema[passedPropertyKey].type 221 | : [...schema[passedPropertyKey].type]; 222 | const receivedPropertyType = checkDataType( 223 | passedProperties[passedPropertyKey], 224 | ); 225 | if (!acceptedTypes.includes(receivedPropertyType)) { 226 | throw new Error( 227 | `The property ${passedPropertyKey} is expected with type(s) [${acceptedTypes.join( 228 | ', ', 229 | )}], but type was set as ${receivedPropertyType}.`, 230 | ); 231 | } 232 | }); 233 | } 234 | 235 | export function isValidHttpUrl(string) { 236 | let url; 237 | 238 | try { 239 | url = new URL(string); 240 | } catch (_) { 241 | return false; 242 | } 243 | 244 | return url.protocol === 'http:' || url.protocol === 'https:'; 245 | } 246 | -------------------------------------------------------------------------------- /src/utils/schema/elasticsearch.js: -------------------------------------------------------------------------------- 1 | import { dataTypes } from '../index'; 2 | 3 | export default { 4 | url: { 5 | type: dataTypes.STRING, 6 | required: true, 7 | }, 8 | app: { 9 | type: dataTypes.STRING, 10 | required: true, 11 | }, 12 | credentials: { 13 | type: dataTypes.STRING, 14 | required: false, 15 | }, 16 | enableTelemetry: { 17 | type: dataTypes.BOOLEAN, 18 | required: false, 19 | }, 20 | username: { 21 | type: dataTypes.STRING, 22 | required: false, 23 | }, 24 | password: { 25 | type: dataTypes.STRING, 26 | required: false, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/utils/schema/index.js: -------------------------------------------------------------------------------- 1 | import mongodb from './mongodb'; 2 | import elasticsearch from './elasticsearch'; 3 | 4 | export default { mongodb, elasticsearch }; 5 | -------------------------------------------------------------------------------- /src/utils/schema/mongodb.js: -------------------------------------------------------------------------------- 1 | import { dataTypes } from '../index'; 2 | 3 | export default { 4 | url: { 5 | type: dataTypes.STRING, 6 | required: true, 7 | }, 8 | app: { 9 | type: dataTypes.STRING, 10 | required: false, 11 | }, 12 | credentials: { 13 | type: dataTypes.STRING, 14 | required: false, 15 | }, 16 | enableTelemetry: { 17 | type: dataTypes.BOOLEAN, 18 | required: false, 19 | }, 20 | mongodb: { 21 | type: dataTypes.OBJECT, 22 | required: true, 23 | }, 24 | username: { 25 | type: dataTypes.STRING, 26 | required: false, 27 | }, 28 | password: { 29 | type: dataTypes.STRING, 30 | required: false, 31 | }, 32 | }; 33 | --------------------------------------------------------------------------------