appbase-js
Version:
Appbase.io streaming client lib for Javascript
978 lines (849 loc) • 25.8 kB
JavaScript
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;