From 6af5d63e7df4ef45db9beb08eeb8d14c97215e18 Mon Sep 17 00:00:00 2001 From: avallete Date: Sun, 30 Mar 2025 14:16:39 +0200 Subject: [PATCH 1/5] feat(functions): add support for functions embeding introspection --- src/lib/sql/functions.sql | 14 ++++ src/lib/types.ts | 3 + test/db/00-init.sql | 29 ++++++++ test/lib/functions.ts | 146 ++++++++++++++++++++++++++++++++++++++ test/server/typegen.ts | 36 ++++++++++ 5 files changed, 228 insertions(+) diff --git a/src/lib/sql/functions.sql b/src/lib/sql/functions.sql index d2258402..f270c5ac 100644 --- a/src/lib/sql/functions.sql +++ b/src/lib/sql/functions.sql @@ -44,6 +44,20 @@ select pg_get_function_result(f.oid) as return_type, nullif(rt.typrelid::int8, 0) as return_type_relation_id, f.proretset as is_set_returning_function, + case + when f.proretset and rt.typrelid != 0 then true + else false + end as returns_set_of_table, + case + when f.proretset and rt.typrelid != 0 then + (select relname from pg_class where oid = rt.typrelid) + else null + end as return_table_name, + case + when f.proretset then + coalesce(f.prorows, 0) > 1 + else false + end as returns_multiple_rows, case when f.provolatile = 'i' then 'IMMUTABLE' when f.provolatile = 's' then 'STABLE' diff --git a/src/lib/types.ts b/src/lib/types.ts index bfd60250..cb4759ea 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -156,6 +156,9 @@ const postgresFunctionSchema = Type.Object({ return_type: Type.String(), return_type_relation_id: Type.Union([Type.Integer(), Type.Null()]), is_set_returning_function: Type.Boolean(), + returns_set_of_table: Type.Boolean(), + return_table_name: Type.Union([Type.String(), Type.Null()]), + returns_multiple_rows: Type.Boolean(), behavior: Type.Union([ Type.Literal('IMMUTABLE'), Type.Literal('STABLE'), diff --git a/test/db/00-init.sql b/test/db/00-init.sql index 00c6a472..40d9b7ba 100644 --- a/test/db/00-init.sql +++ b/test/db/00-init.sql @@ -181,3 +181,32 @@ LANGUAGE SQL STABLE AS $$ SELECT * FROM public.todos WHERE "user-id" = todo_row."user-id"; $$; + +-- SETOF composite_type - Returns multiple rows of a custom composite type +CREATE OR REPLACE FUNCTION public.get_composite_type_data() +RETURNS SETOF composite_type_with_array_attribute +LANGUAGE SQL STABLE +AS $$ + SELECT ROW(ARRAY['hello', 'world']::text[])::composite_type_with_array_attribute + UNION ALL + SELECT ROW(ARRAY['foo', 'bar']::text[])::composite_type_with_array_attribute; +$$; + +-- SETOF record - Returns multiple rows with structure defined in the function +CREATE OR REPLACE FUNCTION public.get_user_summary() +RETURNS SETOF record +LANGUAGE SQL STABLE +AS $$ + SELECT u.id, name, count(t.id) as todo_count + FROM public.users u + LEFT JOIN public.todos t ON t."user-id" = u.id + GROUP BY u.id, u.name; +$$; + +-- SETOF scalar_type - Returns multiple values of a basic type +CREATE OR REPLACE FUNCTION public.get_user_ids() +RETURNS SETOF bigint +LANGUAGE SQL STABLE +AS $$ + SELECT id FROM public.users; +$$; diff --git a/test/lib/functions.ts b/test/lib/functions.ts index 05de3244..bac0a860 100644 --- a/test/lib/functions.ts +++ b/test/lib/functions.ts @@ -36,9 +36,12 @@ test('list', async () => { "is_set_returning_function": false, "language": "sql", "name": "add", + "return_table_name": null, "return_type": "integer", "return_type_id": 23, "return_type_relation_id": null, + "returns_multiple_rows": false, + "returns_set_of_table": false, "schema": "public", "security_definer": false, } @@ -46,6 +49,134 @@ test('list', async () => { ) }) +test('list set-returning function with single object limit', async () => { + const res = await pgMeta.functions.list() + expect(res.data?.filter(({ name }) => name === 'get_user_audit_setof_single_row')) + .toMatchInlineSnapshot(` + [ + { + "args": [ + { + "has_default": false, + "mode": "in", + "name": "user_row", + "type_id": 16395, + }, + ], + "argument_types": "user_row users", + "behavior": "STABLE", + "complete_statement": "CREATE OR REPLACE FUNCTION public.get_user_audit_setof_single_row(user_row users) + RETURNS SETOF users_audit + LANGUAGE sql + STABLE ROWS 1 + AS $function$ + SELECT * FROM public.users_audit WHERE user_id = user_row.id; + $function$ + ", + "config_params": null, + "definition": " + SELECT * FROM public.users_audit WHERE user_id = user_row.id; + ", + "id": 16498, + "identity_argument_types": "user_row users", + "is_set_returning_function": true, + "language": "sql", + "name": "get_user_audit_setof_single_row", + "return_table_name": "users_audit", + "return_type": "SETOF users_audit", + "return_type_id": 16418, + "return_type_relation_id": 16416, + "returns_multiple_rows": false, + "returns_set_of_table": true, + "schema": "public", + "security_definer": false, + }, + ] + `) +}) + +test('list set-returning function with multiples definitions', async () => { + const res = await pgMeta.functions.list() + expect(res.data?.filter(({ name }) => name === 'get_todos_setof_rows')).toMatchInlineSnapshot(` + [ + { + "args": [ + { + "has_default": false, + "mode": "in", + "name": "user_row", + "type_id": 16395, + }, + ], + "argument_types": "user_row users", + "behavior": "STABLE", + "complete_statement": "CREATE OR REPLACE FUNCTION public.get_todos_setof_rows(user_row users) + RETURNS SETOF todos + LANGUAGE sql + STABLE + AS $function$ + SELECT * FROM public.todos WHERE "user-id" = user_row.id; + $function$ + ", + "config_params": null, + "definition": " + SELECT * FROM public.todos WHERE "user-id" = user_row.id; + ", + "id": 16499, + "identity_argument_types": "user_row users", + "is_set_returning_function": true, + "language": "sql", + "name": "get_todos_setof_rows", + "return_table_name": "todos", + "return_type": "SETOF todos", + "return_type_id": 16404, + "return_type_relation_id": 16402, + "returns_multiple_rows": true, + "returns_set_of_table": true, + "schema": "public", + "security_definer": false, + }, + { + "args": [ + { + "has_default": false, + "mode": "in", + "name": "todo_row", + "type_id": 16404, + }, + ], + "argument_types": "todo_row todos", + "behavior": "STABLE", + "complete_statement": "CREATE OR REPLACE FUNCTION public.get_todos_setof_rows(todo_row todos) + RETURNS SETOF todos + LANGUAGE sql + STABLE + AS $function$ + SELECT * FROM public.todos WHERE "user-id" = todo_row."user-id"; + $function$ + ", + "config_params": null, + "definition": " + SELECT * FROM public.todos WHERE "user-id" = todo_row."user-id"; + ", + "id": 16500, + "identity_argument_types": "todo_row todos", + "is_set_returning_function": true, + "language": "sql", + "name": "get_todos_setof_rows", + "return_table_name": "todos", + "return_type": "SETOF todos", + "return_type_id": 16404, + "return_type_relation_id": 16402, + "returns_multiple_rows": true, + "returns_set_of_table": true, + "schema": "public", + "security_definer": false, + }, + ] + `) +}) + test('list functions with included schemas', async () => { let res = await pgMeta.functions.list({ includedSchemas: ['public'], @@ -136,9 +267,12 @@ test('retrieve, create, update, delete', async () => { "is_set_returning_function": false, "language": "sql", "name": "test_func", + "return_table_name": null, "return_type": "integer", "return_type_id": 23, "return_type_relation_id": null, + "returns_multiple_rows": false, + "returns_set_of_table": false, "schema": "public", "security_definer": true, }, @@ -186,9 +320,12 @@ test('retrieve, create, update, delete', async () => { "is_set_returning_function": false, "language": "sql", "name": "test_func", + "return_table_name": null, "return_type": "integer", "return_type_id": 23, "return_type_relation_id": null, + "returns_multiple_rows": false, + "returns_set_of_table": false, "schema": "public", "security_definer": true, }, @@ -240,9 +377,12 @@ test('retrieve, create, update, delete', async () => { "is_set_returning_function": false, "language": "sql", "name": "test_func_renamed", + "return_table_name": null, "return_type": "integer", "return_type_id": 23, "return_type_relation_id": null, + "returns_multiple_rows": false, + "returns_set_of_table": false, "schema": "test_schema", "security_definer": true, }, @@ -290,9 +430,12 @@ test('retrieve, create, update, delete', async () => { "is_set_returning_function": false, "language": "sql", "name": "test_func_renamed", + "return_table_name": null, "return_type": "integer", "return_type_id": 23, "return_type_relation_id": null, + "returns_multiple_rows": false, + "returns_set_of_table": false, "schema": "test_schema", "security_definer": true, }, @@ -345,9 +488,12 @@ test('retrieve set-returning function', async () => { "is_set_returning_function": true, "language": "sql", "name": "function_returning_set_of_rows", + "return_table_name": "users", "return_type": "SETOF users", "return_type_id": Any, "return_type_relation_id": Any, + "returns_multiple_rows": true, + "returns_set_of_table": true, "schema": "public", "security_definer": false, } diff --git a/test/server/typegen.ts b/test/server/typegen.ts index f62631e0..030474cc 100644 --- a/test/server/typegen.ts +++ b/test/server/typegen.ts @@ -432,6 +432,10 @@ test('typegen: typescript', async () => { name: string }[] } + get_composite_type_data: { + Args: Record + Returns: Database["public"]["CompositeTypes"]["composite_type_with_array_attribute"][] + } get_todos_setof_rows: { Args: | { user_row: Database["public"]["Tables"]["users"]["Row"] } @@ -451,6 +455,14 @@ test('typegen: typescript', async () => { user_id: number | null }[] } + get_user_ids: { + Args: Record + Returns: number[] + } + get_user_summary: { + Args: Record + Returns: Record[] + } polymorphic_function: { Args: { "": string } | { "": boolean } Returns: undefined @@ -1065,6 +1077,10 @@ test('typegen w/ one-to-one relationships', async () => { name: string }[] } + get_composite_type_data: { + Args: Record + Returns: Database["public"]["CompositeTypes"]["composite_type_with_array_attribute"][] + } get_todos_setof_rows: { Args: | { user_row: Database["public"]["Tables"]["users"]["Row"] } @@ -1084,6 +1100,14 @@ test('typegen w/ one-to-one relationships', async () => { user_id: number | null }[] } + get_user_ids: { + Args: Record + Returns: number[] + } + get_user_summary: { + Args: Record + Returns: Record[] + } polymorphic_function: { Args: { "": string } | { "": boolean } Returns: undefined @@ -1698,6 +1722,10 @@ test('typegen: typescript w/ one-to-one relationships', async () => { name: string }[] } + get_composite_type_data: { + Args: Record + Returns: Database["public"]["CompositeTypes"]["composite_type_with_array_attribute"][] + } get_todos_setof_rows: { Args: | { user_row: Database["public"]["Tables"]["users"]["Row"] } @@ -1717,6 +1745,14 @@ test('typegen: typescript w/ one-to-one relationships', async () => { user_id: number | null }[] } + get_user_ids: { + Args: Record + Returns: number[] + } + get_user_summary: { + Args: Record + Returns: Record[] + } polymorphic_function: { Args: { "": string } | { "": boolean } Returns: undefined From 96f184c471be172e9a8a76bad9b669164decb5b9 Mon Sep 17 00:00:00 2001 From: avallete Date: Sun, 30 Mar 2025 16:14:05 +0200 Subject: [PATCH 2/5] feat(functions): setup typegen for embeded function type inferences --- src/lib/sql/functions.sql | 56 ++-- src/lib/types.ts | 1 + src/server/templates/typescript.ts | 21 +- test/lib/functions.ts | 43 +-- test/server/typegen.ts | 420 +++++++++++++++-------------- 5 files changed, 305 insertions(+), 236 deletions(-) diff --git a/src/lib/sql/functions.sql b/src/lib/sql/functions.sql index f270c5ac..c921ab09 100644 --- a/src/lib/sql/functions.sql +++ b/src/lib/sql/functions.sql @@ -90,32 +90,48 @@ from select oid, jsonb_agg(jsonb_build_object( - 'mode', t2.mode, + 'mode', mode, 'name', name, 'type_id', type_id, - 'has_default', has_default + 'has_default', has_default, + 'table_name', table_name )) as args from ( select - oid, - unnest(arg_modes) as mode, - unnest(arg_names) as name, - unnest(arg_types)::int8 as type_id, - unnest(arg_has_defaults) as has_default + t1.oid, + t2.mode, + t1.name, + t1.type_id, + t1.has_default, + case + when pt.typrelid != 0 then pc.relname + else null + end as table_name from - functions - ) as t1, - lateral ( - select - case - when t1.mode = 'i' then 'in' - when t1.mode = 'o' then 'out' - when t1.mode = 'b' then 'inout' - when t1.mode = 'v' then 'variadic' - else 'table' - end as mode - ) as t2 + ( + select + oid, + unnest(arg_modes) as mode, + unnest(arg_names) as name, + unnest(arg_types)::int8 as type_id, + unnest(arg_has_defaults) as has_default + from + functions + ) as t1 + cross join lateral ( + select + case + when t1.mode = 'i' then 'in' + when t1.mode = 'o' then 'out' + when t1.mode = 'b' then 'inout' + when t1.mode = 'v' then 'variadic' + else 'table' + end as mode + ) as t2 + left join pg_type pt on pt.oid = t1.type_id + left join pg_class pc on pc.oid = pt.typrelid + ) sub group by - t1.oid + oid ) f_args on f_args.oid = f.oid diff --git a/src/lib/types.ts b/src/lib/types.ts index cb4759ea..4d4c2889 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -148,6 +148,7 @@ const postgresFunctionSchema = Type.Object({ name: Type.String(), type_id: Type.Number(), has_default: Type.Boolean(), + table_name: Type.Union([Type.String(), Type.Null()]), }) ), argument_types: Type.String(), diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index 6d2efde8..2acf3cdb 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -343,7 +343,26 @@ export type Database = { } return 'unknown' - })()}${fns[0].is_set_returning_function ? '[]' : ''} + })()}${fns[0].is_set_returning_function && fns[0].returns_multiple_rows ? '[]' : ''} + ${ + // if the function return a set of a table and some definition take in parameter another table + fns[0].returns_set_of_table && + fns.some((fnd) => fnd.args.length === 1 && fnd.args[0].table_name) + ? `SetofOptions: { + from: ${fns + // if the function take a row as first parameter + .filter((fnd) => fnd.args.length === 1 && fnd.args[0].table_name) + .map((fnd) => { + const arg_type = types.find((t) => t.id === fnd.args[0].type_id) + return JSON.stringify(arg_type?.format) + }) + .join(' | ')} + to: ${JSON.stringify(fns[0].return_table_name)} + isOneToOne: ${fns[0].returns_multiple_rows ? false : true} + } + ` + : '' + } }` ) })()} diff --git a/test/lib/functions.ts b/test/lib/functions.ts index bac0a860..0d0140ef 100644 --- a/test/lib/functions.ts +++ b/test/lib/functions.ts @@ -4,20 +4,21 @@ import { pgMeta } from './utils' test('list', async () => { const res = await pgMeta.functions.list() expect(res.data?.find(({ name }) => name === 'add')).toMatchInlineSnapshot( - { id: expect.any(Number) }, - ` + { id: expect.any(Number) }, ` { "args": [ { "has_default": false, "mode": "in", "name": "", + "table_name": null, "type_id": 23, }, { "has_default": false, "mode": "in", "name": "", + "table_name": null, "type_id": 23, }, ], @@ -45,8 +46,7 @@ test('list', async () => { "schema": "public", "security_definer": false, } - ` - ) + `) }) test('list set-returning function with single object limit', async () => { @@ -60,6 +60,7 @@ test('list set-returning function with single object limit', async () => { "has_default": false, "mode": "in", "name": "user_row", + "table_name": "users", "type_id": 16395, }, ], @@ -105,6 +106,7 @@ test('list set-returning function with multiples definitions', async () => { "has_default": false, "mode": "in", "name": "user_row", + "table_name": "users", "type_id": 16395, }, ], @@ -142,6 +144,7 @@ test('list set-returning function with multiples definitions', async () => { "has_default": false, "mode": "in", "name": "todo_row", + "table_name": "todos", "type_id": 16404, }, ], @@ -229,8 +232,7 @@ test('retrieve, create, update, delete', async () => { config_params: { search_path: 'hooks, auth', role: 'postgres' }, }) expect(res).toMatchInlineSnapshot( - { data: { id: expect.any(Number) } }, - ` + { data: { id: expect.any(Number) } }, ` { "data": { "args": [ @@ -238,12 +240,14 @@ test('retrieve, create, update, delete', async () => { "has_default": false, "mode": "in", "name": "a", + "table_name": null, "type_id": 21, }, { "has_default": false, "mode": "in", "name": "b", + "table_name": null, "type_id": 21, }, ], @@ -278,12 +282,10 @@ test('retrieve, create, update, delete', async () => { }, "error": null, } - ` - ) + `) res = await pgMeta.functions.retrieve({ id: res.data!.id }) expect(res).toMatchInlineSnapshot( - { data: { id: expect.any(Number) } }, - ` + { data: { id: expect.any(Number) } }, ` { "data": { "args": [ @@ -291,12 +293,14 @@ test('retrieve, create, update, delete', async () => { "has_default": false, "mode": "in", "name": "a", + "table_name": null, "type_id": 21, }, { "has_default": false, "mode": "in", "name": "b", + "table_name": null, "type_id": 21, }, ], @@ -331,16 +335,14 @@ test('retrieve, create, update, delete', async () => { }, "error": null, } - ` - ) + `) res = await pgMeta.functions.update(res.data!.id, { name: 'test_func_renamed', schema: 'test_schema', definition: 'select b - a', }) expect(res).toMatchInlineSnapshot( - { data: { id: expect.any(Number) } }, - ` + { data: { id: expect.any(Number) } }, ` { "data": { "args": [ @@ -348,12 +350,14 @@ test('retrieve, create, update, delete', async () => { "has_default": false, "mode": "in", "name": "a", + "table_name": null, "type_id": 21, }, { "has_default": false, "mode": "in", "name": "b", + "table_name": null, "type_id": 21, }, ], @@ -388,12 +392,10 @@ test('retrieve, create, update, delete', async () => { }, "error": null, } - ` - ) + `) res = await pgMeta.functions.remove(res.data!.id) expect(res).toMatchInlineSnapshot( - { data: { id: expect.any(Number) } }, - ` + { data: { id: expect.any(Number) } }, ` { "data": { "args": [ @@ -401,12 +403,14 @@ test('retrieve, create, update, delete', async () => { "has_default": false, "mode": "in", "name": "a", + "table_name": null, "type_id": 21, }, { "has_default": false, "mode": "in", "name": "b", + "table_name": null, "type_id": 21, }, ], @@ -441,8 +445,7 @@ test('retrieve, create, update, delete', async () => { }, "error": null, } - ` - ) + `) res = await pgMeta.functions.retrieve({ id: res.data!.id }) expect(res).toMatchObject({ data: null, diff --git a/test/server/typegen.ts b/test/server/typegen.ts index 030474cc..3eb0fc19 100644 --- a/test/server/typegen.ts +++ b/test/server/typegen.ts @@ -445,6 +445,11 @@ test('typegen: typescript', async () => { id: number "user-id": number }[] + SetofOptions: { + from: "users" | "todos" + to: "todos" + isOneToOne: false + } } get_user_audit_setof_single_row: { Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } @@ -453,7 +458,12 @@ test('typegen: typescript', async () => { id: number previous_value: Json | null user_id: number | null - }[] + } + SetofOptions: { + from: "users" + to: "users_audit" + isOneToOne: true + } } get_user_ids: { Args: Record @@ -1090,6 +1100,11 @@ test('typegen w/ one-to-one relationships', async () => { id: number "user-id": number }[] + SetofOptions: { + from: "users" | "todos" + to: "todos" + isOneToOne: false + } } get_user_audit_setof_single_row: { Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } @@ -1098,7 +1113,12 @@ test('typegen w/ one-to-one relationships', async () => { id: number previous_value: Json | null user_id: number | null - }[] + } + SetofOptions: { + from: "users" + to: "users_audit" + isOneToOne: true + } } get_user_ids: { Args: Record @@ -1735,6 +1755,11 @@ test('typegen: typescript w/ one-to-one relationships', async () => { id: number "user-id": number }[] + SetofOptions: { + from: "users" | "todos" + to: "todos" + isOneToOne: false + } } get_user_audit_setof_single_row: { Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } @@ -1743,7 +1768,12 @@ test('typegen: typescript w/ one-to-one relationships', async () => { id: number previous_value: Json | null user_id: number | null - }[] + } + SetofOptions: { + from: "users" + to: "users_audit" + isOneToOne: true + } } get_user_ids: { Args: Record @@ -1916,198 +1946,198 @@ test('typegen: go', async () => { expect(body).toMatchInlineSnapshot(` "package database -type PublicUsersSelect struct { - Id int64 \`json:"id"\` - Name *string \`json:"name"\` - Status *string \`json:"status"\` -} - -type PublicUsersInsert struct { - Id *int64 \`json:"id"\` - Name *string \`json:"name"\` - Status *string \`json:"status"\` -} - -type PublicUsersUpdate struct { - Id *int64 \`json:"id"\` - Name *string \`json:"name"\` - Status *string \`json:"status"\` -} - -type PublicTodosSelect struct { - Details *string \`json:"details"\` - Id int64 \`json:"id"\` - UserId int64 \`json:"user-id"\` -} - -type PublicTodosInsert struct { - Details *string \`json:"details"\` - Id *int64 \`json:"id"\` - UserId int64 \`json:"user-id"\` -} - -type PublicTodosUpdate struct { - Details *string \`json:"details"\` - Id *int64 \`json:"id"\` - UserId *int64 \`json:"user-id"\` -} - -type PublicUsersAuditSelect struct { - CreatedAt *string \`json:"created_at"\` - Id int64 \`json:"id"\` - PreviousValue interface{} \`json:"previous_value"\` - UserId *int64 \`json:"user_id"\` -} - -type PublicUsersAuditInsert struct { - CreatedAt *string \`json:"created_at"\` - Id *int64 \`json:"id"\` - PreviousValue interface{} \`json:"previous_value"\` - UserId *int64 \`json:"user_id"\` -} - -type PublicUsersAuditUpdate struct { - CreatedAt *string \`json:"created_at"\` - Id *int64 \`json:"id"\` - PreviousValue interface{} \`json:"previous_value"\` - UserId *int64 \`json:"user_id"\` -} - -type PublicUserDetailsSelect struct { - Details *string \`json:"details"\` - UserId int64 \`json:"user_id"\` -} - -type PublicUserDetailsInsert struct { - Details *string \`json:"details"\` - UserId int64 \`json:"user_id"\` -} - -type PublicUserDetailsUpdate struct { - Details *string \`json:"details"\` - UserId *int64 \`json:"user_id"\` -} - -type PublicEmptySelect struct { - -} - -type PublicEmptyInsert struct { - -} - -type PublicEmptyUpdate struct { - -} - -type PublicTableWithOtherTablesRowTypeSelect struct { - Col1 interface{} \`json:"col1"\` - Col2 interface{} \`json:"col2"\` -} - -type PublicTableWithOtherTablesRowTypeInsert struct { - Col1 interface{} \`json:"col1"\` - Col2 interface{} \`json:"col2"\` -} - -type PublicTableWithOtherTablesRowTypeUpdate struct { - Col1 interface{} \`json:"col1"\` - Col2 interface{} \`json:"col2"\` -} - -type PublicTableWithPrimaryKeyOtherThanIdSelect struct { - Name *string \`json:"name"\` - OtherId int64 \`json:"other_id"\` -} - -type PublicTableWithPrimaryKeyOtherThanIdInsert struct { - Name *string \`json:"name"\` - OtherId *int64 \`json:"other_id"\` -} - -type PublicTableWithPrimaryKeyOtherThanIdUpdate struct { - Name *string \`json:"name"\` - OtherId *int64 \`json:"other_id"\` -} - -type PublicCategorySelect struct { - Id int32 \`json:"id"\` - Name string \`json:"name"\` -} - -type PublicCategoryInsert struct { - Id *int32 \`json:"id"\` - Name string \`json:"name"\` -} - -type PublicCategoryUpdate struct { - Id *int32 \`json:"id"\` - Name *string \`json:"name"\` -} - -type PublicMemesSelect struct { - Category *int32 \`json:"category"\` - CreatedAt string \`json:"created_at"\` - Id int32 \`json:"id"\` - Metadata interface{} \`json:"metadata"\` - Name string \`json:"name"\` - Status *string \`json:"status"\` -} - -type PublicMemesInsert struct { - Category *int32 \`json:"category"\` - CreatedAt string \`json:"created_at"\` - Id *int32 \`json:"id"\` - Metadata interface{} \`json:"metadata"\` - Name string \`json:"name"\` - Status *string \`json:"status"\` -} - -type PublicMemesUpdate struct { - Category *int32 \`json:"category"\` - CreatedAt *string \`json:"created_at"\` - Id *int32 \`json:"id"\` - Metadata interface{} \`json:"metadata"\` - Name *string \`json:"name"\` - Status *string \`json:"status"\` -} - -type PublicTodosViewSelect struct { - Details *string \`json:"details"\` - Id *int64 \`json:"id"\` - UserId *int64 \`json:"user-id"\` -} - -type PublicUsersViewSelect struct { - Id *int64 \`json:"id"\` - Name *string \`json:"name"\` - Status *string \`json:"status"\` -} - -type PublicAViewSelect struct { - Id *int64 \`json:"id"\` -} - -type PublicUsersViewWithMultipleRefsToUsersSelect struct { - InitialId *int64 \`json:"initial_id"\` - InitialName *string \`json:"initial_name"\` - SecondId *int64 \`json:"second_id"\` - SecondName *string \`json:"second_name"\` -} - -type PublicTodosMatviewSelect struct { - Details *string \`json:"details"\` - Id *int64 \`json:"id"\` - UserId *int64 \`json:"user-id"\` -} - -type PublicCompositeTypeWithArrayAttribute struct { - MyTextArray interface{} \`json:"my_text_array"\` -} - -type PublicCompositeTypeWithRecordAttribute struct { - Todo interface{} \`json:"todo"\` -}" + type PublicUsersSelect struct { + Id int64 \`json:"id"\` + Name *string \`json:"name"\` + Status *string \`json:"status"\` + } + + type PublicUsersInsert struct { + Id *int64 \`json:"id"\` + Name *string \`json:"name"\` + Status *string \`json:"status"\` + } + + type PublicUsersUpdate struct { + Id *int64 \`json:"id"\` + Name *string \`json:"name"\` + Status *string \`json:"status"\` + } + + type PublicTodosSelect struct { + Details *string \`json:"details"\` + Id int64 \`json:"id"\` + UserId int64 \`json:"user-id"\` + } + + type PublicTodosInsert struct { + Details *string \`json:"details"\` + Id *int64 \`json:"id"\` + UserId int64 \`json:"user-id"\` + } + + type PublicTodosUpdate struct { + Details *string \`json:"details"\` + Id *int64 \`json:"id"\` + UserId *int64 \`json:"user-id"\` + } + + type PublicUsersAuditSelect struct { + CreatedAt *string \`json:"created_at"\` + Id int64 \`json:"id"\` + PreviousValue interface{} \`json:"previous_value"\` + UserId *int64 \`json:"user_id"\` + } + + type PublicUsersAuditInsert struct { + CreatedAt *string \`json:"created_at"\` + Id *int64 \`json:"id"\` + PreviousValue interface{} \`json:"previous_value"\` + UserId *int64 \`json:"user_id"\` + } + + type PublicUsersAuditUpdate struct { + CreatedAt *string \`json:"created_at"\` + Id *int64 \`json:"id"\` + PreviousValue interface{} \`json:"previous_value"\` + UserId *int64 \`json:"user_id"\` + } + + type PublicUserDetailsSelect struct { + Details *string \`json:"details"\` + UserId int64 \`json:"user_id"\` + } + + type PublicUserDetailsInsert struct { + Details *string \`json:"details"\` + UserId int64 \`json:"user_id"\` + } + + type PublicUserDetailsUpdate struct { + Details *string \`json:"details"\` + UserId *int64 \`json:"user_id"\` + } + + type PublicEmptySelect struct { + + } + + type PublicEmptyInsert struct { + + } + + type PublicEmptyUpdate struct { + + } + + type PublicTableWithOtherTablesRowTypeSelect struct { + Col1 interface{} \`json:"col1"\` + Col2 interface{} \`json:"col2"\` + } + + type PublicTableWithOtherTablesRowTypeInsert struct { + Col1 interface{} \`json:"col1"\` + Col2 interface{} \`json:"col2"\` + } + + type PublicTableWithOtherTablesRowTypeUpdate struct { + Col1 interface{} \`json:"col1"\` + Col2 interface{} \`json:"col2"\` + } + + type PublicTableWithPrimaryKeyOtherThanIdSelect struct { + Name *string \`json:"name"\` + OtherId int64 \`json:"other_id"\` + } + + type PublicTableWithPrimaryKeyOtherThanIdInsert struct { + Name *string \`json:"name"\` + OtherId *int64 \`json:"other_id"\` + } + + type PublicTableWithPrimaryKeyOtherThanIdUpdate struct { + Name *string \`json:"name"\` + OtherId *int64 \`json:"other_id"\` + } + + type PublicCategorySelect struct { + Id int32 \`json:"id"\` + Name string \`json:"name"\` + } + + type PublicCategoryInsert struct { + Id *int32 \`json:"id"\` + Name string \`json:"name"\` + } + + type PublicCategoryUpdate struct { + Id *int32 \`json:"id"\` + Name *string \`json:"name"\` + } + + type PublicMemesSelect struct { + Category *int32 \`json:"category"\` + CreatedAt string \`json:"created_at"\` + Id int32 \`json:"id"\` + Metadata interface{} \`json:"metadata"\` + Name string \`json:"name"\` + Status *string \`json:"status"\` + } + + type PublicMemesInsert struct { + Category *int32 \`json:"category"\` + CreatedAt string \`json:"created_at"\` + Id *int32 \`json:"id"\` + Metadata interface{} \`json:"metadata"\` + Name string \`json:"name"\` + Status *string \`json:"status"\` + } + + type PublicMemesUpdate struct { + Category *int32 \`json:"category"\` + CreatedAt *string \`json:"created_at"\` + Id *int32 \`json:"id"\` + Metadata interface{} \`json:"metadata"\` + Name *string \`json:"name"\` + Status *string \`json:"status"\` + } + + type PublicTodosViewSelect struct { + Details *string \`json:"details"\` + Id *int64 \`json:"id"\` + UserId *int64 \`json:"user-id"\` + } + + type PublicUsersViewSelect struct { + Id *int64 \`json:"id"\` + Name *string \`json:"name"\` + Status *string \`json:"status"\` + } + + type PublicAViewSelect struct { + Id *int64 \`json:"id"\` + } + + type PublicUsersViewWithMultipleRefsToUsersSelect struct { + InitialId *int64 \`json:"initial_id"\` + InitialName *string \`json:"initial_name"\` + SecondId *int64 \`json:"second_id"\` + SecondName *string \`json:"second_name"\` + } + + type PublicTodosMatviewSelect struct { + Details *string \`json:"details"\` + Id *int64 \`json:"id"\` + UserId *int64 \`json:"user-id"\` + } + + type PublicCompositeTypeWithArrayAttribute struct { + MyTextArray interface{} \`json:"my_text_array"\` + } + + type PublicCompositeTypeWithRecordAttribute struct { + Todo interface{} \`json:"todo"\` + }" `) }) From 45893b1a2347cbc54bb2869c373cc1e5ef953e16 Mon Sep 17 00:00:00 2001 From: avallete Date: Sun, 30 Mar 2025 17:58:42 +0200 Subject: [PATCH 3/5] chore: fix prettier --- test/lib/functions.ts | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/test/lib/functions.ts b/test/lib/functions.ts index 0d0140ef..4b35aaf6 100644 --- a/test/lib/functions.ts +++ b/test/lib/functions.ts @@ -4,7 +4,8 @@ import { pgMeta } from './utils' test('list', async () => { const res = await pgMeta.functions.list() expect(res.data?.find(({ name }) => name === 'add')).toMatchInlineSnapshot( - { id: expect.any(Number) }, ` + { id: expect.any(Number) }, + ` { "args": [ { @@ -46,7 +47,8 @@ test('list', async () => { "schema": "public", "security_definer": false, } - `) + ` + ) }) test('list set-returning function with single object limit', async () => { @@ -232,7 +234,8 @@ test('retrieve, create, update, delete', async () => { config_params: { search_path: 'hooks, auth', role: 'postgres' }, }) expect(res).toMatchInlineSnapshot( - { data: { id: expect.any(Number) } }, ` + { data: { id: expect.any(Number) } }, + ` { "data": { "args": [ @@ -282,10 +285,12 @@ test('retrieve, create, update, delete', async () => { }, "error": null, } - `) + ` + ) res = await pgMeta.functions.retrieve({ id: res.data!.id }) expect(res).toMatchInlineSnapshot( - { data: { id: expect.any(Number) } }, ` + { data: { id: expect.any(Number) } }, + ` { "data": { "args": [ @@ -335,14 +340,16 @@ test('retrieve, create, update, delete', async () => { }, "error": null, } - `) + ` + ) res = await pgMeta.functions.update(res.data!.id, { name: 'test_func_renamed', schema: 'test_schema', definition: 'select b - a', }) expect(res).toMatchInlineSnapshot( - { data: { id: expect.any(Number) } }, ` + { data: { id: expect.any(Number) } }, + ` { "data": { "args": [ @@ -392,10 +399,12 @@ test('retrieve, create, update, delete', async () => { }, "error": null, } - `) + ` + ) res = await pgMeta.functions.remove(res.data!.id) expect(res).toMatchInlineSnapshot( - { data: { id: expect.any(Number) } }, ` + { data: { id: expect.any(Number) } }, + ` { "data": { "args": [ @@ -445,7 +454,8 @@ test('retrieve, create, update, delete', async () => { }, "error": null, } - `) + ` + ) res = await pgMeta.functions.retrieve({ id: res.data!.id }) expect(res).toMatchObject({ data: null, From 175e31364ba215994bf4987a64eb6c7dcf679b0c Mon Sep 17 00:00:00 2001 From: avallete Date: Tue, 1 Apr 2025 20:48:23 +0200 Subject: [PATCH 4/5] fix(types): include views types --- src/lib/PostgresMetaTypes.ts | 2 +- src/server/templates/typescript.ts | 25 ++- test/db/00-init.sql | 60 +++++++ test/lib/functions.ts | 6 +- test/server/typegen.ts | 252 ++++++++++++++++++++++++++++- 5 files changed, 333 insertions(+), 12 deletions(-) diff --git a/src/lib/PostgresMetaTypes.ts b/src/lib/PostgresMetaTypes.ts index 35371d55..a3d73fd6 100644 --- a/src/lib/PostgresMetaTypes.ts +++ b/src/lib/PostgresMetaTypes.ts @@ -33,7 +33,7 @@ export default class PostgresMetaTypes { t.typrelid = 0 or ( select - c.relkind ${includeTableTypes ? `in ('c', 'r')` : `= 'c'`} + c.relkind ${includeTableTypes ? `in ('c', 'r', 'v')` : `= 'c'`} from pg_class c where diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index 2acf3cdb..2935b8ba 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -27,6 +27,14 @@ export const apply = async ({ const columnsByTableId = Object.fromEntries( [...tables, ...foreignTables, ...views, ...materializedViews].map((t) => [t.id, []]) ) + // group types by id for quicker lookup + const typesById = types.reduce( + (acc, type) => { + acc[type.id] = type + return acc + }, + {} as Record + ) columns .filter((c) => c.table_id in columnsByTableId) .sort(({ name: a }, { name: b }) => a.localeCompare(b)) @@ -45,6 +53,7 @@ export type Database = { const schemaViews = [...views, ...materializedViews] .filter((view) => view.schema === schema.name) .sort(({ name: a }, { name: b }) => a.localeCompare(b)) + const schemaFunctions = functions .filter((func) => { if (func.schema !== schema.name) { @@ -94,7 +103,7 @@ export type Database = { ...schemaFunctions .filter((fn) => fn.argument_types === table.name) .map((fn) => { - const type = types.find(({ id }) => id === fn.return_type_id) + const type = typesById[fn.return_type_id] let tsType = 'unknown' if (type) { tsType = pgTypeToTsType(type.name, { types, schemas, tables, views }) @@ -285,9 +294,8 @@ export type Database = { if (inArgs.length === 0) { return 'Record' } - const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { - const type = types.find(({ id }) => id === type_id) + const type = typesById[type_id] let tsType = 'unknown' if (type) { tsType = pgTypeToTsType(type.name, { types, schemas, tables, views }) @@ -351,10 +359,15 @@ export type Database = { ? `SetofOptions: { from: ${fns // if the function take a row as first parameter - .filter((fnd) => fnd.args.length === 1 && fnd.args[0].table_name) + .filter( + (fnd) => + fnd.args.length === 1 && + fnd.args[0].table_name && + typesById[fnd.args[0].type_id] + ) .map((fnd) => { - const arg_type = types.find((t) => t.id === fnd.args[0].type_id) - return JSON.stringify(arg_type?.format) + const tableType = typesById[fnd.args[0].type_id] + return JSON.stringify(tableType.format) }) .join(' | ')} to: ${JSON.stringify(fns[0].return_table_name)} diff --git a/test/db/00-init.sql b/test/db/00-init.sql index 40d9b7ba..856c70fb 100644 --- a/test/db/00-init.sql +++ b/test/db/00-init.sql @@ -55,6 +55,17 @@ $$ language plpgsql; CREATE VIEW todos_view AS SELECT * FROM public.todos; -- For testing typegen on view-to-view relationships create view users_view as select * from public.users; +-- Create a more complex view for testing +CREATE VIEW user_todos_summary_view AS +SELECT + u.id as user_id, + u.name as user_name, + u.status as user_status, + COUNT(t.id) as todo_count, + array_agg(t.details) FILTER (WHERE t.details IS NOT NULL) as todo_details +FROM public.users u +LEFT JOIN public.todos t ON t."user-id" = u.id +GROUP BY u.id, u.name, u.status; create materialized view todos_matview as select * from public.todos; @@ -210,3 +221,52 @@ LANGUAGE SQL STABLE AS $$ SELECT id FROM public.users; $$; + + +-- Function returning view using scalar as input +CREATE OR REPLACE FUNCTION public.get_single_user_summary_from_view(search_user_id bigint) +RETURNS SETOF user_todos_summary_view +LANGUAGE SQL STABLE +ROWS 1 +AS $$ + SELECT * FROM user_todos_summary_view WHERE user_id = search_user_id; +$$; +-- Function returning view using table row as input +CREATE OR REPLACE FUNCTION public.get_single_user_summary_from_view(user_row users) +RETURNS SETOF user_todos_summary_view +LANGUAGE SQL STABLE +ROWS 1 +AS $$ + SELECT * FROM user_todos_summary_view WHERE user_id = user_row.id; +$$; +-- Function returning view using another view row as input +CREATE OR REPLACE FUNCTION public.get_single_user_summary_from_view(userview_row users_view) +RETURNS SETOF user_todos_summary_view +LANGUAGE SQL STABLE +ROWS 1 +AS $$ + SELECT * FROM user_todos_summary_view WHERE user_id = userview_row.id; +$$; + + +-- Function returning view using scalar as input +CREATE OR REPLACE FUNCTION public.get_todos_from_user(search_user_id bigint) +RETURNS SETOF todos +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM todos WHERE "user-id" = search_user_id; +$$; +-- Function returning view using table row as input +CREATE OR REPLACE FUNCTION public.get_todos_from_user(user_row users) +RETURNS SETOF todos +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM todos WHERE "user-id" = user_row.id; +$$; +-- Function returning view using another view row as input +CREATE OR REPLACE FUNCTION public.get_todos_from_user(userview_row users_view) +RETURNS SETOF todos +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM todos WHERE "user-id" = userview_row.id; +$$; \ No newline at end of file diff --git a/test/lib/functions.ts b/test/lib/functions.ts index 4b35aaf6..b6d6cb84 100644 --- a/test/lib/functions.ts +++ b/test/lib/functions.ts @@ -80,7 +80,7 @@ test('list set-returning function with single object limit', async () => { "definition": " SELECT * FROM public.users_audit WHERE user_id = user_row.id; ", - "id": 16498, + "id": 16502, "identity_argument_types": "user_row users", "is_set_returning_function": true, "language": "sql", @@ -126,7 +126,7 @@ test('list set-returning function with multiples definitions', async () => { "definition": " SELECT * FROM public.todos WHERE "user-id" = user_row.id; ", - "id": 16499, + "id": 16503, "identity_argument_types": "user_row users", "is_set_returning_function": true, "language": "sql", @@ -164,7 +164,7 @@ test('list set-returning function with multiples definitions', async () => { "definition": " SELECT * FROM public.todos WHERE "user-id" = todo_row."user-id"; ", - "id": 16500, + "id": 16504, "identity_argument_types": "todo_row todos", "is_set_returning_function": true, "language": "sql", diff --git a/test/server/typegen.ts b/test/server/typegen.ts index 3eb0fc19..0382fcc6 100644 --- a/test/server/typegen.ts +++ b/test/server/typegen.ts @@ -147,6 +147,12 @@ test('typegen: typescript', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "todos_user-id_fkey" columns: ["user-id"] @@ -193,6 +199,12 @@ test('typegen: typescript', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "user_details_user_id_fkey" + columns: ["user_id"] + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "user_details_user_id_fkey" columns: ["user_id"] @@ -285,6 +297,12 @@ test('typegen: typescript', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "todos_user-id_fkey" columns: ["user-id"] @@ -334,6 +352,12 @@ test('typegen: typescript', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "todos_user-id_fkey" columns: ["user-id"] @@ -360,6 +384,16 @@ test('typegen: typescript', async () => { }, ] } + user_todos_summary_view: { + Row: { + todo_count: number | null + todo_details: string[] | null + user_id: number | null + user_name: string | null + user_status: Database["public"]["Enums"]["user_status"] | null + } + Relationships: [] + } users_view: { Row: { id: number | null @@ -436,6 +470,40 @@ test('typegen: typescript', async () => { Args: Record Returns: Database["public"]["CompositeTypes"]["composite_type_with_array_attribute"][] } + get_single_user_summary_from_view: { + Args: + | { search_user_id: number } + | { user_row: Database["public"]["Tables"]["users"]["Row"] } + | { userview_row: Database["public"]["Views"]["users_view"]["Row"] } + Returns: { + todo_count: number | null + todo_details: string[] | null + user_id: number | null + user_name: string | null + user_status: Database["public"]["Enums"]["user_status"] | null + } + SetofOptions: { + from: "users" | "users_view" + to: "user_todos_summary_view" + isOneToOne: true + } + } + get_todos_from_user: { + Args: + | { search_user_id: number } + | { user_row: Database["public"]["Tables"]["users"]["Row"] } + | { userview_row: Database["public"]["Views"]["users_view"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users" | "users_view" + to: "todos" + isOneToOne: false + } + } get_todos_setof_rows: { Args: | { user_row: Database["public"]["Tables"]["users"]["Row"] } @@ -783,6 +851,13 @@ test('typegen w/ one-to-one relationships', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "todos_user-id_fkey" columns: ["user-id"] @@ -834,6 +909,13 @@ test('typegen w/ one-to-one relationships', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "user_details_user_id_fkey" + columns: ["user_id"] + isOneToOne: true + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "user_details_user_id_fkey" columns: ["user_id"] @@ -931,6 +1013,13 @@ test('typegen w/ one-to-one relationships', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "todos_user-id_fkey" columns: ["user-id"] @@ -985,6 +1074,13 @@ test('typegen w/ one-to-one relationships', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "todos_user-id_fkey" columns: ["user-id"] @@ -1015,6 +1111,16 @@ test('typegen w/ one-to-one relationships', async () => { }, ] } + user_todos_summary_view: { + Row: { + todo_count: number | null + todo_details: string[] | null + user_id: number | null + user_name: string | null + user_status: Database["public"]["Enums"]["user_status"] | null + } + Relationships: [] + } users_view: { Row: { id: number | null @@ -1091,6 +1197,40 @@ test('typegen w/ one-to-one relationships', async () => { Args: Record Returns: Database["public"]["CompositeTypes"]["composite_type_with_array_attribute"][] } + get_single_user_summary_from_view: { + Args: + | { search_user_id: number } + | { user_row: Database["public"]["Tables"]["users"]["Row"] } + | { userview_row: Database["public"]["Views"]["users_view"]["Row"] } + Returns: { + todo_count: number | null + todo_details: string[] | null + user_id: number | null + user_name: string | null + user_status: Database["public"]["Enums"]["user_status"] | null + } + SetofOptions: { + from: "users" | "users_view" + to: "user_todos_summary_view" + isOneToOne: true + } + } + get_todos_from_user: { + Args: + | { search_user_id: number } + | { user_row: Database["public"]["Tables"]["users"]["Row"] } + | { userview_row: Database["public"]["Views"]["users_view"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users" | "users_view" + to: "todos" + isOneToOne: false + } + } get_todos_setof_rows: { Args: | { user_row: Database["public"]["Tables"]["users"]["Row"] } @@ -1438,6 +1578,13 @@ test('typegen: typescript w/ one-to-one relationships', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "todos_user-id_fkey" columns: ["user-id"] @@ -1489,6 +1636,13 @@ test('typegen: typescript w/ one-to-one relationships', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "user_details_user_id_fkey" + columns: ["user_id"] + isOneToOne: true + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "user_details_user_id_fkey" columns: ["user_id"] @@ -1586,6 +1740,13 @@ test('typegen: typescript w/ one-to-one relationships', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "todos_user-id_fkey" columns: ["user-id"] @@ -1640,6 +1801,13 @@ test('typegen: typescript w/ one-to-one relationships', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "todos_user-id_fkey" columns: ["user-id"] @@ -1670,6 +1838,16 @@ test('typegen: typescript w/ one-to-one relationships', async () => { }, ] } + user_todos_summary_view: { + Row: { + todo_count: number | null + todo_details: string[] | null + user_id: number | null + user_name: string | null + user_status: Database["public"]["Enums"]["user_status"] | null + } + Relationships: [] + } users_view: { Row: { id: number | null @@ -1746,6 +1924,40 @@ test('typegen: typescript w/ one-to-one relationships', async () => { Args: Record Returns: Database["public"]["CompositeTypes"]["composite_type_with_array_attribute"][] } + get_single_user_summary_from_view: { + Args: + | { search_user_id: number } + | { user_row: Database["public"]["Tables"]["users"]["Row"] } + | { userview_row: Database["public"]["Views"]["users_view"]["Row"] } + Returns: { + todo_count: number | null + todo_details: string[] | null + user_id: number | null + user_name: string | null + user_status: Database["public"]["Enums"]["user_status"] | null + } + SetofOptions: { + from: "users" | "users_view" + to: "user_todos_summary_view" + isOneToOne: true + } + } + get_todos_from_user: { + Args: + | { search_user_id: number } + | { user_row: Database["public"]["Tables"]["users"]["Row"] } + | { userview_row: Database["public"]["Views"]["users_view"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users" | "users_view" + to: "todos" + isOneToOne: false + } + } get_todos_setof_rows: { Args: | { user_row: Database["public"]["Tables"]["users"]["Row"] } @@ -2102,6 +2314,10 @@ test('typegen: go', async () => { Status *string \`json:"status"\` } + type PublicAViewSelect struct { + Id *int64 \`json:"id"\` + } + type PublicTodosViewSelect struct { Details *string \`json:"details"\` Id *int64 \`json:"id"\` @@ -2114,8 +2330,12 @@ test('typegen: go', async () => { Status *string \`json:"status"\` } - type PublicAViewSelect struct { - Id *int64 \`json:"id"\` + type PublicUserTodosSummaryViewSelect struct { + TodoCount *int64 \`json:"todo_count"\` + TodoDetails []*string \`json:"todo_details"\` + UserId *int64 \`json:"user_id"\` + UserName *string \`json:"user_name"\` + UserStatus *string \`json:"user_status"\` } type PublicUsersViewWithMultipleRefsToUsersSelect struct { @@ -2462,6 +2682,20 @@ test('typegen: swift', async () => { case userId = "user-id" } } + internal struct UserTodosSummaryViewSelect: Codable, Hashable, Sendable { + internal let todoCount: Int64? + internal let todoDetails: [String]? + internal let userId: Int64? + internal let userName: String? + internal let userStatus: UserStatus? + internal enum CodingKeys: String, CodingKey { + case todoCount = "todo_count" + case todoDetails = "todo_details" + case userId = "user_id" + case userName = "user_name" + case userStatus = "user_status" + } + } internal struct UsersViewSelect: Codable, Hashable, Sendable { internal let id: Int64? internal let name: String? @@ -2825,6 +3059,20 @@ test('typegen: swift w/ public access control', async () => { case userId = "user-id" } } + public struct UserTodosSummaryViewSelect: Codable, Hashable, Sendable { + public let todoCount: Int64? + public let todoDetails: [String]? + public let userId: Int64? + public let userName: String? + public let userStatus: UserStatus? + public enum CodingKeys: String, CodingKey { + case todoCount = "todo_count" + case todoDetails = "todo_details" + case userId = "user_id" + case userName = "user_name" + case userStatus = "user_status" + } + } public struct UsersViewSelect: Codable, Hashable, Sendable { public let id: Int64? public let name: String? From f05813e7eafe3c2f622d3a3292b541b93cb38c1f Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 3 Apr 2025 00:15:30 +0200 Subject: [PATCH 5/5] fix: rpc with empty args returning setof tables --- src/lib/sql/functions.sql | 9 ++++++-- src/server/templates/typescript.ts | 22 ++++++++++---------- test/server/typegen.ts | 33 ++++++++++++++++++++++-------- 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/lib/sql/functions.sql b/src/lib/sql/functions.sql index c921ab09..8e50515a 100644 --- a/src/lib/sql/functions.sql +++ b/src/lib/sql/functions.sql @@ -45,8 +45,13 @@ select nullif(rt.typrelid::int8, 0) as return_type_relation_id, f.proretset as is_set_returning_function, case - when f.proretset and rt.typrelid != 0 then true - else false + when f.proretset and rt.typrelid != 0 and exists ( + select 1 from pg_class c + where c.oid = rt.typrelid + -- exclude custom types relation from what is considered a set of table + and c.relkind in ('r', 'p', 'v', 'm', 'f') + ) then true + else false end as returns_set_of_table, case when f.proretset and rt.typrelid != 0 then diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index 2935b8ba..dda2df22 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -354,21 +354,21 @@ export type Database = { })()}${fns[0].is_set_returning_function && fns[0].returns_multiple_rows ? '[]' : ''} ${ // if the function return a set of a table and some definition take in parameter another table - fns[0].returns_set_of_table && - fns.some((fnd) => fnd.args.length === 1 && fnd.args[0].table_name) + fns[0].returns_set_of_table ? `SetofOptions: { from: ${fns - // if the function take a row as first parameter - .filter( - (fnd) => - fnd.args.length === 1 && - fnd.args[0].table_name && - typesById[fnd.args[0].type_id] - ) .map((fnd) => { - const tableType = typesById[fnd.args[0].type_id] - return JSON.stringify(tableType.format) + if (fnd.args.length > 0 && fnd.args[0].table_name) { + const tableType = typesById[fnd.args[0].type_id] + return JSON.stringify(tableType.format) + } else { + // If the function can be called with scalars or without any arguments, then add a * matching everything + return '"*"' + } }) + // Dedup before join + .filter((value, index, self) => self.indexOf(value) === index) + .toSorted() .join(' | ')} to: ${JSON.stringify(fns[0].return_table_name)} isOneToOne: ${fns[0].returns_multiple_rows ? false : true} diff --git a/test/server/typegen.ts b/test/server/typegen.ts index 0382fcc6..f76a0ed1 100644 --- a/test/server/typegen.ts +++ b/test/server/typegen.ts @@ -458,6 +458,11 @@ test('typegen: typescript', async () => { name: string | null status: Database["public"]["Enums"]["user_status"] | null }[] + SetofOptions: { + from: "*" + to: "users" + isOneToOne: false + } } function_returning_table: { Args: Record @@ -483,7 +488,7 @@ test('typegen: typescript', async () => { user_status: Database["public"]["Enums"]["user_status"] | null } SetofOptions: { - from: "users" | "users_view" + from: "*" | "users" | "users_view" to: "user_todos_summary_view" isOneToOne: true } @@ -499,7 +504,7 @@ test('typegen: typescript', async () => { "user-id": number }[] SetofOptions: { - from: "users" | "users_view" + from: "*" | "users" | "users_view" to: "todos" isOneToOne: false } @@ -514,7 +519,7 @@ test('typegen: typescript', async () => { "user-id": number }[] SetofOptions: { - from: "users" | "todos" + from: "todos" | "users" to: "todos" isOneToOne: false } @@ -1185,6 +1190,11 @@ test('typegen w/ one-to-one relationships', async () => { name: string | null status: Database["public"]["Enums"]["user_status"] | null }[] + SetofOptions: { + from: "*" + to: "users" + isOneToOne: false + } } function_returning_table: { Args: Record @@ -1210,7 +1220,7 @@ test('typegen w/ one-to-one relationships', async () => { user_status: Database["public"]["Enums"]["user_status"] | null } SetofOptions: { - from: "users" | "users_view" + from: "*" | "users" | "users_view" to: "user_todos_summary_view" isOneToOne: true } @@ -1226,7 +1236,7 @@ test('typegen w/ one-to-one relationships', async () => { "user-id": number }[] SetofOptions: { - from: "users" | "users_view" + from: "*" | "users" | "users_view" to: "todos" isOneToOne: false } @@ -1241,7 +1251,7 @@ test('typegen w/ one-to-one relationships', async () => { "user-id": number }[] SetofOptions: { - from: "users" | "todos" + from: "todos" | "users" to: "todos" isOneToOne: false } @@ -1912,6 +1922,11 @@ test('typegen: typescript w/ one-to-one relationships', async () => { name: string | null status: Database["public"]["Enums"]["user_status"] | null }[] + SetofOptions: { + from: "*" + to: "users" + isOneToOne: false + } } function_returning_table: { Args: Record @@ -1937,7 +1952,7 @@ test('typegen: typescript w/ one-to-one relationships', async () => { user_status: Database["public"]["Enums"]["user_status"] | null } SetofOptions: { - from: "users" | "users_view" + from: "*" | "users" | "users_view" to: "user_todos_summary_view" isOneToOne: true } @@ -1953,7 +1968,7 @@ test('typegen: typescript w/ one-to-one relationships', async () => { "user-id": number }[] SetofOptions: { - from: "users" | "users_view" + from: "*" | "users" | "users_view" to: "todos" isOneToOne: false } @@ -1968,7 +1983,7 @@ test('typegen: typescript w/ one-to-one relationships', async () => { "user-id": number }[] SetofOptions: { - from: "users" | "todos" + from: "todos" | "users" to: "todos" isOneToOne: false }