11import { HeadObjectCommand , S3Client } from '@aws-sdk/client-s3' ;
2- import type { FunctionHandler } from '@constructive-io/fn-runtime' ;
3- import { createLogger } from '@pgpmjs/logger' ;
4- import { Client as PgClient } from 'pg' ;
2+ import { QuoteUtils } from '@pgsql/quotes' ;
3+ import type { FunctionHandler } from './types' ;
54
65type StorageConfirmUploadPayload = {
76 file_id : string ;
87 key : string ;
98 bucket_id : string ;
10- mime_type : string ;
11- schema ?: string ;
12- table ?: string ;
9+ mime_type ?: string ;
10+ actor_id ?: string ;
1311} ;
1412
15- const logger = createLogger ( 'storage-confirm-upload' ) ;
13+ type Result = {
14+ success : boolean ;
15+ file_id : string ;
16+ bucket : string ;
17+ key : string ;
18+ } ;
1619
17- const createPgClient = ( ) : PgClient => {
18- return new PgClient ( {
19- host : process . env . PGHOST || 'localhost' ,
20- port : Number ( process . env . PGPORT ) || 5432 ,
21- user : process . env . PGUSER || 'postgres' ,
22- password : process . env . PGPASSWORD || 'password' ,
23- database : process . env . PGDATABASE || 'constructive' ,
24- } ) ;
20+ type StorageModuleConfig = {
21+ id : string ;
22+ bucketsSchema : string ;
23+ bucketsTable : string ;
24+ filesSchema : string ;
25+ filesTable : string ;
26+ endpoint : string | null ;
27+ provider : string | null ;
2528} ;
2629
27- const createS3Client = ( ) : S3Client => {
28- const endpoint = process . env . S3_ENDPOINT || process . env . CDN_ENDPOINT ;
30+ const STORAGE_MODULE_QUERY = `
31+ SELECT
32+ sm.id,
33+ bs.schema_name AS buckets_schema,
34+ bt.name AS buckets_table,
35+ fs.schema_name AS files_schema,
36+ ft.name AS files_table,
37+ sm.endpoint,
38+ sm.provider
39+ FROM metaschema_modules_public.storage_module sm
40+ JOIN metaschema_public.table bt ON bt.id = sm.buckets_table_id
41+ JOIN metaschema_public.schema bs ON bs.id = bt.schema_id
42+ JOIN metaschema_public.table ft ON ft.id = sm.files_table_id
43+ JOIN metaschema_public.schema fs ON fs.id = ft.schema_id
44+ WHERE sm.database_id = $1
45+ AND sm.scope = 'app'
46+ LIMIT 1
47+ ` ;
48+
49+ const getS3Client = ( config ?: StorageModuleConfig ) : S3Client => {
50+ const endpoint = config ?. endpoint || process . env . CDN_ENDPOINT ;
51+ const region = process . env . AWS_REGION || 'us-east-1' ;
52+ const accessKeyId = process . env . AWS_ACCESS_KEY || process . env . AWS_ACCESS_KEY_ID || '' ;
53+ const secretAccessKey = process . env . AWS_SECRET_KEY || process . env . AWS_SECRET_ACCESS_KEY || '' ;
54+
2955 return new S3Client ( {
30- region : process . env . CDN_REGION || process . env . AWS_REGION || 'us-east-1' ,
31- endpoint : endpoint ,
56+ region,
57+ endpoint : endpoint || undefined ,
3258 forcePathStyle : true ,
33- credentials : {
34- accessKeyId : process . env . CDN_ACCESS_KEY || process . env . AWS_ACCESS_KEY_ID || '' ,
35- secretAccessKey : process . env . CDN_SECRET_KEY || process . env . AWS_SECRET_ACCESS_KEY || '' ,
36- } ,
59+ credentials : { accessKeyId, secretAccessKey } ,
3760 } ) ;
3861} ;
3962
40- const resolveS3BucketName = async (
41- bucketId : string ,
42- storageSchema : string ,
43- databaseId : string
44- ) : Promise < string > => {
45- const pg = createPgClient ( ) ;
46- try {
47- await pg . connect ( ) ;
48- // Get bucket type to construct S3 bucket name
49- // Format: test-bucket-{type}-{database_id}
50- const result = await pg . query (
51- `SELECT b.type
52- FROM "${ storageSchema } ".app_buckets b
53- WHERE b.id = $1` ,
54- [ bucketId ]
55- ) ;
56- if ( result . rows . length > 0 && result . rows [ 0 ] . type ) {
57- const bucketType = result . rows [ 0 ] . type ;
58- return `test-bucket-${ bucketType } -${ databaseId } ` ;
59- }
60- // Fallback to bucket_id if type not found
61- return bucketId ;
62- } finally {
63- await pg . end ( ) ;
64- }
65- } ;
66-
6763const checkFileExistsInS3 = async (
6864 s3 : S3Client ,
6965 bucket : string ,
@@ -81,63 +77,74 @@ const checkFileExistsInS3 = async (
8177 }
8278} ;
8379
84- const confirmFileUploaded = async (
85- fileId : string ,
86- schema : string ,
87- table : string
88- ) : Promise < void > => {
89- const pg = createPgClient ( ) ;
90- try {
91- await pg . connect ( ) ;
92- const privateSchema = schema . replace ( / - p u b l i c $ / , '-private' ) ;
93- const fnName = `${ table } _confirm_uploaded` ;
94-
95- await pg . query ( `SELECT "${ privateSchema } "."${ fnName } "($1)` , [ fileId ] ) ;
96-
97- logger . info ( '[storage-confirm-upload] File status confirmed' , { fileId, schema, table } ) ;
98- } finally {
99- await pg . end ( ) ;
100- }
101- } ;
102-
103- const handler : FunctionHandler < StorageConfirmUploadPayload > = async (
80+ const handler : FunctionHandler < StorageConfirmUploadPayload , Result > = async (
10481 params ,
10582 context
10683) => {
107- const { job, log } = context ;
108- const { file_id, key, bucket_id, mime_type , schema , table } = params ;
84+ const { job, log, pool } = context ;
85+ const { file_id, key, bucket_id } = params ;
10986
11087 if ( ! file_id || ! key || ! bucket_id ) {
11188 throw new Error ( 'Missing required fields: file_id, key, or bucket_id' ) ;
11289 }
11390
114- log . info ( '[storage-confirm-upload] Processing' , {
115- file_id,
116- key,
117- bucket_id,
118- schema,
119- table,
120- } ) ;
91+ if ( ! job . databaseId ) {
92+ throw new Error ( 'Missing databaseId in job context' ) ;
93+ }
12194
122- const s3 = createS3Client ( ) ;
95+ log . info ( '[storage-confirm-upload] Processing' , { file_id , key , bucket_id } ) ;
12396
124- const storageSchema = schema
125- ? schema . replace ( / - a p p - p u b l i c $ / , '-storage-public' )
126- : 'storage_public' ;
97+ // Load storage module config from metaschema
98+ const client = await pool . connect ( ) ;
99+ let config : StorageModuleConfig ;
100+ try {
101+ const result = await client . query ( STORAGE_MODULE_QUERY , [ job . databaseId ] ) ;
102+ if ( result . rows . length === 0 ) {
103+ throw new Error ( `STORAGE_MODULE_NOT_FOUND for database ${ job . databaseId } ` ) ;
104+ }
105+ const row = result . rows [ 0 ] ;
106+ config = {
107+ id : row . id ,
108+ bucketsSchema : row . buckets_schema ,
109+ bucketsTable : row . buckets_table ,
110+ filesSchema : row . files_schema ,
111+ filesTable : row . files_table ,
112+ endpoint : row . endpoint ,
113+ provider : row . provider ,
114+ } ;
115+ log . info ( '[storage-confirm-upload] Loaded storage config' , {
116+ bucketsSchema : config . bucketsSchema ,
117+ filesSchema : config . filesSchema ,
118+ } ) ;
119+ } finally {
120+ client . release ( ) ;
121+ }
127122
123+ // Resolve S3 bucket name from buckets table
124+ const bucketsQualifiedName = QuoteUtils . quoteQualifiedIdentifier ( config . bucketsSchema , config . bucketsTable ) ;
128125 let bucketName = bucket_id ;
129- if ( schema && job . databaseId ) {
130- try {
131- bucketName = await resolveS3BucketName ( bucket_id , storageSchema , job . databaseId ) ;
126+ const bucketClient = await pool . connect ( ) ;
127+ try {
128+ const result = await bucketClient . query (
129+ `SELECT type FROM ${ bucketsQualifiedName } WHERE id = $1` ,
130+ [ bucket_id ]
131+ ) ;
132+ if ( result . rows . length > 0 && result . rows [ 0 ] . type ) {
133+ const bucketType = result . rows [ 0 ] . type ;
134+ bucketName = `test-bucket-${ bucketType } -${ job . databaseId } ` ;
132135 log . info ( '[storage-confirm-upload] Resolved bucket name' , { bucket_id, bucketName } ) ;
133- } catch ( err ) {
134- log . warn ( '[storage-confirm-upload] Could not resolve bucket, using bucket_id' , {
135- bucket_id,
136- error : ( err as Error ) . message ,
137- } ) ;
138136 }
137+ } catch ( err ) {
138+ log . warn ( '[storage-confirm-upload] Could not resolve bucket, using bucket_id' , {
139+ bucket_id,
140+ error : ( err as Error ) . message ,
141+ } ) ;
142+ } finally {
143+ bucketClient . release ( ) ;
139144 }
140145
146+ // Check file exists in S3
147+ const s3 = getS3Client ( config ) ;
141148 const exists = await checkFileExistsInS3 ( s3 , bucketName , key ) ;
142149
143150 if ( ! exists ) {
@@ -150,10 +157,22 @@ const handler: FunctionHandler<StorageConfirmUploadPayload> = async (
150157
151158 log . info ( '[storage-confirm-upload] File exists in S3' , { bucket : bucketName , key } ) ;
152159
153- const targetSchema = schema || storageSchema ;
154- const targetTable = table || 'app_files' ;
160+ // Confirm file uploaded - private schema is derived from files schema
161+ const privateSchema = config . filesSchema . replace ( / - p u b l i c $ / , '-private' ) ;
162+ const fnName = `${ config . filesTable } _confirm_uploaded` ;
163+ const fnQualifiedName = QuoteUtils . quoteQualifiedIdentifier ( privateSchema , fnName ) ;
155164
156- await confirmFileUploaded ( file_id , targetSchema , targetTable ) ;
165+ const confirmClient = await pool . connect ( ) ;
166+ try {
167+ await confirmClient . query ( `SELECT ${ fnQualifiedName } ($1)` , [ file_id ] ) ;
168+ log . info ( '[storage-confirm-upload] File status confirmed' , {
169+ file_id,
170+ schema : privateSchema ,
171+ function : fnName ,
172+ } ) ;
173+ } finally {
174+ confirmClient . release ( ) ;
175+ }
157176
158177 return {
159178 success : true ,
0 commit comments