From 97c5d31f4f206ec8edfee672be25a0ba6a28ced0 Mon Sep 17 00:00:00 2001 From: Igal Klebanov Date: Sat, 20 Apr 2024 21:29:10 +0300 Subject: [PATCH] add `objectStrategy` option that allows to not mutate result objects/arrays @ `ParseJSONResultsPlugin`. (#953) --- package-lock.json | 4 +- .../parse-json-results-plugin.ts | 51 +++++++++++++++---- .../src/parse-json-results-plugin.test.ts | 27 ++++++++++ 3 files changed, 69 insertions(+), 13 deletions(-) create mode 100644 test/node/src/parse-json-results-plugin.test.ts diff --git a/package-lock.json b/package-lock.json index a83c3fe4d..965f7e86f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "kysely", - "version": "0.27.2", + "version": "0.27.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kysely", - "version": "0.27.2", + "version": "0.27.3", "license": "MIT", "devDependencies": { "@types/better-sqlite3": "^7.6.8", diff --git a/src/plugin/parse-json-results/parse-json-results-plugin.ts b/src/plugin/parse-json-results/parse-json-results-plugin.ts index 899068d2a..59c612d29 100644 --- a/src/plugin/parse-json-results/parse-json-results-plugin.ts +++ b/src/plugin/parse-json-results/parse-json-results-plugin.ts @@ -8,6 +8,22 @@ import { PluginTransformResultArgs, } from '../kysely-plugin.js' +export interface ParseJSONResultsPluginOptions { + /** + * When `'in-place'`, arrays' and objects' values are parsed in-place. This is + * the most time and space efficient option. + * + * This can result in runtime errors if some objects/arrays are readonly. + * + * When `'create'`, new arrays and objects are created to avoid such errors. + * + * Defaults to `'in-place'`. + */ + objectStrategy?: ObjectStrategy +} + +type ObjectStrategy = 'in-place' | 'create' + /** * Parses JSON strings in query results into JSON objects. * @@ -22,6 +38,12 @@ import { * ``` */ export class ParseJSONResultsPlugin implements KyselyPlugin { + readonly #objectStrategy: ObjectStrategy + + constructor(readonly opt: ParseJSONResultsPluginOptions = {}) { + this.#objectStrategy = opt.objectStrategy || 'in-place' + } + // noop transformQuery(args: PluginTransformQueryArgs): RootOperationNode { return args.node @@ -32,30 +54,32 @@ export class ParseJSONResultsPlugin implements KyselyPlugin { ): Promise> { return { ...args.result, - rows: parseArray(args.result.rows), + rows: parseArray(args.result.rows, this.#objectStrategy), } } } -function parseArray(arr: T[]): T[] { +function parseArray(arr: T[], objectStrategy: ObjectStrategy): T[] { + const target = objectStrategy === 'create' ? new Array(arr.length) : arr + for (let i = 0; i < arr.length; ++i) { - arr[i] = parse(arr[i]) as T + target[i] = parse(arr[i], objectStrategy) as T } - return arr + return target } -function parse(obj: unknown): unknown { +function parse(obj: unknown, objectStrategy: ObjectStrategy): unknown { if (isString(obj)) { return parseString(obj) } if (Array.isArray(obj)) { - return parseArray(obj) + return parseArray(obj, objectStrategy) } if (isPlainObject(obj)) { - return parseObject(obj) + return parseObject(obj, objectStrategy) } return obj @@ -64,7 +88,7 @@ function parse(obj: unknown): unknown { function parseString(str: string): unknown { if (maybeJson(str)) { try { - return parse(JSON.parse(str)) + return parse(JSON.parse(str), 'in-place') } catch (err) { // this catch block is intentionally empty. } @@ -77,10 +101,15 @@ function maybeJson(value: string): boolean { return value.match(/^[\[\{]/) != null } -function parseObject(obj: Record): Record { +function parseObject( + obj: Record, + objectStrategy: ObjectStrategy, +): Record { + const target = objectStrategy === 'create' ? {} : obj + for (const key in obj) { - obj[key] = parse(obj[key]) + target[key] = parse(obj[key], objectStrategy) } - return obj + return target } diff --git a/test/node/src/parse-json-results-plugin.test.ts b/test/node/src/parse-json-results-plugin.test.ts new file mode 100644 index 000000000..c529657c7 --- /dev/null +++ b/test/node/src/parse-json-results-plugin.test.ts @@ -0,0 +1,27 @@ +import { ParseJSONResultsPlugin } from '../../..' +import { createQueryId } from '../../../dist/cjs/util/query-id.js' + +describe('ParseJSONResultsPlugin', () => { + describe("when `objectStrategy` is 'create'", () => { + let plugin: ParseJSONResultsPlugin + + beforeEach(() => { + plugin = new ParseJSONResultsPlugin({ objectStrategy: 'create' }) + }) + + it('should parse JSON results that contain readonly arrays/objects', async () => { + await plugin.transformResult({ + queryId: createQueryId(), + result: { + rows: [ + Object.freeze({ + id: 1, + carIds: Object.freeze([1, 2, 3]), + metadata: JSON.stringify({ foo: 'bar' }), + }), + ], + }, + }) + }) + }) +})