-
Notifications
You must be signed in to change notification settings - Fork 268
Handle BigNumber conversions in JSON properly (without loss of precision) #71
Changes from all commits
9c478d2
d214c05
0e93ac8
5b4c893
bdeceba
e927fa1
1d6999c
72174ae
2bcb1b1
00e8cab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,22 @@ | ||
| import fetchMock from 'fetch-mock'; | ||
|
|
||
| import { BigNumber } from 'bignumber.js'; | ||
| import { SupersetClientClass, ClientConfig } from '../src/SupersetClientClass'; | ||
| import throwIfCalled from './utils/throwIfCalled'; | ||
| import { LOGIN_GLOB } from './fixtures/constants'; | ||
|
|
||
| /* NOTE: We're using fetchMock v6.5.2, but corresponding fetchMock type declaration files are only available for v6.0.2 | ||
| * and v7+. It looks like there are behavior changes between v6 and v7 that break our tests, so upgrading to v7 is | ||
| * probably some work. | ||
| * | ||
| * To avoid this, we're using the type declarations for v6.0.2, but there is at least one API inconsistency between that | ||
| * type declaration file and the actual library we're using. It looks like `sendAsJson` was added sometime after that | ||
| * release, or else the type declaration file isn't completely accurate. To get around this, it's necessary to add | ||
| * a `@ts-ignore` decorator before references to `sendAsJson` (there's one instance of that in this file). | ||
| * | ||
| * The **right** solution is probably to upgrade to fetchMock v7 (and the latest type declaration) and fix the tests | ||
| * that become broken as a result. | ||
| */ | ||
| describe('SupersetClientClass', () => { | ||
| beforeAll(() => { | ||
| fetchMock.get(LOGIN_GLOB, { csrf_token: '' }); | ||
|
|
@@ -125,6 +138,8 @@ describe('SupersetClientClass', () => { | |
| it('does not set csrfToken if response is not json', () => { | ||
| fetchMock.get(LOGIN_GLOB, '123', { | ||
| overwriteRoutes: true, | ||
| // @TODO remove once fetchMock is upgraded to 7+, see note at top of this file | ||
| // @ts-ignore | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why does this one require a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's complicated... We're using fetchMock v6.5.2, but type def files are only available for v6.0.2 and v7+. It looks like there are behavior changes between v6 and v7 that break our tests. So I opted to use the type def file for 6.0.2. It looks like I can see a few possible solutions:
I didn't want to invest the time into #1 or #2 without discussion, but I also didn't want to lose the type def information, so I opted for #3 for now. Thoughts?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see, I'm cool with otherwise
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I stuck with #3 for now and added comments + an |
||
| sendAsJson: false, | ||
| }); | ||
|
|
||
|
|
@@ -246,7 +261,8 @@ describe('SupersetClientClass', () => { | |
| const mockGetUrl = `${protocol}//${host}${mockGetEndpoint}`; | ||
| const mockPostUrl = `${protocol}//${host}${mockPostEndpoint}`; | ||
| const mockTextUrl = `${protocol}//${host}${mockTextEndpoint}`; | ||
| const mockTextJsonResponse = '{ "value": 9223372036854775807 }'; | ||
| const mockBigNumber = '9223372036854775807'; | ||
| const mockTextJsonResponse = `{ "value": ${mockBigNumber} }`; | ||
|
|
||
| fetchMock.get(mockGetUrl, { json: 'payload' }); | ||
| fetchMock.post(mockPostUrl, { json: 'payload' }); | ||
|
|
@@ -315,6 +331,21 @@ describe('SupersetClientClass', () => { | |
| ); | ||
| }); | ||
|
|
||
| it('supports parsing a response as JSON while preserving precision of large numbers', () => { | ||
| expect.assertions(2); | ||
| const client = new SupersetClientClass({ protocol, host }); | ||
|
|
||
| return client.init().then(() => | ||
| client.get({ url: mockTextUrl }).then(({ json }) => { | ||
| expect(fetchMock.calls(mockTextUrl)).toHaveLength(1); | ||
| // @ts-ignore | ||
| expect(json.value.toString()).toBe(new BigNumber(mockBigNumber).toString()); | ||
|
|
||
| return Promise.resolve(); | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| it('supports parsing a response as text', () => { | ||
| expect.assertions(2); | ||
| const client = new SupersetClientClass({ protocol, host }); | ||
|
|
@@ -424,6 +455,21 @@ describe('SupersetClientClass', () => { | |
| ); | ||
| }); | ||
|
|
||
| it('supports parsing a response as JSON while preserving precision of large numbers', () => { | ||
| expect.assertions(2); | ||
| const client = new SupersetClientClass({ protocol, host }); | ||
|
|
||
| return client.init().then(() => | ||
| client.post({ url: mockTextUrl }).then(({ json }) => { | ||
| expect(fetchMock.calls(mockTextUrl)).toHaveLength(1); | ||
| // @ts-ignore | ||
| expect(json.value.toString()).toBe(new BigNumber(mockBigNumber).toString()); | ||
|
|
||
| return Promise.resolve(); | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| it('supports parsing a response as text', () => { | ||
| expect.assertions(2); | ||
| const client = new SupersetClientClass({ protocol, host }); | ||
|
|
@@ -446,7 +492,7 @@ describe('SupersetClientClass', () => { | |
|
|
||
| return client.init().then(() => | ||
| client.post({ url: mockPostUrl, postPayload }).then(() => { | ||
| const formData = fetchMock.calls(mockPostUrl)[0][1].body; | ||
| const formData = fetchMock.calls(mockPostUrl)[0][1].body as FormData; | ||
| expect(fetchMock.calls(mockPostUrl)).toHaveLength(1); | ||
| Object.keys(postPayload).forEach(key => { | ||
| expect(formData.get(key)).toBe(JSON.stringify(postPayload[key])); | ||
|
|
@@ -464,7 +510,7 @@ describe('SupersetClientClass', () => { | |
|
|
||
| return client.init().then(() => | ||
| client.post({ url: mockPostUrl, postPayload, stringify: false }).then(() => { | ||
| const formData = fetchMock.calls(mockPostUrl)[0][1].body; | ||
| const formData = fetchMock.calls(mockPostUrl)[0][1].body as FormData; | ||
| expect(fetchMock.calls(mockPostUrl)).toHaveLength(1); | ||
| Object.keys(postPayload).forEach(key => { | ||
| expect(formData.get(key)).toBe(String(postPayload[key])); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,30 @@ | ||
| declare module 'fetch-mock'; | ||
| declare module 'json-bigint' { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @xtinec do you have any thoughts on this type of declaration? it looks okay to me, not sure if we can be more specific than
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we could do Looks like P.S. Unrelated but we could also add the type def for
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let us know what you think about this @soboko, this could be done in a separate PR if you want to get this merged.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I used I agree that adding a type def for I also added the type def for
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @soboko you're absolutely right. I agree we could merge this PR while we get the type def going for DefinitelyTyped.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So I looked into creating a proper type def file for DefinitelyTyped, and there's a bit of a complication. Specifically, the API for json-bigint looks like: module.exports = function(options) {
return {
parse: json_parse(options),
stringify: json_stringify
}
};
//create the default method members with no options applied for backwards compatibility
module.exports.parse = json_parse();
module.exports.stringify = json_stringify;The idea is that you can construct a parser with non-default options using something like: var JSONstrict = require('json-bigint')({"strict": true});AFAICT there's no clean way to reconcile the json-bigint API with ES6/TypeScript-style imports and type definition files. I've submitted a PR that would allow using something like the following to construct a "special" JSONbig parser: const JSONbigspecial = JSONbig.newInstance({ storeAsString: true, strict: true });If that PR is merged, then I can submit a decent type declaration file to DefinitelyTyped and problem solved. |
||
| interface JSONbig { | ||
| /** | ||
| * Converts a JavaScript Object Notation (JSON) string into an object, preserving precision for numeric values. | ||
| * @param text A valid JSON string. | ||
| * @param reviver A function that transforms the results. This function is called for each member of the object. | ||
| * If a member contains nested objects, the nested objects are transformed before the parent object is. | ||
| */ | ||
| parse(text: string, reviver?: (key: any, value: any) => any): any; | ||
|
|
||
| /** | ||
| * Converts a JavaScript value to a JavaScript Object Notation (JSON) string, preserving precision for numeric values. | ||
| * @param value A JavaScript value, usually an object or array, to be converted. | ||
| * @param replacer A function that transforms the results, or an array of strings and numbers that acts | ||
| * as a approved list for selecting the object properties that will be stringified. | ||
| * @param space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read. | ||
| */ | ||
| stringify( | ||
| value: any, | ||
| replacer?: (number | string)[] | null | ((key: string, value: any) => any), | ||
| space?: string | number, | ||
| ): string; | ||
| } | ||
|
|
||
| /** | ||
| * An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format. | ||
| */ | ||
| const JSONbig: JSONbig; | ||
| export = JSONbig; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.