diff options
13 files changed, 168 insertions, 75 deletions
diff --git a/packages/connect/CHANGELOG.md b/packages/connect/CHANGELOG.md index 91dcc2b2c..18c808e5e 100644 --- a/packages/connect/CHANGELOG.md +++ b/packages/connect/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## v0.6.0 - _TBD, 2018_ + + * Add pagination options to HttpClient methods (#393) + ## v0.5.7 - _February 9, 2018_ * Fix publishing issue where .npmignore was not properly excluding undesired content (#389) diff --git a/packages/connect/src/http_client.ts b/packages/connect/src/http_client.ts index 3df77b0f0..815d42e67 100644 --- a/packages/connect/src/http_client.ts +++ b/packages/connect/src/http_client.ts @@ -13,14 +13,26 @@ import { HttpRequestType, OrderbookRequest, OrderbookResponse, - OrdersRequest, + OrdersRequestOpts, + PagedRequestOpts, SignedOrder, TokenPairsItem, - TokenPairsRequest, + TokenPairsRequestOpts, } from './types'; import { relayerResponseJsonParsers } from './utils/relayer_response_json_parsers'; const TRAILING_SLASHES_REGEX = /\/+$/; +const DEFAULT_PAGED_REQUEST_OPTS: PagedRequestOpts = { + page: 0, + perPage: 100, +}; +/** + * This mapping defines how an option property name gets converted into an HTTP request query field + */ +const OPTS_TO_QUERY_FIELD_MAP = { + perPage: 'per_page', +}; + /** * This class includes all the functionality related to interacting with a set of HTTP endpoints * that implement the standard relayer API v0 @@ -28,6 +40,22 @@ const TRAILING_SLASHES_REGEX = /\/+$/; export class HttpClient implements Client { private _apiEndpointUrl: string; /** + * Format parameters to be appended to http requests into query string form + */ + private static _buildQueryStringFromHttpParams(params?: object): string { + // if params are undefined or empty, return an empty string + if (_.isUndefined(params) || _.isEmpty(params)) { + return ''; + } + // format params into a form the api expects + const formattedParams = _.mapKeys(params, (value: any, key: string) => { + return _.get(OPTS_TO_QUERY_FIELD_MAP, key, key); + }); + // stringify the formatted object + const stringifiedParams = queryString.stringify(formattedParams); + return `?${stringifiedParams}`; + } + /** * Instantiates a new HttpClient instance * @param url The relayer API base HTTP url you would like to interact with * @return An instance of HttpClient @@ -38,34 +66,35 @@ export class HttpClient implements Client { } /** * Retrieve token pair info from the API - * @param request A TokenPairsRequest instance describing specific token information - * to retrieve + * @param requestOpts Options specifying token information to retrieve and page information, defaults to { page: 0, perPage: 100 } * @return The resulting TokenPairsItems that match the request */ - public async getTokenPairsAsync(request?: TokenPairsRequest): Promise<TokenPairsItem[]> { - if (!_.isUndefined(request)) { - assert.doesConformToSchema('request', request, clientSchemas.relayerTokenPairsRequestSchema); + public async getTokenPairsAsync(requestOpts?: TokenPairsRequestOpts & PagedRequestOpts): Promise<TokenPairsItem[]> { + if (!_.isUndefined(requestOpts)) { + assert.doesConformToSchema('requestOpts', requestOpts, clientSchemas.tokenPairsRequestOptsSchema); + assert.doesConformToSchema('requestOpts', requestOpts, clientSchemas.pagedRequestOptsSchema); } - const requestOpts = { - params: request, + const httpRequestOpts = { + params: _.defaults({}, requestOpts, DEFAULT_PAGED_REQUEST_OPTS), }; - const responseJson = await this._requestAsync('/token_pairs', HttpRequestType.Get, requestOpts); + const responseJson = await this._requestAsync('/token_pairs', HttpRequestType.Get, httpRequestOpts); const tokenPairs = relayerResponseJsonParsers.parseTokenPairsJson(responseJson); return tokenPairs; } /** * Retrieve orders from the API - * @param request An OrdersRequest instance describing specific orders to retrieve + * @param requestOpts Options specifying orders to retrieve and page information, defaults to { page: 0, perPage: 100 } * @return The resulting SignedOrders that match the request */ - public async getOrdersAsync(request?: OrdersRequest): Promise<SignedOrder[]> { - if (!_.isUndefined(request)) { - assert.doesConformToSchema('request', request, clientSchemas.relayerOrdersRequestSchema); + public async getOrdersAsync(requestOpts?: OrdersRequestOpts & PagedRequestOpts): Promise<SignedOrder[]> { + if (!_.isUndefined(requestOpts)) { + assert.doesConformToSchema('requestOpts', requestOpts, clientSchemas.ordersRequestOptsSchema); + assert.doesConformToSchema('requestOpts', requestOpts, clientSchemas.pagedRequestOptsSchema); } - const requestOpts = { - params: request, + const httpRequestOpts = { + params: _.defaults({}, requestOpts, DEFAULT_PAGED_REQUEST_OPTS), }; - const responseJson = await this._requestAsync(`/orders`, HttpRequestType.Get, requestOpts); + const responseJson = await this._requestAsync(`/orders`, HttpRequestType.Get, httpRequestOpts); const orders = relayerResponseJsonParsers.parseOrdersJson(responseJson); return orders; } @@ -82,15 +111,22 @@ export class HttpClient implements Client { } /** * Retrieve an orderbook from the API - * @param request An OrderbookRequest instance describing the specific orderbook to retrieve + * @param request An OrderbookRequest instance describing the specific orderbook to retrieve + * @param requestOpts Options specifying page information, defaults to { page: 0, perPage: 100 } * @return The resulting OrderbookResponse that matches the request */ - public async getOrderbookAsync(request: OrderbookRequest): Promise<OrderbookResponse> { - assert.doesConformToSchema('request', request, clientSchemas.relayerOrderBookRequestSchema); - const requestOpts = { - params: request, + public async getOrderbookAsync( + request: OrderbookRequest, + requestOpts?: PagedRequestOpts, + ): Promise<OrderbookResponse> { + assert.doesConformToSchema('request', request, clientSchemas.orderBookRequestSchema); + if (!_.isUndefined(requestOpts)) { + assert.doesConformToSchema('requestOpts', requestOpts, clientSchemas.pagedRequestOptsSchema); + } + const httpRequestOpts = { + params: _.defaults({}, request, requestOpts, DEFAULT_PAGED_REQUEST_OPTS), }; - const responseJson = await this._requestAsync('/orderbook', HttpRequestType.Get, requestOpts); + const responseJson = await this._requestAsync('/orderbook', HttpRequestType.Get, httpRequestOpts); const orderbook = relayerResponseJsonParsers.parseOrderbookResponseJson(responseJson); return orderbook; } @@ -100,11 +136,11 @@ export class HttpClient implements Client { * @return The resulting FeesResponse that matches the request */ public async getFeesAsync(request: FeesRequest): Promise<FeesResponse> { - assert.doesConformToSchema('request', request, schemas.relayerApiFeesPayloadSchema); - const requestOpts = { + assert.doesConformToSchema('request', request, clientSchemas.feesRequestSchema); + const httpRequestOpts = { payload: request, }; - const responseJson = await this._requestAsync('/fees', HttpRequestType.Post, requestOpts); + const responseJson = await this._requestAsync('/fees', HttpRequestType.Post, httpRequestOpts); const fees = relayerResponseJsonParsers.parseFeesResponseJson(responseJson); return fees; } @@ -126,11 +162,7 @@ export class HttpClient implements Client { ): Promise<any> { const params = _.get(requestOptions, 'params'); const payload = _.get(requestOptions, 'payload'); - let query = ''; - if (!_.isUndefined(params) && !_.isEmpty(params)) { - const stringifiedParams = queryString.stringify(params); - query = `?${stringifiedParams}`; - } + const query = HttpClient._buildQueryStringFromHttpParams(params); const url = `${this._apiEndpointUrl}${path}${query}`; const headers = new Headers({ 'content-type': 'application/json', diff --git a/packages/connect/src/index.ts b/packages/connect/src/index.ts index a492f5ae9..344a32e28 100644 --- a/packages/connect/src/index.ts +++ b/packages/connect/src/index.ts @@ -11,9 +11,10 @@ export { OrderbookChannelSubscriptionOpts, OrderbookRequest, OrderbookResponse, - OrdersRequest, + OrdersRequestOpts, + PagedRequestOpts, SignedOrder, TokenPairsItem, - TokenPairsRequest, + TokenPairsRequestOpts, TokenTradeInfo, } from './types'; diff --git a/packages/connect/src/schemas/fees_request_schema.ts b/packages/connect/src/schemas/fees_request_schema.ts new file mode 100644 index 000000000..ff3d7b9d3 --- /dev/null +++ b/packages/connect/src/schemas/fees_request_schema.ts @@ -0,0 +1,26 @@ +export const feesRequestSchema = { + id: '/FeesRequest', + type: 'object', + properties: { + exchangeContractAddress: { $ref: '/Address' }, + maker: { $ref: '/Address' }, + taker: { $ref: '/Address' }, + makerTokenAddress: { $ref: '/Address' }, + takerTokenAddress: { $ref: '/Address' }, + makerTokenAmount: { $ref: '/Number' }, + takerTokenAmount: { $ref: '/Number' }, + expirationUnixTimestampSec: { $ref: '/Number' }, + salt: { $ref: '/Number' }, + }, + required: [ + 'exchangeContractAddress', + 'maker', + 'taker', + 'makerTokenAddress', + 'takerTokenAddress', + 'makerTokenAmount', + 'takerTokenAmount', + 'expirationUnixTimestampSec', + 'salt', + ], +}; diff --git a/packages/connect/src/schemas/relayer_fees_request_schema.ts b/packages/connect/src/schemas/orderbook_request_schema.ts index f20e077ba..5f3463242 100644 --- a/packages/connect/src/schemas/relayer_fees_request_schema.ts +++ b/packages/connect/src/schemas/orderbook_request_schema.ts @@ -1,8 +1,9 @@ -export const relayerOrderBookRequestSchema = { - id: '/RelayerOrderBookRequest', +export const orderBookRequestSchema = { + id: '/OrderBookRequest', type: 'object', properties: { baseTokenAddress: { $ref: '/Address' }, quoteTokenAddress: { $ref: '/Address' }, }, + required: ['baseTokenAddress', 'quoteTokenAddress'], }; diff --git a/packages/connect/src/schemas/relayer_orders_request_schema.ts b/packages/connect/src/schemas/orders_request_opts_schema.ts index 570238dae..5facbc959 100644 --- a/packages/connect/src/schemas/relayer_orders_request_schema.ts +++ b/packages/connect/src/schemas/orders_request_opts_schema.ts @@ -1,5 +1,5 @@ -export const relayerOrdersRequestSchema = { - id: '/RelayerOrdersRequest', +export const ordersRequestOptsSchema = { + id: '/OrdersRequestOpts', type: 'object', properties: { exchangeContractAddress: { $ref: '/Address' }, diff --git a/packages/connect/src/schemas/paged_request_opts_schema.ts b/packages/connect/src/schemas/paged_request_opts_schema.ts new file mode 100644 index 000000000..eb2e52100 --- /dev/null +++ b/packages/connect/src/schemas/paged_request_opts_schema.ts @@ -0,0 +1,8 @@ +export const pagedRequestOptsSchema = { + id: '/PagedRequestOpts', + type: 'object', + properties: { + page: { type: 'number' }, + perPage: { type: 'number' }, + }, +}; diff --git a/packages/connect/src/schemas/relayer_orderbook_request_schema.ts b/packages/connect/src/schemas/relayer_orderbook_request_schema.ts deleted file mode 100644 index f20e077ba..000000000 --- a/packages/connect/src/schemas/relayer_orderbook_request_schema.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const relayerOrderBookRequestSchema = { - id: '/RelayerOrderBookRequest', - type: 'object', - properties: { - baseTokenAddress: { $ref: '/Address' }, - quoteTokenAddress: { $ref: '/Address' }, - }, -}; diff --git a/packages/connect/src/schemas/schemas.ts b/packages/connect/src/schemas/schemas.ts index 288d6969d..0b8b798a9 100644 --- a/packages/connect/src/schemas/schemas.ts +++ b/packages/connect/src/schemas/schemas.ts @@ -1,9 +1,13 @@ -import { relayerOrderBookRequestSchema } from './relayer_orderbook_request_schema'; -import { relayerOrdersRequestSchema } from './relayer_orders_request_schema'; -import { relayerTokenPairsRequestSchema } from './relayer_token_pairs_request_schema'; +import { feesRequestSchema } from './fees_request_schema'; +import { orderBookRequestSchema } from './orderbook_request_schema'; +import { ordersRequestOptsSchema } from './orders_request_opts_schema'; +import { pagedRequestOptsSchema } from './paged_request_opts_schema'; +import { tokenPairsRequestOptsSchema } from './token_pairs_request_opts_schema'; export const schemas = { - relayerOrderBookRequestSchema, - relayerOrdersRequestSchema, - relayerTokenPairsRequestSchema, + feesRequestSchema, + orderBookRequestSchema, + ordersRequestOptsSchema, + pagedRequestOptsSchema, + tokenPairsRequestOptsSchema, }; diff --git a/packages/connect/src/schemas/relayer_token_pairs_request_schema.ts b/packages/connect/src/schemas/token_pairs_request_opts_schema.ts index 379232204..9b73a917b 100644 --- a/packages/connect/src/schemas/relayer_token_pairs_request_schema.ts +++ b/packages/connect/src/schemas/token_pairs_request_opts_schema.ts @@ -1,5 +1,5 @@ -export const relayerTokenPairsRequestSchema = { - id: '/RelayerTokenPairsRequest', +export const tokenPairsRequestOptsSchema = { + id: '/TokenPairsRequestOpts', type: 'object', properties: { tokenA: { $ref: '/Address' }, diff --git a/packages/connect/src/types.ts b/packages/connect/src/types.ts index edb6c77a6..970eff498 100644 --- a/packages/connect/src/types.ts +++ b/packages/connect/src/types.ts @@ -30,10 +30,10 @@ export interface ECSignature { } export interface Client { - getTokenPairsAsync: (request?: TokenPairsRequest) => Promise<TokenPairsItem[]>; - getOrdersAsync: (request?: OrdersRequest) => Promise<SignedOrder[]>; + getTokenPairsAsync: (requestOpts?: TokenPairsRequestOpts & PagedRequestOpts) => Promise<TokenPairsItem[]>; + getOrdersAsync: (requestOpts?: OrdersRequestOpts & PagedRequestOpts) => Promise<SignedOrder[]>; getOrderAsync: (orderHash: string) => Promise<SignedOrder>; - getOrderbookAsync: (request: OrderbookRequest) => Promise<OrderbookResponse>; + getOrderbookAsync: (request: OrderbookRequest, requestOpts?: PagedRequestOpts) => Promise<OrderbookResponse>; getFeesAsync: (request: FeesRequest) => Promise<FeesResponse>; submitOrderAsync: (signedOrder: SignedOrder) => Promise<void>; } @@ -111,7 +111,7 @@ export enum WebsocketClientEventType { ConnectFailed = 'connectFailed', } -export interface TokenPairsRequest { +export interface TokenPairsRequestOpts { tokenA?: string; tokenB?: string; } @@ -128,7 +128,7 @@ export interface TokenTradeInfo { precision: number; } -export interface OrdersRequest { +export interface OrdersRequestOpts { exchangeContractAddress?: string; tokenAddress?: string; makerTokenAddress?: string; @@ -167,6 +167,11 @@ export interface FeesResponse { takerFee: BigNumber; } +export interface PagedRequestOpts { + page?: number; + perPage?: number; +} + export interface HttpRequestOptions { params?: object; payload?: object; diff --git a/packages/connect/test/http_client_test.ts b/packages/connect/test/http_client_test.ts index 15759d911..93b252ace 100644 --- a/packages/connect/test/http_client_test.ts +++ b/packages/connect/test/http_client_test.ts @@ -40,19 +40,22 @@ describe('HttpClient', () => { }); describe('#getTokenPairsAsync', () => { const url = `${relayUrl}/token_pairs`; - it('gets token pairs', async () => { - fetchMock.get(url, tokenPairsResponseJSON); + it('gets token pairs with default options when none are provided', async () => { + const urlWithQuery = `${url}?page=0&per_page=100`; + fetchMock.get(urlWithQuery, tokenPairsResponseJSON); const tokenPairs = await relayerClient.getTokenPairsAsync(); expect(tokenPairs).to.be.deep.equal(tokenPairsResponse); }); - it('gets specific token pairs for request', async () => { + it('gets token pairs with specified request options', async () => { const tokenAddress = '0x323b5d4c32345ced77393b3530b1eed0f346429d'; - const tokenPairsRequest = { + const tokenPairsRequestOpts = { tokenA: tokenAddress, + page: 3, + perPage: 50, }; - const urlWithQuery = `${url}?tokenA=${tokenAddress}`; + const urlWithQuery = `${url}?page=3&per_page=50&tokenA=${tokenAddress}`; fetchMock.get(urlWithQuery, tokenPairsResponseJSON); - const tokenPairs = await relayerClient.getTokenPairsAsync(tokenPairsRequest); + const tokenPairs = await relayerClient.getTokenPairsAsync(tokenPairsRequestOpts); expect(tokenPairs).to.be.deep.equal(tokenPairsResponse); }); it('throws an error for invalid JSON response', async () => { @@ -62,17 +65,20 @@ describe('HttpClient', () => { }); describe('#getOrdersAsync', () => { const url = `${relayUrl}/orders`; - it('gets orders', async () => { - fetchMock.get(url, ordersResponseJSON); + it('gets orders with default options when none are provided', async () => { + const urlWithQuery = `${url}?page=0&per_page=100`; + fetchMock.get(urlWithQuery, ordersResponseJSON); const orders = await relayerClient.getOrdersAsync(); expect(orders).to.be.deep.equal(ordersResponse); }); - it('gets specific orders for request', async () => { + it('gets orders with specified request options', async () => { const tokenAddress = '0x323b5d4c32345ced77393b3530b1eed0f346429d'; const ordersRequest = { tokenAddress, + page: 3, + perPage: 50, }; - const urlWithQuery = `${url}?tokenAddress=${tokenAddress}`; + const urlWithQuery = `${url}?page=3&per_page=50&tokenAddress=${tokenAddress}`; fetchMock.get(urlWithQuery, ordersResponseJSON); const orders = await relayerClient.getOrdersAsync(ordersRequest); expect(orders).to.be.deep.equal(ordersResponse); @@ -100,14 +106,27 @@ describe('HttpClient', () => { baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', quoteTokenAddress: '0xa2b31dacf30a9c50ca473337c01d8a201ae33e32', }; - const url = `${relayUrl}/orderbook?baseTokenAddress=${request.baseTokenAddress}"eTokenAddress=${ - request.quoteTokenAddress - }`; - it('gets order book', async () => { - fetchMock.get(url, orderbookJSON); + const url = `${relayUrl}/orderbook`; + it('gets orderbook with default page options when none are provided', async () => { + const urlWithQuery = `${url}?baseTokenAddress=${ + request.baseTokenAddress + }&page=0&per_page=100"eTokenAddress=${request.quoteTokenAddress}`; + fetchMock.get(urlWithQuery, orderbookJSON); const orderbook = await relayerClient.getOrderbookAsync(request); expect(orderbook).to.be.deep.equal(orderbookResponse); }); + it('gets orderbook with specified page options', async () => { + const urlWithQuery = `${url}?baseTokenAddress=${ + request.baseTokenAddress + }&page=3&per_page=50"eTokenAddress=${request.quoteTokenAddress}`; + fetchMock.get(urlWithQuery, orderbookJSON); + const pagedRequestOptions = { + page: 3, + perPage: 50, + }; + const orderbook = await relayerClient.getOrderbookAsync(request, pagedRequestOptions); + expect(orderbook).to.be.deep.equal(orderbookResponse); + }); it('throws an error for invalid JSON response', async () => { fetchMock.get(url, { test: 'dummy' }); expect(relayerClient.getOrderbookAsync(request)).to.be.rejected(); diff --git a/packages/website/ts/containers/connect_documentation.tsx b/packages/website/ts/containers/connect_documentation.tsx index 3e02a7d05..22ba4a7a1 100644 --- a/packages/website/ts/containers/connect_documentation.tsx +++ b/packages/website/ts/containers/connect_documentation.tsx @@ -51,9 +51,10 @@ const docsInfoConfig: DocsInfoConfig = { 'OrderbookChannelSubscriptionOpts', 'OrderbookRequest', 'OrderbookResponse', - 'OrdersRequest', + 'OrdersRequestOpts', + 'PagedRequestOpts', 'TokenPairsItem', - 'TokenPairsRequest', + 'TokenPairsRequestOpts', 'TokenTradeInfo', 'Order', 'SignedOrder', |