Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 66b4fa4

Browse files
committedOct 27, 2024·
add more control through configuration @ ParseJSONResultsPlugin.
1 parent 54d4013 commit 66b4fa4

File tree

1 file changed

+78
-21
lines changed

1 file changed

+78
-21
lines changed
 

‎src/plugin/parse-json-results/parse-json-results-plugin.ts

+78-21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { QueryResult } from '../../driver/database-connection.js'
22
import { RootOperationNode } from '../../query-compiler/query-compiler.js'
3-
import { isPlainObject, isString } from '../../util/object-utils.js'
3+
import { freeze, isPlainObject, isString } from '../../util/object-utils.js'
44
import { UnknownRow } from '../../util/type-utils.js'
55
import {
66
KyselyPlugin,
@@ -9,6 +9,13 @@ import {
99
} from '../kysely-plugin.js'
1010

1111
export interface ParseJSONResultsPluginOptions {
12+
/**
13+
* A function that returns `true` if the given string is a JSON string.
14+
*
15+
* Defaults to a function that checks if the string starts and ends with `{}` or `[]`.
16+
*/
17+
isJSON?: (value: string) => boolean
18+
1219
/**
1320
* When `'in-place'`, arrays' and objects' values are parsed in-place. This is
1421
* the most time and space efficient option.
@@ -20,10 +27,27 @@ export interface ParseJSONResultsPluginOptions {
2027
* Defaults to `'in-place'`.
2128
*/
2229
objectStrategy?: ObjectStrategy
30+
31+
/**
32+
* The reviver function that will be passed to `JSON.parse`.
33+
* See {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#the_reviver_parameter | The reviver parameter}.
34+
*/
35+
reviver?: (key: string, value: unknown, context?: any) => unknown
36+
37+
/**
38+
* An array of keys that should not be parsed inside an object, even if they contain JSON strings.
39+
*/
40+
skipKeys?: string[]
2341
}
2442

2543
type ObjectStrategy = 'in-place' | 'create'
2644

45+
type ProcessedParseJSONResultsPluginOptions = {
46+
readonly [K in keyof ParseJSONResultsPluginOptions]-?: K extends 'skipKeys'
47+
? Record<string, true>
48+
: ParseJSONResultsPluginOptions[K]
49+
}
50+
2751
/**
2852
* Parses JSON strings in query results into JSON objects.
2953
*
@@ -38,10 +62,21 @@ type ObjectStrategy = 'in-place' | 'create'
3862
* ```
3963
*/
4064
export class ParseJSONResultsPlugin implements KyselyPlugin {
41-
readonly #objectStrategy: ObjectStrategy
65+
readonly #options: ProcessedParseJSONResultsPluginOptions
4266

4367
constructor(readonly opt: ParseJSONResultsPluginOptions = {}) {
44-
this.#objectStrategy = opt.objectStrategy || 'in-place'
68+
this.#options = freeze({
69+
isJSON: opt.isJSON || maybeJson,
70+
objectStrategy: opt.objectStrategy || 'in-place',
71+
reviver: opt.reviver || ((_, value) => value),
72+
skipKeys: (opt.skipKeys || []).reduce(
73+
(acc, key) => {
74+
acc[key] = true
75+
return acc
76+
},
77+
{} as Record<string, true>,
78+
),
79+
})
4580
}
4681

4782
// noop
@@ -54,41 +89,58 @@ export class ParseJSONResultsPlugin implements KyselyPlugin {
5489
): Promise<QueryResult<UnknownRow>> {
5590
return {
5691
...args.result,
57-
rows: parseArray(args.result.rows, this.#objectStrategy),
92+
rows: parseArray(args.result.rows, this.#options),
5893
}
5994
}
6095
}
6196

62-
function parseArray<T>(arr: T[], objectStrategy: ObjectStrategy): T[] {
63-
const target = objectStrategy === 'create' ? new Array(arr.length) : arr
97+
function parseArray<T>(
98+
arr: T[],
99+
options: ProcessedParseJSONResultsPluginOptions,
100+
): T[] {
101+
const target =
102+
options.objectStrategy === 'create' ? new Array(arr.length) : arr
64103

65104
for (let i = 0; i < arr.length; ++i) {
66-
target[i] = parse(arr[i], objectStrategy) as T
105+
target[i] = parse(arr[i], options) as T
67106
}
68107

69108
return target
70109
}
71110

72-
function parse(obj: unknown, objectStrategy: ObjectStrategy): unknown {
73-
if (isString(obj)) {
74-
return parseString(obj)
111+
function parse(
112+
value: unknown,
113+
options: ProcessedParseJSONResultsPluginOptions,
114+
): unknown {
115+
if (isString(value)) {
116+
return parseString(value, options)
75117
}
76118

77-
if (Array.isArray(obj)) {
78-
return parseArray(obj, objectStrategy)
119+
if (Array.isArray(value)) {
120+
return parseArray(value, options)
79121
}
80122

81-
if (isPlainObject(obj)) {
82-
return parseObject(obj, objectStrategy)
123+
if (isPlainObject(value)) {
124+
return parseObject(value, options)
83125
}
84126

85-
return obj
127+
return value
86128
}
87129

88-
function parseString(str: string): unknown {
89-
if (maybeJson(str)) {
130+
function parseString(
131+
str: string,
132+
options: ProcessedParseJSONResultsPluginOptions,
133+
): unknown {
134+
if (options.isJSON(str)) {
90135
try {
91-
return parse(JSON.parse(str), 'in-place')
136+
return parse(
137+
JSON.parse(str, (...args) => {
138+
// prevent prototype pollution
139+
if (args[0] === '__proto__') return
140+
return options.reviver(...args)
141+
}),
142+
{ ...options, objectStrategy: 'in-place' },
143+
)
92144
} catch (err) {
93145
// this catch block is intentionally empty.
94146
}
@@ -98,17 +150,22 @@ function parseString(str: string): unknown {
98150
}
99151

100152
function maybeJson(value: string): boolean {
101-
return value.match(/^[\[\{]/) != null
153+
return (
154+
(value.startsWith('{') && value.endsWith('}')) ||
155+
(value.startsWith('[') && value.endsWith(']'))
156+
)
102157
}
103158

104159
function parseObject(
105160
obj: Record<string, unknown>,
106-
objectStrategy: ObjectStrategy,
161+
options: ProcessedParseJSONResultsPluginOptions,
107162
): Record<string, unknown> {
163+
const { objectStrategy, skipKeys } = options
164+
108165
const target = objectStrategy === 'create' ? {} : obj
109166

110167
for (const key in obj) {
111-
target[key] = parse(obj[key], objectStrategy)
168+
target[key] = skipKeys[key] ? obj[key] : parse(obj[key], options)
112169
}
113170

114171
return target

0 commit comments

Comments
 (0)
Please sign in to comment.