From c8754b72d468bec81cb7d6147372f50c5b67032f Mon Sep 17 00:00:00 2001 From: avallete Date: Fri, 21 Feb 2025 17:21:56 +0900 Subject: [PATCH 1/4] fix(types): overrideTypes work on invalid embeded relation When a relation doesn't exist, or that type inference fail to resolve it the new type should be the source of truth for the result --- src/types.ts | 4 ++- test/override-types.test-d.ts | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index 1ad1b6f0..08e39f7a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -131,7 +131,9 @@ type Simplify = T extends object ? { [K in keyof T]: T[K] } : T type MergeDeep = { [K in keyof New | keyof Row]: K extends keyof New ? K extends keyof Row - ? // Check if the override is on a embeded relation (array) + ? Row[K] extends SelectQueryError + ? New[K] + : // Check if the override is on a embeded relation (array) New[K] extends any[] ? Row[K] extends any[] ? Array, NonNullable>>> diff --git a/test/override-types.test-d.ts b/test/override-types.test-d.ts index d3028e5b..ef07eaa4 100644 --- a/test/override-types.test-d.ts +++ b/test/override-types.test-d.ts @@ -435,3 +435,55 @@ const postgrest = new PostgrestClient(REST_URL) > >(true) } + +// Test overrideTypes single object with error embeded relation +{ + const result = await postgrest.from('users').select('*, somerelation(*)').overrideTypes< + { + somerelation: { created_at: Date; data: string } + }[] + >() + if (result.error) { + throw new Error(result.error.message) + } + let data: typeof result.data + expectType< + TypeEqual< + typeof data, + { + username: string + data: CustomUserDataType | null + age_range: unknown + catchphrase: unknown + status: 'ONLINE' | 'OFFLINE' | null + somerelation: { created_at: Date; data: string } + }[] + > + >(true) +} + +// Test overrideTypes array object with error embeded relation +{ + const result = await postgrest.from('users').select('*, somerelation(*)').overrideTypes< + { + somerelation: { created_at: Date; data: string }[] + }[] + >() + if (result.error) { + throw new Error(result.error.message) + } + let data: typeof result.data + expectType< + TypeEqual< + typeof data, + { + username: string + data: CustomUserDataType | null + age_range: unknown + catchphrase: unknown + status: 'ONLINE' | 'OFFLINE' | null + somerelation: { created_at: Date; data: string }[] + }[] + > + >(true) +} From 578c69d5822029ff8a4f4387687e54313bdcebb3 Mon Sep 17 00:00:00 2001 From: avallete Date: Fri, 21 Feb 2025 18:22:29 +0900 Subject: [PATCH 2/4] fix(types): overrideTypes merging with maybeSingle --- src/PostgrestBuilder.ts | 15 ++++++++--- src/types.ts | 21 +++++++++------- test/override-types.test-d.ts | 47 +++++++++++++++++++++++++++++++++-- 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/src/PostgrestBuilder.ts b/src/PostgrestBuilder.ts index 416adc1b..86e48794 100644 --- a/src/PostgrestBuilder.ts +++ b/src/PostgrestBuilder.ts @@ -10,6 +10,7 @@ import type { IsValidResultOverride, } from './types' import PostgrestError from './PostgrestError' +import { ContainsNull } from './select-query-parser/types' export default abstract class PostgrestBuilder implements @@ -257,14 +258,20 @@ export default abstract class PostgrestBuilder(): PostgrestBuilder< - IsValidResultOverride extends true - ? MergePartialResult + IsValidResultOverride extends true + ? // Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`) + ContainsNull extends true + ? MergePartialResult, Options> | null + : MergePartialResult : CheckMatchingArrayTypes, ThrowOnError > { return this as unknown as PostgrestBuilder< - IsValidResultOverride extends true - ? MergePartialResult + IsValidResultOverride extends true + ? // Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`) + ContainsNull extends true + ? MergePartialResult, Options> | null + : MergePartialResult : CheckMatchingArrayTypes, ThrowOnError > diff --git a/src/types.ts b/src/types.ts index 08e39f7a..a5b6c913 100644 --- a/src/types.ts +++ b/src/types.ts @@ -92,20 +92,16 @@ type NonRecursiveType = BuiltIns | Function | (new (...arguments_: any[]) => unk type BuiltIns = Primitive | void | Date | RegExp type Primitive = null | undefined | string | number | boolean | symbol | bigint -export type IsValidResultOverride = +export type IsValidResultOverride = Result extends any[] ? NewResult extends any[] ? // Both are arrays - valid - Ok + true : ErrorResult : NewResult extends any[] ? ErrorNewResult : // Neither are arrays - valid - // Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`) - ContainsNull extends true - ? Ok | null - : Ok - + true /** * Utility type to check if array types match between Result and NewResult. * Returns either the valid NewResult type or an error message type. @@ -117,14 +113,21 @@ export type CheckMatchingArrayTypes = : IsValidResultOverride< Result, NewResult, - NewResult, { Error: 'Type mismatch: Cannot cast array result to a single object. Use .returns> for array results or .single() to convert the result to a single object' }, { 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' } - > + > extends infer ValidationResult + ? ValidationResult extends true + ? // Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`) + ContainsNull extends true + ? NewResult | null + : NewResult + : // contains the error + ValidationResult + : never type Simplify = T extends object ? { [K in keyof T]: T[K] } : T diff --git a/test/override-types.test-d.ts b/test/override-types.test-d.ts index ef07eaa4..368925d0 100644 --- a/test/override-types.test-d.ts +++ b/test/override-types.test-d.ts @@ -60,7 +60,7 @@ const postgrest = new PostgrestClient(REST_URL) let result: typeof singleResult.data expectType>(true) } -// Test with maybeSingle() +// Test with maybeSingle() merging with new field { const maybeSingleResult = await postgrest .from('users') @@ -71,7 +71,50 @@ const postgrest = new PostgrestClient(REST_URL) throw new Error(maybeSingleResult.error.message) } let maybeSingleResultType: typeof maybeSingleResult.data - let expectedType: { custom_field: string } | null + let expectedType: { + age_range: unknown + catchphrase: unknown + data: CustomUserDataType | null + status: 'ONLINE' | 'OFFLINE' | null + username: string + custom_field: string + } | null + expectType>(true) +} +// Test with maybeSingle() merging with override field +{ + const maybeSingleResult = await postgrest + .from('users') + .select() + .maybeSingle() + .overrideTypes<{ catchphrase: string }>() + if (maybeSingleResult.error) { + throw new Error(maybeSingleResult.error.message) + } + let maybeSingleResultType: typeof maybeSingleResult.data + let expectedType: { + age_range: unknown + catchphrase: string + data: CustomUserDataType | null + status: 'ONLINE' | 'OFFLINE' | null + username: string + } | null + expectType>(true) +} +// Test with maybeSingle() replace with override field +{ + const maybeSingleResult = await postgrest + .from('users') + .select() + .maybeSingle() + .overrideTypes<{ catchphrase: string }, { merge: false }>() + if (maybeSingleResult.error) { + throw new Error(maybeSingleResult.error.message) + } + let maybeSingleResultType: typeof maybeSingleResult.data + let expectedType: { + catchphrase: string + } | null expectType>(true) } // Test replacing behavior From 50eb37c52ecb383278a0cdde106fb489c59bb73d Mon Sep 17 00:00:00 2001 From: avallete Date: Fri, 21 Feb 2025 23:04:45 +0900 Subject: [PATCH 3/4] wip --- test/override-types.test-d.ts | 60 ++++++++++++++++++++--------------- test/types.ts | 1 + 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/test/override-types.test-d.ts b/test/override-types.test-d.ts index 368925d0..ce38ed12 100644 --- a/test/override-types.test-d.ts +++ b/test/override-types.test-d.ts @@ -246,6 +246,7 @@ const postgrest = new PostgrestClient(REST_URL) foo: number bar: { baz: number } en: 'ONE' | 'TWO' | 'THREE' + record: Record | null qux: boolean } age_range: unknown @@ -275,6 +276,7 @@ const postgrest = new PostgrestClient(REST_URL) foo: number bar: { baz: number } en: 'ONE' | 'TWO' | 'THREE' + record: Record | null qux: boolean } | null age_range: unknown @@ -342,6 +344,38 @@ const postgrest = new PostgrestClient(REST_URL) foo: string bar: { baz: number; newBaz: string } en: 'FOUR' // Overridden enum value + record: Record | null + } + age_range: unknown + catchphrase: unknown + status: 'ONLINE' | 'OFFLINE' | null + }[] + > + >(true) +} + +// Test merging with Json defined as Record +{ + const result = await postgrest + .from('users') + .select() + .overrideTypes<{ data: { record: { baz: 'foo' } } }[]>() + if (result.error) { + throw new Error(result.error.message) + } + let data: typeof result.data + expectType< + TypeEqual< + typeof data, + { + username: string + data: { + foo: string + bar: { + baz: number + } + en: 'ONE' | 'TWO' | 'THREE' + record: { baz: 'foo' } } age_range: unknown catchphrase: unknown @@ -504,29 +538,3 @@ const postgrest = new PostgrestClient(REST_URL) > >(true) } - -// Test overrideTypes array object with error embeded relation -{ - const result = await postgrest.from('users').select('*, somerelation(*)').overrideTypes< - { - somerelation: { created_at: Date; data: string }[] - }[] - >() - if (result.error) { - throw new Error(result.error.message) - } - let data: typeof result.data - expectType< - TypeEqual< - typeof data, - { - username: string - data: CustomUserDataType | null - age_range: unknown - catchphrase: unknown - status: 'ONLINE' | 'OFFLINE' | null - somerelation: { created_at: Date; data: string }[] - }[] - > - >(true) -} diff --git a/test/types.ts b/test/types.ts index 47d9bde3..e8317ee2 100644 --- a/test/types.ts +++ b/test/types.ts @@ -6,6 +6,7 @@ export type CustomUserDataType = { baz: number } en: 'ONE' | 'TWO' | 'THREE' + record: Record | null } export type Database = { From 29e8c9640ecf36a78ca441fb1cf6118caab4272b Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 24 Feb 2025 00:47:47 +0900 Subject: [PATCH 4/4] fix(types): allow merge Record and litteral object --- src/types.ts | 28 ++++++++++++++++++++++------ test/override-types.test-d.ts | 14 ++++++++++++-- test/types.ts | 1 + 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/types.ts b/src/types.ts index a5b6c913..569866bb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -131,24 +131,31 @@ export type CheckMatchingArrayTypes = type Simplify = T extends object ? { [K in keyof T]: T[K] } : T -type MergeDeep = { - [K in keyof New | keyof Row]: K extends keyof New +// Extract only explicit (non-index-signature) keys. +type ExplicitKeys = { + [K in keyof T]: string extends K ? never : K +}[keyof T] + +type MergeExplicit = { + // We merge all the explicit keys which allows merge and override of types like + // { [key: string]: unknown } and { someSpecificKey: boolean } + [K in ExplicitKeys | ExplicitKeys]: K extends keyof New ? K extends keyof Row ? Row[K] extends SelectQueryError ? New[K] - : // Check if the override is on a embeded relation (array) + : // Check if the override is on a embedded relation (array) New[K] extends any[] ? Row[K] extends any[] ? Array, NonNullable>>> : New[K] - : // Check if both properties are objects omiting a potential null union + : // Check if both properties are objects omitting a potential null union IsPlainObject> extends true ? IsPlainObject> extends true ? // If they are, use the new override as source of truth for the optionality ContainsNull extends true - ? // If the override want to preserve optionality + ? // If the override wants to preserve optionality Simplify, NonNullable>> | null - : // If the override want to enforce non-null result + : // If the override wants to enforce non-null result Simplify>> : New[K] // Override with New type if Row isn't an object : New[K] // Override primitives with New type @@ -158,6 +165,15 @@ type MergeDeep = { : never } +type MergeDeep = Simplify< + MergeExplicit & + // Intersection here is to restore dynamic keys into the merging result + // eg: + // {[key: number]: string} + // or Record + (string extends keyof Row ? { [K: string]: Row[string] } : {}) +> + // Helper to check if a type is a plain object (not an array) type IsPlainObject = T extends any[] ? false : T extends object ? true : false diff --git a/test/override-types.test-d.ts b/test/override-types.test-d.ts index ce38ed12..56164389 100644 --- a/test/override-types.test-d.ts +++ b/test/override-types.test-d.ts @@ -247,6 +247,7 @@ const postgrest = new PostgrestClient(REST_URL) bar: { baz: number } en: 'ONE' | 'TWO' | 'THREE' record: Record | null + recordNumber: Record | null qux: boolean } age_range: unknown @@ -277,6 +278,7 @@ const postgrest = new PostgrestClient(REST_URL) bar: { baz: number } en: 'ONE' | 'TWO' | 'THREE' record: Record | null + recordNumber: Record | null qux: boolean } | null age_range: unknown @@ -345,6 +347,7 @@ const postgrest = new PostgrestClient(REST_URL) bar: { baz: number; newBaz: string } en: 'FOUR' // Overridden enum value record: Record | null + recordNumber: Record | null } age_range: unknown catchphrase: unknown @@ -359,7 +362,7 @@ const postgrest = new PostgrestClient(REST_URL) const result = await postgrest .from('users') .select() - .overrideTypes<{ data: { record: { baz: 'foo' } } }[]>() + .overrideTypes<{ data: { record: { baz: 'foo' }; recordNumber: { bar: 'foo' } } }[]>() if (result.error) { throw new Error(result.error.message) } @@ -375,7 +378,14 @@ const postgrest = new PostgrestClient(REST_URL) baz: number } en: 'ONE' | 'TWO' | 'THREE' - record: { baz: 'foo' } + record: { + [x: string]: Json | undefined + baz: 'foo' + } + recordNumber: { + [x: number]: Json | undefined + bar: 'foo' + } } age_range: unknown catchphrase: unknown diff --git a/test/types.ts b/test/types.ts index e8317ee2..b916a62d 100644 --- a/test/types.ts +++ b/test/types.ts @@ -7,6 +7,7 @@ export type CustomUserDataType = { } en: 'ONE' | 'TWO' | 'THREE' record: Record | null + recordNumber: Record | null } export type Database = {