Skip to content

Commit 4d0a738

Browse files
authored
Merge pull request #604 from supabase/avallete/fix-add-returns-to-builder
fix(types): returns type casting
2 parents a0b56aa + bfeb5cc commit 4d0a738

7 files changed

+645
-4
lines changed

jest.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
module.exports = {
22
preset: 'ts-jest',
33
testEnvironment: 'node',
4+
collectCoverageFrom: ['src/**/*'],
45
}

src/PostgrestBuilder.ts

+61-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
// @ts-ignore
22
import nodeFetch from '@supabase/node-fetch'
33

4-
import type { Fetch, PostgrestSingleResponse, PostgrestResponseSuccess } from './types'
4+
import type {
5+
Fetch,
6+
PostgrestSingleResponse,
7+
PostgrestResponseSuccess,
8+
CheckMatchingArrayTypes,
9+
MergePartialResult,
10+
IsValidResultOverride,
11+
} from './types'
512
import PostgrestError from './PostgrestError'
613

714
export default abstract class PostgrestBuilder<Result, ThrowOnError extends boolean = false>
@@ -209,4 +216,57 @@ export default abstract class PostgrestBuilder<Result, ThrowOnError extends bool
209216

210217
return res.then(onfulfilled, onrejected)
211218
}
219+
220+
/**
221+
* Override the type of the returned `data`.
222+
*
223+
* @typeParam NewResult - The new result type to override with
224+
* @deprecated Use overrideTypes<yourType, { merge: false }>() method at the end of your call chain instead
225+
*/
226+
returns<NewResult>(): PostgrestBuilder<CheckMatchingArrayTypes<Result, NewResult>, ThrowOnError> {
227+
/* istanbul ignore next */
228+
return this as unknown as PostgrestBuilder<
229+
CheckMatchingArrayTypes<Result, NewResult>,
230+
ThrowOnError
231+
>
232+
}
233+
234+
/**
235+
* Override the type of the returned `data` field in the response.
236+
*
237+
* @typeParam NewResult - The new type to cast the response data to
238+
* @typeParam Options - Optional type configuration (defaults to { merge: true })
239+
* @typeParam Options.merge - When true, merges the new type with existing return type. When false, replaces the existing types entirely (defaults to true)
240+
* @example
241+
* ```typescript
242+
* // Merge with existing types (default behavior)
243+
* const query = supabase
244+
* .from('users')
245+
* .select()
246+
* .overrideTypes<{ custom_field: string }>()
247+
*
248+
* // Replace existing types completely
249+
* const replaceQuery = supabase
250+
* .from('users')
251+
* .select()
252+
* .overrideTypes<{ id: number; name: string }, { merge: false }>()
253+
* ```
254+
* @returns A PostgrestBuilder instance with the new type
255+
*/
256+
overrideTypes<
257+
NewResult,
258+
Options extends { merge?: boolean } = { merge: true }
259+
>(): PostgrestBuilder<
260+
IsValidResultOverride<Result, NewResult, true, false, false> extends true
261+
? MergePartialResult<NewResult, Result, Options>
262+
: CheckMatchingArrayTypes<Result, NewResult>,
263+
ThrowOnError
264+
> {
265+
return this as unknown as PostgrestBuilder<
266+
IsValidResultOverride<Result, NewResult, true, false, false> extends true
267+
? MergePartialResult<NewResult, Result, Options>
268+
: CheckMatchingArrayTypes<Result, NewResult>,
269+
ThrowOnError
270+
>
271+
}
212272
}

src/PostgrestTransformBuilder.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import PostgrestBuilder from './PostgrestBuilder'
22
import { GetResult } from './select-query-parser/result'
3-
import { GenericSchema } from './types'
3+
import { GenericSchema, CheckMatchingArrayTypes } from './types'
44

55
export default class PostgrestTransformBuilder<
66
Schema extends GenericSchema,
@@ -307,18 +307,19 @@ export default class PostgrestTransformBuilder<
307307
* Override the type of the returned `data`.
308308
*
309309
* @typeParam NewResult - The new result type to override with
310+
* @deprecated Use overrideTypes<yourType, { merge: false }>() method at the end of your call chain instead
310311
*/
311312
returns<NewResult>(): PostgrestTransformBuilder<
312313
Schema,
313314
Row,
314-
NewResult,
315+
CheckMatchingArrayTypes<Result, NewResult>,
315316
RelationName,
316317
Relationships
317318
> {
318319
return this as unknown as PostgrestTransformBuilder<
319320
Schema,
320321
Row,
321-
NewResult,
322+
CheckMatchingArrayTypes<Result, NewResult>,
322323
RelationName,
323324
Relationships
324325
>

src/types.ts

+65
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import PostgrestError from './PostgrestError'
2+
import { ContainsNull } from './select-query-parser/types'
3+
import { SelectQueryError } from './select-query-parser/utils'
24

35
export type Fetch = typeof fetch
46

@@ -89,3 +91,66 @@ type ConditionalSimplifyDeep<
8991
type NonRecursiveType = BuiltIns | Function | (new (...arguments_: any[]) => unknown)
9092
type BuiltIns = Primitive | void | Date | RegExp
9193
type Primitive = null | undefined | string | number | boolean | symbol | bigint
94+
95+
export type IsValidResultOverride<Result, NewResult, Ok, ErrorResult, ErrorNewResult> =
96+
Result extends any[]
97+
? NewResult extends any[]
98+
? // Both are arrays - valid
99+
Ok
100+
: ErrorResult
101+
: NewResult extends any[]
102+
? ErrorNewResult
103+
: // Neither are arrays - valid
104+
// Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`)
105+
ContainsNull<Result> extends true
106+
? Ok | null
107+
: Ok
108+
109+
/**
110+
* Utility type to check if array types match between Result and NewResult.
111+
* Returns either the valid NewResult type or an error message type.
112+
*/
113+
export type CheckMatchingArrayTypes<Result, NewResult> =
114+
// If the result is a QueryError we allow the user to override anyway
115+
Result extends SelectQueryError<string>
116+
? NewResult
117+
: IsValidResultOverride<
118+
Result,
119+
NewResult,
120+
NewResult,
121+
{
122+
Error: 'Type mismatch: Cannot cast array result to a single object. Use .returns<Array<YourType>> for array results or .single() to convert the result to a single object'
123+
},
124+
{
125+
Error: 'Type mismatch: Cannot cast single object to array type. Remove Array wrapper from return type or make sure you are not using .single() up in the calling chain'
126+
}
127+
>
128+
129+
type Simplify<T> = T extends object ? { [K in keyof T]: T[K] } : T
130+
131+
type MergeDeep<New, Row> = {
132+
[K in keyof New | keyof Row]: K extends keyof Row
133+
? K extends keyof New
134+
? IsPlainObject<New[K]> extends true
135+
? IsPlainObject<Row[K]> extends true
136+
? MergeDeep<New[K], Row[K]>
137+
: Row[K]
138+
: Row[K]
139+
: Row[K]
140+
: K extends keyof New
141+
? New[K]
142+
: never
143+
}
144+
145+
// Helper to check if a type is a plain object (not an array)
146+
type IsPlainObject<T> = T extends any[] ? false : T extends object ? true : false
147+
148+
// Merge the new result with the original (Result) when merge option is true.
149+
// If NewResult is an array, merge each element.
150+
export type MergePartialResult<NewResult, Result, Options> = Options extends { merge: true }
151+
? Result extends any[]
152+
? NewResult extends any[]
153+
? Array<Simplify<MergeDeep<NewResult[number], Result[number]>>>
154+
: never
155+
: Simplify<MergeDeep<NewResult, Result>>
156+
: NewResult

0 commit comments

Comments
 (0)