UNPKG

appbase-js

Version:

Appbase.io streaming client lib for Javascript

978 lines (849 loc) 25.8 kB
import URL$1 from 'url-parser-lite'; import querystring from 'querystring'; import fetch from 'cross-fetch'; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; function contains(string, substring) { return string.indexOf(substring) !== -1; } function isAppbase(url) { return contains(url, 'scalr.api.appbase.io'); } function btoa() { var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; var str = input; var output = ''; // eslint-disable-next-line for (var block = 0, charCode, i = 0, map = chars; str.charAt(i | 0) || (map = '=', i % 1); // eslint-disable-line no-bitwise output += map.charAt(63 & block >> 8 - i % 1 * 8) // eslint-disable-line no-bitwise ) { charCode = str.charCodeAt(i += 3 / 4); if (charCode > 0xff) { throw new Error('"btoa" failed: The string to be encoded contains characters outside of the Latin1 range.'); } block = block << 8 | charCode; // eslint-disable-line no-bitwise } return output; } function validateRSQuery(query) { if (query && Object.prototype.toString.call(query) === '[object Array]') { for (var i = 0; i < query.length; i += 1) { var q = query[i]; if (q) { if (!q.id) { return new Error("'id' field must be present in query object"); } } else { return new Error('query object can not have an empty value'); } } return true; } return new Error("invalid query value, 'query' value must be an array"); } function validate(object, fields) { var invalid = []; var emptyFor = { object: null, string: '', number: 0 }; var keys = Object.keys(fields); keys.forEach(function (key) { var types = fields[key].split('|'); var matchedType = types.find(function (type) { return ( // eslint-disable-next-line _typeof(object[key]) === type ); }); if (!matchedType || object[key] === emptyFor[matchedType]) { invalid.push(key); } }); var missing = ''; for (var i = 0; i < invalid.length; i += 1) { missing += invalid[i] + ', '; } if (invalid.length > 0) { return new Error('fields missing: ' + missing); } return true; } function removeUndefined() { var value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; if (value || !(Object.keys(value).length === 0 && value.constructor === Object)) { return JSON.parse(JSON.stringify(value)); } return null; } function encodeHeaders() { var headers = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var shouldEncode = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; // Encode headers var encodedHeaders = {}; if (shouldEncode) { Object.keys(headers).forEach(function (header) { encodedHeaders[header] = encodeURI(headers[header]); }); } else { encodedHeaders = headers; } return encodedHeaders; } function getMongoRequest(app, mongo) { var mongodb = {}; if (app) { mongodb.index = app; } if (mongo) { if (mongo.db) { mongodb.db = mongo.db; } if (mongo.collection) { mongodb.collection = mongo.collection; } } return mongodb; } function getTelemetryHeaders(enableTelemetry, shouldSetHeaders) { var headers = {}; if (!shouldSetHeaders) { return headers; } Object.assign(headers, { 'X-Search-Client': 'Appbase JS' }); if (enableTelemetry === false) { Object.assign(headers, { 'X-Enable-Telemetry': enableTelemetry }); } return headers; } var backendAlias = { MONGODB: 'mongodb', // mongodb ELASTICSEARCH: 'elasticsearch' // elasticsearch }; var dataTypes = { ARRAY: 'array', FUNCTION: 'function', OBJECT: 'object', NUMBER: 'number', BOOLEAN: 'boolean', STRING: 'string' }; var checkDataType = function checkDataType(temp) { // eslint-disable-next-line if ((typeof temp === 'undefined' ? 'undefined' : _typeof(temp)) === dataTypes.OBJECT) { if (Array.isArray(temp)) { return dataTypes.ARRAY; } return dataTypes.OBJECT; } return typeof temp === 'undefined' ? 'undefined' : _typeof(temp); }; function validateSchema() { var passedProperties = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var schema = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var backendName = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ''; var passedPropertiesKeys = Object.keys(passedProperties).filter(function (propertyKey) { return !!passedProperties[propertyKey]; }); var acceptedProperties = Object.keys(schema); var requiredProperties = []; // fetch required properties acceptedProperties.forEach(function (propName) { var currentProperty = schema[propName]; if (currentProperty.required) { requiredProperties.push(propName); } }); // check for required properties requiredProperties.forEach(function (requiredProperty) { if (!passedPropertiesKeys.includes(requiredProperty)) { throw new Error(requiredProperty + ' is required when using the ' + backendName + ' Search backend.'); } }); // check for accepted properties passedPropertiesKeys.forEach(function (passedPropertyKey) { if (!acceptedProperties.includes(passedPropertyKey)) { throw new Error(passedPropertyKey + ' property isn\'t accepted property by ' + backendName + ' backend.'); } var acceptedTypes = Array.isArray(schema[passedPropertyKey].type) ? schema[passedPropertyKey].type : [].concat(schema[passedPropertyKey].type); var receivedPropertyType = checkDataType(passedProperties[passedPropertyKey]); if (!acceptedTypes.includes(receivedPropertyType)) { throw new Error('The property ' + passedPropertyKey + ' is expected with type(s) [' + acceptedTypes.join(', ') + '], but type was set as ' + receivedPropertyType + '.'); } }); } function isValidHttpUrl(string) { var url = void 0; try { url = new URL(string); } catch (_) { return false; } return url.protocol === 'http:' || url.protocol === 'https:'; } var mongodb = { url: { type: dataTypes.STRING, required: true }, app: { type: dataTypes.STRING, required: false }, credentials: { type: dataTypes.STRING, required: false }, enableTelemetry: { type: dataTypes.BOOLEAN, required: false }, mongodb: { type: dataTypes.OBJECT, required: true }, username: { type: dataTypes.STRING, required: false }, password: { type: dataTypes.STRING, required: false } }; var elasticsearch = { url: { type: dataTypes.STRING, required: true }, app: { type: dataTypes.STRING, required: true }, credentials: { type: dataTypes.STRING, required: false }, enableTelemetry: { type: dataTypes.BOOLEAN, required: false }, username: { type: dataTypes.STRING, required: false }, password: { type: dataTypes.STRING, required: false } }; var SCHEMA = { mongodb: mongodb, elasticsearch: elasticsearch }; /** * Returns an instance of Appbase client * @param {Object} config To configure properties * @param {String} config.url * @param {String} config.app * @param {String} config.credentials * @param {String} config.username * @param {String} config.password * @param {Boolean} config.enableTelemetry * @param {Object} config.mongodb * @param {Object} config.endpoint * @param {Object} config.httpRequestTimeout * A callback function which will be invoked before a fetch request made */ function AppBase(config) { var _URL = URL$1((config.endpoint ? config.endpoint.url : config.url) || ''), _URL$auth = _URL.auth, auth = _URL$auth === undefined ? null : _URL$auth, _URL$host = _URL.host, host = _URL$host === undefined ? '' : _URL$host, _URL$path = _URL.path, path = _URL$path === undefined ? '' : _URL$path, _URL$protocol = _URL.protocol, protocol = _URL$protocol === undefined ? '' : _URL$protocol; var url = config.url; url = host + path; // Parse url if (url.slice(-1) === '/') { url = url.slice(0, -1); } var backendName = backendAlias[config.mongodb ? 'MONGODB' : 'ELASTICSEARCH']; // eslint-disable-next-line var schema = SCHEMA[backendName]; if (config.endpoint && isValidHttpUrl(config.endpoint.url)) { schema.url.required = false; schema.app.required = false; schema.credentials.required = false; } validateSchema({ url: config.url, app: config.app, credentials: config.credentials, username: config.username, password: config.password, enableTelemetry: config.enableTelemetry, mongodb: config.mongodb }, schema, backendName); if (typeof protocol !== 'string' || protocol === '') { throw new Error('Protocol is not present in url. URL should be of the form https://appbase-demo-ansible-abxiydt-arc.searchbase.io'); } var credentials = auth || null; /** * Credentials can be provided as a part of the URL, * as username, password args or as a credentials argument directly */ if (typeof config.credentials === 'string' && config.credentials !== '') { // eslint-disable-next-line credentials = config.credentials; } else if (typeof config.username === 'string' && config.username !== '' && typeof config.password === 'string' && config.password !== '') { credentials = config.username + ':' + config.password; } if (!config.mongodb) { if (isAppbase(url) && credentials === null) { throw new Error('Authentication information is not present. Did you add credentials?'); } } this.url = url; this.protocol = protocol; this.app = config.app; this.credentials = credentials; if (config.mongodb) { this.mongodb = config.mongodb; } if (config.httpRequestTimeout) { this.httpRequestTimeout = config.httpRequestTimeout; } if (typeof config.enableTelemetry === 'boolean') { this.enableTelemetry = config.enableTelemetry; } } /** * To perform fetch request * @param {Object} args * @param {String} args.method * @param {String} args.path * @param {Object} args.params * @param {Object} args.body * @param {Object} args.headers * @param {boolean} args.isSuggestionsAPI * @param {number} args.httpRequestTimeout - Timeout duration in milliseconds */ function fetchRequest(args) { var _this = this; return new Promise(function (resolve, reject) { var parsedArgs = removeUndefined(args); try { var method = parsedArgs.method, path = parsedArgs.path, params = parsedArgs.params, body = parsedArgs.body, isRSAPI = parsedArgs.isRSAPI, isSuggestionsAPI = parsedArgs.isSuggestionsAPI, _parsedArgs$isMongoRe = parsedArgs.isMongoRequest, isMongoRequest = _parsedArgs$isMongoRe === undefined ? false : _parsedArgs$isMongoRe, _parsedArgs$httpReque = parsedArgs.httpRequestTimeout, httpRequestTimeout = _parsedArgs$httpReque === undefined ? 0 : _parsedArgs$httpReque; var app = isSuggestionsAPI ? '.suggestions' : _this.app; var bodyCopy = body; var contentType = path.endsWith('msearch') || path.endsWith('bulk') ? 'application/x-ndjson' : 'application/json'; var headers = Object.assign({}, { Accept: 'application/json', 'Content-Type': contentType }, args.headers, _this.headers); var timestamp = Date.now(); if (_this.credentials) { headers.Authorization = 'Basic ' + btoa(_this.credentials); } var requestOptions = { method: method, headers: headers }; if (Array.isArray(bodyCopy)) { var arrayBody = ''; bodyCopy.forEach(function (item) { arrayBody += JSON.stringify(item); arrayBody += '\n'; }); bodyCopy = arrayBody; } else { bodyCopy = JSON.stringify(bodyCopy) || {}; } if (Object.keys(bodyCopy).length !== 0) { requestOptions.body = bodyCopy; } var handleTransformRequest = function handleTransformRequest(res) { if (_this.transformRequest && typeof _this.transformRequest === 'function') { var transformRequestPromise = _this.transformRequest(res); return transformRequestPromise instanceof Promise ? transformRequestPromise : Promise.resolve(transformRequestPromise); } return Promise.resolve(res); }; var responseHeaders = {}; var paramsString = ''; if (params) { paramsString = '?' + querystring.stringify(params); } var finalURL = isMongoRequest ? _this.protocol + '://' + _this.url : _this.protocol + '://' + _this.url + '/' + app + '/' + path + paramsString; return handleTransformRequest(Object.assign({}, { url: finalURL }, requestOptions)).then(function (ts) { var transformedRequest = Object.assign({}, ts); var url = transformedRequest.url; delete transformedRequest.url; var controller = new AbortController(); var signal = controller.signal; var fetchPromise = fetch(url || finalURL, Object.assign({}, transformedRequest, { // apply timestamp header for RS API headers: isRSAPI && !isMongoRequest ? Object.assign({}, transformedRequest.headers, { 'x-timestamp': new Date().getTime() }) : transformedRequest.headers, signal: signal // Attach the abort signal to the fetch request })); var timeoutPromise = new Promise(function (_, rejectTP) { if (httpRequestTimeout > 0) { setTimeout(function () { rejectTP(new Error('Request timeout')); controller.abort(); }, httpRequestTimeout); } }); return Promise.race([fetchPromise, timeoutPromise]).then(function (res) { if (res.status >= 500) { return reject(res); } responseHeaders = res.headers; return res.json().then(function (data) { if (res.status >= 400) { return reject(res); } if (data && data.error) { return reject(data); } // Handle error from RS API RESPONSE if (isRSAPI && data && Object.prototype.toString.call(data) === '[object Object]') { if (body && body.query && body.query instanceof Array) { var errorResponses = 0; var allResponses = body.query.filter(function (q) { return q.execute || q.execute === undefined; }).length; if (data) { Object.keys(data).forEach(function (key) { if (data[key] && Object.prototype.hasOwnProperty.call(data[key], 'error') && !!data[key].error) { errorResponses += 1; } }); } // reject only when all responses have an error if (errorResponses > 0 && allResponses === errorResponses) { return reject(data); } } } // Handle error from _msearch response if (data && data.responses instanceof Array) { var _allResponses = data.responses.length; var _errorResponses = data.responses.filter(function (entry) { return Object.prototype.hasOwnProperty.call(entry, 'error'); }).length; // reject only when all responses have an error if (_allResponses === _errorResponses) { return reject(data); } } var response = Object.assign({}, data, { _timestamp: timestamp, _headers: responseHeaders }); return resolve(response); }).catch(function (e) { return reject(e); }); }).catch(function (e) { return reject(e); }); }).catch(function (err) { return reject(err); }); } catch (e) { return reject(e); } }); } /** * Index Service * @param {Object} args * @param {String} args.type * @param {Object} args.body * @param {String} args.id */ function indexApi(args) { var parsedArgs = removeUndefined(args); // Validate arguments var valid = validate(parsedArgs, { body: 'object' }); if (valid !== true) { throw valid; } var _parsedArgs$type = parsedArgs.type, type = _parsedArgs$type === undefined ? '_doc' : _parsedArgs$type, id = parsedArgs.id, body = parsedArgs.body; delete parsedArgs.type; delete parsedArgs.body; delete parsedArgs.id; var path = void 0; if (id) { path = type ? type + '/' + encodeURIComponent(id) : encodeURIComponent(id); } else { path = type; } return this.performFetchRequest({ method: 'POST', path: path, params: parsedArgs, body: body }); } /** * Get Service * @param {Object} args * @param {String} args.type * @param {String} args.id */ function getApi(args) { var parsedArgs = removeUndefined(args); // Validate arguments var valid = validate(parsedArgs, { id: 'string|number' }); if (valid !== true) { throw valid; } var _parsedArgs$type = parsedArgs.type, type = _parsedArgs$type === undefined ? '_doc' : _parsedArgs$type, id = parsedArgs.id; delete parsedArgs.type; delete parsedArgs.id; var path = type + '/' + encodeURIComponent(id); return this.performFetchRequest({ method: 'GET', path: path, params: parsedArgs }); } /** * Update Service * @param {Object} args * @param {String} args.type * @param {Object} args.body * @param {String} args.id */ function updateApi(args) { var parsedArgs = removeUndefined(args); // Validate arguments var valid = validate(parsedArgs, { id: 'string|number', body: 'object' }); if (valid !== true) { throw valid; } var _parsedArgs$type = parsedArgs.type, type = _parsedArgs$type === undefined ? '_doc' : _parsedArgs$type, id = parsedArgs.id, body = parsedArgs.body; delete parsedArgs.type; delete parsedArgs.id; delete parsedArgs.body; var path = type + '/' + encodeURIComponent(id) + '/_update'; return this.performFetchRequest({ method: 'POST', path: path, params: parsedArgs, body: body }); } /** * Delete Service * @param {Object} args * @param {String} args.type * @param {String} args.id */ function deleteApi(args) { var parsedArgs = removeUndefined(args); // Validate arguments var valid = validate(parsedArgs, { id: 'string|number' }); if (valid !== true) { throw valid; } var _parsedArgs$type = parsedArgs.type, type = _parsedArgs$type === undefined ? '_doc' : _parsedArgs$type, id = parsedArgs.id; delete parsedArgs.type; delete parsedArgs.id; var path = type + '/' + encodeURIComponent(id); return this.performFetchRequest({ method: 'DELETE', path: path, params: parsedArgs }); } /** * Bulk Service * @param {Object} args * @param {String} args.type * @param {Object} args.body */ function bulkApi(args) { var parsedArgs = removeUndefined(args); // Validate arguments var valid = validate(parsedArgs, { body: 'object' }); if (valid !== true) { throw valid; } var type = parsedArgs.type, body = parsedArgs.body; delete parsedArgs.type; delete parsedArgs.body; var path = void 0; if (type) { path = type + '/_bulk'; } else { path = '_bulk'; } return this.performFetchRequest({ method: 'POST', path: path, params: parsedArgs, body: body }); } /** * Search Service * @param {Object} args * @param {String} args.type * @param {Object} args.body */ function searchApi(args) { var parsedArgs = removeUndefined(args); // Validate arguments var valid = validate(parsedArgs, { body: 'object' }); if (valid !== true) { throw valid; } var type = void 0; if (Array.isArray(parsedArgs.type)) { type = parsedArgs.type.join(); } else { // eslint-disable-next-line type = parsedArgs.type; } var body = parsedArgs.body; delete parsedArgs.type; delete parsedArgs.body; var path = void 0; if (type) { path = type + '/_search'; } else { path = '_search'; } return this.performFetchRequest({ method: 'POST', path: path, params: parsedArgs, body: body }); } /** * Msearch Service * @param {Object} args * @param {String} args.type * @param {Object} args.body */ function msearchApi(args) { var parsedArgs = removeUndefined(args); // Validate arguments var valid = validate(parsedArgs, { body: 'object' }); if (valid !== true) { throw valid; } var type = void 0; if (Array.isArray(parsedArgs.type)) { type = parsedArgs.type.join(); } else { type = parsedArgs.type; } var body = parsedArgs.body; delete parsedArgs.type; delete parsedArgs.body; var path = void 0; if (type) { path = type + '/_msearch'; } else { path = '_msearch'; } return this.performFetchRequest({ method: 'POST', path: path, params: parsedArgs, body: body }); } /** * ReactiveSearch API Service for v3 * @param {Array<Object>} query * @param {Object} settings * @param {boolean} settings.recordAnalytics * @param {boolean} settings.userId * @param {boolean} settings.enableQueryRules * @param {boolean} settings.customEvents */ function reactiveSearchApi(query, settings, params) { var parsedSettings = removeUndefined(settings); // Validate query var valid = validateRSQuery(query); if (valid !== true) { throw valid; } var body = { settings: parsedSettings, query: query }; if (this.mongodb) { Object.assign(body, { mongodb: getMongoRequest(this.app, this.mongodb) }); } return this.performFetchRequest({ method: 'POST', path: '_reactivesearch', body: body, headers: getTelemetryHeaders(this.enableTelemetry, !this.mongodb), isRSAPI: true, isMongoRequest: !!this.mongodb, params: params, httpRequestTimeout: this.httpRequestTimeout || 0 }); } /** * ReactiveSearch API Service for v3 * @param {Array<Object>} query * @param {Object} settings * @param {boolean} settings.recordAnalytics * @param {boolean} settings.userId * @param {boolean} settings.enableQueryRules * @param {boolean} settings.customEvents */ function reactiveSearchv3Api(query, settings, params) { var parsedSettings = removeUndefined(settings); // Validate query var valid = validateRSQuery(query); if (valid !== true) { throw valid; } var body = { settings: parsedSettings, query: query }; if (this.mongodb) { Object.assign(body, { mongodb: getMongoRequest(this.app, this.mongodb) }); } return this.performFetchRequest({ method: 'POST', path: '_reactivesearch.v3', body: body, headers: getTelemetryHeaders(this.enableTelemetry, !this.mongodb), isRSAPI: true, isMongoRequest: !!this.mongodb, params: params, httpRequestTimeout: this.httpRequestTimeout || 0 }); } /** * To get mappings */ function getMappings() { return this.performFetchRequest({ method: 'GET', path: '_mapping' }); } /** * ReactiveSearch suggestions API for v3 * @param {Array<Object>} query * @param {Object} settings * @param {boolean} settings.recordAnalytics * @param {boolean} settings.userId * @param {boolean} settings.enableQueryRules * @param {boolean} settings.customEvents */ function getSuggestionsv3Api(query, settings) { var parsedSettings = removeUndefined(settings); // Validate query var valid = validateRSQuery(query); if (valid !== true) { throw valid; } var body = { settings: parsedSettings, query: query }; if (this.mongodb) { Object.assign(body, { mongodb: getMongoRequest(this.app, this.mongodb) }); } return this.performFetchRequest({ method: 'POST', path: '_reactivesearch.v3', body: body, headers: getTelemetryHeaders(this.enableTelemetry), isRSAPI: true, isSuggestionsAPI: true, isMongoRequest: !!this.mongodb }); } function appbasejs(config) { var client = new AppBase(config); AppBase.prototype.performFetchRequest = fetchRequest; AppBase.prototype.index = indexApi; AppBase.prototype.get = getApi; AppBase.prototype.update = updateApi; AppBase.prototype.delete = deleteApi; AppBase.prototype.bulk = bulkApi; AppBase.prototype.search = searchApi; AppBase.prototype.msearch = msearchApi; AppBase.prototype.reactiveSearch = reactiveSearchApi; AppBase.prototype.reactiveSearchv3 = reactiveSearchv3Api; AppBase.prototype.getQuerySuggestions = getSuggestionsv3Api; AppBase.prototype.getMappings = getMappings; AppBase.prototype.setHeaders = function setHeaders() { var headers = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var shouldEncode = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; // Encode headers if (shouldEncode) { this.headers = encodeHeaders(headers); } else { this.headers = headers; } }; if (typeof window !== 'undefined') { window.Appbase = client; } return client; } export default appbasejs;