diff --git a/graphile/graphile-bucket-provisioner-plugin/__tests__/plugin.test.ts b/graphile/graphile-bucket-provisioner-plugin/__tests__/plugin.test.ts index 0ee54bc9d5..d81fb51279 100644 --- a/graphile/graphile-bucket-provisioner-plugin/__tests__/plugin.test.ts +++ b/graphile/graphile-bucket-provisioner-plugin/__tests__/plugin.test.ts @@ -100,7 +100,7 @@ function createMockPgClient(overrides: Record = {}) { 'jwt_private.current_database_id': { rows: [{ id: 'db-uuid-123' }], }, - 'metaschema_modules_public.storage_module': { + 'metaschema_modules_public.resolve_storage_modules': { rows: [{ id: 'sm-uuid-456', buckets_schema: 'app_public', @@ -369,7 +369,7 @@ describe('createBucketProvisionerPlugin', () => { createBucketProvisionerPlugin(createDefaultOptions()); const pgClient = createMockPgClient({ - 'metaschema_modules_public.storage_module': { rows: [] }, + 'metaschema_modules_public.resolve_storage_modules': { rows: [] }, }); const mockWithPgClient = jest.fn((_settings: any, callback: any) => callback(pgClient), @@ -428,7 +428,7 @@ describe('createBucketProvisionerPlugin', () => { createBucketProvisionerPlugin(createDefaultOptions()); const pgClient = createMockPgClient({ - 'metaschema_modules_public.storage_module': { + 'metaschema_modules_public.resolve_storage_modules': { rows: [{ id: 'sm-uuid-456', buckets_schema: 'app_public', @@ -485,7 +485,7 @@ describe('createBucketProvisionerPlugin', () => { createBucketProvisionerPlugin(createDefaultOptions()); const pgClient = createMockPgClient({ - 'metaschema_modules_public.storage_module': { + 'metaschema_modules_public.resolve_storage_modules': { rows: [{ id: 'sm-uuid-456', buckets_schema: 'app_public', @@ -1026,7 +1026,7 @@ describe('CORS resolution hierarchy', () => { allowed_origins: ['*'], }], }, - 'metaschema_modules_public.storage_module': { + 'metaschema_modules_public.resolve_storage_modules': { rows: [{ id: 'sm-uuid-456', buckets_schema: 'app_public', @@ -1083,7 +1083,7 @@ describe('CORS resolution hierarchy', () => { allowed_origins: null, // No bucket-level override }], }, - 'metaschema_modules_public.storage_module': { + 'metaschema_modules_public.resolve_storage_modules': { rows: [{ id: 'sm-uuid-456', buckets_schema: 'app_public', @@ -1142,7 +1142,7 @@ describe('CORS resolution hierarchy', () => { allowed_origins: null, }], }, - 'metaschema_modules_public.storage_module': { + 'metaschema_modules_public.resolve_storage_modules': { rows: [{ id: 'sm-uuid-456', buckets_schema: 'app_public', diff --git a/graphile/graphile-bucket-provisioner-plugin/src/plugin.ts b/graphile/graphile-bucket-provisioner-plugin/src/plugin.ts index 74eeedff40..7516840b93 100644 --- a/graphile/graphile-bucket-provisioner-plugin/src/plugin.ts +++ b/graphile/graphile-bucket-provisioner-plugin/src/plugin.ts @@ -55,20 +55,17 @@ const log = new Logger('graphile-bucket-provisioner:plugin'); */ const APP_STORAGE_MODULE_QUERY = ` SELECT - sm.id, - sm.scope, - sm.entity_table_id, - bs.schema_name AS buckets_schema, - bt.name AS buckets_table, - sm.endpoint, - sm.public_url_prefix, - sm.provider, - sm.allowed_origins - FROM metaschema_modules_public.storage_module sm - 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.scope = 'app' + id, + scope, + entity_table_id, + buckets_schema, + buckets_table, + endpoint, + public_url_prefix, + provider, + allowed_origins + FROM metaschema_modules_public.resolve_storage_modules($1) + WHERE scope = 'app' LIMIT 1 `; @@ -77,23 +74,18 @@ const APP_STORAGE_MODULE_QUERY = ` */ const ALL_STORAGE_MODULES_QUERY = ` SELECT - sm.id, - sm.scope, - sm.entity_table_id, - bs.schema_name AS buckets_schema, - bt.name AS buckets_table, - sm.endpoint, - sm.public_url_prefix, - sm.provider, - sm.allowed_origins, - es.schema_name AS entity_schema, - et.name AS entity_table - FROM metaschema_modules_public.storage_module sm - JOIN metaschema_public.table bt ON bt.id = sm.buckets_table_id - JOIN metaschema_public.schema bs ON bs.id = bt.schema_id - LEFT JOIN metaschema_public.table et ON et.id = sm.entity_table_id - LEFT JOIN metaschema_public.schema es ON es.id = et.schema_id - WHERE sm.database_id = $1 + id, + scope, + entity_table_id, + buckets_schema, + buckets_table, + endpoint, + public_url_prefix, + provider, + allowed_origins, + entity_schema, + entity_table + FROM metaschema_modules_public.resolve_storage_modules($1) `; interface StorageModuleRow { @@ -360,6 +352,17 @@ export function createBucketProvisionerPlugin( Omit for app-level (database-wide) storage. """ ownerId: UUID + """ + Access type used only when the bucket row does not yet exist: + "public", "private", or "temp". Defaults to "private" + (or "public" when isPublic is true). + """ + type: String + """ + Whether the bucket is publicly readable, used only when creating + the row. Defaults to false unless type is "public". + """ + isPublic: Boolean } type ProvisionBucketPayload { @@ -402,7 +405,7 @@ export function createBucketProvisionerPlugin( }); return lambda($combined, async ({ input, withPgClient, pgSettings }: any) => { - const { bucketKey, ownerId } = input; + const { bucketKey, ownerId, type: requestedType, isPublic: requestedIsPublic } = input; if (!bucketKey || typeof bucketKey !== 'string') { throw new Error('INVALID_BUCKET_KEY'); @@ -428,6 +431,26 @@ export function createBucketProvisionerPlugin( // Look up the bucket row (RLS enforced via pgSettings) const hasOwner = ownerId && storageModule.scope !== 'app'; const bucketsTable = QuoteUtils.quoteQualifiedIdentifier(storageModule.buckets_schema, storageModule.buckets_table); + + // Ensure the bucket row exists (privileged create-on-demand for the + // post-provision Storage panel / re-provision flow). Runs under the + // same role/connection as the lookup below; ON CONFLICT keeps it + // idempotent so an existing bucket is left untouched. database_id and + // actor_id are populated by the buckets table's own triggers. + const ensureType = requestedType || (requestedIsPublic ? 'public' : 'private'); + const ensureIsPublic = + typeof requestedIsPublic === 'boolean' ? requestedIsPublic : ensureType === 'public'; + await pgClient.query( + hasOwner + ? `INSERT INTO ${bucketsTable} (key, type, is_public, owner_id) + VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING` + : `INSERT INTO ${bucketsTable} (key, type, is_public) + VALUES ($1, $2, $3) ON CONFLICT DO NOTHING`, + hasOwner + ? [bucketKey, ensureType, ensureIsPublic, ownerId] + : [bucketKey, ensureType, ensureIsPublic], + ); + const bucketResult = await pgClient.query( hasOwner ? `SELECT id, key, type, is_public, allowed_origins 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 6ea403fbb5..3d99e1787c 100644 --- a/graphile/graphile-presigned-url-plugin/src/storage-module-cache.ts +++ b/graphile/graphile-presigned-url-plugin/src/storage-module-cache.ts @@ -43,34 +43,29 @@ const storageModuleCache = new LRUCache({ */ const APP_STORAGE_MODULE_QUERY = ` SELECT - sm.id, - sm.scope, - sm.entity_table_id, - bs.schema_name AS buckets_schema, - bt.name AS buckets_table, - fs.schema_name AS files_schema, - ft.name AS files_table, - sm.endpoint, - sm.public_url_prefix, - sm.provider, - sm.allowed_origins, - sm.upload_url_expiry_seconds, - sm.download_url_expiry_seconds, - sm.default_max_file_size, - sm.max_filename_length, - sm.cache_ttl_seconds, - sm.max_bulk_files, - sm.max_bulk_total_size, - sm.has_path_shares, - NULL AS entity_schema, - NULL AS entity_table - FROM metaschema_modules_public.storage_module sm - JOIN metaschema_public.table bt ON bt.id = sm.buckets_table_id - JOIN metaschema_public.schema bs ON bs.id = bt.schema_id - 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.scope = 'app' + id, + scope, + entity_table_id, + buckets_schema, + buckets_table, + files_schema, + files_table, + endpoint, + public_url_prefix, + provider, + allowed_origins, + upload_url_expiry_seconds, + download_url_expiry_seconds, + default_max_file_size, + max_filename_length, + cache_ttl_seconds, + max_bulk_files, + max_bulk_total_size, + has_path_shares, + entity_schema, + entity_table + FROM metaschema_modules_public.resolve_storage_modules($1) + WHERE scope = 'app' LIMIT 1 `; @@ -82,35 +77,28 @@ const APP_STORAGE_MODULE_QUERY = ` */ const ALL_STORAGE_MODULES_QUERY = ` SELECT - sm.id, - sm.scope, - sm.entity_table_id, - bs.schema_name AS buckets_schema, - bt.name AS buckets_table, - fs.schema_name AS files_schema, - ft.name AS files_table, - sm.endpoint, - sm.public_url_prefix, - sm.provider, - sm.allowed_origins, - sm.upload_url_expiry_seconds, - sm.download_url_expiry_seconds, - sm.default_max_file_size, - sm.max_filename_length, - sm.cache_ttl_seconds, - sm.max_bulk_files, - sm.max_bulk_total_size, - sm.has_path_shares, - es.schema_name AS entity_schema, - et.name AS entity_table - FROM metaschema_modules_public.storage_module sm - JOIN metaschema_public.table bt ON bt.id = sm.buckets_table_id - JOIN metaschema_public.schema bs ON bs.id = bt.schema_id - JOIN metaschema_public.table ft ON ft.id = sm.files_table_id - JOIN metaschema_public.schema fs ON fs.id = ft.schema_id - LEFT JOIN metaschema_public.table et ON et.id = sm.entity_table_id - LEFT JOIN metaschema_public.schema es ON es.id = et.schema_id - WHERE sm.database_id = $1 + id, + scope, + entity_table_id, + buckets_schema, + buckets_table, + files_schema, + files_table, + endpoint, + public_url_prefix, + provider, + allowed_origins, + upload_url_expiry_seconds, + download_url_expiry_seconds, + default_max_file_size, + max_filename_length, + cache_ttl_seconds, + max_bulk_files, + max_bulk_total_size, + has_path_shares, + entity_schema, + entity_table + FROM metaschema_modules_public.resolve_storage_modules($1) `; interface StorageModuleRow {