diff --git a/packages/core/postgrest-js/package.json b/packages/core/postgrest-js/package.json index 27cf8a7c9..d71ad18bd 100644 --- a/packages/core/postgrest-js/package.json +++ b/packages/core/postgrest-js/package.json @@ -37,6 +37,7 @@ "format": "node scripts/format.js", "format:check": "node scripts/format.js check", "build": "npm run clean && npm run build:cjs && npm run build:esm", + "postbuild": "cp -rf ./src/types/common ../supabase-js/src/lib/rest/types/common", "build:cjs": "tsc -p tsconfig.json", "build:esm": "cpy wrapper.mjs dist/esm/", "docs": "typedoc src/index.ts --out docs/v2", @@ -53,7 +54,7 @@ "type-check:test": "tsc --noEmit --project tsconfig.test.json", "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 && node ../scripts/update-json-type.js && cd ../../ && npm run format" + "db:generate-test-types": "cd test/db && docker compose up --detach && wait-for-localhost 8080 && wait-for-localhost 3000 && curl --location 'http://0.0.0.0:8080/generators/typescript?included_schemas=public,personal&detect_one_to_one_relationships=true' > ../types.generated.ts && node ../../scripts/update-json-type.js && cd ../../ && npm run format" }, "dependencies": { "@supabase/node-fetch": "2.6.15" diff --git a/packages/core/postgrest-js/test/scripts/update-json-type.js b/packages/core/postgrest-js/scripts/update-json-type.js similarity index 85% rename from packages/core/postgrest-js/test/scripts/update-json-type.js rename to packages/core/postgrest-js/scripts/update-json-type.js index fe82b446d..16ccb5f82 100644 --- a/packages/core/postgrest-js/test/scripts/update-json-type.js +++ b/packages/core/postgrest-js/scripts/update-json-type.js @@ -8,7 +8,7 @@ const path = require('path') * This is a cross-platform replacement for the sed command */ function updateJsonType() { - const filePath = path.join(__dirname, '..', 'types.generated.ts') + const filePath = path.join(__dirname, '..', 'test', 'types.generated.ts') try { // Read the file @@ -23,7 +23,7 @@ function updateJsonType() { // Write the updated content back to the file fs.writeFileSync(filePath, updatedContent, 'utf8') - console.log('✅ Successfully updated Json type in types.generated.ts') + console.log('✅ Successfully updated Json type in test/types.generated.ts') } catch (error) { console.error('❌ Error updating Json type:', error.message) process.exit(1) diff --git a/packages/core/postgrest-js/src/PostgrestBuilder.ts b/packages/core/postgrest-js/src/PostgrestBuilder.ts index 97df0601e..44bb7a072 100644 --- a/packages/core/postgrest-js/src/PostgrestBuilder.ts +++ b/packages/core/postgrest-js/src/PostgrestBuilder.ts @@ -2,14 +2,13 @@ import nodeFetch from '@supabase/node-fetch' import type { - Fetch, PostgrestSingleResponse, PostgrestResponseSuccess, CheckMatchingArrayTypes, MergePartialResult, IsValidResultOverride, - ClientServerOptions, -} from './types' +} from './types/types' +import { ClientServerOptions, Fetch } from './types/common/common' import PostgrestError from './PostgrestError' import { ContainsNull } from './select-query-parser/types' diff --git a/packages/core/postgrest-js/src/PostgrestClient.ts b/packages/core/postgrest-js/src/PostgrestClient.ts index 6c3466ac0..a743c65e8 100644 --- a/packages/core/postgrest-js/src/PostgrestClient.ts +++ b/packages/core/postgrest-js/src/PostgrestClient.ts @@ -1,6 +1,7 @@ import PostgrestQueryBuilder from './PostgrestQueryBuilder' import PostgrestFilterBuilder from './PostgrestFilterBuilder' -import { Fetch, GenericSchema, ClientServerOptions } from './types' +import { Fetch, GenericSchema, ClientServerOptions } from './types/common/common' +import { GetRpcFunctionFilterBuilderByArgs } from './types/common/rpc' /** * PostgREST client. @@ -131,9 +132,17 @@ export default class PostgrestClient< * `"estimated"`: Uses exact count for low numbers and planned count for high * numbers. */ - rpc( + rpc< + FnName extends string & keyof Schema['Functions'], + Args extends Schema['Functions'][FnName]['Args'] = never, + FilterBuilder extends GetRpcFunctionFilterBuilderByArgs< + Schema, + FnName, + Args + > = GetRpcFunctionFilterBuilderByArgs, + >( fn: FnName, - args: Fn['Args'] = {}, + args: Args = {} as Args, { head = false, get = false, @@ -146,14 +155,10 @@ export default class PostgrestClient< ): PostgrestFilterBuilder< ClientOptions, Schema, - Fn['Returns'] extends any[] - ? Fn['Returns'][number] extends Record - ? Fn['Returns'][number] - : never - : never, - Fn['Returns'], - FnName, - null, + FilterBuilder['Row'], + FilterBuilder['Result'], + FilterBuilder['RelationName'], + FilterBuilder['Relationships'], 'RPC' > { let method: 'HEAD' | 'GET' | 'POST' diff --git a/packages/core/postgrest-js/src/PostgrestFilterBuilder.ts b/packages/core/postgrest-js/src/PostgrestFilterBuilder.ts index 6683121b4..0a6350416 100644 --- a/packages/core/postgrest-js/src/PostgrestFilterBuilder.ts +++ b/packages/core/postgrest-js/src/PostgrestFilterBuilder.ts @@ -1,6 +1,6 @@ import PostgrestTransformBuilder from './PostgrestTransformBuilder' import { JsonPathToAccessor, JsonPathToType } from './select-query-parser/utils' -import { ClientServerOptions, GenericSchema } from './types' +import { ClientServerOptions, GenericSchema } from './types/common/common' type FilterOperator = | 'eq' diff --git a/packages/core/postgrest-js/src/PostgrestQueryBuilder.ts b/packages/core/postgrest-js/src/PostgrestQueryBuilder.ts index d552954d1..a82310078 100644 --- a/packages/core/postgrest-js/src/PostgrestQueryBuilder.ts +++ b/packages/core/postgrest-js/src/PostgrestQueryBuilder.ts @@ -1,6 +1,12 @@ import PostgrestFilterBuilder from './PostgrestFilterBuilder' import { GetResult } from './select-query-parser/result' -import { ClientServerOptions, Fetch, GenericSchema, GenericTable, GenericView } from './types' +import { + ClientServerOptions, + Fetch, + GenericSchema, + GenericTable, + GenericView, +} from './types/common/common' export default class PostgrestQueryBuilder< ClientOptions extends ClientServerOptions, diff --git a/packages/core/postgrest-js/src/PostgrestTransformBuilder.ts b/packages/core/postgrest-js/src/PostgrestTransformBuilder.ts index 0d2d8f474..cf94aea82 100644 --- a/packages/core/postgrest-js/src/PostgrestTransformBuilder.ts +++ b/packages/core/postgrest-js/src/PostgrestTransformBuilder.ts @@ -1,12 +1,8 @@ import PostgrestBuilder from './PostgrestBuilder' -import { InvalidMethodError } from './PostgrestFilterBuilder' +import PostgrestFilterBuilder, { InvalidMethodError } from './PostgrestFilterBuilder' import { GetResult } from './select-query-parser/result' -import { - GenericSchema, - CheckMatchingArrayTypes, - ClientServerOptions, - MaxAffectedEnabled, -} from './types' +import { CheckMatchingArrayTypes, MaxAffectedEnabled } from './types/types' +import { ClientServerOptions, GenericSchema } from './types/common/common' export default class PostgrestTransformBuilder< ClientOptions extends ClientServerOptions, @@ -31,11 +27,15 @@ export default class PostgrestTransformBuilder< NewResultOne = GetResult, >( columns?: Query - ): PostgrestTransformBuilder< + ): PostgrestFilterBuilder< ClientOptions, Schema, Row, - NewResultOne[], + Method extends 'RPC' + ? Result extends unknown[] + ? NewResultOne[] + : NewResultOne + : NewResultOne[], RelationName, Relationships, Method @@ -56,11 +56,15 @@ export default class PostgrestTransformBuilder< .join('') this.url.searchParams.set('select', cleanedColumns) this.headers.append('Prefer', 'return=representation') - return this as unknown as PostgrestTransformBuilder< + return this as unknown as PostgrestFilterBuilder< ClientOptions, Schema, Row, - NewResultOne[], + Method extends 'RPC' + ? Result extends unknown[] + ? NewResultOne[] + : NewResultOne + : NewResultOne[], RelationName, Relationships, Method diff --git a/packages/core/postgrest-js/src/index.ts b/packages/core/postgrest-js/src/index.ts index 8c435ddac..83772cd56 100644 --- a/packages/core/postgrest-js/src/index.ts +++ b/packages/core/postgrest-js/src/index.ts @@ -28,8 +28,8 @@ export type { PostgrestResponseSuccess, PostgrestSingleResponse, PostgrestMaybeSingleResponse, - ClientServerOptions as PostgrestClientOptions, -} from './types' +} from './types/types' +export type { ClientServerOptions as PostgrestClientOptions } from './types/common/common' // https://github.com/supabase/postgrest-js/issues/551 // To be replaced with a helper type that only uses public types export type { GetResult as UnstableGetResult } from './select-query-parser/result' diff --git a/packages/core/postgrest-js/src/select-query-parser/parser.ts b/packages/core/postgrest-js/src/select-query-parser/parser.ts index b00060621..1f87aa834 100644 --- a/packages/core/postgrest-js/src/select-query-parser/parser.ts +++ b/packages/core/postgrest-js/src/select-query-parser/parser.ts @@ -1,7 +1,7 @@ // Credits to @bnjmnt4n (https://www.npmjs.com/package/postgrest-query) // See https://github.com/PostgREST/postgrest/blob/2f91853cb1de18944a4556df09e52450b881cfb3/src/PostgREST/ApiRequest/QueryParams.hs#L282-L284 -import { SimplifyDeep } from '../types' +import { SimplifyDeep } from '../types/types' import { JsonPathToAccessor } from './utils' /** diff --git a/packages/core/postgrest-js/src/select-query-parser/result.ts b/packages/core/postgrest-js/src/select-query-parser/result.ts index 72094876a..e2c980b61 100644 --- a/packages/core/postgrest-js/src/select-query-parser/result.ts +++ b/packages/core/postgrest-js/src/select-query-parser/result.ts @@ -1,5 +1,3 @@ -import { ClientServerOptions, GenericTable } from '../types' -import { ContainsNull, GenericRelationship, PostgreSQLTypes } from './types' import { Ast, ParseQuery } from './parser' import { AggregateFunctions, @@ -9,6 +7,11 @@ import { Prettify, TablesAndViews, TypeScriptTypes, + ContainsNull, + GenericRelationship, + PostgreSQLTypes, + GenericTable, + ClientServerOptions, } from './types' import { CheckDuplicateEmbededReference, @@ -366,7 +369,7 @@ export type ProcessEmbeddedResource< ResolveRelationship extends infer Resolved ? Resolved extends { referencedTable: Pick - relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' } + relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' | 'func' } direction: string } ? ProcessEmbeddedResourceResult @@ -385,7 +388,12 @@ type ProcessEmbeddedResourceResult< Schema extends GenericSchema, Resolved extends { referencedTable: Pick - relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' } + relation: GenericRelationship & { + match: 'refrel' | 'col' | 'fkname' | 'func' + isNotNullable?: boolean + referencedRelation: string + isSetofReturn?: boolean + } direction: string }, Field extends Ast.FieldNode, @@ -395,7 +403,11 @@ type ProcessEmbeddedResourceResult< ClientOptions, Schema, Resolved['referencedTable']['Row'], - Field['name'], + // For embeded function selection, the source of truth is the 'referencedRelation' + // coming from the SetofOptions.to parameter + Resolved['relation']['match'] extends 'func' + ? Resolved['relation']['referencedRelation'] + : Field['name'], Resolved['referencedTable']['Relationships'], Field['children'] extends undefined ? [] @@ -410,7 +422,18 @@ type ProcessEmbeddedResourceResult< ? ProcessedChildren : ProcessedChildren[] : Resolved['relation']['isOneToOne'] extends true - ? ProcessedChildren | null + ? Resolved['relation']['match'] extends 'func' + ? Resolved['relation']['isNotNullable'] extends true + ? Resolved['relation']['isSetofReturn'] extends true + ? ProcessedChildren + : // TODO: This shouldn't be necessary but is due in an inconsitency in PostgREST v12/13 where if a function + // is declared with RETURNS instead of RETURNS SETOF ROWS 1 + // In case where there is no object matching the relations, the object will be returned with all the properties within it + // set to null, we mimic this buggy behavior for type safety an issue is opened on postgREST here: + // https://github.com/PostgREST/postgrest/issues/4234 + { [P in keyof ProcessedChildren]: ProcessedChildren[P] | null } + : 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 diff --git a/packages/core/postgrest-js/src/select-query-parser/types.ts b/packages/core/postgrest-js/src/select-query-parser/types.ts index 9b0ed49c1..7df05dfce 100644 --- a/packages/core/postgrest-js/src/select-query-parser/types.ts +++ b/packages/core/postgrest-js/src/select-query-parser/types.ts @@ -1,6 +1,22 @@ -import type { GenericRelationship, GenericSchema, GenericTable, Prettify } from '../types' - -export type { GenericRelationship, GenericSchema, GenericTable, Prettify } +import type { + GenericRelationship, + GenericSchema, + GenericTable, + ClientServerOptions, + GenericSetofOption, + GenericFunction, +} from '../types/common/common' +import type { Prettify } from '../types/types' + +export type { + GenericRelationship, + GenericSchema, + GenericTable, + ClientServerOptions, + GenericSetofOption, + Prettify, + GenericFunction, +} export type AggregateWithoutColumnFunctions = 'count' diff --git a/packages/core/postgrest-js/src/select-query-parser/utils.ts b/packages/core/postgrest-js/src/select-query-parser/utils.ts index f5fc5c61a..b766454d4 100644 --- a/packages/core/postgrest-js/src/select-query-parser/utils.ts +++ b/packages/core/postgrest-js/src/select-query-parser/utils.ts @@ -8,6 +8,8 @@ import { IsNonEmptyArray, TablesAndViews, UnionToArray, + GenericFunction, + GenericSetofOption, } from './types' export type IsAny = 0 extends 1 & T ? true : false @@ -457,7 +459,37 @@ export type ResolveForwardRelationship< from: CurrentTableOrView type: 'found-by-join-table' } - : SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`> + : ResolveEmbededFunctionJoinTableRelationship< + Schema, + CurrentTableOrView, + Field['name'] + > extends infer FoundEmbededFunctionJoinTableRelation + ? FoundEmbededFunctionJoinTableRelation extends GenericSetofOption + ? { + referencedTable: TablesAndViews[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 + : FoundEmbededFunctionJoinTableRelation['isSetofReturn'] extends true + ? false + : true + isSetofReturn: FoundEmbededFunctionJoinTableRelation['isSetofReturn'] + } + 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']}`> : SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`> @@ -500,6 +532,20 @@ type ResolveJoinTableRelationship< : never }[keyof TablesAndViews] +type ResolveEmbededFunctionJoinTableRelationship< + Schema extends GenericSchema, + CurrentTableOrView extends keyof TablesAndViews & string, + FieldName extends string, +> = + FindMatchingFunctionBySetofFrom< + Schema['Functions'][FieldName], + CurrentTableOrView + > extends infer Fn + ? Fn extends GenericFunction + ? Fn['SetofOptions'] + : false + : false + export type FindJoinTableRelationship< Schema extends GenericSchema, CurrentTableOrView extends keyof TablesAndViews & string, @@ -589,6 +635,22 @@ export type IsStringUnion = string extends T : true : false +type MatchingFunctionBySetofFrom< + Fn extends GenericFunction, + TableName extends string, +> = Fn['SetofOptions'] extends GenericSetofOption + ? TableName extends Fn['SetofOptions']['from'] + ? Fn + : never + : false + +type FindMatchingFunctionBySetofFrom< + FnUnion, + TableName extends string, +> = FnUnion extends infer Fn extends GenericFunction + ? MatchingFunctionBySetofFrom + : false + type ComputedField< Schema extends GenericSchema, RelationName extends keyof TablesAndViews, diff --git a/packages/core/postgrest-js/src/types/common/common.ts b/packages/core/postgrest-js/src/types/common/common.ts new file mode 100644 index 000000000..9ad962ef5 --- /dev/null +++ b/packages/core/postgrest-js/src/types/common/common.ts @@ -0,0 +1,56 @@ +// Types that are shared between supabase-js and postgrest-js + +export type Fetch = typeof fetch + +export type GenericRelationship = { + foreignKeyName: string + columns: string[] + isOneToOne?: boolean + referencedRelation: string + referencedColumns: string[] +} + +export type GenericTable = { + Row: Record + Insert: Record + Update: Record + Relationships: GenericRelationship[] +} + +export type GenericUpdatableView = { + Row: Record + Insert: Record + Update: Record + Relationships: GenericRelationship[] +} + +export type GenericNonUpdatableView = { + Row: Record + Relationships: GenericRelationship[] +} + +export type GenericView = GenericUpdatableView | GenericNonUpdatableView + +export type GenericSetofOption = { + isSetofReturn?: boolean | undefined + isOneToOne?: boolean | undefined + isNotNullable?: boolean | undefined + to: string + from: string +} + +export type GenericFunction = { + Args: Record | never + Returns: unknown + SetofOptions?: GenericSetofOption +} + +export type GenericSchema = { + Tables: Record + Views: Record + Functions: Record +} + +export type ClientServerOptions = { + PostgrestVersion?: string +} diff --git a/packages/core/postgrest-js/src/types/common/rpc.ts b/packages/core/postgrest-js/src/types/common/rpc.ts new file mode 100644 index 000000000..52e57419a --- /dev/null +++ b/packages/core/postgrest-js/src/types/common/rpc.ts @@ -0,0 +1,135 @@ +import type { GenericFunction, GenericSchema, GenericSetofOption } from './common' + +// Functions matching utils +type IsMatchingArgs< + FnArgs extends GenericFunction['Args'], + PassedArgs extends GenericFunction['Args'], +> = [FnArgs] extends [Record] + ? PassedArgs extends Record + ? true + : false + : keyof PassedArgs extends keyof FnArgs + ? PassedArgs extends FnArgs + ? true + : false + : false + +type MatchingFunctionArgs< + Fn extends GenericFunction, + Args extends GenericFunction['Args'], +> = Fn extends { Args: infer A extends GenericFunction['Args'] } + ? IsMatchingArgs extends true + ? Fn + : never + : false + +type FindMatchingFunctionByArgs< + FnUnion, + Args extends GenericFunction['Args'], +> = FnUnion extends infer Fn extends GenericFunction ? MatchingFunctionArgs : false + +// Types for working with database schemas +type TablesAndViews = Schema['Tables'] & Exclude + +// Utility types for working with unions +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void + ? I + : never + +type LastOf = + UnionToIntersection T : never> extends () => infer R ? R : never + +type IsAny = 0 extends 1 & T ? true : false + +type ExactMatch = [T] extends [S] ? ([S] extends [T] ? true : false) : false + +type ExtractExactFunction = Fns extends infer F + ? F extends GenericFunction + ? ExactMatch extends true + ? F + : never + : never + : never + +type IsNever = [T] extends [never] ? true : false + +type RpcFunctionNotFound = { + Row: any + Result: { + error: true + } & "Couldn't infer function definition matching provided arguments" + RelationName: FnName + Relationships: null +} + +export type GetRpcFunctionFilterBuilderByArgs< + Schema extends GenericSchema, + FnName extends string & keyof Schema['Functions'], + Args, +> = { + 0: Schema['Functions'][FnName] + // If the Args is exactly never (function call without any params) + 1: IsAny extends true + ? any + : IsNever extends true + ? // This is for retro compatibility, if the funcition is defined with an single return and an union of Args + // we fallback to the last function definition matched by name + IsNever> extends true + ? LastOf + : ExtractExactFunction + : Args extends Record + ? LastOf + : // Otherwise, we attempt to match with one of the function definition in the union based + // on the function arguments provided + Args extends GenericFunction['Args'] + ? // This is for retro compatibility, if the funcition is defined with an single return and an union of Args + // we fallback to the last function definition matched by name + IsNever< + LastOf> + > extends true + ? LastOf + : // Otherwise, we use the arguments based function definition narrowing to get the right value + LastOf> + : // If we can't find a matching function by args, we try to find one by function name + ExtractExactFunction extends GenericFunction + ? ExtractExactFunction + : any +}[1] extends infer Fn + ? // If we are dealing with an non-typed client everything is any + IsAny extends true + ? { Row: any; Result: any; RelationName: FnName; Relationships: null } + : // Otherwise, we use the arguments based function definition narrowing to get the rigt value + Fn extends GenericFunction + ? { + Row: Fn['SetofOptions'] extends GenericSetofOption + ? Fn['SetofOptions']['isSetofReturn'] extends true + ? TablesAndViews[Fn['SetofOptions']['to']]['Row'] + : TablesAndViews[Fn['SetofOptions']['to']]['Row'] + : Fn['Returns'] extends any[] + ? Fn['Returns'][number] extends Record + ? Fn['Returns'][number] + : never + : Fn['Returns'] extends Record + ? Fn['Returns'] + : never + Result: Fn['SetofOptions'] extends GenericSetofOption + ? Fn['SetofOptions']['isSetofReturn'] extends true + ? Fn['SetofOptions']['isOneToOne'] extends true + ? Fn['Returns'][] + : Fn['Returns'] + : Fn['Returns'] + : Fn['Returns'] + RelationName: Fn['SetofOptions'] extends GenericSetofOption + ? Fn['SetofOptions']['to'] + : FnName + Relationships: Fn['SetofOptions'] extends GenericSetofOption + ? Fn['SetofOptions']['to'] extends keyof Schema['Tables'] + ? Schema['Tables'][Fn['SetofOptions']['to']]['Relationships'] + : Schema['Views'][Fn['SetofOptions']['to']]['Relationships'] + : null + } + : // If we failed to find the function by argument, we still pass with any but also add an overridable + Fn extends false + ? RpcFunctionNotFound + : RpcFunctionNotFound + : RpcFunctionNotFound diff --git a/packages/core/postgrest-js/src/types.ts b/packages/core/postgrest-js/src/types/types.ts similarity index 83% rename from packages/core/postgrest-js/src/types.ts rename to packages/core/postgrest-js/src/types/types.ts index 6dceb00b2..842810ece 100644 --- a/packages/core/postgrest-js/src/types.ts +++ b/packages/core/postgrest-js/src/types/types.ts @@ -1,8 +1,7 @@ -import PostgrestError from './PostgrestError' -import { ContainsNull } from './select-query-parser/types' -import { IsAny, SelectQueryError } from './select-query-parser/utils' - -export type Fetch = typeof fetch +import PostgrestError from '../PostgrestError' +import { ContainsNull } from '../select-query-parser/types' +import { SelectQueryError } from '../select-query-parser/utils' +import { ClientServerOptions } from './common/common' /** * Response format @@ -31,50 +30,6 @@ export type PostgrestSingleResponse = PostgrestResponseSuccess | Postgrest export type PostgrestMaybeSingleResponse = PostgrestSingleResponse export type PostgrestResponse = PostgrestSingleResponse -export type GenericRelationship = { - foreignKeyName: string - columns: string[] - isOneToOne?: boolean - referencedRelation: string - referencedColumns: string[] -} - -export type GenericTable = { - Row: Record - Insert: Record - Update: Record - Relationships: GenericRelationship[] -} - -export type GenericUpdatableView = { - Row: Record - Insert: Record - Update: Record - Relationships: GenericRelationship[] -} - -export type GenericNonUpdatableView = { - Row: Record - Relationships: GenericRelationship[] -} - -export type GenericView = GenericUpdatableView | GenericNonUpdatableView - -export type GenericFunction = { - Args: Record - Returns: unknown -} - -export type GenericSchema = { - Tables: Record - Views: Record - Functions: Record -} - -export type ClientServerOptions = { - PostgrestVersion?: string -} - export type DatabaseWithOptions = { db: Database options: Options diff --git a/packages/core/postgrest-js/test/advanced_rpc.test.ts b/packages/core/postgrest-js/test/advanced_rpc.test.ts new file mode 100644 index 000000000..fbbc145b4 --- /dev/null +++ b/packages/core/postgrest-js/test/advanced_rpc.test.ts @@ -0,0 +1,1380 @@ +import { PostgrestClient } from '../src/index' +import { Database } from './types.override' +import { TypeEqual, expectType } from './types' +import { SelectQueryError } from '../src/select-query-parser/utils' +import { z } from 'zod' +import { RequiredDeep } from 'type-fest' + +const REST_URL = 'http://localhost:3000' +const postgrest = new PostgrestClient(REST_URL) + +const MessagesWithoutBlurbSchema = z.object({ + channel_id: z.number(), + data: z.unknown().nullable(), + id: z.number(), + message: z.string().nullable(), + username: z.string(), +}) + +const UserProfileSchema = z.object({ + id: z.number(), + username: z.string().nullable(), +}) + +const RecentMessagesSchema = z.object({ + channel_id: z.number().nullable(), + data: z.unknown().nullable(), + id: z.number().nullable(), + message: z.string().nullable(), + username: z.string().nullable(), +}) + +const SelectWithUsersSchema = z.object({ + channel_id: z.number().nullable(), + message: z.string().nullable(), + users: z + .object({ + catchphrase: z.unknown(), + username: z.string(), + }) + .nullable(), +}) + +const SelectWithUsersProfileSchema = z.object({ + id: z.number(), + username: z.string().nullable(), + users: z + .object({ + catchphrase: z.unknown(), + username: z.string(), + }) + .nullable(), +}) + +const FunctionReturningRowSchema = z.object({ + age_range: z.unknown(), + catchphrase: z.unknown(), + data: z.unknown(), + status: z.enum(['ONLINE', 'OFFLINE'] as const).nullable(), + username: z.string(), +}) + +describe('advanced rpc', () => { + test('function returning a setof embeded table', async () => { + const res = await postgrest.rpc('get_messages', { + channel_row: { id: 1, data: null, slug: null }, + }) + let result: Exclude + const ExpectedSchema = z.array(MessagesWithoutBlurbSchema) + let expected: RequiredDeep> + expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + ExpectedSchema.parse(res.data) + }) + + test('function double definition returning a setof embeded table', async () => { + const res = await postgrest.rpc('get_messages', { + user_row: { + username: 'supabot', + data: null, + age_range: null, + catchphrase: null, + status: 'ONLINE', + }, + }) + let result: Exclude + const ExpectedSchema = z.array(MessagesWithoutBlurbSchema) + let expected: RequiredDeep> + expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 3, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + ExpectedSchema.parse(res.data) + }) + + test('function returning a single row embeded table', async () => { + const res = await postgrest.rpc('get_user_profile', { + user_row: { + username: 'supabot', + data: null, + age_range: null, + catchphrase: null, + status: 'ONLINE', + }, + }) + let result: Exclude + let expected: z.infer + expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "id": 1, + "username": "supabot", + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) + UserProfileSchema.parse(res.data) + }) + + test('function with scalar input', async () => { + const res = await postgrest.rpc('get_messages_by_username', { + search_username: 'supabot', + }) + // Type assertion + let result: Exclude + const ExpectedSchema = z.array(MessagesWithoutBlurbSchema) + let expected: RequiredDeep> + expectType>(true) + // Runtime result + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 3, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + ExpectedSchema.parse(res.data) + }) + + test('function with table row input', async () => { + const res = await postgrest.rpc('get_user_messages', { + user_row: { + username: 'supabot', + data: null, + age_range: null, + catchphrase: null, + status: 'ONLINE', + }, + }) + let result: Exclude + const ExpectedSchema = z.array(MessagesWithoutBlurbSchema) + let expected: RequiredDeep> + expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 3, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + ExpectedSchema.parse(res.data) + }) + + test('function with view row input', async () => { + const res = await postgrest.rpc('get_active_user_messages', { + active_user_row: { + username: 'supabot', + data: null, + age_range: null, + catchphrase: null, + status: 'ONLINE', + }, + }) + let result: Exclude + const ExpectedSchema = z.array(MessagesWithoutBlurbSchema) + let expected: RequiredDeep> + expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 3, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + ExpectedSchema.parse(res.data) + }) + + test('function returning view', async () => { + const res = await postgrest.rpc('get_user_recent_messages', { + user_row: { + username: 'supabot', + data: null, + age_range: null, + catchphrase: null, + status: 'ONLINE', + }, + }) + let result: Exclude + let expected: RequiredDeep>[] + expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 3, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + RecentMessagesSchema.array().parse(res.data) + }) + + test('function with scalar input returning view', async () => { + const res = await postgrest.rpc('get_recent_messages_by_username', { + search_username: 'supabot', + }) + let result: Exclude + let expected: RequiredDeep>[] + expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 3, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + RecentMessagesSchema.array().parse(res.data) + }) + + test('function with scalar input with followup select', async () => { + const res = await postgrest + .rpc('get_recent_messages_by_username', { + search_username: 'supabot', + }) + .select('channel_id, message, users(username, catchphrase)') + let result: Exclude + let expected: RequiredDeep>[] + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "channel_id": 3, + "message": "Some message on channel wihtout details", + "users": Object { + "catchphrase": "'cat' 'fat'", + "username": "supabot", + }, + }, + Object { + "channel_id": 3, + "message": "Some message on channel wihtout details", + "users": Object { + "catchphrase": "'cat' 'fat'", + "username": "supabot", + }, + }, + Object { + "channel_id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "users": Object { + "catchphrase": "'cat' 'fat'", + "username": "supabot", + }, + }, + Object { + "channel_id": 1, + "message": "Hello World 👋", + "users": Object { + "catchphrase": "'cat' 'fat'", + "username": "supabot", + }, + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + SelectWithUsersSchema.array().parse(res.data) + }) + + test('function with row input with followup select', async () => { + const res = await postgrest + .rpc('get_user_profile', { + user_row: { + username: 'supabot', + data: null, + age_range: null, + catchphrase: null, + status: 'ONLINE', + }, + }) + .select('id, username, users(username, catchphrase)') + let result: Exclude + let expected: RequiredDeep> + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "id": 1, + "username": "supabot", + "users": Object { + "catchphrase": "'cat' 'fat'", + "username": "supabot", + }, + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) + SelectWithUsersProfileSchema.parse(res.data) + }) + + test('unresolvable function with no params', async () => { + const res = await postgrest.rpc('postgrest_unresolvable_function') + let result: Exclude + let expected: undefined + expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": null, + "status": 204, + "statusText": "No Content", + } + `) + }) + + test('unresolvable function with text param', async () => { + const res = await postgrest.rpc('postgrest_unresolvable_function', { + a: 'test', + }) + let result: Exclude + // Should be an error response due to ambiguous function resolution + let expected: SelectQueryError<'Could not choose the best candidate function between: public.postgrest_unresolvable_function(a => int4), public.postgrest_unresolvable_function(a => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved'> + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST203", + "details": null, + "hint": "Try renaming the parameters or the function itself in the database so function overloading can be resolved", + "message": "Could not choose the best candidate function between: public.postgrest_unresolvable_function(a => integer), public.postgrest_unresolvable_function(a => text)", + }, + "status": 300, + "statusText": "Multiple Choices", + } + `) + }) + + test('unresolvable function with int param', async () => { + const res = await postgrest.rpc('postgrest_unresolvable_function', { + a: 1, + }) + let result: Exclude + // Should be an error response due to ambiguous function resolution + let expected: SelectQueryError<'Could not choose the best candidate function between: public.postgrest_unresolvable_function(a => int4), public.postgrest_unresolvable_function(a => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved'> + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST203", + "details": null, + "hint": "Try renaming the parameters or the function itself in the database so function overloading can be resolved", + "message": "Could not choose the best candidate function between: public.postgrest_unresolvable_function(a => integer), public.postgrest_unresolvable_function(a => text)", + }, + "status": 300, + "statusText": "Multiple Choices", + } + `) + }) + + test('resolvable function with no params', async () => { + const res = await postgrest.rpc('postgrest_resolvable_with_override_function') + let result: Exclude + let expected: undefined + expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": null, + "status": 204, + "statusText": "No Content", + } + `) + }) + + test('resolvable function with text param', async () => { + const res = await postgrest.rpc('postgrest_resolvable_with_override_function', { + a: 'test', + }) + let result: Exclude + let expected: number + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": 1, + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('resolvable function with int param', async () => { + const res = await postgrest.rpc('postgrest_resolvable_with_override_function', { + b: 1, + }) + let result: Exclude + let expected: string + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": "foo", + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('resolvable function with profile_id param', async () => { + const res = await postgrest.rpc('postgrest_resolvable_with_override_function', { + profile_id: 1, + }) + let result: Exclude + let expected: z.infer[] + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "id": 1, + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + UserProfileSchema.array().parse(res.data) + }) + + test('resolvable function with channel_id and search params', async () => { + const res = await postgrest.rpc('postgrest_resolvable_with_override_function', { + cid: 1, + search: 'Hello World 👋', + }) + let result: Exclude + const ExpectedSchema = z.array(MessagesWithoutBlurbSchema) + let expected: RequiredDeep> + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + ExpectedSchema.parse(res.data) + }) + + test('resolvable function with user_row param', async () => { + const res = await postgrest.rpc('postgrest_resolvable_with_override_function', { + user_row: { + username: 'supabot', + data: null, + age_range: null, + catchphrase: null, + status: 'ONLINE', + }, + }) + let result: Exclude + const ExpectedSchema = z.array(MessagesWithoutBlurbSchema) + let expected: RequiredDeep> + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 3, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + ExpectedSchema.parse(res.data) + }) + + test('polymorphic function with text param', async () => { + const res = await postgrest.rpc('polymorphic_function_with_different_return', { + '': 'test', + }) + let result: Exclude + let expected: string + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": "foo", + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with bool param', async () => { + const res = await postgrest.rpc('polymorphic_function_with_different_return', { + // TODO: works with latest postgrest-meta type introspection + ////@ts-expect-error Type 'boolean' is not assignable to type 'string' + '': true, + }) + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": "foo", + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with unnamed int param', async () => { + const res = await postgrest.rpc( + // TODO: works with latest postgrest-meta type introspection + ////@ts-expect-error Argument of type '"polymorphic_function_with_unnamed_integer"' is not assignable to parameter of type '"blurb_message" | "function_returning_row" | "function_returning_set_of_rows" + 'polymorphic_function_with_unnamed_integer', + { + '': 1, + } + ) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST202", + "details": "Searched for the function public.polymorphic_function_with_unnamed_integer with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache.", + "hint": "Perhaps you meant to call the function public.polymorphic_function_with_unnamed_text", + "message": "Could not find the function public.polymorphic_function_with_unnamed_integer() in the schema cache", + }, + "status": 404, + "statusText": "Not Found", + } + `) + }) + + test('polymorphic function with unnamed json param', async () => { + const res = await postgrest.rpc('polymorphic_function_with_unnamed_json', { + '': { test: 'value' }, + }) + let result: Exclude + let expected: number + expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": 1, + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with unnamed jsonb param', async () => { + const res = await postgrest.rpc('polymorphic_function_with_unnamed_jsonb', { + '': { test: 'value' }, + }) + let result: Exclude + let expected: number + expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": 1, + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with unnamed text param', async () => { + const res = await postgrest.rpc('polymorphic_function_with_unnamed_text', { + '': 'test', + }) + let result: Exclude + let expected: number + expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": 1, + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with no params and unnamed params definition call with no params', async () => { + const res = await postgrest.rpc('polymorphic_function_with_no_params_or_unnamed') + let result: Exclude + let expected: number + expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": 1, + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with unnamed params definition call with string param', async () => { + const res = await postgrest.rpc('polymorphic_function_with_no_params_or_unnamed', { + '': '', + }) + let result: Exclude + let expected: string + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": "foo", + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with unnamed params definition call with text param', async () => { + const res = await postgrest.rpc('polymorphic_function_with_no_params_or_unnamed', { + '': 'test', + }) + let result: Exclude + let expected: string + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": "foo", + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with unnamed default no params', async () => { + const res = await postgrest.rpc('polymorphic_function_with_unnamed_default') + let result: Exclude + let expected: SelectQueryError<'Could not choose the best candidate function between: public.polymorphic_function_with_unnamed_default(), public.polymorphic_function_with_unnamed_default( => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved'> + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST203", + "details": null, + "hint": "Try renaming the parameters or the function itself in the database so function overloading can be resolved", + "message": "Could not choose the best candidate function between: public.polymorphic_function_with_unnamed_default(), public.polymorphic_function_with_unnamed_default( => text)", + }, + "status": 300, + "statusText": "Multiple Choices", + } + `) + }) + + test('polymorphic function with unnamed default param undefined', async () => { + const res = await postgrest.rpc('polymorphic_function_with_unnamed_default', {}) + let result: Exclude + // TODO: there is no ways for now to distinguish between a valid optional argument or a missing one if the argument is unnamed + let expected: SelectQueryError<'Could not choose the best candidate function between: public.polymorphic_function_with_unnamed_default(), public.polymorphic_function_with_unnamed_default( => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved'> + // this should be true + expectType>(false) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST203", + "details": null, + "hint": "Try renaming the parameters or the function itself in the database so function overloading can be resolved", + "message": "Could not choose the best candidate function between: public.polymorphic_function_with_unnamed_default(), public.polymorphic_function_with_unnamed_default( => text)", + }, + "status": 300, + "statusText": "Multiple Choices", + } + `) + }) + + test('polymorphic function with unnamed default text param', async () => { + const res = await postgrest.rpc('polymorphic_function_with_unnamed_default', { + '': 'custom text', + }) + let result: Exclude + let expected: string + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": "foo", + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with unnamed default overload no params', async () => { + const res = await postgrest.rpc('polymorphic_function_with_unnamed_default_overload') + let result: Exclude + let expected: SelectQueryError<'Could not choose the best candidate function between: public.polymorphic_function_with_unnamed_default_overload(), public.polymorphic_function_with_unnamed_default_overload( => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved'> + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST203", + "details": null, + "hint": "Try renaming the parameters or the function itself in the database so function overloading can be resolved", + "message": "Could not choose the best candidate function between: public.polymorphic_function_with_unnamed_default_overload(), public.polymorphic_function_with_unnamed_default_overload( => text)", + }, + "status": 300, + "statusText": "Multiple Choices", + } + `) + }) + + test('polymorphic function with unnamed default overload int param', async () => { + const res = await postgrest.rpc('polymorphic_function_with_unnamed_default_overload', undefined) + let result: Exclude + // TODO: there is no ways for now to distinguish between a valid optional argument or a missing one if the argument is unnamed + let expected: SelectQueryError<'Could not choose the best candidate function between: public.polymorphic_function_with_unnamed_default_overload(), public.polymorphic_function_with_unnamed_default_overload( => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved'> + // this should be true + expectType>(false) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST203", + "details": null, + "hint": "Try renaming the parameters or the function itself in the database so function overloading can be resolved", + "message": "Could not choose the best candidate function between: public.polymorphic_function_with_unnamed_default_overload(), public.polymorphic_function_with_unnamed_default_overload( => text)", + }, + "status": 300, + "statusText": "Multiple Choices", + } + `) + }) + + test('polymorphic function with unnamed default overload text param', async () => { + const res = await postgrest.rpc('polymorphic_function_with_unnamed_default_overload', { + '': 'custom text', + }) + let result: Exclude + let expected: string + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": "foo", + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with unnamed default overload bool param', async () => { + const res = await postgrest.rpc('polymorphic_function_with_unnamed_default_overload', { + // TODO: works with latest postgrest-meta type introspection + ////@ts-expect-error Type 'boolean' is not assignable to type 'string' + '': true, + }) + let result: Exclude + let expected: string + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": "foo", + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function with blurb_message', async () => { + const res = await postgrest.rpc('blurb_message') + let result: Exclude + let expected: never + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST202", + "details": "Searched for the function public.blurb_message without parameters or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache.", + "hint": "Perhaps you meant to call the function public.get_messages", + "message": "Could not find the function public.blurb_message without parameters in the schema cache", + }, + "status": 404, + "statusText": "Not Found", + } + `) + }) + + test('function with blurb_message with params', async () => { + const res = await postgrest.rpc('blurb_message', { + '': { + channel_id: 1, + data: null, + id: 1, + message: null, + username: 'test', + blurb_message: null, + }, + }) + let result: Exclude + let expected: SelectQueryError<'the function public.blurb_message with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache'> + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST202", + "details": "Searched for the function public.blurb_message with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache.", + "hint": "Perhaps you meant to call the function public.get_messages", + "message": "Could not find the function public.blurb_message() in the schema cache", + }, + "status": 404, + "statusText": "Not Found", + } + `) + }) + + test('function returning row', async () => { + const res = await postgrest.rpc('function_returning_row') + let result: Exclude + let expected: RequiredDeep> + expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "age_range": "[1,2)", + "catchphrase": "'cat' 'fat'", + "data": null, + "status": "ONLINE", + "username": "supabot", + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) + FunctionReturningRowSchema.parse(res.data) + }) + + test('function returning set of rows', async () => { + const res = await postgrest.rpc('function_returning_set_of_rows') + let result: Exclude + const ExpectedSchema = z.array(FunctionReturningRowSchema) + let expected: RequiredDeep> + expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "age_range": "[1,2)", + "catchphrase": "'cat' 'fat'", + "data": null, + "status": "ONLINE", + "username": "supabot", + }, + Object { + "age_range": "[25,35)", + "catchphrase": "'bat' 'cat'", + "data": null, + "status": "OFFLINE", + "username": "kiwicopple", + }, + Object { + "age_range": "[25,35)", + "catchphrase": "'bat' 'rat'", + "data": null, + "status": "ONLINE", + "username": "awailas", + }, + Object { + "age_range": "[20,30)", + "catchphrase": "'fat' 'rat'", + "data": null, + "status": "ONLINE", + "username": "dragarcia", + }, + Object { + "age_range": "[20,30)", + "catchphrase": "'json' 'test'", + "data": Object { + "foo": Object { + "bar": Object { + "nested": "value", + }, + "baz": "string value", + }, + }, + "status": "ONLINE", + "username": "jsonuser", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + ExpectedSchema.parse(res.data) + }) + + test('function_using_setof_rows_one', async () => { + const res = await postgrest.rpc('function_using_setof_rows_one', { + user_row: { + username: 'supabot', + data: null, + age_range: null, + catchphrase: null, + status: 'ONLINE', + }, + }) + let result: Exclude + const ExpectedSchema = z.array(UserProfileSchema) + let expected: z.infer + expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "id": 1, + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + ExpectedSchema.parse(res.data) + }) + + test('function_using_table_returns', async () => { + const res = await postgrest.rpc('function_using_table_returns', { + user_row: { + username: 'supabot', + data: null, + age_range: null, + catchphrase: null, + status: 'ONLINE', + }, + }) + let result: Exclude + let expected: z.infer + expectType>(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "id": 1, + "username": "supabot", + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) + UserProfileSchema.parse(res.data) + }) +}) + +test('should be able to filter before and after select rpc', async () => { + const res = await postgrest + .rpc('get_user_profile', { + //@ts-expect-error Type '{ username: string; }' is missing the following properties from type '{ age_range: unknown; catchphrase: unknown; data: unknown; status: "ONLINE" | "OFFLINE" | null; username: string; }': age_range, catchphrase, data, status + user_row: { username: 'supabot' }, + }) + .select('id, username, users(username, catchphrase)') + .eq('username', 'nope') + + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": null, + "status": 200, + "statusText": "OK", + } + `) + const res2 = await postgrest + .rpc('get_user_profile', { + //@ts-expect-error Type '{ username: string; }' is missing the following properties from type + user_row: { username: 'supabot' }, + }) + // should also be able to fitler before the select + .eq('username', 'nope') + .select('id, username, users(username, catchphrase)') + + expect(res2).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": null, + "status": 200, + "statusText": "OK", + } + `) + const res3 = await postgrest + .rpc('get_user_profile', { + //@ts-expect-error Type '{ username: string; }' is missing the following properties from type + user_row: { username: 'supabot' }, + }) + // should also be able to fitler before the select + .eq('username', 'supabot') + .select('username, users(username, catchphrase)') + + expect(res3).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "username": "supabot", + "users": Object { + "catchphrase": "'cat' 'fat'", + "username": "supabot", + }, + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('RPC call with subselect and computed field', async () => { + const res = await postgrest + .rpc('get_messages_by_username', { search_username: 'supabot' }) + // should be able to select computed field + .select('message, blurb_message') + // .limit(1) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "blurb_message": "Hel", + "message": "Hello World 👋", + }, + Object { + "blurb_message": "Per", + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + }, + Object { + "blurb_message": "Som", + "message": "Some message on channel wihtout details", + }, + Object { + "blurb_message": "Som", + "message": "Some message on channel wihtout details", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + let result: Exclude + const ExpectedSchema = z.array( + z.object({ + message: z.string().nullable(), + blurb_message: z.string().nullable(), + }) + ) + let expected: z.infer + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + ExpectedSchema.parse(res.data) +}) diff --git a/packages/core/postgrest-js/test/db/00-schema.sql b/packages/core/postgrest-js/test/db/00-schema.sql index f2104c75c..d3ac6748b 100644 --- a/packages/core/postgrest-js/test/db/00-schema.sql +++ b/packages/core/postgrest-js/test/db/00-schema.sql @@ -115,7 +115,6 @@ RETURNS user_status AS $$ RETURNING status; $$ LANGUAGE SQL VOLATILE; - CREATE FUNCTION public.set_users_offline(name_param text) RETURNS SETOF users AS $$ UPDATE users SET status = 'OFFLINE' WHERE username LIKE name_param RETURNING *; @@ -169,8 +168,192 @@ create table public.cornercase ( 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 user_profiles +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM public.user_profiles WHERE username = user_row.username LIMIT 1; +$$; + +-- Same definition, but will be used with a database.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; +$$; +CREATE OR REPLACE FUNCTION public.get_user_first_message(active_user_row active_users) +RETURNS SETOF recent_messages ROWS 1 +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM public.recent_messages WHERE username = active_user_row.username ORDER BY id ASC LIMIT 1; +$$; + + +-- Valid postgresql function override but that produce an unresolvable postgrest function call +create function postgrest_unresolvable_function() returns void language sql as ''; +create function postgrest_unresolvable_function(a text) returns int language sql as 'select 1'; +create function postgrest_unresolvable_function(a int) returns text language sql as $$ + SELECT 'foo' +$$; +-- Valid postgresql function override with differents returns types depending of different arguments +create function postgrest_resolvable_with_override_function() returns void language sql as ''; +create function postgrest_resolvable_with_override_function(a text) returns int language sql as 'select 1'; +create function postgrest_resolvable_with_override_function(b int) returns text language sql as $$ + SELECT 'foo' +$$; +-- Function overrides returning setof tables +create function postgrest_resolvable_with_override_function(profile_id bigint) returns setof user_profiles language sql stable as $$ + SELECT * FROM user_profiles WHERE id = profile_id; +$$; +create function postgrest_resolvable_with_override_function(cid bigint, search text default '') returns setof messages language sql stable as $$ + SELECT * FROM messages WHERE channel_id = cid AND message = search; +$$; +-- Function override taking a table as argument and returning a setof +create function postgrest_resolvable_with_override_function(user_row users) returns setof messages language sql stable as $$ + SELECT * FROM messages WHERE messages.username = user_row.username; +$$; + +create or replace function public.polymorphic_function_with_different_return(bool) returns int language sql as 'SELECT 1'; +create or replace function public.polymorphic_function_with_different_return(int) returns int language sql as 'SELECT 2'; +create or replace function public.polymorphic_function_with_different_return(text) returns text language sql as $$ SELECT 'foo' $$; + +create or replace function public.polymorphic_function_with_no_params_or_unnamed() returns int language sql as 'SELECT 1'; +create or replace function public.polymorphic_function_with_no_params_or_unnamed(bool) returns int language sql as 'SELECT 2'; +create or replace function public.polymorphic_function_with_no_params_or_unnamed(text) returns text language sql as $$ SELECT 'foo' $$; +-- Function with a single unnamed params that isn't a json/jsonb/text should never appears in the type gen as it won't be in postgrest schema +create or replace function public.polymorphic_function_with_unnamed_integer(int) returns int language sql as 'SELECT 1'; +create or replace function public.polymorphic_function_with_unnamed_json(json) returns int language sql as 'SELECT 1'; +create or replace function public.polymorphic_function_with_unnamed_jsonb(jsonb) returns int language sql as 'SELECT 1'; +create or replace function public.polymorphic_function_with_unnamed_text(text) returns int language sql as 'SELECT 1'; + +-- Functions with unnamed parameters that have default values +create or replace function public.polymorphic_function_with_unnamed_default() returns int language sql as 'SELECT 1'; +create or replace function public.polymorphic_function_with_unnamed_default(int default 42) returns int language sql as 'SELECT 2'; +create or replace function public.polymorphic_function_with_unnamed_default(text default 'default') returns text language sql as $$ SELECT 'foo' $$; + +-- Functions with unnamed parameters that have default values and multiple overloads +create or replace function public.polymorphic_function_with_unnamed_default_overload() returns int language sql as 'SELECT 1'; +create or replace function public.polymorphic_function_with_unnamed_default_overload(int default 42) returns int language sql as 'SELECT 2'; +create or replace function public.polymorphic_function_with_unnamed_default_overload(text default 'default') returns text language sql as $$ SELECT 'foo' $$; +create or replace function public.polymorphic_function_with_unnamed_default_overload(bool default true) returns int language sql as 'SELECT 3'; + -- Function creating a computed field create function public.blurb_message(public.messages) returns character varying as $$ select substring($1.message, 1, 3); $$ language sql stable; + + +create or replace function public.function_returning_row() +returns public.users +language sql +stable +as $$ + select * from public.users limit 1; +$$; + + +create or replace function public.function_returning_single_row(messages public.messages) +returns public.users +language sql +stable +as $$ + select * from public.users limit 1; +$$; + +create or replace function public.function_returning_set_of_rows() +returns setof public.users +language sql +stable +as $$ + select * from public.users; +$$; + + +-- Function that returns a single element +CREATE OR REPLACE FUNCTION public.function_using_table_returns(user_row users) +RETURNS user_profiles +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM public.user_profiles WHERE username = user_row.username LIMIT 1; +$$; + +CREATE OR REPLACE FUNCTION public.function_using_setof_rows_one(user_row users) +RETURNS SETOF user_profiles +LANGUAGE SQL STABLE +ROWS 1 +AS $$ + SELECT * FROM public.user_profiles WHERE username = user_row.username LIMIT 1; +$$; diff --git a/packages/core/postgrest-js/test/db/docker-compose.yml b/packages/core/postgrest-js/test/db/docker-compose.yml index eda57d732..5559067ce 100644 --- a/packages/core/postgrest-js/test/db/docker-compose.yml +++ b/packages/core/postgrest-js/test/db/docker-compose.yml @@ -1,6 +1,4 @@ # docker-compose.yml - -version: '3' services: rest13: image: postgrest/postgrest:v13.0.0 diff --git a/packages/core/postgrest-js/test/embeded_functions_join.test.ts b/packages/core/postgrest-js/test/embeded_functions_join.test.ts new file mode 100644 index 000000000..3c63b20fa --- /dev/null +++ b/packages/core/postgrest-js/test/embeded_functions_join.test.ts @@ -0,0 +1,1248 @@ +import { PostgrestClient } from '../src/index' +import { Database } from './types.override' +import { expectType, TypeEqual } from './types' +import { z } from 'zod' +import { RequiredDeep } from 'type-fest' + +const REST_URL = 'http://localhost:3000' +const postgrest = new PostgrestClient(REST_URL) + +describe('embeded functions select', () => { + test('embeded_setof_function - function returning a setof embeded table', async () => { + const res = await postgrest.from('channels').select('id, all_channels_messages:get_messages(*)') + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "all_channels_messages": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + ], + "id": 1, + }, + Object { + "all_channels_messages": Array [ + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + ], + "id": 2, + }, + Object { + "all_channels_messages": Array [ + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + ], + "id": 3, + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + let result: Exclude + const ExpectedSchema = z.array( + z.object({ + id: z.number(), + all_channels_messages: z.array( + z.object({ + channel_id: z.number(), + data: z.unknown().nullable(), + id: z.number(), + message: z.string().nullable(), + username: z.string(), + }) + ), + }) + ) + let expected: RequiredDeep> + // Assert over the keys of the expected and result objects to ensure consistency between versions of types + // should always fallback to a SelectQueryError if the relation cannot be found + expectType>(true) + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + ExpectedSchema.parse(res.data) + }) + + test('embeded_setof_function_fields_selection - function returning a setof embeded table with fields selection', async () => { + const res = await postgrest + .from('channels') + .select('id, all_channels_messages:get_messages(id,message)') + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "all_channels_messages": Array [ + Object { + "id": 1, + "message": "Hello World 👋", + }, + ], + "id": 1, + }, + Object { + "all_channels_messages": Array [ + Object { + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + }, + ], + "id": 2, + }, + Object { + "all_channels_messages": Array [ + Object { + "id": 4, + "message": "Some message on channel wihtout details", + }, + ], + "id": 3, + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + let result: Exclude + const ExpectedSchema = z.array( + z.object({ + id: z.number(), + all_channels_messages: z.array( + z.object({ + id: z.number(), + message: z.string().nullable(), + }) + ), + }) + ) + let expected: z.infer + expectType>(true) + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + ExpectedSchema.parse(res.data) + }) + + test('embeded_setof_function_double_definition - function double definition returning a setof embeded table', async () => { + const res = await postgrest.from('users').select('username, all_user_messages:get_messages(*)') + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "all_user_messages": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + ], + "username": "supabot", + }, + Object { + "all_user_messages": Array [], + "username": "kiwicopple", + }, + Object { + "all_user_messages": Array [], + "username": "awailas", + }, + Object { + "all_user_messages": Array [], + "username": "jsonuser", + }, + Object { + "all_user_messages": Array [], + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + let result: Exclude + const ExpectedSchema = z.array( + z.object({ + username: z.string(), + all_user_messages: z.array( + z.object({ + channel_id: z.number(), + data: z.unknown().nullable(), + id: z.number(), + message: z.string().nullable(), + username: z.string(), + }) + ), + }) + ) + let expected: RequiredDeep> + expectType>(true) + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + ExpectedSchema.parse(res.data) + }) + + test('embeded_setof_function_double_definition_fields_selection - function double definition returning a setof embeded table with fields selection', async () => { + const res = await postgrest + .from('users') + .select('username, all_user_messages:get_messages(id,message)') + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "all_user_messages": Array [ + Object { + "id": 1, + "message": "Hello World 👋", + }, + Object { + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + }, + Object { + "id": 4, + "message": "Some message on channel wihtout details", + }, + ], + "username": "supabot", + }, + Object { + "all_user_messages": Array [], + "username": "kiwicopple", + }, + Object { + "all_user_messages": Array [], + "username": "awailas", + }, + Object { + "all_user_messages": Array [], + "username": "jsonuser", + }, + Object { + "all_user_messages": Array [], + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + let result: Exclude + const ExpectedSchema = z.array( + z.object({ + username: z.string(), + all_user_messages: z.array( + z.object({ + id: z.number(), + message: z.string().nullable(), + }) + ), + }) + ) + let expected: z.infer + expectType>(true) + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + ExpectedSchema.parse(res.data) + }) + + test('embeded_setof_function_double_definition_fields_selection - function double definition returning a setof embeded table with fields selection including computed fields', async () => { + const res = await postgrest + .from('users') + .select('username, all_user_messages:get_messages(id,message, blurb_message)') + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "all_user_messages": Array [ + Object { + "blurb_message": "Hel", + "id": 1, + "message": "Hello World 👋", + }, + Object { + "blurb_message": "Per", + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + }, + Object { + "blurb_message": "Som", + "id": 4, + "message": "Some message on channel wihtout details", + }, + ], + "username": "supabot", + }, + Object { + "all_user_messages": Array [], + "username": "kiwicopple", + }, + Object { + "all_user_messages": Array [], + "username": "awailas", + }, + Object { + "all_user_messages": Array [], + "username": "jsonuser", + }, + Object { + "all_user_messages": Array [], + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + let result: Exclude + const ExpectedSchema = z.array( + z.object({ + username: z.string(), + all_user_messages: z.array( + z.object({ + id: z.number(), + message: z.string().nullable(), + blurb_message: z.string().nullable(), + }) + ), + }) + ) + let expected: z.infer + expectType>(true) + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + ExpectedSchema.parse(res.data) + }) + + test('embeded_function_using_setof_rows_one - function returning a setof single row embeded table', async () => { + const res = await postgrest + .from('users') + .select('username, setof_rows_one:function_using_setof_rows_one(*)') + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "setof_rows_one": Object { + "id": 1, + "username": "supabot", + }, + "username": "supabot", + }, + Object { + "setof_rows_one": null, + "username": "kiwicopple", + }, + Object { + "setof_rows_one": null, + "username": "awailas", + }, + Object { + "setof_rows_one": null, + "username": "jsonuser", + }, + Object { + "setof_rows_one": null, + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + let result: Exclude + const ExpectedSchema = z.array( + z.object({ + username: z.string(), + setof_rows_one: z + .object({ + id: z.number(), + username: z.string().nullable(), + }) + .nullable(), + }) + ) + let expected: z.infer + expectType>(true) + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + ExpectedSchema.parse(res.data) + }) + + test('embeded_function_using_table_returns - function returns row embeded table', async () => { + const res = await postgrest + .from('users') + .select('username, returns_row:function_using_table_returns(*)') + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "returns_row": Object { + "id": 1, + "username": "supabot", + }, + "username": "supabot", + }, + Object { + "returns_row": Object { + "id": null, + "username": null, + }, + "username": "kiwicopple", + }, + Object { + "returns_row": Object { + "id": null, + "username": null, + }, + "username": "awailas", + }, + Object { + "returns_row": Object { + "id": null, + "username": null, + }, + "username": "jsonuser", + }, + Object { + "returns_row": Object { + "id": null, + "username": null, + }, + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + let result: Exclude + const ExpectedSchema = z.array( + z.object({ + username: z.string(), + returns_row: z.object({ + id: z.number().nullable(), + username: z.string().nullable(), + }), + }) + ) + let expected: z.infer + expectType>(true) + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + ExpectedSchema.parse(res.data) + }) + + test('embeded_setof_row_one_function_not_nullable - function returning a single row embeded table not nullable', async () => { + const res = await postgrest + .from('users') + // Inner join to ensure the join result is not nullable can also be set at relation level + // by setting isNotNullable for the function SetofOptions definition to true + .select('username, user_called_profile_not_null:get_user_profile_non_nullable!inner(*)') + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "user_called_profile_not_null": Object { + "id": 1, + "username": "supabot", + }, + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + let result: Exclude + // The override marks this as not nullable, but the data can be null at runtime. + // So the correct runtime schema is nullable, but the type is not. + // We check that the type is as expected (not nullable), but parsing will fail. + const ExpectedSchema = z.array( + z.object({ + username: z.string(), + user_called_profile_not_null: z.object({ + id: z.number(), + username: z.string().nullable(), + }), + }) + ) + let expected: z.infer + expectType>(true) + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + // Can parse the data because the !inner ensure the join result from function is not nullable + ExpectedSchema.parse(res.data) + }) + + test('embeded_setof_row_one_function_with_fields_selection - function returning a single row embeded table with fields selection', async () => { + const res = await postgrest + .from('users') + .select('username, user_called_profile:get_user_profile(username)') + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "user_called_profile": Object { + "username": "supabot", + }, + "username": "supabot", + }, + Object { + "user_called_profile": Object { + "username": null, + }, + "username": "kiwicopple", + }, + Object { + "user_called_profile": Object { + "username": null, + }, + "username": "awailas", + }, + Object { + "user_called_profile": Object { + "username": null, + }, + "username": "jsonuser", + }, + Object { + "user_called_profile": Object { + "username": null, + }, + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + let result: Exclude + const ExpectedSchema = z.array( + z.object({ + username: z.string(), + user_called_profile: z.object({ + username: z.string().nullable(), + }), + }) + ) + let expected: z.infer + expectType>(true) + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + ExpectedSchema.parse(res.data) + }) + + test('embeded_setof_function_with_fields_selection_with_sub_linking - function embedded table with fields selection and sub linking', async () => { + const res = await postgrest + .from('channels') + .select('id, all_channels_messages:get_messages(id,message,channels(id,slug))') + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "all_channels_messages": Array [ + Object { + "channels": Object { + "id": 1, + "slug": "public", + }, + "id": 1, + "message": "Hello World 👋", + }, + ], + "id": 1, + }, + Object { + "all_channels_messages": Array [ + Object { + "channels": Object { + "id": 2, + "slug": "random", + }, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + }, + ], + "id": 2, + }, + Object { + "all_channels_messages": Array [ + Object { + "channels": Object { + "id": 3, + "slug": "other", + }, + "id": 4, + "message": "Some message on channel wihtout details", + }, + ], + "id": 3, + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + let result: Exclude + const ExpectedSchema = z.array( + z.object({ + id: z.number(), + all_channels_messages: z.array( + z.object({ + id: z.number(), + message: z.string().nullable(), + channels: z.object({ + id: z.number(), + slug: z.string().nullable(), + }), + }) + ), + }) + ) + let expected: z.infer + expectType>(true) + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + ExpectedSchema.parse(res.data) + }) + + test('embeded_function_with_table_row_input - function with table row input', async () => { + const res = await postgrest.from('users').select('username, user_messages:get_user_messages(*)') + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "user_messages": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + ], + "username": "supabot", + }, + Object { + "user_messages": Array [], + "username": "kiwicopple", + }, + Object { + "user_messages": Array [], + "username": "awailas", + }, + Object { + "user_messages": Array [], + "username": "jsonuser", + }, + Object { + "user_messages": Array [], + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + let result: Exclude + const ExpectedSchema = z.array( + z.object({ + username: z.string(), + user_messages: z.array( + z.object({ + channel_id: z.number(), + data: z.unknown().nullable(), + id: z.number(), + message: z.string().nullable(), + username: z.string(), + }) + ), + }) + ) + let expected: RequiredDeep> + expectType>(true) + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + ExpectedSchema.parse(res.data) + }) + + test('embeded_function_with_view_row_input - function with view row input', async () => { + const res = await postgrest + .from('active_users') + .select('username, active_user_messages:get_active_user_messages(*)') + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "active_user_messages": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + ], + "username": "supabot", + }, + Object { + "active_user_messages": Array [], + "username": "awailas", + }, + Object { + "active_user_messages": Array [], + "username": "jsonuser", + }, + Object { + "active_user_messages": Array [], + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + let result: Exclude + const ExpectedSchema = z.array( + z.object({ + username: z.string().nullable(), + active_user_messages: z.array( + z.object({ + channel_id: z.number(), + data: z.unknown().nullable(), + id: z.number(), + message: z.string().nullable(), + username: z.string(), + }) + ), + }) + ) + let expected: RequiredDeep> + expectType>(true) + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + ExpectedSchema.parse(res.data) + }) + + test('embeded_function_returning_view - function returning view', async () => { + const res = await postgrest + .from('users') + .select('username, recent_messages:get_user_recent_messages(*)') + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "recent_messages": Array [ + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + ], + "username": "supabot", + }, + Object { + "recent_messages": Array [], + "username": "kiwicopple", + }, + Object { + "recent_messages": Array [], + "username": "awailas", + }, + Object { + "recent_messages": Array [], + "username": "jsonuser", + }, + Object { + "recent_messages": Array [], + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + let result: Exclude + const ExpectedSchema = z.array( + z.object({ + username: z.string(), + recent_messages: z.array( + z.object({ + channel_id: z.number().nullable(), + data: z.unknown().nullable(), + id: z.number().nullable(), + message: z.string().nullable(), + username: z.string().nullable(), + }) + ), + }) + ) + let expected: RequiredDeep> + expectType>(true) + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + ExpectedSchema.parse(res.data) + }) + + test('embeded_function_with_view_input_returning_view - function with view input returning view', async () => { + const res = await postgrest + .from('active_users') + .select('username, recent_messages:get_user_recent_messages(*)') + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "recent_messages": Array [ + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + ], + "username": "supabot", + }, + Object { + "recent_messages": Array [], + "username": "awailas", + }, + Object { + "recent_messages": Array [], + "username": "jsonuser", + }, + Object { + "recent_messages": Array [], + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + let result: Exclude + const ExpectedSchema = z.array( + z.object({ + username: z.string().nullable(), + recent_messages: z.array( + z.object({ + channel_id: z.number().nullable(), + data: z.unknown().nullable(), + id: z.number().nullable(), + message: z.string().nullable(), + username: z.string().nullable(), + }) + ), + }) + ) + let expected: RequiredDeep> + expectType>(true) + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + ExpectedSchema.parse(res.data) + }) + + test('embeded_function_with_blurb_message - function with blurb_message', async () => { + const res = await postgrest + .from('users') + .select('username, user_messages:get_user_messages(id,message,blurb_message)') + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "user_messages": Array [ + Object { + "blurb_message": "Hel", + "id": 1, + "message": "Hello World 👋", + }, + Object { + "blurb_message": "Per", + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + }, + Object { + "blurb_message": "Som", + "id": 4, + "message": "Some message on channel wihtout details", + }, + ], + "username": "supabot", + }, + Object { + "user_messages": Array [], + "username": "kiwicopple", + }, + Object { + "user_messages": Array [], + "username": "awailas", + }, + Object { + "user_messages": Array [], + "username": "jsonuser", + }, + Object { + "user_messages": Array [], + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + let result: Exclude + const ExpectedSchema = z.array( + z.object({ + username: z.string(), + user_messages: z.array( + z.object({ + id: z.number(), + message: z.string().nullable(), + blurb_message: z.string().nullable(), + }) + ), + }) + ) + let expected: z.infer + expectType>(true) + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + ExpectedSchema.parse(res.data) + }) + + test('embeded_function_returning_row - Cannot embed an function that is not a setofOptions one', async () => { + const res = await postgrest.from('channels').select('id, user:function_returning_row(*)') + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST200", + "details": "Searched for a foreign key relationship between 'channels' and 'function_returning_row' in the schema 'public', but no matches were found.", + "hint": null, + "message": "Could not find a relationship between 'channels' and 'function_returning_row' in the schema cache", + }, + "status": 400, + "statusText": "Bad Request", + } + `) + let result: Exclude + let expected: never[] + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + }) + + test('embeded_function_returning_single_row - can embed single row returns function with row single param', async () => { + const res = await postgrest + .from('messages') + .select('id, user:function_returning_single_row(status, username)') + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "id": 1, + "user": Object { + "status": "ONLINE", + "username": "supabot", + }, + }, + Object { + "id": 2, + "user": Object { + "status": "ONLINE", + "username": "supabot", + }, + }, + Object { + "id": 4, + "user": Object { + "status": "ONLINE", + "username": "supabot", + }, + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + let result: Exclude + const ExpectedSchema = z.array( + z.object({ + id: z.number(), + user: z.object({ + status: z.enum(['ONLINE', 'OFFLINE'] as const).nullable(), + username: z.string().nullable(), + }), + }) + ) + let expected: z.infer + expectType>(true) + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + ExpectedSchema.parse(res.data) + }) + + test('embeded_function_returning_set_of_rows - function returning set of rows', async () => { + const res = await postgrest + .from('messages') + .select('id, users:function_returning_set_of_rows(*)') + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST200", + "details": "Searched for a foreign key relationship between 'messages' and 'function_returning_set_of_rows' in the schema 'public', but no matches were found.", + "hint": null, + "message": "Could not find a relationship between 'messages' and 'function_returning_set_of_rows' in the schema cache", + }, + "status": 400, + "statusText": "Bad Request", + } + `) + let result: Exclude + let expected: never[] + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + }) + + test('function_using_setof_rows_one', async () => { + const res = await postgrest + .from('users') + .select('username, profile:function_using_table_returns(*)') + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "profile": Object { + "id": 1, + "username": "supabot", + }, + "username": "supabot", + }, + Object { + "profile": Object { + "id": null, + "username": null, + }, + "username": "kiwicopple", + }, + Object { + "profile": Object { + "id": null, + "username": null, + }, + "username": "awailas", + }, + Object { + "profile": Object { + "id": null, + "username": null, + }, + "username": "jsonuser", + }, + Object { + "profile": Object { + "id": null, + "username": null, + }, + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + let result: Exclude + const ExpectedSchema = z.array( + z.object({ + username: z.string(), + profile: z.object({ + id: z.number().nullable(), + username: z.string().nullable(), + }), + }) + ) + let expected: z.infer + expectType>(true) + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + ExpectedSchema.parse(res.data) + }) + + test('function_using_table_returns', async () => { + const res = await postgrest + .from('users') + .select('username, profile:function_using_setof_rows_one(*)') + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "profile": Object { + "id": 1, + "username": "supabot", + }, + "username": "supabot", + }, + Object { + "profile": null, + "username": "kiwicopple", + }, + Object { + "profile": null, + "username": "awailas", + }, + Object { + "profile": null, + "username": "jsonuser", + }, + Object { + "profile": null, + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + let result: Exclude + const ExpectedSchema = z.array( + z.object({ + username: z.string(), + profile: z + .object({ + id: z.number(), + username: z.string().nullable(), + }) + .nullable(), + }) + ) + let expected: z.infer + expectType>(true) + // TODO: works with latest postgrest-meta type introspection + // expectType>(true) + ExpectedSchema.parse(res.data) + }) +}) diff --git a/packages/core/postgrest-js/test/index.test-d.ts b/packages/core/postgrest-js/test/index.test-d.ts index 216f24355..6310ee69b 100644 --- a/packages/core/postgrest-js/test/index.test-d.ts +++ b/packages/core/postgrest-js/test/index.test-d.ts @@ -1,6 +1,6 @@ import { expectType, TypeEqual } from './types' import { PostgrestClient, PostgrestError } from '../src/index' -import { Prettify } from '../src/types' +import { Prettify } from '../src/types/types' import { Json } from '../src/select-query-parser/types' import { Database } from './types.override' import { Database as DatabaseWithOptions } from './types.override-with-options-postgrest13' @@ -199,15 +199,6 @@ const postgrestWithOptions = new PostgrestClient(REST_URL) expectType(result.data.baz) } -// rpc return type -{ - const result = await postgrest.rpc('get_status') - if (result.error) { - throw new Error(result.error.message) - } - expectType<'ONLINE' | 'OFFLINE'>(result.data) -} - // PostgrestBuilder's children retains class when using inherited methods { const x = postgrest.from('channels').select() @@ -301,9 +292,28 @@ const postgrestWithOptions = new PostgrestClient(REST_URL) > >(true) } + // Check that client options __InternalSupabase isn't considered like the other schemas { await postgrestWithOptions // @ts-expect-error Argument of type '"__InternalSupabase"' is not assignable to parameter of type '"personal" | "public"' .schema('__InternalSupabase') } + +// Json string Accessor with custom types overrides +{ + const result = await postgrest + .schema('personal') + .from('users') + .select('data->bar->>baz, data->>en, data->>bar') + if (result.error) { + throw new Error(result.error.message) + } + expectType< + { + baz: string + en: 'ONE' | 'TWO' | 'THREE' + bar: string + }[] + >(result.data) +} diff --git a/packages/core/postgrest-js/test/relationships-aggregate-operations.test.ts b/packages/core/postgrest-js/test/relationships-aggregate-operations.test.ts index e9b501859..81a7ae379 100644 --- a/packages/core/postgrest-js/test/relationships-aggregate-operations.test.ts +++ b/packages/core/postgrest-js/test/relationships-aggregate-operations.test.ts @@ -4,7 +4,7 @@ import { expectType, TypeEqual } from './types' import { z } from 'zod' const REST_URL = 'http://localhost:3000' -export const postgrest = new PostgrestClient(REST_URL) +const postgrest = new PostgrestClient(REST_URL) test('select with aggregate count function', async () => { const res = await postgrest.from('users').select('username, messages(count)').limit(1).single() diff --git a/packages/core/postgrest-js/test/relationships-error-handling.test.ts b/packages/core/postgrest-js/test/relationships-error-handling.test.ts index 54129e2a4..6cc68389f 100644 --- a/packages/core/postgrest-js/test/relationships-error-handling.test.ts +++ b/packages/core/postgrest-js/test/relationships-error-handling.test.ts @@ -4,7 +4,7 @@ import { expectType, TypeEqual } from './types' import { SelectQueryError } from '../src/select-query-parser/utils' const REST_URL = 'http://localhost:3000' -export const postgrest = new PostgrestClient(REST_URL) +const postgrest = new PostgrestClient(REST_URL) test('join over a 1-1 relation with both nullables and non-nullables fields with no hinting', async () => { const res = await postgrest diff --git a/packages/core/postgrest-js/test/relationships-join-operations.test.ts b/packages/core/postgrest-js/test/relationships-join-operations.test.ts index fc00adc06..a8a85ea0b 100644 --- a/packages/core/postgrest-js/test/relationships-join-operations.test.ts +++ b/packages/core/postgrest-js/test/relationships-join-operations.test.ts @@ -5,7 +5,7 @@ import { z } from 'zod' import { RequiredDeep } from 'type-fest' const REST_URL = 'http://localhost:3000' -export const postgrest = new PostgrestClient(REST_URL) +const postgrest = new PostgrestClient(REST_URL) const userColumn: 'catchphrase' | 'username' = 'username' // Zod schemas for common types diff --git a/packages/core/postgrest-js/test/relationships-spread-operations.test.ts b/packages/core/postgrest-js/test/relationships-spread-operations.test.ts index 4f9d9b396..642fa9c7c 100644 --- a/packages/core/postgrest-js/test/relationships-spread-operations.test.ts +++ b/packages/core/postgrest-js/test/relationships-spread-operations.test.ts @@ -5,7 +5,7 @@ import { expectType, TypeEqual } from './types' import { z } from 'zod' const REST_URL = 'http://localhost:3000' -export const postgrest = new PostgrestClient(REST_URL) +const postgrest = new PostgrestClient(REST_URL) const REST_URL_13 = 'http://localhost:3001' const postgrest13 = new PostgrestClient(REST_URL_13) const postgrest13FromDatabaseTypes = new PostgrestClient(REST_URL_13) diff --git a/packages/core/postgrest-js/test/relationships.test.ts b/packages/core/postgrest-js/test/relationships.test.ts index 065a3a83d..e917cc1cf 100644 --- a/packages/core/postgrest-js/test/relationships.test.ts +++ b/packages/core/postgrest-js/test/relationships.test.ts @@ -6,7 +6,7 @@ import { Json } from '../src/select-query-parser/types' import { RequiredDeep } from 'type-fest' const REST_URL = 'http://localhost:3000' -export const postgrest = new PostgrestClient(REST_URL) +const postgrest = new PostgrestClient(REST_URL) const UsersRowSchema = z.object({ age_range: z.unknown().nullable(), diff --git a/packages/core/postgrest-js/test/transforms.test.ts b/packages/core/postgrest-js/test/transforms.test.ts index ac1033f24..3ad2f0e72 100644 --- a/packages/core/postgrest-js/test/transforms.test.ts +++ b/packages/core/postgrest-js/test/transforms.test.ts @@ -334,8 +334,7 @@ test('abort signal', async () => { error: { code: expect.any(String), details: expect.any(String), - // Match both "AbortError:" and "Error: AbortError" formats - message: expect.stringMatching(/AbortError/), + message: expect.stringMatching(/^Error: AbortError/), }, }, ` @@ -346,7 +345,7 @@ test('abort signal', async () => { "code": Any, "details": Any, "hint": "", - "message": StringMatching /AbortError/, + "message": StringMatching /\\^Error: AbortError/, }, "status": 0, "statusText": "", diff --git a/packages/core/postgrest-js/test/types.generated-with-options-postgrest13.ts b/packages/core/postgrest-js/test/types.generated-with-options-postgrest13.ts index 6a9f2c3f1..f497c2e7d 100644 --- a/packages/core/postgrest-js/test/types.generated-with-options-postgrest13.ts +++ b/packages/core/postgrest-js/test/types.generated-with-options-postgrest13.ts @@ -1,546 +1,12 @@ +import { Database as OriginalDatabase } from './types.generated' export type Json = unknown -export type Database = { +export type Database = OriginalDatabase & { // This is a dummy non existent schema to allow automatically passing down options // to the instanciated client at type levels from the introspected database __InternalSupabase: { PostgrestVersion: '13.0.12' } - personal: { - Tables: { - users: { - Row: { - age_range: unknown | null - data: Json | null - status: Database['public']['Enums']['user_status'] | null - username: string - } - Insert: { - age_range?: unknown | null - data?: Json | null - status?: Database['public']['Enums']['user_status'] | null - username: string - } - Update: { - age_range?: unknown | null - data?: Json | null - status?: Database['public']['Enums']['user_status'] | null - username?: string - } - Relationships: [] - } - } - Views: { - [_ in never]: never - } - Functions: { - get_status: { - Args: { - name_param: string - } - Returns: Database['public']['Enums']['user_status'] - } - } - Enums: { - user_status: 'ONLINE' | 'OFFLINE' - } - CompositeTypes: { - [_ in never]: never - } - } - public: { - Tables: { - best_friends: { - Row: { - first_user: string - id: number - second_user: string - third_wheel: string | null - } - Insert: { - first_user: string - id?: number - second_user: string - third_wheel?: string | null - } - Update: { - first_user?: string - id?: number - second_user?: string - third_wheel?: string | null - } - Relationships: [ - { - foreignKeyName: 'best_friends_first_user_fkey' - columns: ['first_user'] - isOneToOne: false - referencedRelation: 'non_updatable_view' - referencedColumns: ['username'] - }, - { - foreignKeyName: 'best_friends_first_user_fkey' - columns: ['first_user'] - isOneToOne: false - referencedRelation: 'updatable_view' - referencedColumns: ['username'] - }, - { - foreignKeyName: 'best_friends_first_user_fkey' - columns: ['first_user'] - isOneToOne: false - referencedRelation: 'users' - referencedColumns: ['username'] - }, - { - foreignKeyName: 'best_friends_second_user_fkey' - columns: ['second_user'] - isOneToOne: false - referencedRelation: 'non_updatable_view' - referencedColumns: ['username'] - }, - { - foreignKeyName: 'best_friends_second_user_fkey' - columns: ['second_user'] - isOneToOne: false - referencedRelation: 'updatable_view' - referencedColumns: ['username'] - }, - { - foreignKeyName: 'best_friends_second_user_fkey' - columns: ['second_user'] - isOneToOne: false - referencedRelation: 'users' - referencedColumns: ['username'] - }, - { - foreignKeyName: 'best_friends_third_wheel_fkey' - columns: ['third_wheel'] - isOneToOne: false - referencedRelation: 'non_updatable_view' - referencedColumns: ['username'] - }, - { - foreignKeyName: 'best_friends_third_wheel_fkey' - columns: ['third_wheel'] - isOneToOne: false - referencedRelation: 'updatable_view' - referencedColumns: ['username'] - }, - { - foreignKeyName: 'best_friends_third_wheel_fkey' - columns: ['third_wheel'] - isOneToOne: false - referencedRelation: 'users' - referencedColumns: ['username'] - }, - ] - } - booking: { - Row: { - hotel_id: number | null - id: number - } - Insert: { - hotel_id?: number | null - id?: number - } - Update: { - hotel_id?: number | null - id?: number - } - Relationships: [ - { - foreignKeyName: 'booking_hotel_id_fkey' - columns: ['hotel_id'] - isOneToOne: false - referencedRelation: 'hotel' - referencedColumns: ['id'] - }, - ] - } - categories: { - Row: { - description: string | null - id: number - name: string - } - Insert: { - description?: string | null - id?: number - name: string - } - Update: { - description?: string | null - id?: number - name?: string - } - Relationships: [] - } - channel_details: { - Row: { - details: string | null - id: number - } - Insert: { - details?: string | null - id: number - } - Update: { - details?: string | null - id?: number - } - Relationships: [ - { - foreignKeyName: 'channel_details_id_fkey' - columns: ['id'] - isOneToOne: true - referencedRelation: 'channels' - referencedColumns: ['id'] - }, - ] - } - channels: { - Row: { - data: Json | null - id: number - slug: string | null - } - Insert: { - data?: Json | null - id?: number - slug?: string | null - } - Update: { - data?: Json | null - id?: number - slug?: string | null - } - Relationships: [] - } - collections: { - Row: { - description: string | null - id: number - parent_id: number | null - } - Insert: { - description?: string | null - id?: number - parent_id?: number | null - } - Update: { - description?: string | null - id?: number - parent_id?: number | null - } - Relationships: [ - { - foreignKeyName: 'collections_parent_id_fkey' - columns: ['parent_id'] - isOneToOne: false - referencedRelation: 'collections' - referencedColumns: ['id'] - }, - ] - } - cornercase: { - Row: { - array_column: string[] | null - 'column whitespace': string | null - id: number - } - Insert: { - array_column?: string[] | null - 'column whitespace'?: string | null - id: number - } - Update: { - array_column?: string[] | null - 'column whitespace'?: string | null - id?: number - } - Relationships: [] - } - hotel: { - Row: { - id: number - name: string | null - } - Insert: { - id?: number - name?: string | null - } - Update: { - id?: number - name?: string | null - } - Relationships: [] - } - messages: { - Row: { - channel_id: number - data: Json | null - id: number - message: string | null - username: string - } - Insert: { - channel_id: number - data?: Json | null - id?: number - message?: string | null - username: string - } - Update: { - channel_id?: number - data?: Json | null - id?: number - message?: string | null - username?: string - } - Relationships: [ - { - foreignKeyName: 'messages_channel_id_fkey' - columns: ['channel_id'] - isOneToOne: false - referencedRelation: 'channels' - referencedColumns: ['id'] - }, - { - foreignKeyName: 'messages_username_fkey' - columns: ['username'] - isOneToOne: false - referencedRelation: 'non_updatable_view' - referencedColumns: ['username'] - }, - { - foreignKeyName: 'messages_username_fkey' - columns: ['username'] - isOneToOne: false - referencedRelation: 'updatable_view' - referencedColumns: ['username'] - }, - { - foreignKeyName: 'messages_username_fkey' - columns: ['username'] - isOneToOne: false - referencedRelation: 'users' - referencedColumns: ['username'] - }, - ] - } - product_categories: { - Row: { - category_id: number - product_id: number - } - Insert: { - category_id: number - product_id: number - } - Update: { - category_id?: number - product_id?: number - } - Relationships: [ - { - foreignKeyName: 'product_categories_category_id_fkey' - columns: ['category_id'] - isOneToOne: false - referencedRelation: 'categories' - referencedColumns: ['id'] - }, - { - foreignKeyName: 'product_categories_product_id_fkey' - columns: ['product_id'] - isOneToOne: false - referencedRelation: 'products' - referencedColumns: ['id'] - }, - ] - } - products: { - Row: { - description: string | null - id: number - name: string - price: number - } - Insert: { - description?: string | null - id?: number - name: string - price: number - } - Update: { - description?: string | null - id?: number - name?: string - price?: number - } - Relationships: [] - } - shops: { - Row: { - address: string | null - id: number - shop_geom: unknown | null - } - Insert: { - address?: string | null - id: number - shop_geom?: unknown | null - } - Update: { - address?: string | null - id?: number - shop_geom?: unknown | null - } - Relationships: [] - } - user_profiles: { - Row: { - id: number - username: string | null - } - Insert: { - id?: number - username?: string | null - } - Update: { - id?: number - username?: string | null - } - Relationships: [ - { - foreignKeyName: 'user_profiles_username_fkey' - columns: ['username'] - isOneToOne: false - referencedRelation: 'non_updatable_view' - referencedColumns: ['username'] - }, - { - foreignKeyName: 'user_profiles_username_fkey' - columns: ['username'] - isOneToOne: false - referencedRelation: 'updatable_view' - referencedColumns: ['username'] - }, - { - foreignKeyName: 'user_profiles_username_fkey' - columns: ['username'] - isOneToOne: false - referencedRelation: 'users' - referencedColumns: ['username'] - }, - ] - } - users: { - Row: { - age_range: unknown | null - catchphrase: unknown | null - data: Json | null - status: Database['public']['Enums']['user_status'] | null - username: string - } - Insert: { - age_range?: unknown | null - catchphrase?: unknown | null - data?: Json | null - status?: Database['public']['Enums']['user_status'] | null - username: string - } - Update: { - age_range?: unknown | null - catchphrase?: unknown | null - data?: Json | null - status?: Database['public']['Enums']['user_status'] | null - username?: string - } - Relationships: [] - } - } - Views: { - non_updatable_view: { - Row: { - username: string | null - } - Relationships: [] - } - updatable_view: { - Row: { - non_updatable_column: number | null - username: string | null - } - Insert: { - non_updatable_column?: never - username?: string | null - } - Update: { - non_updatable_column?: never - username?: string | null - } - Relationships: [] - } - } - Functions: { - function_with_array_param: { - Args: { - param: string[] - } - Returns: undefined - } - function_with_optional_param: { - Args: { - param?: string - } - Returns: string - } - get_status: { - Args: { - name_param: string - } - Returns: Database['public']['Enums']['user_status'] - } - get_username_and_status: { - Args: { - name_param: string - } - Returns: { - username: string - status: Database['public']['Enums']['user_status'] - }[] - } - offline_user: { - Args: { - name_param: string - } - Returns: Database['public']['Enums']['user_status'] - } - set_users_offline: { - Args: { - name_param: string - } - Returns: { - age_range: unknown | null - catchphrase: unknown | null - data: Json | null - status: Database['public']['Enums']['user_status'] | null - username: string - }[] - } - void_func: { - Args: Record - Returns: undefined - } - } - Enums: { - user_status: 'ONLINE' | 'OFFLINE' - } - CompositeTypes: { - [_ in never]: never - } - } } type DatabaseWithoutInternals = Omit diff --git a/packages/core/postgrest-js/test/types.generated.ts b/packages/core/postgrest-js/test/types.generated.ts index 9860ee7d8..ea731f970 100644 --- a/packages/core/postgrest-js/test/types.generated.ts +++ b/packages/core/postgrest-js/test/types.generated.ts @@ -63,6 +63,13 @@ export type Database = { third_wheel?: string | null } Relationships: [ + { + foreignKeyName: 'best_friends_first_user_fkey' + columns: ['first_user'] + isOneToOne: false + referencedRelation: 'active_users' + referencedColumns: ['username'] + }, { foreignKeyName: 'best_friends_first_user_fkey' columns: ['first_user'] @@ -84,6 +91,13 @@ export type Database = { referencedRelation: 'users' referencedColumns: ['username'] }, + { + foreignKeyName: 'best_friends_second_user_fkey' + columns: ['second_user'] + isOneToOne: false + referencedRelation: 'active_users' + referencedColumns: ['username'] + }, { foreignKeyName: 'best_friends_second_user_fkey' columns: ['second_user'] @@ -105,6 +119,13 @@ export type Database = { referencedRelation: 'users' referencedColumns: ['username'] }, + { + foreignKeyName: 'best_friends_third_wheel_fkey' + columns: ['third_wheel'] + isOneToOne: false + referencedRelation: 'active_users' + referencedColumns: ['username'] + }, { foreignKeyName: 'best_friends_third_wheel_fkey' columns: ['third_wheel'] @@ -300,6 +321,13 @@ export type Database = { referencedRelation: 'channels' referencedColumns: ['id'] }, + { + foreignKeyName: 'messages_username_fkey' + columns: ['username'] + isOneToOne: false + referencedRelation: 'active_users' + referencedColumns: ['username'] + }, { foreignKeyName: 'messages_username_fkey' columns: ['username'] @@ -406,6 +434,13 @@ export type Database = { username?: string | null } Relationships: [ + { + foreignKeyName: 'user_profiles_username_fkey' + columns: ['username'] + isOneToOne: false + referencedRelation: 'active_users' + referencedColumns: ['username'] + }, { foreignKeyName: 'user_profiles_username_fkey' columns: ['username'] @@ -455,12 +490,82 @@ export type Database = { } } Views: { + active_users: { + Row: { + age_range: unknown | null + catchphrase: unknown | null + data: Json | null + status: Database['public']['Enums']['user_status'] | null + username: string | null + } + Insert: { + age_range?: unknown | null + catchphrase?: unknown | null + data?: Json | null + status?: Database['public']['Enums']['user_status'] | null + username?: string | null + } + Update: { + age_range?: unknown | null + catchphrase?: unknown | null + data?: Json | null + status?: Database['public']['Enums']['user_status'] | null + username?: string | null + } + Relationships: [] + } non_updatable_view: { Row: { username: string | null } Relationships: [] } + recent_messages: { + Row: { + channel_id: number | null + data: Json | null + id: number | null + message: string | null + username: string | null + } + Relationships: [ + { + foreignKeyName: 'messages_channel_id_fkey' + columns: ['channel_id'] + isOneToOne: false + referencedRelation: 'channels' + referencedColumns: ['id'] + }, + { + foreignKeyName: 'messages_username_fkey' + columns: ['username'] + isOneToOne: false + referencedRelation: 'active_users' + referencedColumns: ['username'] + }, + { + foreignKeyName: 'messages_username_fkey' + columns: ['username'] + isOneToOne: false + referencedRelation: 'non_updatable_view' + referencedColumns: ['username'] + }, + { + foreignKeyName: 'messages_username_fkey' + columns: ['username'] + isOneToOne: false + referencedRelation: 'updatable_view' + referencedColumns: ['username'] + }, + { + foreignKeyName: 'messages_username_fkey' + columns: ['username'] + isOneToOne: false + referencedRelation: 'users' + referencedColumns: ['username'] + }, + ] + } updatable_view: { Row: { non_updatable_column: number | null @@ -482,6 +587,50 @@ export type Database = { Args: { '': Database['public']['Tables']['messages']['Row'] } Returns: string } + function_returning_row: { + Args: Record + Returns: { + age_range: unknown | null + catchphrase: unknown | null + data: Json | null + status: Database['public']['Enums']['user_status'] | null + username: string + } + } + function_returning_set_of_rows: { + Args: Record + Returns: { + age_range: unknown | null + catchphrase: unknown | null + data: Json | null + status: Database['public']['Enums']['user_status'] | null + username: string + }[] + } + function_returning_single_row: { + Args: { messages: Database['public']['Tables']['messages']['Row'] } + Returns: { + age_range: unknown | null + catchphrase: unknown | null + data: Json | null + status: Database['public']['Enums']['user_status'] | null + username: string + } + } + function_using_setof_rows_one: { + Args: { user_row: Database['public']['Tables']['users']['Row'] } + Returns: { + id: number + username: string | null + }[] + } + function_using_table_returns: { + Args: { user_row: Database['public']['Tables']['users']['Row'] } + Returns: { + id: number + username: string | null + } + } function_with_array_param: { Args: { param: string[] } Returns: undefined @@ -490,10 +639,98 @@ export type Database = { Args: { param?: string } Returns: string } + get_active_user_messages: { + Args: { active_user_row: unknown } + Returns: { + channel_id: number + data: Json | null + id: number + message: string | null + username: string + }[] + } + get_messages: { + Args: + | { channel_row: Database['public']['Tables']['channels']['Row'] } + | { user_row: Database['public']['Tables']['users']['Row'] } + Returns: { + channel_id: number + data: Json | null + id: number + message: string | null + username: string + }[] + } + get_messages_by_username: { + Args: { search_username: string } + Returns: { + channel_id: number + data: Json | null + id: number + message: string | null + username: string + }[] + } + get_recent_messages_by_username: { + Args: { search_username: string } + Returns: { + channel_id: number | null + data: Json | null + id: number | null + message: string | null + username: string | null + }[] + } get_status: { Args: { name_param: string } Returns: Database['public']['Enums']['user_status'] } + get_user_first_message: { + Args: { active_user_row: unknown } + Returns: { + channel_id: number | null + data: Json | null + id: number | null + message: string | null + username: string | null + }[] + } + get_user_messages: { + Args: { user_row: Database['public']['Tables']['users']['Row'] } + Returns: { + channel_id: number + data: Json | null + id: number + message: string | null + username: string + }[] + } + get_user_profile: { + Args: { user_row: Database['public']['Tables']['users']['Row'] } + Returns: { + id: number + username: string | null + } + } + get_user_profile_non_nullable: { + Args: { user_row: Database['public']['Tables']['users']['Row'] } + Returns: { + id: number + username: string | null + }[] + } + get_user_recent_messages: { + Args: + | { active_user_row: unknown } + | { user_row: Database['public']['Tables']['users']['Row'] } + Returns: { + channel_id: number | null + data: Json | null + id: number | null + message: string | null + username: string | null + }[] + } get_username_and_status: { Args: { name_param: string } Returns: { @@ -505,6 +742,52 @@ export type Database = { Args: { name_param: string } Returns: Database['public']['Enums']['user_status'] } + polymorphic_function_with_different_return: { + Args: { '': boolean } | { '': number } | { '': string } + Returns: number + } + polymorphic_function_with_no_params_or_unnamed: { + Args: Record | { '': boolean } | { '': string } + Returns: number + } + polymorphic_function_with_unnamed_default: { + Args: Record | { ''?: number } | { ''?: string } + Returns: number + } + polymorphic_function_with_unnamed_default_overload: { + Args: Record | { ''?: boolean } | { ''?: number } | { ''?: string } + Returns: number + } + polymorphic_function_with_unnamed_integer: { + Args: { '': number } + Returns: number + } + polymorphic_function_with_unnamed_json: { + Args: { '': Json } + Returns: number + } + polymorphic_function_with_unnamed_jsonb: { + Args: { '': Json } + Returns: number + } + polymorphic_function_with_unnamed_text: { + Args: { '': string } + Returns: number + } + postgrest_resolvable_with_override_function: { + Args: + | Record + | { a: string } + | { b: number } + | { cid: number; search?: string } + | { profile_id: number } + | { user_row: Database['public']['Tables']['users']['Row'] } + Returns: undefined + } + postgrest_unresolvable_function: { + Args: Record | { a: number } | { a: string } + Returns: undefined + } set_users_offline: { Args: { name_param: string } Returns: { diff --git a/packages/core/postgrest-js/test/types.override-with-options-postgrest13.ts b/packages/core/postgrest-js/test/types.override-with-options-postgrest13.ts index 4769afdc4..ab7999ab8 100644 --- a/packages/core/postgrest-js/test/types.override-with-options-postgrest13.ts +++ b/packages/core/postgrest-js/test/types.override-with-options-postgrest13.ts @@ -28,6 +28,9 @@ export type Database = MergeDeep< } } } + Views: {} + Enums: {} + CompositeTypes: {} } public: { Tables: { @@ -43,6 +46,9 @@ export type Database = MergeDeep< } } } + Views: {} + Enums: {} + CompositeTypes: {} } } > diff --git a/packages/core/postgrest-js/test/types.override.ts b/packages/core/postgrest-js/test/types.override.ts index 86c998ea1..1f6568eb0 100644 --- a/packages/core/postgrest-js/test/types.override.ts +++ b/packages/core/postgrest-js/test/types.override.ts @@ -32,6 +32,9 @@ export type Database = MergeDeep< } } } + Views: {} + Enums: {} + CompositeTypes: {} } public: { Tables: { @@ -47,25 +50,33 @@ export type Database = MergeDeep< } } } + Functions: {} + Views: {} + Enums: {} + CompositeTypes: {} } } > -type DefaultSchema = Database[Extract] +type DatabaseWithoutInternals = Omit + +type DefaultSchema = DatabaseWithoutInternals[Extract] export type Tables< DefaultSchemaTableNameOrOptions extends | keyof (DefaultSchema['Tables'] & DefaultSchema['Views']) - | { schema: keyof Database }, + | { schema: keyof DatabaseWithoutInternals }, TableName extends DefaultSchemaTableNameOrOptions extends { - schema: keyof Database + schema: keyof DatabaseWithoutInternals } - ? keyof (Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] & - Database[DefaultSchemaTableNameOrOptions['schema']]['Views']) + ? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'] & + DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Views']) : never = never, -> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } - ? (Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] & - Database[DefaultSchemaTableNameOrOptions['schema']]['Views'])[TableName] extends { +> = DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals +} + ? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'] & + DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Views'])[TableName] extends { Row: infer R } ? R @@ -81,14 +92,16 @@ export type Tables< export type TablesInsert< DefaultSchemaTableNameOrOptions extends | keyof DefaultSchema['Tables'] - | { schema: keyof Database }, + | { schema: keyof DatabaseWithoutInternals }, TableName extends DefaultSchemaTableNameOrOptions extends { - schema: keyof Database + schema: keyof DatabaseWithoutInternals } - ? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] + ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'] : never = never, -> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } - ? Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends { +> = DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals +} + ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends { Insert: infer I } ? I @@ -104,14 +117,16 @@ export type TablesInsert< export type TablesUpdate< DefaultSchemaTableNameOrOptions extends | keyof DefaultSchema['Tables'] - | { schema: keyof Database }, + | { schema: keyof DatabaseWithoutInternals }, TableName extends DefaultSchemaTableNameOrOptions extends { - schema: keyof Database + schema: keyof DatabaseWithoutInternals } - ? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] + ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'] : never = never, -> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } - ? Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends { +> = DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals +} + ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends { Update: infer U } ? U @@ -125,14 +140,18 @@ export type TablesUpdate< : never export type Enums< - DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums'] | { schema: keyof Database }, + DefaultSchemaEnumNameOrOptions extends + | keyof DefaultSchema['Enums'] + | { schema: keyof DatabaseWithoutInternals }, EnumName extends DefaultSchemaEnumNameOrOptions extends { - schema: keyof Database + schema: keyof DatabaseWithoutInternals } - ? keyof Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums'] + ? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions['schema']]['Enums'] : never = never, -> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database } - ? Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums'][EnumName] +> = DefaultSchemaEnumNameOrOptions extends { + schema: keyof DatabaseWithoutInternals +} + ? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions['schema']]['Enums'][EnumName] : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums'] ? DefaultSchema['Enums'][DefaultSchemaEnumNameOrOptions] : never @@ -140,14 +159,24 @@ export type Enums< export type CompositeTypes< PublicCompositeTypeNameOrOptions extends | keyof DefaultSchema['CompositeTypes'] - | { schema: keyof Database }, + | { schema: keyof DatabaseWithoutInternals }, CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { - schema: keyof Database + schema: keyof DatabaseWithoutInternals } - ? keyof Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'] + ? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'] : never = never, -> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database } - ? Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName] +> = PublicCompositeTypeNameOrOptions extends { + schema: keyof DatabaseWithoutInternals +} + ? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName] : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema['CompositeTypes'] ? DefaultSchema['CompositeTypes'][PublicCompositeTypeNameOrOptions] : never + +export const Constants = { + public: { + Enums: { + user_status: ['ONLINE', 'OFFLINE'], + }, + }, +} as const diff --git a/packages/core/supabase-js/src/SupabaseClient.ts b/packages/core/supabase-js/src/SupabaseClient.ts index 0526ef3cd..511d999c7 100644 --- a/packages/core/supabase-js/src/SupabaseClient.ts +++ b/packages/core/supabase-js/src/SupabaseClient.ts @@ -1,27 +1,33 @@ +import type { AuthChangeEvent } from '@supabase/auth-js' import { FunctionsClient } from '@supabase/functions-js' -import { AuthChangeEvent } from '@supabase/auth-js' import { PostgrestClient, - PostgrestFilterBuilder, - PostgrestQueryBuilder, + type PostgrestFilterBuilder, + type PostgrestQueryBuilder, } from '@supabase/postgrest-js' import { - RealtimeChannel, - RealtimeChannelOptions, + type RealtimeChannel, + type RealtimeChannelOptions, RealtimeClient, - RealtimeClientOptions, + type RealtimeClientOptions, } from '@supabase/realtime-js' import { StorageClient as SupabaseStorageClient } from '@supabase/storage-js' import { - DEFAULT_GLOBAL_OPTIONS, - DEFAULT_DB_OPTIONS, DEFAULT_AUTH_OPTIONS, + DEFAULT_DB_OPTIONS, + DEFAULT_GLOBAL_OPTIONS, DEFAULT_REALTIME_OPTIONS, } from './lib/constants' import { fetchWithAuth } from './lib/fetch' import { applySettingDefaults, validateSupabaseUrl } from './lib/helpers' import { SupabaseAuthClient } from './lib/SupabaseAuthClient' -import { Fetch, GenericSchema, SupabaseClientOptions, SupabaseAuthClientOptions } from './lib/types' +import type { + Fetch, + GenericSchema, + SupabaseAuthClientOptions, + SupabaseClientOptions, +} from './lib/types' +import { GetRpcFunctionFilterBuilderByArgs } from './lib/rest/types/common/rpc' /** * Supabase Client. @@ -238,28 +244,44 @@ export default class SupabaseClient< * `"estimated"`: Uses exact count for low numbers and planned count for high * numbers. */ - rpc( + rpc< + FnName extends string & keyof Schema['Functions'], + Args extends Schema['Functions'][FnName]['Args'] = never, + FilterBuilder extends GetRpcFunctionFilterBuilderByArgs< + Schema, + FnName, + Args + > = GetRpcFunctionFilterBuilderByArgs, + >( fn: FnName, - args: Fn['Args'] = {}, + args: Args = {} as Args, options: { head?: boolean get?: boolean count?: 'exact' | 'planned' | 'estimated' - } = {} + } = { + head: false, + get: false, + count: undefined, + } ): PostgrestFilterBuilder< ClientOptions, Schema, - Fn['Returns'] extends any[] - ? Fn['Returns'][number] extends Record - ? Fn['Returns'][number] - : never - : never, - Fn['Returns'], - FnName, - null, + FilterBuilder['Row'], + FilterBuilder['Result'], + FilterBuilder['RelationName'], + FilterBuilder['Relationships'], 'RPC' > { - return this.rest.rpc(fn, args, options) + return this.rest.rpc(fn, args, options) as unknown as PostgrestFilterBuilder< + ClientOptions, + Schema, + FilterBuilder['Row'], + FilterBuilder['Result'], + FilterBuilder['RelationName'], + FilterBuilder['Relationships'], + 'RPC' + > } /** @@ -355,7 +377,7 @@ export default class SupabaseClient< } private _listenForAuthEvents() { - let data = this.auth.onAuthStateChange((event, session) => { + const data = this.auth.onAuthStateChange((event, session) => { this._handleTokenChanged(event, 'CLIENT', session?.access_token) }) return data diff --git a/packages/core/supabase-js/src/lib/rest/types/common/common.ts b/packages/core/supabase-js/src/lib/rest/types/common/common.ts new file mode 100644 index 000000000..9ad962ef5 --- /dev/null +++ b/packages/core/supabase-js/src/lib/rest/types/common/common.ts @@ -0,0 +1,56 @@ +// Types that are shared between supabase-js and postgrest-js + +export type Fetch = typeof fetch + +export type GenericRelationship = { + foreignKeyName: string + columns: string[] + isOneToOne?: boolean + referencedRelation: string + referencedColumns: string[] +} + +export type GenericTable = { + Row: Record + Insert: Record + Update: Record + Relationships: GenericRelationship[] +} + +export type GenericUpdatableView = { + Row: Record + Insert: Record + Update: Record + Relationships: GenericRelationship[] +} + +export type GenericNonUpdatableView = { + Row: Record + Relationships: GenericRelationship[] +} + +export type GenericView = GenericUpdatableView | GenericNonUpdatableView + +export type GenericSetofOption = { + isSetofReturn?: boolean | undefined + isOneToOne?: boolean | undefined + isNotNullable?: boolean | undefined + to: string + from: string +} + +export type GenericFunction = { + Args: Record | never + Returns: unknown + SetofOptions?: GenericSetofOption +} + +export type GenericSchema = { + Tables: Record + Views: Record + Functions: Record +} + +export type ClientServerOptions = { + PostgrestVersion?: string +} diff --git a/packages/core/supabase-js/src/lib/rest/types/common/rpc.ts b/packages/core/supabase-js/src/lib/rest/types/common/rpc.ts new file mode 100644 index 000000000..52e57419a --- /dev/null +++ b/packages/core/supabase-js/src/lib/rest/types/common/rpc.ts @@ -0,0 +1,135 @@ +import type { GenericFunction, GenericSchema, GenericSetofOption } from './common' + +// Functions matching utils +type IsMatchingArgs< + FnArgs extends GenericFunction['Args'], + PassedArgs extends GenericFunction['Args'], +> = [FnArgs] extends [Record] + ? PassedArgs extends Record + ? true + : false + : keyof PassedArgs extends keyof FnArgs + ? PassedArgs extends FnArgs + ? true + : false + : false + +type MatchingFunctionArgs< + Fn extends GenericFunction, + Args extends GenericFunction['Args'], +> = Fn extends { Args: infer A extends GenericFunction['Args'] } + ? IsMatchingArgs extends true + ? Fn + : never + : false + +type FindMatchingFunctionByArgs< + FnUnion, + Args extends GenericFunction['Args'], +> = FnUnion extends infer Fn extends GenericFunction ? MatchingFunctionArgs : false + +// Types for working with database schemas +type TablesAndViews = Schema['Tables'] & Exclude + +// Utility types for working with unions +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void + ? I + : never + +type LastOf = + UnionToIntersection T : never> extends () => infer R ? R : never + +type IsAny = 0 extends 1 & T ? true : false + +type ExactMatch = [T] extends [S] ? ([S] extends [T] ? true : false) : false + +type ExtractExactFunction = Fns extends infer F + ? F extends GenericFunction + ? ExactMatch extends true + ? F + : never + : never + : never + +type IsNever = [T] extends [never] ? true : false + +type RpcFunctionNotFound = { + Row: any + Result: { + error: true + } & "Couldn't infer function definition matching provided arguments" + RelationName: FnName + Relationships: null +} + +export type GetRpcFunctionFilterBuilderByArgs< + Schema extends GenericSchema, + FnName extends string & keyof Schema['Functions'], + Args, +> = { + 0: Schema['Functions'][FnName] + // If the Args is exactly never (function call without any params) + 1: IsAny extends true + ? any + : IsNever extends true + ? // This is for retro compatibility, if the funcition is defined with an single return and an union of Args + // we fallback to the last function definition matched by name + IsNever> extends true + ? LastOf + : ExtractExactFunction + : Args extends Record + ? LastOf + : // Otherwise, we attempt to match with one of the function definition in the union based + // on the function arguments provided + Args extends GenericFunction['Args'] + ? // This is for retro compatibility, if the funcition is defined with an single return and an union of Args + // we fallback to the last function definition matched by name + IsNever< + LastOf> + > extends true + ? LastOf + : // Otherwise, we use the arguments based function definition narrowing to get the right value + LastOf> + : // If we can't find a matching function by args, we try to find one by function name + ExtractExactFunction extends GenericFunction + ? ExtractExactFunction + : any +}[1] extends infer Fn + ? // If we are dealing with an non-typed client everything is any + IsAny extends true + ? { Row: any; Result: any; RelationName: FnName; Relationships: null } + : // Otherwise, we use the arguments based function definition narrowing to get the rigt value + Fn extends GenericFunction + ? { + Row: Fn['SetofOptions'] extends GenericSetofOption + ? Fn['SetofOptions']['isSetofReturn'] extends true + ? TablesAndViews[Fn['SetofOptions']['to']]['Row'] + : TablesAndViews[Fn['SetofOptions']['to']]['Row'] + : Fn['Returns'] extends any[] + ? Fn['Returns'][number] extends Record + ? Fn['Returns'][number] + : never + : Fn['Returns'] extends Record + ? Fn['Returns'] + : never + Result: Fn['SetofOptions'] extends GenericSetofOption + ? Fn['SetofOptions']['isSetofReturn'] extends true + ? Fn['SetofOptions']['isOneToOne'] extends true + ? Fn['Returns'][] + : Fn['Returns'] + : Fn['Returns'] + : Fn['Returns'] + RelationName: Fn['SetofOptions'] extends GenericSetofOption + ? Fn['SetofOptions']['to'] + : FnName + Relationships: Fn['SetofOptions'] extends GenericSetofOption + ? Fn['SetofOptions']['to'] extends keyof Schema['Tables'] + ? Schema['Tables'][Fn['SetofOptions']['to']]['Relationships'] + : Schema['Views'][Fn['SetofOptions']['to']]['Relationships'] + : null + } + : // If we failed to find the function by argument, we still pass with any but also add an overridable + Fn extends false + ? RpcFunctionNotFound + : RpcFunctionNotFound + : RpcFunctionNotFound diff --git a/packages/core/supabase-js/src/lib/types.ts b/packages/core/supabase-js/src/lib/types.ts index e12187f33..d20845692 100644 --- a/packages/core/supabase-js/src/lib/types.ts +++ b/packages/core/supabase-js/src/lib/types.ts @@ -2,6 +2,24 @@ import { AuthClient } from '@supabase/auth-js' import { RealtimeClientOptions } from '@supabase/realtime-js' import { PostgrestError } from '@supabase/postgrest-js' import type { StorageClientOptions } from '@supabase/storage-js' +import type { + GenericSchema, + GenericRelationship, + GenericTable, + GenericUpdatableView, + GenericNonUpdatableView, + GenericView, + GenericFunction, +} from './rest/types/common/common' +export type { + GenericSchema, + GenericRelationship, + GenericTable, + GenericUpdatableView, + GenericNonUpdatableView, + GenericView, + GenericFunction, +} type AuthClientOptions = ConstructorParameters[0] @@ -90,41 +108,6 @@ export type SupabaseClientOptions = { accessToken?: () => Promise } -export type GenericRelationship = { - foreignKeyName: string - columns: string[] - isOneToOne?: boolean - referencedRelation: string - referencedColumns: string[] -} - -export type GenericTable = { - Row: Record - Insert: Record - Update: Record - Relationships: GenericRelationship[] -} - -export type GenericUpdatableView = GenericTable - -export type GenericNonUpdatableView = { - Row: Record - Relationships: GenericRelationship[] -} - -export type GenericView = GenericUpdatableView | GenericNonUpdatableView - -export type GenericFunction = { - Args: Record - Returns: unknown -} - -export type GenericSchema = { - Tables: Record - Views: Record - Functions: Record -} - /** * Helper types for query results. */ diff --git a/packages/core/supabase-js/test/types/index.test-d.ts b/packages/core/supabase-js/test/types/index.test-d.ts index a95d6c001..5f4d6713b 100644 --- a/packages/core/supabase-js/test/types/index.test-d.ts +++ b/packages/core/supabase-js/test/types/index.test-d.ts @@ -71,7 +71,7 @@ const supabase = createClient(URL, KEY) // rpc return type { - const { data, error } = await supabase.rpc('get_status') + const { data, error } = await supabase.rpc('get_status', { name_param: 'supabot' }) if (error) { throw new Error(error.message) }