diff --git a/package-lock.json b/package-lock.json index 1149220f..fdda4634 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@supabase/auth-js": "2.71.1", "@supabase/functions-js": "2.4.5", "@supabase/node-fetch": "2.6.15", - "@supabase/postgrest-js": "1.19.4", + "@supabase/postgrest-js": "1.21.3", "@supabase/realtime-js": "2.15.1", "@supabase/storage-js": "^2.10.4" }, @@ -1117,9 +1117,9 @@ } }, "node_modules/@supabase/postgrest-js": { - "version": "1.19.4", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.4.tgz", - "integrity": "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==", + "version": "1.21.3", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.21.3.tgz", + "integrity": "sha512-rg3DmmZQKEVCreXq6Am29hMVe1CzemXyIWVYyyua69y6XubfP+DzGfLxME/1uvdgwqdoaPbtjBDpEBhqxq1ZwA==", "license": "MIT", "dependencies": { "@supabase/node-fetch": "^2.6.14" diff --git a/package.json b/package.json index 9a801a41..d4bf3e22 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "@supabase/auth-js": "2.71.1", "@supabase/functions-js": "2.4.5", "@supabase/node-fetch": "2.6.15", - "@supabase/postgrest-js": "1.19.4", + "@supabase/postgrest-js": "1.21.3", "@supabase/realtime-js": "2.15.1", "@supabase/storage-js": "^2.10.4" }, diff --git a/src/SupabaseClient.ts b/src/SupabaseClient.ts index ca5733cd..1a0bb0ff 100644 --- a/src/SupabaseClient.ts +++ b/src/SupabaseClient.ts @@ -30,12 +30,36 @@ import { Fetch, GenericSchema, SupabaseClientOptions, SupabaseAuthClientOptions */ export default class SupabaseClient< Database = any, - SchemaName extends string & keyof Database = 'public' extends keyof Database + // The second type parameter is also used for specifying db_schema, so we + // support both cases. + // TODO: Allow setting db_schema from ClientOptions. + SchemaNameOrClientOptions extends + | (string & keyof Omit) + | { PostgrestVersion: string } = 'public' extends keyof Omit ? 'public' - : string & keyof Database, - Schema extends GenericSchema = Database[SchemaName] extends GenericSchema - ? Database[SchemaName] - : any + : string & keyof Omit, + SchemaName extends string & + keyof Omit = SchemaNameOrClientOptions extends string & + keyof Omit + ? SchemaNameOrClientOptions + : 'public' extends keyof Omit + ? 'public' + : string & keyof Omit, '__InternalSupabase'>, + Schema extends Omit[SchemaName] extends GenericSchema + ? Omit[SchemaName] + : never = Omit[SchemaName] extends GenericSchema + ? Omit[SchemaName] + : never, + ClientOptions extends { PostgrestVersion: string } = SchemaNameOrClientOptions extends string & + keyof Omit + ? // If the version isn't explicitly set, look for it in the __InternalSupabase object to infer the right version + Database extends { __InternalSupabase: { PostgrestVersion: string } } + ? Database['__InternalSupabase'] + : // otherwise default to 12 + { PostgrestVersion: '12' } + : SchemaNameOrClientOptions extends { PostgrestVersion: string } + ? SchemaNameOrClientOptions + : never > { /** * Supabase Auth allows you to create and manage user sessions for access to data that is secured by access policies. @@ -51,7 +75,7 @@ export default class SupabaseClient< protected authUrl: URL protected storageUrl: URL protected functionsUrl: URL - protected rest: PostgrestClient + protected rest: PostgrestClient protected storageKey: string protected fetch?: Fetch protected changedAccessToken?: string @@ -161,16 +185,16 @@ export default class SupabaseClient< from< TableName extends string & keyof Schema['Tables'], Table extends Schema['Tables'][TableName] - >(relation: TableName): PostgrestQueryBuilder + >(relation: TableName): PostgrestQueryBuilder from( relation: ViewName - ): PostgrestQueryBuilder + ): PostgrestQueryBuilder /** * Perform a query on a table or a view. * * @param relation - The table or view name to query */ - from(relation: string): PostgrestQueryBuilder { + from(relation: string): PostgrestQueryBuilder { return this.rest.from(relation) } @@ -182,10 +206,11 @@ export default class SupabaseClient< * * @param schema - The schema to query */ - schema( + schema>( schema: DynamicSchema ): PostgrestClient< Database, + ClientOptions, DynamicSchema, Database[DynamicSchema] extends GenericSchema ? Database[DynamicSchema] : any > { @@ -225,6 +250,7 @@ export default class SupabaseClient< count?: 'exact' | 'planned' | 'estimated' } = {} ): PostgrestFilterBuilder< + ClientOptions, Schema, Fn['Returns'] extends any[] ? Fn['Returns'][number] extends Record @@ -233,7 +259,8 @@ export default class SupabaseClient< : never, Fn['Returns'], FnName, - null + null, + 'RPC' > { return this.rest.rpc(fn, args, options) } diff --git a/src/index.ts b/src/index.ts index 90d2b550..95b97461 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import SupabaseClient from './SupabaseClient' -import type { GenericSchema, SupabaseClientOptions } from './lib/types' +import type { SupabaseClientOptions } from './lib/types' export * from '@supabase/auth-js' export type { User as AuthUser, Session as AuthSession } from '@supabase/auth-js' @@ -26,18 +26,28 @@ export type { SupabaseClientOptions, QueryResult, QueryData, QueryError } from ' */ export const createClient = < Database = any, - SchemaName extends string & keyof Database = 'public' extends keyof Database + SchemaNameOrClientOptions extends + | (string & keyof Omit) + | { PostgrestVersion: string } = 'public' extends keyof Omit ? 'public' - : string & keyof Database, - Schema extends GenericSchema = Database[SchemaName] extends GenericSchema - ? Database[SchemaName] - : any + : string & keyof Omit, + SchemaName extends string & + keyof Omit = SchemaNameOrClientOptions extends string & + keyof Omit + ? SchemaNameOrClientOptions + : 'public' extends keyof Omit + ? 'public' + : string & keyof Omit, '__InternalSupabase'> >( supabaseUrl: string, supabaseKey: string, options?: SupabaseClientOptions -): SupabaseClient => { - return new SupabaseClient(supabaseUrl, supabaseKey, options) +): SupabaseClient => { + return new SupabaseClient( + supabaseUrl, + supabaseKey, + options + ) } // Check for Node.js <= 18 deprecation diff --git a/test/integration/next/app/layout.tsx b/test/integration/next/app/layout.tsx new file mode 100644 index 00000000..0cc4b8ea --- /dev/null +++ b/test/integration/next/app/layout.tsx @@ -0,0 +1,14 @@ +import type { Metadata } from 'next' + +export const metadata: Metadata = { + title: 'Supabase Integration Test', + description: 'Testing Supabase integration with Next.js', +} + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/integration/next/package.json b/test/integration/next/package.json index 6ee2dc94..374cbda1 100644 --- a/test/integration/next/package.json +++ b/test/integration/next/package.json @@ -7,7 +7,8 @@ "lint": "next lint", "test": "playwright test", "test:ui": "playwright test --ui", - "test:debug": "playwright test --debug" + "test:debug": "playwright test --debug", + "test:types": "npx tsd --files tests/types/*.test-d.ts" }, "dependencies": { "@radix-ui/react-checkbox": "^1.3.1", @@ -37,6 +38,7 @@ "postcss": "^8", "tailwindcss": "^3.4.1", "tailwindcss-animate": "^1.0.7", + "tsd": "^0.33.0", "typescript": "^5" } } diff --git a/test/integration/next/tests/types/types.test-d.ts b/test/integration/next/tests/types/types.test-d.ts new file mode 100644 index 00000000..830d4f59 --- /dev/null +++ b/test/integration/next/tests/types/types.test-d.ts @@ -0,0 +1,180 @@ +import { createServerClient, createBrowserClient } from '@supabase/ssr' +import { expectType } from 'tsd' + +// Copied from ts-expect +// https://github.com/TypeStrong/ts-expect/blob/master/src/index.ts#L23-L27 +export type TypeEqual = (() => T extends Target ? 1 : 2) extends < + T +>() => T extends Value ? 1 : 2 + ? true + : false + +type Database = { + public: { + Tables: { + 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: [] + } + } + Views: { + [_ in never]: never + } + Functions: { + [_ in never]: never + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } +} + +{ + // createBrowserClient should return a typed client + const pg12Client = createBrowserClient('HTTP://localhost:3000', '') + const res12 = await pg12Client.from('shops').select('*') + expectType< + TypeEqual< + | { + address: string | null + id: number + shop_geom: unknown | null + }[] + | null, + typeof res12.data + > + >(true) +} + +{ + // createBrowserClient should infer everything to any without types provided + const pg12Client = createBrowserClient('HTTP://localhost:3000', '') + const res12 = await pg12Client.from('shops').select('address, id, relation(field)') + expectType< + TypeEqual< + | { + address: any + id: any + relation: { + field: any + }[] + }[] + | null, + typeof res12.data + > + >(true) +} + +{ + // createServerClient should return a typed client + const pg12Server = createServerClient('HTTP://localhost:3000', '') + const res12 = await pg12Server.from('shops').select('*') + expectType< + TypeEqual< + | { + address: string | null + id: number + shop_geom: unknown | null + }[] + | null, + typeof res12.data + > + >(true) +} + +{ + // createServerClient should infer everything to any without types provided + const pg12Server = createServerClient('HTTP://localhost:3000', '') + const res12 = await pg12Server.from('shops').select('address, id, relation(field)') + expectType< + TypeEqual< + | { + address: any + id: any + relation: { + field: any + }[] + }[] + | null, + typeof res12.data + > + >(true) +} +// Should be able to get a PostgrestVersion 13 client from __InternalSupabase +{ + type DatabaseWithInternals = { + __InternalSupabase: { + PostgrestVersion: '13' + } + public: { + Tables: { + 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: [] + } + } + Views: { + [_ in never]: never + } + Functions: { + [_ in never]: never + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } + } + const pg13BrowserClient = createBrowserClient('HTTP://localhost:3000', '') + const pg13ServerClient = createServerClient('HTTP://localhost:3000', '', { + cookies: { getAll: () => [], setAll: () => {} }, + }) + const res13 = await pg13BrowserClient.from('shops').update({ id: 21 }).maxAffected(1) + expectType(null) + const res13Server = await pg13ServerClient.from('shops').update({ id: 21 }).maxAffected(1) + expectType(null) +} +{ + // Should default to PostgrestVersion 12 + const pg12BrowserClient = createBrowserClient('HTTP://localhost:3000', '') + const pg12ServerClient = createServerClient('HTTP://localhost:3000', '', { + cookies: { getAll: () => [], setAll: () => {} }, + }) + const res12 = await pg12BrowserClient.from('shops').update({ id: 21 }).maxAffected(1) + expectType('maxAffected method only available on postgrest 13+') + const res12Server = await pg12ServerClient.from('shops').update({ id: 21 }).maxAffected(1) + expectType('maxAffected method only available on postgrest 13+') +} diff --git a/test/integration/next/tsconfig.json b/test/integration/next/tsconfig.json index ac0369a8..32d8de30 100644 --- a/test/integration/next/tsconfig.json +++ b/test/integration/next/tsconfig.json @@ -24,5 +24,5 @@ } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "test/types/*.test-d.ts"] } diff --git a/test/types/index.test-d.ts b/test/types/index.test-d.ts index 0ae3fc35..a95d6c00 100644 --- a/test/types/index.test-d.ts +++ b/test/types/index.test-d.ts @@ -1,5 +1,5 @@ import { expectError, expectType } from 'tsd' -import { PostgrestSingleResponse, createClient } from '../../src/index' +import { PostgrestSingleResponse, SupabaseClient, createClient } from '../../src/index' import { Database, Json } from '../types' const URL = 'http://localhost:3000' @@ -136,3 +136,132 @@ const supabase = createClient(URL, KEY) }[] >(channels) } + +// Test Postgrest13 +// should be able to declare specific PostgrestVersion +{ + // @ts-expect-error should raise error if provinding invalid version + createClient('HTTP://localhost:3000', KEY) +} + +// should be able to infer PostgrestVersion from Database __InternalSupabase +{ + type DatabaseWithInternals = { + __InternalSupabase: { + PostgrestVersion: '13' + } + public: { + Tables: { + 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: [] + } + } + Views: { + [_ in never]: never + } + Functions: { + [_ in never]: never + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } + } + // Note: The template argument properties (PostgrestVersion) will not be autocompleted + // due to a Typescript bug tracked here: https://github.com/microsoft/TypeScript/issues/56299 + const pg13Client = createClient('HTTP://localhost:3000', KEY) + // @ts-expect-error should raise error if providing __InternalSupabase as schema name + createClient('HTTP://localhost:3000', KEY) + // @ts-expect-error should raise error if providing __InternalSupabase as schema name + new SupabaseClient('HTTP://localhost:3000', KEY) + const pg12Client = createClient('HTTP://localhost:3000', KEY) + const res13 = await pg13Client.from('shops').update({ id: 21 }).maxAffected(1) + const res12 = await pg12Client.from('shops').update({ id: 21 }).maxAffected(1) + const pg13ClientNew = new SupabaseClient('HTTP://localhost:3000', KEY) + const res13New = await pg13ClientNew.from('shops').update({ id: 21 }).maxAffected(1) + expectType(null) + expectType(null) + expectType('maxAffected method only available on postgrest 13+') + // Explicitly set PostgrestVersion should override the inferred __InternalSupabase schema version + const internal13Set12 = new SupabaseClient( + URL, + KEY + ) + const resinternal13Set12 = await internal13Set12.from('shops').update({ id: 21 }).maxAffected(1) + // The explicitly set PostgrestVersion should override the inferred __InternalSupabase schema version + expectType('maxAffected method only available on postgrest 13+') +} + +// createClient with custom schema +{ + const pg12CustomSchemaClient = createClient(URL, KEY, { + db: { schema: 'personal' }, + }) + const pg12CustomSchemaNewClient = new SupabaseClient(URL, KEY, { + db: { schema: 'personal' }, + }) + const res12new = await pg12CustomSchemaNewClient + .from('users') + .update({ username: 'test' }) + .maxAffected(1) + const res12 = await pg12CustomSchemaClient + .from('users') + .update({ username: 'test' }) + .maxAffected(1) + expectType('maxAffected method only available on postgrest 13+') + expectType('maxAffected method only available on postgrest 13+') + // @ts-expect-error should raise error if providing table name not in the schema + pg12CustomSchemaClient.from('channels_details') + // @ts-expect-error should raise error if providing table name not in the schema + pg12CustomSchemaNewClient.from('channels_details') +} + +// createClient with custom schema and PostgrestVersion explicitly set +{ + const pg13CustomSchemaClient = createClient( + URL, + KEY, + { + db: { schema: 'personal' }, + } + ) + const pg12CustomSchemaNewClient = new SupabaseClient< + Database, + { PostgrestVersion: '12' }, + 'personal' + >(URL, KEY, { + db: { schema: 'personal' }, + }) + const res12new = await pg12CustomSchemaNewClient + .from('users') + .update({ username: 'test' }) + .maxAffected(1) + const res13 = await pg13CustomSchemaClient + .from('users') + .update({ username: 'test' }) + .maxAffected(1) + expectType(null) + expectType('maxAffected method only available on postgrest 13+') + // @ts-expect-error should raise error if providing table name not in the schema + pg12CustomSchemaClient.from('channels_details') + // @ts-expect-error should raise error if providing table name not in the schema + pg13CustomSchemaClient.from('channels_details') +} diff --git a/test/unit/SupabaseClient.test.ts b/test/unit/SupabaseClient.test.ts index 8e91f2da..81f420be 100644 --- a/test/unit/SupabaseClient.test.ts +++ b/test/unit/SupabaseClient.test.ts @@ -74,18 +74,20 @@ describe('SupabaseClient', () => { test('should have custom header set', () => { const customHeader = { 'X-Test-Header': 'value' } const request = createClient(URL, KEY, { global: { headers: customHeader } }).rpc('') - // @ts-ignore - const getHeaders = request.headers - expect(getHeaders).toHaveProperty('X-Test-Header', 'value') + //@ts-expect-error headers is protected attribute + const requestHeader = request.headers.get('X-Test-Header') + expect(requestHeader).toBe(customHeader['X-Test-Header']) }) test('should merge custom headers with default headers', () => { const customHeader = { 'X-Test-Header': 'value' } - const client = createClient(URL, KEY, { global: { headers: customHeader } }) - // @ts-ignore - expect(client.headers).toHaveProperty('X-Test-Header', 'value') - // @ts-ignore - expect(client.headers).toHaveProperty('X-Client-Info') + const request = createClient(URL, KEY, { global: { headers: customHeader } }).rpc('') + + //@ts-expect-error headers is protected attribute + const requestHeader = request.headers.get('X-Test-Header') + expect(requestHeader).toBe(customHeader['X-Test-Header']) + //@ts-expect-error headers is protected attribute + expect(request.headers.get('X-Client-Info')).not.toBeNull() }) })