From 74c8bb6d09b7d04e9b316080cfa7296dc0d51094 Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 15 May 2025 23:25:11 +0200 Subject: [PATCH 01/15] wip --- src/PostgrestFilterBuilder.ts | 21 +++- src/PostgrestQueryBuilder.ts | 32 ++++-- src/PostgrestTransformBuilder.ts | 14 ++- src/select-query-parser/result.ts | 20 +++- src/utils.ts | 64 +++++++++++ test/db/docker-compose.yml | 2 +- test/index.test.ts | 2 + test/max-affected.ts | 129 ++++++++++++++++++++++ test/relationships.ts | 4 +- test/select-query-parser/select.test-d.ts | 5 +- test/utils.ts | 128 +++++++++++++++++++++ 11 files changed, 397 insertions(+), 24 deletions(-) create mode 100644 src/utils.ts create mode 100644 test/max-affected.ts create mode 100644 test/utils.ts diff --git a/src/PostgrestFilterBuilder.ts b/src/PostgrestFilterBuilder.ts index c0de7d33..d7c8969d 100644 --- a/src/PostgrestFilterBuilder.ts +++ b/src/PostgrestFilterBuilder.ts @@ -1,6 +1,7 @@ import PostgrestTransformBuilder from './PostgrestTransformBuilder' import { JsonPathToAccessor, JsonPathToType } from './select-query-parser/utils' import { GenericSchema } from './types' +import { HeaderManager } from './utils' type FilterOperator = | 'eq' @@ -69,13 +70,29 @@ type ResolveFilterRelationshipValue< : unknown : never +export type InvalidMethodError = { Error: S } + export default class PostgrestFilterBuilder< Schema extends GenericSchema, Row extends Record, Result, RelationName = unknown, - Relationships = unknown -> extends PostgrestTransformBuilder { + Relationships = unknown, + Method = unknown +> extends PostgrestTransformBuilder { + maxAffected( + value: number + ): Method extends 'PATCH' | 'DELETE' + ? this + : InvalidMethodError<'maxAffected method only available on update or delete'> { + const preferHeaderManager = new HeaderManager('Prefer', this.headers['Prefer']) + preferHeaderManager.add('handling=strict') + preferHeaderManager.add(`max-affected=${value}`) + this.headers['Prefer'] = preferHeaderManager.get() + return this as unknown as Method extends 'PATCH' | 'DELETE' + ? this + : InvalidMethodError<'maxAffected method only available on update or delete'> + } /** * Match only rows where `column` is equal to `value`. * diff --git a/src/PostgrestQueryBuilder.ts b/src/PostgrestQueryBuilder.ts index 44e7320a..7aed7df0 100644 --- a/src/PostgrestQueryBuilder.ts +++ b/src/PostgrestQueryBuilder.ts @@ -66,7 +66,14 @@ export default class PostgrestQueryBuilder< head?: boolean count?: 'exact' | 'planned' | 'estimated' } = {} - ): PostgrestFilterBuilder { + ): PostgrestFilterBuilder< + Schema, + Relation['Row'], + ResultOne[], + RelationName, + Relationships, + 'GET' + > { const method = head ? 'HEAD' : 'GET' // Remove whitespaces except when quoted let quoted = false @@ -103,14 +110,14 @@ export default class PostgrestQueryBuilder< options?: { count?: 'exact' | 'planned' | 'estimated' } - ): PostgrestFilterBuilder + ): PostgrestFilterBuilder insert( values: Row[], options?: { count?: 'exact' | 'planned' | 'estimated' defaultToNull?: boolean } - ): PostgrestFilterBuilder + ): PostgrestFilterBuilder /** * Perform an INSERT into the table or view. * @@ -146,7 +153,7 @@ export default class PostgrestQueryBuilder< count?: 'exact' | 'planned' | 'estimated' defaultToNull?: boolean } = {} - ): PostgrestFilterBuilder { + ): PostgrestFilterBuilder { const method = 'POST' const prefersHeaders = [] @@ -188,7 +195,7 @@ export default class PostgrestQueryBuilder< ignoreDuplicates?: boolean count?: 'exact' | 'planned' | 'estimated' } - ): PostgrestFilterBuilder + ): PostgrestFilterBuilder upsert( values: Row[], options?: { @@ -197,7 +204,7 @@ export default class PostgrestQueryBuilder< count?: 'exact' | 'planned' | 'estimated' defaultToNull?: boolean } - ): PostgrestFilterBuilder + ): PostgrestFilterBuilder /** * Perform an UPSERT on the table or view. Depending on the column(s) passed * to `onConflict`, `.upsert()` allows you to perform the equivalent of @@ -249,7 +256,7 @@ export default class PostgrestQueryBuilder< count?: 'exact' | 'planned' | 'estimated' defaultToNull?: boolean } = {} - ): PostgrestFilterBuilder { + ): PostgrestFilterBuilder { const method = 'POST' const prefersHeaders = [`resolution=${ignoreDuplicates ? 'ignore' : 'merge'}-duplicates`] @@ -313,7 +320,7 @@ export default class PostgrestQueryBuilder< }: { count?: 'exact' | 'planned' | 'estimated' } = {} - ): PostgrestFilterBuilder { + ): PostgrestFilterBuilder { const method = 'PATCH' const prefersHeaders = [] if (this.headers['Prefer']) { @@ -358,7 +365,14 @@ export default class PostgrestQueryBuilder< count, }: { count?: 'exact' | 'planned' | 'estimated' - } = {}): PostgrestFilterBuilder { + } = {}): PostgrestFilterBuilder< + Schema, + Relation['Row'], + null, + RelationName, + Relationships, + 'DELETE' + > { const method = 'DELETE' const prefersHeaders = [] if (count) { diff --git a/src/PostgrestTransformBuilder.ts b/src/PostgrestTransformBuilder.ts index c9fb5781..8545fef2 100644 --- a/src/PostgrestTransformBuilder.ts +++ b/src/PostgrestTransformBuilder.ts @@ -7,7 +7,8 @@ export default class PostgrestTransformBuilder< Row extends Record, Result, RelationName = unknown, - Relationships = unknown + Relationships = unknown, + Method = unknown > extends PostgrestBuilder { /** * Perform a SELECT on the query result. @@ -23,7 +24,7 @@ export default class PostgrestTransformBuilder< NewResultOne = GetResult >( columns?: Query - ): PostgrestTransformBuilder { + ): PostgrestTransformBuilder { // Remove whitespaces except when quoted let quoted = false const cleanedColumns = (columns ?? '*') @@ -48,7 +49,8 @@ export default class PostgrestTransformBuilder< Row, NewResultOne[], RelationName, - Relationships + Relationships, + Method > } @@ -314,14 +316,16 @@ export default class PostgrestTransformBuilder< Row, CheckMatchingArrayTypes, RelationName, - Relationships + Relationships, + Method > { return this as unknown as PostgrestTransformBuilder< Schema, Row, CheckMatchingArrayTypes, RelationName, - Relationships + Relationships, + Method > } } diff --git a/src/select-query-parser/result.ts b/src/select-query-parser/result.ts index a32567c2..59fe2480 100644 --- a/src/select-query-parser/result.ts +++ b/src/select-query-parser/result.ts @@ -401,12 +401,26 @@ type ProcessSpreadNode< ? Result extends SelectQueryError ? SelectQueryError : ExtractFirstProperty extends unknown[] - ? { - [K in Spread['target']['name']]: SelectQueryError<`"${RelationName}" and "${Spread['target']['name']}" do not form a many-to-one or one-to-one relationship spread not possible`> - } + ? // Spread over an many-to-many relationship, turn all the result fields into arrays + ProcessManyToManySpreadNodeResult : ProcessSpreadNodeResult : never +/** + * Helper type to process the result of a many-to-many spread node. + * Converts all fields in the spread object into arrays. + */ +type ProcessManyToManySpreadNodeResult = Result extends Record< + string, + SelectQueryError | null +> + ? Result + : ExtractFirstProperty extends infer SpreadedObject + ? SpreadedObject extends Array> + ? { [K in keyof SpreadedObject[number]]: Array } + : SelectQueryError<'An error occurred spreading the many-to-many object'> + : SelectQueryError<'An error occurred spreading the many-to-many object'> + /** * Helper type to process the result of a spread node. */ diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 00000000..47b0fbd6 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,64 @@ +export class HeaderManager { + private headers: Map> = new Map() + + /** + * Create a new HeaderManager, optionally parsing an existing header string + * @param header The header name to manage + * @param existingValue Optional existing header value to parse + */ + constructor(private readonly header: string, existingValue?: string) { + if (existingValue) { + this.parseHeaderString(existingValue) + } + } + + /** + * Parse an existing header string into the internal Set + * @param headerString The header string to parse + */ + private parseHeaderString(headerString: string): void { + if (!headerString.trim()) return + + const values = headerString.split(',') + values.forEach((value) => { + const trimmedValue = value.trim() + if (trimmedValue) { + this.add(trimmedValue) + } + }) + } + + /** + * Add a value to the header. If the header doesn't exist, it will be created. + * @param value The value to add + */ + add(value: string): void { + if (!this.headers.has(this.header)) { + this.headers.set(this.header, new Set()) + } + this.headers.get(this.header)!.add(value) + } + + /** + * Get the formatted string value for the header + */ + get(): string { + const values = this.headers.get(this.header) + return values ? Array.from(values).join(',') : '' + } + + /** + * Check if the header has a specific value + * @param value The value to check + */ + has(value: string): boolean { + return this.headers.get(this.header)?.has(value) ?? false + } + + /** + * Clear all values for the header + */ + clear(): void { + this.headers.delete(this.header) + } +} diff --git a/test/db/docker-compose.yml b/test/db/docker-compose.yml index c2f91a1d..22fc990c 100644 --- a/test/db/docker-compose.yml +++ b/test/db/docker-compose.yml @@ -3,7 +3,7 @@ version: '3' services: rest: - image: postgrest/postgrest:v12.2.0 + image: postgrest/postgrest:v13.0.0 ports: - '3000:3000' environment: diff --git a/test/index.test.ts b/test/index.test.ts index a7f2ffc6..80b93afb 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -4,3 +4,5 @@ import './filters' import './resource-embedding' import './transforms' import './rpc' +import './utils' +// import './max-affected' diff --git a/test/max-affected.ts b/test/max-affected.ts new file mode 100644 index 00000000..df30b1f6 --- /dev/null +++ b/test/max-affected.ts @@ -0,0 +1,129 @@ +import { PostgrestClient } from '../src/index' +import { Database } from './types.override' +import { expectType } from 'tsd' +import { InvalidMethodError } from '../src/PostgrestFilterBuilder' + +const REST_URL = 'http://localhost:3000' +const postgrest = new PostgrestClient(REST_URL) + +describe('maxAffected', () => { + // Type checking tests + test('maxAffected should show warning on non update / delete', async () => { + const resSelect = await postgrest.from('messages').select('*').maxAffected(10) + const resInsert = await postgrest + .from('messages') + .insert({ message: 'foo', username: 'supabot', channel_id: 1 }) + .maxAffected(10) + const resUpsert = await postgrest + .from('messages') + .upsert({ id: 3, message: 'foo', username: 'supabot', channel_id: 2 }) + .maxAffected(10) + const resUpdate = await postgrest + .from('messages') + .update({ channel_id: 2 }) + .eq('message', 'foo') + .maxAffected(1) + .select() + const resDelete = await postgrest + .from('messages') + .delete() + .eq('message', 'foo') + .maxAffected(1) + .select() + expectType>( + resSelect + ) + expectType>( + resInsert + ) + expectType>( + resUpsert + ) + expectType>( + // @ts-expect-error update method shouldn't return an error + resUpdate + ) + expectType>( + // @ts-expect-error delete method shouldn't return an error + resDelete + ) + }) + + // Runtime behavior tests + test('update should fail when maxAffected is exceeded', async () => { + // First create multiple rows + await postgrest.from('messages').insert([ + { message: 'test1', username: 'supabot', channel_id: 1 }, + { message: 'test1', username: 'supabot', channel_id: 1 }, + { message: 'test1', username: 'supabot', channel_id: 1 }, + ]) + + // Try to update all rows with maxAffected=2 + const result = await postgrest + .from('messages') + .update({ message: 'updated' }) + .eq('message', 'test1') + .maxAffected(2) + const { error } = result + expect(error).toBeDefined() + expect(error?.message).toBe('Query result exceeds max-affected preference constraint') + }) + + test('update should succeed when within maxAffected limit', async () => { + // First create a single row + await postgrest + .from('messages') + .insert([{ message: 'test2', username: 'supabot', channel_id: 1 }]) + + // Try to update with maxAffected=2 + const { data, error } = await postgrest + .from('messages') + .update({ message: 'updated' }) + .eq('message', 'test2') + .maxAffected(2) + .select() + + expect(error).toBeNull() + expect(data).toHaveLength(1) + expect(data?.[0].message).toBe('updated') + }) + + test('delete should fail when maxAffected is exceeded', async () => { + // First create multiple rows + await postgrest.from('messages').insert([ + { message: 'test3', username: 'supabot', channel_id: 1 }, + { message: 'test3', username: 'supabot', channel_id: 1 }, + { message: 'test3', username: 'supabot', channel_id: 1 }, + ]) + + // Try to delete all rows with maxAffected=2 + const { error } = await postgrest + .from('messages') + .delete() + .eq('message', 'test3') + .maxAffected(2) + .select() + + expect(error).toBeDefined() + expect(error?.message).toBe('Query result exceeds max-affected preference constraint') + }) + + test('delete should succeed when within maxAffected limit', async () => { + // First create a single row + await postgrest + .from('messages') + .insert([{ message: 'test4', username: 'supabot', channel_id: 1 }]) + + // Try to delete with maxAffected=2 + const { data, error } = await postgrest + .from('messages') + .delete() + .eq('message', 'test4') + .maxAffected(2) + .select() + + expect(error).toBeNull() + expect(data).toHaveLength(1) + expect(data?.[0].message).toBe('test4') + }) +}) diff --git a/test/relationships.ts b/test/relationships.ts index c826f165..2e46c548 100644 --- a/test/relationships.ts +++ b/test/relationships.ts @@ -146,7 +146,7 @@ export const selectParams = { }, selectSpreadOnManyRelation: { from: 'channels', - select: 'id, ...messages(id, message)', + select: 'channel_id:id, ...messages(id, message)', }, selectWithDuplicatesFields: { from: 'channels', @@ -1734,7 +1734,7 @@ test('join with same dest twice column hinting', async () => { `) }) -test('join with same dest twice column hinting', async () => { +test('select spread on many relation', async () => { const res = await selectQueries.selectSpreadOnManyRelation.limit(1).single() expect(res).toMatchInlineSnapshot(` Object { diff --git a/test/select-query-parser/select.test-d.ts b/test/select-query-parser/select.test-d.ts index 11726d9f..71b700ec 100644 --- a/test/select-query-parser/select.test-d.ts +++ b/test/select-query-parser/select.test-d.ts @@ -654,8 +654,9 @@ type Schema = Database['public'] const { data } = await selectQueries.selectSpreadOnManyRelation.limit(1).single() let result: Exclude let expected: { - id: number - messages: SelectQueryError<'"channels" and "messages" do not form a many-to-one or one-to-one relationship spread not possible'> + channel_id: number + id: Array + message: Array } expectType>(true) } diff --git a/test/utils.ts b/test/utils.ts new file mode 100644 index 00000000..6d7466ea --- /dev/null +++ b/test/utils.ts @@ -0,0 +1,128 @@ +import { HeaderManager } from '../src/utils' + +describe('HeaderManager', () => { + describe('constructor', () => { + it('should initialize with empty header when no value provided', () => { + const manager = new HeaderManager('Prefer') + expect(manager.get()).toBe('') + }) + + it('should parse existing header string correctly', () => { + const manager = new HeaderManager('Prefer', 'return=representation,tx=rollback') + expect(manager.get()).toBe('return=representation,tx=rollback') + }) + + it('should handle empty string in constructor', () => { + const manager = new HeaderManager('Prefer', '') + expect(manager.get()).toBe('') + }) + + it('should handle whitespace in header string', () => { + const manager = new HeaderManager('Prefer', ' return=representation , tx=rollback ') + expect(manager.get()).toBe('return=representation,tx=rollback') + }) + }) + + describe('add', () => { + it('should add single value', () => { + const manager = new HeaderManager('Prefer') + manager.add('return=representation') + expect(manager.get()).toBe('return=representation') + }) + + it('should add multiple values', () => { + const manager = new HeaderManager('Prefer') + manager.add('return=representation') + manager.add('tx=rollback') + expect(manager.get()).toBe('return=representation,tx=rollback') + }) + + it('should not add duplicate values', () => { + const manager = new HeaderManager('Prefer') + manager.add('return=representation') + manager.add('return=representation') + expect(manager.get()).toBe('return=representation') + }) + + it('should append to existing values', () => { + const manager = new HeaderManager('Prefer', 'return=representation') + manager.add('tx=rollback') + expect(manager.get()).toBe('return=representation,tx=rollback') + }) + }) + + describe('has', () => { + it('should return true for existing value', () => { + const manager = new HeaderManager('Prefer', 'return=representation') + expect(manager.has('return=representation')).toBe(true) + }) + + it('should return false for non-existing value', () => { + const manager = new HeaderManager('Prefer', 'return=representation') + expect(manager.has('tx=rollback')).toBe(false) + }) + + it('should handle whitespace in value check', () => { + const manager = new HeaderManager('Prefer', 'return=representation') + expect(manager.has(' return=representation ')).toBe(false) + }) + }) + + describe('clear', () => { + it('should clear all values', () => { + const manager = new HeaderManager('Prefer', 'return=representation,tx=rollback') + manager.clear() + expect(manager.get()).toBe('') + }) + + it('should not throw when clearing empty header', () => { + const manager = new HeaderManager('Prefer') + expect(() => manager.clear()).not.toThrow() + }) + }) + + describe('integration scenarios', () => { + it('should handle select() scenario', () => { + const manager = new HeaderManager('Prefer') + manager.add('return=representation') + expect(manager.get()).toBe('return=representation') + }) + + it('should handle rollback() scenario', () => { + const manager = new HeaderManager('Prefer', 'return=representation') + manager.add('tx=rollback') + expect(manager.get()).toBe('return=representation,tx=rollback') + }) + + it('should handle multiple operations in sequence', () => { + const manager = new HeaderManager('Prefer') + manager.add('return=representation') + manager.add('tx=rollback') + manager.add('count=exact') + expect(manager.get()).toBe('return=representation,tx=rollback,count=exact') + }) + + it('should handle existing header with multiple values', () => { + const manager = new HeaderManager('Prefer', 'return=representation,tx=rollback') + manager.add('count=exact') + expect(manager.get()).toBe('return=representation,tx=rollback,count=exact') + }) + }) + + describe('edge cases', () => { + it('should handle empty values in header string', () => { + const manager = new HeaderManager('Prefer', 'return=representation,,tx=rollback') + expect(manager.get()).toBe('return=representation,tx=rollback') + }) + + it('should handle multiple commas in header string', () => { + const manager = new HeaderManager('Prefer', 'return=representation,,,tx=rollback') + expect(manager.get()).toBe('return=representation,tx=rollback') + }) + + it('should handle whitespace-only values in header string', () => { + const manager = new HeaderManager('Prefer', 'return=representation, ,tx=rollback') + expect(manager.get()).toBe('return=representation,tx=rollback') + }) + }) +}) From 6705c5c5f3b600e3e8cc74bc063c86ba90cb456c Mon Sep 17 00:00:00 2001 From: avallete Date: Sun, 18 May 2025 18:20:10 +0200 Subject: [PATCH 02/15] feat(types): add ClientOptions to handle multi postgrest versions --- src/PostgrestBuilder.ts | 23 ++++-- src/PostgrestClient.ts | 13 ++-- src/PostgrestFilterBuilder.ts | 13 +++- src/PostgrestQueryBuilder.ts | 94 +++++++++++++++++++---- src/PostgrestTransformBuilder.ts | 50 ++++++++---- src/select-query-parser/result.ts | 71 ++++++++++++++--- src/types.ts | 5 ++ test/basic.ts | 4 +- test/db/docker-compose.yml | 16 +++- test/max-affected.ts | 30 ++++---- test/relationships.ts | 23 ++++++ test/select-query-parser/select.test-d.ts | 13 +++- 12 files changed, 281 insertions(+), 74 deletions(-) diff --git a/src/PostgrestBuilder.ts b/src/PostgrestBuilder.ts index 86e48794..d7bcc26a 100644 --- a/src/PostgrestBuilder.ts +++ b/src/PostgrestBuilder.ts @@ -8,12 +8,16 @@ import type { CheckMatchingArrayTypes, MergePartialResult, IsValidResultOverride, + ClientServerOptions, } from './types' import PostgrestError from './PostgrestError' import { ContainsNull } from './select-query-parser/types' -export default abstract class PostgrestBuilder - implements +export default abstract class PostgrestBuilder< + ClientOptions extends ClientServerOptions, + Result, + ThrowOnError extends boolean = false +> implements PromiseLike< ThrowOnError extends true ? PostgrestResponseSuccess : PostgrestSingleResponse > @@ -28,7 +32,7 @@ export default abstract class PostgrestBuilder) { + constructor(builder: PostgrestBuilder) { this.method = builder.method this.url = builder.url this.headers = builder.headers @@ -53,9 +57,9 @@ export default abstract class PostgrestBuilder { + throwOnError(): this & PostgrestBuilder { this.shouldThrowOnError = true - return this as this & PostgrestBuilder + return this as this & PostgrestBuilder } /** @@ -224,9 +228,14 @@ export default abstract class PostgrestBuilder() method at the end of your call chain instead */ - returns(): PostgrestBuilder, ThrowOnError> { + returns(): PostgrestBuilder< + ClientOptions, + CheckMatchingArrayTypes, + ThrowOnError + > { /* istanbul ignore next */ return this as unknown as PostgrestBuilder< + ClientOptions, CheckMatchingArrayTypes, ThrowOnError > @@ -258,6 +267,7 @@ export default abstract class PostgrestBuilder(): PostgrestBuilder< + ClientOptions, IsValidResultOverride extends true ? // Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`) ContainsNull extends true @@ -267,6 +277,7 @@ export default abstract class PostgrestBuilder { return this as unknown as PostgrestBuilder< + ClientOptions, IsValidResultOverride extends true ? // Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`) ContainsNull extends true diff --git a/src/PostgrestClient.ts b/src/PostgrestClient.ts index 8a37b09c..eef09eba 100644 --- a/src/PostgrestClient.ts +++ b/src/PostgrestClient.ts @@ -2,7 +2,7 @@ import PostgrestQueryBuilder from './PostgrestQueryBuilder' import PostgrestFilterBuilder from './PostgrestFilterBuilder' import PostgrestBuilder from './PostgrestBuilder' import { DEFAULT_HEADERS } from './constants' -import { Fetch, GenericSchema } from './types' +import { Fetch, GenericSchema, ClientServerOptions } from './types' /** * PostgREST client. @@ -16,6 +16,7 @@ import { Fetch, GenericSchema } from './types' */ export default class PostgrestClient< Database = any, + ClientOptions extends ClientServerOptions = { postgrestVersion: 12 }, SchemaName extends string & keyof Database = 'public' extends keyof Database ? 'public' : string & keyof Database, @@ -59,16 +60,16 @@ export default class PostgrestClient< 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 { const url = new URL(`${this.url}/${relation}`) return new PostgrestQueryBuilder(url, { headers: { ...this.headers }, @@ -88,6 +89,7 @@ export default class PostgrestClient< schema: DynamicSchema ): PostgrestClient< Database, + ClientOptions, DynamicSchema, Database[DynamicSchema] extends GenericSchema ? Database[DynamicSchema] : any > { @@ -134,6 +136,7 @@ export default class PostgrestClient< count?: 'exact' | 'planned' | 'estimated' } = {} ): PostgrestFilterBuilder< + ClientOptions, Schema, Fn['Returns'] extends any[] ? Fn['Returns'][number] extends Record @@ -176,6 +179,6 @@ export default class PostgrestClient< body, fetch: this.fetch, allowEmpty: false, - } as unknown as PostgrestBuilder) + } as unknown as PostgrestBuilder) } } diff --git a/src/PostgrestFilterBuilder.ts b/src/PostgrestFilterBuilder.ts index d7c8969d..cf79ffa4 100644 --- a/src/PostgrestFilterBuilder.ts +++ b/src/PostgrestFilterBuilder.ts @@ -1,6 +1,6 @@ import PostgrestTransformBuilder from './PostgrestTransformBuilder' import { JsonPathToAccessor, JsonPathToType } from './select-query-parser/utils' -import { GenericSchema } from './types' +import { ClientServerOptions, GenericSchema } from './types' import { HeaderManager } from './utils' type FilterOperator = @@ -73,13 +73,22 @@ type ResolveFilterRelationshipValue< export type InvalidMethodError = { Error: S } export default class PostgrestFilterBuilder< + ClientOptions extends ClientServerOptions, Schema extends GenericSchema, Row extends Record, Result, RelationName = unknown, Relationships = unknown, Method = unknown -> extends PostgrestTransformBuilder { +> extends PostgrestTransformBuilder< + ClientOptions, + Schema, + Row, + Result, + RelationName, + Relationships, + Method +> { maxAffected( value: number ): Method extends 'PATCH' | 'DELETE' diff --git a/src/PostgrestQueryBuilder.ts b/src/PostgrestQueryBuilder.ts index 7aed7df0..e2598e92 100644 --- a/src/PostgrestQueryBuilder.ts +++ b/src/PostgrestQueryBuilder.ts @@ -1,9 +1,10 @@ import PostgrestBuilder from './PostgrestBuilder' import PostgrestFilterBuilder from './PostgrestFilterBuilder' import { GetResult } from './select-query-parser/result' -import { Fetch, GenericSchema, GenericTable, GenericView } from './types' +import { ClientServerOptions, Fetch, GenericSchema, GenericTable, GenericView } from './types' export default class PostgrestQueryBuilder< + ClientOptions extends ClientServerOptions, Schema extends GenericSchema, Relation extends GenericTable | GenericView, RelationName = unknown, @@ -56,7 +57,14 @@ export default class PostgrestQueryBuilder< */ select< Query extends string = '*', - ResultOne = GetResult + ResultOne = GetResult< + Schema, + Relation['Row'], + RelationName, + Relationships, + Query, + ClientOptions + > >( columns?: Query, { @@ -67,6 +75,7 @@ export default class PostgrestQueryBuilder< count?: 'exact' | 'planned' | 'estimated' } = {} ): PostgrestFilterBuilder< + ClientOptions, Schema, Relation['Row'], ResultOne[], @@ -101,7 +110,7 @@ export default class PostgrestQueryBuilder< schema: this.schema, fetch: this.fetch, allowEmpty: false, - } as unknown as PostgrestBuilder) + } as unknown as PostgrestBuilder) } // TODO(v3): Make `defaultToNull` consistent for both single & bulk inserts. @@ -110,14 +119,30 @@ export default class PostgrestQueryBuilder< options?: { count?: 'exact' | 'planned' | 'estimated' } - ): PostgrestFilterBuilder + ): PostgrestFilterBuilder< + ClientOptions, + Schema, + Relation['Row'], + null, + RelationName, + Relationships, + 'POST' + > insert( values: Row[], options?: { count?: 'exact' | 'planned' | 'estimated' defaultToNull?: boolean } - ): PostgrestFilterBuilder + ): PostgrestFilterBuilder< + ClientOptions, + Schema, + Relation['Row'], + null, + RelationName, + Relationships, + 'POST' + > /** * Perform an INSERT into the table or view. * @@ -153,7 +178,15 @@ export default class PostgrestQueryBuilder< count?: 'exact' | 'planned' | 'estimated' defaultToNull?: boolean } = {} - ): PostgrestFilterBuilder { + ): PostgrestFilterBuilder< + ClientOptions, + Schema, + Relation['Row'], + null, + RelationName, + Relationships, + 'POST' + > { const method = 'POST' const prefersHeaders = [] @@ -184,7 +217,7 @@ export default class PostgrestQueryBuilder< body: values, fetch: this.fetch, allowEmpty: false, - } as unknown as PostgrestBuilder) + } as unknown as PostgrestBuilder) } // TODO(v3): Make `defaultToNull` consistent for both single & bulk upserts. @@ -195,7 +228,15 @@ export default class PostgrestQueryBuilder< ignoreDuplicates?: boolean count?: 'exact' | 'planned' | 'estimated' } - ): PostgrestFilterBuilder + ): PostgrestFilterBuilder< + ClientOptions, + Schema, + Relation['Row'], + null, + RelationName, + Relationships, + 'POST' + > upsert( values: Row[], options?: { @@ -204,7 +245,15 @@ export default class PostgrestQueryBuilder< count?: 'exact' | 'planned' | 'estimated' defaultToNull?: boolean } - ): PostgrestFilterBuilder + ): PostgrestFilterBuilder< + ClientOptions, + Schema, + Relation['Row'], + null, + RelationName, + Relationships, + 'POST' + > /** * Perform an UPSERT on the table or view. Depending on the column(s) passed * to `onConflict`, `.upsert()` allows you to perform the equivalent of @@ -256,7 +305,15 @@ export default class PostgrestQueryBuilder< count?: 'exact' | 'planned' | 'estimated' defaultToNull?: boolean } = {} - ): PostgrestFilterBuilder { + ): PostgrestFilterBuilder< + ClientOptions, + Schema, + Relation['Row'], + null, + RelationName, + Relationships, + 'POST' + > { const method = 'POST' const prefersHeaders = [`resolution=${ignoreDuplicates ? 'ignore' : 'merge'}-duplicates`] @@ -289,7 +346,7 @@ export default class PostgrestQueryBuilder< body: values, fetch: this.fetch, allowEmpty: false, - } as unknown as PostgrestBuilder) + } as unknown as PostgrestBuilder) } /** @@ -320,7 +377,15 @@ export default class PostgrestQueryBuilder< }: { count?: 'exact' | 'planned' | 'estimated' } = {} - ): PostgrestFilterBuilder { + ): PostgrestFilterBuilder< + ClientOptions, + Schema, + Relation['Row'], + null, + RelationName, + Relationships, + 'PATCH' + > { const method = 'PATCH' const prefersHeaders = [] if (this.headers['Prefer']) { @@ -339,7 +404,7 @@ export default class PostgrestQueryBuilder< body: values, fetch: this.fetch, allowEmpty: false, - } as unknown as PostgrestBuilder) + } as unknown as PostgrestBuilder) } /** @@ -366,6 +431,7 @@ export default class PostgrestQueryBuilder< }: { count?: 'exact' | 'planned' | 'estimated' } = {}): PostgrestFilterBuilder< + ClientOptions, Schema, Relation['Row'], null, @@ -390,6 +456,6 @@ export default class PostgrestQueryBuilder< schema: this.schema, fetch: this.fetch, allowEmpty: false, - } as unknown as PostgrestBuilder) + } as unknown as PostgrestBuilder) } } diff --git a/src/PostgrestTransformBuilder.ts b/src/PostgrestTransformBuilder.ts index 8545fef2..d8950ec8 100644 --- a/src/PostgrestTransformBuilder.ts +++ b/src/PostgrestTransformBuilder.ts @@ -1,15 +1,16 @@ import PostgrestBuilder from './PostgrestBuilder' import { GetResult } from './select-query-parser/result' -import { GenericSchema, CheckMatchingArrayTypes } from './types' +import { GenericSchema, CheckMatchingArrayTypes, ClientServerOptions } from './types' export default class PostgrestTransformBuilder< + ClientOptions extends ClientServerOptions, Schema extends GenericSchema, Row extends Record, Result, RelationName = unknown, Relationships = unknown, Method = unknown -> extends PostgrestBuilder { +> extends PostgrestBuilder { /** * Perform a SELECT on the query result. * @@ -21,10 +22,18 @@ export default class PostgrestTransformBuilder< */ select< Query extends string = '*', - NewResultOne = GetResult + NewResultOne = GetResult >( columns?: Query - ): PostgrestTransformBuilder { + ): PostgrestTransformBuilder< + ClientOptions, + Schema, + Row, + NewResultOne[], + RelationName, + Relationships, + Method + > { // Remove whitespaces except when quoted let quoted = false const cleanedColumns = (columns ?? '*') @@ -45,6 +54,7 @@ export default class PostgrestTransformBuilder< } this.headers['Prefer'] += 'return=representation' return this as unknown as PostgrestTransformBuilder< + ClientOptions, Schema, Row, NewResultOne[], @@ -190,11 +200,12 @@ export default class PostgrestTransformBuilder< * Query result must be one row (e.g. using `.limit(1)`), otherwise this * returns an error. */ - single< - ResultOne = Result extends (infer ResultOne)[] ? ResultOne : never - >(): PostgrestBuilder { + single(): PostgrestBuilder< + ClientOptions, + ResultOne + > { this.headers['Accept'] = 'application/vnd.pgrst.object+json' - return this as unknown as PostgrestBuilder + return this as unknown as PostgrestBuilder } /** @@ -205,7 +216,7 @@ export default class PostgrestTransformBuilder< */ maybeSingle< ResultOne = Result extends (infer ResultOne)[] ? ResultOne : never - >(): PostgrestBuilder { + >(): PostgrestBuilder { // Temporary partial fix for https://github.com/supabase/postgrest-js/issues/361 // Issue persists e.g. for `.insert([...]).select().maybeSingle()` if (this.method === 'GET') { @@ -214,23 +225,23 @@ export default class PostgrestTransformBuilder< this.headers['Accept'] = 'application/vnd.pgrst.object+json' } this.isMaybeSingle = true - return this as unknown as PostgrestBuilder + return this as unknown as PostgrestBuilder } /** * Return `data` as a string in CSV format. */ - csv(): PostgrestBuilder { + csv(): PostgrestBuilder { this.headers['Accept'] = 'text/csv' - return this as unknown as PostgrestBuilder + return this as unknown as PostgrestBuilder } /** * Return `data` as an object in [GeoJSON](https://geojson.org) format. */ - geojson(): PostgrestBuilder> { + geojson(): PostgrestBuilder> { this.headers['Accept'] = 'application/geo+json' - return this as unknown as PostgrestBuilder> + return this as unknown as PostgrestBuilder> } /** @@ -272,7 +283,9 @@ export default class PostgrestTransformBuilder< buffers?: boolean wal?: boolean format?: 'json' | 'text' - } = {}): PostgrestBuilder[]> | PostgrestBuilder { + } = {}): + | PostgrestBuilder[]> + | PostgrestBuilder { const options = [ analyze ? 'analyze' : null, verbose ? 'verbose' : null, @@ -287,8 +300,9 @@ export default class PostgrestTransformBuilder< this.headers[ 'Accept' ] = `application/vnd.pgrst.plan+${format}; for="${forMediatype}"; options=${options};` - if (format === 'json') return this as unknown as PostgrestBuilder[]> - else return this as unknown as PostgrestBuilder + if (format === 'json') + return this as unknown as PostgrestBuilder[]> + else return this as unknown as PostgrestBuilder } /** @@ -312,6 +326,7 @@ export default class PostgrestTransformBuilder< * @deprecated Use overrideTypes() method at the end of your call chain instead */ returns(): PostgrestTransformBuilder< + ClientOptions, Schema, Row, CheckMatchingArrayTypes, @@ -320,6 +335,7 @@ export default class PostgrestTransformBuilder< Method > { return this as unknown as PostgrestTransformBuilder< + ClientOptions, Schema, Row, CheckMatchingArrayTypes, diff --git a/src/select-query-parser/result.ts b/src/select-query-parser/result.ts index 59fe2480..d8807107 100644 --- a/src/select-query-parser/result.ts +++ b/src/select-query-parser/result.ts @@ -1,4 +1,4 @@ -import { GenericTable } from '../types' +import { ClientServerOptions, GenericTable } from '../types' import { ContainsNull, GenericRelationship, PostgreSQLTypes } from './types' import { Ast, ParseQuery } from './parser' import { @@ -35,7 +35,8 @@ export type GetResult< Row extends Record, RelationName, Relationships, - Query extends string + Query extends string, + ClientOptions extends ClientServerOptions > = IsAny extends true ? ParseQuery extends infer ParsedQuery ? ParsedQuery extends Ast.Node[] @@ -54,7 +55,7 @@ export type GetResult< ? ParsedQuery extends Ast.Node[] ? RelationName extends string ? Relationships extends GenericRelationship[] - ? ProcessNodes + ? ProcessNodes : SelectQueryError<'Invalid Relationships cannot infer result type'> : SelectQueryError<'Invalid RelationName cannot infer result type'> : ParsedQuery @@ -173,6 +174,7 @@ export type RPCCallNodes< * @param Acc - Accumulator for the constructed type. */ export type ProcessNodes< + ClientOptions extends ClientServerOptions, Schema extends GenericSchema, Row extends Record, RelationName extends string, @@ -183,9 +185,24 @@ export type ProcessNodes< ? Nodes extends [infer FirstNode, ...infer RestNodes] ? FirstNode extends Ast.Node ? RestNodes extends Ast.Node[] - ? ProcessNode extends infer FieldResult + ? ProcessNode< + ClientOptions, + Schema, + Row, + RelationName, + Relationships, + FirstNode + > extends infer FieldResult ? FieldResult extends Record - ? ProcessNodes + ? ProcessNodes< + ClientOptions, + Schema, + Row, + RelationName, + Relationships, + RestNodes, + Acc & FieldResult + > : FieldResult extends SelectQueryError ? SelectQueryError : SelectQueryError<'Could not retrieve a valid record or error value'> @@ -205,6 +222,7 @@ export type ProcessNodes< * @param NodeType - The Node to process. */ export type ProcessNode< + ClientOptions extends ClientServerOptions, Schema extends GenericSchema, Row extends Record, RelationName extends string, @@ -215,9 +233,23 @@ export type ProcessNode< NodeType['type'] extends Ast.StarNode['type'] // If the selection is * ? Row : NodeType['type'] extends Ast.SpreadNode['type'] // If the selection is a ...spread - ? ProcessSpreadNode> + ? ProcessSpreadNode< + ClientOptions, + Schema, + Row, + RelationName, + Relationships, + Extract + > : NodeType['type'] extends Ast.FieldNode['type'] - ? ProcessFieldNode> + ? ProcessFieldNode< + ClientOptions, + Schema, + Row, + RelationName, + Relationships, + Extract + > : SelectQueryError<'Unsupported node type.'> /** @@ -230,6 +262,7 @@ export type ProcessNode< * @param Field - The FieldNode to process. */ type ProcessFieldNode< + ClientOptions extends ClientServerOptions, Schema extends GenericSchema, Row extends Record, RelationName extends string, @@ -238,7 +271,7 @@ type ProcessFieldNode< > = Field['children'] extends [] ? {} : IsNonEmptyArray extends true // Has embedded resource? - ? ProcessEmbeddedResource + ? ProcessEmbeddedResource : ProcessSimpleField type ResolveJsonPathType< @@ -303,6 +336,7 @@ type ProcessSimpleField< * @param Field - The FieldNode to process. */ export type ProcessEmbeddedResource< + ClientOptions extends ClientServerOptions, Schema extends GenericSchema, Relationships extends GenericRelationship[], Field extends Ast.FieldNode, @@ -313,7 +347,7 @@ export type ProcessEmbeddedResource< relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' } direction: string } - ? ProcessEmbeddedResourceResult + ? ProcessEmbeddedResourceResult : // Otherwise the Resolved is a SelectQueryError return it { [K in GetFieldNodeResultName]: Resolved } : { @@ -325,6 +359,7 @@ export type ProcessEmbeddedResource< * Helper type to process the result of an embedded resource. */ type ProcessEmbeddedResourceResult< + ClientOptions extends ClientServerOptions, Schema extends GenericSchema, Resolved extends { referencedTable: Pick @@ -334,6 +369,7 @@ type ProcessEmbeddedResourceResult< Field extends Ast.FieldNode, CurrentTableOrView extends keyof TablesAndViews > = ProcessNodes< + ClientOptions, Schema, Resolved['referencedTable']['Row'], Field['name'], @@ -392,17 +428,28 @@ type ProcessEmbeddedResourceResult< * @param Spread - The SpreadNode to process. */ type ProcessSpreadNode< + ClientOptions extends ClientServerOptions, Schema extends GenericSchema, Row extends Record, RelationName extends string, Relationships extends GenericRelationship[], Spread extends Ast.SpreadNode -> = ProcessNode extends infer Result +> = ProcessNode< + ClientOptions, + Schema, + Row, + RelationName, + Relationships, + Spread['target'] +> extends infer Result ? Result extends SelectQueryError ? SelectQueryError : ExtractFirstProperty extends unknown[] - ? // Spread over an many-to-many relationship, turn all the result fields into arrays - ProcessManyToManySpreadNodeResult + ? ClientOptions['postgrestVersion'] extends 13 // Spread over an many-to-many relationship, turn all the result fields into arrays + ? ProcessManyToManySpreadNodeResult + : { + [K in Spread['target']['name']]: SelectQueryError<`"${RelationName}" and "${Spread['target']['name']}" do not form a many-to-one or one-to-one relationship spread not possible`> + } : ProcessSpreadNodeResult : never diff --git a/src/types.ts b/src/types.ts index 51d58a70..08b8648f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -71,6 +71,11 @@ export type GenericSchema = { Functions: Record } +export type PostgRESTVersion = 12 | 13 +export type ClientServerOptions = { + postgrestVersion: PostgRESTVersion +} + // https://twitter.com/mattpocockuk/status/1622730173446557697 export type Prettify = { [K in keyof T]: T[K] } & {} // https://github.com/sindresorhus/type-fest diff --git a/test/basic.ts b/test/basic.ts index d2317cab..3ef1fb2c 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -319,7 +319,9 @@ describe('custom prefer headers with ', () => { }) test('switch schema', async () => { - const postgrest = new PostgrestClient(REST_URL, { schema: 'personal' }) + const postgrest = new PostgrestClient(REST_URL, { + schema: 'personal', + }) const res = await postgrest.from('users').select() expect(res).toMatchInlineSnapshot(` Object { diff --git a/test/db/docker-compose.yml b/test/db/docker-compose.yml index 22fc990c..59df0632 100644 --- a/test/db/docker-compose.yml +++ b/test/db/docker-compose.yml @@ -2,8 +2,22 @@ version: '3' services: - rest: + rest13: image: postgrest/postgrest:v13.0.0 + ports: + - '3001:3000' + environment: + PGRST_DB_URI: postgres://postgres:postgres@db:5432/postgres + PGRST_DB_SCHEMAS: public,personal + PGRST_DB_EXTRA_SEARCH_PATH: extensions + PGRST_DB_ANON_ROLE: postgres + PGRST_DB_PLAN_ENABLED: 1 + PGRST_DB_TX_END: commit-allow-override + PGRST_DB_AGGREGATES_ENABLED: true + depends_on: + - db + rest12: + image: postgrest/postgrest:v12.2.0 ports: - '3000:3000' environment: diff --git a/test/max-affected.ts b/test/max-affected.ts index df30b1f6..ded56ff7 100644 --- a/test/max-affected.ts +++ b/test/max-affected.ts @@ -3,28 +3,28 @@ import { Database } from './types.override' import { expectType } from 'tsd' import { InvalidMethodError } from '../src/PostgrestFilterBuilder' -const REST_URL = 'http://localhost:3000' -const postgrest = new PostgrestClient(REST_URL) +const REST_URL_13 = 'http://localhost:3001' +const postgrest13 = new PostgrestClient(REST_URL_13) describe('maxAffected', () => { // Type checking tests test('maxAffected should show warning on non update / delete', async () => { - const resSelect = await postgrest.from('messages').select('*').maxAffected(10) - const resInsert = await postgrest + const resSelect = await postgrest13.from('messages').select('*').maxAffected(10) + const resInsert = await postgrest13 .from('messages') .insert({ message: 'foo', username: 'supabot', channel_id: 1 }) .maxAffected(10) - const resUpsert = await postgrest + const resUpsert = await postgrest13 .from('messages') .upsert({ id: 3, message: 'foo', username: 'supabot', channel_id: 2 }) .maxAffected(10) - const resUpdate = await postgrest + const resUpdate = await postgrest13 .from('messages') .update({ channel_id: 2 }) .eq('message', 'foo') .maxAffected(1) .select() - const resDelete = await postgrest + const resDelete = await postgrest13 .from('messages') .delete() .eq('message', 'foo') @@ -52,14 +52,14 @@ describe('maxAffected', () => { // Runtime behavior tests test('update should fail when maxAffected is exceeded', async () => { // First create multiple rows - await postgrest.from('messages').insert([ + await postgrest13.from('messages').insert([ { message: 'test1', username: 'supabot', channel_id: 1 }, { message: 'test1', username: 'supabot', channel_id: 1 }, { message: 'test1', username: 'supabot', channel_id: 1 }, ]) // Try to update all rows with maxAffected=2 - const result = await postgrest + const result = await postgrest13 .from('messages') .update({ message: 'updated' }) .eq('message', 'test1') @@ -71,12 +71,12 @@ describe('maxAffected', () => { test('update should succeed when within maxAffected limit', async () => { // First create a single row - await postgrest + await postgrest13 .from('messages') .insert([{ message: 'test2', username: 'supabot', channel_id: 1 }]) // Try to update with maxAffected=2 - const { data, error } = await postgrest + const { data, error } = await postgrest13 .from('messages') .update({ message: 'updated' }) .eq('message', 'test2') @@ -90,14 +90,14 @@ describe('maxAffected', () => { test('delete should fail when maxAffected is exceeded', async () => { // First create multiple rows - await postgrest.from('messages').insert([ + await postgrest13.from('messages').insert([ { message: 'test3', username: 'supabot', channel_id: 1 }, { message: 'test3', username: 'supabot', channel_id: 1 }, { message: 'test3', username: 'supabot', channel_id: 1 }, ]) // Try to delete all rows with maxAffected=2 - const { error } = await postgrest + const { error } = await postgrest13 .from('messages') .delete() .eq('message', 'test3') @@ -110,12 +110,12 @@ describe('maxAffected', () => { test('delete should succeed when within maxAffected limit', async () => { // First create a single row - await postgrest + await postgrest13 .from('messages') .insert([{ message: 'test4', username: 'supabot', channel_id: 1 }]) // Try to delete with maxAffected=2 - const { data, error } = await postgrest + const { data, error } = await postgrest13 .from('messages') .delete() .eq('message', 'test4') diff --git a/test/relationships.ts b/test/relationships.ts index 2e46c548..8597d9aa 100644 --- a/test/relationships.ts +++ b/test/relationships.ts @@ -3,6 +3,8 @@ import { Database } from './types.override' const REST_URL = 'http://localhost:3000' export const postgrest = new PostgrestClient(REST_URL) +const REST_URL_13 = 'http://localhost:3001' +const postgrest13 = new PostgrestClient(REST_URL_13) const userColumn: 'catchphrase' | 'username' = 'username' @@ -345,6 +347,9 @@ export const selectQueries = { selectSpreadOnManyRelation: postgrest .from(selectParams.selectSpreadOnManyRelation.from) .select(selectParams.selectSpreadOnManyRelation.select), + selectSpreadOnManyRelation13: postgrest13 + .from(selectParams.selectSpreadOnManyRelation.from) + .select(selectParams.selectSpreadOnManyRelation.select), selectWithDuplicatesFields: postgrest .from(selectParams.selectWithDuplicatesFields.from) .select(selectParams.selectWithDuplicatesFields.select), @@ -1752,6 +1757,24 @@ test('select spread on many relation', async () => { `) }) +test('select spread on many relation postgrest13', async () => { + const res = await selectQueries.selectSpreadOnManyRelation13.limit(1).single() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST119", + "details": "'channels' and 'messages' do not form a many-to-one or one-to-one relationship", + "hint": null, + "message": "A spread operation on 'messages' is not possible", + }, + "status": 400, + "statusText": "Bad Request", + } + `) +}) + test('multiple times the same column in selection', async () => { const res = await selectQueries.selectWithDuplicatesFields.limit(1).single() expect(res).toMatchInlineSnapshot(` diff --git a/test/select-query-parser/select.test-d.ts b/test/select-query-parser/select.test-d.ts index 71b700ec..ad20b258 100644 --- a/test/select-query-parser/select.test-d.ts +++ b/test/select-query-parser/select.test-d.ts @@ -649,10 +649,21 @@ type Schema = Database['public'] expectType>(true) } -// join with same dest twice column hinting +// spread over a many relation with postgrest12 { const { data } = await selectQueries.selectSpreadOnManyRelation.limit(1).single() let result: Exclude + let expected: { + channel_id: number + messages: SelectQueryError<'"channels" and "messages" do not form a many-to-one or one-to-one relationship spread not possible'> + } + expectType>(true) +} + +// spread over a many relation with postgrest13 +{ + const { data } = await selectQueries.selectSpreadOnManyRelation13.limit(1).single() + let result: Exclude let expected: { channel_id: number id: Array From 1ceb00dc947fd77499a850debb6396f5649003aa Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 19 May 2025 08:40:39 +0200 Subject: [PATCH 03/15] chore: scope changes to pgrst13 introduction --- src/PostgrestFilterBuilder.ts | 20 +--- src/PostgrestQueryBuilder.ts | 27 ++--- src/utils.ts | 64 ----------- test/index.test.ts | 2 - test/max-affected.ts | 129 ---------------------- test/relationships.ts | 19 ++-- test/returns.test-d.ts | 1 + test/select-query-parser/result.test-d.ts | 6 +- test/utils.ts | 128 --------------------- 9 files changed, 27 insertions(+), 369 deletions(-) delete mode 100644 src/utils.ts delete mode 100644 test/max-affected.ts delete mode 100644 test/utils.ts diff --git a/src/PostgrestFilterBuilder.ts b/src/PostgrestFilterBuilder.ts index cf79ffa4..78b053ef 100644 --- a/src/PostgrestFilterBuilder.ts +++ b/src/PostgrestFilterBuilder.ts @@ -1,7 +1,6 @@ import PostgrestTransformBuilder from './PostgrestTransformBuilder' import { JsonPathToAccessor, JsonPathToType } from './select-query-parser/utils' import { ClientServerOptions, GenericSchema } from './types' -import { HeaderManager } from './utils' type FilterOperator = | 'eq' @@ -78,30 +77,15 @@ export default class PostgrestFilterBuilder< Row extends Record, Result, RelationName = unknown, - Relationships = unknown, - Method = unknown + Relationships = unknown > extends PostgrestTransformBuilder< ClientOptions, Schema, Row, Result, RelationName, - Relationships, - Method + Relationships > { - maxAffected( - value: number - ): Method extends 'PATCH' | 'DELETE' - ? this - : InvalidMethodError<'maxAffected method only available on update or delete'> { - const preferHeaderManager = new HeaderManager('Prefer', this.headers['Prefer']) - preferHeaderManager.add('handling=strict') - preferHeaderManager.add(`max-affected=${value}`) - this.headers['Prefer'] = preferHeaderManager.get() - return this as unknown as Method extends 'PATCH' | 'DELETE' - ? this - : InvalidMethodError<'maxAffected method only available on update or delete'> - } /** * Match only rows where `column` is equal to `value`. * diff --git a/src/PostgrestQueryBuilder.ts b/src/PostgrestQueryBuilder.ts index e2598e92..23059e9a 100644 --- a/src/PostgrestQueryBuilder.ts +++ b/src/PostgrestQueryBuilder.ts @@ -80,8 +80,7 @@ export default class PostgrestQueryBuilder< Relation['Row'], ResultOne[], RelationName, - Relationships, - 'GET' + Relationships > { const method = head ? 'HEAD' : 'GET' // Remove whitespaces except when quoted @@ -125,8 +124,7 @@ export default class PostgrestQueryBuilder< Relation['Row'], null, RelationName, - Relationships, - 'POST' + Relationships > insert( values: Row[], @@ -140,8 +138,7 @@ export default class PostgrestQueryBuilder< Relation['Row'], null, RelationName, - Relationships, - 'POST' + Relationships > /** * Perform an INSERT into the table or view. @@ -184,8 +181,7 @@ export default class PostgrestQueryBuilder< Relation['Row'], null, RelationName, - Relationships, - 'POST' + Relationships > { const method = 'POST' @@ -234,8 +230,7 @@ export default class PostgrestQueryBuilder< Relation['Row'], null, RelationName, - Relationships, - 'POST' + Relationships > upsert( values: Row[], @@ -251,8 +246,7 @@ export default class PostgrestQueryBuilder< Relation['Row'], null, RelationName, - Relationships, - 'POST' + Relationships > /** * Perform an UPSERT on the table or view. Depending on the column(s) passed @@ -311,8 +305,7 @@ export default class PostgrestQueryBuilder< Relation['Row'], null, RelationName, - Relationships, - 'POST' + Relationships > { const method = 'POST' @@ -383,8 +376,7 @@ export default class PostgrestQueryBuilder< Relation['Row'], null, RelationName, - Relationships, - 'PATCH' + Relationships > { const method = 'PATCH' const prefersHeaders = [] @@ -436,8 +428,7 @@ export default class PostgrestQueryBuilder< Relation['Row'], null, RelationName, - Relationships, - 'DELETE' + Relationships > { const method = 'DELETE' const prefersHeaders = [] diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index 47b0fbd6..00000000 --- a/src/utils.ts +++ /dev/null @@ -1,64 +0,0 @@ -export class HeaderManager { - private headers: Map> = new Map() - - /** - * Create a new HeaderManager, optionally parsing an existing header string - * @param header The header name to manage - * @param existingValue Optional existing header value to parse - */ - constructor(private readonly header: string, existingValue?: string) { - if (existingValue) { - this.parseHeaderString(existingValue) - } - } - - /** - * Parse an existing header string into the internal Set - * @param headerString The header string to parse - */ - private parseHeaderString(headerString: string): void { - if (!headerString.trim()) return - - const values = headerString.split(',') - values.forEach((value) => { - const trimmedValue = value.trim() - if (trimmedValue) { - this.add(trimmedValue) - } - }) - } - - /** - * Add a value to the header. If the header doesn't exist, it will be created. - * @param value The value to add - */ - add(value: string): void { - if (!this.headers.has(this.header)) { - this.headers.set(this.header, new Set()) - } - this.headers.get(this.header)!.add(value) - } - - /** - * Get the formatted string value for the header - */ - get(): string { - const values = this.headers.get(this.header) - return values ? Array.from(values).join(',') : '' - } - - /** - * Check if the header has a specific value - * @param value The value to check - */ - has(value: string): boolean { - return this.headers.get(this.header)?.has(value) ?? false - } - - /** - * Clear all values for the header - */ - clear(): void { - this.headers.delete(this.header) - } -} diff --git a/test/index.test.ts b/test/index.test.ts index 80b93afb..a7f2ffc6 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -4,5 +4,3 @@ import './filters' import './resource-embedding' import './transforms' import './rpc' -import './utils' -// import './max-affected' diff --git a/test/max-affected.ts b/test/max-affected.ts deleted file mode 100644 index ded56ff7..00000000 --- a/test/max-affected.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { PostgrestClient } from '../src/index' -import { Database } from './types.override' -import { expectType } from 'tsd' -import { InvalidMethodError } from '../src/PostgrestFilterBuilder' - -const REST_URL_13 = 'http://localhost:3001' -const postgrest13 = new PostgrestClient(REST_URL_13) - -describe('maxAffected', () => { - // Type checking tests - test('maxAffected should show warning on non update / delete', async () => { - const resSelect = await postgrest13.from('messages').select('*').maxAffected(10) - const resInsert = await postgrest13 - .from('messages') - .insert({ message: 'foo', username: 'supabot', channel_id: 1 }) - .maxAffected(10) - const resUpsert = await postgrest13 - .from('messages') - .upsert({ id: 3, message: 'foo', username: 'supabot', channel_id: 2 }) - .maxAffected(10) - const resUpdate = await postgrest13 - .from('messages') - .update({ channel_id: 2 }) - .eq('message', 'foo') - .maxAffected(1) - .select() - const resDelete = await postgrest13 - .from('messages') - .delete() - .eq('message', 'foo') - .maxAffected(1) - .select() - expectType>( - resSelect - ) - expectType>( - resInsert - ) - expectType>( - resUpsert - ) - expectType>( - // @ts-expect-error update method shouldn't return an error - resUpdate - ) - expectType>( - // @ts-expect-error delete method shouldn't return an error - resDelete - ) - }) - - // Runtime behavior tests - test('update should fail when maxAffected is exceeded', async () => { - // First create multiple rows - await postgrest13.from('messages').insert([ - { message: 'test1', username: 'supabot', channel_id: 1 }, - { message: 'test1', username: 'supabot', channel_id: 1 }, - { message: 'test1', username: 'supabot', channel_id: 1 }, - ]) - - // Try to update all rows with maxAffected=2 - const result = await postgrest13 - .from('messages') - .update({ message: 'updated' }) - .eq('message', 'test1') - .maxAffected(2) - const { error } = result - expect(error).toBeDefined() - expect(error?.message).toBe('Query result exceeds max-affected preference constraint') - }) - - test('update should succeed when within maxAffected limit', async () => { - // First create a single row - await postgrest13 - .from('messages') - .insert([{ message: 'test2', username: 'supabot', channel_id: 1 }]) - - // Try to update with maxAffected=2 - const { data, error } = await postgrest13 - .from('messages') - .update({ message: 'updated' }) - .eq('message', 'test2') - .maxAffected(2) - .select() - - expect(error).toBeNull() - expect(data).toHaveLength(1) - expect(data?.[0].message).toBe('updated') - }) - - test('delete should fail when maxAffected is exceeded', async () => { - // First create multiple rows - await postgrest13.from('messages').insert([ - { message: 'test3', username: 'supabot', channel_id: 1 }, - { message: 'test3', username: 'supabot', channel_id: 1 }, - { message: 'test3', username: 'supabot', channel_id: 1 }, - ]) - - // Try to delete all rows with maxAffected=2 - const { error } = await postgrest13 - .from('messages') - .delete() - .eq('message', 'test3') - .maxAffected(2) - .select() - - expect(error).toBeDefined() - expect(error?.message).toBe('Query result exceeds max-affected preference constraint') - }) - - test('delete should succeed when within maxAffected limit', async () => { - // First create a single row - await postgrest13 - .from('messages') - .insert([{ message: 'test4', username: 'supabot', channel_id: 1 }]) - - // Try to delete with maxAffected=2 - const { data, error } = await postgrest13 - .from('messages') - .delete() - .eq('message', 'test4') - .maxAffected(2) - .select() - - expect(error).toBeNull() - expect(data).toHaveLength(1) - expect(data?.[0].message).toBe('test4') - }) -}) diff --git a/test/relationships.ts b/test/relationships.ts index 8597d9aa..8b85d9d7 100644 --- a/test/relationships.ts +++ b/test/relationships.ts @@ -1762,15 +1762,18 @@ test('select spread on many relation postgrest13', async () => { expect(res).toMatchInlineSnapshot(` Object { "count": null, - "data": null, - "error": Object { - "code": "PGRST119", - "details": "'channels' and 'messages' do not form a many-to-one or one-to-one relationship", - "hint": null, - "message": "A spread operation on 'messages' is not possible", + "data": Object { + "channel_id": 1, + "id": Array [ + 1, + ], + "message": Array [ + "Hello World 👋", + ], }, - "status": 400, - "statusText": "Bad Request", + "error": null, + "status": 200, + "statusText": "OK", } `) }) diff --git a/test/returns.test-d.ts b/test/returns.test-d.ts index f6e5f5a8..e73708b8 100644 --- a/test/returns.test-d.ts +++ b/test/returns.test-d.ts @@ -53,6 +53,7 @@ const postgrest = new PostgrestClient(REST_URL) .returns<{ username: string }[]>() expectType< PostgrestBuilder< + { postgrestVersion: 12 }, { Error: 'Type mismatch: Cannot cast single object to array type. Remove Array wrapper from return type or make sure you are not using .single() up in the calling chain' }, diff --git a/test/select-query-parser/result.test-d.ts b/test/select-query-parser/result.test-d.ts index b2564258..d4c6d559 100644 --- a/test/select-query-parser/result.test-d.ts +++ b/test/select-query-parser/result.test-d.ts @@ -15,7 +15,8 @@ type SelectQueryFromTableResult< Database['public']['Tables'][TableName]['Row'], TableName, Database['public']['Tables'][TableName]['Relationships'], - Q + Q, + { postgrestVersion: 12 } > // This test file is here to help develop, debug and maintain the GetResult @@ -130,7 +131,8 @@ type SelectQueryFromTableResult< Database['personal']['Tables'][TableName]['Row'], TableName, Database['personal']['Tables'][TableName]['Relationships'], - Q + Q, + { postgrestVersion: 12 } > // Should work with Json object accessor { diff --git a/test/utils.ts b/test/utils.ts deleted file mode 100644 index 6d7466ea..00000000 --- a/test/utils.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { HeaderManager } from '../src/utils' - -describe('HeaderManager', () => { - describe('constructor', () => { - it('should initialize with empty header when no value provided', () => { - const manager = new HeaderManager('Prefer') - expect(manager.get()).toBe('') - }) - - it('should parse existing header string correctly', () => { - const manager = new HeaderManager('Prefer', 'return=representation,tx=rollback') - expect(manager.get()).toBe('return=representation,tx=rollback') - }) - - it('should handle empty string in constructor', () => { - const manager = new HeaderManager('Prefer', '') - expect(manager.get()).toBe('') - }) - - it('should handle whitespace in header string', () => { - const manager = new HeaderManager('Prefer', ' return=representation , tx=rollback ') - expect(manager.get()).toBe('return=representation,tx=rollback') - }) - }) - - describe('add', () => { - it('should add single value', () => { - const manager = new HeaderManager('Prefer') - manager.add('return=representation') - expect(manager.get()).toBe('return=representation') - }) - - it('should add multiple values', () => { - const manager = new HeaderManager('Prefer') - manager.add('return=representation') - manager.add('tx=rollback') - expect(manager.get()).toBe('return=representation,tx=rollback') - }) - - it('should not add duplicate values', () => { - const manager = new HeaderManager('Prefer') - manager.add('return=representation') - manager.add('return=representation') - expect(manager.get()).toBe('return=representation') - }) - - it('should append to existing values', () => { - const manager = new HeaderManager('Prefer', 'return=representation') - manager.add('tx=rollback') - expect(manager.get()).toBe('return=representation,tx=rollback') - }) - }) - - describe('has', () => { - it('should return true for existing value', () => { - const manager = new HeaderManager('Prefer', 'return=representation') - expect(manager.has('return=representation')).toBe(true) - }) - - it('should return false for non-existing value', () => { - const manager = new HeaderManager('Prefer', 'return=representation') - expect(manager.has('tx=rollback')).toBe(false) - }) - - it('should handle whitespace in value check', () => { - const manager = new HeaderManager('Prefer', 'return=representation') - expect(manager.has(' return=representation ')).toBe(false) - }) - }) - - describe('clear', () => { - it('should clear all values', () => { - const manager = new HeaderManager('Prefer', 'return=representation,tx=rollback') - manager.clear() - expect(manager.get()).toBe('') - }) - - it('should not throw when clearing empty header', () => { - const manager = new HeaderManager('Prefer') - expect(() => manager.clear()).not.toThrow() - }) - }) - - describe('integration scenarios', () => { - it('should handle select() scenario', () => { - const manager = new HeaderManager('Prefer') - manager.add('return=representation') - expect(manager.get()).toBe('return=representation') - }) - - it('should handle rollback() scenario', () => { - const manager = new HeaderManager('Prefer', 'return=representation') - manager.add('tx=rollback') - expect(manager.get()).toBe('return=representation,tx=rollback') - }) - - it('should handle multiple operations in sequence', () => { - const manager = new HeaderManager('Prefer') - manager.add('return=representation') - manager.add('tx=rollback') - manager.add('count=exact') - expect(manager.get()).toBe('return=representation,tx=rollback,count=exact') - }) - - it('should handle existing header with multiple values', () => { - const manager = new HeaderManager('Prefer', 'return=representation,tx=rollback') - manager.add('count=exact') - expect(manager.get()).toBe('return=representation,tx=rollback,count=exact') - }) - }) - - describe('edge cases', () => { - it('should handle empty values in header string', () => { - const manager = new HeaderManager('Prefer', 'return=representation,,tx=rollback') - expect(manager.get()).toBe('return=representation,tx=rollback') - }) - - it('should handle multiple commas in header string', () => { - const manager = new HeaderManager('Prefer', 'return=representation,,,tx=rollback') - expect(manager.get()).toBe('return=representation,tx=rollback') - }) - - it('should handle whitespace-only values in header string', () => { - const manager = new HeaderManager('Prefer', 'return=representation, ,tx=rollback') - expect(manager.get()).toBe('return=representation,tx=rollback') - }) - }) -}) From b8644af82c1732064935a70d34537a455d0c152f Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 19 May 2025 08:57:32 +0200 Subject: [PATCH 04/15] chore: upgrade ubuntu workflow instances Related to: https://github.com/actions/runner-images/issues/11101 Fix: https://github.com/supabase/postgrest-js/actions/runs/15106213807\?pr\=618 --- .github/workflows/ci.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4412210f..167f3b4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ on: jobs: test: name: Test - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9e519aa6..0d4a51c6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -9,7 +9,7 @@ on: jobs: docs: name: Publish docs - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 75794c82..876b09a3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ on: jobs: release: name: Release - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v2 From 9d64fb9963da2f41a46b8c0dec52dffb02107bf3 Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 19 May 2025 09:37:32 +0200 Subject: [PATCH 05/15] chore: export ClientServerOptions --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 466a71bf..23b12d52 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,7 @@ export type { PostgrestResponseSuccess, PostgrestSingleResponse, PostgrestMaybeSingleResponse, + ClientServerOptions, } from './types' // https://github.com/supabase/postgrest-js/issues/551 // To be replaced with a helper type that only uses public types From 8c15d0f90fe7768b0b37578057de906a9512e840 Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 19 May 2025 09:48:48 +0200 Subject: [PATCH 06/15] chore: make postgrestVersion optional --- src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index 08b8648f..de350db4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -73,7 +73,7 @@ export type GenericSchema = { export type PostgRESTVersion = 12 | 13 export type ClientServerOptions = { - postgrestVersion: PostgRESTVersion + postgrestVersion?: PostgRESTVersion } // https://twitter.com/mattpocockuk/status/1622730173446557697 From ceae114164fdd5de9853b81683298b170d8b4064 Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Tue, 20 May 2025 10:56:53 +0200 Subject: [PATCH 07/15] Update src/select-query-parser/result.ts Co-authored-by: Steve Chavez --- src/select-query-parser/result.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/select-query-parser/result.ts b/src/select-query-parser/result.ts index d8807107..f10c870a 100644 --- a/src/select-query-parser/result.ts +++ b/src/select-query-parser/result.ts @@ -445,7 +445,7 @@ type ProcessSpreadNode< ? Result extends SelectQueryError ? SelectQueryError : ExtractFirstProperty extends unknown[] - ? ClientOptions['postgrestVersion'] extends 13 // Spread over an many-to-many relationship, turn all the result fields into arrays + ? ClientOptions['postgrestVersion'] extends 13 // Spread over an many-to-many relationship, turn all the result fields into correlated arrays ? ProcessManyToManySpreadNodeResult : { [K in Spread['target']['name']]: SelectQueryError<`"${RelationName}" and "${Spread['target']['name']}" do not form a many-to-one or one-to-one relationship spread not possible`> From fa63bdf569ce2d30d46f29728929dbeb37178400 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 21 May 2025 11:26:30 +0200 Subject: [PATCH 08/15] feat: handle options declaration within Database --- src/PostgrestClient.ts | 15 +- src/index.ts | 1 + src/types.ts | 22 +- test/index.test-d.ts | 21 +- test/relationships.ts | 26 + test/select-query-parser/select.test-d.ts | 14 + ...ypes.generated-with-options-postgrest13.ts | 662 ++++++++++++++++++ ...types.override-with-options-postgrest13.ts | 149 ++++ 8 files changed, 887 insertions(+), 23 deletions(-) create mode 100644 test/types.generated-with-options-postgrest13.ts create mode 100644 test/types.override-with-options-postgrest13.ts diff --git a/src/PostgrestClient.ts b/src/PostgrestClient.ts index eef09eba..cf19de77 100644 --- a/src/PostgrestClient.ts +++ b/src/PostgrestClient.ts @@ -2,7 +2,7 @@ import PostgrestQueryBuilder from './PostgrestQueryBuilder' import PostgrestFilterBuilder from './PostgrestFilterBuilder' import PostgrestBuilder from './PostgrestBuilder' import { DEFAULT_HEADERS } from './constants' -import { Fetch, GenericSchema, ClientServerOptions } from './types' +import { Fetch, GenericSchema, ClientServerOptions, GetGenericDatabaseWithOptions } from './types' /** * PostgREST client. @@ -16,12 +16,13 @@ import { Fetch, GenericSchema, ClientServerOptions } from './types' */ export default class PostgrestClient< Database = any, - ClientOptions extends ClientServerOptions = { postgrestVersion: 12 }, - SchemaName extends string & keyof Database = 'public' extends keyof Database + ClientOptions extends ClientServerOptions = GetGenericDatabaseWithOptions['options'], + SchemaName extends string & + keyof GetGenericDatabaseWithOptions['db'] = 'public' extends keyof GetGenericDatabaseWithOptions['db'] ? 'public' - : string & keyof Database, - Schema extends GenericSchema = Database[SchemaName] extends GenericSchema - ? Database[SchemaName] + : string & keyof GetGenericDatabaseWithOptions['db'], + Schema extends GenericSchema = GetGenericDatabaseWithOptions['db'][SchemaName] extends GenericSchema + ? GetGenericDatabaseWithOptions['db'][SchemaName] : any > { url: string @@ -85,7 +86,7 @@ export default class PostgrestClient< * * @param schema - The schema to query */ - schema( + schema['db']>( schema: DynamicSchema ): PostgrestClient< Database, diff --git a/src/index.ts b/src/index.ts index 23b12d52..49d74583 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,7 @@ export type { PostgrestSingleResponse, PostgrestMaybeSingleResponse, ClientServerOptions, + PostgRESTVersion, } from './types' // https://github.com/supabase/postgrest-js/issues/551 // To be replaced with a helper type that only uses public types diff --git a/src/types.ts b/src/types.ts index de350db4..71a79c81 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,6 @@ import PostgrestError from './PostgrestError' import { ContainsNull } from './select-query-parser/types' -import { SelectQueryError } from './select-query-parser/utils' +import { IsAny, SelectQueryError } from './select-query-parser/utils' export type Fetch = typeof fetch @@ -76,8 +76,28 @@ export type ClientServerOptions = { postgrestVersion?: PostgRESTVersion } +export type DatabaseWithOptions = { + db: Database + options: Options +} + +const INTERNAL_SUPABASE_OPTIONS = '__internal_supabase' + +export type GetGenericDatabaseWithOptions< + Database, + Opts extends ClientServerOptions = { postgrestVersion: 12 } +> = IsAny extends true + ? DatabaseWithOptions + : typeof INTERNAL_SUPABASE_OPTIONS extends keyof Database + ? DatabaseWithOptions< + Omit, + Database[typeof INTERNAL_SUPABASE_OPTIONS] + > + : DatabaseWithOptions + // https://twitter.com/mattpocockuk/status/1622730173446557697 export type Prettify = { [K in keyof T]: T[K] } & {} + // https://github.com/sindresorhus/type-fest export type SimplifyDeep = ConditionalSimplifyDeep< Type, diff --git a/test/index.test-d.ts b/test/index.test-d.ts index b357050c..0e1185d6 100644 --- a/test/index.test-d.ts +++ b/test/index.test-d.ts @@ -4,9 +4,11 @@ import { PostgrestClient, PostgrestError } from '../src/index' import { Prettify } from '../src/types' import { Json } from '../src/select-query-parser/types' import { Database } from './types.override' +import { Database as DatabaseWithOptions } from './types.override-with-options-postgrest13' const REST_URL = 'http://localhost:3000' const postgrest = new PostgrestClient(REST_URL) +const postgrestWithOptions = new PostgrestClient(REST_URL) // table invalid type { @@ -295,20 +297,9 @@ const postgrest = new PostgrestClient(REST_URL) }[] >(result.data) } -// Json string Accessor with custom types overrides +// Check that client options __internal_supabase isn't considered like the other schemas { - 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) + await postgrestWithOptions + // @ts-expect-error supabase internal shouldn't be available as one of the selectable schema + .schema('__internal_supabase') } diff --git a/test/relationships.ts b/test/relationships.ts index 8b85d9d7..ff7e740a 100644 --- a/test/relationships.ts +++ b/test/relationships.ts @@ -1,10 +1,12 @@ import { PostgrestClient } from '../src/index' import { Database } from './types.override' +import { Database as DatabaseWithOptions13 } from './types.override-with-options-postgrest13' const REST_URL = 'http://localhost:3000' export 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) const userColumn: 'catchphrase' | 'username' = 'username' @@ -350,6 +352,9 @@ export const selectQueries = { selectSpreadOnManyRelation13: postgrest13 .from(selectParams.selectSpreadOnManyRelation.from) .select(selectParams.selectSpreadOnManyRelation.select), + selectSpreadOnManyRelation13FromDatabaseType: postgrest13FromDatabaseTypes + .from(selectParams.selectSpreadOnManyRelation.from) + .select(selectParams.selectSpreadOnManyRelation.select), selectWithDuplicatesFields: postgrest .from(selectParams.selectWithDuplicatesFields.from) .select(selectParams.selectWithDuplicatesFields.select), @@ -1778,6 +1783,27 @@ test('select spread on many relation postgrest13', async () => { `) }) +test('select spread on many relation postgrest13FromDatabaseTypes', async () => { + const res = await selectQueries.selectSpreadOnManyRelation13FromDatabaseType.limit(1).single() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "channel_id": 1, + "id": Array [ + 1, + ], + "message": Array [ + "Hello World 👋", + ], + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + test('multiple times the same column in selection', async () => { const res = await selectQueries.selectWithDuplicatesFields.limit(1).single() expect(res).toMatchInlineSnapshot(` diff --git a/test/select-query-parser/select.test-d.ts b/test/select-query-parser/select.test-d.ts index ad20b258..3d83e7ac 100644 --- a/test/select-query-parser/select.test-d.ts +++ b/test/select-query-parser/select.test-d.ts @@ -672,6 +672,20 @@ type Schema = Database['public'] expectType>(true) } +// spread over a many relation with postgrest13 passed within the Database type +{ + const { data } = await selectQueries.selectSpreadOnManyRelation13FromDatabaseType + .limit(1) + .single() + let result: Exclude + let expected: { + channel_id: number + id: Array + message: Array + } + expectType>(true) +} + // multiple times the same column in selection { const { data } = await selectQueries.selectWithDuplicatesFields.limit(1).single() diff --git a/test/types.generated-with-options-postgrest13.ts b/test/types.generated-with-options-postgrest13.ts new file mode 100644 index 00000000..e5819721 --- /dev/null +++ b/test/types.generated-with-options-postgrest13.ts @@ -0,0 +1,662 @@ +export type Json = unknown + +export type Database = { + // This is a dummy non existent schema to allow automatically passing down options + // to the instanciated client at type levels from the introspected database + __internal_supabase: { + postgrestVersion: 13 + // We make this still abide to `GenericSchema` to allow types helpers bellow to work the same + Tables: { + [_ in never]: never + } + Views: { + [_ in never]: never + } + Functions: { + [_ in never]: never + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } + 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'] + } + void_func: { + Args: Record + Returns: undefined + } + } + Enums: { + user_status: 'ONLINE' | 'OFFLINE' + } + CompositeTypes: { + [_ in never]: never + } + } +} + +type DefaultSchema = Database[Extract] + +export type Tables< + DefaultSchemaTableNameOrOptions extends + | keyof (DefaultSchema['Tables'] & DefaultSchema['Views']) + | { schema: keyof Database }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof Database + } + ? keyof (Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] & + Database[DefaultSchemaTableNameOrOptions['schema']]['Views']) + : never = never +> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } + ? (Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] & + Database[DefaultSchemaTableNameOrOptions['schema']]['Views'])[TableName] extends { + Row: infer R + } + ? R + : never + : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema['Tables'] & DefaultSchema['Views']) + ? (DefaultSchema['Tables'] & DefaultSchema['Views'])[DefaultSchemaTableNameOrOptions] extends { + Row: infer R + } + ? R + : never + : never + +export type TablesInsert< + DefaultSchemaTableNameOrOptions extends + | keyof DefaultSchema['Tables'] + | { schema: keyof Database }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof Database + } + ? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] + : never = never +> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } + ? Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends { + Insert: infer I + } + ? I + : never + : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables'] + ? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends { + Insert: infer I + } + ? I + : never + : never + +export type TablesUpdate< + DefaultSchemaTableNameOrOptions extends + | keyof DefaultSchema['Tables'] + | { schema: keyof Database }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof Database + } + ? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] + : never = never +> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } + ? Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends { + Update: infer U + } + ? U + : never + : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables'] + ? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends { + Update: infer U + } + ? U + : never + : never + +export type Enums< + DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums'] | { schema: keyof Database }, + EnumName extends DefaultSchemaEnumNameOrOptions extends { + schema: keyof Database + } + ? keyof Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums'] + : never = never +> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database } + ? Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums'][EnumName] + : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums'] + ? DefaultSchema['Enums'][DefaultSchemaEnumNameOrOptions] + : never + +export type CompositeTypes< + PublicCompositeTypeNameOrOptions extends + | keyof DefaultSchema['CompositeTypes'] + | { schema: keyof Database }, + CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { + schema: keyof Database + } + ? keyof Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'] + : never = never +> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database } + ? Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName] + : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema['CompositeTypes'] + ? DefaultSchema['CompositeTypes'][PublicCompositeTypeNameOrOptions] + : never + +export const Constants = { + personal: { + Enums: { + user_status: ['ONLINE', 'OFFLINE'], + }, + }, + public: { + Enums: { + user_status: ['ONLINE', 'OFFLINE'], + }, + }, +} as const diff --git a/test/types.override-with-options-postgrest13.ts b/test/types.override-with-options-postgrest13.ts new file mode 100644 index 00000000..56974416 --- /dev/null +++ b/test/types.override-with-options-postgrest13.ts @@ -0,0 +1,149 @@ +import type { Database as GeneratedDatabase } from './types.generated-with-options-postgrest13' +import { MergeDeep } from 'type-fest' + +export type CustomUserDataType = { + foo: string + bar: { + baz: number + } + en: 'ONE' | 'TWO' | 'THREE' + record: Record | null + recordNumber: Record | null +} + +export type Database = MergeDeep< + GeneratedDatabase, + { + personal: { + Tables: { + users: { + Row: { + data: CustomUserDataType | null + } + Insert: { + data?: CustomUserDataType | null + } + Update: { + data?: CustomUserDataType | null + } + } + } + } + public: { + Tables: { + users: { + Row: { + data: CustomUserDataType | null + } + Insert: { + data?: CustomUserDataType | null + } + Update: { + data?: CustomUserDataType | null + } + } + } + } + } +> + +type DefaultSchema = Database[Extract] + +export type Tables< + DefaultSchemaTableNameOrOptions extends + | keyof (DefaultSchema['Tables'] & DefaultSchema['Views']) + | { schema: keyof Database }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof Database + } + ? keyof (Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] & + Database[DefaultSchemaTableNameOrOptions['schema']]['Views']) + : never = never +> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } + ? (Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] & + Database[DefaultSchemaTableNameOrOptions['schema']]['Views'])[TableName] extends { + Row: infer R + } + ? R + : never + : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema['Tables'] & DefaultSchema['Views']) + ? (DefaultSchema['Tables'] & DefaultSchema['Views'])[DefaultSchemaTableNameOrOptions] extends { + Row: infer R + } + ? R + : never + : never + +export type TablesInsert< + DefaultSchemaTableNameOrOptions extends + | keyof DefaultSchema['Tables'] + | { schema: keyof Database }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof Database + } + ? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] + : never = never +> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } + ? Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends { + Insert: infer I + } + ? I + : never + : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables'] + ? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends { + Insert: infer I + } + ? I + : never + : never + +export type TablesUpdate< + DefaultSchemaTableNameOrOptions extends + | keyof DefaultSchema['Tables'] + | { schema: keyof Database }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof Database + } + ? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] + : never = never +> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } + ? Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends { + Update: infer U + } + ? U + : never + : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables'] + ? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends { + Update: infer U + } + ? U + : never + : never + +export type Enums< + DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums'] | { schema: keyof Database }, + EnumName extends DefaultSchemaEnumNameOrOptions extends { + schema: keyof Database + } + ? keyof Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums'] + : never = never +> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database } + ? Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums'][EnumName] + : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums'] + ? DefaultSchema['Enums'][DefaultSchemaEnumNameOrOptions] + : never + +export type CompositeTypes< + PublicCompositeTypeNameOrOptions extends + | keyof DefaultSchema['CompositeTypes'] + | { schema: keyof Database }, + CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { + schema: keyof Database + } + ? keyof Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'] + : never = never +> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database } + ? Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName] + : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema['CompositeTypes'] + ? DefaultSchema['CompositeTypes'][PublicCompositeTypeNameOrOptions] + : never From ab5b06d80c2acd5d2d3419327135cf82e9ffd906 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 21 May 2025 11:32:29 +0200 Subject: [PATCH 09/15] fix: types --- src/PostgrestClient.ts | 5 ++++- src/types.ts | 10 ++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/PostgrestClient.ts b/src/PostgrestClient.ts index cf19de77..8cb69a19 100644 --- a/src/PostgrestClient.ts +++ b/src/PostgrestClient.ts @@ -16,7 +16,10 @@ import { Fetch, GenericSchema, ClientServerOptions, GetGenericDatabaseWithOption */ export default class PostgrestClient< Database = any, - ClientOptions extends ClientServerOptions = GetGenericDatabaseWithOptions['options'], + ClientOptions extends ClientServerOptions = GetGenericDatabaseWithOptions< + Database, + { postgrestVersion: 12 } + >['options'], SchemaName extends string & keyof GetGenericDatabaseWithOptions['db'] = 'public' extends keyof GetGenericDatabaseWithOptions['db'] ? 'public' diff --git a/src/types.ts b/src/types.ts index 71a79c81..35d46210 100644 --- a/src/types.ts +++ b/src/types.ts @@ -89,10 +89,12 @@ export type GetGenericDatabaseWithOptions< > = IsAny extends true ? DatabaseWithOptions : typeof INTERNAL_SUPABASE_OPTIONS extends keyof Database - ? DatabaseWithOptions< - Omit, - Database[typeof INTERNAL_SUPABASE_OPTIONS] - > + ? Database[typeof INTERNAL_SUPABASE_OPTIONS] extends ClientServerOptions + ? DatabaseWithOptions< + Omit, + Database[typeof INTERNAL_SUPABASE_OPTIONS] + > + : DatabaseWithOptions, Opts> : DatabaseWithOptions // https://twitter.com/mattpocockuk/status/1622730173446557697 From 4110185e00e7aa70d0e22021595a4222f7b9ecb6 Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 22 May 2025 14:49:16 +0200 Subject: [PATCH 10/15] chore: use helper for feature flag from version --- src/index.ts | 1 - src/select-query-parser/result.ts | 5 ++++- src/types.ts | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 49d74583..23b12d52 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,7 +29,6 @@ export type { PostgrestSingleResponse, PostgrestMaybeSingleResponse, ClientServerOptions, - PostgRESTVersion, } from './types' // https://github.com/supabase/postgrest-js/issues/551 // To be replaced with a helper type that only uses public types diff --git a/src/select-query-parser/result.ts b/src/select-query-parser/result.ts index f10c870a..accb91f1 100644 --- a/src/select-query-parser/result.ts +++ b/src/select-query-parser/result.ts @@ -21,6 +21,9 @@ import { SelectQueryError, } from './utils' +export type SpreadOnManyEnabled = + postgrestVersion extends 13 ? true : false + /** * Main entry point for constructing the result type of a PostgREST query. * @@ -445,7 +448,7 @@ type ProcessSpreadNode< ? Result extends SelectQueryError ? SelectQueryError : ExtractFirstProperty extends unknown[] - ? ClientOptions['postgrestVersion'] extends 13 // Spread over an many-to-many relationship, turn all the result fields into correlated arrays + ? SpreadOnManyEnabled extends true // Spread over an many-to-many relationship, turn all the result fields into correlated arrays ? ProcessManyToManySpreadNodeResult : { [K in Spread['target']['name']]: SelectQueryError<`"${RelationName}" and "${Spread['target']['name']}" do not form a many-to-one or one-to-one relationship spread not possible`> diff --git a/src/types.ts b/src/types.ts index 35d46210..655f703e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -71,9 +71,9 @@ export type GenericSchema = { Functions: Record } -export type PostgRESTVersion = 12 | 13 +type PostgRESTMajorVersion = number export type ClientServerOptions = { - postgrestVersion?: PostgRESTVersion + postgrestVersion?: PostgRESTMajorVersion } export type DatabaseWithOptions = { From 3be45ad6b3e16f46460f54ace335507f87c2fe5d Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 22 May 2025 15:50:57 +0200 Subject: [PATCH 11/15] chore: use string for postgrestVersion Allows loosy matching over the version minor --- src/PostgrestClient.ts | 2 +- src/select-query-parser/result.ts | 4 ++-- src/types.ts | 5 ++--- test/basic.ts | 9 ++++++--- test/relationships.ts | 2 +- test/returns.test-d.ts | 2 +- test/select-query-parser/result.test-d.ts | 4 ++-- test/types.generated-with-options-postgrest13.ts | 2 +- 8 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/PostgrestClient.ts b/src/PostgrestClient.ts index 8cb69a19..e97bab3c 100644 --- a/src/PostgrestClient.ts +++ b/src/PostgrestClient.ts @@ -18,7 +18,7 @@ export default class PostgrestClient< Database = any, ClientOptions extends ClientServerOptions = GetGenericDatabaseWithOptions< Database, - { postgrestVersion: 12 } + { postgrestVersion: '12' } >['options'], SchemaName extends string & keyof GetGenericDatabaseWithOptions['db'] = 'public' extends keyof GetGenericDatabaseWithOptions['db'] diff --git a/src/select-query-parser/result.ts b/src/select-query-parser/result.ts index accb91f1..e0eb5bde 100644 --- a/src/select-query-parser/result.ts +++ b/src/select-query-parser/result.ts @@ -21,8 +21,8 @@ import { SelectQueryError, } from './utils' -export type SpreadOnManyEnabled = - postgrestVersion extends 13 ? true : false +export type SpreadOnManyEnabled = + postgrestVersion extends `13${string}` ? true : false /** * Main entry point for constructing the result type of a PostgREST query. diff --git a/src/types.ts b/src/types.ts index 655f703e..2cddf70f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -71,9 +71,8 @@ export type GenericSchema = { Functions: Record } -type PostgRESTMajorVersion = number export type ClientServerOptions = { - postgrestVersion?: PostgRESTMajorVersion + postgrestVersion?: string } export type DatabaseWithOptions = { @@ -85,7 +84,7 @@ const INTERNAL_SUPABASE_OPTIONS = '__internal_supabase' export type GetGenericDatabaseWithOptions< Database, - Opts extends ClientServerOptions = { postgrestVersion: 12 } + Opts extends ClientServerOptions = { postgrestVersion: '12' } > = IsAny extends true ? DatabaseWithOptions : typeof INTERNAL_SUPABASE_OPTIONS extends keyof Database diff --git a/test/basic.ts b/test/basic.ts index 3ef1fb2c..31b9b9e4 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -319,9 +319,12 @@ describe('custom prefer headers with ', () => { }) test('switch schema', async () => { - const postgrest = new PostgrestClient(REST_URL, { - schema: 'personal', - }) + const postgrest = new PostgrestClient( + REST_URL, + { + schema: 'personal', + } + ) const res = await postgrest.from('users').select() expect(res).toMatchInlineSnapshot(` Object { diff --git a/test/relationships.ts b/test/relationships.ts index ff7e740a..2acdae54 100644 --- a/test/relationships.ts +++ b/test/relationships.ts @@ -5,7 +5,7 @@ import { Database as DatabaseWithOptions13 } from './types.override-with-options const REST_URL = 'http://localhost:3000' export const postgrest = new PostgrestClient(REST_URL) const REST_URL_13 = 'http://localhost:3001' -const postgrest13 = new PostgrestClient(REST_URL_13) +const postgrest13 = new PostgrestClient(REST_URL_13) const postgrest13FromDatabaseTypes = new PostgrestClient(REST_URL_13) const userColumn: 'catchphrase' | 'username' = 'username' diff --git a/test/returns.test-d.ts b/test/returns.test-d.ts index e73708b8..f9760284 100644 --- a/test/returns.test-d.ts +++ b/test/returns.test-d.ts @@ -53,7 +53,7 @@ const postgrest = new PostgrestClient(REST_URL) .returns<{ username: string }[]>() expectType< PostgrestBuilder< - { postgrestVersion: 12 }, + { postgrestVersion: '12' }, { Error: 'Type mismatch: Cannot cast single object to array type. Remove Array wrapper from return type or make sure you are not using .single() up in the calling chain' }, diff --git a/test/select-query-parser/result.test-d.ts b/test/select-query-parser/result.test-d.ts index d4c6d559..e8ec925d 100644 --- a/test/select-query-parser/result.test-d.ts +++ b/test/select-query-parser/result.test-d.ts @@ -16,7 +16,7 @@ type SelectQueryFromTableResult< TableName, Database['public']['Tables'][TableName]['Relationships'], Q, - { postgrestVersion: 12 } + { postgrestVersion: '12' } > // This test file is here to help develop, debug and maintain the GetResult @@ -132,7 +132,7 @@ type SelectQueryFromTableResult< TableName, Database['personal']['Tables'][TableName]['Relationships'], Q, - { postgrestVersion: 12 } + { postgrestVersion: '12' } > // Should work with Json object accessor { diff --git a/test/types.generated-with-options-postgrest13.ts b/test/types.generated-with-options-postgrest13.ts index e5819721..7cad255d 100644 --- a/test/types.generated-with-options-postgrest13.ts +++ b/test/types.generated-with-options-postgrest13.ts @@ -4,7 +4,7 @@ export type Database = { // This is a dummy non existent schema to allow automatically passing down options // to the instanciated client at type levels from the introspected database __internal_supabase: { - postgrestVersion: 13 + postgrestVersion: '13.0.12' // We make this still abide to `GenericSchema` to allow types helpers bellow to work the same Tables: { [_ in never]: never From ce274ac92b36bdfb7dca5b14652b9c1395452cb1 Mon Sep 17 00:00:00 2001 From: avallete Date: Tue, 3 Jun 2025 11:42:34 +0200 Subject: [PATCH 12/15] chore: exclude types.ts from coverage --- jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index bc64026a..9029cc9c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,5 +1,5 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', - collectCoverageFrom: ['src/**/*'], + collectCoverageFrom: ['src/**/*', '!src/types.ts'], } From a376d5f2aced39c89de544831156ccae5aa8cd45 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 2 Jul 2025 15:19:45 +0200 Subject: [PATCH 13/15] chore: use CamelCasing convention --- src/PostgrestClient.ts | 2 +- src/select-query-parser/result.ts | 6 +++--- src/types.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/PostgrestClient.ts b/src/PostgrestClient.ts index e97bab3c..1d8220e1 100644 --- a/src/PostgrestClient.ts +++ b/src/PostgrestClient.ts @@ -18,7 +18,7 @@ export default class PostgrestClient< Database = any, ClientOptions extends ClientServerOptions = GetGenericDatabaseWithOptions< Database, - { postgrestVersion: '12' } + { PostgrestVersion: '12' } >['options'], SchemaName extends string & keyof GetGenericDatabaseWithOptions['db'] = 'public' extends keyof GetGenericDatabaseWithOptions['db'] diff --git a/src/select-query-parser/result.ts b/src/select-query-parser/result.ts index e0eb5bde..20eb46da 100644 --- a/src/select-query-parser/result.ts +++ b/src/select-query-parser/result.ts @@ -21,8 +21,8 @@ import { SelectQueryError, } from './utils' -export type SpreadOnManyEnabled = - postgrestVersion extends `13${string}` ? true : false +export type SpreadOnManyEnabled = + PostgrestVersion extends `13${string}` ? true : false /** * Main entry point for constructing the result type of a PostgREST query. @@ -448,7 +448,7 @@ type ProcessSpreadNode< ? Result extends SelectQueryError ? SelectQueryError : ExtractFirstProperty extends unknown[] - ? SpreadOnManyEnabled extends true // Spread over an many-to-many relationship, turn all the result fields into correlated arrays + ? SpreadOnManyEnabled extends true // Spread over an many-to-many relationship, turn all the result fields into correlated arrays ? ProcessManyToManySpreadNodeResult : { [K in Spread['target']['name']]: SelectQueryError<`"${RelationName}" and "${Spread['target']['name']}" do not form a many-to-one or one-to-one relationship spread not possible`> diff --git a/src/types.ts b/src/types.ts index 2cddf70f..e58d8d68 100644 --- a/src/types.ts +++ b/src/types.ts @@ -72,7 +72,7 @@ export type GenericSchema = { } export type ClientServerOptions = { - postgrestVersion?: string + PostgrestVersion?: string } export type DatabaseWithOptions = { @@ -80,11 +80,11 @@ export type DatabaseWithOptions = options: Options } -const INTERNAL_SUPABASE_OPTIONS = '__internal_supabase' +const INTERNAL_SUPABASE_OPTIONS = '__InternalSupabase' export type GetGenericDatabaseWithOptions< Database, - Opts extends ClientServerOptions = { postgrestVersion: '12' } + Opts extends ClientServerOptions = { PostgrestVersion: '12' } > = IsAny extends true ? DatabaseWithOptions : typeof INTERNAL_SUPABASE_OPTIONS extends keyof Database From f0eb70f529f96fa2a507d86661b9eb4da745068b Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 2 Jul 2025 15:43:30 +0200 Subject: [PATCH 14/15] chore: update tests --- test/basic.ts | 2 +- test/index.test-d.ts | 4 ++-- test/relationships.ts | 2 +- test/returns.test-d.ts | 2 +- test/select-query-parser/result.test-d.ts | 4 ++-- test/types.generated-with-options-postgrest13.ts | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/test/basic.ts b/test/basic.ts index 31b9b9e4..b00f666e 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -319,7 +319,7 @@ describe('custom prefer headers with ', () => { }) test('switch schema', async () => { - const postgrest = new PostgrestClient( + const postgrest = new PostgrestClient( REST_URL, { schema: 'personal', diff --git a/test/index.test-d.ts b/test/index.test-d.ts index 0e1185d6..fe66c3bf 100644 --- a/test/index.test-d.ts +++ b/test/index.test-d.ts @@ -297,9 +297,9 @@ const postgrestWithOptions = new PostgrestClient(REST_URL) }[] >(result.data) } -// Check that client options __internal_supabase isn't considered like the other schemas +// Check that client options __InternalSupabase isn't considered like the other schemas { await postgrestWithOptions // @ts-expect-error supabase internal shouldn't be available as one of the selectable schema - .schema('__internal_supabase') + .schema('__InternalSupabase') } diff --git a/test/relationships.ts b/test/relationships.ts index 2acdae54..2f93beae 100644 --- a/test/relationships.ts +++ b/test/relationships.ts @@ -5,7 +5,7 @@ import { Database as DatabaseWithOptions13 } from './types.override-with-options const REST_URL = 'http://localhost:3000' export const postgrest = new PostgrestClient(REST_URL) const REST_URL_13 = 'http://localhost:3001' -const postgrest13 = new PostgrestClient(REST_URL_13) +const postgrest13 = new PostgrestClient(REST_URL_13) const postgrest13FromDatabaseTypes = new PostgrestClient(REST_URL_13) const userColumn: 'catchphrase' | 'username' = 'username' diff --git a/test/returns.test-d.ts b/test/returns.test-d.ts index f9760284..b444dfd0 100644 --- a/test/returns.test-d.ts +++ b/test/returns.test-d.ts @@ -53,7 +53,7 @@ const postgrest = new PostgrestClient(REST_URL) .returns<{ username: string }[]>() expectType< PostgrestBuilder< - { postgrestVersion: '12' }, + { PostgrestVersion: '12' }, { Error: 'Type mismatch: Cannot cast single object to array type. Remove Array wrapper from return type or make sure you are not using .single() up in the calling chain' }, diff --git a/test/select-query-parser/result.test-d.ts b/test/select-query-parser/result.test-d.ts index e8ec925d..c58fd03f 100644 --- a/test/select-query-parser/result.test-d.ts +++ b/test/select-query-parser/result.test-d.ts @@ -16,7 +16,7 @@ type SelectQueryFromTableResult< TableName, Database['public']['Tables'][TableName]['Relationships'], Q, - { postgrestVersion: '12' } + { PostgrestVersion: '12' } > // This test file is here to help develop, debug and maintain the GetResult @@ -132,7 +132,7 @@ type SelectQueryFromTableResult< TableName, Database['personal']['Tables'][TableName]['Relationships'], Q, - { postgrestVersion: '12' } + { PostgrestVersion: '12' } > // Should work with Json object accessor { diff --git a/test/types.generated-with-options-postgrest13.ts b/test/types.generated-with-options-postgrest13.ts index 7cad255d..f173d1f3 100644 --- a/test/types.generated-with-options-postgrest13.ts +++ b/test/types.generated-with-options-postgrest13.ts @@ -3,8 +3,8 @@ export type Json = unknown export type Database = { // This is a dummy non existent schema to allow automatically passing down options // to the instanciated client at type levels from the introspected database - __internal_supabase: { - postgrestVersion: '13.0.12' + __InternalSupabase: { + PostgrestVersion: '13.0.12' // We make this still abide to `GenericSchema` to allow types helpers bellow to work the same Tables: { [_ in never]: never From 83923f8ea2c9b8d76a1c59e0913a7ece2b29deaa Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 2 Jul 2025 15:53:07 +0200 Subject: [PATCH 15/15] chore: use minimal schema --- ...ypes.generated-with-options-postgrest13.ts | 76 ++++++++----------- ...types.override-with-options-postgrest13.ts | 73 +++++++++++------- 2 files changed, 77 insertions(+), 72 deletions(-) diff --git a/test/types.generated-with-options-postgrest13.ts b/test/types.generated-with-options-postgrest13.ts index f173d1f3..c040e671 100644 --- a/test/types.generated-with-options-postgrest13.ts +++ b/test/types.generated-with-options-postgrest13.ts @@ -5,22 +5,6 @@ export type Database = { // to the instanciated client at type levels from the introspected database __InternalSupabase: { PostgrestVersion: '13.0.12' - // We make this still abide to `GenericSchema` to allow types helpers bellow to work the same - Tables: { - [_ in never]: never - } - Views: { - [_ in never]: never - } - Functions: { - [_ in never]: never - } - Enums: { - [_ in never]: never - } - CompositeTypes: { - [_ in never]: never - } } personal: { Tables: { @@ -547,21 +531,23 @@ export type Database = { } } -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 @@ -577,14 +563,14 @@ 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 @@ -600,14 +586,14 @@ 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 @@ -621,14 +607,16 @@ 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 @@ -636,14 +624,14 @@ 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 diff --git a/test/types.override-with-options-postgrest13.ts b/test/types.override-with-options-postgrest13.ts index 56974416..f10a30e9 100644 --- a/test/types.override-with-options-postgrest13.ts +++ b/test/types.override-with-options-postgrest13.ts @@ -47,21 +47,23 @@ export type Database = MergeDeep< } > -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 @@ -77,14 +79,14 @@ 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 @@ -100,14 +102,14 @@ 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 @@ -121,14 +123,16 @@ 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 @@ -136,14 +140,27 @@ 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 = { + personal: { + Enums: { + user_status: ['ONLINE', 'OFFLINE'], + }, + }, + public: { + Enums: { + user_status: ['ONLINE', 'OFFLINE'], + }, + }, +} as const