UNPKG

ecomplus-sdk

Version:

JS SDK for E-Com Plus APIs public resources

968 lines (867 loc) 28.2 kB
(function () { 'use strict' var isNodeJs = false // Verify if the script is Node JS if (typeof module !== 'undefined' && module.exports) { isNodeJs = true } // define APIs hosts var apiStore = 'api.e-com.plus' var apiStoreCache = 'ioapi.ecvol.com' var apiMain = 'e-com.plus' var apiMainCache = 'io.ecvol.com' var apiSearch = 'apx-search.e-com.plus' var apiGraphs = 'apx-graphs.e-com.plus' var apiModules = 'apx-mods.e-com.plus' var apiStorefront = 'iostorefront.ecvol.com' var EcomIo = (function () { var storeId, storeObjectId, https, logger if (isNodeJs && typeof XMLHttpRequest === 'undefined' && typeof XDomainRequest === 'undefined') { https = require('https') } var logHeader = function (logType) { // common header for logger.log return 'E-Com Plus SDK ' + logType + ':' } var errorHandling = function (callback, errMsg, responseBody) { if (typeof callback === 'function') { var err = new Error(errMsg) if (responseBody === undefined) { // body null when error occurs before send API request callback(err, null) } else { callback(err, responseBody) } } logger.log(logHeader('WARNING') + '\n' + errMsg) } var runMethod = function (callback, endpoint, host, body, tries) { var msg if (typeof callback !== 'function') { msg = 'You need to specify a callback function to receive the request response' errorHandling(null, msg) return } var path if (!host) { // default to E-Com Plus Store API // https://developers.e-com.plus/docs/reference/store/ host = apiStore path = '/v1' } else { switch (host) { case apiStoreCache: // host defined to Store API cache // store ID on URL, eg.: /100/v1 path = '/' + storeId + '/v1' break case apiStore: path = '/v1' break default: // Storefront API, Main API, Graphs API or Search API // https://ecomplus.docs.apiary.io/ // https://developers.e-com.plus/docs/reference/graphs/ // https://developers.e-com.plus/docs/reference/search/ path = '/api/v1' } } path += endpoint // eg.: /v1/products.json // retry up to 3 times if API returns 503 if (typeof tries !== 'number') { tries = 0 } sendRequest(tries, host, path, body, callback, endpoint) /* msg = logHeader('INFO') + '\nAPI endpoint' + '\n' + endpoint logger.log(msg) */ } var sendRequest = function (tries, host, path, body, callback, endpoint) { // send request to API var method if (!body) { // default to GET request method = 'GET' } else { // request with body method = 'POST' } // default request headers var headers = {} // add custom headers only when necessary // prevent CORS preflight request switch (host) { case apiStore: case apiSearch: case apiGraphs: case apiModules: headers['X-Store-ID'] = storeId headers['Content-Type'] = 'application/json' break } var resend = function () { if (endpoint) { // tried once with error // try the inverse, live to cache or cache to live switch (host) { case apiStoreCache: // try with live Store API host = apiStore runMethod(callback, endpoint, host, body, tries) return case apiMainCache: // try with live E-Com Plus Main API host = apiMain runMethod(callback, endpoint, host, body, tries) return case apiStore: // try with cached Store API host = apiStoreCache runMethod(callback, endpoint, host, body, tries) return case apiMain: // try with cached E-Com Plus Main API host = apiMainCache runMethod(callback, endpoint, host, body, tries) return } } // try to resend request setTimeout(function () { sendRequest(tries, host, path, body, callback) }, 500) } if (https) { // call with NodeJS http module var options = { hostname: host, path: path, method: method, headers: headers } var req = https.request(options, function (res) { if (res.statusCode === 503 && tries < 3) { resend() // consume response data to free up memory res.resume() return } var rawData = '' res.setEncoding('utf8') res.on('data', function (chunk) { // buffer rawData += chunk }) res.on('end', function () { // treat response response(res.statusCode, rawData, callback) }) }) req.on('error', function (err) { logger.error(err) // callback with null body callback(err, null) }) if (body) { // send JSON body req.write(JSON.stringify(body)) } req.end() } else { // call with AJAX var ajax if (window.XDomainRequest) { // IE 8,9 ajax = new XDomainRequest() ajax.ontimeout = ajax.onerror = function () { resend() } ajax.onload = function () { // treat response response(200, this.responseText, callback) } } else { // supported by recent browsers ajax = new XMLHttpRequest() ajax.onreadystatechange = function () { if (this.readyState === 4) { // request finished and response is ready if (this.status === 503 && tries < 3) { resend() } else { // treat response response(this.status, this.responseText, callback) } } } } var url = 'https://' + host + path ajax.open(method, url, true) for (var header in headers) { if (headers.hasOwnProperty(header)) { ajax.setRequestHeader(header, headers[header]) } } if (body) { // send JSON body ajax.send(JSON.stringify(body)) } else { ajax.send() } } // tried more once tries++ } var response = function (status, data, callback) { // treat request response var body try { // expecting valid JSON response body body = JSON.parse(data) } catch (err) { logger.error(err) // callback with null body callback(err, null) return } if (status === 200) { // err null callback(null, body) } else { var msg if (body.hasOwnProperty('message')) { msg = body.message } else { // probably an error response from Graphs or Search API // not handling Neo4j and Elasticsearch errors msg = 'Unknown error, see response objet to more info' } errorHandling(callback, msg, body) } } var idValidate = function (callback, id) { // check MongoDB ObjectID var msg if (typeof id === 'string') { // RegEx pattern if (id.match(/^[a-f0-9]{24}$/)) { return true } else { msg = 'ID must be a valid 24 hexadecimal digits string, lowercase only' } } else { msg = 'ID is required and must be a string' } errorHandling(callback, msg) return false } var getById = function (callback, resource, id) { if (idValidate(callback, id)) { // use Cloudflare cache of Store API var host = apiStoreCache runMethod(callback, '/' + resource + '/' + id + '.json', host) } } var getByField = function (callback, field, fieldName, resource, endpoint, ioMethod) { // common function to all getAnyByAny methods if (typeof field === 'string') { runMethod(function (err, body) { // replace callback if (!err) { var results = body.result // results must be an array of one object with _id property if (Array.isArray(results) && typeof results[0] === 'object' && results[0] !== null) { // return resource object ioMethod(callback, results[0]._id) } else { var msg = 'Any ' + resource + ' found with this ' + fieldName errorHandling(callback, msg, body) } } else { // only pass the error callback(err, body) } }, endpoint) } else { var msg = 'The' + fieldName + ' is required and must be a string' errorHandling(callback, msg) } } var getList = function (callback, resource, offset, limit, sort, fields, customQuery) { // common function to all listAny methods var query = queryString(offset, limit, sort, fields, customQuery) var endpoint = '/' + resource + '.json' var host if (query === '') { // cacheable // use Cloudflare cache of Store API host = apiStoreCache } else { endpoint += query } runMethod(callback, endpoint, host) } var queryString = function (offset, limit, sort, fields, customQuery) { // mount query string with function params // common Restful URL params // ref.: https://developers.e-com.plus/docs/reference/store/#url-params var params = {} // default for empty string var query = '' if (typeof customQuery !== 'string') { // pagination if (typeof offset === 'number' && offset > 0) { params.offset = offset } if (typeof limit === 'number' && limit > 0) { params.limit = limit } if (typeof sort === 'number') { // defines most common sorting options switch (sort) { case 1: // sort by name asc params.sort = 'name' break case 2: // sort by name desc params.sort = '-name' break } } if (Array.isArray(fields)) { var fieldsString = '' for (var i = 0; i < fields.length; i++) { if (typeof fields[i] === 'string') { if (fieldsString !== '') { // separate fields names by , // url encoded fieldsString += '%2C' } // fields must be valid object properties // does not need to encode fieldsString += fields[i] } } if (fieldsString !== '') { params.fields = fieldsString } } // serialize object to query string for (var param in params) { if (params.hasOwnProperty(param)) { if (query !== '') { query += '&' } query += param + '=' + params[param] } } } else { query = customQuery } if (query !== '') { query = '?' + query } return query } return { 'init': function (callback, StoreId, StoreObjectId, Logger) { if (typeof Logger === 'object' && Logger.hasOwnProperty('log') && Logger.hasOwnProperty('error')) { // log on file logger = Logger } else { logger = console } if (StoreId && StoreObjectId) { // set store ID storeId = StoreId storeObjectId = StoreObjectId if (typeof callback === 'function') { // simulate domains API response var body = { 'id': null, 'channel_id': null, 'store_id': storeId, 'store_object_id': storeObjectId, 'default_lang': null } // err null callback(null, body) } } else { if (typeof location === 'object') { // get store ID from Main API // http://ecomplus.docs.apiary.io/ var host = apiMainCache var endpoint = '/domains/' + window.location.hostname + '.json' // middleware callback var Callback = function (err, body) { if (!err) { storeId = body.store_id storeObjectId = body.store_object_id } if (typeof callback === 'function') { // pass to callback callback(err, body) } } runMethod(Callback, endpoint, host) } else { var msg = 'It is necessary to specify the store ID as an init argument' errorHandling(callback, msg) } } }, // return current store ID 'storeId': function () { return storeId }, // return current store object ID 'storeObjectId': function () { return storeObjectId }, /* Store API https://developers.e-com.plus/docs/reference/store/ */ 'getStore': function (callback, id) { if (id === undefined && storeObjectId) { id = storeObjectId } getById(callback, 'stores', id) }, 'getProduct': function (callback, id) { getById(callback, 'products', id) }, 'getProductBySku': function (callback, sku) { var endpoint = '/products.json?sku=' + sku getByField(callback, sku, 'SKU', 'product', endpoint, EcomIo.getProduct) }, 'getOrder': function (callback, id) { getById(callback, 'orders', id) }, 'getCart': function (callback, id) { getById(callback, 'carts', id) }, 'getCustomer': function (callback, id) { getById(callback, 'customers', id) }, 'getApplication': function (callback, id) { getById(callback, 'applications', id) }, 'getBrand': function (callback, id) { getById(callback, 'brands', id) }, 'findBrandBySlug': function (callback, slug) { var endpoint = '/brands.json?limit=1&slug=' + slug runMethod(callback, endpoint) }, 'listBrands': function (callback, offset, limit, sort, fields, customQuery) { getList(callback, 'brands', offset, limit, sort, fields, customQuery) }, 'getCategory': function (callback, id) { getById(callback, 'categories', id) }, 'findCategoryBySlug': function (callback, slug) { var endpoint = '/categories.json?limit=1&slug=' + slug runMethod(callback, endpoint) }, 'listCategories': function (callback, offset, limit, sort, fields, customQuery) { getList(callback, 'categories', offset, limit, sort, fields, customQuery) }, 'getCollection': function (callback, id) { getById(callback, 'collections', id) }, 'findCollectionBySlug': function (callback, slug) { var endpoint = '/collections.json?limit=1&slug=' + slug runMethod(callback, endpoint) }, 'listCollections': function (callback, offset, limit, sort, fields, customQuery) { getList(callback, 'collections', offset, limit, sort, fields, customQuery) }, // generic methods to extend usage 'getById': getById, 'getList': getList, /* Search API https://developers.e-com.plus/docs/reference/search/ */ 'searchProducts': function (callback, term, from, size, sort, specs, ids, brands, categories, prices, dsl) { var host = apiSearch // proxy will pass XGET // var method = 'POST' var endpoint = '/items.json' var body if (typeof dsl === 'object' && dsl !== null) { // custom Query DSL // must be a valid search request body // https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html body = dsl } else { /* // term is required if (typeof term !== 'string') { msg = 'Search term is required and must be a string' errorHandling(callback, msg) return } */ // handle results sorting var order = [ { 'available': { 'order': 'desc' } }, { 'ad_relevance': { 'order': 'desc' } }, '_score' ] // defines most common sorting options switch (sort) { case 1: // sort by sales after relevance order.splice(2, 0, { 'sales': { 'order': 'desc' } }) break case 2: // sort by price // lowest price -> highest price order.splice(1, 0, { 'price': { 'order': 'asc' } }) break case 3: // sort by price // highest price -> lowest price order.splice(1, 0, { 'price': { 'order': 'desc' } }) break default: // default sort by views after preseted sorting options order.push({ 'views': { 'order': 'desc' } }) } body = { 'query': { 'bool': { // condition, only visible products 'filter': [ { 'term': { 'visible': true } } ] } }, 'sort': order, 'aggs': { 'brands': { 'terms': { 'field': 'brands.name' } }, 'categories': { 'terms': { 'field': 'categories.name' } }, // ref.: https://github.com/elastic/elasticsearch/issues/5789 'specs': { 'nested': { 'path': 'specs' }, 'aggs': { 'grid': { 'terms': { 'field': 'specs.grid', 'size': 30 }, 'aggs': { 'text': { 'terms': { 'field': 'specs.text' } } } } } }, // Metric Aggregations 'min_price': { 'min': { 'field': 'price' } }, 'max_price': { 'max': { 'field': 'price' } }, 'avg_price': { 'avg': { 'field': 'price' } } } } // search term if (typeof term === 'string') { // https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html body.query.bool.must = { 'multi_match': { 'query': term, 'fields': [ 'name', 'keywords' ] } } // handle terms suggestion // 'did you mean?' // https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters.html body.suggest = { 'text': term, 'words': { 'term': { 'field': 'name' } } } } // pagination // https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-from-size.html if (typeof from === 'number' && from > 0) { // offset body.from = from } if (typeof size === 'number' && size > 0) { // limit body.size = size } else { // default page size body.size = 24 } // filters if (typeof specs === 'object' && specs !== null && Object.keys(specs).length > 0) { // nested ELS object // http://nocf-www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-nested-query.html for (var key in specs) { if (Array.isArray(specs[key]) && specs[key].length > 0) { // add filter body.query.bool.filter.push({ 'nested': { 'path': 'specs', 'query': { 'bool': { 'filter': [ { 'term': { 'specs.grid': key } }, { 'terms': { 'specs.text': specs[key] } } ] } } } }) } } } if (Array.isArray(ids) && ids.length > 0) { // add filter // search by product Object IDs body.query.bool.filter.push({ 'terms': { '_id': ids } }) } // handle filter by categories and brands with IDs or names var filterFields = { 'brands': brands, 'categories': categories } for (var field in filterFields) { var filterVal = filterFields[field] if (Array.isArray(filterVal) && filterVal.length > 0) { if (/^[a-f0-9]{24}$/.test(filterVal[0])) { // filtering by ObjectID field += '._id' } else { // filtering by name field += '.name' } // add filter var filterObj = { 'terms': {} } filterObj.terms[field] = filterVal body.query.bool.filter.push(filterObj) } } // price ranges if (typeof prices === 'object' && prices !== null && Object.keys(prices).length > 0) { var rangeQuery = { 'range': { 'price': {} } } if (typeof prices.min === 'number' && !isNaN(prices.min)) { rangeQuery.range.price.gte = prices.min } if (typeof prices.max === 'number' && !isNaN(prices.max)) { rangeQuery.range.price.lte = prices.max } // add filter body.query.bool.filter.push(rangeQuery) } } /* msg = logHeader('INFO') + '\nQuery DSL' + '\n' + JSON.stringify(body) logger.log(msg) */ runMethod(callback, endpoint, host, body) }, /* Storefront API no documentation yet */ 'mapBySlug': function (callback, slug) { // returns resource and ID var host = apiStorefront // replace / with $ on slug to escape URL var endpoint = '/' + storeId + '@' + slug.replace(/\//g, '$') + '.json' // middleware callback var Callback = function (err, body) { if (!err) { var val = body.GET if (val) { /* { "GET": "[resource]@[id]" } */ val = val.split('@') // overwrite body body = { 'resource': val[0], '_id': val[1] } // success, pass the object callback(null, body) } else { // Redis key not found body = { 'status': 404, 'error_code': -44, 'message': 'Page not found, invalid slug or store ID' } var msg = body.message errorHandling(callback, msg, body) } } else { // just pass the error callback(err, body) } } runMethod(Callback, endpoint, host) }, 'mapByWindowUri': function (callback) { // convenience only if (typeof location === 'object') { // remove the first / char from pathname var slug = window.location.pathname.slice(1) EcomIo.mapBySlug(callback, slug) } else { // on browser only var msg = 'This method is available client side only (JS on browser)' errorHandling(callback, msg) } }, /* Graphs API https://developers.e-com.plus/docs/reference/graphs/ */ 'listRecommendedProducts': function (callback, id) { if (idValidate(callback, id)) { var host = apiGraphs var endpoint = '/products/' + id + '/recommended.json' runMethod(callback, endpoint, host) } }, 'listRelatedProducts': function (callback, id) { if (idValidate(callback, id)) { var host = apiGraphs var endpoint = '/products/' + id + '/related.json' runMethod(callback, endpoint, host) } }, /* Modules API no documentation yet */ 'calculateShipping': function (callback, body) { /* body reference: https://apx-mods.e-com.plus/api/v1/calculate_shipping/schema.json?store_id=100 https://apx-mods.e-com.plus/api/v1/calculate_shipping/response_schema.json?store_id=100 */ var endpoint = '/calculate_shipping.json' runMethod(callback, endpoint, apiModules, body) }, 'listPayments': function (callback, body) { /* body reference: https://apx-mods.e-com.plus/api/v1/list_payments/schema.json?store_id=100 https://apx-mods.e-com.plus/api/v1/list_payments/response_schema.json?store_id=100 */ var endpoint = '/list_payments.json' runMethod(callback, endpoint, apiModules, body) }, 'createTransaction': function (callback, body) { /* body reference: https://apx-mods.e-com.plus/api/v1/create_transaction/schema.json?store_id=100 https://apx-mods.e-com.plus/api/v1/create_transaction/response_schema.json?store_id=100 */ var endpoint = '/create_transaction.json' runMethod(callback, endpoint, apiModules, body) }, 'checkout': function (callback, body) { /* body reference: https://apx-mods.e-com.plus/api/v1/checkout/schema.json?store_id=100 */ var endpoint = '/@checkout.json' runMethod(callback, endpoint, apiModules, body) }, // provide external use as http client 'http': runMethod } }()) if (isNodeJs) { module.exports = EcomIo } else { // declare globally window.EcomIo = EcomIo } }())