graphile-presigned-url-plugin 0.21.1 → 0.22.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 +23 -20
- package/esm/download-url-field.js +23 -20
- package/esm/plugin.js +69 -62
- package/package.json +4 -4
- package/plugin.js +69 -62
package/download-url-field.js
CHANGED
|
@@ -115,30 +115,33 @@ function createDownloadUrlPlugin(options) {
|
|
|
115
115
|
let downloadUrlExpirySeconds = 3600;
|
|
116
116
|
try {
|
|
117
117
|
if (withPgClient && pgSettings) {
|
|
118
|
-
const
|
|
118
|
+
const databaseId = await withPgClient(pgSettings, async (pgClient) => {
|
|
119
119
|
const dbResult = await pgClient.query({
|
|
120
120
|
text: `SELECT jwt_private.current_database_id() AS id`,
|
|
121
121
|
});
|
|
122
|
-
|
|
123
|
-
if (!databaseId)
|
|
124
|
-
return null;
|
|
125
|
-
const allConfigs = await (0, storage_module_cache_1.loadAllStorageModules)(pgClient, databaseId);
|
|
126
|
-
const config = (0, storage_module_cache_1.resolveStorageConfigFromCodec)(capturedCodec, allConfigs);
|
|
127
|
-
if (!config)
|
|
128
|
-
return null;
|
|
129
|
-
// Look up the bucket key for scoped S3 resolution
|
|
130
|
-
let bucketKey = 'public';
|
|
131
|
-
if (bucketId) {
|
|
132
|
-
const bucketResult = await pgClient.query({
|
|
133
|
-
text: `SELECT key FROM ${config.bucketsQualifiedName} WHERE id = $1 LIMIT 1`,
|
|
134
|
-
values: [bucketId],
|
|
135
|
-
});
|
|
136
|
-
if (bucketResult.rows[0]?.key) {
|
|
137
|
-
bucketKey = bucketResult.rows[0].key;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return { config, databaseId, bucketKey };
|
|
122
|
+
return dbResult.rows[0]?.id ?? null;
|
|
141
123
|
});
|
|
124
|
+
// Module registration is server config, not user data:
|
|
125
|
+
// resolve it without the request role's pgSettings.
|
|
126
|
+
const config = databaseId
|
|
127
|
+
? (0, storage_module_cache_1.resolveStorageConfigFromCodec)(capturedCodec, await withPgClient(null, (pgClient) => (0, storage_module_cache_1.loadAllStorageModules)(pgClient, databaseId)))
|
|
128
|
+
: null;
|
|
129
|
+
const resolved = config
|
|
130
|
+
? await withPgClient(pgSettings, async (pgClient) => {
|
|
131
|
+
// Look up the bucket key for scoped S3 resolution
|
|
132
|
+
let bucketKey = 'public';
|
|
133
|
+
if (bucketId) {
|
|
134
|
+
const bucketResult = await pgClient.query({
|
|
135
|
+
text: `SELECT key FROM ${config.bucketsQualifiedName} WHERE id = $1 LIMIT 1`,
|
|
136
|
+
values: [bucketId],
|
|
137
|
+
});
|
|
138
|
+
if (bucketResult.rows[0]?.key) {
|
|
139
|
+
bucketKey = bucketResult.rows[0].key;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { config, databaseId, bucketKey };
|
|
143
|
+
})
|
|
144
|
+
: null;
|
|
142
145
|
if (resolved) {
|
|
143
146
|
downloadUrlExpirySeconds = resolved.config.downloadUrlExpirySeconds;
|
|
144
147
|
s3ForDb = resolveS3ForDatabase(options, resolved.config, resolved.databaseId, resolved.bucketKey);
|
|
@@ -112,30 +112,33 @@ export function createDownloadUrlPlugin(options) {
|
|
|
112
112
|
let downloadUrlExpirySeconds = 3600;
|
|
113
113
|
try {
|
|
114
114
|
if (withPgClient && pgSettings) {
|
|
115
|
-
const
|
|
115
|
+
const databaseId = await withPgClient(pgSettings, async (pgClient) => {
|
|
116
116
|
const dbResult = await pgClient.query({
|
|
117
117
|
text: `SELECT jwt_private.current_database_id() AS id`,
|
|
118
118
|
});
|
|
119
|
-
|
|
120
|
-
if (!databaseId)
|
|
121
|
-
return null;
|
|
122
|
-
const allConfigs = await loadAllStorageModules(pgClient, databaseId);
|
|
123
|
-
const config = resolveStorageConfigFromCodec(capturedCodec, allConfigs);
|
|
124
|
-
if (!config)
|
|
125
|
-
return null;
|
|
126
|
-
// Look up the bucket key for scoped S3 resolution
|
|
127
|
-
let bucketKey = 'public';
|
|
128
|
-
if (bucketId) {
|
|
129
|
-
const bucketResult = await pgClient.query({
|
|
130
|
-
text: `SELECT key FROM ${config.bucketsQualifiedName} WHERE id = $1 LIMIT 1`,
|
|
131
|
-
values: [bucketId],
|
|
132
|
-
});
|
|
133
|
-
if (bucketResult.rows[0]?.key) {
|
|
134
|
-
bucketKey = bucketResult.rows[0].key;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return { config, databaseId, bucketKey };
|
|
119
|
+
return dbResult.rows[0]?.id ?? null;
|
|
138
120
|
});
|
|
121
|
+
// Module registration is server config, not user data:
|
|
122
|
+
// resolve it without the request role's pgSettings.
|
|
123
|
+
const config = databaseId
|
|
124
|
+
? resolveStorageConfigFromCodec(capturedCodec, await withPgClient(null, (pgClient) => loadAllStorageModules(pgClient, databaseId)))
|
|
125
|
+
: null;
|
|
126
|
+
const resolved = config
|
|
127
|
+
? await withPgClient(pgSettings, async (pgClient) => {
|
|
128
|
+
// Look up the bucket key for scoped S3 resolution
|
|
129
|
+
let bucketKey = 'public';
|
|
130
|
+
if (bucketId) {
|
|
131
|
+
const bucketResult = await pgClient.query({
|
|
132
|
+
text: `SELECT key FROM ${config.bucketsQualifiedName} WHERE id = $1 LIMIT 1`,
|
|
133
|
+
values: [bucketId],
|
|
134
|
+
});
|
|
135
|
+
if (bucketResult.rows[0]?.key) {
|
|
136
|
+
bucketKey = bucketResult.rows[0].key;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { config, databaseId, bucketKey };
|
|
140
|
+
})
|
|
141
|
+
: null;
|
|
139
142
|
if (resolved) {
|
|
140
143
|
downloadUrlExpirySeconds = resolved.config.downloadUrlExpirySeconds;
|
|
141
144
|
s3ForDb = resolveS3ForDatabase(options, resolved.config, resolved.databaseId, resolved.bucketKey);
|
package/esm/plugin.js
CHANGED
|
@@ -202,15 +202,17 @@ export function createPresignedUrlPlugin(options) {
|
|
|
202
202
|
pgSettings: $pgSettings,
|
|
203
203
|
});
|
|
204
204
|
return lambda($combined, async (vals) => {
|
|
205
|
+
const databaseId = await vals.withPgClient(vals.pgSettings, (pgClient) => resolveDatabaseId(pgClient));
|
|
206
|
+
if (!databaseId)
|
|
207
|
+
throw new Error('DATABASE_NOT_FOUND');
|
|
208
|
+
// Module registration is server config, not user data:
|
|
209
|
+
// resolve it without the request role's pgSettings.
|
|
210
|
+
const allConfigs = await vals.withPgClient(null, (pgClient) => loadAllStorageModules(pgClient, databaseId));
|
|
211
|
+
const storageConfig = resolveStorageConfigFromCodec(capturedFilesCodec, allConfigs);
|
|
212
|
+
if (!storageConfig)
|
|
213
|
+
throw new Error('STORAGE_MODULE_NOT_FOUND');
|
|
205
214
|
return vals.withPgClient(vals.pgSettings, async (pgClient) => {
|
|
206
215
|
return pgClient.withTransaction(async (txClient) => {
|
|
207
|
-
const databaseId = await resolveDatabaseId(txClient);
|
|
208
|
-
if (!databaseId)
|
|
209
|
-
throw new Error('DATABASE_NOT_FOUND');
|
|
210
|
-
const allConfigs = await loadAllStorageModules(txClient, databaseId);
|
|
211
|
-
const storageConfig = resolveStorageConfigFromCodec(capturedFilesCodec, allConfigs);
|
|
212
|
-
if (!storageConfig)
|
|
213
|
-
throw new Error('STORAGE_MODULE_NOT_FOUND');
|
|
214
216
|
const bucket = await getBucketConfig(txClient, storageConfig, databaseId, vals.bucketKey, vals.ownerId || undefined);
|
|
215
217
|
if (!bucket)
|
|
216
218
|
throw new Error('BUCKET_NOT_FOUND');
|
|
@@ -289,15 +291,17 @@ export function createPresignedUrlPlugin(options) {
|
|
|
289
291
|
pgSettings: $pgSettings,
|
|
290
292
|
});
|
|
291
293
|
return lambda($combined, async (vals) => {
|
|
294
|
+
const databaseId = await vals.withPgClient(vals.pgSettings, (pgClient) => resolveDatabaseId(pgClient));
|
|
295
|
+
if (!databaseId)
|
|
296
|
+
throw new Error('DATABASE_NOT_FOUND');
|
|
297
|
+
// Module registration is server config, not user data:
|
|
298
|
+
// resolve it without the request role's pgSettings.
|
|
299
|
+
const allConfigs = await vals.withPgClient(null, (pgClient) => loadAllStorageModules(pgClient, databaseId));
|
|
300
|
+
const storageConfig = resolveStorageConfigFromCodec(capturedFilesCodec, allConfigs);
|
|
301
|
+
if (!storageConfig)
|
|
302
|
+
throw new Error('STORAGE_MODULE_NOT_FOUND');
|
|
292
303
|
return vals.withPgClient(vals.pgSettings, async (pgClient) => {
|
|
293
304
|
return pgClient.withTransaction(async (txClient) => {
|
|
294
|
-
const databaseId = await resolveDatabaseId(txClient);
|
|
295
|
-
if (!databaseId)
|
|
296
|
-
throw new Error('DATABASE_NOT_FOUND');
|
|
297
|
-
const allConfigs = await loadAllStorageModules(txClient, databaseId);
|
|
298
|
-
const storageConfig = resolveStorageConfigFromCodec(capturedFilesCodec, allConfigs);
|
|
299
|
-
if (!storageConfig)
|
|
300
|
-
throw new Error('STORAGE_MODULE_NOT_FOUND');
|
|
301
305
|
const bucket = await getBucketConfig(txClient, storageConfig, databaseId, vals.bucketKey, vals.ownerId || undefined);
|
|
302
306
|
if (!bucket)
|
|
303
307
|
throw new Error('BUCKET_NOT_FOUND');
|
|
@@ -369,23 +373,25 @@ export function createPresignedUrlPlugin(options) {
|
|
|
369
373
|
const pgSettings = graphqlContext.pgSettings;
|
|
370
374
|
if (withPgClient) {
|
|
371
375
|
try {
|
|
372
|
-
await withPgClient(pgSettings,
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
376
|
+
const databaseId = await withPgClient(pgSettings, (pgClient) => resolveDatabaseId(pgClient));
|
|
377
|
+
// Module registration is server config, not user data:
|
|
378
|
+
// resolve it without the request role's pgSettings.
|
|
379
|
+
const allConfigs = databaseId
|
|
380
|
+
? await withPgClient(null, (pgClient) => loadAllStorageModules(pgClient, databaseId))
|
|
381
|
+
: [];
|
|
382
|
+
const storageConfig = resolveStorageConfigFromCodec(capturedCodec, allConfigs);
|
|
383
|
+
if (storageConfig) {
|
|
384
|
+
await withPgClient(pgSettings, async (pgClient) => {
|
|
385
|
+
// Read the file row (RLS enforced)
|
|
386
|
+
const result = await pgClient.query({
|
|
387
|
+
text: `SELECT key, bucket_id FROM ${storageConfig.filesQualifiedName} WHERE id = $1 LIMIT 1`,
|
|
388
|
+
values: [fileInput],
|
|
389
|
+
});
|
|
390
|
+
if (result.rows.length > 0) {
|
|
391
|
+
fileRow = result.rows[0];
|
|
392
|
+
}
|
|
384
393
|
});
|
|
385
|
-
|
|
386
|
-
fileRow = result.rows[0];
|
|
387
|
-
}
|
|
388
|
-
});
|
|
394
|
+
}
|
|
389
395
|
}
|
|
390
396
|
catch (err) {
|
|
391
397
|
log.warn(`Pre-delete file lookup failed: ${err.message}`);
|
|
@@ -400,39 +406,40 @@ export function createPresignedUrlPlugin(options) {
|
|
|
400
406
|
const pgSettings = graphqlContext.pgSettings;
|
|
401
407
|
if (withPgClient) {
|
|
402
408
|
try {
|
|
403
|
-
await withPgClient(pgSettings,
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
409
|
+
const databaseId = await withPgClient(pgSettings, (pgClient) => resolveDatabaseId(pgClient));
|
|
410
|
+
// Module registration is server config, not user data:
|
|
411
|
+
// resolve it without the request role's pgSettings.
|
|
412
|
+
const allConfigs = databaseId
|
|
413
|
+
? await withPgClient(null, (pgClient) => loadAllStorageModules(pgClient, databaseId))
|
|
414
|
+
: [];
|
|
415
|
+
const storageConfig = resolveStorageConfigFromCodec(capturedCodec, allConfigs);
|
|
416
|
+
if (storageConfig)
|
|
417
|
+
await withPgClient(pgSettings, async (pgClient) => {
|
|
418
|
+
// Check refcount: any other file with the same key in this bucket?
|
|
419
|
+
const refResult = await pgClient.query({
|
|
420
|
+
text: `SELECT COUNT(*)::int AS ref_count FROM ${storageConfig.filesQualifiedName} WHERE key = $1 AND bucket_id = $2`,
|
|
421
|
+
values: [fileRow.key, fileRow.bucket_id],
|
|
422
|
+
});
|
|
423
|
+
const refCount = refResult.rows[0]?.ref_count ?? 0;
|
|
424
|
+
if (refCount > 0) {
|
|
425
|
+
log.info(`File deleted from DB; S3 key ${fileRow.key} still referenced by ${refCount} file(s)`);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
// No other references — attempt sync S3 delete
|
|
429
|
+
// Look up the bucket key for scoped S3 resolution
|
|
430
|
+
const bucketResult = await pgClient.query({
|
|
431
|
+
text: `SELECT key FROM ${storageConfig.bucketsQualifiedName} WHERE id = $1 LIMIT 1`,
|
|
432
|
+
values: [fileRow.bucket_id],
|
|
433
|
+
});
|
|
434
|
+
const bucketKey = bucketResult.rows[0]?.key;
|
|
435
|
+
if (!bucketKey) {
|
|
436
|
+
log.warn(`Bucket not found for bucket_id=${fileRow.bucket_id}; skipping S3 delete`);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const s3ForDb = resolveS3ForDatabase(options, storageConfig, databaseId, bucketKey);
|
|
440
|
+
await deleteS3Object(s3ForDb, fileRow.key);
|
|
441
|
+
log.info(`Sync S3 delete succeeded for key=${fileRow.key}`);
|
|
426
442
|
});
|
|
427
|
-
const bucketKey = bucketResult.rows[0]?.key;
|
|
428
|
-
if (!bucketKey) {
|
|
429
|
-
log.warn(`Bucket not found for bucket_id=${fileRow.bucket_id}; skipping S3 delete`);
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
const s3ForDb = resolveS3ForDatabase(options, storageConfig, databaseId, bucketKey);
|
|
433
|
-
await deleteS3Object(s3ForDb, fileRow.key);
|
|
434
|
-
log.info(`Sync S3 delete succeeded for key=${fileRow.key}`);
|
|
435
|
-
});
|
|
436
443
|
}
|
|
437
444
|
catch (err) {
|
|
438
445
|
// Sync S3 delete failed — the AFTER DELETE trigger has enqueued an async GC job
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "graphile-presigned-url-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"description": "Presigned URL upload plugin for PostGraphile v5 — requestUploadUrl mutation and downloadUrl computed field",
|
|
5
5
|
"author": "Constructive <developers@constructive.io>",
|
|
6
6
|
"homepage": "https://github.com/constructive-io/constructive",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@aws-sdk/client-s3": "^3.1052.0",
|
|
44
44
|
"@aws-sdk/s3-request-presigner": "^3.1052.0",
|
|
45
|
-
"@pgpmjs/logger": "^2.
|
|
45
|
+
"@pgpmjs/logger": "^2.14.0",
|
|
46
46
|
"@pgsql/quotes": "^17.1.0",
|
|
47
47
|
"lru-cache": "^11.2.7"
|
|
48
48
|
},
|
|
@@ -56,9 +56,9 @@
|
|
|
56
56
|
"postgraphile": "5.0.3"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@constructive-io/s3-utils": "^2.
|
|
59
|
+
"@constructive-io/s3-utils": "^2.20.0",
|
|
60
60
|
"@types/node": "^22.19.11",
|
|
61
61
|
"makage": "^0.3.0"
|
|
62
62
|
},
|
|
63
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "dd2accb683fe20c7278a0d58fd36a04a3b07ac63"
|
|
64
64
|
}
|
package/plugin.js
CHANGED
|
@@ -206,15 +206,17 @@ function createPresignedUrlPlugin(options) {
|
|
|
206
206
|
pgSettings: $pgSettings,
|
|
207
207
|
});
|
|
208
208
|
return (0, grafast_1.lambda)($combined, async (vals) => {
|
|
209
|
+
const databaseId = await vals.withPgClient(vals.pgSettings, (pgClient) => resolveDatabaseId(pgClient));
|
|
210
|
+
if (!databaseId)
|
|
211
|
+
throw new Error('DATABASE_NOT_FOUND');
|
|
212
|
+
// Module registration is server config, not user data:
|
|
213
|
+
// resolve it without the request role's pgSettings.
|
|
214
|
+
const allConfigs = await vals.withPgClient(null, (pgClient) => (0, storage_module_cache_1.loadAllStorageModules)(pgClient, databaseId));
|
|
215
|
+
const storageConfig = (0, storage_module_cache_1.resolveStorageConfigFromCodec)(capturedFilesCodec, allConfigs);
|
|
216
|
+
if (!storageConfig)
|
|
217
|
+
throw new Error('STORAGE_MODULE_NOT_FOUND');
|
|
209
218
|
return vals.withPgClient(vals.pgSettings, async (pgClient) => {
|
|
210
219
|
return pgClient.withTransaction(async (txClient) => {
|
|
211
|
-
const databaseId = await resolveDatabaseId(txClient);
|
|
212
|
-
if (!databaseId)
|
|
213
|
-
throw new Error('DATABASE_NOT_FOUND');
|
|
214
|
-
const allConfigs = await (0, storage_module_cache_1.loadAllStorageModules)(txClient, databaseId);
|
|
215
|
-
const storageConfig = (0, storage_module_cache_1.resolveStorageConfigFromCodec)(capturedFilesCodec, allConfigs);
|
|
216
|
-
if (!storageConfig)
|
|
217
|
-
throw new Error('STORAGE_MODULE_NOT_FOUND');
|
|
218
220
|
const bucket = await (0, storage_module_cache_1.getBucketConfig)(txClient, storageConfig, databaseId, vals.bucketKey, vals.ownerId || undefined);
|
|
219
221
|
if (!bucket)
|
|
220
222
|
throw new Error('BUCKET_NOT_FOUND');
|
|
@@ -293,15 +295,17 @@ function createPresignedUrlPlugin(options) {
|
|
|
293
295
|
pgSettings: $pgSettings,
|
|
294
296
|
});
|
|
295
297
|
return (0, grafast_1.lambda)($combined, async (vals) => {
|
|
298
|
+
const databaseId = await vals.withPgClient(vals.pgSettings, (pgClient) => resolveDatabaseId(pgClient));
|
|
299
|
+
if (!databaseId)
|
|
300
|
+
throw new Error('DATABASE_NOT_FOUND');
|
|
301
|
+
// Module registration is server config, not user data:
|
|
302
|
+
// resolve it without the request role's pgSettings.
|
|
303
|
+
const allConfigs = await vals.withPgClient(null, (pgClient) => (0, storage_module_cache_1.loadAllStorageModules)(pgClient, databaseId));
|
|
304
|
+
const storageConfig = (0, storage_module_cache_1.resolveStorageConfigFromCodec)(capturedFilesCodec, allConfigs);
|
|
305
|
+
if (!storageConfig)
|
|
306
|
+
throw new Error('STORAGE_MODULE_NOT_FOUND');
|
|
296
307
|
return vals.withPgClient(vals.pgSettings, async (pgClient) => {
|
|
297
308
|
return pgClient.withTransaction(async (txClient) => {
|
|
298
|
-
const databaseId = await resolveDatabaseId(txClient);
|
|
299
|
-
if (!databaseId)
|
|
300
|
-
throw new Error('DATABASE_NOT_FOUND');
|
|
301
|
-
const allConfigs = await (0, storage_module_cache_1.loadAllStorageModules)(txClient, databaseId);
|
|
302
|
-
const storageConfig = (0, storage_module_cache_1.resolveStorageConfigFromCodec)(capturedFilesCodec, allConfigs);
|
|
303
|
-
if (!storageConfig)
|
|
304
|
-
throw new Error('STORAGE_MODULE_NOT_FOUND');
|
|
305
309
|
const bucket = await (0, storage_module_cache_1.getBucketConfig)(txClient, storageConfig, databaseId, vals.bucketKey, vals.ownerId || undefined);
|
|
306
310
|
if (!bucket)
|
|
307
311
|
throw new Error('BUCKET_NOT_FOUND');
|
|
@@ -373,23 +377,25 @@ function createPresignedUrlPlugin(options) {
|
|
|
373
377
|
const pgSettings = graphqlContext.pgSettings;
|
|
374
378
|
if (withPgClient) {
|
|
375
379
|
try {
|
|
376
|
-
await withPgClient(pgSettings,
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
380
|
+
const databaseId = await withPgClient(pgSettings, (pgClient) => resolveDatabaseId(pgClient));
|
|
381
|
+
// Module registration is server config, not user data:
|
|
382
|
+
// resolve it without the request role's pgSettings.
|
|
383
|
+
const allConfigs = databaseId
|
|
384
|
+
? await withPgClient(null, (pgClient) => (0, storage_module_cache_1.loadAllStorageModules)(pgClient, databaseId))
|
|
385
|
+
: [];
|
|
386
|
+
const storageConfig = (0, storage_module_cache_1.resolveStorageConfigFromCodec)(capturedCodec, allConfigs);
|
|
387
|
+
if (storageConfig) {
|
|
388
|
+
await withPgClient(pgSettings, async (pgClient) => {
|
|
389
|
+
// Read the file row (RLS enforced)
|
|
390
|
+
const result = await pgClient.query({
|
|
391
|
+
text: `SELECT key, bucket_id FROM ${storageConfig.filesQualifiedName} WHERE id = $1 LIMIT 1`,
|
|
392
|
+
values: [fileInput],
|
|
393
|
+
});
|
|
394
|
+
if (result.rows.length > 0) {
|
|
395
|
+
fileRow = result.rows[0];
|
|
396
|
+
}
|
|
388
397
|
});
|
|
389
|
-
|
|
390
|
-
fileRow = result.rows[0];
|
|
391
|
-
}
|
|
392
|
-
});
|
|
398
|
+
}
|
|
393
399
|
}
|
|
394
400
|
catch (err) {
|
|
395
401
|
log.warn(`Pre-delete file lookup failed: ${err.message}`);
|
|
@@ -404,39 +410,40 @@ function createPresignedUrlPlugin(options) {
|
|
|
404
410
|
const pgSettings = graphqlContext.pgSettings;
|
|
405
411
|
if (withPgClient) {
|
|
406
412
|
try {
|
|
407
|
-
await withPgClient(pgSettings,
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
413
|
+
const databaseId = await withPgClient(pgSettings, (pgClient) => resolveDatabaseId(pgClient));
|
|
414
|
+
// Module registration is server config, not user data:
|
|
415
|
+
// resolve it without the request role's pgSettings.
|
|
416
|
+
const allConfigs = databaseId
|
|
417
|
+
? await withPgClient(null, (pgClient) => (0, storage_module_cache_1.loadAllStorageModules)(pgClient, databaseId))
|
|
418
|
+
: [];
|
|
419
|
+
const storageConfig = (0, storage_module_cache_1.resolveStorageConfigFromCodec)(capturedCodec, allConfigs);
|
|
420
|
+
if (storageConfig)
|
|
421
|
+
await withPgClient(pgSettings, async (pgClient) => {
|
|
422
|
+
// Check refcount: any other file with the same key in this bucket?
|
|
423
|
+
const refResult = await pgClient.query({
|
|
424
|
+
text: `SELECT COUNT(*)::int AS ref_count FROM ${storageConfig.filesQualifiedName} WHERE key = $1 AND bucket_id = $2`,
|
|
425
|
+
values: [fileRow.key, fileRow.bucket_id],
|
|
426
|
+
});
|
|
427
|
+
const refCount = refResult.rows[0]?.ref_count ?? 0;
|
|
428
|
+
if (refCount > 0) {
|
|
429
|
+
log.info(`File deleted from DB; S3 key ${fileRow.key} still referenced by ${refCount} file(s)`);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
// No other references — attempt sync S3 delete
|
|
433
|
+
// Look up the bucket key for scoped S3 resolution
|
|
434
|
+
const bucketResult = await pgClient.query({
|
|
435
|
+
text: `SELECT key FROM ${storageConfig.bucketsQualifiedName} WHERE id = $1 LIMIT 1`,
|
|
436
|
+
values: [fileRow.bucket_id],
|
|
437
|
+
});
|
|
438
|
+
const bucketKey = bucketResult.rows[0]?.key;
|
|
439
|
+
if (!bucketKey) {
|
|
440
|
+
log.warn(`Bucket not found for bucket_id=${fileRow.bucket_id}; skipping S3 delete`);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const s3ForDb = resolveS3ForDatabase(options, storageConfig, databaseId, bucketKey);
|
|
444
|
+
await (0, s3_signer_1.deleteS3Object)(s3ForDb, fileRow.key);
|
|
445
|
+
log.info(`Sync S3 delete succeeded for key=${fileRow.key}`);
|
|
430
446
|
});
|
|
431
|
-
const bucketKey = bucketResult.rows[0]?.key;
|
|
432
|
-
if (!bucketKey) {
|
|
433
|
-
log.warn(`Bucket not found for bucket_id=${fileRow.bucket_id}; skipping S3 delete`);
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
const s3ForDb = resolveS3ForDatabase(options, storageConfig, databaseId, bucketKey);
|
|
437
|
-
await (0, s3_signer_1.deleteS3Object)(s3ForDb, fileRow.key);
|
|
438
|
-
log.info(`Sync S3 delete succeeded for key=${fileRow.key}`);
|
|
439
|
-
});
|
|
440
447
|
}
|
|
441
448
|
catch (err) {
|
|
442
449
|
// Sync S3 delete failed — the AFTER DELETE trigger has enqueued an async GC job
|