Skip to content

Commit f91aa29

Browse files
committed
feat: detect one-to-one relationships
1 parent 56c39a5 commit f91aa29

File tree

6 files changed

+118
-33
lines changed

6 files changed

+118
-33
lines changed

src/PostgrestClient.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,19 @@ export default class PostgrestClient<
5959
from<
6060
TableName extends string & keyof Schema['Tables'],
6161
Table extends Schema['Tables'][TableName]
62-
>(relation: TableName): PostgrestQueryBuilder<Schema, Table>
62+
>(relation: TableName): PostgrestQueryBuilder<Schema, Table, TableName>
6363
from<ViewName extends string & keyof Schema['Views'], View extends Schema['Views'][ViewName]>(
6464
relation: ViewName
65-
): PostgrestQueryBuilder<Schema, View>
66-
from(relation: string): PostgrestQueryBuilder<Schema, any>
65+
): PostgrestQueryBuilder<Schema, View, ViewName>
66+
from(relation: string): PostgrestQueryBuilder<Schema, any, any>
6767
/**
6868
* Perform a query on a table or a view.
6969
*
7070
* @param relation - The table or view name to query
7171
*/
72-
from(relation: string): PostgrestQueryBuilder<Schema, any> {
72+
from(relation: string): PostgrestQueryBuilder<Schema, any, any> {
7373
const url = new URL(`${this.url}/${relation}`)
74-
return new PostgrestQueryBuilder<Schema, any>(url, {
74+
return new PostgrestQueryBuilder(url, {
7575
headers: { ...this.headers },
7676
schema: this.schemaName,
7777
fetch: this.fetch,
@@ -92,11 +92,7 @@ export default class PostgrestClient<
9292
DynamicSchema,
9393
Database[DynamicSchema] extends GenericSchema ? Database[DynamicSchema] : any
9494
> {
95-
return new PostgrestClient<
96-
Database,
97-
DynamicSchema,
98-
Database[DynamicSchema] extends GenericSchema ? Database[DynamicSchema] : any
99-
>(this.url, {
95+
return new PostgrestClient(this.url, {
10096
headers: this.headers,
10197
schema,
10298
fetch: this.fetch,

src/PostgrestFilterBuilder.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ export default class PostgrestFilterBuilder<
2929
Schema extends GenericSchema,
3030
Row extends Record<string, unknown>,
3131
Result,
32+
RelationName = unknown,
3233
Relationships = unknown
33-
> extends PostgrestTransformBuilder<Schema, Row, Result, Relationships> {
34+
> extends PostgrestTransformBuilder<Schema, Row, Result, RelationName, Relationships> {
3435
eq<ColumnName extends string & keyof Row>(
3536
column: ColumnName,
3637
value: NonNullable<Row[ColumnName]>

src/PostgrestQueryBuilder.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Fetch, GenericSchema, GenericTable, GenericView } from './types'
66
export default class PostgrestQueryBuilder<
77
Schema extends GenericSchema,
88
Relation extends GenericTable | GenericView,
9+
RelationName = unknown,
910
Relationships = Relation extends { Relationships: infer R } ? R : unknown
1011
> {
1112
url: URL
@@ -55,7 +56,7 @@ export default class PostgrestQueryBuilder<
5556
*/
5657
select<
5758
Query extends string = '*',
58-
ResultOne = GetResult<Schema, Relation['Row'], Relationships, Query>
59+
ResultOne = GetResult<Schema, Relation['Row'], RelationName, Relationships, Query>
5960
>(
6061
columns?: Query,
6162
{
@@ -65,7 +66,7 @@ export default class PostgrestQueryBuilder<
6566
head?: boolean
6667
count?: 'exact' | 'planned' | 'estimated'
6768
} = {}
68-
): PostgrestFilterBuilder<Schema, Relation['Row'], ResultOne[], Relationships> {
69+
): PostgrestFilterBuilder<Schema, Relation['Row'], ResultOne[], RelationName, Relationships> {
6970
const method = head ? 'HEAD' : 'GET'
7071
// Remove whitespaces except when quoted
7172
let quoted = false
@@ -102,14 +103,14 @@ export default class PostgrestQueryBuilder<
102103
options?: {
103104
count?: 'exact' | 'planned' | 'estimated'
104105
}
105-
): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships>
106+
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships>
106107
insert<Row extends Relation extends { Insert: unknown } ? Relation['Insert'] : never>(
107108
values: Row[],
108109
options?: {
109110
count?: 'exact' | 'planned' | 'estimated'
110111
defaultToNull?: boolean
111112
}
112-
): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships>
113+
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships>
113114
/**
114115
* Perform an INSERT into the table or view.
115116
*
@@ -145,7 +146,7 @@ export default class PostgrestQueryBuilder<
145146
count?: 'exact' | 'planned' | 'estimated'
146147
defaultToNull?: boolean
147148
} = {}
148-
): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships> {
149+
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships> {
149150
const method = 'POST'
150151

151152
const prefersHeaders = []
@@ -187,7 +188,7 @@ export default class PostgrestQueryBuilder<
187188
ignoreDuplicates?: boolean
188189
count?: 'exact' | 'planned' | 'estimated'
189190
}
190-
): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships>
191+
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships>
191192
upsert<Row extends Relation extends { Insert: unknown } ? Relation['Insert'] : never>(
192193
values: Row[],
193194
options?: {
@@ -196,7 +197,7 @@ export default class PostgrestQueryBuilder<
196197
count?: 'exact' | 'planned' | 'estimated'
197198
defaultToNull?: boolean
198199
}
199-
): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships>
200+
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships>
200201
/**
201202
* Perform an UPSERT on the table or view. Depending on the column(s) passed
202203
* to `onConflict`, `.upsert()` allows you to perform the equivalent of
@@ -248,7 +249,7 @@ export default class PostgrestQueryBuilder<
248249
count?: 'exact' | 'planned' | 'estimated'
249250
defaultToNull?: boolean
250251
} = {}
251-
): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships> {
252+
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships> {
252253
const method = 'POST'
253254

254255
const prefersHeaders = [`resolution=${ignoreDuplicates ? 'ignore' : 'merge'}-duplicates`]
@@ -312,7 +313,7 @@ export default class PostgrestQueryBuilder<
312313
}: {
313314
count?: 'exact' | 'planned' | 'estimated'
314315
} = {}
315-
): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships> {
316+
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships> {
316317
const method = 'PATCH'
317318
const prefersHeaders = []
318319
if (this.headers['Prefer']) {
@@ -357,7 +358,7 @@ export default class PostgrestQueryBuilder<
357358
count,
358359
}: {
359360
count?: 'exact' | 'planned' | 'estimated'
360-
} = {}): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships> {
361+
} = {}): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships> {
361362
const method = 'DELETE'
362363
const prefersHeaders = []
363364
if (count) {

src/PostgrestTransformBuilder.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export default class PostgrestTransformBuilder<
66
Schema extends GenericSchema,
77
Row extends Record<string, unknown>,
88
Result,
9+
RelationName = unknown,
910
Relationships = unknown
1011
> extends PostgrestBuilder<Result> {
1112
/**
@@ -17,9 +18,12 @@ export default class PostgrestTransformBuilder<
1718
*
1819
* @param columns - The columns to retrieve, separated by commas
1920
*/
20-
select<Query extends string = '*', NewResultOne = GetResult<Schema, Row, Relationships, Query>>(
21+
select<
22+
Query extends string = '*',
23+
NewResultOne = GetResult<Schema, Row, RelationName, Relationships, Query>
24+
>(
2125
columns?: Query
22-
): PostgrestTransformBuilder<Schema, Row, NewResultOne[], Relationships> {
26+
): PostgrestTransformBuilder<Schema, Row, NewResultOne[], RelationName, Relationships> {
2327
// Remove whitespaces except when quoted
2428
let quoted = false
2529
const cleanedColumns = (columns ?? '*')
@@ -39,7 +43,13 @@ export default class PostgrestTransformBuilder<
3943
this.headers['Prefer'] += ','
4044
}
4145
this.headers['Prefer'] += 'return=representation'
42-
return this as unknown as PostgrestTransformBuilder<Schema, Row, NewResultOne[], Relationships>
46+
return this as unknown as PostgrestTransformBuilder<
47+
Schema,
48+
Row,
49+
NewResultOne[],
50+
RelationName,
51+
Relationships
52+
>
4353
}
4454

4555
order<ColumnName extends string & keyof Row>(
@@ -294,7 +304,19 @@ export default class PostgrestTransformBuilder<
294304
*
295305
* @typeParam NewResult - The new result type to override with
296306
*/
297-
returns<NewResult>(): PostgrestTransformBuilder<Schema, Row, NewResult, Relationships> {
298-
return this as unknown as PostgrestTransformBuilder<Schema, Row, NewResult, Relationships>
307+
returns<NewResult>(): PostgrestTransformBuilder<
308+
Schema,
309+
Row,
310+
NewResult,
311+
RelationName,
312+
Relationships
313+
> {
314+
return this as unknown as PostgrestTransformBuilder<
315+
Schema,
316+
Row,
317+
NewResult,
318+
RelationName,
319+
Relationships
320+
>
299321
}
300322
}

src/select-query-parser.ts

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,16 @@ type HasFKey<FKeyName, Relationships> = Relationships extends [infer R]
6666
: HasFKey<FKeyName, Rest>
6767
: false
6868

69+
type HasUniqueFKey<FKeyName, Relationships> = Relationships extends [infer R]
70+
? R extends { foreignKeyName: FKeyName; isOneToOne: true }
71+
? true
72+
: false
73+
: Relationships extends [infer R, ...infer Rest]
74+
? HasUniqueFKey<FKeyName, [R]> extends true
75+
? true
76+
: HasUniqueFKey<FKeyName, Rest>
77+
: false
78+
6979
type HasFKeyToFRel<FRelName, Relationships> = Relationships extends [infer R]
7080
? R extends { referencedRelation: FRelName }
7181
? true
@@ -76,6 +86,16 @@ type HasFKeyToFRel<FRelName, Relationships> = Relationships extends [infer R]
7686
: HasFKeyToFRel<FRelName, Rest>
7787
: false
7888

89+
type HasUniqueFKeyToFRel<FRelName, Relationships> = Relationships extends [infer R]
90+
? R extends { referencedRelation: FRelName; isOneToOne: true }
91+
? true
92+
: false
93+
: Relationships extends [infer R, ...infer Rest]
94+
? HasUniqueFKeyToFRel<FRelName, [R]> extends true
95+
? true
96+
: HasUniqueFKeyToFRel<FRelName, Rest>
97+
: false
98+
7999
/**
80100
* Constructs a type definition for a single field of an object.
81101
*
@@ -86,6 +106,7 @@ type HasFKeyToFRel<FRelName, Relationships> = Relationships extends [infer R]
86106
type ConstructFieldDefinition<
87107
Schema extends GenericSchema,
88108
Row extends Record<string, unknown>,
109+
RelationName,
89110
Relationships,
90111
Field
91112
> = Field extends { star: true }
@@ -95,13 +116,24 @@ type ConstructFieldDefinition<
95116
[_ in Field['name']]: GetResultHelper<
96117
Schema,
97118
(Schema['Tables'] & Schema['Views'])[Field['original']]['Row'],
119+
Field['original'],
98120
(Schema['Tables'] & Schema['Views'])[Field['original']] extends { Relationships: infer R }
99121
? R
100122
: unknown,
101123
Field['children'],
102124
unknown
103125
> extends infer Child
104-
? Relationships extends unknown[]
126+
? // One-to-one relationship - referencing column(s) has unique/pkey constraint.
127+
HasUniqueFKey<
128+
Field['hint'],
129+
(Schema['Tables'] & Schema['Views'])[Field['original']] extends {
130+
Relationships: infer R
131+
}
132+
? R
133+
: unknown
134+
> extends true
135+
? Child | null
136+
: Relationships extends unknown[]
105137
? HasFKey<Field['hint'], Relationships> extends true
106138
? Child | null
107139
: Child[]
@@ -113,13 +145,24 @@ type ConstructFieldDefinition<
113145
[_ in Field['name']]: GetResultHelper<
114146
Schema,
115147
(Schema['Tables'] & Schema['Views'])[Field['original']]['Row'],
148+
Field['original'],
116149
(Schema['Tables'] & Schema['Views'])[Field['original']] extends { Relationships: infer R }
117150
? R
118151
: unknown,
119152
Field['children'],
120153
unknown
121154
> extends infer Child
122-
? Relationships extends unknown[]
155+
? // One-to-one relationship - referencing column(s) has unique/pkey constraint.
156+
HasUniqueFKeyToFRel<
157+
RelationName,
158+
(Schema['Tables'] & Schema['Views'])[Field['original']] extends {
159+
Relationships: infer R
160+
}
161+
? R
162+
: unknown
163+
> extends true
164+
? Child | null
165+
: Relationships extends unknown[]
123166
? HasFKeyToFRel<Field['original'], Relationships> extends true
124167
? Child | null
125168
: Child[]
@@ -404,28 +447,35 @@ type ParseQuery<Query extends string> = string extends Query
404447
type GetResultHelper<
405448
Schema extends GenericSchema,
406449
Row extends Record<string, unknown>,
450+
RelationName,
407451
Relationships,
408452
Fields extends unknown[],
409453
Acc
410454
> = Fields extends [infer R]
411-
? ConstructFieldDefinition<Schema, Row, Relationships, R> extends SelectQueryError<infer E>
455+
? ConstructFieldDefinition<Schema, Row, RelationName, Relationships, R> extends SelectQueryError<
456+
infer E
457+
>
412458
? SelectQueryError<E>
413459
: GetResultHelper<
414460
Schema,
415461
Row,
462+
RelationName,
416463
Relationships,
417464
[],
418-
ConstructFieldDefinition<Schema, Row, Relationships, R> & Acc
465+
ConstructFieldDefinition<Schema, Row, RelationName, Relationships, R> & Acc
419466
>
420467
: Fields extends [infer R, ...infer Rest]
421-
? ConstructFieldDefinition<Schema, Row, Relationships, R> extends SelectQueryError<infer E>
468+
? ConstructFieldDefinition<Schema, Row, RelationName, Relationships, R> extends SelectQueryError<
469+
infer E
470+
>
422471
? SelectQueryError<E>
423472
: GetResultHelper<
424473
Schema,
425474
Row,
475+
RelationName,
426476
Relationships,
427477
Rest,
428-
ConstructFieldDefinition<Schema, Row, Relationships, R> & Acc
478+
ConstructFieldDefinition<Schema, Row, RelationName, Relationships, R> & Acc
429479
>
430480
: Prettify<Acc>
431481

@@ -438,8 +488,9 @@ type GetResultHelper<
438488
export type GetResult<
439489
Schema extends GenericSchema,
440490
Row extends Record<string, unknown>,
491+
RelationName,
441492
Relationships,
442493
Query extends string
443494
> = ParseQuery<Query> extends unknown[]
444-
? GetResultHelper<Schema, Row, Relationships, ParseQuery<Query>, unknown>
495+
? GetResultHelper<Schema, Row, RelationName, Relationships, ParseQuery<Query>, unknown>
445496
: ParseQuery<Query>

test/index.test-d.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,17 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
100100
const res = await postgrest.from('users').select('username, dat')
101101
expectType<PostgrestSingleResponse<SelectQueryError<`Referencing missing column \`dat\``>[]>>(res)
102102
}
103+
104+
// one-to-one relationship
105+
{
106+
const { data: channels, error } = await postgrest
107+
.from('channels')
108+
.select('channel_details(*)')
109+
.single()
110+
if (error) {
111+
throw new Error(error.message)
112+
}
113+
expectType<Database['public']['Tables']['channel_details']['Row'] | null>(
114+
channels.channel_details
115+
)
116+
}

0 commit comments

Comments
 (0)