Skip to content

Commit

Permalink
fix: Correctly validate relationship enum values in eq, neq and in me…
Browse files Browse the repository at this point in the history
…thods (#589)
  • Loading branch information
kamilogorek authored Jan 6, 2025
1 parent e556d3f commit 09083ea
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 5 deletions.
43 changes: 39 additions & 4 deletions src/PostgrestFilterBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import PostgrestTransformBuilder from './PostgrestTransformBuilder'
import { GenericSchema } from './types'
import { GenericSchema, GenericTable } from './types'

type FilterOperator =
| 'eq'
Expand All @@ -25,6 +25,35 @@ type FilterOperator =
| 'phfts'
| 'wfts'

// Match relationship filters with `table.column` syntax and resolve underlying
// column value. If not matched, fallback to generic type.
// TODO: Validate the relationship itself ala select-query-parser. Currently we
// assume that all tables have valid relationships to each other, despite
// nonexistent foreign keys.
type ResolveFilterValue<
Tables extends Record<string, GenericTable>,
Row extends Record<string, unknown>,
ColumnName extends string
> = ColumnName extends `${infer RelationshipTable}.${infer Remainder}`
? Remainder extends `${infer _}.${infer _}`
? ResolveFilterValue<Tables, Row, Remainder>
: ResolveFilterRelationshipValue<Tables, RelationshipTable, Remainder>
: ColumnName extends keyof Row
? Row[ColumnName]
: never

type ResolveFilterRelationshipValue<
Tables extends Record<string, GenericTable>,
RelationshipTable extends string,
RelationshipColumn extends string
> = RelationshipTable extends keyof Tables
? 'Row' extends keyof Tables[RelationshipTable]
? RelationshipColumn extends keyof Tables[RelationshipTable]['Row']
? Tables[RelationshipTable]['Row'][RelationshipColumn]
: unknown
: unknown
: unknown

export default class PostgrestFilterBuilder<
Schema extends GenericSchema,
Row extends Record<string, unknown>,
Expand All @@ -42,7 +71,9 @@ export default class PostgrestFilterBuilder<
*/
eq<ColumnName extends string>(
column: ColumnName,
value: ColumnName extends keyof Row ? NonNullable<Row[ColumnName]> : NonNullable<unknown>
value: ResolveFilterValue<Schema['Tables'], Row, ColumnName> extends never
? NonNullable<unknown>
: NonNullable<ResolveFilterValue<Schema['Tables'], Row, ColumnName>>
): this {
this.url.searchParams.append(column, `eq.${value}`)
return this
Expand All @@ -56,7 +87,9 @@ export default class PostgrestFilterBuilder<
*/
neq<ColumnName extends string>(
column: ColumnName,
value: ColumnName extends keyof Row ? Row[ColumnName] : unknown
value: ResolveFilterValue<Schema['Tables'], Row, ColumnName> extends never
? unknown
: ResolveFilterValue<Schema['Tables'], Row, ColumnName>
): this {
this.url.searchParams.append(column, `neq.${value}`)
return this
Expand Down Expand Up @@ -234,7 +267,9 @@ export default class PostgrestFilterBuilder<
*/
in<ColumnName extends string>(
column: ColumnName,
values: ColumnName extends keyof Row ? ReadonlyArray<Row[ColumnName]> : unknown[]
values: ResolveFilterValue<Schema['Tables'], Row, ColumnName> extends never
? unknown[]
: ReadonlyArray<ResolveFilterValue<Schema['Tables'], Row, ColumnName>>
): this {
const cleanedValues = Array.from(new Set(values))
.map((s) => {
Expand Down
59 changes: 58 additions & 1 deletion test/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,36 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
expectError(postgrest.from('users').select().eq('username', nullableVar))
}

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

expectError(
postgrest.from('best_friends').select('users!first_user(status)').eq('users.status', 'invalid')
)
expectError(
postgrest.from('best_friends').select('users!first_user(status)').neq('users.status', 'invalid')
)
expectError(
postgrest
.from('best_friends')
.select('users!first_user(status)')
.in('users.status', ['invalid'])
)
// Validate deeply nested embedded tables
expectError(
postgrest.from('users').select('messages(channels(*))').eq('messages.channels.id', 'invalid')
)
expectError(
postgrest.from('users').select('messages(channels(*))').neq('messages.channels.id', 'invalid')
)
expectError(
postgrest.from('users').select('messages(channels(*))').in('messages.channels.id', ['invalid'])
)

{
const { data, error } = await postgrest.from('users').select('status').eq('status', 'ONLINE')
if (error) {
Expand All @@ -53,6 +77,39 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
}
expectType<{ status: Database['public']['Enums']['user_status'] | null }[]>(data)
}

{
const { data, error } = await postgrest
.from('best_friends')
.select('users!first_user(status)')
.eq('users.status', 'ONLINE')
if (error) {
throw new Error(error.message)
}
expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>(data)
}

{
const { data, error } = await postgrest
.from('best_friends')
.select('users!first_user(status)')
.neq('users.status', 'ONLINE')
if (error) {
throw new Error(error.message)
}
expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>(data)
}

{
const { data, error } = await postgrest
.from('best_friends')
.select('users!first_user(status)')
.in('users.status', ['ONLINE', 'OFFLINE'])
if (error) {
throw new Error(error.message)
}
expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>(data)
}
}

// can override result type
Expand Down

0 comments on commit 09083ea

Please sign in to comment.