Skip to content

Commit

Permalink
Merge pull request #426 from supabase/feat/relationship-cardinality
Browse files Browse the repository at this point in the history
feat: determine relationship cardinality from types
  • Loading branch information
soedirgo authored May 26, 2023
2 parents 878034b + fc59958 commit 56376c0
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 80 deletions.
5 changes: 3 additions & 2 deletions src/PostgrestFilterBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ type FilterOperator =
export default class PostgrestFilterBuilder<
Schema extends GenericSchema,
Row extends Record<string, unknown>,
Result
> extends PostgrestTransformBuilder<Schema, Row, Result> {
Result,
Relationships = unknown
> extends PostgrestTransformBuilder<Schema, Row, Result, Relationships> {
eq<ColumnName extends string & keyof Row>(column: ColumnName, value: Row[ColumnName]): this
eq(column: string, value: unknown): this
/**
Expand Down
18 changes: 11 additions & 7 deletions src/PostgrestQueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { Fetch, GenericSchema, GenericTable, GenericView } from './types'

export default class PostgrestQueryBuilder<
Schema extends GenericSchema,
Relation extends GenericTable | GenericView
Relation extends GenericTable | GenericView,
Relationships = Relation extends { Relationships: infer R } ? R : unknown
> {
url: URL
headers: Record<string, string>
Expand Down Expand Up @@ -52,7 +53,10 @@ export default class PostgrestQueryBuilder<
* `"estimated"`: Uses exact count for low numbers and planned count for high
* numbers.
*/
select<Query extends string = '*', ResultOne = GetResult<Schema, Relation['Row'], Query>>(
select<
Query extends string = '*',
ResultOne = GetResult<Schema, Relation['Row'], Relationships, Query>
>(
columns?: Query,
{
head = false,
Expand All @@ -61,7 +65,7 @@ export default class PostgrestQueryBuilder<
head?: boolean
count?: 'exact' | 'planned' | 'estimated'
} = {}
): PostgrestFilterBuilder<Schema, Relation['Row'], ResultOne[]> {
): PostgrestFilterBuilder<Schema, Relation['Row'], ResultOne[], Relationships> {
const method = head ? 'HEAD' : 'GET'
// Remove whitespaces except when quoted
let quoted = false
Expand Down Expand Up @@ -126,7 +130,7 @@ export default class PostgrestQueryBuilder<
count?: 'exact' | 'planned' | 'estimated'
defaultToNull?: boolean
} = {}
): PostgrestFilterBuilder<Schema, Relation['Row'], null> {
): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships> {
const method = 'POST'

const prefersHeaders = []
Expand Down Expand Up @@ -211,7 +215,7 @@ export default class PostgrestQueryBuilder<
count?: 'exact' | 'planned' | 'estimated'
defaultToNull?: boolean
} = {}
): PostgrestFilterBuilder<Schema, Relation['Row'], null> {
): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships> {
const method = 'POST'

const prefersHeaders = [`resolution=${ignoreDuplicates ? 'ignore' : 'merge'}-duplicates`]
Expand Down Expand Up @@ -275,7 +279,7 @@ export default class PostgrestQueryBuilder<
}: {
count?: 'exact' | 'planned' | 'estimated'
} = {}
): PostgrestFilterBuilder<Schema, Relation['Row'], null> {
): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships> {
const method = 'PATCH'
const prefersHeaders = []
if (this.headers['Prefer']) {
Expand Down Expand Up @@ -320,7 +324,7 @@ export default class PostgrestQueryBuilder<
count,
}: {
count?: 'exact' | 'planned' | 'estimated'
} = {}): PostgrestFilterBuilder<Schema, Relation['Row'], null> {
} = {}): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships> {
const method = 'DELETE'
const prefersHeaders = []
if (count) {
Expand Down
13 changes: 7 additions & 6 deletions src/PostgrestTransformBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { GenericSchema } from './types'
export default class PostgrestTransformBuilder<
Schema extends GenericSchema,
Row extends Record<string, unknown>,
Result
Result,
Relationships = unknown
> extends PostgrestBuilder<Result> {
/**
* Perform a SELECT on the query result.
Expand All @@ -16,9 +17,9 @@ export default class PostgrestTransformBuilder<
*
* @param columns - The columns to retrieve, separated by commas
*/
select<Query extends string = '*', NewResultOne = GetResult<Schema, Row, Query>>(
select<Query extends string = '*', NewResultOne = GetResult<Schema, Row, Relationships, Query>>(
columns?: Query
): PostgrestTransformBuilder<Schema, Row, NewResultOne[]> {
): PostgrestTransformBuilder<Schema, Row, NewResultOne[], Relationships> {
// Remove whitespaces except when quoted
let quoted = false
const cleanedColumns = (columns ?? '*')
Expand All @@ -38,7 +39,7 @@ export default class PostgrestTransformBuilder<
this.headers['Prefer'] += ','
}
this.headers['Prefer'] += 'return=representation'
return this as unknown as PostgrestTransformBuilder<Schema, Row, NewResultOne[]>
return this as unknown as PostgrestTransformBuilder<Schema, Row, NewResultOne[], Relationships>
}

order<ColumnName extends string & keyof Row>(
Expand Down Expand Up @@ -249,7 +250,7 @@ export default class PostgrestTransformBuilder<
*
* @typeParam NewResult - The new result type to override with
*/
returns<NewResult>(): PostgrestTransformBuilder<Schema, Row, NewResult> {
return this as unknown as PostgrestTransformBuilder<Schema, Row, NewResult>
returns<NewResult>(): PostgrestTransformBuilder<Schema, Row, NewResult, Relationships> {
return this as unknown as PostgrestTransformBuilder<Schema, Row, NewResult, Relationships>
}
}
86 changes: 74 additions & 12 deletions src/select-query-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,26 @@ type EatWhitespace<Input extends string> = string extends Input
? EatWhitespace<Remainder>
: Input

type HasFKey<FKeyName, Relationships> = Relationships extends [infer R]
? R extends { foreignKeyName: FKeyName }
? true
: false
: Relationships extends [infer R, ...infer Rest]
? HasFKey<FKeyName, [R]> extends true
? true
: HasFKey<FKeyName, Rest>
: false

type HasFKeyToFRel<FRelName, Relationships> = Relationships extends [infer R]
? R extends { referencedRelation: FRelName }
? true
: false
: Relationships extends [infer R, ...infer Rest]
? HasFKeyToFRel<FRelName, [R]> extends true
? true
: HasFKeyToFRel<FRelName, Rest>
: false

/**
* Constructs a type definition for a single field of an object.
*
Expand All @@ -75,20 +95,44 @@ type EatWhitespace<Input extends string> = string extends Input
type ConstructFieldDefinition<
Schema extends GenericSchema,
Row extends Record<string, unknown>,
Relationships,
Field
> = Field extends {
star: true
}
> = Field extends { star: true }
? Row
: Field extends { name: string; original: string; hint: string; children: unknown[] }
? {
[_ in Field['name']]: GetResultHelper<
Schema,
(Schema['Tables'] & Schema['Views'])[Field['original']]['Row'],
(Schema['Tables'] & Schema['Views'])[Field['original']] extends { Relationships: infer R }
? R
: unknown,
Field['children'],
unknown
> extends infer Child
? Relationships extends unknown[]
? HasFKey<Field['hint'], Relationships> extends true
? Child | null
: Child[]
: Child[]
: never
}
: Field extends { name: string; original: string; children: unknown[] }
? {
[_ in Field['name']]: GetResultHelper<
Schema,
(Schema['Tables'] & Schema['Views'])[Field['original']]['Row'],
(Schema['Tables'] & Schema['Views'])[Field['original']] extends { Relationships: infer R }
? R
: unknown,
Field['children'],
unknown
> extends infer Child
? Child | Child[] | null
? Relationships extends unknown[]
? HasFKeyToFRel<Field['original'], Relationships> extends true
? Child | null
: Child[]
: Child[]
: never
}
: Field extends { name: string; original: string }
Expand Down Expand Up @@ -191,14 +235,14 @@ type ParseNode<Input extends string> = Input extends ''
? ParseEmbeddedResource<EatWhitespace<Remainder>>
: ParserError<'Expected embedded resource after `!inner`'>
: EatWhitespace<Remainder> extends `!${infer Remainder}`
? ParseIdentifier<EatWhitespace<Remainder>> extends [infer _Hint, `${infer Remainder}`]
? ParseIdentifier<EatWhitespace<Remainder>> extends [infer Hint, `${infer Remainder}`]
? EatWhitespace<Remainder> extends `!inner${infer Remainder}`
? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
infer Fields,
`${infer Remainder}`
]
? // `field!hint!inner(nodes)`
[{ name: Name; original: Name; children: Fields }, EatWhitespace<Remainder>]
[{ name: Name; original: Name; hint: Hint; children: Fields }, EatWhitespace<Remainder>]
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
? ParseEmbeddedResource<EatWhitespace<Remainder>>
: ParserError<'Expected embedded resource after `!inner`'>
Expand All @@ -207,7 +251,7 @@ type ParseNode<Input extends string> = Input extends ''
`${infer Remainder}`
]
? // `field!hint(nodes)`
[{ name: Name; original: Name; children: Fields }, EatWhitespace<Remainder>]
[{ name: Name; original: Name; hint: Hint; children: Fields }, EatWhitespace<Remainder>]
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
? ParseEmbeddedResource<EatWhitespace<Remainder>>
: ParserError<'Expected embedded resource after `!hint`'>
Expand All @@ -225,14 +269,17 @@ type ParseNode<Input extends string> = Input extends ''
? ParseEmbeddedResource<EatWhitespace<Remainder>>
: ParserError<'Expected embedded resource after `!inner`'>
: EatWhitespace<Remainder> extends `!${infer Remainder}`
? ParseIdentifier<EatWhitespace<Remainder>> extends [infer _Hint, `${infer Remainder}`]
? ParseIdentifier<EatWhitespace<Remainder>> extends [infer Hint, `${infer Remainder}`]
? EatWhitespace<Remainder> extends `!inner${infer Remainder}`
? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
infer Fields,
`${infer Remainder}`
]
? // `renamed_field:field!hint!inner(nodes)`
[{ name: Name; original: OriginalName; children: Fields }, EatWhitespace<Remainder>]
[
{ name: Name; original: OriginalName; hint: Hint; children: Fields },
EatWhitespace<Remainder>
]
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
? ParseEmbeddedResource<EatWhitespace<Remainder>>
: ParserError<'Expected embedded resource after `!inner`'>
Expand All @@ -245,6 +292,7 @@ type ParseNode<Input extends string> = Input extends ''
{
name: Name
original: OriginalName
hint: Hint
children: Fields
},
EatWhitespace<Remainder>
Expand Down Expand Up @@ -363,12 +411,25 @@ type ParseQuery<Query extends string> = string extends Query
type GetResultHelper<
Schema extends GenericSchema,
Row extends Record<string, unknown>,
Relationships,
Fields extends unknown[],
Acc
> = Fields extends [infer R]
? GetResultHelper<Schema, Row, [], ConstructFieldDefinition<Schema, Row, R> & Acc>
? GetResultHelper<
Schema,
Row,
Relationships,
[],
ConstructFieldDefinition<Schema, Row, Relationships, R> & Acc
>
: Fields extends [infer R, ...infer Rest]
? GetResultHelper<Schema, Row, Rest, ConstructFieldDefinition<Schema, Row, R> & Acc>
? GetResultHelper<
Schema,
Row,
Relationships,
Rest,
ConstructFieldDefinition<Schema, Row, Relationships, R> & Acc
>
: Prettify<Acc>

/**
Expand All @@ -380,7 +441,8 @@ type GetResultHelper<
export type GetResult<
Schema extends GenericSchema,
Row extends Record<string, unknown>,
Relationships,
Query extends string
> = ParseQuery<Query> extends unknown[]
? GetResultHelper<Schema, Row, ParseQuery<Query>, unknown>
? GetResultHelper<Schema, Row, Relationships, ParseQuery<Query>, unknown>
: ParseQuery<Query>
18 changes: 18 additions & 0 deletions test/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,21 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
}
expectType<'ONLINE' | 'OFFLINE'>(data)
}

// many-to-one relationship
{
const { data: message, error } = await postgrest.from('messages').select('user:users(*)').single()
if (error) {
throw new Error(error.message)
}
expectType<Database['public']['Tables']['users']['Row'] | null>(message.user)
}

// one-to-many relationship
{
const { data: user, error } = await postgrest.from('users').select('messages(*)').single()
if (error) {
throw new Error(error.message)
}
expectType<Database['public']['Tables']['messages']['Row'][]>(user.messages)
}
Loading

0 comments on commit 56376c0

Please sign in to comment.