Skip to content

Commit 1a55e6a

Browse files
committed
fix: use @dataplan/pg query object format and withTransaction for presigned URL plugin
The @dataplan/pg adapter wraps pgClient with a custom query method that expects { text, values } objects, not raw SQL strings. The plugin was calling pgClient.query('BEGIN') etc. with raw strings, causing 'text' to be undefined when destructured by the adapter. Changes: - Convert all pgClient.query(string, params) calls to pgClient.query({ text: string, values: params }) format - Replace manual BEGIN/COMMIT/ROLLBACK with pgClient.withTransaction() since the adapter already manages transactions when pgSettings are present - Update storage-module-cache.ts and download-url-field.ts similarly
1 parent 5209d43 commit 1a55e6a

3 files changed

Lines changed: 54 additions & 67 deletions

File tree

graphile/graphile-presigned-url-plugin/src/download-url-field.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,9 @@ export function createDownloadUrlPlugin(
135135
: null;
136136
if (withPgClient) {
137137
const resolved = await withPgClient(null, async (pgClient: any) => {
138-
const dbResult = await pgClient.query(
139-
`SELECT jwt_private.current_database_id() AS id`,
140-
);
138+
const dbResult = await pgClient.query({
139+
text: `SELECT jwt_private.current_database_id() AS id`,
140+
});
141141
const databaseId = dbResult.rows[0]?.id;
142142
if (!databaseId) return null;
143143
const config = await getStorageModuleConfig(pgClient, databaseId);

graphile/graphile-presigned-url-plugin/src/plugin.ts

Lines changed: 44 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ function buildS3Key(contentHash: string): string {
5959
* metaschema query needed.
6060
*/
6161
async function resolveDatabaseId(pgClient: any): Promise<string | null> {
62-
const result = await pgClient.query(
63-
`SELECT jwt_private.current_database_id() AS id`,
64-
);
62+
const result = await pgClient.query({
63+
text: `SELECT jwt_private.current_database_id() AS id`,
64+
});
6565
return result.rows[0]?.id ?? null;
6666
}
6767

@@ -235,15 +235,14 @@ export function createPresignedUrlPlugin(
235235
}
236236

237237
return withPgClient(pgSettings, async (pgClient: any) => {
238-
await pgClient.query('BEGIN');
239-
try {
238+
return pgClient.withTransaction(async (txClient: any) => {
240239
// --- Resolve storage module config (all limits come from here) ---
241-
const databaseId = await resolveDatabaseId(pgClient);
240+
const databaseId = await resolveDatabaseId(txClient);
242241
if (!databaseId) {
243242
throw new Error('DATABASE_NOT_FOUND');
244243
}
245244

246-
const storageConfig = await getStorageModuleConfig(pgClient, databaseId);
245+
const storageConfig = await getStorageModuleConfig(txClient, databaseId);
247246
if (!storageConfig) {
248247
throw new Error('STORAGE_MODULE_NOT_PROVISIONED');
249248
}
@@ -259,7 +258,7 @@ export function createPresignedUrlPlugin(
259258
}
260259

261260
// --- Look up the bucket (cached; first miss queries via RLS) ---
262-
const bucket = await getBucketConfig(pgClient, storageConfig, databaseId, bucketKey);
261+
const bucket = await getBucketConfig(txClient, storageConfig, databaseId, bucketKey);
263262
if (!bucket) {
264263
throw new Error('BUCKET_NOT_FOUND');
265264
}
@@ -288,29 +287,28 @@ export function createPresignedUrlPlugin(
288287
const s3Key = buildS3Key(contentHash);
289288

290289
// --- Dedup check: look for existing file with same content_hash in this bucket ---
291-
const dedupResult = await pgClient.query(
292-
`SELECT id, status
290+
const dedupResult = await txClient.query({
291+
text: `SELECT id, status
293292
FROM ${storageConfig.filesQualifiedName}
294293
WHERE content_hash = $1
295294
AND bucket_id = $2
296295
AND status IN ('ready', 'processed')
297296
LIMIT 1`,
298-
[contentHash, bucket.id],
299-
);
297+
values: [contentHash, bucket.id],
298+
});
300299

301300
if (dedupResult.rows.length > 0) {
302301
const existingFile = dedupResult.rows[0];
303302
log.info(`Dedup hit: file ${existingFile.id} for hash ${contentHash}`);
304303

305304
// Track the dedup request
306-
await pgClient.query(
307-
`INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
305+
await txClient.query({
306+
text: `INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
308307
(file_id, bucket_id, key, content_type, content_hash, size, status, expires_at)
309308
VALUES ($1, $2, $3, $4, $5, $6, 'confirmed', NOW())`,
310-
[existingFile.id, bucket.id, s3Key, contentType, contentHash, size],
311-
);
309+
values: [existingFile.id, bucket.id, s3Key, contentType, contentHash, size],
310+
});
312311

313-
await pgClient.query('COMMIT');
314312
return {
315313
uploadUrl: null,
316314
fileId: existingFile.id,
@@ -321,12 +319,12 @@ export function createPresignedUrlPlugin(
321319
}
322320

323321
// --- Create file record (status=pending) ---
324-
const fileResult = await pgClient.query(
325-
`INSERT INTO ${storageConfig.filesQualifiedName}
322+
const fileResult = await txClient.query({
323+
text: `INSERT INTO ${storageConfig.filesQualifiedName}
326324
(bucket_id, key, content_type, content_hash, size, filename, owner_id, is_public, status)
327325
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'pending')
328326
RETURNING id`,
329-
[
327+
values: [
330328
bucket.id,
331329
s3Key,
332330
contentType,
@@ -336,7 +334,7 @@ export function createPresignedUrlPlugin(
336334
bucket.owner_id,
337335
bucket.is_public,
338336
],
339-
);
337+
});
340338

341339
const fileId = fileResult.rows[0].id;
342340

@@ -356,25 +354,21 @@ export function createPresignedUrlPlugin(
356354
const expiresAt = new Date(Date.now() + storageConfig.uploadUrlExpirySeconds * 1000).toISOString();
357355

358356
// --- Track the upload request ---
359-
await pgClient.query(
360-
`INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
357+
await txClient.query({
358+
text: `INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
361359
(file_id, bucket_id, key, content_type, content_hash, size, status, expires_at)
362360
VALUES ($1, $2, $3, $4, $5, $6, 'issued', $7)`,
363-
[fileId, bucket.id, s3Key, contentType, contentHash, size, expiresAt],
364-
);
361+
values: [fileId, bucket.id, s3Key, contentType, contentHash, size, expiresAt],
362+
});
365363

366-
await pgClient.query('COMMIT');
367364
return {
368365
uploadUrl,
369366
fileId,
370367
key: s3Key,
371368
deduplicated: false,
372369
expiresAt,
373370
};
374-
} catch (err) {
375-
await pgClient.query('ROLLBACK');
376-
throw err;
377-
}
371+
});
378372
});
379373
});
380374
},
@@ -397,27 +391,26 @@ export function createPresignedUrlPlugin(
397391
}
398392

399393
return withPgClient(pgSettings, async (pgClient: any) => {
400-
await pgClient.query('BEGIN');
401-
try {
394+
return pgClient.withTransaction(async (txClient: any) => {
402395
// --- Resolve storage module config ---
403-
const databaseId = await resolveDatabaseId(pgClient);
396+
const databaseId = await resolveDatabaseId(txClient);
404397
if (!databaseId) {
405398
throw new Error('DATABASE_NOT_FOUND');
406399
}
407400

408-
const storageConfig = await getStorageModuleConfig(pgClient, databaseId);
401+
const storageConfig = await getStorageModuleConfig(txClient, databaseId);
409402
if (!storageConfig) {
410403
throw new Error('STORAGE_MODULE_NOT_PROVISIONED');
411404
}
412405

413406
// --- Look up the file (RLS enforced) ---
414-
const fileResult = await pgClient.query(
415-
`SELECT id, key, content_type, status, bucket_id
407+
const fileResult = await txClient.query({
408+
text: `SELECT id, key, content_type, status, bucket_id
416409
FROM ${storageConfig.filesQualifiedName}
417410
WHERE id = $1
418411
LIMIT 1`,
419-
[fileId],
420-
);
412+
values: [fileId],
413+
});
421414

422415
if (fileResult.rows.length === 0) {
423416
throw new Error('FILE_NOT_FOUND');
@@ -427,7 +420,6 @@ export function createPresignedUrlPlugin(
427420

428421
if (file.status !== 'pending') {
429422
// File is already confirmed or processed — idempotent success
430-
await pgClient.query('COMMIT');
431423
return {
432424
fileId: file.id,
433425
status: file.status,
@@ -446,45 +438,40 @@ export function createPresignedUrlPlugin(
446438
// --- Content-type verification ---
447439
if (s3Head.contentType && s3Head.contentType !== file.content_type) {
448440
// Mark upload_request as rejected
449-
await pgClient.query(
450-
`UPDATE ${storageConfig.uploadRequestsQualifiedName}
441+
await txClient.query({
442+
text: `UPDATE ${storageConfig.uploadRequestsQualifiedName}
451443
SET status = 'rejected'
452444
WHERE file_id = $1 AND status = 'issued'`,
453-
[fileId],
454-
);
445+
values: [fileId],
446+
});
455447

456-
await pgClient.query('COMMIT');
457448
throw new Error(
458449
`CONTENT_TYPE_MISMATCH: expected ${file.content_type}, got ${s3Head.contentType}`,
459450
);
460451
}
461452

462453
// --- Transition file to 'ready' ---
463-
await pgClient.query(
464-
`UPDATE ${storageConfig.filesQualifiedName}
454+
await txClient.query({
455+
text: `UPDATE ${storageConfig.filesQualifiedName}
465456
SET status = 'ready'
466457
WHERE id = $1`,
467-
[fileId],
468-
);
458+
values: [fileId],
459+
});
469460

470461
// --- Update upload_request to 'confirmed' ---
471-
await pgClient.query(
472-
`UPDATE ${storageConfig.uploadRequestsQualifiedName}
462+
await txClient.query({
463+
text: `UPDATE ${storageConfig.uploadRequestsQualifiedName}
473464
SET status = 'confirmed', confirmed_at = NOW()
474465
WHERE file_id = $1 AND status = 'issued'`,
475-
[fileId],
476-
);
466+
values: [fileId],
467+
});
477468

478-
await pgClient.query('COMMIT');
479469
return {
480470
fileId: file.id,
481471
status: 'ready',
482472
success: true,
483473
};
484-
} catch (err) {
485-
await pgClient.query('ROLLBACK');
486-
throw err;
487-
}
474+
});
488475
});
489476
});
490477
},

graphile/graphile-presigned-url-plugin/src/storage-module-cache.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ interface StorageModuleRow {
9191
* @returns StorageModuleConfig or null if no storage module is provisioned
9292
*/
9393
export async function getStorageModuleConfig(
94-
pgClient: { query: (sql: string, params: unknown[]) => Promise<{ rows: unknown[] }> },
94+
pgClient: { query: (opts: { text: string; values?: unknown[] }) => Promise<{ rows: unknown[] }> },
9595
databaseId: string,
9696
): Promise<StorageModuleConfig | null> {
9797
const cacheKey = `storage:${databaseId}`;
@@ -102,7 +102,7 @@ export async function getStorageModuleConfig(
102102

103103
log.debug(`Cache miss for database ${databaseId}, querying metaschema...`);
104104

105-
const result = await pgClient.query(STORAGE_MODULE_QUERY, [databaseId]);
105+
const result = await pgClient.query({ text: STORAGE_MODULE_QUERY, values: [databaseId] });
106106
if (result.rows.length === 0) {
107107
log.warn(`No storage module found for database ${databaseId}`);
108108
return null;
@@ -172,7 +172,7 @@ const bucketCache = new LRUCache<string, BucketConfig>({
172172
* @returns BucketConfig or null if the bucket doesn't exist / isn't accessible
173173
*/
174174
export async function getBucketConfig(
175-
pgClient: { query: (sql: string, params: unknown[]) => Promise<{ rows: unknown[] }> },
175+
pgClient: { query: (opts: { text: string; values?: unknown[] }) => Promise<{ rows: unknown[] }> },
176176
storageConfig: StorageModuleConfig,
177177
databaseId: string,
178178
bucketKey: string,
@@ -185,13 +185,13 @@ export async function getBucketConfig(
185185

186186
log.debug(`Bucket cache miss for ${databaseId}:${bucketKey}, querying DB...`);
187187

188-
const result = await pgClient.query(
189-
`SELECT id, key, type, is_public, owner_id, allowed_mime_types, max_file_size
188+
const result = await pgClient.query({
189+
text: `SELECT id, key, type, is_public, owner_id, allowed_mime_types, max_file_size
190190
FROM ${storageConfig.bucketsQualifiedName}
191191
WHERE key = $1
192192
LIMIT 1`,
193-
[bucketKey],
194-
);
193+
values: [bucketKey],
194+
});
195195

196196
if (result.rows.length === 0) {
197197
return null;

0 commit comments

Comments
 (0)