loop54-js-connector
Version:
JS Wrapper for Loop54 JSON API
787 lines (634 loc) • 28.6 kB
JavaScript
'use strict';
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var axios = _interopDefault(require('axios'));
let cookies = {
getItem: function (sKey) {
if (!sKey || typeof(document) == "undefined") {
return null;
}
return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
},
setItem: function (sKey, sValue, vEnd, sPath, sDomain, bSecure) {
if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey) || typeof(document) == "undefined") {
return false;
}
var sExpires = "";
if (vEnd) {
switch (vEnd.constructor) {
case Number:
sExpires = vEnd === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; max-age=" + vEnd;
break;
case String:
sExpires = "; expires=" + vEnd;
break;
case Date:
sExpires = "; expires=" + vEnd.toUTCString();
break;
}
}
document.cookie = encodeURIComponent(sKey) + "=" + encodeURIComponent(sValue) + sExpires + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "") + (bSecure ? "; secure" : "");
return true;
},
removeItem: function (sKey, sPath, sDomain) {
if (!this.hasItem(sKey)) {
return false;
}
document.cookie = encodeURIComponent(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT" + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "");
return true;
},
hasItem: function (sKey) {
if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) {
return false;
}
return (new RegExp("(?:^|;\\s*)" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie);
},
keys: function () {
var aKeys = document.cookie.replace(/((?:^|\s*;)[^\=]+)(?=;|$)|^\s*|\s*(?:\=[^;]*)?(?:\1|$)/g, "").split(/\s*(?:\=[^;]*)?;\s*/);
for (var nLen = aKeys.length, nIdx = 0; nIdx < nLen; nIdx++) {
aKeys[nIdx] = decodeURIComponent(aKeys[nIdx]);
}
return aKeys;
}
};
let core = {
versions: {
// Do not change `libVersion` manually. See README.
// The "5454545454-build-number" part will be replaced automatically during the build process.
libVersion: "1.16.186",
apiVersion: "V3"
},
userIdCookieKey: "Loop54User",
userIdCookiePath: "/",
setUserId: function () {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < 10; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
cookies.setItem(core.userIdCookieKey, text, 365 * 24 * 60 * 60, core.userIdCookiePath); //365 days
return text;
},
getUserId: function () {
var existing = cookies.getItem(core.userIdCookieKey);
if (existing) {
return existing;
}
return core.setUserId();
},
removeUserId: function () {
return cookies.removeItem(core.userIdCookieKey, core.userIdCookiePath);
},
call: function (endpoint, path, body, method, callback, userId, apiKey, customHeaders) {
if(!userId)
userId = core.getUserId();
body = JSON.parse(JSON.stringify(body));
var url = core.ensureProtocol(endpoint) + path;
var headers = {
"user-id": userId,
"lib-version": "JS:" + core.versions.libVersion,
"api-version": core.versions.apiVersion,
};
if(customHeaders){
for (const key in customHeaders) {
headers[key] = customHeaders[key];
}
}
if(apiKey)
headers["Loop54-key"] = apiKey;
var cancellationSource = axios.CancelToken.source();
var request = axios({
method: method ? method : "post",
url: url,
headers: headers,
responseType: "json",
data: body,
cancelToken: cancellationSource.token
})
.then(function (response) {
if(response.status === 200)
return response;
else {
return Promise.reject(response);
}
})
.catch(function (error) {
if (axios.isCancel(error)) {
return { cancelled:true };
}
var ret = error;
//if there is no data, that means something went wrong before we got a response
//construct a "fake" response object with the same properties as an error from the engine
if(!ret.response || !ret.response.data) {
ret = {
data: {
error: {
title: error.message
}
}
};
}
return Promise.reject(ret);
});
request.cancel = function () {
cancellationSource.cancel();
return request;
};
if (callback) {
request.then(callback).catch(function(response){
callback(response);
});
}
return request;
},
ensureProtocol: function (url) {
//make sure it starts with http or https
if (!url.startsWith("http://") && !url.startsWith("https://"))
url = "https://" + url;
//make sure it doesnt end with slash
while (url.endsWith("/"))
url = url.substring(0, url.length - 1);
return url;
},
returnError: function (message, callback) {
//construct a "fake" response object with the same properties as an error from the engine
var ret = {
data: {
error: {
title: message
}
}
};
if (callback) {
return callback(ret);
} else {
return Promise.reject(ret);
}
},
getOptionsAndCallback: function (args, numRequiredParameters, maxNumParameters) {
//too few parameters
if (args.length < numRequiredParameters)
return {
error: "Expected at least " + numRequiredParameters + " parameters."
};
//no options or other extra parameter
if (args.length == numRequiredParameters)
return {};
if(!maxNumParameters)
maxNumParameters = numRequiredParameters + 2;
//too many parameters
if (args.length > maxNumParameters)
return {
error: "Expected at most " + (maxNumParameters) + " parameters."
};
var options;
var callback;
//loop through the last parameters to find options and callback
for (var i = numRequiredParameters; i < args.length; i++) {
if (typeof(args[i]) === "function")
callback = args[i];
else if (typeof(args[i]) === "object")
options = args[i];
}
return {
options: options,
callback: callback
};
},
validateEvent: function(event) {
if (typeof event !== 'object') {
return "event needs to be an object.";
}
if (typeof event.type !== "string" || event.type.length === 0) {
return "type needs to be set, standard events are \"click\", \"addtocart\" and \"purchase\".";
}
if(!event.entity) {
return "entity needs to be set.";
}
if (!event.entity.type) {
return "entity needs to have a \"type\" set, usually this is \"Product\".";
}
if (!event.entity.id) {
return "entity needs to have an \"id\" provided, this is usually the productId.";
}
return null;
},
deleteCustomData: function(options){
let ret = JSON.parse(JSON.stringify(options));
delete ret.customData;
return ret;
}
};
/**
* Returns a client that can be used to communicate with the engine.
* @param {string} endpoint The complete URL to the engine.
*/
function getLoop54Client (endpoint, userId, apiKey, customHeaders) {
if (typeof endpoint !== 'string' || endpoint.length === 0) {
throw new Error('Parameter "endpoint" must be present and have a non-zero length.');
}
const currentUserPlaceholder = "(CurrentUser)";
return {
/**
* The engine endpoint this client communicates with.
*/
endpoint: endpoint,
/**
* The user ID to use for communications. If left falsy, cookies will be used to track user ID.
*/
userId: userId,
/**
* The api key to use for communications. This is required for administrative operations.
*/
apiKey: apiKey,
/**
* Object of custom headers to be set with the calls.
*/
customHeaders: customHeaders,
/**
* Used for performing autocomplete requests to the engine.
* @param {string} searchTerm The query to find suggestions.
*/
autoComplete: function (query) {
var args = core.getOptionsAndCallback(arguments, 1);
if (args.error) {
return core.returnError(args.error, args.callback);
}
var options = args.options || {};
var callback = args.callback || null;
if (typeof(query) !== "string" || query.length === 0) {
return core.returnError("query is either missing or not a string", callback);
}
var req = core.call(this.endpoint, "/autoComplete", {
query: query,
queriesOptions: core.deleteCustomData(options),
customData:options.customData
}, null, callback, userId, apiKey, customHeaders);
return req;
},
/**
* Used for tracking a user interaction.
* @param {string} eventType The type of event. Typically "click", "addtocart" or "purchase".
* @param {object} entity The entity that was interacted with. Must have properties "type" and "id".
* @param {string} orderId The id of the order. Typically only used with purchase events.
* @param {number} quantity The quantity of this product in the order. Typically only used with purchase events.
* @param {number} revenue The revenue for this product in the order. Typically only used with purchase events.
*/
createEvent: function (eventType, entity, orderId, quantity, revenue) {
var args = core.getOptionsAndCallback(arguments, 2, 7);
if (args.error) {
return core.returnError(args.error, args.callback);
}
var options = args.options || {};
var callback = args.callback || null;
var event = {
type: eventType,
entity: entity
};
// purchase event extra attributes
if (orderId) {
event.orderId = orderId;
}
if (quantity) {
event.quantity = parseFloat(quantity);
}
if (revenue) {
event.revenue = parseFloat(revenue);
}
var error = core.validateEvent(event, callback);
if(error)
return core.returnError(error, callback);
var req = core.call(this.endpoint, "/createEvents", {
events: [event],
customData:options.customData
}, null, callback, userId, apiKey, customHeaders);
return req;
},
/**
* Used for tracking a multiple user interactions, for instance when purchasing multiple products at once.
* @param {array} events The events to push. See createEvent for detailed information about events.
*/
createEvents: function (events) {
var args = core.getOptionsAndCallback(arguments, 1);
if (args.error) {
return core.returnError(args.error, args.callback);
}
var options = args.options || {};
var callback = args.callback || null;
if (!Array.isArray(events) || events.length === 0) {
return core.returnError("Events must be a non-empty array", callback);
}
if (events.some(core.validateEvent)) {
return core.returnError(
"Malformed event errors: [" + events.map(core.validateEvent).filter(function (e) {
return e;
}).map(function (e) {
return '"' + e + '"';
}).join(",") + "]",
callback
)
}
var req = core.call(this.endpoint, "/createEvents", {
events: events,
customData:options.customData
}, null, callback, userId, apiKey, customHeaders);
return req;
},
/**
* Used for performing getRelatedEntities requests to the engine.
* @param {object} entity The entity for which to find related entities.
*/
getRelatedEntities: function (entity) {
var args = core.getOptionsAndCallback(arguments, 1, 4);
if (args.error) {
return core.returnError(args.error, args.callback);
}
var options = args.options || {};
var callback = args.callback || null;
//validate entity
if (typeof(entity) !== "object" || !entity.type || !entity.id) {
return core.returnError('entity must be an object with properties "type" and "id".', callback);
}
var req = core.call(this.endpoint, "/getRelatedEntities", {
entity: entity,
resultsOptions: core.deleteCustomData(options),
customData: options.customData,
}, null, callback, userId, apiKey, customHeaders);
return req;
},
/**
* Used for performing getComplementaryEntities requests to the engine.
* @param {object} entity The entity for which to find complementary entities.
*/
getComplementaryEntities: function (entity) {
var args = core.getOptionsAndCallback(arguments, 1, 4);
if (args.error) {
return core.returnError(args.error, args.callback);
}
var options = args.options || {};
var callback = args.callback || null;
//validate entity
if (typeof(entity) !== "object" || !entity.type || !entity.id) {
return core.returnError('entity must be an object with properties "type" and "id".', callback);
}
var req = core.call(this.endpoint, "/getComplementaryEntities", {
entity: entity,
resultsOptions: core.deleteCustomData(options),
customData: options.customData,
}, null, callback, userId, apiKey, customHeaders);
return req;
},
/**
* Used for performing getBasketRecommendations requests to the engine.
* @param {object[]} entities The set of entities in the basket to get recommendations for.
*/
getBasketRecommendations: function (entities) {
var args = core.getOptionsAndCallback(arguments, 1, 4);
if (args.error) {
return core.returnError(args.error, args.callback);
}
var options = args.options || {};
var callback = args.callback || null;
//validate entities
if (!Array.isArray(entities) || entities.filter(entity => entity.type && entity.id).length !== entities.length) {
return core.returnError('entities must be a non-empty array where each item is an object with properties "type" and "id".', callback);
}
var req = core.call(this.endpoint, "/getBasketRecommendations", {
entities: entities,
resultsOptions: core.deleteCustomData(options),
customData: options.customData,
}, null, callback, userId, apiKey, customHeaders);
return req;
},
/**
* Used for performing getRecommendedEntities requests to the engine.
*/
getRecommendedEntities: function () {
var args = core.getOptionsAndCallback(arguments, 0, 2);
if (args.error) {
return core.returnError(args.error, args.callback);
}
var options = args.options || {};
var callback = args.callback || null;
var req = core.call(this.endpoint, "/getRecommendedEntities", {
resultsOptions: core.deleteCustomData(options),
customData: options.customData,
}, null, callback, userId, apiKey, customHeaders);
return req;
},
/**
* Used for performing getRecentEntities requests to the engine for the current user.
* @param {string} behaviorType The interaction or navigation type to include (such as "click", "purchase" or "search").
* @param {string[]} [entityType] The entity types to include (such as "Product" or "Query") or null for all.
*/
getRecentEntitiesForCurrentUser: function (behaviorType, entityType) {
return this.getRecentEntities(behaviorType, currentUserPlaceholder, entityType);
},
/**
* Used for performing getRecentEntities requests to the engine.
* @param {string} behaviorType The interaction or navigation type to include (such as "click", "purchase" or "search").
* @param {string} [forUserId] User ID (normally the same as the one in the User-Id header) to retrieve the most recent entities for that user, null or undefined to retrieve the globally most recent entities.
* @param {string[]} [entityType] The entity types to include (such as "Product" or "Query") or null for all.
*/
getRecentEntities: function (behaviorType, forUserId, entityType) {
var argumentsArray = Array.prototype.slice.call(arguments);
if (typeof forUserId !== "string" && forUserId !== null) {
argumentsArray.splice(1, 0, null);
forUserId = null;
}
if (!Array.isArray(entityType) && entityType !== null) {
argumentsArray.splice(2, 0, null);
entityType = null;
}
var args = core.getOptionsAndCallback(argumentsArray.slice(2), 1);
if (args.error) {
return core.returnError(args.error, args.callback);
}
var options = args.options || {};
var callback = args.callback || null;
var req = core.call(this.endpoint, "/getRecentEntities", {
behaviorType,
forUserId,
entityType,
resultsOptions: core.deleteCustomData(options),
customData: options.customData,
}, null, callback, userId, apiKey, customHeaders);
return req;
},
/**
* Used for performing getPopularEntities requests to the engine for the current user.
* @param {string} behaviorType The interaction or navigation type to include (such as "click", "purchase" or "search").
* @param {string[]} [entityType] The entity types to include (such as "Product" or "Query") or null for all.
*/
getPopularEntitiesForCurrentUser: function (behaviorType, entityType) {
return this.getPopularEntities(behaviorType, currentUserPlaceholder, entityType);
},
/**
* Used for performing getPopularEntities requests to the engine.
* @param {string} behaviorType The interaction or navigation type to include (such as "click", "purchase" or "search").
* @param {string} [forUserId] User ID (normally the same as the one in the User-Id header) to retrieve the most common entities for that user, null or undefined to retrieve the globally most common entities.
* @param {string[]} [entityType] The entity types to include (such as "Product" or "Query") or null for all.
*/
getPopularEntities: function (behaviorType, forUserId, entityType) {
var argumentsArray = Array.prototype.slice.call(arguments);
if (typeof forUserId !== "string" && forUserId !== null) {
argumentsArray.splice(1, 0, null);
forUserId = null;
}
if (!Array.isArray(entityType) && entityType !== null) {
argumentsArray.splice(2, 0, null);
entityType = null;
}
var args = core.getOptionsAndCallback(argumentsArray.slice(2), 1);
if (args.error) {
return core.returnError(args.error, args.callback);
}
var options = args.options || {};
var callback = args.callback || null;
var req = core.call(this.endpoint, "/getPopularEntities", {
behaviorType,
forUserId,
entityType,
resultsOptions: core.deleteCustomData(options),
customData: options.customData,
}, null, callback, userId, apiKey, customHeaders);
return req;
},
/**
* Used for performing getEntitiesByAttribute requests to the engine.
* @param {string} attributeName The name of the attribute for which to get entities.
* @param {any} attributeValues The value of the attribute for which to get entities. This can be a single value, or an array of values.
*/
getEntitiesByAttribute: function (attributeName, attributeValues) {
var args = core.getOptionsAndCallback(arguments, 2);
if (args.error) {
return core.returnError(args.error, args.callback);
}
var options = args.options || {};
var callback = args.callback || null;
//validate arguments
if (typeof(attributeName) !== "string") {
return core.returnError("Missing argument attributeName or attributeName was not of type string.", callback);
}
//copy alias to separate variable and remove it from options
var alias = options.requestAlias;
if (alias)
delete options.requestAlias;
var req = core.call(this.endpoint, "/getEntitiesByAttribute", {
attribute: {
name: attributeName,
value: attributeValues
},
requestAlias: alias,
resultsOptions: core.deleteCustomData(options),
customData:options.customData
}, null, callback, userId, apiKey, customHeaders);
return req;
},
/**
* Used for performing getEntities requests to the engine.
*/
getEntities: function () {
var args = core.getOptionsAndCallback(arguments, 0);
if (args.error) {
return core.returnError(args.error, args.callback);
}
var options = args.options || {};
var callback = args.callback || null;
var req = core.call(this.endpoint, "/getEntities", {
resultsOptions: core.deleteCustomData(options),
customData:options.customData
}, null, callback, userId, apiKey, customHeaders);
return req;
},
/**
* Used for performing search requests to the engine.
* @param {string} query The query to search for
*/
search: function (query) {
var args = core.getOptionsAndCallback(arguments, 1);
if (args.error) {
return core.returnError(args.error, args.callback);
}
var options = args.options || {};
var callback = args.callback || null;
//validate input
if (typeof(query) !== "string" || query.length === 0) {
return core.returnError("query is either missing or not a string", callback);
}
var req = core.call(this.endpoint, "/search", {
query: query,
relatedResultsOptions: options.relatedResultsOptions,
spellingSuggestionsOptions: options.spellingSuggestionsOptions,
relatedQueriesOptions: options.relatedQueriesOptions,
resultsOptions: {
sortBy: options.sortBy,
filter: options.filter,
take: options.take,
skip: options.skip,
facets: options.facets
},
customData:options.customData
}, null, callback, userId, apiKey, customHeaders);
return req;
},
/**
* Used for telling the engine to re-sync the catalog.
*/
sync: function () {
var args = core.getOptionsAndCallback(arguments, 0);
if (args.error) {
return core.returnError(args.error, args.callback);
}
var options = args.options || {};
var callback = args.callback || null;
var req = core.call(this.endpoint, "/sync", {
customData:options.customData
}, null, callback, userId, apiKey, customHeaders);
return req;
},
/**
* Used to perform a request to get information about attributes, indexed and non-indexed.
*/
getIndexedAttributes: function () {
var args = core.getOptionsAndCallback(arguments, 0);
if (args.error) {
return core.returnError(args.error, args.callback);
}
var options = args.options || {};
var callback = args.callback || null;
var req = core.call(this.endpoint, "/getIndexedAttributes", {
customData:options.customData
}, null, callback, userId, apiKey, customHeaders);
return req;
},
/**
* Used to perform a request to get a list of all unique values that are indexed for the provided attribute.
* @param {string} attributeName The name of the attribute for which to fetch indexed values.
*/
getIndexedAttributeValues: function (attributeName) {
var args = core.getOptionsAndCallback(arguments, 1);
if (args.error) {
return core.returnError(args.error, args.callback);
}
var options = args.options || {};
var callback = args.callback || null;
//validate input
if (typeof(attributeName) !== "string" || attributeName.length === 0) {
return core.returnError("attributeName is either missing or not a string", callback);
}
var req = core.call(this.endpoint, "/getIndexedAttributeValues", {
attributeName: attributeName,
customData:options.customData
}, null, callback, userId, apiKey, customHeaders);
return req;
},
/**
* Used for removing current userId cookie and setting a new one.
*/
generateNewUserId: function () {
core.removeUserId();
return core.setUserId();
}
}
}
module.exports = getLoop54Client;