diff --git a/graphql/server/src/middleware/graphile.ts b/graphql/server/src/middleware/graphile.ts index 477e61288..21aa35e5c 100644 --- a/graphql/server/src/middleware/graphile.ts +++ b/graphql/server/src/middleware/graphile.ts @@ -249,6 +249,12 @@ const buildPreset = ( context['jwt.claims.device_token'] = req.deviceToken; } + // Export category exclusion — set 'platform_mirror' as default so + // platform-integrated mirror triggers are always excluded from + // db_migrate.sql_actions queries via the export_category_filter RLS + // policy. The X-Exclude-Categories header can override if needed. + context['export.exclude_categories'] = req.get('X-Exclude-Categories') || 'platform_mirror'; + if (req.token?.user_id) { const pgSettings: Record = { role: roleName, diff --git a/pgpm/export/src/export-graphql.ts b/pgpm/export/src/export-graphql.ts index 5f5853ec0..7591e1898 100644 --- a/pgpm/export/src/export-graphql.ts +++ b/pgpm/export/src/export-graphql.ts @@ -25,7 +25,8 @@ import { installMissingModules, makeReplacer, preparePackage, - normalizeOutdir + normalizeOutdir, + filterPlatformLeakage } from './export-utils'; // ============================================================================= @@ -204,8 +205,9 @@ export const exportGraphQL = async ({ await installMissingModules(dbModuleDir, dbMissingResult.missingModules); } - writePgpmPlan(sqlActionRows as unknown as PgpmRow[], opts); - writePgpmFiles(sqlActionRows as unknown as PgpmRow[], opts); + const filteredRows = filterPlatformLeakage(sqlActionRows as unknown as any[], schema_names); + writePgpmPlan(filteredRows as unknown as PgpmRow[], opts); + writePgpmFiles(filteredRows as unknown as PgpmRow[], opts); } else { console.log('No sql_actions found. Skipping database module export.'); } diff --git a/pgpm/export/src/export-migrations.ts b/pgpm/export/src/export-migrations.ts index 9edf0fc62..645459e37 100644 --- a/pgpm/export/src/export-migrations.ts +++ b/pgpm/export/src/export-migrations.ts @@ -14,7 +14,8 @@ import { installMissingModules, makeReplacer, preparePackage, - normalizeOutdir + normalizeOutdir, + filterPlatformLeakage } from './export-utils'; interface ExportMigrationsToDiskOptions { @@ -148,6 +149,14 @@ const exportMigrationsToDisk = async ({ [databaseId] ); + // ========================================================================= + // Platform leakage filter — strip platform-integrated actions and + // cross-package requires from the exported package. See + // filterPlatformLeakage() in export-utils.ts for details. + // ========================================================================= + const filteredRows = filterPlatformLeakage(results?.rows ?? [], schema_names); + + const opts: SqlWriteOptions = { name, replacer, @@ -158,7 +167,7 @@ const exportMigrationsToDisk = async ({ // Build description for the database extension package const dbExtensionDesc = extensionDesc || `${name} database schema for ${databaseName}`; - if (results?.rows?.length > 0) { + if (filteredRows.length > 0) { // Detect missing modules at workspace level and prompt user const dbMissingResult = await detectMissingModules(project, [...DB_REQUIRED_EXTENSIONS], prompter, argv); @@ -180,8 +189,8 @@ const exportMigrationsToDisk = async ({ await installMissingModules(dbModuleDir, dbMissingResult.missingModules); } - writePgpmPlan(results.rows, opts); - writePgpmFiles(results.rows, opts); + writePgpmPlan(filteredRows, opts); + writePgpmFiles(filteredRows, opts); } else { console.log('No sql_actions found — skipping database module. Meta/service module will still be exported.'); } diff --git a/pgpm/export/src/export-utils.ts b/pgpm/export/src/export-utils.ts index 847a1421a..ba2fdc38e 100644 --- a/pgpm/export/src/export-utils.ts +++ b/pgpm/export/src/export-utils.ts @@ -866,3 +866,34 @@ export const preparePackage = async ({ export const normalizeOutdir = (outdir: string): string => { return outdir.endsWith(path.sep) ? outdir : outdir + path.sep; }; + +// ============================================================================= +// Platform leakage filter +// ============================================================================= + +/** + * Filters cross-package deps from sql_actions rows before writing them + * as a per-tenant package. Strips deps that reference schemas not owned + * by this database so requires directives only reference tenant-owned + * schemas. + * + * Mirror trigger exclusion is handled upstream via the category mechanism: + * create_trigger_function(v_category := 'platform_mirror') → insert_action + * → depase_ast trigger → db_migrate.sql_actions.category = 'platform_mirror'. + * The server sets export.exclude_categories = 'platform_mirror' (via the + * X-Exclude-Categories header → pgSettings GUC) which activates the + * export_category_filter RLS policy, making those rows invisible at the + * SQL layer before the export code sees them. + */ +export const filterPlatformLeakage = (rows: any[], schema_names: string[]): any[] => { + const ownedPrefixes = schema_names.map(s => `schemas/${s}/`); + + return rows.map((row: any) => { + if (Array.isArray(row.deps)) { + row.deps = row.deps.filter((dep: string) => + ownedPrefixes.some(p => dep.includes(p)) + ); + } + return row; + }); +}; diff --git a/pgpm/export/src/index.ts b/pgpm/export/src/index.ts index b357a3d2a..d70395972 100644 --- a/pgpm/export/src/index.ts +++ b/pgpm/export/src/index.ts @@ -16,7 +16,8 @@ export { preparePackage, normalizeOutdir, detectMissingModules, - installMissingModules + installMissingModules, + filterPlatformLeakage } from './export-utils'; export type { FieldType,