Skip to content

Commit 56376c0

Browse files
authored
Merge pull request #426 from supabase/feat/relationship-cardinality
feat: determine relationship cardinality from types
2 parents 878034b + fc59958 commit 56376c0

File tree

6 files changed

+201
-80
lines changed

6 files changed

+201
-80
lines changed

src/PostgrestFilterBuilder.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ type FilterOperator =
2828
export default class PostgrestFilterBuilder<
2929
Schema extends GenericSchema,
3030
Row extends Record<string, unknown>,
31-
Result
32-
> extends PostgrestTransformBuilder<Schema, Row, Result> {
31+
Result,
32+
Relationships = unknown
33+
> extends PostgrestTransformBuilder<Schema, Row, Result, Relationships> {
3334
eq<ColumnName extends string & keyof Row>(column: ColumnName, value: Row[ColumnName]): this
3435
eq(column: string, value: unknown): this
3536
/**

src/PostgrestQueryBuilder.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { Fetch, GenericSchema, GenericTable, GenericView } from './types'
55

66
export default class PostgrestQueryBuilder<
77
Schema extends GenericSchema,
8-
Relation extends GenericTable | GenericView
8+
Relation extends GenericTable | GenericView,
9+
Relationships = Relation extends { Relationships: infer R } ? R : unknown
910
> {
1011
url: URL
1112
headers: Record<string, string>
@@ -52,7 +53,10 @@ export default class PostgrestQueryBuilder<
5253
* `"estimated"`: Uses exact count for low numbers and planned count for high
5354
* numbers.
5455
*/
55-
select<Query extends string = '*', ResultOne = GetResult<Schema, Relation['Row'], Query>>(
56+
select<
57+
Query extends string = '*',
58+
ResultOne = GetResult<Schema, Relation['Row'], Relationships, Query>
59+
>(
5660
columns?: Query,
5761
{
5862
head = false,
@@ -61,7 +65,7 @@ export default class PostgrestQueryBuilder<
6165
head?: boolean
6266
count?: 'exact' | 'planned' | 'estimated'
6367
} = {}
64-
): PostgrestFilterBuilder<Schema, Relation['Row'], ResultOne[]> {
68+
): PostgrestFilterBuilder<Schema, Relation['Row'], ResultOne[], Relationships> {
6569
const method = head ? 'HEAD' : 'GET'
6670
// Remove whitespaces except when quoted
6771
let quoted = false
@@ -126,7 +130,7 @@ export default class PostgrestQueryBuilder<
126130
count?: 'exact' | 'planned' | 'estimated'
127131
defaultToNull?: boolean
128132
} = {}
129-
): PostgrestFilterBuilder<Schema, Relation['Row'], null> {
133+
): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships> {
130134
const method = 'POST'
131135

132136
const prefersHeaders = []
@@ -211,7 +215,7 @@ export default class PostgrestQueryBuilder<
211215
count?: 'exact' | 'planned' | 'estimated'
212216
defaultToNull?: boolean
213217
} = {}
214-
): PostgrestFilterBuilder<Schema, Relation['Row'], null> {
218+
): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships> {
215219
const method = 'POST'
216220

217221
const prefersHeaders = [`resolution=${ignoreDuplicates ? 'ignore' : 'merge'}-duplicates`]
@@ -275,7 +279,7 @@ export default class PostgrestQueryBuilder<
275279
}: {
276280
count?: 'exact' | 'planned' | 'estimated'
277281
} = {}
278-
): PostgrestFilterBuilder<Schema, Relation['Row'], null> {
282+
): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships> {
279283
const method = 'PATCH'
280284
const prefersHeaders = []
281285
if (this.headers['Prefer']) {
@@ -320,7 +324,7 @@ export default class PostgrestQueryBuilder<
320324
count,
321325
}: {
322326
count?: 'exact' | 'planned' | 'estimated'
323-
} = {}): PostgrestFilterBuilder<Schema, Relation['Row'], null> {
327+
} = {}): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships> {
324328
const method = 'DELETE'
325329
const prefersHeaders = []
326330
if (count) {

src/PostgrestTransformBuilder.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { GenericSchema } from './types'
55
export default class PostgrestTransformBuilder<
66
Schema extends GenericSchema,
77
Row extends Record<string, unknown>,
8-
Result
8+
Result,
9+
Relationships = unknown
910
> extends PostgrestBuilder<Result> {
1011
/**
1112
* Perform a SELECT on the query result.
@@ -16,9 +17,9 @@ export default class PostgrestTransformBuilder<
1617
*
1718
* @param columns - The columns to retrieve, separated by commas
1819
*/
19-
select<Query extends string = '*', NewResultOne = GetResult<Schema, Row, Query>>(
20+
select<Query extends string = '*', NewResultOne = GetResult<Schema, Row, Relationships, Query>>(
2021
columns?: Query
21-
): PostgrestTransformBuilder<Schema, Row, NewResultOne[]> {
22+
): PostgrestTransformBuilder<Schema, Row, NewResultOne[], Relationships> {
2223
// Remove whitespaces except when quoted
2324
let quoted = false
2425
const cleanedColumns = (columns ?? '*')
@@ -38,7 +39,7 @@ export default class PostgrestTransformBuilder<
3839
this.headers['Prefer'] += ','
3940
}
4041
this.headers['Prefer'] += 'return=representation'
41-
return this as unknown as PostgrestTransformBuilder<Schema, Row, NewResultOne[]>
42+
return this as unknown as PostgrestTransformBuilder<Schema, Row, NewResultOne[], Relationships>
4243
}
4344

4445
order<ColumnName extends string & keyof Row>(
@@ -249,7 +250,7 @@ export default class PostgrestTransformBuilder<
249250
*
250251
* @typeParam NewResult - The new result type to override with
251252
*/
252-
returns<NewResult>(): PostgrestTransformBuilder<Schema, Row, NewResult> {
253-
return this as unknown as PostgrestTransformBuilder<Schema, Row, NewResult>
253+
returns<NewResult>(): PostgrestTransformBuilder<Schema, Row, NewResult, Relationships> {
254+
return this as unknown as PostgrestTransformBuilder<Schema, Row, NewResult, Relationships>
254255
}
255256
}

src/select-query-parser.ts

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,26 @@ type EatWhitespace<Input extends string> = string extends Input
6565
? EatWhitespace<Remainder>
6666
: Input
6767

68+
type HasFKey<FKeyName, Relationships> = Relationships extends [infer R]
69+
? R extends { foreignKeyName: FKeyName }
70+
? true
71+
: false
72+
: Relationships extends [infer R, ...infer Rest]
73+
? HasFKey<FKeyName, [R]> extends true
74+
? true
75+
: HasFKey<FKeyName, Rest>
76+
: false
77+
78+
type HasFKeyToFRel<FRelName, Relationships> = Relationships extends [infer R]
79+
? R extends { referencedRelation: FRelName }
80+
? true
81+
: false
82+
: Relationships extends [infer R, ...infer Rest]
83+
? HasFKeyToFRel<FRelName, [R]> extends true
84+
? true
85+
: HasFKeyToFRel<FRelName, Rest>
86+
: false
87+
6888
/**
6989
* Constructs a type definition for a single field of an object.
7090
*
@@ -75,20 +95,44 @@ type EatWhitespace<Input extends string> = string extends Input
7595
type ConstructFieldDefinition<
7696
Schema extends GenericSchema,
7797
Row extends Record<string, unknown>,
98+
Relationships,
7899
Field
79-
> = Field extends {
80-
star: true
81-
}
100+
> = Field extends { star: true }
82101
? Row
102+
: Field extends { name: string; original: string; hint: string; children: unknown[] }
103+
? {
104+
[_ in Field['name']]: GetResultHelper<
105+
Schema,
106+
(Schema['Tables'] & Schema['Views'])[Field['original']]['Row'],
107+
(Schema['Tables'] & Schema['Views'])[Field['original']] extends { Relationships: infer R }
108+
? R
109+
: unknown,
110+
Field['children'],
111+
unknown
112+
> extends infer Child
113+
? Relationships extends unknown[]
114+
? HasFKey<Field['hint'], Relationships> extends true
115+
? Child | null
116+
: Child[]
117+
: Child[]
118+
: never
119+
}
83120
: Field extends { name: string; original: string; children: unknown[] }
84121
? {
85122
[_ in Field['name']]: GetResultHelper<
86123
Schema,
87124
(Schema['Tables'] & Schema['Views'])[Field['original']]['Row'],
125+
(Schema['Tables'] & Schema['Views'])[Field['original']] extends { Relationships: infer R }
126+
? R
127+
: unknown,
88128
Field['children'],
89129
unknown
90130
> extends infer Child
91-
? Child | Child[] | null
131+
? Relationships extends unknown[]
132+
? HasFKeyToFRel<Field['original'], Relationships> extends true
133+
? Child | null
134+
: Child[]
135+
: Child[]
92136
: never
93137
}
94138
: Field extends { name: string; original: string }
@@ -191,14 +235,14 @@ type ParseNode<Input extends string> = Input extends ''
191235
? ParseEmbeddedResource<EatWhitespace<Remainder>>
192236
: ParserError<'Expected embedded resource after `!inner`'>
193237
: EatWhitespace<Remainder> extends `!${infer Remainder}`
194-
? ParseIdentifier<EatWhitespace<Remainder>> extends [infer _Hint, `${infer Remainder}`]
238+
? ParseIdentifier<EatWhitespace<Remainder>> extends [infer Hint, `${infer Remainder}`]
195239
? EatWhitespace<Remainder> extends `!inner${infer Remainder}`
196240
? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
197241
infer Fields,
198242
`${infer Remainder}`
199243
]
200244
? // `field!hint!inner(nodes)`
201-
[{ name: Name; original: Name; children: Fields }, EatWhitespace<Remainder>]
245+
[{ name: Name; original: Name; hint: Hint; children: Fields }, EatWhitespace<Remainder>]
202246
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
203247
? ParseEmbeddedResource<EatWhitespace<Remainder>>
204248
: ParserError<'Expected embedded resource after `!inner`'>
@@ -207,7 +251,7 @@ type ParseNode<Input extends string> = Input extends ''
207251
`${infer Remainder}`
208252
]
209253
? // `field!hint(nodes)`
210-
[{ name: Name; original: Name; children: Fields }, EatWhitespace<Remainder>]
254+
[{ name: Name; original: Name; hint: Hint; children: Fields }, EatWhitespace<Remainder>]
211255
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
212256
? ParseEmbeddedResource<EatWhitespace<Remainder>>
213257
: ParserError<'Expected embedded resource after `!hint`'>
@@ -225,14 +269,17 @@ type ParseNode<Input extends string> = Input extends ''
225269
? ParseEmbeddedResource<EatWhitespace<Remainder>>
226270
: ParserError<'Expected embedded resource after `!inner`'>
227271
: EatWhitespace<Remainder> extends `!${infer Remainder}`
228-
? ParseIdentifier<EatWhitespace<Remainder>> extends [infer _Hint, `${infer Remainder}`]
272+
? ParseIdentifier<EatWhitespace<Remainder>> extends [infer Hint, `${infer Remainder}`]
229273
? EatWhitespace<Remainder> extends `!inner${infer Remainder}`
230274
? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
231275
infer Fields,
232276
`${infer Remainder}`
233277
]
234278
? // `renamed_field:field!hint!inner(nodes)`
235-
[{ name: Name; original: OriginalName; children: Fields }, EatWhitespace<Remainder>]
279+
[
280+
{ name: Name; original: OriginalName; hint: Hint; children: Fields },
281+
EatWhitespace<Remainder>
282+
]
236283
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
237284
? ParseEmbeddedResource<EatWhitespace<Remainder>>
238285
: ParserError<'Expected embedded resource after `!inner`'>
@@ -245,6 +292,7 @@ type ParseNode<Input extends string> = Input extends ''
245292
{
246293
name: Name
247294
original: OriginalName
295+
hint: Hint
248296
children: Fields
249297
},
250298
EatWhitespace<Remainder>
@@ -363,12 +411,25 @@ type ParseQuery<Query extends string> = string extends Query
363411
type GetResultHelper<
364412
Schema extends GenericSchema,
365413
Row extends Record<string, unknown>,
414+
Relationships,
366415
Fields extends unknown[],
367416
Acc
368417
> = Fields extends [infer R]
369-
? GetResultHelper<Schema, Row, [], ConstructFieldDefinition<Schema, Row, R> & Acc>
418+
? GetResultHelper<
419+
Schema,
420+
Row,
421+
Relationships,
422+
[],
423+
ConstructFieldDefinition<Schema, Row, Relationships, R> & Acc
424+
>
370425
: Fields extends [infer R, ...infer Rest]
371-
? GetResultHelper<Schema, Row, Rest, ConstructFieldDefinition<Schema, Row, R> & Acc>
426+
? GetResultHelper<
427+
Schema,
428+
Row,
429+
Relationships,
430+
Rest,
431+
ConstructFieldDefinition<Schema, Row, Relationships, R> & Acc
432+
>
372433
: Prettify<Acc>
373434

374435
/**
@@ -380,7 +441,8 @@ type GetResultHelper<
380441
export type GetResult<
381442
Schema extends GenericSchema,
382443
Row extends Record<string, unknown>,
444+
Relationships,
383445
Query extends string
384446
> = ParseQuery<Query> extends unknown[]
385-
? GetResultHelper<Schema, Row, ParseQuery<Query>, unknown>
447+
? GetResultHelper<Schema, Row, Relationships, ParseQuery<Query>, unknown>
386448
: ParseQuery<Query>

test/index.test-d.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,21 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
6363
}
6464
expectType<'ONLINE' | 'OFFLINE'>(data)
6565
}
66+
67+
// many-to-one relationship
68+
{
69+
const { data: message, error } = await postgrest.from('messages').select('user:users(*)').single()
70+
if (error) {
71+
throw new Error(error.message)
72+
}
73+
expectType<Database['public']['Tables']['users']['Row'] | null>(message.user)
74+
}
75+
76+
// one-to-many relationship
77+
{
78+
const { data: user, error } = await postgrest.from('users').select('messages(*)').single()
79+
if (error) {
80+
throw new Error(error.message)
81+
}
82+
expectType<Database['public']['Tables']['messages']['Row'][]>(user.messages)
83+
}

0 commit comments

Comments
 (0)