Skip to content

Commit

Permalink
feat: detect one-to-one relationships
Browse files Browse the repository at this point in the history
  • Loading branch information
soedirgo committed Nov 22, 2023
1 parent 56c39a5 commit f91aa29
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 33 deletions.
16 changes: 6 additions & 10 deletions src/PostgrestClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,19 @@ export default class PostgrestClient<
from<
TableName extends string & keyof Schema['Tables'],
Table extends Schema['Tables'][TableName]
>(relation: TableName): PostgrestQueryBuilder<Schema, Table>
>(relation: TableName): PostgrestQueryBuilder<Schema, Table, TableName>
from<ViewName extends string & keyof Schema['Views'], View extends Schema['Views'][ViewName]>(
relation: ViewName
): PostgrestQueryBuilder<Schema, View>
from(relation: string): PostgrestQueryBuilder<Schema, any>
): PostgrestQueryBuilder<Schema, View, ViewName>
from(relation: string): PostgrestQueryBuilder<Schema, any, any>
/**
* Perform a query on a table or a view.
*
* @param relation - The table or view name to query
*/
from(relation: string): PostgrestQueryBuilder<Schema, any> {
from(relation: string): PostgrestQueryBuilder<Schema, any, any> {
const url = new URL(`${this.url}/${relation}`)
return new PostgrestQueryBuilder<Schema, any>(url, {
return new PostgrestQueryBuilder(url, {
headers: { ...this.headers },
schema: this.schemaName,
fetch: this.fetch,
Expand All @@ -92,11 +92,7 @@ export default class PostgrestClient<
DynamicSchema,
Database[DynamicSchema] extends GenericSchema ? Database[DynamicSchema] : any
> {
return new PostgrestClient<
Database,
DynamicSchema,
Database[DynamicSchema] extends GenericSchema ? Database[DynamicSchema] : any
>(this.url, {
return new PostgrestClient(this.url, {
headers: this.headers,
schema,
fetch: this.fetch,
Expand Down
3 changes: 2 additions & 1 deletion src/PostgrestFilterBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ export default class PostgrestFilterBuilder<
Schema extends GenericSchema,
Row extends Record<string, unknown>,
Result,
RelationName = unknown,
Relationships = unknown
> extends PostgrestTransformBuilder<Schema, Row, Result, Relationships> {
> extends PostgrestTransformBuilder<Schema, Row, Result, RelationName, Relationships> {
eq<ColumnName extends string & keyof Row>(
column: ColumnName,
value: NonNullable<Row[ColumnName]>
Expand Down
21 changes: 11 additions & 10 deletions src/PostgrestQueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Fetch, GenericSchema, GenericTable, GenericView } from './types'
export default class PostgrestQueryBuilder<
Schema extends GenericSchema,
Relation extends GenericTable | GenericView,
RelationName = unknown,
Relationships = Relation extends { Relationships: infer R } ? R : unknown
> {
url: URL
Expand Down Expand Up @@ -55,7 +56,7 @@ export default class PostgrestQueryBuilder<
*/
select<
Query extends string = '*',
ResultOne = GetResult<Schema, Relation['Row'], Relationships, Query>
ResultOne = GetResult<Schema, Relation['Row'], RelationName, Relationships, Query>
>(
columns?: Query,
{
Expand All @@ -65,7 +66,7 @@ export default class PostgrestQueryBuilder<
head?: boolean
count?: 'exact' | 'planned' | 'estimated'
} = {}
): PostgrestFilterBuilder<Schema, Relation['Row'], ResultOne[], Relationships> {
): PostgrestFilterBuilder<Schema, Relation['Row'], ResultOne[], RelationName, Relationships> {
const method = head ? 'HEAD' : 'GET'
// Remove whitespaces except when quoted
let quoted = false
Expand Down Expand Up @@ -102,14 +103,14 @@ export default class PostgrestQueryBuilder<
options?: {
count?: 'exact' | 'planned' | 'estimated'
}
): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships>
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships>
insert<Row extends Relation extends { Insert: unknown } ? Relation['Insert'] : never>(
values: Row[],
options?: {
count?: 'exact' | 'planned' | 'estimated'
defaultToNull?: boolean
}
): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships>
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships>
/**
* Perform an INSERT into the table or view.
*
Expand Down Expand Up @@ -145,7 +146,7 @@ export default class PostgrestQueryBuilder<
count?: 'exact' | 'planned' | 'estimated'
defaultToNull?: boolean
} = {}
): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships> {
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships> {
const method = 'POST'

const prefersHeaders = []
Expand Down Expand Up @@ -187,7 +188,7 @@ export default class PostgrestQueryBuilder<
ignoreDuplicates?: boolean
count?: 'exact' | 'planned' | 'estimated'
}
): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships>
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships>
upsert<Row extends Relation extends { Insert: unknown } ? Relation['Insert'] : never>(
values: Row[],
options?: {
Expand All @@ -196,7 +197,7 @@ export default class PostgrestQueryBuilder<
count?: 'exact' | 'planned' | 'estimated'
defaultToNull?: boolean
}
): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships>
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships>
/**
* Perform an UPSERT on the table or view. Depending on the column(s) passed
* to `onConflict`, `.upsert()` allows you to perform the equivalent of
Expand Down Expand Up @@ -248,7 +249,7 @@ export default class PostgrestQueryBuilder<
count?: 'exact' | 'planned' | 'estimated'
defaultToNull?: boolean
} = {}
): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships> {
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships> {
const method = 'POST'

const prefersHeaders = [`resolution=${ignoreDuplicates ? 'ignore' : 'merge'}-duplicates`]
Expand Down Expand Up @@ -312,7 +313,7 @@ export default class PostgrestQueryBuilder<
}: {
count?: 'exact' | 'planned' | 'estimated'
} = {}
): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships> {
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships> {
const method = 'PATCH'
const prefersHeaders = []
if (this.headers['Prefer']) {
Expand Down Expand Up @@ -357,7 +358,7 @@ export default class PostgrestQueryBuilder<
count,
}: {
count?: 'exact' | 'planned' | 'estimated'
} = {}): PostgrestFilterBuilder<Schema, Relation['Row'], null, Relationships> {
} = {}): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships> {
const method = 'DELETE'
const prefersHeaders = []
if (count) {
Expand Down
32 changes: 27 additions & 5 deletions src/PostgrestTransformBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default class PostgrestTransformBuilder<
Schema extends GenericSchema,
Row extends Record<string, unknown>,
Result,
RelationName = unknown,
Relationships = unknown
> extends PostgrestBuilder<Result> {
/**
Expand All @@ -17,9 +18,12 @@ export default class PostgrestTransformBuilder<
*
* @param columns - The columns to retrieve, separated by commas
*/
select<Query extends string = '*', NewResultOne = GetResult<Schema, Row, Relationships, Query>>(
select<
Query extends string = '*',
NewResultOne = GetResult<Schema, Row, RelationName, Relationships, Query>
>(
columns?: Query
): PostgrestTransformBuilder<Schema, Row, NewResultOne[], Relationships> {
): PostgrestTransformBuilder<Schema, Row, NewResultOne[], RelationName, Relationships> {
// Remove whitespaces except when quoted
let quoted = false
const cleanedColumns = (columns ?? '*')
Expand All @@ -39,7 +43,13 @@ export default class PostgrestTransformBuilder<
this.headers['Prefer'] += ','
}
this.headers['Prefer'] += 'return=representation'
return this as unknown as PostgrestTransformBuilder<Schema, Row, NewResultOne[], Relationships>
return this as unknown as PostgrestTransformBuilder<
Schema,
Row,
NewResultOne[],
RelationName,
Relationships
>
}

order<ColumnName extends string & keyof Row>(
Expand Down Expand Up @@ -294,7 +304,19 @@ export default class PostgrestTransformBuilder<
*
* @typeParam NewResult - The new result type to override with
*/
returns<NewResult>(): PostgrestTransformBuilder<Schema, Row, NewResult, Relationships> {
return this as unknown as PostgrestTransformBuilder<Schema, Row, NewResult, Relationships>
returns<NewResult>(): PostgrestTransformBuilder<
Schema,
Row,
NewResult,
RelationName,
Relationships
> {
return this as unknown as PostgrestTransformBuilder<
Schema,
Row,
NewResult,
RelationName,
Relationships
>
}
}
65 changes: 58 additions & 7 deletions src/select-query-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ type HasFKey<FKeyName, Relationships> = Relationships extends [infer R]
: HasFKey<FKeyName, Rest>
: false

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

type HasFKeyToFRel<FRelName, Relationships> = Relationships extends [infer R]
? R extends { referencedRelation: FRelName }
? true
Expand All @@ -76,6 +86,16 @@ type HasFKeyToFRel<FRelName, Relationships> = Relationships extends [infer R]
: HasFKeyToFRel<FRelName, Rest>
: false

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

/**
* Constructs a type definition for a single field of an object.
*
Expand All @@ -86,6 +106,7 @@ type HasFKeyToFRel<FRelName, Relationships> = Relationships extends [infer R]
type ConstructFieldDefinition<
Schema extends GenericSchema,
Row extends Record<string, unknown>,
RelationName,
Relationships,
Field
> = Field extends { star: true }
Expand All @@ -95,13 +116,24 @@ type ConstructFieldDefinition<
[_ in Field['name']]: GetResultHelper<
Schema,
(Schema['Tables'] & Schema['Views'])[Field['original']]['Row'],
Field['original'],
(Schema['Tables'] & Schema['Views'])[Field['original']] extends { Relationships: infer R }
? R
: unknown,
Field['children'],
unknown
> extends infer Child
? Relationships extends unknown[]
? // One-to-one relationship - referencing column(s) has unique/pkey constraint.
HasUniqueFKey<
Field['hint'],
(Schema['Tables'] & Schema['Views'])[Field['original']] extends {
Relationships: infer R
}
? R
: unknown
> extends true
? Child | null
: Relationships extends unknown[]
? HasFKey<Field['hint'], Relationships> extends true
? Child | null
: Child[]
Expand All @@ -113,13 +145,24 @@ type ConstructFieldDefinition<
[_ in Field['name']]: GetResultHelper<
Schema,
(Schema['Tables'] & Schema['Views'])[Field['original']]['Row'],
Field['original'],
(Schema['Tables'] & Schema['Views'])[Field['original']] extends { Relationships: infer R }
? R
: unknown,
Field['children'],
unknown
> extends infer Child
? Relationships extends unknown[]
? // One-to-one relationship - referencing column(s) has unique/pkey constraint.
HasUniqueFKeyToFRel<
RelationName,
(Schema['Tables'] & Schema['Views'])[Field['original']] extends {
Relationships: infer R
}
? R
: unknown
> extends true
? Child | null
: Relationships extends unknown[]
? HasFKeyToFRel<Field['original'], Relationships> extends true
? Child | null
: Child[]
Expand Down Expand Up @@ -404,28 +447,35 @@ type ParseQuery<Query extends string> = string extends Query
type GetResultHelper<
Schema extends GenericSchema,
Row extends Record<string, unknown>,
RelationName,
Relationships,
Fields extends unknown[],
Acc
> = Fields extends [infer R]
? ConstructFieldDefinition<Schema, Row, Relationships, R> extends SelectQueryError<infer E>
? ConstructFieldDefinition<Schema, Row, RelationName, Relationships, R> extends SelectQueryError<
infer E
>
? SelectQueryError<E>
: GetResultHelper<
Schema,
Row,
RelationName,
Relationships,
[],
ConstructFieldDefinition<Schema, Row, Relationships, R> & Acc
ConstructFieldDefinition<Schema, Row, RelationName, Relationships, R> & Acc
>
: Fields extends [infer R, ...infer Rest]
? ConstructFieldDefinition<Schema, Row, Relationships, R> extends SelectQueryError<infer E>
? ConstructFieldDefinition<Schema, Row, RelationName, Relationships, R> extends SelectQueryError<
infer E
>
? SelectQueryError<E>
: GetResultHelper<
Schema,
Row,
RelationName,
Relationships,
Rest,
ConstructFieldDefinition<Schema, Row, Relationships, R> & Acc
ConstructFieldDefinition<Schema, Row, RelationName, Relationships, R> & Acc
>
: Prettify<Acc>

Expand All @@ -438,8 +488,9 @@ type GetResultHelper<
export type GetResult<
Schema extends GenericSchema,
Row extends Record<string, unknown>,
RelationName,
Relationships,
Query extends string
> = ParseQuery<Query> extends unknown[]
? GetResultHelper<Schema, Row, Relationships, ParseQuery<Query>, unknown>
? GetResultHelper<Schema, Row, RelationName, Relationships, ParseQuery<Query>, unknown>
: ParseQuery<Query>
14 changes: 14 additions & 0 deletions test/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,17 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
const res = await postgrest.from('users').select('username, dat')
expectType<PostgrestSingleResponse<SelectQueryError<`Referencing missing column \`dat\``>[]>>(res)
}

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

0 comments on commit f91aa29

Please sign in to comment.