1
1
import { QueryResult } from '../../driver/database-connection.js'
2
2
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'
4
4
import { UnknownRow } from '../../util/type-utils.js'
5
5
import {
6
6
KyselyPlugin ,
@@ -9,6 +9,13 @@ import {
9
9
} from '../kysely-plugin.js'
10
10
11
11
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
+
12
19
/**
13
20
* When `'in-place'`, arrays' and objects' values are parsed in-place. This is
14
21
* the most time and space efficient option.
@@ -20,10 +27,27 @@ export interface ParseJSONResultsPluginOptions {
20
27
* Defaults to `'in-place'`.
21
28
*/
22
29
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 [ ]
23
41
}
24
42
25
43
type ObjectStrategy = 'in-place' | 'create'
26
44
45
+ type ProcessedParseJSONResultsPluginOptions = {
46
+ readonly [ K in keyof ParseJSONResultsPluginOptions ] -?: K extends 'skipKeys'
47
+ ? Record < string , true >
48
+ : ParseJSONResultsPluginOptions [ K ]
49
+ }
50
+
27
51
/**
28
52
* Parses JSON strings in query results into JSON objects.
29
53
*
@@ -38,10 +62,21 @@ type ObjectStrategy = 'in-place' | 'create'
38
62
* ```
39
63
*/
40
64
export class ParseJSONResultsPlugin implements KyselyPlugin {
41
- readonly #objectStrategy: ObjectStrategy
65
+ readonly #options: ProcessedParseJSONResultsPluginOptions
42
66
43
67
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
+ } )
45
80
}
46
81
47
82
// noop
@@ -54,41 +89,58 @@ export class ParseJSONResultsPlugin implements KyselyPlugin {
54
89
) : Promise < QueryResult < UnknownRow > > {
55
90
return {
56
91
...args . result ,
57
- rows : parseArray ( args . result . rows , this . #objectStrategy ) ,
92
+ rows : parseArray ( args . result . rows , this . #options ) ,
58
93
}
59
94
}
60
95
}
61
96
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
64
103
65
104
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
67
106
}
68
107
69
108
return target
70
109
}
71
110
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 )
75
117
}
76
118
77
- if ( Array . isArray ( obj ) ) {
78
- return parseArray ( obj , objectStrategy )
119
+ if ( Array . isArray ( value ) ) {
120
+ return parseArray ( value , options )
79
121
}
80
122
81
- if ( isPlainObject ( obj ) ) {
82
- return parseObject ( obj , objectStrategy )
123
+ if ( isPlainObject ( value ) ) {
124
+ return parseObject ( value , options )
83
125
}
84
126
85
- return obj
127
+ return value
86
128
}
87
129
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 ) ) {
90
135
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
+ )
92
144
} catch ( err ) {
93
145
// this catch block is intentionally empty.
94
146
}
@@ -98,17 +150,22 @@ function parseString(str: string): unknown {
98
150
}
99
151
100
152
function maybeJson ( value : string ) : boolean {
101
- return value . match ( / ^ [ \[ \{ ] / ) != null
153
+ return (
154
+ ( value . startsWith ( '{' ) && value . endsWith ( '}' ) ) ||
155
+ ( value . startsWith ( '[' ) && value . endsWith ( ']' ) )
156
+ )
102
157
}
103
158
104
159
function parseObject (
105
160
obj : Record < string , unknown > ,
106
- objectStrategy : ObjectStrategy ,
161
+ options : ProcessedParseJSONResultsPluginOptions ,
107
162
) : Record < string , unknown > {
163
+ const { objectStrategy, skipKeys } = options
164
+
108
165
const target = objectStrategy === 'create' ? { } : obj
109
166
110
167
for ( const key in obj ) {
111
- target [ key ] = parse ( obj [ key ] , objectStrategy )
168
+ target [ key ] = skipKeys [ key ] ? obj [ key ] : parse ( obj [ key ] , options )
112
169
}
113
170
114
171
return target
0 commit comments