From 87d3e23735cd43953cc48f3fae27616c929d42fe Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Mon, 11 May 2026 09:18:00 +0000 Subject: [PATCH 1/2] fix: fall back to legacy rls_module when rls_settings has NULL function names When metaschema_public.function rows are missing (e.g. the insert_rls_module trigger was skipped during migration), the RLS_SETTINGS_SQL LEFT JOINs resolve NULL for function names. toRlsModuleFromSettings was returning an RlsModule with null fields, preventing fallback to the working legacy api_modules lookup. Add a guard to return undefined when critical fields (authenticate, authenticate_schema) are null, allowing queryRlsModule to fall back to queryRlsModuleLegacy. --- graphql/server/src/middleware/api.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/graphql/server/src/middleware/api.ts b/graphql/server/src/middleware/api.ts index b21134165..42eea6ab1 100644 --- a/graphql/server/src/middleware/api.ts +++ b/graphql/server/src/middleware/api.ts @@ -424,6 +424,10 @@ const toRlsModule = (row: RlsModuleRow | null): RlsModule | undefined => { const toRlsModuleFromSettings = (row: RlsModuleData | null): RlsModule | undefined => { if (!row) return undefined; + // If metaschema_public.function rows are missing (e.g. trigger was skipped + // during migration), the LEFT JOINs resolve NULL. Return undefined so the + // caller falls back to the legacy api_modules lookup. + if (!row.authenticate || !row.authenticate_schema) return undefined; return { authenticate: row.authenticate, authenticateStrict: row.authenticate_strict, From 229e375c5a92ad328d13b467493255d2048f1d52 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Mon, 11 May 2026 09:32:53 +0000 Subject: [PATCH 2/2] fix: export metaschema_public.function rows in services migration The export pipeline was not including metaschema_public.function in the constructive-services migration output. This table is referenced by rls_settings (6 FK columns) and pubkey_settings (4 FK columns). Since all constructive-services migrations deploy with session_replication_role=replica (disabling triggers), the insert_rls_module trigger never fires to create these function rows. The result is dangling UUID references in rls_settings, causing the GraphQL server to resolve NULL function names and skip authentication. Changes: - Add function to META_TABLE_CONFIG with schema/fields definition - Add function to META_TABLE_ORDER (after schema, before table) - Add queryAndParse for metaschema_public.function in export-meta.ts - Add queryAndParse for function in export-graphql-meta.ts After this fix, running generate:constructive will produce a new function.sql migration file in constructive-services containing the INSERT statements for all registered functions. --- pgpm/export/src/export-graphql-meta.ts | 1 + pgpm/export/src/export-meta.ts | 1 + pgpm/export/src/export-utils.ts | 11 +++++++++++ 3 files changed, 13 insertions(+) diff --git a/pgpm/export/src/export-graphql-meta.ts b/pgpm/export/src/export-graphql-meta.ts index 775f839d4..cef0239f8 100644 --- a/pgpm/export/src/export-graphql-meta.ts +++ b/pgpm/export/src/export-graphql-meta.ts @@ -124,6 +124,7 @@ export const exportGraphQLMeta = async ({ queryAndParse('database'), queryAndParse('database_extension'), queryAndParse('schema'), + queryAndParse('function'), queryAndParse('table'), queryAndParse('field'), queryAndParse('policy'), diff --git a/pgpm/export/src/export-meta.ts b/pgpm/export/src/export-meta.ts index 3949413f6..9e4203b5b 100644 --- a/pgpm/export/src/export-meta.ts +++ b/pgpm/export/src/export-meta.ts @@ -132,6 +132,7 @@ export const exportMeta = async ({ opts, dbname, database_id }: ExportMetaParams await queryAndParse('database', `SELECT * FROM metaschema_public.database WHERE id = $1 ORDER BY id`); await queryAndParse('database_extension', `SELECT * FROM metaschema_public.database_extension WHERE database_id = $1 ORDER BY id`); await queryAndParse('schema', `SELECT * FROM metaschema_public.schema WHERE database_id = $1 ORDER BY id`); + await queryAndParse('function', `SELECT * FROM metaschema_public.function WHERE database_id = $1 ORDER BY id`); await queryAndParse('table', `SELECT * FROM metaschema_public.table WHERE database_id = $1 ORDER BY id`); await queryAndParse('field', `SELECT * FROM metaschema_public.field WHERE database_id = $1 ORDER BY id`); await queryAndParse('policy', `SELECT * FROM metaschema_public.policy WHERE database_id = $1 ORDER BY id`); diff --git a/pgpm/export/src/export-utils.ts b/pgpm/export/src/export-utils.ts index 49b680aa1..58cc66b7f 100644 --- a/pgpm/export/src/export-utils.ts +++ b/pgpm/export/src/export-utils.ts @@ -124,6 +124,7 @@ SET session_replication_role TO DEFAULT;`; export const META_TABLE_ORDER = [ 'database', 'schema', + 'function', 'table', 'field', 'policy', @@ -243,6 +244,16 @@ export const META_TABLE_CONFIG: Record = { is_public: 'boolean' } }, + function: { + schema: 'metaschema_public', + table: 'function', + fields: { + id: 'uuid', + database_id: 'uuid', + schema_id: 'uuid', + name: 'text' + } + }, table: { schema: 'metaschema_public', table: 'table',