├── .npmignore ├── .gitignore ├── .travis.yml ├── src ├── definitions │ ├── index.js │ ├── shopping.js │ └── finding.js ├── constants.js ├── index.js ├── api.js ├── util.js ├── query.js └── request.js ├── init.js ├── .babelrc ├── .eslintrc ├── test ├── mocks │ └── server.js ├── util.js ├── ebay.js ├── api.js ├── intergration.tests.js ├── query.js └── request.js ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | .idea -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | dist 4 | .idea 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "5" 5 | - "4" 6 | - "0.12" 7 | -------------------------------------------------------------------------------- /src/definitions/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | finding: require('./finding'), 3 | shopping: require('./shopping') 4 | }; -------------------------------------------------------------------------------- /init.js: -------------------------------------------------------------------------------- 1 | const Ebay = require('./dist/index.js').default; 2 | 3 | // Factory function for initializing module 4 | module.exports = function(options) { 5 | return new Ebay(options); 6 | }; 7 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": ["transform-es2015-destructuring", "transform-async-to-generator", ["transform-runtime", {"polyfill": true}]], 4 | "retainLines": "true" 5 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaVersion": 7 5 | }, 6 | "env": { 7 | "node": true 8 | }, 9 | "extends": "eslint:recommended", 10 | "rules": { 11 | "semi": ["error", "always"], 12 | "quotes": ["error", "single"], 13 | "no-console": "off" 14 | } 15 | } -------------------------------------------------------------------------------- /test/mocks/server.js: -------------------------------------------------------------------------------- 1 | const nock = require('nock'); 2 | 3 | function serverMock(host, path, data, query = true) { 4 | return nock(host).persist() 5 | .get(path) 6 | .query(query) 7 | .reply(200, JSON.stringify(data)); 8 | } 9 | 10 | module.exports = serverMock; 11 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import _ from '../dist/util'; 3 | 4 | describe('Pick deep', function() { 5 | it('Extract fields from nested object', function() { 6 | const predicate = ['itemId', 'title']; 7 | const object = {itemId: ['123'], test: [{ title: ['test'] }], dummy: 123}; 8 | 9 | const result = _.pickDeep(object, predicate); 10 | 11 | assert.notProperty(result, 'dummy'); 12 | assert.property(result, 'itemId'); 13 | assert.property(result, 'title'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/ebay.js: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import ebay from '../init'; 3 | import {keys, each, lowerFirst, flatMap} from '../dist/util'; 4 | import apiList from '../dist/definitions/index'; 5 | 6 | describe('eBay', function () { 7 | it('Validator', () => { 8 | const noDevKey = () => ebay(); 9 | const invalidResponseFormat = () => ebay({responseFormat: 'binary'}); 10 | 11 | assert.throws(noDevKey); 12 | assert.throws(invalidResponseFormat); 13 | 14 | const shouldPass = () => ebay({devKey: 123}); 15 | 16 | assert.doesNotThrow(shouldPass); 17 | }); 18 | 19 | it('Initialize all api', () => { 20 | const instance = ebay({devKey: 123}); 21 | const index = flatMap(apiList, chunk => (keys(chunk))); 22 | 23 | each(index, v => assert.property(instance, lowerFirst(v))); 24 | }) 25 | }); 26 | -------------------------------------------------------------------------------- /test/api.js: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import sinon from 'sinon'; 3 | import mock from './mocks/server'; 4 | import {each, values} from '../dist/util'; 5 | import {normalizeQuery, EbayOperation} from '../dist/api'; 6 | import apiList from '../dist/definitions/index'; 7 | import {fieldNames} from '../dist/constants'; 8 | 9 | describe('Api', () => { 10 | const profile = {devKey: 123}; 11 | 12 | it('Create returns a promise + stream interface', () => { 13 | const host = 'http://google.com'; 14 | const path = '/'; 15 | const operation = 'GetUserProfile'; 16 | const api = apiList['shopping'][operation]; 17 | 18 | mock(host, path, {}); 19 | 20 | const call = new EbayOperation(operation, host + path, api, profile)(); 21 | 22 | assert.property(call, 'then'); 23 | assert.property(call, 'on'); 24 | assert.property(call, 'pipe'); 25 | }); 26 | 27 | }); 28 | -------------------------------------------------------------------------------- /test/intergration.tests.js: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import sinon from 'sinon'; 3 | import Ebay from '../dist/index'; 4 | import _ from '../dist/util'; 5 | 6 | describe('Integration tests', function() { 7 | this.timeout(10000); 8 | 9 | const options = { 10 | devKey : 'devdummy-24f2-47f4-a685-25d207cf23fe', 11 | responseFormat: 'JSON', 12 | serviceVersion: '1.13.0', 13 | sandbox : true 14 | }; 15 | 16 | const ebay = new Ebay(options); 17 | 18 | it('Finding API', function (done) { 19 | 20 | const query = {keywords: 'nexus player'}; 21 | 22 | ebay.findCompletedItems(query) 23 | .then(d => { 24 | assert.property(d, 'findCompletedItemsResponse'); 25 | done(); 26 | }); 27 | }); 28 | 29 | it('Shopping API', function (done) { 30 | 31 | const query = {CategoryID: -1}; 32 | 33 | ebay.getCategoryInfo(query) 34 | .then(d => { 35 | assert.property(d, 'CategoryArray'); 36 | done(); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Default endpoints 3 | endpoints: { 4 | production:{ 5 | finding: 'http://svcs.ebay.com/services/search/FindingService/v1', 6 | shopping: 'http://open.api.ebay.com/shopping' 7 | }, 8 | sandbox:{ 9 | finding: 'http://svcs.sandbox.ebay.com/services/search/FindingService/v1', 10 | shopping: 'http://open.api.sandbox.ebay.com/shopping' 11 | } 12 | }, 13 | 14 | // Default service versions for each api 15 | serviceVersions: { 16 | finding: '1.13.0', 17 | shopping: '949' 18 | }, 19 | 20 | /** 21 | * eBay have different field names for their standard parameters for every api 22 | * This is for looking up the proper field names for each api 23 | */ 24 | fieldNames: { 25 | finding: { 26 | devKey: 'SECURITY-APPNAME', 27 | serviceVersion: 'SERVICE-VERSION', 28 | responseFormat: 'RESPONSE-DATA-FORMAT', 29 | operation: 'OPERATION-NAME' 30 | }, 31 | shopping: { 32 | devKey: 'appid', 33 | operation: 'callname', 34 | serviceVersion: 'version', 35 | responseFormat: 'responseencoding' 36 | } 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import {EbayOperation} from './api'; 2 | import apiList from './definitions/index.js'; 3 | import {endpoints, fieldNames, serviceVersions} from './constants'; 4 | import {expect} from 'chai'; 5 | import {keys, each, lowerFirst, get} from './util'; 6 | 7 | export default class Ebay { 8 | constructor({devKey, responseFormat = 'JSON', serviceVersion, sandbox}) { 9 | expect(devKey, 'devKey').to.exist; 10 | expect(responseFormat, 'responseFormat').to.match(/json|xml/i); 11 | 12 | const endpoint = endpoints[sandbox ? 'sandbox' : 'production']; 13 | 14 | each(apiList, (api, service) => { 15 | let operationList = keys(api); 16 | 17 | each(operationList, operation => { 18 | let field = fieldNames[service]; 19 | let requiredFields = { 20 | [field.operation]: operation, 21 | [field.devKey]: devKey, 22 | [field.serviceVersion]: get(serviceVersion, service) || serviceVersions[service], 23 | [field.responseFormat]: responseFormat 24 | }; 25 | 26 | this[lowerFirst(operation)] = new EbayOperation(endpoint[service], apiList[service][operation], requiredFields); 27 | }); 28 | }); 29 | } 30 | } -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | import {EbayRequest} from './request'; 2 | import {expect} from 'chai'; 3 | import {extend, transform, isPlainObject} from './util'; 4 | 5 | export class EbayOperation { 6 | constructor(endpoint, apiList, requiredFields) { 7 | 8 | return query => { 9 | const options = extend({}, requiredFields, normalizeQuery(query, apiList)); 10 | return new EbayRequest(endpoint, options); 11 | }; 12 | 13 | } 14 | } 15 | 16 | /** Validates query and prepends @ to attributes fields */ 17 | export function normalizeQuery(query, apiList) { 18 | 19 | return transform(query, (result, value, field) => { 20 | const matchedField = apiList[field]; 21 | 22 | expect(value).to.exist.and.not.empty; 23 | expect(field).to.exist.and.not.empty; 24 | expect(matchedField, 'Field ' + field).exist.and.not.empty; 25 | 26 | // appends @ to attribute fields 27 | const normalizedField = (matchedField === 'attribute') ? ['@' + field] : field; 28 | 29 | if (isPlainObject(value) && isPlainObject(matchedField)) { 30 | return result[normalizedField] = normalizeQuery(value, matchedField); 31 | } 32 | 33 | return result[normalizedField] = value; 34 | }); 35 | } -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | /* 4 | * Picks key/value pairs from nested objects 5 | * @collection : The source object 6 | * @predicate : An array of keys to extract from the collection 7 | * @returns {object} 8 | */ 9 | 10 | function pickDeep(collection, predicate) { 11 | if (!_.isArray(predicate)) predicate = [predicate]; 12 | 13 | var multiArg = predicate.length > 1; 14 | 15 | return _.reduce(collection, function(result, val, key) { 16 | var included = {}; 17 | 18 | if (_.indexOf(predicate, key) !== -1) { 19 | multiArg 20 | ? included[key] = val 21 | : included = val; 22 | } else if (_.isObject(val)) { 23 | included = pickDeep(val, predicate); 24 | } 25 | 26 | if (!_.isEmpty(included)) { 27 | result = multiArg 28 | ? _.extend(result, included) 29 | : included; 30 | } 31 | 32 | return result; 33 | }, multiArg ? {} : ''); 34 | } 35 | 36 | function deepMerge(result, ...sources) { 37 | return _.mergeWith(result, ...sources, arrayMerger); 38 | 39 | function arrayMerger(objValue, srcValue) { 40 | if (_.isArray(objValue)) return objValue.concat(srcValue); 41 | } 42 | } 43 | 44 | _.mixin({pickDeep, deepMerge}); 45 | 46 | module.exports = _; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ebay-sdk", 3 | "version": "0.5.8", 4 | "description": "eBay SDK with promise / stream interface", 5 | "main": "init.js", 6 | "scripts": { 7 | "build": "babel --source-maps inline, -d dist/ src/", 8 | "lint": "eslint src/**.js", 9 | "test": "npm run build && mocha --require source-map-support/register --require babel-register", 10 | "prepublish": "npm run lint && npm run test" 11 | }, 12 | "keywords": [ 13 | "ebay" 14 | ], 15 | "author": "John Wu", 16 | "license": "ISC", 17 | "dependencies": { 18 | "babel-runtime": "^6.9.2", 19 | "check-types": "^7.0.0", 20 | "lodash": "^4.0.0", 21 | "moment": "^2.14.1", 22 | "qs": "git+https://github.com/katsuroo/qs.git", 23 | "request": "^2.75.0", 24 | "request-promise": "^4.1.1" 25 | }, 26 | "devDependencies": { 27 | "babel-cli": "^6.8.0", 28 | "babel-eslint": "^6.1.2", 29 | "babel-plugin-transform-async-to-generator": "^6.8.0", 30 | "babel-plugin-transform-es2015-block-scoping": "^6.15.0", 31 | "babel-plugin-transform-es2015-destructuring": "^6.9.0", 32 | "babel-plugin-transform-object-rest-spread": "^6.8.0", 33 | "babel-plugin-transform-runtime": "^6.9.0", 34 | "babel-polyfill": "^6.9.1", 35 | "babel-preset-es2015": "^6.6.0", 36 | "babel-register": "^6.14.0", 37 | "bluebird": "^3.4.6", 38 | "chai": "^3.2.0", 39 | "eslint": "^2.13.1", 40 | "memorystream": "^0.3.1", 41 | "mocha": "^3.0.0", 42 | "mockery": "^1.7.0", 43 | "nock": "^5.2.1", 44 | "sinon": "^1.17.2", 45 | "source-map-support": "^0.4.2" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "https://github.com/katsuroo/eBay-SDK.git" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/query.js: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import sinon from 'sinon'; 3 | import Query from '../dist/query'; 4 | import mockery from 'mockery'; 5 | import _ from '../dist/util'; 6 | 7 | const getFilter = (q, name) => _.get(q.getFilter(name), 'value'); 8 | const getEndTimeFrom = q => getFilter(q, 'EndTimeFrom'); 9 | const getEndTimeTo = q => getFilter(q, 'EndTimeTo'); 10 | 11 | describe('Query', () => { 12 | it('Interface', () => { 13 | const query = new Query('', {}); 14 | 15 | assert.property(query, 'setPage'); 16 | assert.property(query, 'setEntriesPerPage'); 17 | assert.property(query, 'setFilter'); 18 | assert.property(query, 'setEndTimeTo'); 19 | assert.property(query, 'setEndTimeFrom'); 20 | }); 21 | 22 | it('Set filter', () => { 23 | const query = new Query('', {}); 24 | 25 | const name = 'seller'; 26 | const value = 'bestbuy'; 27 | 28 | query.setFilter(name, value); 29 | 30 | assert.include(query._options.itemFilter, {name, value}); 31 | }); 32 | 33 | it('Set time', () => { 34 | const query = new Query('', {}); 35 | 36 | const endTimeFrom = '01-05-2015'; 37 | query.setEndTimeFrom(endTimeFrom); 38 | 39 | assert.include(query._options.itemFilter, {name: 'EndTimeFrom', value: endTimeFrom}); 40 | 41 | const endTimeTo = '01-08-2015'; 42 | query.setEndTimeTo(endTimeTo); 43 | 44 | assert.include(query._options.itemFilter, {name: 'EndTimeTo', value: endTimeTo}); 45 | }); 46 | 47 | it('Set pagination', () => { 48 | const query = new Query('', {}); 49 | 50 | const entries = 10; 51 | query.setEntriesPerPage(entries); 52 | 53 | assert.equal(query._options.paginationInput.entriesPerPage, entries); 54 | 55 | const page = 20; 56 | query.setPage(page); 57 | 58 | assert.equal(query._options.paginationInput.pageNumber, page); 59 | }); 60 | 61 | it('Split query', () => { 62 | const itemFilter = [ 63 | {name: 'EndTimeFrom', value: '2015-01-01T00:00:00.000Z'}, 64 | {name: 'EndTimeTo', value: '2015-01-03T00:00:00.000Z'} 65 | ]; 66 | 67 | const query = new Query('', {itemFilter}); 68 | 69 | const [first, second] = query.split(2); 70 | 71 | assert.equal(getEndTimeFrom(first()), '2015-01-01T00:00:00.000Z'); 72 | assert.equal(getEndTimeTo(first()), '2015-01-02T00:00:00.000Z'); 73 | 74 | assert.equal(getEndTimeFrom(second()), '2015-01-02T00:00:00.000Z'); 75 | assert.equal(getEndTimeTo(second()), '2015-01-03T00:00:00.000Z'); 76 | }); 77 | }); -------------------------------------------------------------------------------- /src/query.js: -------------------------------------------------------------------------------- 1 | import {Request} from './request'; 2 | import moment from 'moment'; 3 | import qs from 'qs'; 4 | import {expect} from 'chai'; 5 | import {cloneDeep, castArray, set, get, find, isEmpty, range} from 'lodash'; 6 | 7 | export default class Query { 8 | constructor(endpoint, options) { 9 | this._endpoint = endpoint; 10 | this._options = cloneDeep(options); 11 | this._options.itemFilter = isEmpty(this._options.itemFilter) ? [] : castArray(this._options.itemFilter); 12 | } 13 | 14 | setPage(page) { 15 | expect(page, 'pages').to.be.within(1, 100); 16 | 17 | set(this._options, 'paginationInput.pageNumber', page); 18 | 19 | return this; 20 | } 21 | 22 | setEntriesPerPage(entries) { 23 | expect(entries, 'entries').to.be.within(1, 100); 24 | 25 | set(this._options, 'paginationInput.entriesPerPage', entries); 26 | 27 | return this; 28 | } 29 | 30 | setFilter(name, value) { 31 | const currentValue = this.getFilter(name); 32 | 33 | currentValue 34 | ? currentValue['value'] = value 35 | : this._options.itemFilter.push({name, value}); 36 | 37 | return this; 38 | } 39 | 40 | getFilter(name) { 41 | return find(this._options.itemFilter, {name}); 42 | } 43 | 44 | setEndTimeTo(time) { 45 | this.setFilter('EndTimeTo', time.toISOString ? time.toISOString() : time); 46 | 47 | return this; 48 | } 49 | 50 | setEndTimeFrom(time) { 51 | this.setFilter('EndTimeFrom', time.toISOString ? time.toISOString() : time); 52 | 53 | return this; 54 | } 55 | 56 | /** 57 | * Splits query into smaller chunks; 58 | * @param {Number} parts 59 | * @returns {Request} 60 | */ 61 | split(parts) { 62 | const endTimeFrom = get(this.getFilter('EndTimeFrom'), 'value'), 63 | endTimeTo = get(this.getFilter('EndTimeTo'), 'value'); 64 | 65 | expect(endTimeFrom, 'EndTimeFrom').to.exists; 66 | expect(endTimeTo, 'EndTimeTo').to.exists; 67 | 68 | const chunks = range(0, parts); 69 | const chunkSize = moment(endTimeTo).diff(endTimeFrom) / parts; 70 | 71 | return chunks.map(n => { 72 | const start = moment(endTimeFrom).add((n * chunkSize), 'ms'), 73 | end = moment(start).add(chunkSize, 'ms'); 74 | 75 | return () => new Query(this._endpoint, this._options).setEndTimeFrom(start) 76 | .setEndTimeTo(end); 77 | }); 78 | } 79 | 80 | invoke() { 81 | const queryString = qs.stringify(this._options, {delimiter: '&'}); 82 | 83 | return new Request(this._endpoint + '?' + queryString, {json: true}); 84 | } 85 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/katsuroo/eBay-SDK.svg?branch=master)](https://travis-ci.org/katsuroo/eBay-SDK) 2 | 3 | # eBay SDK 4 | 5 | 6 | API Support 7 | 8 | * **Finding** (Full) 9 | * **Shopping** (Full) 10 | 11 | ## Usage: 12 | 13 | ```javascript 14 | var config = {devKey: xxxxxxx}; 15 | var ebay = require('ebay-sdk')(config); 16 | var query = {keywords: 'iphone'}; 17 | 18 | ebay 19 | .findCompletedItems(query) // eBay operation 20 | 21 | // Promise 22 | .then(function(result) { /* Do Something */ }); 23 | 24 | // Stream 25 | .pipe(stream); 26 | ``` 27 | 28 | ## Setup: 29 | 30 | ``` 31 | require('ebay-sdk')({config}) 32 | ``` 33 | 34 | The configuration object takes in the following parameters: 35 | 36 | - ***devKey*** (_required_): 37 | ebay developer key 38 | 39 | 40 | - ***serviceVersion*** (_optional_): 41 | takes in object with api service name as key and service number as value 42 | 43 | 44 | - ***responseFormat*** (_optional_): 45 | xml or json 46 | 47 | ## Call: 48 | ``` 49 | ebay.[api]({query}) 50 | ``` 51 | 52 | Returns a **Request** object 53 | 54 | - **api**: All the supported api under the services that are supported, reference ebay api doc for exact names 55 | 56 | 57 | - **query**: api arguments in key / value pairs 58 | 59 | ```javascript 60 | var query = { 61 | 62 | keywords: 'iphone', 63 | 64 | itemFilter: [ 65 | {name: 'Condition', value: ['New', 'Like New']} // Multiple values 66 | {name: 'ExcludeCategory', value: '132112112'} 67 | ] 68 | 69 | } 70 | ``` 71 | 72 | 73 | 74 | ## Request: 75 | Object returned from call. It contains the promise / stream interface to interact with results along with other methods to manipulate the request. 76 | 77 | *Note*: 78 | The request is not made until it has been consumed by one of the following methods. 79 | 80 | #### then 81 | ``` 82 | request.then([result handler]) 83 | ``` 84 | 85 | Promise interface to interact with data 86 | 87 | #### Pipe 88 | ``` 89 | request.pipe([stream]) 90 | ``` 91 | 92 | Stream interface to interact with data 93 | 94 | #### getAllPages 95 | ``` 96 | request.getAllPages([consume]).then([result handler]) 97 | ``` 98 | 99 | 100 | Fetches all pages (up to 100) from query. 101 | 102 | _consume_ \: When set to false, will return an array of raw request objects. 103 | 104 | #### getAllEntries 105 | ``` 106 | request.getAllEntries([consume]).then([result handler]) 107 | ``` 108 | 109 | Fetches all entries from query. Any query that are bigger than the ebay return limit will be split into multiple queries with smaller time ranges. 110 | 111 | _consume_ \: When set to false, will return an array of raw request objects. 112 | 113 | 114 | -------------------------------------------------------------------------------- /src/request.js: -------------------------------------------------------------------------------- 1 | import promise from 'bluebird'; 2 | import Http from 'request-promise'; 3 | import {expect} from 'chai'; 4 | import Query from './query'; 5 | import _ from './util'; 6 | 7 | const MAX_ENTRIES_PER_REQUEST = 10000; 8 | const MAX_ENTRIES_PER_PAGE = 100; 9 | 10 | /** 11 | * Wraps request object to delay http calls until request is consumed 12 | */ 13 | export class Request extends Http {} 14 | 15 | const requestPrototype = Request.Request.prototype; 16 | 17 | const originalInit = requestPrototype.init; 18 | 19 | requestPrototype.init = function (options) { 20 | 21 | this.init = function () { 22 | originalInit.call(this, options); 23 | }; 24 | }; 25 | 26 | ['then', 'catch', 'finally', 'pipe', 'once'].forEach(method => { 27 | const originalMethod = requestPrototype[method]; 28 | 29 | requestPrototype[method] = function () { 30 | this.init(); 31 | 32 | return originalMethod.call(this, ...arguments); 33 | }; 34 | }); 35 | 36 | export class EbayRequest { 37 | 38 | constructor(endpoint, options) { 39 | 40 | const request = new Query(endpoint, options).invoke(); 41 | 42 | request._createQuery = () => new Query(endpoint, options); 43 | request.getPages = getPages; 44 | request.getAllPages = getAllPages; 45 | request.getAllEntries = getAllEntries; 46 | request.getEntryCount = getEntryCount; 47 | 48 | return request; 49 | } 50 | } 51 | 52 | async function getEntryCount(query = this._createQuery) { 53 | 54 | try { 55 | const request = query().setEntriesPerPage(1) 56 | .invoke(); 57 | 58 | const [totalEntries] = await request.then(result => _.pickDeep(result, 'totalEntries')) 59 | .catch(err => console.log(err)); 60 | 61 | return totalEntries; 62 | } catch (err) { 63 | throw new Error(err); 64 | } 65 | } 66 | 67 | 68 | async function getAllEntries(consume = true, createQuery = this._createQuery) { 69 | 70 | try { 71 | const totalEntries = await this.getEntryCount(createQuery); 72 | const chunks = Math.ceil(totalEntries / MAX_ENTRIES_PER_REQUEST); 73 | const pages = Math.ceil(totalEntries / MAX_ENTRIES_PER_PAGE); 74 | 75 | if (totalEntries <= 0) return []; 76 | 77 | if (totalEntries <= MAX_ENTRIES_PER_REQUEST) { 78 | 79 | return this.getPages(1, pages, createQuery, consume); 80 | 81 | } else { 82 | 83 | const queryChunks = createQuery().split(chunks) 84 | .map(q => this.getAllEntries(false, q)); 85 | 86 | const results = _.flatten(await promise.all(queryChunks)); 87 | 88 | return consume ? promise.all(results) : results; 89 | } 90 | } catch (err) { 91 | throw new Error(err); 92 | } 93 | } 94 | 95 | 96 | async function getAllPages(consume = true) { 97 | 98 | try { 99 | const totalEntries = await this.getEntryCount(); 100 | const totalPages = Math.ceil(totalEntries / MAX_ENTRIES_PER_PAGE); 101 | 102 | return this.getPages(1, totalPages <= 100 ? totalPages : 100, consume); 103 | } catch (err) { 104 | 105 | throw new Error(err); 106 | } 107 | } 108 | 109 | 110 | function getPages() { 111 | let from, to, createQuery = this._createQuery, consume = true; 112 | 113 | _.each(arguments, v => { 114 | if (_.isNumber(v)) v > from ? (to = v) : (to = from, from = v); 115 | if (_.isBoolean(v)) consume = v; 116 | if (_.isFunction(v)) createQuery = v; 117 | }); 118 | 119 | expect(to, 'Page to').to.exist; 120 | expect(from, 'Page from').to.exist; 121 | 122 | const pages = _.range(from, to + 1) 123 | .map(p => createQuery().setPage(p).invoke()); 124 | 125 | return consume ? promise.all(pages) : pages; 126 | } -------------------------------------------------------------------------------- /test/request.js: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import sinon from 'sinon'; 3 | import MemoryStream from 'memorystream'; 4 | import {EbayRequest} from '../dist/request'; 5 | import _ from '../dist/util'; 6 | 7 | // Mock server setup 8 | const mockHost = 'http://ebay.test'; 9 | const mockPath = '/'; 10 | const mockData = require('./mocks/data.js'); 11 | const mockServer = require('./mocks/server.js'); 12 | 13 | mockServer(mockHost, mockPath, mockData); 14 | 15 | describe('EbayRequest', function () { 16 | this.timeout(5000); 17 | 18 | const generateRequest = (q) => (new EbayRequest(mockHost + mockPath, q || {keywords: 'iphone'})); 19 | 20 | it('Return promise interface', () => { 21 | const request = generateRequest(); 22 | 23 | const result = request.catch(d => { 24 | }); 25 | 26 | assert.property(result, 'then'); 27 | }); 28 | 29 | it('Return result in promise', done => { 30 | const request = generateRequest(); 31 | 32 | request.then(d => done()); 33 | }); 34 | 35 | it('Return result in stream', done => { 36 | const request = generateRequest(); 37 | const stream = new MemoryStream(null, {readable: false}); 38 | 39 | request.pipe(stream); 40 | 41 | stream.on('finish', () => { 42 | const result = stream.toString(); 43 | 44 | assert.equal(result, JSON.stringify(mockData)); 45 | 46 | done() 47 | }) 48 | }); 49 | 50 | it('Http calls should not be made before consumption', done => { 51 | const host = 'http://www.google.com'; 52 | const server = mockServer(host, '/', {test: 'test'}); 53 | 54 | new EbayRequest(host + '/', {keywords: 'empty'}); 55 | 56 | setTimeout(() => { 57 | assert.equal(server.isDone(), false); 58 | done(); 59 | }, 500); 60 | }); 61 | 62 | it('Get page range', done => { 63 | const query = {keywords: 'titan'}; 64 | const request = generateRequest(query); 65 | const from = 2; 66 | const to = 5; 67 | 68 | request.getPages(from, to).then(d => { 69 | assert.equal(d.length, 4); 70 | 71 | _.each(d, v => assert.equal(JSON.stringify(v), JSON.stringify(mockData))); 72 | 73 | done(); 74 | }); 75 | }); 76 | 77 | it('Return results for all pages', done => { 78 | const request = generateRequest(); 79 | 80 | request.getAllPages().then(d => { 81 | assert.equal(d.length, 100); 82 | 83 | _.each(d, v => assert.equal(JSON.stringify(v), JSON.stringify(mockData))); 84 | 85 | done(); 86 | }); 87 | }); 88 | 89 | it('Return array of raw request objects for all pages', done => { 90 | const request = generateRequest(); 91 | const isRequestObject = v => (v.then && v.pipe && v.on); 92 | 93 | request.getAllPages(false).then(d => { 94 | assert.equal(d.length, 100); 95 | 96 | _.each(d, v => assert.isOk(isRequestObject(v))); 97 | 98 | done(); 99 | }); 100 | }); 101 | 102 | it('Return entry count', done => { 103 | const request = generateRequest(); 104 | const getTotalEntries = request.getEntryCount(); 105 | 106 | getTotalEntries.then(totalEntries => { 107 | assert.equal(totalEntries, 20000); 108 | done(); 109 | }); 110 | }); 111 | 112 | it('Get All Entries', done => { 113 | const itemFilter = [ 114 | {name: 'EndTimeFrom', value: '2015-01-01T00:00:00.000Z'}, 115 | {name: 'EndTimeTo', value: '2015-01-03T00:00:00.000Z'} 116 | ]; 117 | 118 | const request = generateRequest({itemFilter}); 119 | 120 | const getPagesSpy = sinon.spy(request, 'getPages'); 121 | const entryCountStub = sinon.stub(request, 'getEntryCount'); 122 | const getAllEntriesSpy = sinon.spy(request, 'getAllEntries'); 123 | 124 | entryCountStub.onFirstCall().returns(20000); 125 | entryCountStub.onSecondCall().returns(500); 126 | entryCountStub.onThirdCall().returns(800); 127 | 128 | request.getAllEntries(true).then(() => { 129 | assert.equal(getAllEntriesSpy.calledThrice, true); 130 | assert.equal(entryCountStub.calledThrice, true); 131 | assert.equal(getPagesSpy.firstCall.calledWith(1, 5), true); 132 | assert.equal(getPagesSpy.secondCall.calledWith(1, 8), true); 133 | 134 | done(); 135 | }); 136 | }); 137 | }); -------------------------------------------------------------------------------- /src/definitions/shopping.js: -------------------------------------------------------------------------------- 1 | var value = 'value'; 2 | var attribute = 'attribute'; 3 | 4 | const includeSelector = { 5 | "Item,": value, 6 | "Item": { 7 | "AutoPay,": value, 8 | "BestOfferEnabled,": value, 9 | "BidCount,": value, 10 | "BusinessSellerDetails,": value, 11 | "BusinessSellerDetails": { 12 | "AdditionalContactInformation,": value, 13 | "Address,": value, 14 | "Address": { 15 | "CityName,": value, 16 | "CompanyName,": value, 17 | "FirstName,": value, 18 | "LastName,": value, 19 | "Name,": value, 20 | "Phone,": value, 21 | "PostalCode,": value, 22 | "StateOrProvince,": value, 23 | "Street1,": value, 24 | "Street2,": value 25 | }, 26 | "Email,": value, 27 | "Fax,": value, 28 | "LegalInvoice,": value, 29 | "TermsAndConditions,": value, 30 | "TradeRegistrationNumber,": value, 31 | "VATDetails,": value, 32 | "VATDetails": { 33 | "VATID,": value, 34 | "VATSite,": value 35 | } 36 | }, 37 | "Charity,": value, 38 | "Charity": { 39 | "CharityID,": value, 40 | "CharityName,": value, 41 | "CharityNumber,": value, 42 | "DonationPercent,": value, 43 | "LogoURL,": value, 44 | "Mission,": value, 45 | "Status,": value 46 | }, 47 | "ConditionDescription,": value, 48 | "ConditionDisplayName,": value, 49 | "ConditionID,": value, 50 | "ConvertedCurrentPrice,": value, 51 | "Country,": value, 52 | "CurrentPrice,": value, 53 | "Description,": value, 54 | "eBayNowEligible,": value, 55 | "EndTime,": value, 56 | "GalleryURL,": value, 57 | "GlobalShipping,": value, 58 | "HandlingTime,": value, 59 | "HighBidder,": value, 60 | "HighBidder": { 61 | "FeedbackPrivate,": value, 62 | "FeedbackRatingStar,": value, 63 | "FeedbackScore,": value, 64 | "UserAnonymized,": value, 65 | "UserID,": value 66 | }, 67 | "HitCount,": value, 68 | "IntegratedMerchantCreditCardEnabled,": value, 69 | "ItemID,": value, 70 | "ItemSpecifics,": value, 71 | "ItemSpecifics": { 72 | "NameValueList,": value, 73 | "NameValueList": { 74 | "Name,": value, 75 | "Value,": value 76 | } 77 | }, 78 | "ListingStatus,": value, 79 | "ListingType,": value, 80 | "Location,": value, 81 | "LotSize,": value, 82 | "MinimumToBid,": value, 83 | "NewBestOffer,": value, 84 | "PaymentAllowedSite,": value, 85 | "PaymentMethods,": value, 86 | "PictureURL,": value, 87 | "PostalCode,": value, 88 | "PrimaryCategoryID,": value, 89 | "PrimaryCategoryIDPath,": value, 90 | "PrimaryCategoryName,": value, 91 | "ProductID,": value, 92 | "Quantity,": value, 93 | "QuantityAvailableHint,": value, 94 | "QuantityInfo,": value, 95 | "QuantitySold,": value, 96 | "QuantitySoldByPickupInStore,": value, 97 | "QuantityThreshold,": value, 98 | "ReserveMet,": value, 99 | "ReturnPolicy,": value, 100 | "ReturnPolicy": { 101 | "Description,": value, 102 | "EAN,": value, 103 | "Refund,": value, 104 | "ReturnsAccepted,": value, 105 | "ReturnsWithin,": value, 106 | "ShippingCostPaidBy,": value, 107 | "WarrantyDuration,": value, 108 | "WarrantyOffered,": value, 109 | "WarrantyType,": value 110 | }, 111 | "SecondaryCategoryID,": value, 112 | "SecondaryCategoryIDPath,": value, 113 | "SecondaryCategoryName,": value, 114 | "Seller,": value, 115 | "Seller": { 116 | "FeedbackRatingStar,": value, 117 | "FeedbackScore,": value, 118 | "PositiveFeedbackPercent,": value, 119 | "TopRatedSeller,": value, 120 | "UserID,": value 121 | }, 122 | "ShippingCostSummary,": value, 123 | "ShippingCostSummary": { 124 | "InsuranceCost,": value, 125 | "ListedShippingServiceCost,": value, 126 | "LocalPickup,": value, 127 | "ShippingServiceCost,": value, 128 | "ShippingType,": value 129 | }, 130 | "ShipToLocations,": value, 131 | "Site,": value, 132 | "SKU,": value, 133 | "StartTime,": value, 134 | "Storefront,": value, 135 | "Storefront": { 136 | "StoreName,": value, 137 | "StoreURL,": value 138 | }, 139 | "Subtitle,": value, 140 | "TimeLeft,": value, 141 | "Title,": value, 142 | "TopRatedListing,": value, 143 | "UnitInfo,": value, 144 | "Variations,": value, 145 | "Variations": { 146 | "Pictures,": value, 147 | "Pictures": { 148 | "VariationSpecificName,": value, 149 | "VariationSpecificPictureSet,": value, 150 | "VariationSpecificPictureSet": value 151 | }, 152 | "Variation,": value, 153 | "Variation": { 154 | "Quantity,": value, 155 | "SellingStatus,": value, 156 | "SellingStatus": value, 157 | "SKU,": value, 158 | "StartPrice,": value, 159 | "VariationSpecifics,": value, 160 | "VariationSpecifics": { 161 | "NameValueList,": value, 162 | "NameValueList": { 163 | "Name,": value, 164 | "Value,": value 165 | } 166 | } 167 | }, 168 | "VariationSpecificsSet,": value, 169 | "VariationSpecificsSet": { 170 | "NameValueList,": value, 171 | "NameValueList": { 172 | "Name,": value, 173 | "Value,": value 174 | } 175 | } 176 | }, 177 | "VhrAvailable,": value, 178 | "VhrUrl,": value, 179 | "ViewItemURLForNaturalSearch": value 180 | }, 181 | "VariationSpecificValue,": value, 182 | "QuantitySoldByPickupInStore,": value 183 | }; 184 | 185 | const shopping = { 186 | FindHalfProduct: { 187 | itemID: value, 188 | IncludeSelector: includeSelector, 189 | AvailableItemsOnly: value, 190 | DomainName: value, 191 | MaxEntries: value, 192 | PageNumber: value, 193 | ProductID: { 194 | type: attribute 195 | }, 196 | ProductSort: value, 197 | QueryKeywords: value, 198 | SellerId: value, 199 | SortOrder: value, 200 | MessageID: value 201 | }, 202 | FindProduct: { 203 | itemID: value, 204 | IncludeSelector: includeSelector, 205 | AvailableItemsOnly: value, 206 | HideDuplicateItems: value, 207 | DomainName: value, 208 | MaxEntries: value, 209 | PageNumber: value, 210 | ProductID: { 211 | type: attribute 212 | }, 213 | ProductSort: value, 214 | QueryKeywords: value, 215 | SortOrder: value, 216 | MessageID: value 217 | }, 218 | FindReviewsAndGuides: { 219 | CategoryID: value, 220 | IncludeSelector: includeSelector, 221 | MaxResultsPerPage: value, 222 | PageNumber: value, 223 | ProductID: { 224 | type: attribute 225 | }, 226 | ReviewSort: value, 227 | UserID: value, 228 | SortOrder: value, 229 | MessageID: value 230 | }, 231 | GetCategoryInfo: { 232 | CategoryID: value, 233 | IncludeSelector: includeSelector, 234 | MessageID: value 235 | }, 236 | GetUserProfile: { 237 | userID: value 238 | }, 239 | GetItemStatus: { 240 | itemID: value, 241 | MessageID: value 242 | }, 243 | GetMultipleItems: { 244 | itemID: value, 245 | IncludeSelector: includeSelector, 246 | MessageID: value 247 | }, 248 | GetSingleItem: { 249 | itemID: value, 250 | IncludeSelector: includeSelector, 251 | MessageID: value, 252 | VariationSKU: value, 253 | VariationSpecifics: { 254 | NameValueList: { 255 | Name: value, 256 | Value: value 257 | } 258 | } 259 | }, 260 | GetShippingCosts: { 261 | DestinationCountryCode: value, 262 | DestinationPostalCode: value, 263 | IncludeDetails: value, 264 | ItemID: value, 265 | QuantitySold: value 266 | } 267 | }; 268 | 269 | module.exports = shopping; 270 | -------------------------------------------------------------------------------- /src/definitions/finding.js: -------------------------------------------------------------------------------- 1 | var value = 'value'; 2 | var attribute = 'attribute'; 3 | 4 | var outputSelector = { 5 | ack: value, 6 | aspectHistogramContainer: { 7 | aspect: { 8 | valueHistogram: { 9 | count: value 10 | } 11 | } 12 | }, 13 | categoryHistogramContainer: { 14 | categoryHistogram: { 15 | categoryId: value, 16 | categoryName: value, 17 | childCategoryHistogram: { 18 | categoryId: value, 19 | categoryName: value, 20 | childCategoryHistogram: { 21 | categoryId: value, 22 | categoryName: value, 23 | childCategoryHistogram: { 24 | count: value 25 | } 26 | }, 27 | count: value 28 | }, 29 | count: value 30 | } 31 | }, 32 | conditionHistogramContainer: { 33 | conditionHistogram: { 34 | condition: { 35 | conditionDisplayName: value, 36 | conditionId: value 37 | } 38 | }, 39 | count: value 40 | }, 41 | errorMessage: { 42 | error: { 43 | category: value, 44 | domain: value, 45 | errorId: value, 46 | exceptionId: value, 47 | message: value, 48 | parameter: value, 49 | severity: value, 50 | subdomain: value 51 | } 52 | }, 53 | paginationOutput: { 54 | entriesPerPage: value, 55 | pageNumber: value, 56 | totalEntries: value, 57 | totalPages: value 58 | }, 59 | searchResult: { 60 | item:{ 61 | attribute: { 62 | name: value, 63 | value: value, 64 | }, 65 | autoPay: value, 66 | charityId: value, 67 | condition: { 68 | conditionDisplayName: value, 69 | conditionId: value 70 | }, 71 | country: value, 72 | discountPriceInfo: value, 73 | distance: value, 74 | eekStatus: value, 75 | galleryInfoContainer: { 76 | galleryURL: value 77 | }, 78 | galleryPlusPictureURL: value, 79 | galleryURL: value, 80 | globalId: value, 81 | isMultiVariationListing: value, 82 | itemId: value, 83 | listingInfo: { 84 | bestOfferEnabled: value, 85 | buyItNowAvailable: value, 86 | buyItNowPrice: value, 87 | convertedBuyItNowPrice: value, 88 | endTime: value, 89 | gift: value, 90 | listingType: value, 91 | startTime: value 92 | }, 93 | location: value, 94 | paymentMethod: value, 95 | pictureURLLarge: value, 96 | pictureURLSuperSize: value, 97 | postalCode: value, 98 | primaryCategory: { 99 | categoryId: value, 100 | categoryName: value 101 | }, 102 | productId: value, 103 | secondaryCategory: { 104 | categoryId: value, 105 | categoryName: value, 106 | }, 107 | sellerInfo: { 108 | feedbackRatingStar: value, 109 | feedbackScore: value, 110 | positiveFeedbackPercent: value, 111 | sellerUserName: value, 112 | topRatedSeller: value 113 | }, 114 | sellingStatus: { 115 | bidCount: value, 116 | convertedCurrentPrice: value, 117 | currentPrice: value, 118 | sellingState: value, 119 | timeLeft: value 120 | }, 121 | shippingInfo: { 122 | shippingServiceCost: value, 123 | shippingType: value, 124 | shipToLocations: value 125 | }, 126 | storeInfo: { 127 | storeName: value, 128 | storeURL: value 129 | }, 130 | subtitle: value, 131 | title: value, 132 | topRatedListing: value, 133 | unitPrice: { 134 | quantity: value, 135 | type: value 136 | }, 137 | viewItemURL: value 138 | } 139 | }, 140 | timestamp: value, 141 | version: value 142 | }; 143 | 144 | var finding = { 145 | findCompletedItems: { 146 | aspectFilter: { 147 | aspectName: value, 148 | aspectValueName: value 149 | }, 150 | categoryId: value, 151 | itemFilter: { 152 | name: value, 153 | paramName: value, 154 | paramValue: value, 155 | value: value 156 | }, 157 | keywords: value, 158 | outputSelector: outputSelector, 159 | productId: { 160 | name: attribute, 161 | type: attribute 162 | }, 163 | affiliate: { 164 | customId: value, 165 | geoTargeting: value, 166 | networkId: value, 167 | trackingId: value 168 | }, 169 | buyerPostalCode: value, 170 | paginationInput: { 171 | entriesPerPage: value, 172 | pageNumber: value 173 | }, 174 | sortOrder: value 175 | }, 176 | findItemsAdvanced: { 177 | aspectFilter: { 178 | aspectName: value, 179 | aspectValueName: value 180 | }, 181 | categoryId: value, 182 | descriptionSearch: value, 183 | itemFilter: { 184 | name: value, 185 | paramName: value, 186 | paramValue: value, 187 | value: value 188 | }, 189 | keywords: value, 190 | outputSelector: outputSelector, 191 | affiliate: { 192 | customId: value, 193 | geoTargeting: value, 194 | networkId: value, 195 | trackingId: value 196 | }, 197 | buyerPostalCode: value, 198 | paginationInput: { 199 | entriesPerPage: value, 200 | pageNumber: value 201 | }, 202 | sortOrder: value 203 | }, 204 | findItemsByCategory: { 205 | aspectFilter: { 206 | aspectName: value, 207 | aspectValueName: value 208 | }, 209 | categoryId: value, 210 | itemFilter: { 211 | name: value, 212 | paramName: value, 213 | paramValue: value, 214 | value: value 215 | }, 216 | outputSelector: outputSelector, 217 | affiliate: { 218 | customId: value, 219 | geoTargeting: value, 220 | networkId: value, 221 | trackingId: value 222 | }, 223 | buyerPostalCode: value, 224 | paginationInput: { 225 | entriesPerPage: value, 226 | pageNumber: value 227 | }, 228 | sortOrder: value 229 | }, 230 | findItemsByImage: { 231 | aspectFilter: { 232 | aspectName: value, 233 | aspectValueName: value 234 | }, 235 | categoryId: value, 236 | itemFilter: { 237 | name: value, 238 | paramName: value, 239 | paramValue: value, 240 | value: value 241 | }, 242 | itemId: value, 243 | outputSelector: outputSelector, 244 | affiliate: { 245 | customId: value, 246 | geoTargeting: value, 247 | networkId: value, 248 | trackingId: value 249 | }, 250 | buyerPostalCode: value, 251 | paginationInput: { 252 | entriesPerPage: value, 253 | pageNumber: value 254 | } 255 | }, 256 | findItemsByKeywords: { 257 | aspectFilter: { 258 | aspectName: value, 259 | aspectValueName: value 260 | }, 261 | itemFilter: { 262 | name: value, 263 | paramName: value, 264 | paramValue: value, 265 | value: value 266 | }, 267 | keywords: value, 268 | outputSelector: outputSelector, 269 | affiliate: { 270 | customId: value, 271 | geoTargeting: value, 272 | networkId: value, 273 | trackingId: value 274 | }, 275 | buyerPostalCode: value, 276 | paginationInput: { 277 | entriesPerPage: value, 278 | pageNumber: value 279 | }, 280 | sortOrder: value 281 | }, 282 | findItemsByProduct: { 283 | itemFilter: { 284 | name: value, 285 | paramName: value, 286 | paramValue: value, 287 | value: value 288 | }, 289 | outputSelector: outputSelector, 290 | productId: { 291 | type: value 292 | }, 293 | affiliate: { 294 | customId: value, 295 | geoTargeting: value, 296 | networkId: value, 297 | trackingId: value 298 | }, 299 | buyerPostalCode: value, 300 | paginationInput: { 301 | entriesPerPage: value, 302 | pageNumber: value 303 | }, 304 | sortOrder: value 305 | }, 306 | findItemsIneBayStores: { 307 | aspectFilter: { 308 | aspectName: value, 309 | aspectValueName: value 310 | }, 311 | categoryId: value, 312 | itemFilter: { 313 | name: value, 314 | paramName: value, 315 | paramValue: value, 316 | value: value 317 | }, 318 | keywords: value, 319 | outputSelector: outputSelector, 320 | storeName: value, 321 | affiliate: { 322 | customId: value, 323 | geoTargeting: value, 324 | networkId: value, 325 | trackingId: value 326 | }, 327 | buyerPostalCode: value, 328 | paginationInput: { 329 | entriesPerPage: value, 330 | pageNumber: value 331 | }, 332 | sortOrder: value 333 | }, 334 | getHistograms: { 335 | categoryId: value 336 | }, 337 | getSearchKeywordsRecommendation: { 338 | keywords: value 339 | } 340 | }; 341 | 342 | module.exports = finding; 343 | --------------------------------------------------------------------------------