Skip to content

Commit

Permalink
[Console] Add support for JSON with long numerals
Browse files Browse the repository at this point in the history
Also:
* Add support for parsing and stringifying JSON with long numerals into `@osd/std`
* Upgrade `@opensearch/opensearch@2.3.0` which supports long numerals
* Add support for long numerals to `http/fetch`

Signed-off-by: Miki <miki@amazon.com>
  • Loading branch information
AMoo-Miki committed Jul 13, 2023
1 parent d746595 commit 92f46b5
Show file tree
Hide file tree
Showing 19 changed files with 630 additions and 27 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- Enable plugins to augment visualizations with additional data and context ([#4361](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4361))
- Dashboard De-Angularization ([#4502](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4502))
- New management overview page and rename stack management to dashboard management ([#4287](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4287))
- [Console] Add support for JSON with long numerals ([#4562](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4562))
- [Vis Augmenter] Update base vis height in view events flyout ([#4535](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4535))

### 🐛 Bug Fixes
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@
"@hapi/podium": "^4.1.3",
"@hapi/vision": "^6.1.0",
"@hapi/wreck": "^17.1.0",
"@opensearch-project/opensearch": "^2.2.0",
"@opensearch-project/opensearch": "^2.3.0",
"@osd/ace": "1.0.0",
"@osd/analytics": "1.0.0",
"@osd/apm-config-loader": "1.0.0",
Expand Down
3 changes: 2 additions & 1 deletion packages/osd-opensearch-archiver/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
},
"dependencies": {
"@osd/dev-utils": "1.0.0",
"@opensearch-project/opensearch": "^2.2.0"
"@osd/std": "1.0.0",
"@opensearch-project/opensearch": "^2.3.0"
},
"devDependencies": {}
}
3 changes: 2 additions & 1 deletion packages/osd-opensearch-archiver/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import readline from 'readline';

import { RunWithCommands, createFlagError } from '@osd/dev-utils';
import { Client, ClientOptions } from '@opensearch-project/opensearch';
import { parse } from '@osd/std';
import { readConfigFile } from '@osd/test';

import { OpenSearchArchiver } from './opensearch_archiver';
Expand Down Expand Up @@ -157,7 +158,7 @@ export function runCli() {
let parsedQuery;
if (typeof query === 'string' && query.length > 0) {
try {
parsedQuery = JSON.parse(query);
parsedQuery = parse(query);
} catch (err) {
throw createFlagError('--query should be valid JSON');
}
Expand Down
3 changes: 2 additions & 1 deletion packages/osd-opensearch-archiver/src/lib/archives/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
* under the License.
*/

import { parse } from '@osd/std';
import { createGunzip } from 'zlib';
import { PassThrough } from 'stream';
import {
Expand All @@ -45,6 +46,6 @@ export function createParseArchiveStreams({ gzip = false } = {}) {
createReplaceStream('\r\n', '\n'),
createSplitStream(RECORD_SEPARATOR),
createFilterStream<string>((l) => !!l.match(/[^\s]/)),
createMapStream<string>((json) => JSON.parse(json.trim())),
createMapStream<string>((json) => parse(json.trim())),
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
*/

import { Client } from '@opensearch-project/opensearch';
import { stringify } from '@osd/std';
import { Writable } from 'stream';
import { Stats } from '../stats';
import { Progress } from '../progress';
Expand Down Expand Up @@ -60,7 +61,7 @@ export function createIndexDocRecordsStream(

const resp = await client.bulk({ body }, { requestTimeout: 2 * 60 * 1000 });
if (resp.body.errors) {
throw new Error(`Failed to index all documents: ${JSON.stringify(resp, null, 2)}`);
throw new Error(`Failed to index all documents: ${stringify(resp, null, 2)}`);
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/osd-opensearch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"osd:watch": "../../scripts/use_node scripts/build --watch"
},
"dependencies": {
"@opensearch-project/opensearch": "^2.2.0",
"@opensearch-project/opensearch": "^2.3.0",
"@osd/dev-utils": "1.0.0",
"abort-controller": "^3.0.0",
"chalk": "^4.1.0",
Expand Down
72 changes: 71 additions & 1 deletion packages/osd-std/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,73 @@
# `@osd/std` — OpenSearch Dashboards standard library

This package is a set of utilities that can be used both on server-side and client-side.
This package is a set of utilities that can be used both on server-side and client-side.

## API

#### `assertNever`

Can be used in switch statements to ensure we perform exhaustive checks.

#### `deepFreeze`

Apply `Object.freeze` to a value recursively and convert the return type to `Readonly` variant recursively.

#### `get`

Retrieve the value for the specified path of an object.

#### `getFlattenedObject`

Flatten a deeply nested object to a map of dot-separated paths, pointing to all of its primitive values and arrays.

#### `stringify` and `parse`

Drop-in replacement for `JSON.stringify` and `JSON.parse`, capable of handling long numerals and `BigInt` values.

#### `mapToObject`

Convert a map to an object.

#### `mapValuesOfMap`

Create a new `Map` populated with the results of calling a provided function on every element in the input `Map`.

#### `groupIntoMap`

Group elements of an `Array` into a `Map` based on a provided function.

#### `merge`

Deeply merge two objects, omitting undefined values, and not deeply merging arrays.

#### `pick`

Create a new `Object` of specified keys and their values from an input `Object`.

#### `withTimeout`

Apply a `timeout` duration to a `Promise` before throwing an `Error` with the provided message.

#### `firstValueFrom` and `lastValueFrom`

Get a `Promise` that resolves as soon as the first or last value arrives from an observable.

#### `unset`

Unset a (potentially nested) key from given object.

#### `modifyUrl`

Get an `Object` resulting from applying a provided function to the meaningful parts of a URL.

#### `isRelativeUrl`

Determine if a url is relative.

#### `getUrlOrigin`

Get the origin URL of a provided URL.

#### `validateObject`

Deeply validate that an `Object` does not contain any `__proto__` or `constructor.prototype` keys, or circular references.
19 changes: 19 additions & 0 deletions packages/osd-std/src/__snapshots__/json.test.ts.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/osd-std/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ export { unset } from './unset';
export { getFlattenedObject } from './get_flattened_object';
export { validateObject } from './validate_object';
export * from './rxjs_7';
export { parse, stringify } from './json';
176 changes: 176 additions & 0 deletions packages/osd-std/src/json.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { stringify, parse } from './json';

describe('json', () => {
it('can parse', () => {
const input = {
a: [
{ A: 1 },
{ B: '2' },
{ C: [1, 2, 3, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'] },
],
b: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
c: {
i: {},
ii: [],
iii: '',
iv: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
},
};
const result = parse(JSON.stringify(input));
expect(result).toEqual(input);
});

it('can stringify', () => {
const input = {
a: [
{ A: 1 },
{ B: '2' },
{ C: [1, 2, 3, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'] },
],
b: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
c: {
i: {},
ii: [],
iii: '',
iv: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
},
};
const result = stringify(input);
expect(result).toEqual(JSON.stringify(input));
});

it('can apply a reviver while parsing', () => {
const input = {
A: 255,
B: {
i: [[]],
ii: 'Lorem ipsum',
iii: {},
rand: Math.random(),
},
};
const text = JSON.stringify(input);
function reviver(this: any, key: string, val: any) {
if (Array.isArray(val) && toString.call(this) === '[object Object]') this._hasArrays = true;
else if (typeof val === 'string') val = `<![CDATA[${val}]]>`;
else if (typeof val === 'number') val = val.toString(16);
else if (toString.call(this) === '[object Object]' && key === 'rand' && val === input.B.rand)
this._found = true;
return val;
}

expect(parse(text, reviver)).toEqual(JSON.parse(text, reviver));
});

it('can apply a replacer and spaces while stringifying', () => {
const input = {
A: 255,
B: {
i: [[]],
ii: 'Lorem ipsum',
iii: {},
rand: Math.random(),
},
};

function replacer(this: any, key: string, val: any) {
if (Array.isArray(val) && val.length === 0) val.push('<empty>');
else if (typeof val === 'string') val = `<![CDATA[${val}]]>`;
else if (typeof val === 'number') val = val.toString(16);
else if (toString.call(this) === '[object Object]' && key === 'rand' && val === input.B.rand)
val = 1;
return val;
}

expect(stringify(input, replacer, 2)).toEqual(JSON.stringify(input, replacer, 2));
});

it('can handle long numerals while parsing', () => {
const longPositive = BigInt(Number.MAX_SAFE_INTEGER) * 2n;
const longNegative = BigInt(Number.MIN_SAFE_INTEGER) * 2n;
const text =
`{` +
// The space before and after the values, and the lack of spaces before comma are intentional
`"\\":${longPositive}": "[ ${longNegative.toString()}, ${longPositive.toString()} ]", ` +
`"positive": ${longPositive.toString()}, ` +
`"array": [ ${longNegative.toString()}, ${longPositive.toString()} ], ` +
`"negative": ${longNegative.toString()},` +
`"number": 102931203123987` +
`}`;

const result = parse(text);
expect(result.positive).toBe(longPositive);
expect(result.negative).toBe(longNegative);
expect(result.array).toEqual([longNegative, longPositive]);
expect(result['":' + longPositive]).toBe(
`[ ${longNegative.toString()}, ${longPositive.toString()} ]`
);
expect(result.number).toBe(102931203123987);
});

it('can handle BigInt values while stringifying', () => {
const longPositive = BigInt(Number.MAX_SAFE_INTEGER) * 2n;
const longNegative = BigInt(Number.MIN_SAFE_INTEGER) * 2n;
const input = {
[`": ${longPositive}`]: `[ ${longNegative.toString()}, ${longPositive.toString()} ]`,
positive: longPositive,
negative: longNegative,
array: [longNegative, longPositive],
number: 102931203123987,
};

expect(stringify(input)).toMatchSnapshot();
});

it('can apply a reviver on long numerals while parsing', () => {
const longPositive = BigInt(Number.MAX_SAFE_INTEGER) * 2n;
const longNegative = BigInt(Number.MIN_SAFE_INTEGER) * 2n;
const text =
`{` +
// The space before and after the values, and the lack of spaces before comma are intentional
`"\\":${longPositive}": "[ ${longNegative.toString()}, ${longPositive.toString()} ]", ` +
`"positive": ${longPositive.toString()}, ` +
`"array": [ ${longNegative.toString()}, ${longPositive.toString()} ], ` +
`"negative": ${longNegative.toString()},` +
`"number": 102931203123987` +
`}`;

const reviver = (key: string, val: any) => (typeof val === 'bigint' ? val * 3n : val);

const result = parse(text, reviver);
expect(result.positive).toBe(longPositive * 3n);
expect(result.negative).toBe(longNegative * 3n);
expect(result.array).toEqual([longNegative * 3n, longPositive * 3n]);
expect(result['":' + longPositive]).toBe(
`[ ${longNegative.toString()}, ${longPositive.toString()} ]`
);
expect(result.number).toBe(102931203123987);
});

it('can apply a replacer and spaces values while stringifying BigInts', () => {
const longPositive = BigInt(Number.MAX_SAFE_INTEGER) * 2n;
const longNegative = BigInt(Number.MIN_SAFE_INTEGER) * 2n;
const input = {
[`": ${longPositive}`]: `[ ${longNegative.toString()}, ${longPositive.toString()} ]`,
positive: longPositive,
negative: longNegative,
array: [longNegative, longPositive, []],
number: 102931203123987,
};

function replacer(this: any, key: string, val: any) {
if (typeof val === 'bigint') val = val * 3n;
else if (Array.isArray(val) && val.length === 0) val.push('<empty>');
else if (typeof val === 'string') val = `<![CDATA[${val}]]>`;
else if (typeof val === 'number') val = val.toString(16);
return val;
}

expect(stringify(input, replacer, 4)).toMatchSnapshot();
});
});
Loading

0 comments on commit 92f46b5

Please sign in to comment.