Skip to content

Commit 95f6d4c

Browse files
refactor(storage-confirm-upload): query storage_module metadata
- Query metaschema_modules_public.storage_module for schema/table names - Use QuoteUtils.quoteQualifiedIdentifier for SQL identifier quoting - Remove string manipulation for schema derivation - Use connection pool from context instead of manual PgClient - Standardize env vars (CDN_ENDPOINT, AWS_ACCESS_KEY, AWS_REGION) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent cdc2ba7 commit 95f6d4c

1 file changed

Lines changed: 113 additions & 94 deletions

File tree

functions/storage-confirm-upload/handler.ts

Lines changed: 113 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,65 @@
11
import { 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

65
type 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-
6763
const 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(/-public$/, '-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(/-app-public$/, '-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(/-public$/, '-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

Comments
 (0)