Skip to content

Commit 1f93ee5

Browse files
authored
Merge pull request #606 from supabase/avallete/fix-override-on-invalid-relations
fix(types): overrideTypes work on invalid embeded relation
2 parents 9b2c8fc + 29e8c96 commit 1f93ee5

File tree

4 files changed

+164
-21
lines changed

4 files changed

+164
-21
lines changed

src/PostgrestBuilder.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
IsValidResultOverride,
1111
} from './types'
1212
import PostgrestError from './PostgrestError'
13+
import { ContainsNull } from './select-query-parser/types'
1314

1415
export default abstract class PostgrestBuilder<Result, ThrowOnError extends boolean = false>
1516
implements
@@ -257,14 +258,20 @@ export default abstract class PostgrestBuilder<Result, ThrowOnError extends bool
257258
NewResult,
258259
Options extends { merge?: boolean } = { merge: true }
259260
>(): PostgrestBuilder<
260-
IsValidResultOverride<Result, NewResult, true, false, false> extends true
261-
? MergePartialResult<NewResult, Result, Options>
261+
IsValidResultOverride<Result, NewResult, false, false> extends true
262+
? // Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`)
263+
ContainsNull<Result> extends true
264+
? MergePartialResult<NewResult, NonNullable<Result>, Options> | null
265+
: MergePartialResult<NewResult, Result, Options>
262266
: CheckMatchingArrayTypes<Result, NewResult>,
263267
ThrowOnError
264268
> {
265269
return this as unknown as PostgrestBuilder<
266-
IsValidResultOverride<Result, NewResult, true, false, false> extends true
267-
? MergePartialResult<NewResult, Result, Options>
270+
IsValidResultOverride<Result, NewResult, false, false> extends true
271+
? // Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`)
272+
ContainsNull<Result> extends true
273+
? MergePartialResult<NewResult, NonNullable<Result>, Options> | null
274+
: MergePartialResult<NewResult, Result, Options>
268275
: CheckMatchingArrayTypes<Result, NewResult>,
269276
ThrowOnError
270277
>

src/types.ts

+36-15
Original file line numberDiff line numberDiff line change
@@ -92,20 +92,16 @@ type NonRecursiveType = BuiltIns | Function | (new (...arguments_: any[]) => unk
9292
type BuiltIns = Primitive | void | Date | RegExp
9393
type Primitive = null | undefined | string | number | boolean | symbol | bigint
9494

95-
export type IsValidResultOverride<Result, NewResult, Ok, ErrorResult, ErrorNewResult> =
95+
export type IsValidResultOverride<Result, NewResult, ErrorResult, ErrorNewResult> =
9696
Result extends any[]
9797
? NewResult extends any[]
9898
? // Both are arrays - valid
99-
Ok
99+
true
100100
: ErrorResult
101101
: NewResult extends any[]
102102
? ErrorNewResult
103103
: // 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-
104+
true
109105
/**
110106
* Utility type to check if array types match between Result and NewResult.
111107
* Returns either the valid NewResult type or an error message type.
@@ -117,33 +113,49 @@ export type CheckMatchingArrayTypes<Result, NewResult> =
117113
: IsValidResultOverride<
118114
Result,
119115
NewResult,
120-
NewResult,
121116
{
122117
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'
123118
},
124119
{
125120
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'
126121
}
127-
>
122+
> extends infer ValidationResult
123+
? ValidationResult extends true
124+
? // Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`)
125+
ContainsNull<Result> extends true
126+
? NewResult | null
127+
: NewResult
128+
: // contains the error
129+
ValidationResult
130+
: never
128131

129132
type Simplify<T> = T extends object ? { [K in keyof T]: T[K] } : T
130133

131-
type MergeDeep<New, Row> = {
132-
[K in keyof New | keyof Row]: K extends keyof New
134+
// Extract only explicit (non-index-signature) keys.
135+
type ExplicitKeys<T> = {
136+
[K in keyof T]: string extends K ? never : K
137+
}[keyof T]
138+
139+
type MergeExplicit<New, Row> = {
140+
// We merge all the explicit keys which allows merge and override of types like
141+
// { [key: string]: unknown } and { someSpecificKey: boolean }
142+
[K in ExplicitKeys<New> | ExplicitKeys<Row>]: K extends keyof New
133143
? K extends keyof Row
134-
? // Check if the override is on a embeded relation (array)
144+
? Row[K] extends SelectQueryError<string>
145+
? New[K]
146+
: // Check if the override is on a embedded relation (array)
135147
New[K] extends any[]
136148
? Row[K] extends any[]
137149
? Array<Simplify<MergeDeep<NonNullable<New[K][number]>, NonNullable<Row[K][number]>>>>
138150
: New[K]
139-
: // Check if both properties are objects omiting a potential null union
151+
: // Check if both properties are objects omitting a potential null union
140152
IsPlainObject<NonNullable<New[K]>> extends true
141153
? IsPlainObject<NonNullable<Row[K]>> extends true
142154
? // If they are, use the new override as source of truth for the optionality
143155
ContainsNull<New[K]> extends true
144-
? // If the override want to preserve optionality
156+
? // If the override wants to preserve optionality
145157
Simplify<MergeDeep<NonNullable<New[K]>, NonNullable<Row[K]>>> | null
146-
: // If the override want to enforce non-null result
158+
: // If the override wants to enforce non-null result
147159
Simplify<MergeDeep<New[K], NonNullable<Row[K]>>>
148160
: New[K] // Override with New type if Row isn't an object
149161
: New[K] // Override primitives with New type
@@ -153,6 +165,15 @@ type MergeDeep<New, Row> = {
153165
: never
154166
}
155167

168+
type MergeDeep<New, Row> = Simplify<
169+
MergeExplicit<New, Row> &
170+
// Intersection here is to restore dynamic keys into the merging result
171+
// eg:
172+
// {[key: number]: string}
173+
// or Record<string, number | null>
174+
(string extends keyof Row ? { [K: string]: Row[string] } : {})
175+
>
176+
156177
// Helper to check if a type is a plain object (not an array)
157178
type IsPlainObject<T> = T extends any[] ? false : T extends object ? true : false
158179

test/override-types.test-d.ts

+115-2
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
6060
let result: typeof singleResult.data
6161
expectType<TypeEqual<(typeof result)['custom_field'], string>>(true)
6262
}
63-
// Test with maybeSingle()
63+
// Test with maybeSingle() merging with new field
6464
{
6565
const maybeSingleResult = await postgrest
6666
.from('users')
@@ -71,7 +71,50 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
7171
throw new Error(maybeSingleResult.error.message)
7272
}
7373
let maybeSingleResultType: typeof maybeSingleResult.data
74-
let expectedType: { custom_field: string } | null
74+
let expectedType: {
75+
age_range: unknown
76+
catchphrase: unknown
77+
data: CustomUserDataType | null
78+
status: 'ONLINE' | 'OFFLINE' | null
79+
username: string
80+
custom_field: string
81+
} | null
82+
expectType<TypeEqual<typeof maybeSingleResultType, typeof expectedType>>(true)
83+
}
84+
// Test with maybeSingle() merging with override field
85+
{
86+
const maybeSingleResult = await postgrest
87+
.from('users')
88+
.select()
89+
.maybeSingle()
90+
.overrideTypes<{ catchphrase: string }>()
91+
if (maybeSingleResult.error) {
92+
throw new Error(maybeSingleResult.error.message)
93+
}
94+
let maybeSingleResultType: typeof maybeSingleResult.data
95+
let expectedType: {
96+
age_range: unknown
97+
catchphrase: string
98+
data: CustomUserDataType | null
99+
status: 'ONLINE' | 'OFFLINE' | null
100+
username: string
101+
} | null
102+
expectType<TypeEqual<typeof maybeSingleResultType, typeof expectedType>>(true)
103+
}
104+
// Test with maybeSingle() replace with override field
105+
{
106+
const maybeSingleResult = await postgrest
107+
.from('users')
108+
.select()
109+
.maybeSingle()
110+
.overrideTypes<{ catchphrase: string }, { merge: false }>()
111+
if (maybeSingleResult.error) {
112+
throw new Error(maybeSingleResult.error.message)
113+
}
114+
let maybeSingleResultType: typeof maybeSingleResult.data
115+
let expectedType: {
116+
catchphrase: string
117+
} | null
75118
expectType<TypeEqual<typeof maybeSingleResultType, typeof expectedType>>(true)
76119
}
77120
// Test replacing behavior
@@ -203,6 +246,8 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
203246
foo: number
204247
bar: { baz: number }
205248
en: 'ONE' | 'TWO' | 'THREE'
249+
record: Record<string, Json | undefined> | null
250+
recordNumber: Record<number, Json | undefined> | null
206251
qux: boolean
207252
}
208253
age_range: unknown
@@ -232,6 +277,8 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
232277
foo: number
233278
bar: { baz: number }
234279
en: 'ONE' | 'TWO' | 'THREE'
280+
record: Record<string, Json | undefined> | null
281+
recordNumber: Record<number, Json | undefined> | null
235282
qux: boolean
236283
} | null
237284
age_range: unknown
@@ -299,6 +346,46 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
299346
foo: string
300347
bar: { baz: number; newBaz: string }
301348
en: 'FOUR' // Overridden enum value
349+
record: Record<string, Json | undefined> | null
350+
recordNumber: Record<number, Json | undefined> | null
351+
}
352+
age_range: unknown
353+
catchphrase: unknown
354+
status: 'ONLINE' | 'OFFLINE' | null
355+
}[]
356+
>
357+
>(true)
358+
}
359+
360+
// Test merging with Json defined as Record
361+
{
362+
const result = await postgrest
363+
.from('users')
364+
.select()
365+
.overrideTypes<{ data: { record: { baz: 'foo' }; recordNumber: { bar: 'foo' } } }[]>()
366+
if (result.error) {
367+
throw new Error(result.error.message)
368+
}
369+
let data: typeof result.data
370+
expectType<
371+
TypeEqual<
372+
typeof data,
373+
{
374+
username: string
375+
data: {
376+
foo: string
377+
bar: {
378+
baz: number
379+
}
380+
en: 'ONE' | 'TWO' | 'THREE'
381+
record: {
382+
[x: string]: Json | undefined
383+
baz: 'foo'
384+
}
385+
recordNumber: {
386+
[x: number]: Json | undefined
387+
bar: 'foo'
388+
}
302389
}
303390
age_range: unknown
304391
catchphrase: unknown
@@ -435,3 +522,29 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
435522
>
436523
>(true)
437524
}
525+
526+
// Test overrideTypes single object with error embeded relation
527+
{
528+
const result = await postgrest.from('users').select('*, somerelation(*)').overrideTypes<
529+
{
530+
somerelation: { created_at: Date; data: string }
531+
}[]
532+
>()
533+
if (result.error) {
534+
throw new Error(result.error.message)
535+
}
536+
let data: typeof result.data
537+
expectType<
538+
TypeEqual<
539+
typeof data,
540+
{
541+
username: string
542+
data: CustomUserDataType | null
543+
age_range: unknown
544+
catchphrase: unknown
545+
status: 'ONLINE' | 'OFFLINE' | null
546+
somerelation: { created_at: Date; data: string }
547+
}[]
548+
>
549+
>(true)
550+
}

test/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export type CustomUserDataType = {
66
baz: number
77
}
88
en: 'ONE' | 'TWO' | 'THREE'
9+
record: Record<string, Json | undefined> | null
10+
recordNumber: Record<number, Json | undefined> | null
911
}
1012

1113
export type Database = {

0 commit comments

Comments
 (0)