Skip to content

Commit c0d25d3

Browse files
authored
fix(types): computed field and star selector (#626)
* wip: reproduce typing error * fix(types): exclude computed field with star selector * chore: re-use generated types
1 parent 9d9c2b5 commit c0d25d3

File tree

8 files changed

+533
-66
lines changed

8 files changed

+533
-66
lines changed

src/select-query-parser/result.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from './types'
1313
import {
1414
CheckDuplicateEmbededReference,
15+
GetComputedFields,
1516
GetFieldNodeResultName,
1617
IsAny,
1718
IsRelationNullable,
@@ -234,7 +235,12 @@ export type ProcessNode<
234235
> =
235236
// TODO: figure out why comparing the `type` property is necessary vs. `NodeType extends Ast.StarNode`
236237
NodeType['type'] extends Ast.StarNode['type'] // If the selection is *
237-
? Row
238+
? // If the row has computed field, postgrest will omit them from star selection per default
239+
GetComputedFields<Schema, RelationName> extends never
240+
? // If no computed fields are detected on the row, we can return it as is
241+
Row
242+
: // otherwise we omit all the computed field from the star result return
243+
Omit<Row, GetComputedFields<Schema, RelationName>>
238244
: NodeType['type'] extends Ast.SpreadNode['type'] // If the selection is a ...spread
239245
? ProcessSpreadNode<
240246
ClientOptions,

src/select-query-parser/utils.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,3 +578,25 @@ export type IsStringUnion<T> = string extends T
578578
? false
579579
: true
580580
: false
581+
582+
type ComputedField<
583+
Schema extends GenericSchema,
584+
RelationName extends keyof TablesAndViews<Schema>,
585+
FieldName extends keyof TablesAndViews<Schema>[RelationName]['Row']
586+
> = FieldName extends keyof Schema['Functions']
587+
? Schema['Functions'][FieldName] extends {
588+
Args: { '': TablesAndViews<Schema>[RelationName]['Row'] }
589+
Returns: any
590+
}
591+
? FieldName
592+
: never
593+
: never
594+
595+
// Given a relation name (Table or View) extract all the "computed fields" based on the Row
596+
// object, and the schema functions definitions
597+
export type GetComputedFields<
598+
Schema extends GenericSchema,
599+
RelationName extends keyof TablesAndViews<Schema>
600+
> = {
601+
[K in keyof TablesAndViews<Schema>[RelationName]['Row']]: ComputedField<Schema, RelationName, K>
602+
}[keyof TablesAndViews<Schema>[RelationName]['Row']]

test/db/00-schema.sql

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,14 @@ $$ language sql immutable;
163163
create function public.function_with_array_param(param uuid[])
164164
returns void as '' language sql immutable;
165165

166-
167166
create table public.cornercase (
168167
id int primary key,
169168
"column whitespace" text,
170169
array_column text[]
171170
);
171+
172+
-- Function creating a computed field
173+
create function public.blurb_message(public.messages) returns character varying as
174+
$$
175+
select substring($1.message, 1, 3);
176+
$$ language sql stable;

test/relationships-join-operations.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ test('!left oneToMany', async () => {
171171
`)
172172
let result: Exclude<typeof res.data, null>
173173
let expected: {
174-
messages: Array<Database['public']['Tables']['messages']['Row']>
174+
messages: Array<Omit<Database['public']['Tables']['messages']['Row'], 'blurb_message'>>
175175
}
176176
expectType<TypeEqual<typeof result, typeof expected>>(true)
177177
})

test/relationships.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ test('one-to-many relationship', async () => {
205205
`)
206206
let result: Exclude<typeof res.data, null>
207207
let expected: {
208-
messages: Database['public']['Tables']['messages']['Row'][]
208+
messages: Array<Omit<Database['public']['Tables']['messages']['Row'], 'blurb_message'>>
209209
}
210210
expectType<TypeEqual<typeof result, typeof expected>>(true)
211211
})

test/resource-embedding.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { TypeEqual } from 'ts-expect'
66
const postgrest = new PostgrestClient<Database>('http://localhost:3000')
77

88
test('embedded select', async () => {
9+
// By default postgrest will omit computed field from "star" selector
910
const res = await postgrest.from('users').select('messages(*)')
1011
expect(res).toMatchInlineSnapshot(`
1112
Object {
@@ -67,6 +68,73 @@ test('embedded select', async () => {
6768
expectType<TypeEqual<typeof result, typeof expected>>(true)
6869
})
6970

71+
test('embedded select with computed field explicit selection', async () => {
72+
// If the computed field is explicitely requested on top of the star selector, it should be present in the result
73+
const res = await postgrest.from('users').select('messages(*, blurb_message)')
74+
expect(res).toMatchInlineSnapshot(`
75+
Object {
76+
"count": null,
77+
"data": Array [
78+
Object {
79+
"messages": Array [
80+
Object {
81+
"blurb_message": "Hel",
82+
"channel_id": 1,
83+
"data": null,
84+
"id": 1,
85+
"message": "Hello World 👋",
86+
"username": "supabot",
87+
},
88+
Object {
89+
"blurb_message": "Per",
90+
"channel_id": 2,
91+
"data": null,
92+
"id": 2,
93+
"message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.",
94+
"username": "supabot",
95+
},
96+
Object {
97+
"blurb_message": "Som",
98+
"channel_id": 3,
99+
"data": null,
100+
"id": 4,
101+
"message": "Some message on channel wihtout details",
102+
"username": "supabot",
103+
},
104+
],
105+
},
106+
Object {
107+
"messages": Array [],
108+
},
109+
Object {
110+
"messages": Array [],
111+
},
112+
Object {
113+
"messages": Array [],
114+
},
115+
Object {
116+
"messages": Array [],
117+
},
118+
],
119+
"error": null,
120+
"status": 200,
121+
"statusText": "OK",
122+
}
123+
`)
124+
let result: Exclude<typeof res.data, null>
125+
let expected: {
126+
messages: {
127+
channel_id: number
128+
data: unknown
129+
id: number
130+
message: string | null
131+
username: string
132+
blurb_message: string | null
133+
}[]
134+
}[]
135+
expectType<TypeEqual<typeof result, typeof expected>>(true)
136+
})
137+
70138
describe('embedded filters', () => {
71139
// TODO: Test more filters
72140
test('embedded eq', async () => {

test/select-query-parser/types.test-d.ts

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { expectType } from 'tsd'
22
import { TypeEqual } from 'ts-expect'
3-
import { DeduplicateRelationships } from '../../src/select-query-parser/utils'
3+
import { DeduplicateRelationships, GetComputedFields } from '../../src/select-query-parser/utils'
4+
import { Database } from '../types.generated'
5+
46
// Deduplicate exact sames relationships
57
{
68
type rels = [
@@ -53,3 +55,113 @@ import { DeduplicateRelationships } from '../../src/select-query-parser/utils'
5355
type result = DeduplicateRelationships<rels>
5456
expectType<TypeEqual<result, expected>>(true)
5557
}
58+
59+
// Test GetComputedFields basic
60+
{
61+
type Schema = Database['public']
62+
type result = GetComputedFields<Schema, 'users'>
63+
type expected = never
64+
expectType<TypeEqual<result, expected>>(true)
65+
}
66+
67+
// Test GetComputedFields multiples computed fields
68+
{
69+
type Json = unknown
70+
71+
type Database = {
72+
personal: {
73+
Tables: {
74+
[_ in never]: never
75+
}
76+
Views: {
77+
[_ in never]: never
78+
}
79+
Functions: {
80+
[_ in never]: never
81+
}
82+
Enums: {
83+
user_status: 'ONLINE' | 'OFFLINE'
84+
}
85+
CompositeTypes: {
86+
[_ in never]: never
87+
}
88+
}
89+
public: {
90+
Tables: {
91+
messages: {
92+
Row: {
93+
channel_id: number
94+
data: Json | null
95+
id: number
96+
message: string | null
97+
username: string
98+
blurb_message: string | null
99+
blurb_message2: number | null
100+
blurb_message3: null | null
101+
}
102+
Insert: {
103+
channel_id: number
104+
data?: Json | null
105+
id?: number
106+
message?: string | null
107+
username: string
108+
}
109+
Update: {
110+
channel_id?: number
111+
data?: Json | null
112+
id?: number
113+
message?: string | null
114+
username?: string
115+
}
116+
Relationships: []
117+
}
118+
}
119+
Views: {
120+
[_ in never]: never
121+
}
122+
Functions: {
123+
blurb_message: {
124+
Args: { '': Database['public']['Tables']['messages']['Row'] }
125+
Returns: string
126+
}
127+
blurb_message2: {
128+
Args: { '': Database['public']['Tables']['messages']['Row'] }
129+
Returns: number
130+
}
131+
blurb_message3: {
132+
Args: { '': Database['public']['Tables']['messages']['Row'] }
133+
Returns: null
134+
}
135+
function_returning_row: {
136+
Args: Record<PropertyKey, never>
137+
Returns: {
138+
age_range: unknown | null
139+
catchphrase: unknown | null
140+
data: Json | null
141+
username: string
142+
}
143+
}
144+
function_returning_set_of_rows: {
145+
Args: Record<PropertyKey, never>
146+
Returns: {
147+
age_range: unknown | null
148+
catchphrase: unknown | null
149+
data: Json | null
150+
username: string
151+
}[]
152+
}
153+
}
154+
Enums: {
155+
[_ in never]: never
156+
}
157+
CompositeTypes: {
158+
[_ in never]: never
159+
}
160+
}
161+
}
162+
163+
type Schema = Database['public']
164+
type result = GetComputedFields<Schema, 'messages'>
165+
type expected = 'blurb_message' | 'blurb_message2' | 'blurb_message3'
166+
expectType<TypeEqual<result, expected>>(true)
167+
}

0 commit comments

Comments
 (0)