graphile-presigned-url-plugin 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/download-url-field.js +3 -1
- package/esm/download-url-field.js +3 -1
- package/esm/plugin.js +61 -50
- package/esm/storage-module-cache.d.ts +8 -2
- package/esm/storage-module-cache.js +6 -3
- package/package.json +2 -2
- package/plugin.js +61 -50
- package/storage-module-cache.d.ts +8 -2
- package/storage-module-cache.js +6 -3
package/download-url-field.js
CHANGED
|
@@ -104,7 +104,9 @@ function createDownloadUrlPlugin(options) {
|
|
|
104
104
|
: null;
|
|
105
105
|
if (withPgClient) {
|
|
106
106
|
const resolved = await withPgClient(null, async (pgClient) => {
|
|
107
|
-
const dbResult = await pgClient.query(
|
|
107
|
+
const dbResult = await pgClient.query({
|
|
108
|
+
text: `SELECT jwt_private.current_database_id() AS id`,
|
|
109
|
+
});
|
|
108
110
|
const databaseId = dbResult.rows[0]?.id;
|
|
109
111
|
if (!databaseId)
|
|
110
112
|
return null;
|
|
@@ -101,7 +101,9 @@ export function createDownloadUrlPlugin(options) {
|
|
|
101
101
|
: null;
|
|
102
102
|
if (withPgClient) {
|
|
103
103
|
const resolved = await withPgClient(null, async (pgClient) => {
|
|
104
|
-
const dbResult = await pgClient.query(
|
|
104
|
+
const dbResult = await pgClient.query({
|
|
105
|
+
text: `SELECT jwt_private.current_database_id() AS id`,
|
|
106
|
+
});
|
|
105
107
|
const databaseId = dbResult.rows[0]?.id;
|
|
106
108
|
if (!databaseId)
|
|
107
109
|
return null;
|
package/esm/plugin.js
CHANGED
|
@@ -48,7 +48,9 @@ function buildS3Key(contentHash) {
|
|
|
48
48
|
* metaschema query needed.
|
|
49
49
|
*/
|
|
50
50
|
async function resolveDatabaseId(pgClient) {
|
|
51
|
-
const result = await pgClient.query(
|
|
51
|
+
const result = await pgClient.query({
|
|
52
|
+
text: `SELECT jwt_private.current_database_id() AS id`,
|
|
53
|
+
});
|
|
52
54
|
return result.rows[0]?.id ?? null;
|
|
53
55
|
}
|
|
54
56
|
// --- Plugin factory ---
|
|
@@ -200,14 +202,13 @@ export function createPresignedUrlPlugin(options) {
|
|
|
200
202
|
throw new Error('INVALID_CONTENT_TYPE');
|
|
201
203
|
}
|
|
202
204
|
return withPgClient(pgSettings, async (pgClient) => {
|
|
203
|
-
|
|
204
|
-
try {
|
|
205
|
+
return pgClient.withTransaction(async (txClient) => {
|
|
205
206
|
// --- Resolve storage module config (all limits come from here) ---
|
|
206
|
-
const databaseId = await resolveDatabaseId(
|
|
207
|
+
const databaseId = await resolveDatabaseId(txClient);
|
|
207
208
|
if (!databaseId) {
|
|
208
209
|
throw new Error('DATABASE_NOT_FOUND');
|
|
209
210
|
}
|
|
210
|
-
const storageConfig = await getStorageModuleConfig(
|
|
211
|
+
const storageConfig = await getStorageModuleConfig(txClient, databaseId);
|
|
211
212
|
if (!storageConfig) {
|
|
212
213
|
throw new Error('STORAGE_MODULE_NOT_PROVISIONED');
|
|
213
214
|
}
|
|
@@ -221,7 +222,7 @@ export function createPresignedUrlPlugin(options) {
|
|
|
221
222
|
}
|
|
222
223
|
}
|
|
223
224
|
// --- Look up the bucket (cached; first miss queries via RLS) ---
|
|
224
|
-
const bucket = await getBucketConfig(
|
|
225
|
+
const bucket = await getBucketConfig(txClient, storageConfig, databaseId, bucketKey);
|
|
225
226
|
if (!bucket) {
|
|
226
227
|
throw new Error('BUCKET_NOT_FOUND');
|
|
227
228
|
}
|
|
@@ -247,20 +248,25 @@ export function createPresignedUrlPlugin(options) {
|
|
|
247
248
|
}
|
|
248
249
|
const s3Key = buildS3Key(contentHash);
|
|
249
250
|
// --- Dedup check: look for existing file with same content_hash in this bucket ---
|
|
250
|
-
const dedupResult = await
|
|
251
|
+
const dedupResult = await txClient.query({
|
|
252
|
+
text: `SELECT id, status
|
|
251
253
|
FROM ${storageConfig.filesQualifiedName}
|
|
252
254
|
WHERE content_hash = $1
|
|
253
255
|
AND bucket_id = $2
|
|
254
256
|
AND status IN ('ready', 'processed')
|
|
255
|
-
LIMIT 1`,
|
|
257
|
+
LIMIT 1`,
|
|
258
|
+
values: [contentHash, bucket.id],
|
|
259
|
+
});
|
|
256
260
|
if (dedupResult.rows.length > 0) {
|
|
257
261
|
const existingFile = dedupResult.rows[0];
|
|
258
262
|
log.info(`Dedup hit: file ${existingFile.id} for hash ${contentHash}`);
|
|
259
263
|
// Track the dedup request
|
|
260
|
-
await
|
|
264
|
+
await txClient.query({
|
|
265
|
+
text: `INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
|
|
261
266
|
(file_id, bucket_id, key, content_type, content_hash, size, status, expires_at)
|
|
262
|
-
VALUES ($1, $2, $3, $4, $5, $6, 'confirmed', NOW())`,
|
|
263
|
-
|
|
267
|
+
VALUES ($1, $2, $3, $4, $5, $6, 'confirmed', NOW())`,
|
|
268
|
+
values: [existingFile.id, bucket.id, s3Key, contentType, contentHash, size],
|
|
269
|
+
});
|
|
264
270
|
return {
|
|
265
271
|
uploadUrl: null,
|
|
266
272
|
fileId: existingFile.id,
|
|
@@ -270,19 +276,22 @@ export function createPresignedUrlPlugin(options) {
|
|
|
270
276
|
};
|
|
271
277
|
}
|
|
272
278
|
// --- Create file record (status=pending) ---
|
|
273
|
-
const fileResult = await
|
|
279
|
+
const fileResult = await txClient.query({
|
|
280
|
+
text: `INSERT INTO ${storageConfig.filesQualifiedName}
|
|
274
281
|
(bucket_id, key, content_type, content_hash, size, filename, owner_id, is_public, status)
|
|
275
282
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'pending')
|
|
276
|
-
RETURNING id`,
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
283
|
+
RETURNING id`,
|
|
284
|
+
values: [
|
|
285
|
+
bucket.id,
|
|
286
|
+
s3Key,
|
|
287
|
+
contentType,
|
|
288
|
+
contentHash,
|
|
289
|
+
size,
|
|
290
|
+
filename || null,
|
|
291
|
+
bucket.owner_id,
|
|
292
|
+
bucket.is_public,
|
|
293
|
+
],
|
|
294
|
+
});
|
|
286
295
|
const fileId = fileResult.rows[0].id;
|
|
287
296
|
// --- Ensure the S3 bucket exists (lazy provisioning) ---
|
|
288
297
|
const s3ForDb = resolveS3ForDatabase(options, storageConfig, databaseId);
|
|
@@ -291,10 +300,12 @@ export function createPresignedUrlPlugin(options) {
|
|
|
291
300
|
const uploadUrl = await generatePresignedPutUrl(s3ForDb, s3Key, contentType, size, storageConfig.uploadUrlExpirySeconds);
|
|
292
301
|
const expiresAt = new Date(Date.now() + storageConfig.uploadUrlExpirySeconds * 1000).toISOString();
|
|
293
302
|
// --- Track the upload request ---
|
|
294
|
-
await
|
|
303
|
+
await txClient.query({
|
|
304
|
+
text: `INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
|
|
295
305
|
(file_id, bucket_id, key, content_type, content_hash, size, status, expires_at)
|
|
296
|
-
VALUES ($1, $2, $3, $4, $5, $6, 'issued', $7)`,
|
|
297
|
-
|
|
306
|
+
VALUES ($1, $2, $3, $4, $5, $6, 'issued', $7)`,
|
|
307
|
+
values: [fileId, bucket.id, s3Key, contentType, contentHash, size, expiresAt],
|
|
308
|
+
});
|
|
298
309
|
return {
|
|
299
310
|
uploadUrl,
|
|
300
311
|
fileId,
|
|
@@ -302,11 +313,7 @@ export function createPresignedUrlPlugin(options) {
|
|
|
302
313
|
deduplicated: false,
|
|
303
314
|
expiresAt,
|
|
304
315
|
};
|
|
305
|
-
}
|
|
306
|
-
catch (err) {
|
|
307
|
-
await pgClient.query('ROLLBACK');
|
|
308
|
-
throw err;
|
|
309
|
-
}
|
|
316
|
+
});
|
|
310
317
|
});
|
|
311
318
|
});
|
|
312
319
|
},
|
|
@@ -325,29 +332,30 @@ export function createPresignedUrlPlugin(options) {
|
|
|
325
332
|
throw new Error('INVALID_FILE_ID');
|
|
326
333
|
}
|
|
327
334
|
return withPgClient(pgSettings, async (pgClient) => {
|
|
328
|
-
|
|
329
|
-
try {
|
|
335
|
+
return pgClient.withTransaction(async (txClient) => {
|
|
330
336
|
// --- Resolve storage module config ---
|
|
331
|
-
const databaseId = await resolveDatabaseId(
|
|
337
|
+
const databaseId = await resolveDatabaseId(txClient);
|
|
332
338
|
if (!databaseId) {
|
|
333
339
|
throw new Error('DATABASE_NOT_FOUND');
|
|
334
340
|
}
|
|
335
|
-
const storageConfig = await getStorageModuleConfig(
|
|
341
|
+
const storageConfig = await getStorageModuleConfig(txClient, databaseId);
|
|
336
342
|
if (!storageConfig) {
|
|
337
343
|
throw new Error('STORAGE_MODULE_NOT_PROVISIONED');
|
|
338
344
|
}
|
|
339
345
|
// --- Look up the file (RLS enforced) ---
|
|
340
|
-
const fileResult = await
|
|
346
|
+
const fileResult = await txClient.query({
|
|
347
|
+
text: `SELECT id, key, content_type, status, bucket_id
|
|
341
348
|
FROM ${storageConfig.filesQualifiedName}
|
|
342
349
|
WHERE id = $1
|
|
343
|
-
LIMIT 1`,
|
|
350
|
+
LIMIT 1`,
|
|
351
|
+
values: [fileId],
|
|
352
|
+
});
|
|
344
353
|
if (fileResult.rows.length === 0) {
|
|
345
354
|
throw new Error('FILE_NOT_FOUND');
|
|
346
355
|
}
|
|
347
356
|
const file = fileResult.rows[0];
|
|
348
357
|
if (file.status !== 'pending') {
|
|
349
358
|
// File is already confirmed or processed — idempotent success
|
|
350
|
-
await pgClient.query('COMMIT');
|
|
351
359
|
return {
|
|
352
360
|
fileId: file.id,
|
|
353
361
|
status: file.status,
|
|
@@ -363,31 +371,34 @@ export function createPresignedUrlPlugin(options) {
|
|
|
363
371
|
// --- Content-type verification ---
|
|
364
372
|
if (s3Head.contentType && s3Head.contentType !== file.content_type) {
|
|
365
373
|
// Mark upload_request as rejected
|
|
366
|
-
await
|
|
374
|
+
await txClient.query({
|
|
375
|
+
text: `UPDATE ${storageConfig.uploadRequestsQualifiedName}
|
|
367
376
|
SET status = 'rejected'
|
|
368
|
-
WHERE file_id = $1 AND status = 'issued'`,
|
|
369
|
-
|
|
377
|
+
WHERE file_id = $1 AND status = 'issued'`,
|
|
378
|
+
values: [fileId],
|
|
379
|
+
});
|
|
370
380
|
throw new Error(`CONTENT_TYPE_MISMATCH: expected ${file.content_type}, got ${s3Head.contentType}`);
|
|
371
381
|
}
|
|
372
382
|
// --- Transition file to 'ready' ---
|
|
373
|
-
await
|
|
383
|
+
await txClient.query({
|
|
384
|
+
text: `UPDATE ${storageConfig.filesQualifiedName}
|
|
374
385
|
SET status = 'ready'
|
|
375
|
-
WHERE id = $1`,
|
|
386
|
+
WHERE id = $1`,
|
|
387
|
+
values: [fileId],
|
|
388
|
+
});
|
|
376
389
|
// --- Update upload_request to 'confirmed' ---
|
|
377
|
-
await
|
|
390
|
+
await txClient.query({
|
|
391
|
+
text: `UPDATE ${storageConfig.uploadRequestsQualifiedName}
|
|
378
392
|
SET status = 'confirmed', confirmed_at = NOW()
|
|
379
|
-
WHERE file_id = $1 AND status = 'issued'`,
|
|
380
|
-
|
|
393
|
+
WHERE file_id = $1 AND status = 'issued'`,
|
|
394
|
+
values: [fileId],
|
|
395
|
+
});
|
|
381
396
|
return {
|
|
382
397
|
fileId: file.id,
|
|
383
398
|
status: 'ready',
|
|
384
399
|
success: true,
|
|
385
400
|
};
|
|
386
|
-
}
|
|
387
|
-
catch (err) {
|
|
388
|
-
await pgClient.query('ROLLBACK');
|
|
389
|
-
throw err;
|
|
390
|
-
}
|
|
401
|
+
});
|
|
391
402
|
});
|
|
392
403
|
});
|
|
393
404
|
},
|
|
@@ -7,7 +7,10 @@ import type { StorageModuleConfig, BucketConfig } from './types';
|
|
|
7
7
|
* @returns StorageModuleConfig or null if no storage module is provisioned
|
|
8
8
|
*/
|
|
9
9
|
export declare function getStorageModuleConfig(pgClient: {
|
|
10
|
-
query: (
|
|
10
|
+
query: (opts: {
|
|
11
|
+
text: string;
|
|
12
|
+
values?: unknown[];
|
|
13
|
+
}) => Promise<{
|
|
11
14
|
rows: unknown[];
|
|
12
15
|
}>;
|
|
13
16
|
}, databaseId: string): Promise<StorageModuleConfig | null>;
|
|
@@ -24,7 +27,10 @@ export declare function getStorageModuleConfig(pgClient: {
|
|
|
24
27
|
* @returns BucketConfig or null if the bucket doesn't exist / isn't accessible
|
|
25
28
|
*/
|
|
26
29
|
export declare function getBucketConfig(pgClient: {
|
|
27
|
-
query: (
|
|
30
|
+
query: (opts: {
|
|
31
|
+
text: string;
|
|
32
|
+
values?: unknown[];
|
|
33
|
+
}) => Promise<{
|
|
28
34
|
rows: unknown[];
|
|
29
35
|
}>;
|
|
30
36
|
}, storageConfig: StorageModuleConfig, databaseId: string, bucketKey: string): Promise<BucketConfig | null>;
|
|
@@ -71,7 +71,7 @@ export async function getStorageModuleConfig(pgClient, databaseId) {
|
|
|
71
71
|
return cached;
|
|
72
72
|
}
|
|
73
73
|
log.debug(`Cache miss for database ${databaseId}, querying metaschema...`);
|
|
74
|
-
const result = await pgClient.query(STORAGE_MODULE_QUERY, [databaseId]);
|
|
74
|
+
const result = await pgClient.query({ text: STORAGE_MODULE_QUERY, values: [databaseId] });
|
|
75
75
|
if (result.rows.length === 0) {
|
|
76
76
|
log.warn(`No storage module found for database ${databaseId}`);
|
|
77
77
|
return null;
|
|
@@ -140,10 +140,13 @@ export async function getBucketConfig(pgClient, storageConfig, databaseId, bucke
|
|
|
140
140
|
return cached;
|
|
141
141
|
}
|
|
142
142
|
log.debug(`Bucket cache miss for ${databaseId}:${bucketKey}, querying DB...`);
|
|
143
|
-
const result = await pgClient.query(
|
|
143
|
+
const result = await pgClient.query({
|
|
144
|
+
text: `SELECT id, key, type, is_public, owner_id, allowed_mime_types, max_file_size
|
|
144
145
|
FROM ${storageConfig.bucketsQualifiedName}
|
|
145
146
|
WHERE key = $1
|
|
146
|
-
LIMIT 1`,
|
|
147
|
+
LIMIT 1`,
|
|
148
|
+
values: [bucketKey],
|
|
149
|
+
});
|
|
147
150
|
if (result.rows.length === 0) {
|
|
148
151
|
return null;
|
|
149
152
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "graphile-presigned-url-plugin",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Presigned URL upload plugin for PostGraphile v5 — requestUploadUrl, confirmUpload mutations and downloadUrl computed field",
|
|
5
5
|
"author": "Constructive <developers@constructive.io>",
|
|
6
6
|
"homepage": "https://github.com/constructive-io/constructive",
|
|
@@ -59,5 +59,5 @@
|
|
|
59
59
|
"@types/node": "^22.19.11",
|
|
60
60
|
"makage": "^0.1.10"
|
|
61
61
|
},
|
|
62
|
-
"gitHead": "
|
|
62
|
+
"gitHead": "79cd3e66871804a22c672c7ca2fa5e2105d4b368"
|
|
63
63
|
}
|
package/plugin.js
CHANGED
|
@@ -52,7 +52,9 @@ function buildS3Key(contentHash) {
|
|
|
52
52
|
* metaschema query needed.
|
|
53
53
|
*/
|
|
54
54
|
async function resolveDatabaseId(pgClient) {
|
|
55
|
-
const result = await pgClient.query(
|
|
55
|
+
const result = await pgClient.query({
|
|
56
|
+
text: `SELECT jwt_private.current_database_id() AS id`,
|
|
57
|
+
});
|
|
56
58
|
return result.rows[0]?.id ?? null;
|
|
57
59
|
}
|
|
58
60
|
// --- Plugin factory ---
|
|
@@ -204,14 +206,13 @@ function createPresignedUrlPlugin(options) {
|
|
|
204
206
|
throw new Error('INVALID_CONTENT_TYPE');
|
|
205
207
|
}
|
|
206
208
|
return withPgClient(pgSettings, async (pgClient) => {
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
+
return pgClient.withTransaction(async (txClient) => {
|
|
209
210
|
// --- Resolve storage module config (all limits come from here) ---
|
|
210
|
-
const databaseId = await resolveDatabaseId(
|
|
211
|
+
const databaseId = await resolveDatabaseId(txClient);
|
|
211
212
|
if (!databaseId) {
|
|
212
213
|
throw new Error('DATABASE_NOT_FOUND');
|
|
213
214
|
}
|
|
214
|
-
const storageConfig = await (0, storage_module_cache_1.getStorageModuleConfig)(
|
|
215
|
+
const storageConfig = await (0, storage_module_cache_1.getStorageModuleConfig)(txClient, databaseId);
|
|
215
216
|
if (!storageConfig) {
|
|
216
217
|
throw new Error('STORAGE_MODULE_NOT_PROVISIONED');
|
|
217
218
|
}
|
|
@@ -225,7 +226,7 @@ function createPresignedUrlPlugin(options) {
|
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
228
|
// --- Look up the bucket (cached; first miss queries via RLS) ---
|
|
228
|
-
const bucket = await (0, storage_module_cache_1.getBucketConfig)(
|
|
229
|
+
const bucket = await (0, storage_module_cache_1.getBucketConfig)(txClient, storageConfig, databaseId, bucketKey);
|
|
229
230
|
if (!bucket) {
|
|
230
231
|
throw new Error('BUCKET_NOT_FOUND');
|
|
231
232
|
}
|
|
@@ -251,20 +252,25 @@ function createPresignedUrlPlugin(options) {
|
|
|
251
252
|
}
|
|
252
253
|
const s3Key = buildS3Key(contentHash);
|
|
253
254
|
// --- Dedup check: look for existing file with same content_hash in this bucket ---
|
|
254
|
-
const dedupResult = await
|
|
255
|
+
const dedupResult = await txClient.query({
|
|
256
|
+
text: `SELECT id, status
|
|
255
257
|
FROM ${storageConfig.filesQualifiedName}
|
|
256
258
|
WHERE content_hash = $1
|
|
257
259
|
AND bucket_id = $2
|
|
258
260
|
AND status IN ('ready', 'processed')
|
|
259
|
-
LIMIT 1`,
|
|
261
|
+
LIMIT 1`,
|
|
262
|
+
values: [contentHash, bucket.id],
|
|
263
|
+
});
|
|
260
264
|
if (dedupResult.rows.length > 0) {
|
|
261
265
|
const existingFile = dedupResult.rows[0];
|
|
262
266
|
log.info(`Dedup hit: file ${existingFile.id} for hash ${contentHash}`);
|
|
263
267
|
// Track the dedup request
|
|
264
|
-
await
|
|
268
|
+
await txClient.query({
|
|
269
|
+
text: `INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
|
|
265
270
|
(file_id, bucket_id, key, content_type, content_hash, size, status, expires_at)
|
|
266
|
-
VALUES ($1, $2, $3, $4, $5, $6, 'confirmed', NOW())`,
|
|
267
|
-
|
|
271
|
+
VALUES ($1, $2, $3, $4, $5, $6, 'confirmed', NOW())`,
|
|
272
|
+
values: [existingFile.id, bucket.id, s3Key, contentType, contentHash, size],
|
|
273
|
+
});
|
|
268
274
|
return {
|
|
269
275
|
uploadUrl: null,
|
|
270
276
|
fileId: existingFile.id,
|
|
@@ -274,19 +280,22 @@ function createPresignedUrlPlugin(options) {
|
|
|
274
280
|
};
|
|
275
281
|
}
|
|
276
282
|
// --- Create file record (status=pending) ---
|
|
277
|
-
const fileResult = await
|
|
283
|
+
const fileResult = await txClient.query({
|
|
284
|
+
text: `INSERT INTO ${storageConfig.filesQualifiedName}
|
|
278
285
|
(bucket_id, key, content_type, content_hash, size, filename, owner_id, is_public, status)
|
|
279
286
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'pending')
|
|
280
|
-
RETURNING id`,
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
287
|
+
RETURNING id`,
|
|
288
|
+
values: [
|
|
289
|
+
bucket.id,
|
|
290
|
+
s3Key,
|
|
291
|
+
contentType,
|
|
292
|
+
contentHash,
|
|
293
|
+
size,
|
|
294
|
+
filename || null,
|
|
295
|
+
bucket.owner_id,
|
|
296
|
+
bucket.is_public,
|
|
297
|
+
],
|
|
298
|
+
});
|
|
290
299
|
const fileId = fileResult.rows[0].id;
|
|
291
300
|
// --- Ensure the S3 bucket exists (lazy provisioning) ---
|
|
292
301
|
const s3ForDb = resolveS3ForDatabase(options, storageConfig, databaseId);
|
|
@@ -295,10 +304,12 @@ function createPresignedUrlPlugin(options) {
|
|
|
295
304
|
const uploadUrl = await (0, s3_signer_1.generatePresignedPutUrl)(s3ForDb, s3Key, contentType, size, storageConfig.uploadUrlExpirySeconds);
|
|
296
305
|
const expiresAt = new Date(Date.now() + storageConfig.uploadUrlExpirySeconds * 1000).toISOString();
|
|
297
306
|
// --- Track the upload request ---
|
|
298
|
-
await
|
|
307
|
+
await txClient.query({
|
|
308
|
+
text: `INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
|
|
299
309
|
(file_id, bucket_id, key, content_type, content_hash, size, status, expires_at)
|
|
300
|
-
VALUES ($1, $2, $3, $4, $5, $6, 'issued', $7)`,
|
|
301
|
-
|
|
310
|
+
VALUES ($1, $2, $3, $4, $5, $6, 'issued', $7)`,
|
|
311
|
+
values: [fileId, bucket.id, s3Key, contentType, contentHash, size, expiresAt],
|
|
312
|
+
});
|
|
302
313
|
return {
|
|
303
314
|
uploadUrl,
|
|
304
315
|
fileId,
|
|
@@ -306,11 +317,7 @@ function createPresignedUrlPlugin(options) {
|
|
|
306
317
|
deduplicated: false,
|
|
307
318
|
expiresAt,
|
|
308
319
|
};
|
|
309
|
-
}
|
|
310
|
-
catch (err) {
|
|
311
|
-
await pgClient.query('ROLLBACK');
|
|
312
|
-
throw err;
|
|
313
|
-
}
|
|
320
|
+
});
|
|
314
321
|
});
|
|
315
322
|
});
|
|
316
323
|
},
|
|
@@ -329,29 +336,30 @@ function createPresignedUrlPlugin(options) {
|
|
|
329
336
|
throw new Error('INVALID_FILE_ID');
|
|
330
337
|
}
|
|
331
338
|
return withPgClient(pgSettings, async (pgClient) => {
|
|
332
|
-
|
|
333
|
-
try {
|
|
339
|
+
return pgClient.withTransaction(async (txClient) => {
|
|
334
340
|
// --- Resolve storage module config ---
|
|
335
|
-
const databaseId = await resolveDatabaseId(
|
|
341
|
+
const databaseId = await resolveDatabaseId(txClient);
|
|
336
342
|
if (!databaseId) {
|
|
337
343
|
throw new Error('DATABASE_NOT_FOUND');
|
|
338
344
|
}
|
|
339
|
-
const storageConfig = await (0, storage_module_cache_1.getStorageModuleConfig)(
|
|
345
|
+
const storageConfig = await (0, storage_module_cache_1.getStorageModuleConfig)(txClient, databaseId);
|
|
340
346
|
if (!storageConfig) {
|
|
341
347
|
throw new Error('STORAGE_MODULE_NOT_PROVISIONED');
|
|
342
348
|
}
|
|
343
349
|
// --- Look up the file (RLS enforced) ---
|
|
344
|
-
const fileResult = await
|
|
350
|
+
const fileResult = await txClient.query({
|
|
351
|
+
text: `SELECT id, key, content_type, status, bucket_id
|
|
345
352
|
FROM ${storageConfig.filesQualifiedName}
|
|
346
353
|
WHERE id = $1
|
|
347
|
-
LIMIT 1`,
|
|
354
|
+
LIMIT 1`,
|
|
355
|
+
values: [fileId],
|
|
356
|
+
});
|
|
348
357
|
if (fileResult.rows.length === 0) {
|
|
349
358
|
throw new Error('FILE_NOT_FOUND');
|
|
350
359
|
}
|
|
351
360
|
const file = fileResult.rows[0];
|
|
352
361
|
if (file.status !== 'pending') {
|
|
353
362
|
// File is already confirmed or processed — idempotent success
|
|
354
|
-
await pgClient.query('COMMIT');
|
|
355
363
|
return {
|
|
356
364
|
fileId: file.id,
|
|
357
365
|
status: file.status,
|
|
@@ -367,31 +375,34 @@ function createPresignedUrlPlugin(options) {
|
|
|
367
375
|
// --- Content-type verification ---
|
|
368
376
|
if (s3Head.contentType && s3Head.contentType !== file.content_type) {
|
|
369
377
|
// Mark upload_request as rejected
|
|
370
|
-
await
|
|
378
|
+
await txClient.query({
|
|
379
|
+
text: `UPDATE ${storageConfig.uploadRequestsQualifiedName}
|
|
371
380
|
SET status = 'rejected'
|
|
372
|
-
WHERE file_id = $1 AND status = 'issued'`,
|
|
373
|
-
|
|
381
|
+
WHERE file_id = $1 AND status = 'issued'`,
|
|
382
|
+
values: [fileId],
|
|
383
|
+
});
|
|
374
384
|
throw new Error(`CONTENT_TYPE_MISMATCH: expected ${file.content_type}, got ${s3Head.contentType}`);
|
|
375
385
|
}
|
|
376
386
|
// --- Transition file to 'ready' ---
|
|
377
|
-
await
|
|
387
|
+
await txClient.query({
|
|
388
|
+
text: `UPDATE ${storageConfig.filesQualifiedName}
|
|
378
389
|
SET status = 'ready'
|
|
379
|
-
WHERE id = $1`,
|
|
390
|
+
WHERE id = $1`,
|
|
391
|
+
values: [fileId],
|
|
392
|
+
});
|
|
380
393
|
// --- Update upload_request to 'confirmed' ---
|
|
381
|
-
await
|
|
394
|
+
await txClient.query({
|
|
395
|
+
text: `UPDATE ${storageConfig.uploadRequestsQualifiedName}
|
|
382
396
|
SET status = 'confirmed', confirmed_at = NOW()
|
|
383
|
-
WHERE file_id = $1 AND status = 'issued'`,
|
|
384
|
-
|
|
397
|
+
WHERE file_id = $1 AND status = 'issued'`,
|
|
398
|
+
values: [fileId],
|
|
399
|
+
});
|
|
385
400
|
return {
|
|
386
401
|
fileId: file.id,
|
|
387
402
|
status: 'ready',
|
|
388
403
|
success: true,
|
|
389
404
|
};
|
|
390
|
-
}
|
|
391
|
-
catch (err) {
|
|
392
|
-
await pgClient.query('ROLLBACK');
|
|
393
|
-
throw err;
|
|
394
|
-
}
|
|
405
|
+
});
|
|
395
406
|
});
|
|
396
407
|
});
|
|
397
408
|
},
|
|
@@ -7,7 +7,10 @@ import type { StorageModuleConfig, BucketConfig } from './types';
|
|
|
7
7
|
* @returns StorageModuleConfig or null if no storage module is provisioned
|
|
8
8
|
*/
|
|
9
9
|
export declare function getStorageModuleConfig(pgClient: {
|
|
10
|
-
query: (
|
|
10
|
+
query: (opts: {
|
|
11
|
+
text: string;
|
|
12
|
+
values?: unknown[];
|
|
13
|
+
}) => Promise<{
|
|
11
14
|
rows: unknown[];
|
|
12
15
|
}>;
|
|
13
16
|
}, databaseId: string): Promise<StorageModuleConfig | null>;
|
|
@@ -24,7 +27,10 @@ export declare function getStorageModuleConfig(pgClient: {
|
|
|
24
27
|
* @returns BucketConfig or null if the bucket doesn't exist / isn't accessible
|
|
25
28
|
*/
|
|
26
29
|
export declare function getBucketConfig(pgClient: {
|
|
27
|
-
query: (
|
|
30
|
+
query: (opts: {
|
|
31
|
+
text: string;
|
|
32
|
+
values?: unknown[];
|
|
33
|
+
}) => Promise<{
|
|
28
34
|
rows: unknown[];
|
|
29
35
|
}>;
|
|
30
36
|
}, storageConfig: StorageModuleConfig, databaseId: string, bucketKey: string): Promise<BucketConfig | null>;
|
package/storage-module-cache.js
CHANGED
|
@@ -79,7 +79,7 @@ async function getStorageModuleConfig(pgClient, databaseId) {
|
|
|
79
79
|
return cached;
|
|
80
80
|
}
|
|
81
81
|
log.debug(`Cache miss for database ${databaseId}, querying metaschema...`);
|
|
82
|
-
const result = await pgClient.query(STORAGE_MODULE_QUERY, [databaseId]);
|
|
82
|
+
const result = await pgClient.query({ text: STORAGE_MODULE_QUERY, values: [databaseId] });
|
|
83
83
|
if (result.rows.length === 0) {
|
|
84
84
|
log.warn(`No storage module found for database ${databaseId}`);
|
|
85
85
|
return null;
|
|
@@ -148,10 +148,13 @@ async function getBucketConfig(pgClient, storageConfig, databaseId, bucketKey) {
|
|
|
148
148
|
return cached;
|
|
149
149
|
}
|
|
150
150
|
log.debug(`Bucket cache miss for ${databaseId}:${bucketKey}, querying DB...`);
|
|
151
|
-
const result = await pgClient.query(
|
|
151
|
+
const result = await pgClient.query({
|
|
152
|
+
text: `SELECT id, key, type, is_public, owner_id, allowed_mime_types, max_file_size
|
|
152
153
|
FROM ${storageConfig.bucketsQualifiedName}
|
|
153
154
|
WHERE key = $1
|
|
154
|
-
LIMIT 1`,
|
|
155
|
+
LIMIT 1`,
|
|
156
|
+
values: [bucketKey],
|
|
157
|
+
});
|
|
155
158
|
if (result.rows.length === 0) {
|
|
156
159
|
return null;
|
|
157
160
|
}
|