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.
@@ -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(`SELECT jwt_private.current_database_id() AS id`);
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(`SELECT jwt_private.current_database_id() AS id`);
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(`SELECT jwt_private.current_database_id() AS id`);
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
- await pgClient.query('BEGIN');
204
- try {
205
+ return pgClient.withTransaction(async (txClient) => {
205
206
  // --- Resolve storage module config (all limits come from here) ---
206
- const databaseId = await resolveDatabaseId(pgClient);
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(pgClient, databaseId);
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(pgClient, storageConfig, databaseId, bucketKey);
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 pgClient.query(`SELECT id, status
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`, [contentHash, bucket.id]);
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 pgClient.query(`INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
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())`, [existingFile.id, bucket.id, s3Key, contentType, contentHash, size]);
263
- await pgClient.query('COMMIT');
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 pgClient.query(`INSERT INTO ${storageConfig.filesQualifiedName}
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
- bucket.id,
278
- s3Key,
279
- contentType,
280
- contentHash,
281
- size,
282
- filename || null,
283
- bucket.owner_id,
284
- bucket.is_public,
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 pgClient.query(`INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
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)`, [fileId, bucket.id, s3Key, contentType, contentHash, size, expiresAt]);
297
- await pgClient.query('COMMIT');
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
- await pgClient.query('BEGIN');
329
- try {
335
+ return pgClient.withTransaction(async (txClient) => {
330
336
  // --- Resolve storage module config ---
331
- const databaseId = await resolveDatabaseId(pgClient);
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(pgClient, databaseId);
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 pgClient.query(`SELECT id, key, content_type, status, bucket_id
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`, [fileId]);
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 pgClient.query(`UPDATE ${storageConfig.uploadRequestsQualifiedName}
374
+ await txClient.query({
375
+ text: `UPDATE ${storageConfig.uploadRequestsQualifiedName}
367
376
  SET status = 'rejected'
368
- WHERE file_id = $1 AND status = 'issued'`, [fileId]);
369
- await pgClient.query('COMMIT');
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 pgClient.query(`UPDATE ${storageConfig.filesQualifiedName}
383
+ await txClient.query({
384
+ text: `UPDATE ${storageConfig.filesQualifiedName}
374
385
  SET status = 'ready'
375
- WHERE id = $1`, [fileId]);
386
+ WHERE id = $1`,
387
+ values: [fileId],
388
+ });
376
389
  // --- Update upload_request to 'confirmed' ---
377
- await pgClient.query(`UPDATE ${storageConfig.uploadRequestsQualifiedName}
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'`, [fileId]);
380
- await pgClient.query('COMMIT');
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: (sql: string, params: unknown[]) => Promise<{
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: (sql: string, params: unknown[]) => Promise<{
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(`SELECT id, key, type, is_public, owner_id, allowed_mime_types, max_file_size
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`, [bucketKey]);
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.0",
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": "3bf7c522cf9f9d2595750ac7cea81d470b3e6c30"
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(`SELECT jwt_private.current_database_id() AS id`);
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
- await pgClient.query('BEGIN');
208
- try {
209
+ return pgClient.withTransaction(async (txClient) => {
209
210
  // --- Resolve storage module config (all limits come from here) ---
210
- const databaseId = await resolveDatabaseId(pgClient);
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)(pgClient, databaseId);
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)(pgClient, storageConfig, databaseId, bucketKey);
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 pgClient.query(`SELECT id, status
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`, [contentHash, bucket.id]);
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 pgClient.query(`INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
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())`, [existingFile.id, bucket.id, s3Key, contentType, contentHash, size]);
267
- await pgClient.query('COMMIT');
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 pgClient.query(`INSERT INTO ${storageConfig.filesQualifiedName}
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
- bucket.id,
282
- s3Key,
283
- contentType,
284
- contentHash,
285
- size,
286
- filename || null,
287
- bucket.owner_id,
288
- bucket.is_public,
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 pgClient.query(`INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
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)`, [fileId, bucket.id, s3Key, contentType, contentHash, size, expiresAt]);
301
- await pgClient.query('COMMIT');
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
- await pgClient.query('BEGIN');
333
- try {
339
+ return pgClient.withTransaction(async (txClient) => {
334
340
  // --- Resolve storage module config ---
335
- const databaseId = await resolveDatabaseId(pgClient);
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)(pgClient, databaseId);
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 pgClient.query(`SELECT id, key, content_type, status, bucket_id
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`, [fileId]);
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 pgClient.query(`UPDATE ${storageConfig.uploadRequestsQualifiedName}
378
+ await txClient.query({
379
+ text: `UPDATE ${storageConfig.uploadRequestsQualifiedName}
371
380
  SET status = 'rejected'
372
- WHERE file_id = $1 AND status = 'issued'`, [fileId]);
373
- await pgClient.query('COMMIT');
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 pgClient.query(`UPDATE ${storageConfig.filesQualifiedName}
387
+ await txClient.query({
388
+ text: `UPDATE ${storageConfig.filesQualifiedName}
378
389
  SET status = 'ready'
379
- WHERE id = $1`, [fileId]);
390
+ WHERE id = $1`,
391
+ values: [fileId],
392
+ });
380
393
  // --- Update upload_request to 'confirmed' ---
381
- await pgClient.query(`UPDATE ${storageConfig.uploadRequestsQualifiedName}
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'`, [fileId]);
384
- await pgClient.query('COMMIT');
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: (sql: string, params: unknown[]) => Promise<{
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: (sql: string, params: unknown[]) => Promise<{
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>;
@@ -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(`SELECT id, key, type, is_public, owner_id, allowed_mime_types, max_file_size
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`, [bucketKey]);
155
+ LIMIT 1`,
156
+ values: [bucketKey],
157
+ });
155
158
  if (result.rows.length === 0) {
156
159
  return null;
157
160
  }