From 93810f351fac8ae6bdb65a6a63895c500f78e48b Mon Sep 17 00:00:00 2001 From: Miki Date: Wed, 12 Jul 2023 22:39:15 -0700 Subject: [PATCH] [Console] Add support for JSON with long numerals 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 --- CHANGELOG.md | 1 + package.json | 2 +- packages/osd-opensearch-archiver/package.json | 2 +- packages/osd-opensearch/package.json | 2 +- .../src/__snapshots__/json.test.ts.snap | 19 ++ packages/osd-std/src/index.ts | 1 + packages/osd-std/src/json.test.ts | 176 ++++++++++ packages/osd-std/src/json.ts | 315 ++++++++++++++++++ src/core/public/http/fetch.ts | 6 +- .../send_request_to_opensearch.ts | 5 +- src/plugins/console/public/lib/utils/index.ts | 5 +- .../console/public/services/storage.ts | 5 +- .../api/console/proxy/create_handler.ts | 9 +- .../json_xjson_translation_tools/index.ts | 5 +- yarn.lock | 8 +- 15 files changed, 540 insertions(+), 21 deletions(-) create mode 100644 packages/osd-std/src/__snapshots__/json.test.ts.snap create mode 100644 packages/osd-std/src/json.test.ts create mode 100644 packages/osd-std/src/json.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index cb7f3da8747e..83f6d53e1e9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/package.json b/package.json index ec99d6b134a6..77d44f311303 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/osd-opensearch-archiver/package.json b/packages/osd-opensearch-archiver/package.json index 1c036dc10c50..88e50469b63f 100644 --- a/packages/osd-opensearch-archiver/package.json +++ b/packages/osd-opensearch-archiver/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@osd/dev-utils": "1.0.0", - "@opensearch-project/opensearch": "^2.2.0" + "@opensearch-project/opensearch": "^2.3.0" }, "devDependencies": {} } diff --git a/packages/osd-opensearch/package.json b/packages/osd-opensearch/package.json index 44404a9ae5a3..8f188a42a091 100644 --- a/packages/osd-opensearch/package.json +++ b/packages/osd-opensearch/package.json @@ -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", diff --git a/packages/osd-std/src/__snapshots__/json.test.ts.snap b/packages/osd-std/src/__snapshots__/json.test.ts.snap new file mode 100644 index 000000000000..42854db1dd78 --- /dev/null +++ b/packages/osd-std/src/__snapshots__/json.test.ts.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`json can apply a replacer and spaces values while stringifying BigInts 1`] = ` +"{ + \\"\\\\\\": 18014398509481982\\": \\"\\", + \\"positive\\": 54043195528445946, + \\"negative\\": -54043195528445946, + \\"array\\": [ + -54043195528445946, + 54043195528445946, + [ + \\"]]>\\" + ] + ], + \\"number\\": \\"5d9d89cc6b13\\" +}" +`; + +exports[`json can handle BigInt values while stringifying 1`] = `"{\\"\\\\\\": 18014398509481982\\":\\"[ -18014398509481982, 18014398509481982 ]\\",\\"positive\\":18014398509481982,\\"negative\\":-18014398509481982,\\"array\\":[-18014398509481982,18014398509481982],\\"number\\":102931203123987}"`; diff --git a/packages/osd-std/src/index.ts b/packages/osd-std/src/index.ts index 49902a2c2479..58a9031564f5 100644 --- a/packages/osd-std/src/index.ts +++ b/packages/osd-std/src/index.ts @@ -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'; diff --git a/packages/osd-std/src/json.test.ts b/packages/osd-std/src/json.test.ts new file mode 100644 index 000000000000..33abd71d91d2 --- /dev/null +++ b/packages/osd-std/src/json.test.ts @@ -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 = ``; + 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(''); + else if (typeof val === 'string') 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(''); + else if (typeof val === 'string') val = ``; + else if (typeof val === 'number') val = val.toString(16); + return val; + } + + expect(stringify(input, replacer, 4)).toMatchSnapshot(); + }); +}); diff --git a/packages/osd-std/src/json.ts b/packages/osd-std/src/json.ts new file mode 100644 index 000000000000..8b149fad3a2e --- /dev/null +++ b/packages/osd-std/src/json.ts @@ -0,0 +1,315 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +/* In JavaScript, a `Number` is a 64-bit floating-point value which can store 16 digits. However, the + * serializer and deserializer will need to cater to numeric values generated by other languages which + * can have up to 19 digits. Native JSON parser and stringifier, incapable of handling the extra + * digits, corrupt the values, making them unusable. + * + * To work around this limitation, the deserializer converts long sequences of digits into strings and + * marks them before applying the parser. During the parsing, string values that begin with the mark + * are converted to `BigInt` values. + * Similarly, during stringification, the serializer converts `BigInt` values to marked strings and + * when done, it replaces them with plain numerals. + * + * `Number.MAX_SAFE_INTEGER`, 9,007,199,254,740,991, is the largest number that the native methods can + * parse and stringify, and any numeral greater than that would need to be translated using the + * workaround; all 17-digits or longer and only tail-end of the 16-digits need translation. It would + * be unfair to all the 16-digit numbers if the translation applied to `\d{16,}` only to cover the + * less than 10%. Hence, a RegExp is created to only match numerals too long to be a number. + * + * To make the explanation simpler, let's assume that MAX_SAFE_INTEGER is 8921 which has 4 digits. + * Starting from the right, we take each digit onwards, `[-9]`: + * 1) 7922 - 7929: 792[2-9]\d{0} + * 2) 7930 - 7999: 79[3-9]\d{1} + * 9) 9 + 1 = 10 which results in a rollover; no need to do anything. + * 8) 9000 - 9999: [9-9]\d{3} + * Finally we add anything 5 digits or longer: `\d{5,} + * + * Note: A better solution would use AST but considering its performance penalty, RegExp is the next + * best thing. + */ +const maxIntAsString = String(Number.MAX_SAFE_INTEGER); +const maxIntLength = maxIntAsString.length; +// Sub-patterns for each digit +const longNumeralMatcherTokens = [`\\d{${maxIntAsString.length + 1},}`]; +for (let i = 0; i < maxIntLength; i++) { + if (maxIntAsString[i] !== '9') { + longNumeralMatcherTokens.push( + maxIntAsString.substring(0, i) + + `[${parseInt(maxIntAsString[i], 10) + 1}-9]` + + `\\d{${maxIntLength - i - 1}}` + ); + } +} + +/* The matcher that looks for `": , ...}` and `[..., , ...]` + * + * The pattern starts by looking for `":` not immediately preceded by a `\`. That should be + * followed by any of the numeric sub-patterns. A comma, end of an array, end of an object, or + * the end of the input are the only acceptable elements after it. + * + * Note: This RegExp can result in false-positive hits on the likes of `{"key": "[ ]"}` and + * those are cleaned out during parsing. + */ +const longNumeralMatcher = new RegExp( + `((?:\\[|,|(? { + const choices = []; + const arr = markerChars; + const arrLength = arr.length; + const temp = Array(length); + + (function fill(pos, start) { + if (pos === length) return choices.push(temp.join('')); + + for (let i = start; i < arrLength; i++) { + temp[pos] = arr[i]; + fill(pos + 1, i); + } + })(0, 0); + + return choices; +}; + +/* Experiments with different combinations of various lengths, until one is found to not be in + * the input string. + */ +const getMarker = (text: string): { marker: string; length: number } => { + let marker; + let length = 0; + do { + length++; + getMarkerChoices(length).some((markerChoice) => { + if (text.indexOf(markerChoice) === -1) { + marker = markerChoice; + return true; + } + }); + } while (!marker); + + return { + marker, + length, + }; +}; + +const parseStringWithLongNumerals = ( + text: string, + reviver?: (this: any, key: string, value: any) => any +): any => { + const { marker, length } = getMarker(text); + + let hadException; + let obj; + let markedJSON = text.replace(longNumeralMatcher, `$1"${marker}$2"$3`); + const markedValueMatcher = new RegExp(`^${marker}-?\\d+$`); + + /* Convert marked values to BigInt values. + * The `startsWith` is purely for performance, to avoid running `test` if not needed. + */ + const convertMarkedValues = (val: any) => + typeof val === 'string' && val.startsWith(marker) && markedValueMatcher.test(val) + ? BigInt(val.substring(length)) + : val; + + /* For better performance, instead of testing for existence of `reviver` on each value, two almost + * identical functions are used. + */ + const parseMarkedText = reviver + ? (markedText: string) => + JSON.parse(markedText, function (key, val) { + return reviver.call(this, key, convertMarkedValues(val)); + }) + : (markedText: string) => JSON.parse(markedText, (key, val) => convertMarkedValues(val)); + + /* RegExp cannot replace AST and the process of marking adds quotes. So, any false-positive hit + * will make the JSON string unparseable. + * + * To find those instances, we try to parse and watch for the location of any errors. If an error + * is caused by the marking, we remove that single marking and try again. + */ + do { + try { + hadException = false; + // Not using a reviver here to save time + obj = parseMarkedText(markedJSON); + } catch (e) { + hadException = true; + /* There are two types of exception objects that can be raised: + * 1) a proper object with lineNumber and columnNumber which we can use + * 2) a textual message with the position that we need to parse + */ + let { lineNumber, columnNumber } = e; + if (!lineNumber || !columnNumber) { + const match = e?.message?.match?.(/^Unexpected token.*at position (\d+)$/); + if (match) { + lineNumber = 1; + // The position is zero-indexed; adding 1 to normalize it for the -2 that comes later + columnNumber = parseInt(match[1], 10) + 1; + } + } + + if (lineNumber < 1 || columnNumber < 2) { + // The problem is not with this replacement; just return a failure. + throw e; + } + + /* We need to skip e.lineNumber - 1 number of `\n` occurrences. + * Then, we need to go to e.columnNumber - 2 to look for `"\d+"`; we need to `-1` to + * account for the quote but an additional `-1` is needed because columnNumber starts from 1. + */ + const re = new RegExp( + `^((?:.*\\n){${lineNumber - 1}}[^\\n]{${columnNumber - 2}})"${marker}(-?\\d+)"` + ); + if (!re.test(markedJSON)) { + // The exception is not caused by adding the marker + throw e; + } + + // We have found a bad replacement; let's remove it. + markedJSON = markedJSON.replace(re, '$1$2'); + } + } while (hadException); + + return obj; +}; + +const stringifyObjectWithBigInts = ( + obj: any, + candidate: string, + replacer?: (this: any, key: string, value: any) => any, + space?: string | number +): string => { + const { marker } = getMarker(candidate); + + /* The matcher that looks for "" + * Because we have made sure that `marker` was never present in the original object, we can + * carelessly assume every "" is due to our marking. + */ + const markedBigIntMatcher = new RegExp(`"${marker}(-?\\d+)"`, 'g'); + + /* Convert BigInt values to a string and mark them. + * Can't be bothered with Number values outside the safe range because they are already corrupted. + * + * For better performance, instead of testing for existence of `replacer` on each value, two almost + * identical functions are used. + */ + const addMarkerToBigInts = replacer + ? function (this: any, key: string, val: any) { + // replacer is called before marking because marking changes the type + const newVal = replacer.call(this, key, val); + return typeof newVal === 'bigint' ? `${marker}${newVal.toString()}` : newVal; + } + : (key: string, val: any) => (typeof val === 'bigint' ? `${marker}${val.toString()}` : val); + + return ( + JSON.stringify(obj, addMarkerToBigInts, space) + // Replace marked substrings with just the numerals + .replace(markedBigIntMatcher, '$1') + ); +}; + +export const stringify = ( + obj: any, + replacer?: (this: any, key: string, value: any) => any, + space?: string | number +): string => { + let text; + let numeralsAreNumbers = true; + /* For better performance, instead of testing for existence of `replacer` on each value, two almost + * identical functions are used. + * + * Note: Converting BigInt values to numbers, `Number()` is much faster that `parseInt()`. Since we + * check the `type`, it is safe to just use `Number()`. + */ + const checkForBigInts = replacer + ? function (this: any, key: string, val: any) { + if (typeof val === 'bigint') { + numeralsAreNumbers = false; + return replacer.call(this, key, Number(val)); + } + return replacer.call(this, key, val); + } + : (key: string, val: any) => { + if (typeof val === 'bigint') { + numeralsAreNumbers = false; + return Number(val); + } + return val; + }; + + /* While this is a check for possibly having BigInt values, if none were found, the results is + * sufficient to fulfill the purpose of the function. However, if BigInt values were found, we will + * use `stringifyObjectWithBigInts` to do this again. + * + * The goal was not to punish every object that doesn't have a BigInt with the more expensive + * `stringifyObjectWithBigInts`. Those with BigInt values are also not unduly burdened because we + * still need it in its string form to find a suitable marker. + */ + text = JSON.stringify(obj, checkForBigInts, space); + + if (!numeralsAreNumbers) { + const temp = stringifyObjectWithBigInts(obj, text, replacer, space); + if (temp) text = temp; + } + + return text; +}; + +export const parse = (text: string, reviver?: (this: any, key: string, value: any) => any): any => { + let obj; + let numeralsAreNumbers = true; + const inspectValueForLargeNumerals = (val: any) => { + if ( + numeralsAreNumbers && + typeof val === 'number' && + (val < Number.MAX_SAFE_INTEGER || val > Number.MAX_SAFE_INTEGER) + ) { + numeralsAreNumbers = false; + } + + // This function didn't have to have a return value but having it makes the rest cleaner + return val; + }; + + /* For better performance, instead of testing for existence of `reviver` on each value, two almost + * identical functions are used. + */ + const checkForLargeNumerals = reviver + ? function (this: any, key: string, val: any) { + return inspectValueForLargeNumerals(reviver.call(this, key, val)); + } + : (key: string, val: any) => inspectValueForLargeNumerals(val); + + /* While this is a check for possibly having BigInt values, if none were found, the results is + * sufficient to fulfill the purpose of the function. However, if BigInt values were found, we will + * use `stringifyObjectWithBigInts` to do this again. + * + * The goal was not to punish every object that doesn't have a BigInt with the more expensive + * `stringifyObjectWithBigInts`. Those with BigInt values are also not unduly burdened because we + * still need it in its string form to find a suitable marker. + */ + obj = JSON.parse(text, checkForLargeNumerals); + + if (!numeralsAreNumbers) { + const temp = parseStringWithLongNumerals(text, reviver); + if (temp) { + obj = temp; + } + } + + return obj; +}; diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts index 694372c46d99..a8ae1ff0eaa2 100644 --- a/src/core/public/http/fetch.ts +++ b/src/core/public/http/fetch.ts @@ -31,7 +31,7 @@ import { omitBy } from 'lodash'; import { format } from 'url'; import { BehaviorSubject } from 'rxjs'; -import { isRelativeUrl } from '@osd/std'; +import { isRelativeUrl, parse } from '@osd/std'; import { IBasePath, @@ -190,12 +190,12 @@ export class Fetch { if (NDJSON_CONTENT.test(contentType)) { body = await response.blob(); } else if (JSON_CONTENT.test(contentType)) { - body = await response.json(); + body = parse(await response.text()); } else { const text = await response.text(); try { - body = JSON.parse(text); + body = parse(text); } catch (err) { body = text; } diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.ts index 4e1ae7267542..1cb992a7a99c 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.ts @@ -28,6 +28,7 @@ * under the License. */ +import { stringify } from '@osd/std'; import { HttpFetchError, HttpSetup } from 'opensearch-dashboards/public'; import { extractDeprecationMessages } from '../../../lib/utils'; import { XJson } from '../../../../../opensearch_ui_shared/public'; @@ -116,7 +117,7 @@ export function sendRequestToOpenSearch( const contentType = httpResponse.response.headers.get('Content-Type') as BaseResponseType; let value = ''; if (contentType.includes('application/json')) { - value = JSON.stringify(httpResponse.body, null, 2); + value = stringify(httpResponse.body, null, 2); } else { value = httpResponse.body; } @@ -155,7 +156,7 @@ export function sendRequestToOpenSearch( if (httpError.body) { contentType = httpResponse.headers.get('Content-Type') as string; if (contentType?.includes('application/json')) { - value = JSON.stringify(httpError.body, null, 2); + value = stringify(httpError.body, null, 2); } else { value = httpError.body; } diff --git a/src/plugins/console/public/lib/utils/index.ts b/src/plugins/console/public/lib/utils/index.ts index 93a0688ae725..7ea4cd9b893f 100644 --- a/src/plugins/console/public/lib/utils/index.ts +++ b/src/plugins/console/public/lib/utils/index.ts @@ -28,6 +28,7 @@ * under the License. */ +import { parse, stringify } from '@osd/std'; import _ from 'lodash'; import { XJson } from '../../../../opensearch_ui_shared/public'; @@ -42,7 +43,7 @@ export function textFromRequest(request: any) { } export function jsonToString(data: any, indent: boolean) { - return JSON.stringify(data, null, indent ? 2 : 0); + return stringify(data, null, indent ? 2 : 0); } export function formatRequestBodyDoc(data: string[], indent: boolean) { @@ -51,7 +52,7 @@ export function formatRequestBodyDoc(data: string[], indent: boolean) { for (let i = 0; i < data.length; i++) { const curDoc = data[i]; try { - let newDoc = jsonToString(JSON.parse(collapseLiteralStrings(curDoc)), indent); + let newDoc = jsonToString(parse(collapseLiteralStrings(curDoc)), indent); if (indent) { newDoc = expandLiteralStrings(newDoc); } diff --git a/src/plugins/console/public/services/storage.ts b/src/plugins/console/public/services/storage.ts index f143b0cefcfe..870904a4b3d8 100644 --- a/src/plugins/console/public/services/storage.ts +++ b/src/plugins/console/public/services/storage.ts @@ -29,6 +29,7 @@ */ import { transform, keys, startsWith } from 'lodash'; +import { parse, stringify } from '@osd/std'; type IStorageEngine = typeof window.localStorage; @@ -40,12 +41,12 @@ export class Storage { constructor(private readonly engine: IStorageEngine, private readonly prefix: string) {} encode(val: any) { - return JSON.stringify(val); + return stringify(val); } decode(val: any) { if (typeof val === 'string') { - return JSON.parse(val); + return parse(val); } } diff --git a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts index 0a401ded813b..d17328741e81 100644 --- a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts +++ b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts @@ -32,7 +32,7 @@ import { OpenSearchDashboardsRequest, RequestHandler } from 'opensearch-dashboar import { trimStart } from 'lodash'; import { Readable } from 'stream'; -import { ApiResponse } from '@opensearch-project/opensearch/'; +import { ApiResponse, Serializer } from '@opensearch-project/opensearch'; // eslint-disable-next-line @osd/eslint/no-restricted-paths import { ensureRawRequest } from '../../../../../../../core/server/http/router'; @@ -44,6 +44,8 @@ import { RouteDependencies } from '../../../'; import { Body, Query } from './validation_config'; import { buildBufferedBody } from './utils'; +const serializer = new Serializer(); + function getProxyHeaders(req: OpenSearchDashboardsRequest) { const headers = Object.create(null); @@ -121,9 +123,10 @@ export const createHandler = ({ if (method.toUpperCase() !== 'HEAD') { return response.custom({ statusCode: statusCode!, - body: responseContent, + body: serializer.serialize(responseContent), headers: { warning: warnings || '', + 'Content-Type': 'application/json; charset=utf-8', }, }); } @@ -139,7 +142,7 @@ export const createHandler = ({ } catch (e: any) { const isResponseErrorFlag = isResponseError(e); if (!isResponseError) log.error(e); - const errorMessage = isResponseErrorFlag ? JSON.stringify(e.meta.body) : e.message; + const errorMessage = isResponseErrorFlag ? stringify(e.meta.body) : e.message; // core http route handler has special logic that asks for stream readable input to pass error opaquely const errorResponseBody = new Readable({ read() { diff --git a/src/plugins/opensearch_ui_shared/__packages_do_not_import__/xjson/json_xjson_translation_tools/index.ts b/src/plugins/opensearch_ui_shared/__packages_do_not_import__/xjson/json_xjson_translation_tools/index.ts index 9f42e669b280..9860be7800e4 100644 --- a/src/plugins/opensearch_ui_shared/__packages_do_not_import__/xjson/json_xjson_translation_tools/index.ts +++ b/src/plugins/opensearch_ui_shared/__packages_do_not_import__/xjson/json_xjson_translation_tools/index.ts @@ -28,12 +28,13 @@ * under the License. */ +import { parse, stringify } from '@osd/std'; import { extractJSONStringValues } from './parser'; export function collapseLiteralStrings(data: string) { const splitData = data.split(`"""`); for (let idx = 1; idx < splitData.length - 1; idx += 2) { - splitData[idx] = JSON.stringify(splitData[idx]); + splitData[idx] = stringify(splitData[idx]); } return splitData.join(''); } @@ -79,7 +80,7 @@ export function expandLiteralStrings(data: string) { (candidate[candidate.length - 2] === '"' && candidate[candidate.length - 3] === '\\'); if (!skip && candidate.match(/\\./)) { - result += `"""${JSON.parse(candidate)}"""`; + result += `"""${parse(candidate)}"""`; } else { result += candidate; } diff --git a/yarn.lock b/yarn.lock index cdbe028128d7..1bd1545e26fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2498,10 +2498,10 @@ mkdirp "^1.0.4" rimraf "^3.0.2" -"@opensearch-project/opensearch@^2.2.0": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@opensearch-project/opensearch/-/opensearch-2.2.1.tgz#a400203afa6512ef73945663163a404763a10f5a" - integrity sha512-8zfQX1acL9eWG+ohIc9nJVT9LSqXCdbVEJs0rCPRtji3XF6ahzsiKmGNTeWLxCPDxWCjAIWq9t95xP3Y5Egi6Q== +"@opensearch-project/opensearch@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@opensearch-project/opensearch/-/opensearch-2.3.0.tgz#ac1703e14e29f20e6e2513d3b33d363e05409dc5" + integrity sha512-sqMLZj477aoSEcODcTjJCCzm58cbv6BUionZ2jCcBzfSLgHS9jnmNFdVrUy998QOOkoRHIcZ6DAzjInmI9WQzg== dependencies: aws4 "^1.11.0" debug "^4.3.1"