From 5c4cd30c08d541f3e9b0c22d31cf39a977599615 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sat, 30 May 2026 04:53:33 +0000 Subject: [PATCH 1/2] fix: update storage plugins to use scope instead of membership_type The constructive-db scope+prefix unification replaced membership_type int with scope text on all module config tables. Update the graphile plugins that query storage_module to use sm.scope instead of sm.membership_type. --- .../src/plugin.ts | 12 ++++++------ .../src/storage-module-cache.ts | 16 ++++++++-------- .../graphile-presigned-url-plugin/src/types.ts | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/graphile/graphile-bucket-provisioner-plugin/src/plugin.ts b/graphile/graphile-bucket-provisioner-plugin/src/plugin.ts index 3e96dd1e0b..74eeedff40 100644 --- a/graphile/graphile-bucket-provisioner-plugin/src/plugin.ts +++ b/graphile/graphile-bucket-provisioner-plugin/src/plugin.ts @@ -51,12 +51,12 @@ const log = new Logger('graphile-bucket-provisioner:plugin'); // --- Storage module queries --- /** - * Resolve the app-level storage module (membership_type IS NULL). + * Resolve the app-level storage module (scope = 'app'). */ const APP_STORAGE_MODULE_QUERY = ` SELECT sm.id, - sm.membership_type, + sm.scope, sm.entity_table_id, bs.schema_name AS buckets_schema, bt.name AS buckets_table, @@ -68,7 +68,7 @@ const APP_STORAGE_MODULE_QUERY = ` JOIN metaschema_public.table bt ON bt.id = sm.buckets_table_id JOIN metaschema_public.schema bs ON bs.id = bt.schema_id WHERE sm.database_id = $1 - AND sm.membership_type IS NULL + AND sm.scope = 'app' LIMIT 1 `; @@ -78,7 +78,7 @@ const APP_STORAGE_MODULE_QUERY = ` const ALL_STORAGE_MODULES_QUERY = ` SELECT sm.id, - sm.membership_type, + sm.scope, sm.entity_table_id, bs.schema_name AS buckets_schema, bt.name AS buckets_table, @@ -98,7 +98,7 @@ const ALL_STORAGE_MODULES_QUERY = ` interface StorageModuleRow { id: string; - membership_type: number | null; + scope: string; entity_table_id: string | null; buckets_schema: string; buckets_table: string; @@ -426,7 +426,7 @@ export function createBucketProvisionerPlugin( } // Look up the bucket row (RLS enforced via pgSettings) - const hasOwner = ownerId && storageModule.membership_type !== null; + const hasOwner = ownerId && storageModule.scope !== 'app'; const bucketsTable = QuoteUtils.quoteQualifiedIdentifier(storageModule.buckets_schema, storageModule.buckets_table); const bucketResult = await pgClient.query( hasOwner diff --git a/graphile/graphile-presigned-url-plugin/src/storage-module-cache.ts b/graphile/graphile-presigned-url-plugin/src/storage-module-cache.ts index 3aee201df8..86f625249c 100644 --- a/graphile/graphile-presigned-url-plugin/src/storage-module-cache.ts +++ b/graphile/graphile-presigned-url-plugin/src/storage-module-cache.ts @@ -37,14 +37,14 @@ const storageModuleCache = new LRUCache({ * SQL query to resolve the app-level storage module config for a database. * * Joins storage_module → table → schema to get fully-qualified table names. - * Filters to app-level (membership_type IS NULL) by default. + * Filters to app-level (scope = 'app') by default. * - * Requires the multi-scope schema (membership_type column on storage_module). + * Requires the multi-scope schema (scope column on storage_module). */ const APP_STORAGE_MODULE_QUERY = ` SELECT sm.id, - sm.membership_type, + sm.scope, sm.entity_table_id, bs.schema_name AS buckets_schema, bt.name AS buckets_table, @@ -70,7 +70,7 @@ const APP_STORAGE_MODULE_QUERY = ` JOIN metaschema_public.table ft ON ft.id = sm.files_table_id JOIN metaschema_public.schema fs ON fs.id = ft.schema_id WHERE sm.database_id = $1 - AND sm.membership_type IS NULL + AND sm.scope = 'app' LIMIT 1 `; @@ -83,7 +83,7 @@ const APP_STORAGE_MODULE_QUERY = ` const ALL_STORAGE_MODULES_QUERY = ` SELECT sm.id, - sm.membership_type, + sm.scope, sm.entity_table_id, bs.schema_name AS buckets_schema, bt.name AS buckets_table, @@ -115,7 +115,7 @@ const ALL_STORAGE_MODULES_QUERY = ` interface StorageModuleRow { id: string; - membership_type: number | null; + scope: string; entity_table_id: string | null; buckets_schema: string; buckets_table: string; @@ -149,7 +149,7 @@ function buildConfig(row: StorageModuleRow): StorageModuleConfig { schemaName: row.buckets_schema, bucketsTableName: row.buckets_table, filesTableName: row.files_table, - membershipType: row.membership_type, + membershipType: row.scope === 'app' ? null : row.scope, entityTableId: row.entity_table_id, entityQualifiedName: row.entity_schema && row.entity_table ? QuoteUtils.quoteQualifiedIdentifier(row.entity_schema, row.entity_table) @@ -173,7 +173,7 @@ function buildConfig(row: StorageModuleRow): StorageModuleConfig { * Resolve the app-level storage module config for a database, using the LRU cache. * * This is the default path when no ownerId is provided. It returns the - * storage module with membership_type IS NULL (app-level / database-wide). + * storage module with scope = 'app' (app-level / database-wide). * * @param pgClient - A pg client from the Graphile context (withPgClient or pgClient) * @param databaseId - The metaschema database UUID diff --git a/graphile/graphile-presigned-url-plugin/src/types.ts b/graphile/graphile-presigned-url-plugin/src/types.ts index fc20759e8a..0a5b520ca6 100644 --- a/graphile/graphile-presigned-url-plugin/src/types.ts +++ b/graphile/graphile-presigned-url-plugin/src/types.ts @@ -33,8 +33,8 @@ export interface StorageModuleConfig { // --- Scope identity --- - /** Membership type (NULL for app-level, non-NULL for entity-scoped) */ - membershipType: number | null; + /** Scope (null for app-level, scope string for entity-scoped) */ + membershipType: string | null; /** Entity table ID for entity-scoped storage (NULL for app-level) */ entityTableId: string | null; /** Qualified entity table name for ownerId lookups (NULL for app-level) */ From f7ffb26dc1924514a59f7f41a4f56d64556d0dd6 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sat, 30 May 2026 07:23:45 +0000 Subject: [PATCH 2/2] fix: rename membershipType to scope in storage plugin interfaces --- .../src/plugin.ts | 2 +- .../src/storage-module-cache.ts | 23 ++++++++----------- .../src/types.ts | 4 ++-- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/graphile/graphile-presigned-url-plugin/src/plugin.ts b/graphile/graphile-presigned-url-plugin/src/plugin.ts index f9a3de1b45..9623a5395f 100644 --- a/graphile/graphile-presigned-url-plugin/src/plugin.ts +++ b/graphile/graphile-presigned-url-plugin/src/plugin.ts @@ -692,7 +692,7 @@ async function processSingleFile( const derivedPath = isCustomKey && storageConfig.hasPathShares ? derivePathFromKey(s3Key) : null; // Create file record - const hasOwnerColumn = storageConfig.membershipType !== null; + const hasOwnerColumn = storageConfig.scope !== 'app'; const columns = ['bucket_id', 'key', 'content_hash', 'mime_type', 'size', 'filename', 'is_public']; const values: any[] = [bucket.id, s3Key, contentHash, contentType, size, filename || null, bucket.is_public]; diff --git a/graphile/graphile-presigned-url-plugin/src/storage-module-cache.ts b/graphile/graphile-presigned-url-plugin/src/storage-module-cache.ts index 86f625249c..79a55048ff 100644 --- a/graphile/graphile-presigned-url-plugin/src/storage-module-cache.ts +++ b/graphile/graphile-presigned-url-plugin/src/storage-module-cache.ts @@ -149,7 +149,7 @@ function buildConfig(row: StorageModuleRow): StorageModuleConfig { schemaName: row.buckets_schema, bucketsTableName: row.buckets_table, filesTableName: row.files_table, - membershipType: row.scope === 'app' ? null : row.scope, + scope: row.scope, entityTableId: row.entity_table_id, entityQualifiedName: row.entity_schema && row.entity_table ? QuoteUtils.quoteQualifiedIdentifier(row.entity_schema, row.entity_table) @@ -249,11 +249,9 @@ export async function getStorageModuleConfigForOwner( const result = await pgClient.query({ text: ALL_STORAGE_MODULES_QUERY, values: [databaseId] }); allConfigs = (result.rows as StorageModuleRow[]).map(buildConfig); - // Cache each individual config by its membership type + // Cache each individual config by its scope for (const config of allConfigs) { - const key = config.membershipType === null - ? `storage:${databaseId}:app` - : `storage:${databaseId}:mt:${config.membershipType}`; + const key = `storage:${databaseId}:scope:${config.scope}`; storageModuleCache.set(key, config); } } @@ -271,7 +269,7 @@ export async function getStorageModuleConfigForOwner( storageModuleCache.set(ownerCacheKey, mod); log.debug( `Resolved ownerId ${ownerId} to storage module ${mod.id} ` + - `(membershipType=${mod.membershipType}, table=${mod.bucketsQualifiedName})`, + `(scope=${mod.scope}, table=${mod.bucketsQualifiedName})`, ); return mod; } @@ -341,11 +339,9 @@ export async function loadAllStorageModules( const result = await pgClient.query({ text: ALL_STORAGE_MODULES_QUERY, values: [databaseId] }); const configs = (result.rows as StorageModuleRow[]).map(buildConfig); - // Cache each individual config by its membership type + // Cache each individual config by its scope for (const config of configs) { - const key = config.membershipType === null - ? `storage:${databaseId}:app` - : `storage:${databaseId}:mt:${config.membershipType}`; + const key = `storage:${databaseId}:scope:${config.scope}`; storageModuleCache.set(key, config); } @@ -433,14 +429,15 @@ export async function getBucketConfig( // Entity-scoped buckets use (owner_id, key) composite lookup; // app-level buckets just use key. - const hasOwner = ownerId && storageConfig.membershipType !== null; + const isEntityScoped = storageConfig.scope !== 'app'; + const hasOwner = ownerId && isEntityScoped; const result = await pgClient.query({ text: hasOwner ? `SELECT id, key, type, is_public, owner_id, allowed_mime_types, max_file_size, allow_custom_keys FROM ${storageConfig.bucketsQualifiedName} WHERE key = $1 AND owner_id = $2 LIMIT 1` - : `SELECT id, key, type, is_public, ${storageConfig.membershipType !== null ? 'owner_id,' : ''} allowed_mime_types, max_file_size, allow_custom_keys + : `SELECT id, key, type, is_public, ${isEntityScoped ? 'owner_id,' : ''} allowed_mime_types, max_file_size, allow_custom_keys FROM ${storageConfig.bucketsQualifiedName} WHERE key = $1 LIMIT 1`, @@ -474,7 +471,7 @@ export async function getBucketConfig( }; bucketCache.set(cacheKey, config); - log.debug(`Cached bucket config for ${databaseId}:${bucketKey} (id=${config.id}, scope=${storageConfig.membershipType ?? 'app'})`); + log.debug(`Cached bucket config for ${databaseId}:${bucketKey} (id=${config.id}, scope=${storageConfig.scope})`); return config; } diff --git a/graphile/graphile-presigned-url-plugin/src/types.ts b/graphile/graphile-presigned-url-plugin/src/types.ts index 0a5b520ca6..13fa8c34a3 100644 --- a/graphile/graphile-presigned-url-plugin/src/types.ts +++ b/graphile/graphile-presigned-url-plugin/src/types.ts @@ -33,8 +33,8 @@ export interface StorageModuleConfig { // --- Scope identity --- - /** Scope (null for app-level, scope string for entity-scoped) */ - membershipType: string | null; + /** Scope name (e.g., 'app', 'org', 'team') */ + scope: string; /** Entity table ID for entity-scoped storage (NULL for app-level) */ entityTableId: string | null; /** Qualified entity table name for ownerId lookups (NULL for app-level) */