Skip to content

Commit

Permalink
feat: Add HTTP Post to cubejs client core (#1608). Thanks to @mnifakram!
Browse files Browse the repository at this point in the history
  • Loading branch information
mnifakram committed Dec 18, 2020
1 parent cb047b0 commit 1ebd6a0
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 15 deletions.
1 change: 1 addition & 0 deletions docs/content/Cube.js-Frontend/@cubejs-client-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@ Name | Type | Description |
------ | ------ | ------ |
apiUrl | string | URL of your Cube.js Backend. By default, in the development environment it is `http://localhost:4000/cubejs-api/v1` |
credentials? | "omit" | "same-origin" | "include" | - |
method? | "GET" | "POST" | | HTTP method, by default it use GET. |
headers? | Record‹string, string› | - |
parseDateMeasures? | boolean | - |
pollInterval? | number | - |
Expand Down
34 changes: 21 additions & 13 deletions packages/cubejs-client-core/src/HttpTransport.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,42 @@ import fetch from 'cross-fetch';
import 'url-search-params-polyfill';

class HttpTransport {
constructor({ authorization, apiUrl, headers = {}, credentials }) {
constructor({ authorization, apiUrl, method, headers = {}, credentials }) {
this.authorization = authorization;
this.apiUrl = apiUrl;
this.method = method;
this.headers = headers;
this.credentials = credentials;
}

request(method, { baseRequestId, ...params }) {
let spanCounter = 1;
const searchParams = new URLSearchParams(
params && Object.keys(params)
.map(k => ({ [k]: typeof params[k] === 'object' ? JSON.stringify(params[k]) : params[k] }))
.reduce((a, b) => ({ ...a, ...b }), {})
);

let spanCounter = 1;
let url = `${this.apiUrl}/${method}${searchParams.toString().length ? `?${searchParams}` : ''}`;

this.method = this.method || (url.length < 2000 ? 'GET' : 'POST');
if (this.method === 'POST') {
url = `${this.apiUrl}/${method}`;
this.headers['Content-Type'] = 'application/json';
}

// Currently, all methods make GET requests. If a method makes a request with a body payload,
// remember to add a 'Content-Type' header.
const runRequest = () => fetch(
`${this.apiUrl}/${method}${searchParams.toString().length ? `?${searchParams}` : ''}`, {
headers: {
Authorization: this.authorization,
'x-request-id': baseRequestId && `${baseRequestId}-span-${spanCounter++}`,
...this.headers
},
credentials: this.credentials
}
);
// remember to add {'Content-Type': 'application/json'} to the header.
const runRequest = () => fetch(url, {
method: this.method,
headers: {
Authorization: this.authorization,
'x-request-id': baseRequestId && `${baseRequestId}-span-${spanCounter++}`,
...this.headers
},
credentials: this.credentials,
body: this.method === 'POST' ? JSON.stringify(params) : null
});

return {
async subscribe(callback) {
Expand Down
61 changes: 59 additions & 2 deletions packages/cubejs-client-core/src/HttpTransport.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ describe('HttpTransport', () => {
dimensions: ['Users.country']
};
const queryUrlEncoded = '%7B%22measures%22%3A%5B%22Orders.count%22%5D%2C%22dimensions%22%3A%5B%22Users.country%22%5D%7D';
const queryJson = '{"query":{"measures":["Orders.count"],"dimensions":["Users.country"]}}';

const ids = [];
for (let i = 0; i < 40; i++) ids.push('a40b2052-4137-11eb-b378-0242ac130002');
const LargeQuery = {
measures: ['Orders.count'],
dimensions: ['Users.country'],
filters: [
{
member: 'Users.id',
operator: 'equals',
values: ids
}
]
};
const largeQueryJson = `{"query":{"measures":["Orders.count"],"dimensions":["Users.country"],"filters":[{"member":"Users.id","operator":"equals","values":${JSON.stringify(ids)}}]}}`;

afterEach(() => {
fetch.mockClear();
Expand All @@ -27,9 +43,11 @@ describe('HttpTransport', () => {
await req.subscribe(() => { });
expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledWith(`${apiUrl}/load?query=${queryUrlEncoded}`, {
method: 'GET',
headers: {
Authorization: 'token',
}
},
body: null
});
});

Expand All @@ -47,10 +65,49 @@ describe('HttpTransport', () => {
await req.subscribe(() => { });
expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledWith(`${apiUrl}/meta?extraParams=${serializedExtraParams}`, {
method: 'GET',
headers: {
Authorization: 'token',
'X-Extra-Header': '42'
}
},
body: null
});
});

test('it serializes the query object and sends it in the body', async () => {
const transport = new HttpTransport({
authorization: 'token',
apiUrl,
method: 'POST'
});
const req = transport.request('load', { query });
await req.subscribe(() => { });
expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledWith(`${apiUrl}/load`, {
method: 'POST',
headers: {
Authorization: 'token',
'Content-Type': 'application/json'
},
body: queryJson
});
});

test('it use POST over GET if url length is more than 2000 characters', async () => {
const transport = new HttpTransport({
authorization: 'token',
apiUrl
});
const req = transport.request('load', { query: LargeQuery });
await req.subscribe(() => { });
expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledWith(`${apiUrl}/load`, {
method: 'POST',
headers: {
Authorization: 'token',
'Content-Type': 'application/json'
},
body: largeQueryJson
});
});
});
2 changes: 2 additions & 0 deletions packages/cubejs-client-core/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ class CubejsApi {
options = options || {};
this.apiToken = apiToken;
this.apiUrl = options.apiUrl || API_URL;
this.method = options.method;
this.headers = options.headers || {};
this.credentials = options.credentials;
this.transport = options.transport || new HttpTransport({
authorization: typeof apiToken === 'function' ? undefined : apiToken,
apiUrl: this.apiUrl,
method: this.method,
headers: this.headers,
credentials: this.credentials
});
Expand Down

0 comments on commit 1ebd6a0

Please sign in to comment.