Skip to content

fix: Correctly validate relationship enum values in eq, neq and in methods #589

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 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,30 @@ type FilterOperator =
| 'phfts'
| 'wfts'

// Match relationship filters with `table.column` syntax and resolve underlying column value.
// If not matched, fallback to generic type.
type ResolveFilterValue<
Tables extends Record<string, GenericTable>,
Row extends Record<string, unknown>,
ColumnName extends string
> = ColumnName extends `${infer RelationshipTable}.${infer RelationshipColumn}`
? ResolveFilterRelationshipValue<Tables, RelationshipTable, RelationshipColumn>
: 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 +66,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 +82,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 +262,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
49 changes: 48 additions & 1 deletion test/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,26 @@ 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'])
)

{
const { data, error } = await postgrest.from('users').select('status').eq('status', 'ONLINE')
if (error) {
Expand All @@ -53,6 +67,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