Skip to content
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

feat(types): add inference for embeded joins by functions #614

Draft
wants to merge 11 commits into
base: chore/add-auto-types-gen-and-override-for-testing
Choose a base branch
from
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"test:types:watch": "run-s build && tsd --files 'test/**/*.test-d.ts' --watch",
"db:clean": "cd test/db && docker compose down --volumes",
"db:run": "cd test/db && docker compose up --detach && wait-for-localhost 3000",
"db:generate-test-types": "cd test/db && docker compose up --detach && wait-for-localhost 8080 && curl --location 'http://0.0.0.0:8080/generators/typescript?included_schemas=public,personal&detect_one_to_one_relationships=true' > ../types.generated.ts && sed -i 's/export type Json = .*/export type Json = unknown;/' ../types.generated.ts"
"db:generate-test-types": "cd test/db && docker compose up --detach && wait-for-localhost 8080 && curl --location 'http://0.0.0.0:8080/generators/typescript?included_schemas=public,personal&detect_one_to_one_relationships=true' > ../types.generated.ts"
},
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
Expand Down
47 changes: 35 additions & 12 deletions src/PostgrestClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import PostgrestQueryBuilder from './PostgrestQueryBuilder'
import PostgrestFilterBuilder from './PostgrestFilterBuilder'
import PostgrestBuilder from './PostgrestBuilder'
import { DEFAULT_HEADERS } from './constants'
import { Fetch, GenericSchema } from './types'
import { Fetch, GenericSchema, GenericSetofOption } from './types'
import { IsAny } from './select-query-parser/utils'

/**
* PostgREST client.
Expand Down Expand Up @@ -133,17 +134,39 @@ export default class PostgrestClient<
get?: boolean
count?: 'exact' | 'planned' | 'estimated'
} = {}
): PostgrestFilterBuilder<
Schema,
Fn['Returns'] extends any[]
? Fn['Returns'][number] extends Record<string, unknown>
? Fn['Returns'][number]
: never
: never,
Fn['Returns'],
FnName,
null
> {
// if rpc is called with a typeless client, default to infering everything as any
): IsAny<Fn> extends true
? PostgrestFilterBuilder<
Schema,
Fn['Returns'] extends any[]
? Fn['Returns'][number] extends Record<string, unknown>
? Fn['Returns'][number]
: never
: Fn['Returns'] extends Record<string, unknown>
? Fn['Returns']
: never,
Fn['Returns'],
FnName,
null
>
: PostgrestFilterBuilder<
// otherwise, provide the right params for typed .select chaining
Schema,
Fn['Returns'] extends any[]
? Fn['Returns'][number] extends Record<string, unknown>
? Fn['Returns'][number]
: never
: Fn['Returns'] extends Record<string, unknown>
? Fn['Returns']
: never,
Fn['Returns'],
Fn['SetofOptions'] extends GenericSetofOption ? Fn['SetofOptions']['to'] : FnName,
Fn['SetofOptions'] extends GenericSetofOption
? Fn['SetofOptions']['to'] extends keyof Schema['Tables']
? Schema['Tables'][Fn['SetofOptions']['to']]['Relationships']
: Schema['Views'][Fn['SetofOptions']['to']]['Relationships']
: null
> {
Comment on lines +137 to +169
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note

This part should fix: supabase/supabase-js#1366

Now that we're able to detect embed from function within select, we can also use the same types to do the opposite and make:

.rpc('function_that_return_setof_table_A', {}).select('field_from_table_A')

And infer the right result.

let method: 'HEAD' | 'GET' | 'POST'
const url = new URL(`${this.url}/rpc/${fn}`)
let body: unknown | undefined
Expand Down
5 changes: 3 additions & 2 deletions src/PostgrestTransformBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import PostgrestBuilder from './PostgrestBuilder'
import PostgrestFilterBuilder from './PostgrestFilterBuilder'
import { GetResult } from './select-query-parser/result'
import { GenericSchema, CheckMatchingArrayTypes } from './types'

Expand All @@ -23,7 +24,7 @@ export default class PostgrestTransformBuilder<
NewResultOne = GetResult<Schema, Row, RelationName, Relationships, Query>
>(
columns?: Query
): PostgrestTransformBuilder<Schema, Row, NewResultOne[], RelationName, Relationships> {
): PostgrestFilterBuilder<Schema, Row, NewResultOne[], RelationName, Relationships> {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note

This should fix: supabase/supabase-js#1365

// Remove whitespaces except when quoted
let quoted = false
const cleanedColumns = (columns ?? '*')
Expand All @@ -43,7 +44,7 @@ export default class PostgrestTransformBuilder<
this.headers['Prefer'] += ','
}
this.headers['Prefer'] += 'return=representation'
return this as unknown as PostgrestTransformBuilder<
return this as unknown as PostgrestFilterBuilder<
Schema,
Row,
NewResultOne[],
Expand Down
13 changes: 10 additions & 3 deletions src/select-query-parser/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ export type ProcessEmbeddedResource<
> = ResolveRelationship<Schema, Relationships, Field, CurrentTableOrView> extends infer Resolved
? Resolved extends {
referencedTable: Pick<GenericTable, 'Row' | 'Relationships'>
relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' }
relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' | 'func' }
direction: string
}
? ProcessEmbeddedResourceResult<Schema, Resolved, Field, CurrentTableOrView>
Expand All @@ -328,7 +328,10 @@ type ProcessEmbeddedResourceResult<
Schema extends GenericSchema,
Resolved extends {
referencedTable: Pick<GenericTable, 'Row' | 'Relationships'>
relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' }
relation: GenericRelationship & {
match: 'refrel' | 'col' | 'fkname' | 'func'
isNotNullable?: boolean
}
direction: string
},
Field extends Ast.FieldNode,
Expand All @@ -351,7 +354,11 @@ type ProcessEmbeddedResourceResult<
? ProcessedChildren
: ProcessedChildren[]
: Resolved['relation']['isOneToOne'] extends true
? ProcessedChildren | null
? Resolved['relation']['match'] extends 'func'
? Resolved['relation']['isNotNullable'] extends true
? ProcessedChildren
: ProcessedChildren | null
: ProcessedChildren | null
: ProcessedChildren[]
: // If the relation is a self-reference it'll always be considered as reverse relationship
Resolved['relation']['referencedRelation'] extends CurrentTableOrView
Expand Down
40 changes: 40 additions & 0 deletions src/select-query-parser/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { GenericFunction, GenericSetofOption } from '../types'
import { Ast } from './parser'
import {
AggregateFunctions,
Expand Down Expand Up @@ -452,6 +453,33 @@ export type ResolveForwardRelationship<
from: CurrentTableOrView
type: 'found-by-join-table'
}
: ResolveEmbededFunctionJoinTableRelationship<
Schema,
CurrentTableOrView,
Field['name']
> extends infer FoundEmbededFunctionJoinTableRelation
? FoundEmbededFunctionJoinTableRelation extends GenericSetofOption
? {
referencedTable: TablesAndViews<Schema>[FoundEmbededFunctionJoinTableRelation['to']]
relation: {
foreignKeyName: `${Field['name']}_${CurrentTableOrView}_${FoundEmbededFunctionJoinTableRelation['to']}_forward`
columns: []
isOneToOne: FoundEmbededFunctionJoinTableRelation['isOneToOne'] extends true
? true
: false
referencedColumns: []
referencedRelation: FoundEmbededFunctionJoinTableRelation['to']
} & {
match: 'func'
isNotNullable: FoundEmbededFunctionJoinTableRelation['isNotNullable'] extends true
? true
: false
}
direction: 'forward'
from: CurrentTableOrView
type: 'found-by-embeded-function'
}
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
Expand Down Expand Up @@ -495,6 +523,18 @@ type ResolveJoinTableRelationship<
: never
}[keyof TablesAndViews<Schema>]

type ResolveEmbededFunctionJoinTableRelationship<
Schema extends GenericSchema,
CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
FieldName extends string
> = Schema['Functions'][FieldName] extends GenericFunction
? Schema['Functions'][FieldName]['SetofOptions'] extends GenericSetofOption
? CurrentTableOrView extends Schema['Functions'][FieldName]['SetofOptions']['from']
? Schema['Functions'][FieldName]['SetofOptions']
: false
: false
: false

export type FindJoinTableRelationship<
Schema extends GenericSchema,
CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
Expand Down
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,17 @@ export type GenericNonUpdatableView = {

export type GenericView = GenericUpdatableView | GenericNonUpdatableView

export type GenericSetofOption = {
isOneToOne?: boolean | undefined
isNotNullable?: boolean | undefined
to: string
from: string
}

export type GenericFunction = {
Args: Record<string, unknown>
Returns: unknown
SetofOptions?: GenericSetofOption
}

export type GenericSchema = {
Expand Down
88 changes: 88 additions & 0 deletions test/db/00-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,91 @@ create table public.cornercase (
"column whitespace" text,
array_column text[]
);

-- Function that returns a single user profile for a user
CREATE OR REPLACE FUNCTION public.get_user_profile(user_row users)
RETURNS SETOF user_profiles
LANGUAGE SQL STABLE
ROWS 1
AS $$
SELECT * FROM public.user_profiles WHERE username = user_row.username;
$$;

-- Same definition, but will be used with a type override to pretend this can't ever return null
CREATE OR REPLACE FUNCTION public.get_user_profile_non_nullable(user_row users)
RETURNS SETOF user_profiles
LANGUAGE SQL STABLE
ROWS 1
AS $$
SELECT * FROM public.user_profiles WHERE username = user_row.username;
$$;


CREATE OR REPLACE FUNCTION public.get_messages(channel_row channels)
RETURNS SETOF messages
LANGUAGE SQL STABLE
AS $$
SELECT * FROM public.messages WHERE channel_id = channel_row.id;
$$;

CREATE OR REPLACE FUNCTION public.get_messages(user_row users)
RETURNS SETOF messages
LANGUAGE SQL STABLE
AS $$
SELECT * FROM public.messages WHERE username = user_row.username;
$$;


-- Create a view based on users table
CREATE VIEW public.active_users AS
SELECT * FROM public.users WHERE status = 'ONLINE'::public.user_status;

-- Create a view based on messages table
CREATE VIEW public.recent_messages AS
SELECT * FROM public.messages ORDER BY id DESC LIMIT 100;

-- Function returning messages using scalar as input (username)
CREATE OR REPLACE FUNCTION public.get_messages_by_username(search_username text)
RETURNS SETOF messages
LANGUAGE SQL STABLE
AS $$
SELECT * FROM public.messages WHERE username = search_username;
$$;

-- Function returning messages using table row as input
CREATE OR REPLACE FUNCTION public.get_user_messages(user_row users)
RETURNS SETOF messages
LANGUAGE SQL STABLE
AS $$
SELECT * FROM public.messages WHERE username = user_row.username;
$$;

-- Function returning messages using view row as input
CREATE OR REPLACE FUNCTION public.get_active_user_messages(active_user_row active_users)
RETURNS SETOF messages
LANGUAGE SQL STABLE
AS $$
SELECT * FROM public.messages WHERE username = active_user_row.username;
$$;

-- Function returning view using scalar as input
CREATE OR REPLACE FUNCTION public.get_recent_messages_by_username(search_username text)
RETURNS SETOF recent_messages
LANGUAGE SQL STABLE
AS $$
SELECT * FROM public.recent_messages WHERE username = search_username;
$$;

-- Function returning view using table row as input
CREATE OR REPLACE FUNCTION public.get_user_recent_messages(user_row users)
RETURNS SETOF recent_messages
LANGUAGE SQL STABLE
AS $$
SELECT * FROM public.recent_messages WHERE username = user_row.username;
$$;
CREATE OR REPLACE FUNCTION public.get_user_recent_messages(active_user_row active_users)
RETURNS SETOF recent_messages
LANGUAGE SQL STABLE
AS $$
SELECT * FROM public.recent_messages WHERE username = active_user_row.username;
$$;
1 change: 0 additions & 1 deletion test/db/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# docker-compose.yml

version: '3'
services:
rest:
image: postgrest/postgrest:v12.2.0
Expand Down
Loading
Loading