Skip to content

Commit 09083ea

Browse files
authored
fix: Correctly validate relationship enum values in eq, neq and in methods (#589)
1 parent e556d3f commit 09083ea

File tree

2 files changed

+97
-5
lines changed

2 files changed

+97
-5
lines changed

src/PostgrestFilterBuilder.ts

+39-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import PostgrestTransformBuilder from './PostgrestTransformBuilder'
2-
import { GenericSchema } from './types'
2+
import { GenericSchema, GenericTable } from './types'
33

44
type FilterOperator =
55
| 'eq'
@@ -25,6 +25,35 @@ type FilterOperator =
2525
| 'phfts'
2626
| 'wfts'
2727

28+
// Match relationship filters with `table.column` syntax and resolve underlying
29+
// column value. If not matched, fallback to generic type.
30+
// TODO: Validate the relationship itself ala select-query-parser. Currently we
31+
// assume that all tables have valid relationships to each other, despite
32+
// nonexistent foreign keys.
33+
type ResolveFilterValue<
34+
Tables extends Record<string, GenericTable>,
35+
Row extends Record<string, unknown>,
36+
ColumnName extends string
37+
> = ColumnName extends `${infer RelationshipTable}.${infer Remainder}`
38+
? Remainder extends `${infer _}.${infer _}`
39+
? ResolveFilterValue<Tables, Row, Remainder>
40+
: ResolveFilterRelationshipValue<Tables, RelationshipTable, Remainder>
41+
: ColumnName extends keyof Row
42+
? Row[ColumnName]
43+
: never
44+
45+
type ResolveFilterRelationshipValue<
46+
Tables extends Record<string, GenericTable>,
47+
RelationshipTable extends string,
48+
RelationshipColumn extends string
49+
> = RelationshipTable extends keyof Tables
50+
? 'Row' extends keyof Tables[RelationshipTable]
51+
? RelationshipColumn extends keyof Tables[RelationshipTable]['Row']
52+
? Tables[RelationshipTable]['Row'][RelationshipColumn]
53+
: unknown
54+
: unknown
55+
: unknown
56+
2857
export default class PostgrestFilterBuilder<
2958
Schema extends GenericSchema,
3059
Row extends Record<string, unknown>,
@@ -42,7 +71,9 @@ export default class PostgrestFilterBuilder<
4271
*/
4372
eq<ColumnName extends string>(
4473
column: ColumnName,
45-
value: ColumnName extends keyof Row ? NonNullable<Row[ColumnName]> : NonNullable<unknown>
74+
value: ResolveFilterValue<Schema['Tables'], Row, ColumnName> extends never
75+
? NonNullable<unknown>
76+
: NonNullable<ResolveFilterValue<Schema['Tables'], Row, ColumnName>>
4677
): this {
4778
this.url.searchParams.append(column, `eq.${value}`)
4879
return this
@@ -56,7 +87,9 @@ export default class PostgrestFilterBuilder<
5687
*/
5788
neq<ColumnName extends string>(
5889
column: ColumnName,
59-
value: ColumnName extends keyof Row ? Row[ColumnName] : unknown
90+
value: ResolveFilterValue<Schema['Tables'], Row, ColumnName> extends never
91+
? unknown
92+
: ResolveFilterValue<Schema['Tables'], Row, ColumnName>
6093
): this {
6194
this.url.searchParams.append(column, `neq.${value}`)
6295
return this
@@ -234,7 +267,9 @@ export default class PostgrestFilterBuilder<
234267
*/
235268
in<ColumnName extends string>(
236269
column: ColumnName,
237-
values: ColumnName extends keyof Row ? ReadonlyArray<Row[ColumnName]> : unknown[]
270+
values: ResolveFilterValue<Schema['Tables'], Row, ColumnName> extends never
271+
? unknown[]
272+
: ReadonlyArray<ResolveFilterValue<Schema['Tables'], Row, ColumnName>>
238273
): this {
239274
const cleanedValues = Array.from(new Set(values))
240275
.map((s) => {

test/index.test-d.ts

+58-1
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,36 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
2121
expectError(postgrest.from('users').select().eq('username', nullableVar))
2222
}
2323

24-
// `.eq()`, '.neq()' and `.in()` validate value when column is an enum
24+
// `.eq()`, '.neq()' and `.in()` validate provided filter value when column is an enum.
25+
// Behaves the same for simple columns, as well as relationship filters.
2526
{
2627
expectError(postgrest.from('users').select().eq('status', 'invalid'))
2728
expectError(postgrest.from('users').select().neq('status', 'invalid'))
2829
expectError(postgrest.from('users').select().in('status', ['invalid']))
2930

31+
expectError(
32+
postgrest.from('best_friends').select('users!first_user(status)').eq('users.status', 'invalid')
33+
)
34+
expectError(
35+
postgrest.from('best_friends').select('users!first_user(status)').neq('users.status', 'invalid')
36+
)
37+
expectError(
38+
postgrest
39+
.from('best_friends')
40+
.select('users!first_user(status)')
41+
.in('users.status', ['invalid'])
42+
)
43+
// Validate deeply nested embedded tables
44+
expectError(
45+
postgrest.from('users').select('messages(channels(*))').eq('messages.channels.id', 'invalid')
46+
)
47+
expectError(
48+
postgrest.from('users').select('messages(channels(*))').neq('messages.channels.id', 'invalid')
49+
)
50+
expectError(
51+
postgrest.from('users').select('messages(channels(*))').in('messages.channels.id', ['invalid'])
52+
)
53+
3054
{
3155
const { data, error } = await postgrest.from('users').select('status').eq('status', 'ONLINE')
3256
if (error) {
@@ -53,6 +77,39 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
5377
}
5478
expectType<{ status: Database['public']['Enums']['user_status'] | null }[]>(data)
5579
}
80+
81+
{
82+
const { data, error } = await postgrest
83+
.from('best_friends')
84+
.select('users!first_user(status)')
85+
.eq('users.status', 'ONLINE')
86+
if (error) {
87+
throw new Error(error.message)
88+
}
89+
expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>(data)
90+
}
91+
92+
{
93+
const { data, error } = await postgrest
94+
.from('best_friends')
95+
.select('users!first_user(status)')
96+
.neq('users.status', 'ONLINE')
97+
if (error) {
98+
throw new Error(error.message)
99+
}
100+
expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>(data)
101+
}
102+
103+
{
104+
const { data, error } = await postgrest
105+
.from('best_friends')
106+
.select('users!first_user(status)')
107+
.in('users.status', ['ONLINE', 'OFFLINE'])
108+
if (error) {
109+
throw new Error(error.message)
110+
}
111+
expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>(data)
112+
}
56113
}
57114

58115
// can override result type

0 commit comments

Comments
 (0)