Skip to content

Commit 4babce7

Browse files
committed
fix: deny passing Buffer columns to SQLite JSON helpers
- Added HasUint8Array<T> and ObjectHasUint8ArrayProperty<O> type utilities - SQLite JSON helpers now return KyselyTypeError when binary columns are detected - Error message includes workarounds: eb.cast<string>(column, "text") or sql<string>`hex(column)`
1 parent 102d477 commit 4babce7

4 files changed

Lines changed: 336 additions & 28 deletions

File tree

src/helpers/sqlite.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,24 @@ import type { SelectQueryNode } from '../operation-node/select-query-node.js'
33
import type { SelectQueryBuilderExpression } from '../query-builder/select-query-builder-expression.js'
44
import type { RawBuilder } from '../raw-builder/raw-builder.js'
55
import { sql } from '../raw-builder/sql.js'
6+
import type { KyselyTypeError } from '../util/type-error.js'
67
import { getJsonObjectArgs } from '../util/json-object-args.js'
78
import type {
9+
HasUint8Array,
10+
ObjectHasUint8ArrayProperty,
811
ShallowDehydrateObject,
912
ShallowDehydrateValue,
1013
Simplify,
1114
} from '../util/type-utils.js'
1215

16+
type ExpressionRecordHasUint8Array<
17+
O extends Record<string, Expression<unknown>>,
18+
> = true extends {
19+
[K in keyof O]: O[K] extends Expression<infer V> ? HasUint8Array<V> : false
20+
}[keyof O]
21+
? true
22+
: false
23+
1324
/**
1425
* A SQLite helper for aggregating a subquery into a JSON array.
1526
*
@@ -73,10 +84,12 @@ import type {
7384
*/
7485
export function jsonArrayFrom<O>(
7586
expr: SelectQueryBuilderExpression<O>,
76-
): RawBuilder<Simplify<ShallowDehydrateObject<O>>[]> {
87+
): ObjectHasUint8ArrayProperty<O> extends true
88+
? KyselyTypeError<'SQLite jsonArrayFrom does not support Buffer/Uint8Array columns. Cast to text using eb.cast<string>(column, "text") or sql<string>`hex(column)`'>
89+
: RawBuilder<Simplify<ShallowDehydrateObject<O>>[]> {
7790
return sql`(select coalesce(json_group_array(json_object(${sql.join(
7891
getSqliteJsonObjectArgs(expr.toOperationNode(), 'agg'),
79-
)})), '[]') from ${expr} as agg)`
92+
)})), '[]') from ${expr} as agg)` as any
8093
}
8194

8295
/**
@@ -144,10 +157,12 @@ export function jsonArrayFrom<O>(
144157
*/
145158
export function jsonObjectFrom<O>(
146159
expr: SelectQueryBuilderExpression<O>,
147-
): RawBuilder<Simplify<ShallowDehydrateObject<O>> | null> {
160+
): ObjectHasUint8ArrayProperty<O> extends true
161+
? KyselyTypeError<'SQLite jsonObjectFrom does not support Buffer/Uint8Array columns. Cast to text using eb.cast<string>(column, "text") or sql<string>`hex(column)`'>
162+
: RawBuilder<Simplify<ShallowDehydrateObject<O>> | null> {
148163
return sql`(select json_object(${sql.join(
149164
getSqliteJsonObjectArgs(expr.toOperationNode(), 'obj'),
150-
)}) from ${expr} as obj)`
165+
)}) from ${expr} as obj)` as any
151166
}
152167

153168
/**
@@ -208,16 +223,18 @@ export function jsonObjectFrom<O>(
208223
*/
209224
export function jsonBuildObject<O extends Record<string, Expression<unknown>>>(
210225
obj: O,
211-
): RawBuilder<
212-
Simplify<{
213-
[K in keyof O]: O[K] extends Expression<infer V>
214-
? ShallowDehydrateValue<V>
215-
: never
216-
}>
217-
> {
226+
): ExpressionRecordHasUint8Array<O> extends true
227+
? KyselyTypeError<'SQLite jsonBuildObject does not support Buffer/Uint8Array values. Cast to text using eb.cast<string>(column, "text") or sql<string>`hex(column)`'>
228+
: RawBuilder<
229+
Simplify<{
230+
[K in keyof O]: O[K] extends Expression<infer V>
231+
? ShallowDehydrateValue<V>
232+
: never
233+
}>
234+
> {
218235
return sql`json_object(${sql.join(
219236
Object.keys(obj).flatMap((k) => [sql.lit(k), obj[k]]),
220-
)})`
237+
)})` as any
221238
}
222239

223240
function getSqliteJsonObjectArgs(

src/util/type-utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,3 +260,21 @@ export type StringsWhenDataTypeNotAvailable =
260260
export type NumbersWhenDataTypeNotAvailable = bigint | NumericString
261261

262262
export type NumericString = `${number}`
263+
264+
/**
265+
* Evaluates to `true` if type `T` contains `Uint8Array`.
266+
*/
267+
export type HasUint8Array<T> = Uint8Array extends T
268+
? true
269+
: T extends Uint8Array
270+
? true
271+
: false
272+
273+
/**
274+
* Evaluates to `true` if any property in object type `O` contains `Uint8Array`.
275+
*/
276+
export type ObjectHasUint8ArrayProperty<O> = true extends {
277+
[K in keyof O]: HasUint8Array<O[K]>
278+
}[keyof O]
279+
? true
280+
: false

test/node/src/json.test.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -414,14 +414,17 @@ for (const dialect of DIALECTS) {
414414
).as('doggo'),
415415

416416
// Nest an object that holds the person's formatted name
417-
jsonBuildObject({
418-
first: eb.ref('first_name'),
419-
last: eb.ref('last_name'),
420-
full:
421-
dialect === 'sqlite'
422-
? sql<string>`first_name || ' ' || last_name`
423-
: eb.fn('concat', ['first_name', sql.lit(' '), 'last_name']),
424-
}).as('name'),
417+
// Type assertion needed due to union of dialect helper types
418+
(
419+
jsonBuildObject({
420+
first: eb.ref('first_name'),
421+
last: eb.ref('last_name'),
422+
full:
423+
dialect === 'sqlite'
424+
? sql<string>`first_name || ' ' || last_name`
425+
: eb.fn('concat', ['first_name', sql.lit(' '), 'last_name']),
426+
}) as RawBuilder<{ first: string; last: string | null; full: string }>
427+
).as('name'),
425428

426429
// Nest an empty list
427430
jsonArrayFrom(
@@ -564,14 +567,17 @@ for (const dialect of DIALECTS) {
564567
const result = await db
565568
.selectNoFrom([
566569
buffer,
567-
jsonObjectFrom(
568-
db.selectNoFrom([
569-
dialect === 'sqlite'
570-
? expressionBuilder()
571-
.cast<string>(buffer.expression, 'text')
572-
.as('buffer')
573-
: buffer,
574-
]),
570+
// Type assertion needed due to union of dialect helper types
571+
(
572+
jsonObjectFrom(
573+
db.selectNoFrom([
574+
dialect === 'sqlite'
575+
? expressionBuilder()
576+
.cast<string>(buffer.expression, 'text')
577+
.as('buffer')
578+
: buffer,
579+
]),
580+
) as RawBuilder<{ buffer: string } | null>
575581
)
576582
.$notNull()
577583
.as('dehydrated'),

0 commit comments

Comments
 (0)