@@ -22,8 +22,8 @@ import type { GraphileConfig } from 'graphile-config';
2222import { extendSchema , gql } from 'graphile-utils' ;
2323import { Logger } from '@pgpmjs/logger' ;
2424
25- import type { PresignedUrlPluginOptions , S3Config , StorageModuleConfig } from './types' ;
26- import { getStorageModuleConfig , getBucketConfig } from './storage-module-cache' ;
25+ import type { PresignedUrlPluginOptions , S3Config , StorageModuleConfig , BucketConfig } from './types' ;
26+ import { getStorageModuleConfig , getBucketConfig , isS3BucketProvisioned , markS3BucketProvisioned } from './storage-module-cache' ;
2727import { generatePresignedPutUrl , headObject } from './s3-signer' ;
2828
2929const log = new Logger ( 'graphile-presigned-url:plugin' ) ;
@@ -112,6 +112,32 @@ function resolveS3ForDatabase(
112112 } ;
113113}
114114
115+ /**
116+ * Ensure the S3 bucket for a database exists, provisioning it lazily if needed.
117+ *
118+ * Checks an in-memory Set of known-provisioned bucket names. On the first
119+ * request for an unseen bucket, calls the `ensureBucketProvisioned` callback
120+ * (which creates the bucket with correct CORS, policies, etc.), then marks
121+ * it as provisioned so subsequent requests skip the check entirely.
122+ *
123+ * If no `ensureBucketProvisioned` callback is configured, this is a no-op.
124+ */
125+ async function ensureS3BucketExists (
126+ options : PresignedUrlPluginOptions ,
127+ s3BucketName : string ,
128+ bucket : BucketConfig ,
129+ databaseId : string ,
130+ allowedOrigins : string [ ] | null ,
131+ ) : Promise < void > {
132+ if ( ! options . ensureBucketProvisioned ) return ;
133+ if ( isS3BucketProvisioned ( s3BucketName ) ) return ;
134+
135+ log . info ( `Lazy-provisioning S3 bucket "${ s3BucketName } " for database ${ databaseId } ` ) ;
136+ await options . ensureBucketProvisioned ( s3BucketName , bucket . type , databaseId , allowedOrigins ) ;
137+ markS3BucketProvisioned ( s3BucketName ) ;
138+ log . info ( `Lazy-provisioned S3 bucket "${ s3BucketName } " successfully` ) ;
139+ }
140+
115141export function createPresignedUrlPlugin (
116142 options : PresignedUrlPluginOptions ,
117143) : GraphileConfig . Plugin {
@@ -314,8 +340,11 @@ export function createPresignedUrlPlugin(
314340
315341 const fileId = fileResult . rows [ 0 ] . id ;
316342
317- // --- Generate presigned PUT URL (per-database bucket ) ---
343+ // --- Ensure the S3 bucket exists (lazy provisioning ) ---
318344 const s3ForDb = resolveS3ForDatabase ( options , storageConfig , databaseId ) ;
345+ await ensureS3BucketExists ( options , s3ForDb . bucket , bucket , databaseId , storageConfig . allowedOrigins ) ;
346+
347+ // --- Generate presigned PUT URL (per-database bucket) ---
319348 const uploadUrl = await generatePresignedPutUrl (
320349 s3ForDb ,
321350 s3Key ,
0 commit comments