graphile-presigned-url-plugin 0.6.5 → 0.8.0
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/README.md +0 -2
- package/download-url-field.js +1 -7
- package/esm/download-url-field.js +1 -7
- package/esm/index.d.ts +2 -3
- package/esm/index.js +1 -2
- package/esm/plugin.d.ts +2 -6
- package/esm/plugin.js +10 -126
- package/esm/preset.d.ts +2 -2
- package/esm/preset.js +2 -2
- package/esm/s3-signer.d.ts +1 -2
- package/esm/s3-signer.js +1 -2
- package/esm/storage-module-cache.d.ts +0 -2
- package/esm/storage-module-cache.js +3 -14
- package/esm/types.d.ts +0 -22
- package/index.d.ts +2 -3
- package/index.js +1 -2
- package/package.json +3 -3
- package/plugin.d.ts +2 -6
- package/plugin.js +8 -124
- package/preset.d.ts +2 -2
- package/preset.js +2 -2
- package/s3-signer.d.ts +1 -2
- package/s3-signer.js +1 -2
- package/storage-module-cache.d.ts +0 -2
- package/storage-module-cache.js +3 -14
- package/types.d.ts +0 -22
package/README.md
CHANGED
|
@@ -17,11 +17,9 @@ Presigned URL upload plugin for PostGraphile v5.
|
|
|
17
17
|
## Features
|
|
18
18
|
|
|
19
19
|
- `requestUploadUrl` mutation — generates presigned PUT URLs for direct client-to-S3 upload
|
|
20
|
-
- `confirmUpload` mutation — verifies upload and transitions file status to 'ready'
|
|
21
20
|
- `downloadUrl` computed field — presigned GET URLs for private files, public URLs for public files
|
|
22
21
|
- Content-hash based S3 keys (SHA-256) with automatic deduplication
|
|
23
22
|
- Per-bucket MIME type and file size validation
|
|
24
|
-
- Upload request tracking for audit and rate limiting
|
|
25
23
|
|
|
26
24
|
## Usage
|
|
27
25
|
|
package/download-url-field.js
CHANGED
|
@@ -96,7 +96,6 @@ function createDownloadUrlPlugin(options) {
|
|
|
96
96
|
const $key = $parent.get('key');
|
|
97
97
|
const $isPublic = $parent.get('is_public');
|
|
98
98
|
const $filename = $parent.get('filename');
|
|
99
|
-
const $status = $parent.get('status');
|
|
100
99
|
// Access GraphQL context for per-database config resolution
|
|
101
100
|
const $withPgClient = (0, grafast_1.context)().get('withPgClient');
|
|
102
101
|
const $pgSettings = (0, grafast_1.context)().get('pgSettings');
|
|
@@ -104,17 +103,12 @@ function createDownloadUrlPlugin(options) {
|
|
|
104
103
|
key: $key,
|
|
105
104
|
isPublic: $isPublic,
|
|
106
105
|
filename: $filename,
|
|
107
|
-
status: $status,
|
|
108
106
|
withPgClient: $withPgClient,
|
|
109
107
|
pgSettings: $pgSettings,
|
|
110
108
|
});
|
|
111
|
-
return (0, grafast_1.lambda)($combined, async ({ key, isPublic, filename,
|
|
109
|
+
return (0, grafast_1.lambda)($combined, async ({ key, isPublic, filename, withPgClient, pgSettings }) => {
|
|
112
110
|
if (!key)
|
|
113
111
|
return null;
|
|
114
|
-
// Only provide download URLs for ready/processed files
|
|
115
|
-
if (status !== 'ready' && status !== 'processed') {
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
112
|
// Resolve per-database config (bucket, publicUrlPrefix, expiry)
|
|
119
113
|
let s3ForDb = resolveS3(options); // fallback to global
|
|
120
114
|
let downloadUrlExpirySeconds = 3600; // fallback default
|
|
@@ -93,7 +93,6 @@ export function createDownloadUrlPlugin(options) {
|
|
|
93
93
|
const $key = $parent.get('key');
|
|
94
94
|
const $isPublic = $parent.get('is_public');
|
|
95
95
|
const $filename = $parent.get('filename');
|
|
96
|
-
const $status = $parent.get('status');
|
|
97
96
|
// Access GraphQL context for per-database config resolution
|
|
98
97
|
const $withPgClient = grafastContext().get('withPgClient');
|
|
99
98
|
const $pgSettings = grafastContext().get('pgSettings');
|
|
@@ -101,17 +100,12 @@ export function createDownloadUrlPlugin(options) {
|
|
|
101
100
|
key: $key,
|
|
102
101
|
isPublic: $isPublic,
|
|
103
102
|
filename: $filename,
|
|
104
|
-
status: $status,
|
|
105
103
|
withPgClient: $withPgClient,
|
|
106
104
|
pgSettings: $pgSettings,
|
|
107
105
|
});
|
|
108
|
-
return lambda($combined, async ({ key, isPublic, filename,
|
|
106
|
+
return lambda($combined, async ({ key, isPublic, filename, withPgClient, pgSettings }) => {
|
|
109
107
|
if (!key)
|
|
110
108
|
return null;
|
|
111
|
-
// Only provide download URLs for ready/processed files
|
|
112
|
-
if (status !== 'ready' && status !== 'processed') {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
109
|
// Resolve per-database config (bucket, publicUrlPrefix, expiry)
|
|
116
110
|
let s3ForDb = resolveS3(options); // fallback to global
|
|
117
111
|
let downloadUrlExpirySeconds = 3600; // fallback default
|
package/esm/index.d.ts
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* Presigned URL Plugin for PostGraphile v5
|
|
3
3
|
*
|
|
4
4
|
* Provides presigned URL upload capabilities for PostGraphile v5:
|
|
5
|
-
* - requestUploadUrl mutation (presigned PUT URL generation)
|
|
6
|
-
* - confirmUpload mutation (upload verification + status transition)
|
|
5
|
+
* - requestUploadUrl mutation (presigned PUT URL generation + dedup)
|
|
7
6
|
* - downloadUrl computed field (presigned GET URL / public URL)
|
|
8
7
|
*
|
|
9
8
|
* @example
|
|
@@ -31,4 +30,4 @@ export { createDownloadUrlPlugin } from './download-url-field';
|
|
|
31
30
|
export { PresignedUrlPreset } from './preset';
|
|
32
31
|
export { getStorageModuleConfig, getStorageModuleConfigForOwner, getBucketConfig, resolveStorageModuleByFileId, clearStorageModuleCache, clearBucketCache, isS3BucketProvisioned, markS3BucketProvisioned } from './storage-module-cache';
|
|
33
32
|
export { generatePresignedPutUrl, generatePresignedGetUrl, headObject } from './s3-signer';
|
|
34
|
-
export type { BucketConfig, StorageModuleConfig, RequestUploadUrlInput, RequestUploadUrlPayload,
|
|
33
|
+
export type { BucketConfig, StorageModuleConfig, RequestUploadUrlInput, RequestUploadUrlPayload, S3Config, S3ConfigOrGetter, PresignedUrlPluginOptions, BucketNameResolver, EnsureBucketProvisioned, } from './types';
|
package/esm/index.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* Presigned URL Plugin for PostGraphile v5
|
|
3
3
|
*
|
|
4
4
|
* Provides presigned URL upload capabilities for PostGraphile v5:
|
|
5
|
-
* - requestUploadUrl mutation (presigned PUT URL generation)
|
|
6
|
-
* - confirmUpload mutation (upload verification + status transition)
|
|
5
|
+
* - requestUploadUrl mutation (presigned PUT URL generation + dedup)
|
|
7
6
|
* - downloadUrl computed field (presigned GET URL / public URL)
|
|
8
7
|
*
|
|
9
8
|
* @example
|
package/esm/plugin.d.ts
CHANGED
|
@@ -5,13 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* 1. `requestUploadUrl` mutation — generates a presigned PUT URL for direct
|
|
7
7
|
* client-to-S3 upload. Checks bucket access via RLS, deduplicates by
|
|
8
|
-
* content hash
|
|
8
|
+
* content hash via UNIQUE(bucket_id, key) constraint.
|
|
9
9
|
*
|
|
10
|
-
* 2. `
|
|
11
|
-
* the object exists with correct content-type, transitions file status
|
|
12
|
-
* from 'pending' to 'ready'.
|
|
13
|
-
*
|
|
14
|
-
* 3. `downloadUrl` computed field on File types — generates presigned GET URLs
|
|
10
|
+
* 2. `downloadUrl` computed field on File types — generates presigned GET URLs
|
|
15
11
|
* for private files, returns public URL prefix + key for public files.
|
|
16
12
|
*
|
|
17
13
|
* Uses the extendSchema + grafast plan pattern (same as PublicKeySignature).
|
package/esm/plugin.js
CHANGED
|
@@ -5,13 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* 1. `requestUploadUrl` mutation — generates a presigned PUT URL for direct
|
|
7
7
|
* client-to-S3 upload. Checks bucket access via RLS, deduplicates by
|
|
8
|
-
* content hash
|
|
8
|
+
* content hash via UNIQUE(bucket_id, key) constraint.
|
|
9
9
|
*
|
|
10
|
-
* 2. `
|
|
11
|
-
* the object exists with correct content-type, transitions file status
|
|
12
|
-
* from 'pending' to 'ready'.
|
|
13
|
-
*
|
|
14
|
-
* 3. `downloadUrl` computed field on File types — generates presigned GET URLs
|
|
10
|
+
* 2. `downloadUrl` computed field on File types — generates presigned GET URLs
|
|
15
11
|
* for private files, returns public URL prefix + key for public files.
|
|
16
12
|
*
|
|
17
13
|
* Uses the extendSchema + grafast plan pattern (same as PublicKeySignature).
|
|
@@ -19,8 +15,8 @@
|
|
|
19
15
|
import { context as grafastContext, lambda, object } from 'grafast';
|
|
20
16
|
import { extendSchema, gql } from 'graphile-utils';
|
|
21
17
|
import { Logger } from '@pgpmjs/logger';
|
|
22
|
-
import { getStorageModuleConfig, getStorageModuleConfigForOwner, getBucketConfig,
|
|
23
|
-
import { generatePresignedPutUrl
|
|
18
|
+
import { getStorageModuleConfig, getStorageModuleConfigForOwner, getBucketConfig, isS3BucketProvisioned, markS3BucketProvisioned } from './storage-module-cache';
|
|
19
|
+
import { generatePresignedPutUrl } from './s3-signer';
|
|
24
20
|
const log = new Logger('graphile-presigned-url:plugin');
|
|
25
21
|
// --- Protocol-level constants (not configurable) ---
|
|
26
22
|
const MAX_CONTENT_HASH_LENGTH = 128;
|
|
@@ -147,20 +143,6 @@ export function createPresignedUrlPlugin(options) {
|
|
|
147
143
|
expiresAt: Datetime
|
|
148
144
|
}
|
|
149
145
|
|
|
150
|
-
input ConfirmUploadInput {
|
|
151
|
-
"""The file ID returned by requestUploadUrl"""
|
|
152
|
-
fileId: UUID!
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
type ConfirmUploadPayload {
|
|
156
|
-
"""The confirmed file ID"""
|
|
157
|
-
fileId: UUID!
|
|
158
|
-
"""New file status"""
|
|
159
|
-
status: String!
|
|
160
|
-
"""Whether confirmation succeeded"""
|
|
161
|
-
success: Boolean!
|
|
162
|
-
}
|
|
163
|
-
|
|
164
146
|
extend type Mutation {
|
|
165
147
|
"""
|
|
166
148
|
Request a presigned URL for uploading a file directly to S3.
|
|
@@ -171,15 +153,6 @@ export function createPresignedUrlPlugin(options) {
|
|
|
171
153
|
requestUploadUrl(
|
|
172
154
|
input: RequestUploadUrlInput!
|
|
173
155
|
): RequestUploadUrlPayload
|
|
174
|
-
|
|
175
|
-
"""
|
|
176
|
-
Confirm that a file has been uploaded to S3.
|
|
177
|
-
Verifies the object exists in S3, checks content-type,
|
|
178
|
-
and transitions the file status from 'pending' to 'ready'.
|
|
179
|
-
"""
|
|
180
|
-
confirmUpload(
|
|
181
|
-
input: ConfirmUploadInput!
|
|
182
|
-
): ConfirmUploadPayload
|
|
183
156
|
}
|
|
184
157
|
`,
|
|
185
158
|
plans: {
|
|
@@ -261,24 +234,16 @@ export function createPresignedUrlPlugin(options) {
|
|
|
261
234
|
const s3Key = buildS3Key(contentHash);
|
|
262
235
|
// --- Dedup check: look for existing file with same key (content hash) in this bucket ---
|
|
263
236
|
const dedupResult = await txClient.query({
|
|
264
|
-
text: `SELECT id
|
|
237
|
+
text: `SELECT id
|
|
265
238
|
FROM ${storageConfig.filesQualifiedName}
|
|
266
239
|
WHERE key = $1
|
|
267
240
|
AND bucket_id = $2
|
|
268
|
-
AND status IN ('ready', 'processed')
|
|
269
241
|
LIMIT 1`,
|
|
270
242
|
values: [s3Key, bucket.id],
|
|
271
243
|
});
|
|
272
244
|
if (dedupResult.rows.length > 0) {
|
|
273
245
|
const existingFile = dedupResult.rows[0];
|
|
274
246
|
log.info(`Dedup hit: file ${existingFile.id} for hash ${contentHash}`);
|
|
275
|
-
// Track the dedup request
|
|
276
|
-
await txClient.query({
|
|
277
|
-
text: `INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
|
|
278
|
-
(file_id, bucket_id, key, content_type, content_hash, status, expires_at)
|
|
279
|
-
VALUES ($1, $2, $3, $4, $5, 'confirmed', NOW())`,
|
|
280
|
-
values: [existingFile.id, bucket.id, s3Key, contentType, contentHash],
|
|
281
|
-
});
|
|
282
247
|
return {
|
|
283
248
|
uploadUrl: null,
|
|
284
249
|
fileId: existingFile.id,
|
|
@@ -287,18 +252,18 @@ export function createPresignedUrlPlugin(options) {
|
|
|
287
252
|
expiresAt: null,
|
|
288
253
|
};
|
|
289
254
|
}
|
|
290
|
-
// --- Create file record
|
|
255
|
+
// --- Create file record ---
|
|
291
256
|
// For app-level storage (no owner_id column), omit owner_id from the INSERT.
|
|
292
257
|
const hasOwnerColumn = storageConfig.membershipType !== null;
|
|
293
258
|
const fileResult = await txClient.query({
|
|
294
259
|
text: hasOwnerColumn
|
|
295
260
|
? `INSERT INTO ${storageConfig.filesQualifiedName}
|
|
296
|
-
(bucket_id, key, mime_type, size, filename, owner_id, is_public
|
|
297
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7
|
|
261
|
+
(bucket_id, key, mime_type, size, filename, owner_id, is_public)
|
|
262
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
298
263
|
RETURNING id`
|
|
299
264
|
: `INSERT INTO ${storageConfig.filesQualifiedName}
|
|
300
|
-
(bucket_id, key, mime_type, size, filename, is_public
|
|
301
|
-
VALUES ($1, $2, $3, $4, $5, $6
|
|
265
|
+
(bucket_id, key, mime_type, size, filename, is_public)
|
|
266
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
302
267
|
RETURNING id`,
|
|
303
268
|
values: hasOwnerColumn
|
|
304
269
|
? [
|
|
@@ -326,13 +291,6 @@ export function createPresignedUrlPlugin(options) {
|
|
|
326
291
|
// --- Generate presigned PUT URL (per-database bucket) ---
|
|
327
292
|
const uploadUrl = await generatePresignedPutUrl(s3ForDb, s3Key, contentType, size, storageConfig.uploadUrlExpirySeconds);
|
|
328
293
|
const expiresAt = new Date(Date.now() + storageConfig.uploadUrlExpirySeconds * 1000).toISOString();
|
|
329
|
-
// --- Track the upload request ---
|
|
330
|
-
await txClient.query({
|
|
331
|
-
text: `INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
|
|
332
|
-
(file_id, bucket_id, key, content_type, content_hash, status, expires_at)
|
|
333
|
-
VALUES ($1, $2, $3, $4, $5, 'issued', $6)`,
|
|
334
|
-
values: [fileId, bucket.id, s3Key, contentType, contentHash, expiresAt],
|
|
335
|
-
});
|
|
336
294
|
return {
|
|
337
295
|
uploadUrl,
|
|
338
296
|
fileId,
|
|
@@ -344,80 +302,6 @@ export function createPresignedUrlPlugin(options) {
|
|
|
344
302
|
});
|
|
345
303
|
});
|
|
346
304
|
},
|
|
347
|
-
confirmUpload(_$mutation, fieldArgs) {
|
|
348
|
-
const $input = fieldArgs.getRaw('input');
|
|
349
|
-
const $withPgClient = grafastContext().get('withPgClient');
|
|
350
|
-
const $pgSettings = grafastContext().get('pgSettings');
|
|
351
|
-
const $combined = object({
|
|
352
|
-
input: $input,
|
|
353
|
-
withPgClient: $withPgClient,
|
|
354
|
-
pgSettings: $pgSettings,
|
|
355
|
-
});
|
|
356
|
-
return lambda($combined, async ({ input, withPgClient, pgSettings }) => {
|
|
357
|
-
const { fileId } = input;
|
|
358
|
-
if (!fileId || typeof fileId !== 'string') {
|
|
359
|
-
throw new Error('INVALID_FILE_ID');
|
|
360
|
-
}
|
|
361
|
-
return withPgClient(pgSettings, async (pgClient) => {
|
|
362
|
-
return pgClient.withTransaction(async (txClient) => {
|
|
363
|
-
// --- Resolve storage module by file ID (probes all file tables) ---
|
|
364
|
-
const databaseId = await resolveDatabaseId(txClient);
|
|
365
|
-
if (!databaseId) {
|
|
366
|
-
throw new Error('DATABASE_NOT_FOUND');
|
|
367
|
-
}
|
|
368
|
-
const resolved = await resolveStorageModuleByFileId(txClient, databaseId, fileId);
|
|
369
|
-
if (!resolved) {
|
|
370
|
-
throw new Error('FILE_NOT_FOUND');
|
|
371
|
-
}
|
|
372
|
-
const { storageConfig, file } = resolved;
|
|
373
|
-
if (file.status !== 'pending') {
|
|
374
|
-
// File is already confirmed or processed — idempotent success
|
|
375
|
-
return {
|
|
376
|
-
fileId: file.id,
|
|
377
|
-
status: file.status,
|
|
378
|
-
success: true,
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
// --- Verify file exists in S3 (per-database bucket) ---
|
|
382
|
-
const s3ForDb = resolveS3ForDatabase(options, storageConfig, databaseId);
|
|
383
|
-
const s3Head = await headObject(s3ForDb, file.key, file.mime_type);
|
|
384
|
-
if (!s3Head) {
|
|
385
|
-
throw new Error('FILE_NOT_IN_S3: the file has not been uploaded yet');
|
|
386
|
-
}
|
|
387
|
-
// --- Content-type verification ---
|
|
388
|
-
if (s3Head.contentType && s3Head.contentType !== file.mime_type) {
|
|
389
|
-
// Mark upload_request as rejected
|
|
390
|
-
await txClient.query({
|
|
391
|
-
text: `UPDATE ${storageConfig.uploadRequestsQualifiedName}
|
|
392
|
-
SET status = 'rejected'
|
|
393
|
-
WHERE file_id = $1 AND status = 'issued'`,
|
|
394
|
-
values: [fileId],
|
|
395
|
-
});
|
|
396
|
-
throw new Error(`CONTENT_TYPE_MISMATCH: expected ${file.mime_type}, got ${s3Head.contentType}`);
|
|
397
|
-
}
|
|
398
|
-
// --- Transition file to 'ready' ---
|
|
399
|
-
await txClient.query({
|
|
400
|
-
text: `UPDATE ${storageConfig.filesQualifiedName}
|
|
401
|
-
SET status = 'ready'
|
|
402
|
-
WHERE id = $1`,
|
|
403
|
-
values: [fileId],
|
|
404
|
-
});
|
|
405
|
-
// --- Update upload_request to 'confirmed' ---
|
|
406
|
-
await txClient.query({
|
|
407
|
-
text: `UPDATE ${storageConfig.uploadRequestsQualifiedName}
|
|
408
|
-
SET status = 'confirmed', confirmed_at = NOW()
|
|
409
|
-
WHERE file_id = $1 AND status = 'issued'`,
|
|
410
|
-
values: [fileId],
|
|
411
|
-
});
|
|
412
|
-
return {
|
|
413
|
-
fileId: file.id,
|
|
414
|
-
status: 'ready',
|
|
415
|
-
success: true,
|
|
416
|
-
};
|
|
417
|
-
});
|
|
418
|
-
});
|
|
419
|
-
});
|
|
420
|
-
},
|
|
421
305
|
},
|
|
422
306
|
},
|
|
423
307
|
}));
|
package/esm/preset.d.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* PostGraphile v5 Presigned URL Preset
|
|
3
3
|
*
|
|
4
4
|
* Provides a convenient preset for including presigned URL upload support
|
|
5
|
-
* in PostGraphile. Combines the main mutation plugin (requestUploadUrl
|
|
6
|
-
*
|
|
5
|
+
* in PostGraphile. Combines the main mutation plugin (requestUploadUrl)
|
|
6
|
+
* with the downloadUrl computed field plugin.
|
|
7
7
|
*/
|
|
8
8
|
import type { GraphileConfig } from 'graphile-config';
|
|
9
9
|
import type { PresignedUrlPluginOptions } from './types';
|
package/esm/preset.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* PostGraphile v5 Presigned URL Preset
|
|
3
3
|
*
|
|
4
4
|
* Provides a convenient preset for including presigned URL upload support
|
|
5
|
-
* in PostGraphile. Combines the main mutation plugin (requestUploadUrl
|
|
6
|
-
*
|
|
5
|
+
* in PostGraphile. Combines the main mutation plugin (requestUploadUrl)
|
|
6
|
+
* with the downloadUrl computed field plugin.
|
|
7
7
|
*/
|
|
8
8
|
import { createPresignedUrlPlugin } from './plugin';
|
|
9
9
|
import { createDownloadUrlPlugin } from './download-url-field';
|
package/esm/s3-signer.d.ts
CHANGED
|
@@ -30,8 +30,7 @@ export declare function generatePresignedGetUrl(s3Config: S3Config, key: string,
|
|
|
30
30
|
/**
|
|
31
31
|
* Check if an object exists in S3 and optionally verify its content-type.
|
|
32
32
|
*
|
|
33
|
-
*
|
|
34
|
-
* and that the content-type matches what was declared.
|
|
33
|
+
* Checks whether an object exists in S3 and retrieves its content-type.
|
|
35
34
|
*
|
|
36
35
|
* @param s3Config - S3 client and bucket configuration
|
|
37
36
|
* @param key - S3 object key
|
package/esm/s3-signer.js
CHANGED
|
@@ -56,8 +56,7 @@ export async function generatePresignedGetUrl(s3Config, key, expiresIn = 3600, f
|
|
|
56
56
|
/**
|
|
57
57
|
* Check if an object exists in S3 and optionally verify its content-type.
|
|
58
58
|
*
|
|
59
|
-
*
|
|
60
|
-
* and that the content-type matches what was declared.
|
|
59
|
+
* Checks whether an object exists in S3 and retrieves its content-type.
|
|
61
60
|
*
|
|
62
61
|
* @param s3Config - S3 client and bucket configuration
|
|
63
62
|
* @param key - S3 object key
|
|
@@ -43,7 +43,6 @@ export declare function getStorageModuleConfigForOwner(pgClient: {
|
|
|
43
43
|
/**
|
|
44
44
|
* Resolve the storage module that owns a specific file by probing all file tables.
|
|
45
45
|
*
|
|
46
|
-
* Used by confirmUpload when only a fileId (UUID) is available.
|
|
47
46
|
* Since UUIDs are globally unique, exactly one table will contain the file.
|
|
48
47
|
*
|
|
49
48
|
* @param pgClient - A pg client from the Graphile context
|
|
@@ -64,7 +63,6 @@ export declare function resolveStorageModuleByFileId(pgClient: {
|
|
|
64
63
|
id: string;
|
|
65
64
|
key: string;
|
|
66
65
|
mime_type: string;
|
|
67
|
-
status: string;
|
|
68
66
|
bucket_id: string;
|
|
69
67
|
};
|
|
70
68
|
} | null>;
|
|
@@ -14,8 +14,8 @@ const ONE_HOUR_MS = 1000 * 60 * 60;
|
|
|
14
14
|
* LRU cache for per-database StorageModuleConfig.
|
|
15
15
|
*
|
|
16
16
|
* Each PostGraphile instance serves a single database, but the presigned URL
|
|
17
|
-
* plugin needs to know the generated table names (buckets, files
|
|
18
|
-
*
|
|
17
|
+
* plugin needs to know the generated table names (buckets, files)
|
|
18
|
+
* and their schemas. This cache avoids re-querying metaschema
|
|
19
19
|
* on every request.
|
|
20
20
|
*
|
|
21
21
|
* Pattern: same as graphile-cache's LRU with TTL-based eviction.
|
|
@@ -42,8 +42,6 @@ const APP_STORAGE_MODULE_QUERY = `
|
|
|
42
42
|
bt.name AS buckets_table,
|
|
43
43
|
fs.schema_name AS files_schema,
|
|
44
44
|
ft.name AS files_table,
|
|
45
|
-
urs.schema_name AS upload_requests_schema,
|
|
46
|
-
urt.name AS upload_requests_table,
|
|
47
45
|
sm.endpoint,
|
|
48
46
|
sm.public_url_prefix,
|
|
49
47
|
sm.provider,
|
|
@@ -60,8 +58,6 @@ const APP_STORAGE_MODULE_QUERY = `
|
|
|
60
58
|
JOIN metaschema_public.schema bs ON bs.id = bt.schema_id
|
|
61
59
|
JOIN metaschema_public.table ft ON ft.id = sm.files_table_id
|
|
62
60
|
JOIN metaschema_public.schema fs ON fs.id = ft.schema_id
|
|
63
|
-
JOIN metaschema_public.table urt ON urt.id = sm.upload_requests_table_id
|
|
64
|
-
JOIN metaschema_public.schema urs ON urs.id = urt.schema_id
|
|
65
61
|
WHERE sm.database_id = $1
|
|
66
62
|
AND sm.membership_type IS NULL
|
|
67
63
|
LIMIT 1
|
|
@@ -81,8 +77,6 @@ const ALL_STORAGE_MODULES_QUERY = `
|
|
|
81
77
|
bt.name AS buckets_table,
|
|
82
78
|
fs.schema_name AS files_schema,
|
|
83
79
|
ft.name AS files_table,
|
|
84
|
-
urs.schema_name AS upload_requests_schema,
|
|
85
|
-
urt.name AS upload_requests_table,
|
|
86
80
|
sm.endpoint,
|
|
87
81
|
sm.public_url_prefix,
|
|
88
82
|
sm.provider,
|
|
@@ -99,8 +93,6 @@ const ALL_STORAGE_MODULES_QUERY = `
|
|
|
99
93
|
JOIN metaschema_public.schema bs ON bs.id = bt.schema_id
|
|
100
94
|
JOIN metaschema_public.table ft ON ft.id = sm.files_table_id
|
|
101
95
|
JOIN metaschema_public.schema fs ON fs.id = ft.schema_id
|
|
102
|
-
JOIN metaschema_public.table urt ON urt.id = sm.upload_requests_table_id
|
|
103
|
-
JOIN metaschema_public.schema urs ON urs.id = urt.schema_id
|
|
104
96
|
LEFT JOIN metaschema_public.table et ON et.id = sm.entity_table_id
|
|
105
97
|
LEFT JOIN metaschema_public.schema es ON es.id = et.schema_id
|
|
106
98
|
WHERE sm.database_id = $1
|
|
@@ -114,11 +106,9 @@ function buildConfig(row) {
|
|
|
114
106
|
id: row.id,
|
|
115
107
|
bucketsQualifiedName: QuoteUtils.quoteQualifiedIdentifier(row.buckets_schema, row.buckets_table),
|
|
116
108
|
filesQualifiedName: QuoteUtils.quoteQualifiedIdentifier(row.files_schema, row.files_table),
|
|
117
|
-
uploadRequestsQualifiedName: QuoteUtils.quoteQualifiedIdentifier(row.upload_requests_schema, row.upload_requests_table),
|
|
118
109
|
schemaName: row.buckets_schema,
|
|
119
110
|
bucketsTableName: row.buckets_table,
|
|
120
111
|
filesTableName: row.files_table,
|
|
121
|
-
uploadRequestsTableName: row.upload_requests_table,
|
|
122
112
|
membershipType: row.membership_type,
|
|
123
113
|
entityTableId: row.entity_table_id,
|
|
124
114
|
entityQualifiedName: row.entity_schema && row.entity_table
|
|
@@ -229,7 +219,6 @@ export async function getStorageModuleConfigForOwner(pgClient, databaseId, owner
|
|
|
229
219
|
/**
|
|
230
220
|
* Resolve the storage module that owns a specific file by probing all file tables.
|
|
231
221
|
*
|
|
232
|
-
* Used by confirmUpload when only a fileId (UUID) is available.
|
|
233
222
|
* Since UUIDs are globally unique, exactly one table will contain the file.
|
|
234
223
|
*
|
|
235
224
|
* @param pgClient - A pg client from the Graphile context
|
|
@@ -244,7 +233,7 @@ export async function resolveStorageModuleByFileId(pgClient, databaseId, fileId)
|
|
|
244
233
|
// Probe each module's files table for the fileId
|
|
245
234
|
for (const config of allConfigs) {
|
|
246
235
|
const fileResult = await pgClient.query({
|
|
247
|
-
text: `SELECT id, key, mime_type,
|
|
236
|
+
text: `SELECT id, key, mime_type, bucket_id
|
|
248
237
|
FROM ${config.filesQualifiedName}
|
|
249
238
|
WHERE id = $1
|
|
250
239
|
LIMIT 1`,
|
package/esm/types.d.ts
CHANGED
|
@@ -21,16 +21,12 @@ export interface StorageModuleConfig {
|
|
|
21
21
|
bucketsQualifiedName: string;
|
|
22
22
|
/** Resolved schema.table for files */
|
|
23
23
|
filesQualifiedName: string;
|
|
24
|
-
/** Resolved schema.table for upload_requests */
|
|
25
|
-
uploadRequestsQualifiedName: string;
|
|
26
24
|
/** Schema name (e.g., "app_public") */
|
|
27
25
|
schemaName: string;
|
|
28
26
|
/** Buckets table name */
|
|
29
27
|
bucketsTableName: string;
|
|
30
28
|
/** Files table name */
|
|
31
29
|
filesTableName: string;
|
|
32
|
-
/** Upload requests table name */
|
|
33
|
-
uploadRequestsTableName: string;
|
|
34
30
|
/** Membership type (NULL for app-level, non-NULL for entity-scoped) */
|
|
35
31
|
membershipType: number | null;
|
|
36
32
|
/** Entity table ID for entity-scoped storage (NULL for app-level) */
|
|
@@ -93,24 +89,6 @@ export interface RequestUploadUrlPayload {
|
|
|
93
89
|
/** Presigned URL expiry time (null if deduplicated) */
|
|
94
90
|
expiresAt: string | null;
|
|
95
91
|
}
|
|
96
|
-
/**
|
|
97
|
-
* Input for the confirmUpload mutation.
|
|
98
|
-
*/
|
|
99
|
-
export interface ConfirmUploadInput {
|
|
100
|
-
/** The file ID returned by requestUploadUrl */
|
|
101
|
-
fileId: string;
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Result of the confirmUpload mutation.
|
|
105
|
-
*/
|
|
106
|
-
export interface ConfirmUploadPayload {
|
|
107
|
-
/** The confirmed file ID */
|
|
108
|
-
fileId: string;
|
|
109
|
-
/** New file status (should be 'ready') */
|
|
110
|
-
status: string;
|
|
111
|
-
/** Whether confirmation succeeded */
|
|
112
|
-
success: boolean;
|
|
113
|
-
}
|
|
114
92
|
/**
|
|
115
93
|
* S3 configuration for the presigned URL plugin.
|
|
116
94
|
*/
|
package/index.d.ts
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* Presigned URL Plugin for PostGraphile v5
|
|
3
3
|
*
|
|
4
4
|
* Provides presigned URL upload capabilities for PostGraphile v5:
|
|
5
|
-
* - requestUploadUrl mutation (presigned PUT URL generation)
|
|
6
|
-
* - confirmUpload mutation (upload verification + status transition)
|
|
5
|
+
* - requestUploadUrl mutation (presigned PUT URL generation + dedup)
|
|
7
6
|
* - downloadUrl computed field (presigned GET URL / public URL)
|
|
8
7
|
*
|
|
9
8
|
* @example
|
|
@@ -31,4 +30,4 @@ export { createDownloadUrlPlugin } from './download-url-field';
|
|
|
31
30
|
export { PresignedUrlPreset } from './preset';
|
|
32
31
|
export { getStorageModuleConfig, getStorageModuleConfigForOwner, getBucketConfig, resolveStorageModuleByFileId, clearStorageModuleCache, clearBucketCache, isS3BucketProvisioned, markS3BucketProvisioned } from './storage-module-cache';
|
|
33
32
|
export { generatePresignedPutUrl, generatePresignedGetUrl, headObject } from './s3-signer';
|
|
34
|
-
export type { BucketConfig, StorageModuleConfig, RequestUploadUrlInput, RequestUploadUrlPayload,
|
|
33
|
+
export type { BucketConfig, StorageModuleConfig, RequestUploadUrlInput, RequestUploadUrlPayload, S3Config, S3ConfigOrGetter, PresignedUrlPluginOptions, BucketNameResolver, EnsureBucketProvisioned, } from './types';
|
package/index.js
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
* Presigned URL Plugin for PostGraphile v5
|
|
4
4
|
*
|
|
5
5
|
* Provides presigned URL upload capabilities for PostGraphile v5:
|
|
6
|
-
* - requestUploadUrl mutation (presigned PUT URL generation)
|
|
7
|
-
* - confirmUpload mutation (upload verification + status transition)
|
|
6
|
+
* - requestUploadUrl mutation (presigned PUT URL generation + dedup)
|
|
8
7
|
* - downloadUrl computed field (presigned GET URL / public URL)
|
|
9
8
|
*
|
|
10
9
|
* @example
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "graphile-presigned-url-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Presigned URL upload plugin for PostGraphile v5 — requestUploadUrl
|
|
3
|
+
"version": "0.8.0",
|
|
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",
|
|
7
7
|
"license": "MIT",
|
|
@@ -60,5 +60,5 @@
|
|
|
60
60
|
"@types/node": "^22.19.11",
|
|
61
61
|
"makage": "^0.1.10"
|
|
62
62
|
},
|
|
63
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "0238640b70fed4b203eb84f48315c2bd807923b9"
|
|
64
64
|
}
|
package/plugin.d.ts
CHANGED
|
@@ -5,13 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* 1. `requestUploadUrl` mutation — generates a presigned PUT URL for direct
|
|
7
7
|
* client-to-S3 upload. Checks bucket access via RLS, deduplicates by
|
|
8
|
-
* content hash
|
|
8
|
+
* content hash via UNIQUE(bucket_id, key) constraint.
|
|
9
9
|
*
|
|
10
|
-
* 2. `
|
|
11
|
-
* the object exists with correct content-type, transitions file status
|
|
12
|
-
* from 'pending' to 'ready'.
|
|
13
|
-
*
|
|
14
|
-
* 3. `downloadUrl` computed field on File types — generates presigned GET URLs
|
|
10
|
+
* 2. `downloadUrl` computed field on File types — generates presigned GET URLs
|
|
15
11
|
* for private files, returns public URL prefix + key for public files.
|
|
16
12
|
*
|
|
17
13
|
* Uses the extendSchema + grafast plan pattern (same as PublicKeySignature).
|
package/plugin.js
CHANGED
|
@@ -6,13 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 1. `requestUploadUrl` mutation — generates a presigned PUT URL for direct
|
|
8
8
|
* client-to-S3 upload. Checks bucket access via RLS, deduplicates by
|
|
9
|
-
* content hash
|
|
9
|
+
* content hash via UNIQUE(bucket_id, key) constraint.
|
|
10
10
|
*
|
|
11
|
-
* 2. `
|
|
12
|
-
* the object exists with correct content-type, transitions file status
|
|
13
|
-
* from 'pending' to 'ready'.
|
|
14
|
-
*
|
|
15
|
-
* 3. `downloadUrl` computed field on File types — generates presigned GET URLs
|
|
11
|
+
* 2. `downloadUrl` computed field on File types — generates presigned GET URLs
|
|
16
12
|
* for private files, returns public URL prefix + key for public files.
|
|
17
13
|
*
|
|
18
14
|
* Uses the extendSchema + grafast plan pattern (same as PublicKeySignature).
|
|
@@ -151,20 +147,6 @@ function createPresignedUrlPlugin(options) {
|
|
|
151
147
|
expiresAt: Datetime
|
|
152
148
|
}
|
|
153
149
|
|
|
154
|
-
input ConfirmUploadInput {
|
|
155
|
-
"""The file ID returned by requestUploadUrl"""
|
|
156
|
-
fileId: UUID!
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
type ConfirmUploadPayload {
|
|
160
|
-
"""The confirmed file ID"""
|
|
161
|
-
fileId: UUID!
|
|
162
|
-
"""New file status"""
|
|
163
|
-
status: String!
|
|
164
|
-
"""Whether confirmation succeeded"""
|
|
165
|
-
success: Boolean!
|
|
166
|
-
}
|
|
167
|
-
|
|
168
150
|
extend type Mutation {
|
|
169
151
|
"""
|
|
170
152
|
Request a presigned URL for uploading a file directly to S3.
|
|
@@ -175,15 +157,6 @@ function createPresignedUrlPlugin(options) {
|
|
|
175
157
|
requestUploadUrl(
|
|
176
158
|
input: RequestUploadUrlInput!
|
|
177
159
|
): RequestUploadUrlPayload
|
|
178
|
-
|
|
179
|
-
"""
|
|
180
|
-
Confirm that a file has been uploaded to S3.
|
|
181
|
-
Verifies the object exists in S3, checks content-type,
|
|
182
|
-
and transitions the file status from 'pending' to 'ready'.
|
|
183
|
-
"""
|
|
184
|
-
confirmUpload(
|
|
185
|
-
input: ConfirmUploadInput!
|
|
186
|
-
): ConfirmUploadPayload
|
|
187
160
|
}
|
|
188
161
|
`,
|
|
189
162
|
plans: {
|
|
@@ -265,24 +238,16 @@ function createPresignedUrlPlugin(options) {
|
|
|
265
238
|
const s3Key = buildS3Key(contentHash);
|
|
266
239
|
// --- Dedup check: look for existing file with same key (content hash) in this bucket ---
|
|
267
240
|
const dedupResult = await txClient.query({
|
|
268
|
-
text: `SELECT id
|
|
241
|
+
text: `SELECT id
|
|
269
242
|
FROM ${storageConfig.filesQualifiedName}
|
|
270
243
|
WHERE key = $1
|
|
271
244
|
AND bucket_id = $2
|
|
272
|
-
AND status IN ('ready', 'processed')
|
|
273
245
|
LIMIT 1`,
|
|
274
246
|
values: [s3Key, bucket.id],
|
|
275
247
|
});
|
|
276
248
|
if (dedupResult.rows.length > 0) {
|
|
277
249
|
const existingFile = dedupResult.rows[0];
|
|
278
250
|
log.info(`Dedup hit: file ${existingFile.id} for hash ${contentHash}`);
|
|
279
|
-
// Track the dedup request
|
|
280
|
-
await txClient.query({
|
|
281
|
-
text: `INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
|
|
282
|
-
(file_id, bucket_id, key, content_type, content_hash, status, expires_at)
|
|
283
|
-
VALUES ($1, $2, $3, $4, $5, 'confirmed', NOW())`,
|
|
284
|
-
values: [existingFile.id, bucket.id, s3Key, contentType, contentHash],
|
|
285
|
-
});
|
|
286
251
|
return {
|
|
287
252
|
uploadUrl: null,
|
|
288
253
|
fileId: existingFile.id,
|
|
@@ -291,18 +256,18 @@ function createPresignedUrlPlugin(options) {
|
|
|
291
256
|
expiresAt: null,
|
|
292
257
|
};
|
|
293
258
|
}
|
|
294
|
-
// --- Create file record
|
|
259
|
+
// --- Create file record ---
|
|
295
260
|
// For app-level storage (no owner_id column), omit owner_id from the INSERT.
|
|
296
261
|
const hasOwnerColumn = storageConfig.membershipType !== null;
|
|
297
262
|
const fileResult = await txClient.query({
|
|
298
263
|
text: hasOwnerColumn
|
|
299
264
|
? `INSERT INTO ${storageConfig.filesQualifiedName}
|
|
300
|
-
(bucket_id, key, mime_type, size, filename, owner_id, is_public
|
|
301
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7
|
|
265
|
+
(bucket_id, key, mime_type, size, filename, owner_id, is_public)
|
|
266
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
302
267
|
RETURNING id`
|
|
303
268
|
: `INSERT INTO ${storageConfig.filesQualifiedName}
|
|
304
|
-
(bucket_id, key, mime_type, size, filename, is_public
|
|
305
|
-
VALUES ($1, $2, $3, $4, $5, $6
|
|
269
|
+
(bucket_id, key, mime_type, size, filename, is_public)
|
|
270
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
306
271
|
RETURNING id`,
|
|
307
272
|
values: hasOwnerColumn
|
|
308
273
|
? [
|
|
@@ -330,13 +295,6 @@ function createPresignedUrlPlugin(options) {
|
|
|
330
295
|
// --- Generate presigned PUT URL (per-database bucket) ---
|
|
331
296
|
const uploadUrl = await (0, s3_signer_1.generatePresignedPutUrl)(s3ForDb, s3Key, contentType, size, storageConfig.uploadUrlExpirySeconds);
|
|
332
297
|
const expiresAt = new Date(Date.now() + storageConfig.uploadUrlExpirySeconds * 1000).toISOString();
|
|
333
|
-
// --- Track the upload request ---
|
|
334
|
-
await txClient.query({
|
|
335
|
-
text: `INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
|
|
336
|
-
(file_id, bucket_id, key, content_type, content_hash, status, expires_at)
|
|
337
|
-
VALUES ($1, $2, $3, $4, $5, 'issued', $6)`,
|
|
338
|
-
values: [fileId, bucket.id, s3Key, contentType, contentHash, expiresAt],
|
|
339
|
-
});
|
|
340
298
|
return {
|
|
341
299
|
uploadUrl,
|
|
342
300
|
fileId,
|
|
@@ -348,80 +306,6 @@ function createPresignedUrlPlugin(options) {
|
|
|
348
306
|
});
|
|
349
307
|
});
|
|
350
308
|
},
|
|
351
|
-
confirmUpload(_$mutation, fieldArgs) {
|
|
352
|
-
const $input = fieldArgs.getRaw('input');
|
|
353
|
-
const $withPgClient = (0, grafast_1.context)().get('withPgClient');
|
|
354
|
-
const $pgSettings = (0, grafast_1.context)().get('pgSettings');
|
|
355
|
-
const $combined = (0, grafast_1.object)({
|
|
356
|
-
input: $input,
|
|
357
|
-
withPgClient: $withPgClient,
|
|
358
|
-
pgSettings: $pgSettings,
|
|
359
|
-
});
|
|
360
|
-
return (0, grafast_1.lambda)($combined, async ({ input, withPgClient, pgSettings }) => {
|
|
361
|
-
const { fileId } = input;
|
|
362
|
-
if (!fileId || typeof fileId !== 'string') {
|
|
363
|
-
throw new Error('INVALID_FILE_ID');
|
|
364
|
-
}
|
|
365
|
-
return withPgClient(pgSettings, async (pgClient) => {
|
|
366
|
-
return pgClient.withTransaction(async (txClient) => {
|
|
367
|
-
// --- Resolve storage module by file ID (probes all file tables) ---
|
|
368
|
-
const databaseId = await resolveDatabaseId(txClient);
|
|
369
|
-
if (!databaseId) {
|
|
370
|
-
throw new Error('DATABASE_NOT_FOUND');
|
|
371
|
-
}
|
|
372
|
-
const resolved = await (0, storage_module_cache_1.resolveStorageModuleByFileId)(txClient, databaseId, fileId);
|
|
373
|
-
if (!resolved) {
|
|
374
|
-
throw new Error('FILE_NOT_FOUND');
|
|
375
|
-
}
|
|
376
|
-
const { storageConfig, file } = resolved;
|
|
377
|
-
if (file.status !== 'pending') {
|
|
378
|
-
// File is already confirmed or processed — idempotent success
|
|
379
|
-
return {
|
|
380
|
-
fileId: file.id,
|
|
381
|
-
status: file.status,
|
|
382
|
-
success: true,
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
// --- Verify file exists in S3 (per-database bucket) ---
|
|
386
|
-
const s3ForDb = resolveS3ForDatabase(options, storageConfig, databaseId);
|
|
387
|
-
const s3Head = await (0, s3_signer_1.headObject)(s3ForDb, file.key, file.mime_type);
|
|
388
|
-
if (!s3Head) {
|
|
389
|
-
throw new Error('FILE_NOT_IN_S3: the file has not been uploaded yet');
|
|
390
|
-
}
|
|
391
|
-
// --- Content-type verification ---
|
|
392
|
-
if (s3Head.contentType && s3Head.contentType !== file.mime_type) {
|
|
393
|
-
// Mark upload_request as rejected
|
|
394
|
-
await txClient.query({
|
|
395
|
-
text: `UPDATE ${storageConfig.uploadRequestsQualifiedName}
|
|
396
|
-
SET status = 'rejected'
|
|
397
|
-
WHERE file_id = $1 AND status = 'issued'`,
|
|
398
|
-
values: [fileId],
|
|
399
|
-
});
|
|
400
|
-
throw new Error(`CONTENT_TYPE_MISMATCH: expected ${file.mime_type}, got ${s3Head.contentType}`);
|
|
401
|
-
}
|
|
402
|
-
// --- Transition file to 'ready' ---
|
|
403
|
-
await txClient.query({
|
|
404
|
-
text: `UPDATE ${storageConfig.filesQualifiedName}
|
|
405
|
-
SET status = 'ready'
|
|
406
|
-
WHERE id = $1`,
|
|
407
|
-
values: [fileId],
|
|
408
|
-
});
|
|
409
|
-
// --- Update upload_request to 'confirmed' ---
|
|
410
|
-
await txClient.query({
|
|
411
|
-
text: `UPDATE ${storageConfig.uploadRequestsQualifiedName}
|
|
412
|
-
SET status = 'confirmed', confirmed_at = NOW()
|
|
413
|
-
WHERE file_id = $1 AND status = 'issued'`,
|
|
414
|
-
values: [fileId],
|
|
415
|
-
});
|
|
416
|
-
return {
|
|
417
|
-
fileId: file.id,
|
|
418
|
-
status: 'ready',
|
|
419
|
-
success: true,
|
|
420
|
-
};
|
|
421
|
-
});
|
|
422
|
-
});
|
|
423
|
-
});
|
|
424
|
-
},
|
|
425
309
|
},
|
|
426
310
|
},
|
|
427
311
|
}));
|
package/preset.d.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* PostGraphile v5 Presigned URL Preset
|
|
3
3
|
*
|
|
4
4
|
* Provides a convenient preset for including presigned URL upload support
|
|
5
|
-
* in PostGraphile. Combines the main mutation plugin (requestUploadUrl
|
|
6
|
-
*
|
|
5
|
+
* in PostGraphile. Combines the main mutation plugin (requestUploadUrl)
|
|
6
|
+
* with the downloadUrl computed field plugin.
|
|
7
7
|
*/
|
|
8
8
|
import type { GraphileConfig } from 'graphile-config';
|
|
9
9
|
import type { PresignedUrlPluginOptions } from './types';
|
package/preset.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* PostGraphile v5 Presigned URL Preset
|
|
4
4
|
*
|
|
5
5
|
* Provides a convenient preset for including presigned URL upload support
|
|
6
|
-
* in PostGraphile. Combines the main mutation plugin (requestUploadUrl
|
|
7
|
-
*
|
|
6
|
+
* in PostGraphile. Combines the main mutation plugin (requestUploadUrl)
|
|
7
|
+
* with the downloadUrl computed field plugin.
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.PresignedUrlPreset = PresignedUrlPreset;
|
package/s3-signer.d.ts
CHANGED
|
@@ -30,8 +30,7 @@ export declare function generatePresignedGetUrl(s3Config: S3Config, key: string,
|
|
|
30
30
|
/**
|
|
31
31
|
* Check if an object exists in S3 and optionally verify its content-type.
|
|
32
32
|
*
|
|
33
|
-
*
|
|
34
|
-
* and that the content-type matches what was declared.
|
|
33
|
+
* Checks whether an object exists in S3 and retrieves its content-type.
|
|
35
34
|
*
|
|
36
35
|
* @param s3Config - S3 client and bucket configuration
|
|
37
36
|
* @param key - S3 object key
|
package/s3-signer.js
CHANGED
|
@@ -61,8 +61,7 @@ async function generatePresignedGetUrl(s3Config, key, expiresIn = 3600, filename
|
|
|
61
61
|
/**
|
|
62
62
|
* Check if an object exists in S3 and optionally verify its content-type.
|
|
63
63
|
*
|
|
64
|
-
*
|
|
65
|
-
* and that the content-type matches what was declared.
|
|
64
|
+
* Checks whether an object exists in S3 and retrieves its content-type.
|
|
66
65
|
*
|
|
67
66
|
* @param s3Config - S3 client and bucket configuration
|
|
68
67
|
* @param key - S3 object key
|
|
@@ -43,7 +43,6 @@ export declare function getStorageModuleConfigForOwner(pgClient: {
|
|
|
43
43
|
/**
|
|
44
44
|
* Resolve the storage module that owns a specific file by probing all file tables.
|
|
45
45
|
*
|
|
46
|
-
* Used by confirmUpload when only a fileId (UUID) is available.
|
|
47
46
|
* Since UUIDs are globally unique, exactly one table will contain the file.
|
|
48
47
|
*
|
|
49
48
|
* @param pgClient - A pg client from the Graphile context
|
|
@@ -64,7 +63,6 @@ export declare function resolveStorageModuleByFileId(pgClient: {
|
|
|
64
63
|
id: string;
|
|
65
64
|
key: string;
|
|
66
65
|
mime_type: string;
|
|
67
|
-
status: string;
|
|
68
66
|
bucket_id: string;
|
|
69
67
|
};
|
|
70
68
|
} | null>;
|
package/storage-module-cache.js
CHANGED
|
@@ -24,8 +24,8 @@ const ONE_HOUR_MS = 1000 * 60 * 60;
|
|
|
24
24
|
* LRU cache for per-database StorageModuleConfig.
|
|
25
25
|
*
|
|
26
26
|
* Each PostGraphile instance serves a single database, but the presigned URL
|
|
27
|
-
* plugin needs to know the generated table names (buckets, files
|
|
28
|
-
*
|
|
27
|
+
* plugin needs to know the generated table names (buckets, files)
|
|
28
|
+
* and their schemas. This cache avoids re-querying metaschema
|
|
29
29
|
* on every request.
|
|
30
30
|
*
|
|
31
31
|
* Pattern: same as graphile-cache's LRU with TTL-based eviction.
|
|
@@ -52,8 +52,6 @@ const APP_STORAGE_MODULE_QUERY = `
|
|
|
52
52
|
bt.name AS buckets_table,
|
|
53
53
|
fs.schema_name AS files_schema,
|
|
54
54
|
ft.name AS files_table,
|
|
55
|
-
urs.schema_name AS upload_requests_schema,
|
|
56
|
-
urt.name AS upload_requests_table,
|
|
57
55
|
sm.endpoint,
|
|
58
56
|
sm.public_url_prefix,
|
|
59
57
|
sm.provider,
|
|
@@ -70,8 +68,6 @@ const APP_STORAGE_MODULE_QUERY = `
|
|
|
70
68
|
JOIN metaschema_public.schema bs ON bs.id = bt.schema_id
|
|
71
69
|
JOIN metaschema_public.table ft ON ft.id = sm.files_table_id
|
|
72
70
|
JOIN metaschema_public.schema fs ON fs.id = ft.schema_id
|
|
73
|
-
JOIN metaschema_public.table urt ON urt.id = sm.upload_requests_table_id
|
|
74
|
-
JOIN metaschema_public.schema urs ON urs.id = urt.schema_id
|
|
75
71
|
WHERE sm.database_id = $1
|
|
76
72
|
AND sm.membership_type IS NULL
|
|
77
73
|
LIMIT 1
|
|
@@ -91,8 +87,6 @@ const ALL_STORAGE_MODULES_QUERY = `
|
|
|
91
87
|
bt.name AS buckets_table,
|
|
92
88
|
fs.schema_name AS files_schema,
|
|
93
89
|
ft.name AS files_table,
|
|
94
|
-
urs.schema_name AS upload_requests_schema,
|
|
95
|
-
urt.name AS upload_requests_table,
|
|
96
90
|
sm.endpoint,
|
|
97
91
|
sm.public_url_prefix,
|
|
98
92
|
sm.provider,
|
|
@@ -109,8 +103,6 @@ const ALL_STORAGE_MODULES_QUERY = `
|
|
|
109
103
|
JOIN metaschema_public.schema bs ON bs.id = bt.schema_id
|
|
110
104
|
JOIN metaschema_public.table ft ON ft.id = sm.files_table_id
|
|
111
105
|
JOIN metaschema_public.schema fs ON fs.id = ft.schema_id
|
|
112
|
-
JOIN metaschema_public.table urt ON urt.id = sm.upload_requests_table_id
|
|
113
|
-
JOIN metaschema_public.schema urs ON urs.id = urt.schema_id
|
|
114
106
|
LEFT JOIN metaschema_public.table et ON et.id = sm.entity_table_id
|
|
115
107
|
LEFT JOIN metaschema_public.schema es ON es.id = et.schema_id
|
|
116
108
|
WHERE sm.database_id = $1
|
|
@@ -124,11 +116,9 @@ function buildConfig(row) {
|
|
|
124
116
|
id: row.id,
|
|
125
117
|
bucketsQualifiedName: quotes_1.QuoteUtils.quoteQualifiedIdentifier(row.buckets_schema, row.buckets_table),
|
|
126
118
|
filesQualifiedName: quotes_1.QuoteUtils.quoteQualifiedIdentifier(row.files_schema, row.files_table),
|
|
127
|
-
uploadRequestsQualifiedName: quotes_1.QuoteUtils.quoteQualifiedIdentifier(row.upload_requests_schema, row.upload_requests_table),
|
|
128
119
|
schemaName: row.buckets_schema,
|
|
129
120
|
bucketsTableName: row.buckets_table,
|
|
130
121
|
filesTableName: row.files_table,
|
|
131
|
-
uploadRequestsTableName: row.upload_requests_table,
|
|
132
122
|
membershipType: row.membership_type,
|
|
133
123
|
entityTableId: row.entity_table_id,
|
|
134
124
|
entityQualifiedName: row.entity_schema && row.entity_table
|
|
@@ -239,7 +229,6 @@ async function getStorageModuleConfigForOwner(pgClient, databaseId, ownerId) {
|
|
|
239
229
|
/**
|
|
240
230
|
* Resolve the storage module that owns a specific file by probing all file tables.
|
|
241
231
|
*
|
|
242
|
-
* Used by confirmUpload when only a fileId (UUID) is available.
|
|
243
232
|
* Since UUIDs are globally unique, exactly one table will contain the file.
|
|
244
233
|
*
|
|
245
234
|
* @param pgClient - A pg client from the Graphile context
|
|
@@ -254,7 +243,7 @@ async function resolveStorageModuleByFileId(pgClient, databaseId, fileId) {
|
|
|
254
243
|
// Probe each module's files table for the fileId
|
|
255
244
|
for (const config of allConfigs) {
|
|
256
245
|
const fileResult = await pgClient.query({
|
|
257
|
-
text: `SELECT id, key, mime_type,
|
|
246
|
+
text: `SELECT id, key, mime_type, bucket_id
|
|
258
247
|
FROM ${config.filesQualifiedName}
|
|
259
248
|
WHERE id = $1
|
|
260
249
|
LIMIT 1`,
|
package/types.d.ts
CHANGED
|
@@ -21,16 +21,12 @@ export interface StorageModuleConfig {
|
|
|
21
21
|
bucketsQualifiedName: string;
|
|
22
22
|
/** Resolved schema.table for files */
|
|
23
23
|
filesQualifiedName: string;
|
|
24
|
-
/** Resolved schema.table for upload_requests */
|
|
25
|
-
uploadRequestsQualifiedName: string;
|
|
26
24
|
/** Schema name (e.g., "app_public") */
|
|
27
25
|
schemaName: string;
|
|
28
26
|
/** Buckets table name */
|
|
29
27
|
bucketsTableName: string;
|
|
30
28
|
/** Files table name */
|
|
31
29
|
filesTableName: string;
|
|
32
|
-
/** Upload requests table name */
|
|
33
|
-
uploadRequestsTableName: string;
|
|
34
30
|
/** Membership type (NULL for app-level, non-NULL for entity-scoped) */
|
|
35
31
|
membershipType: number | null;
|
|
36
32
|
/** Entity table ID for entity-scoped storage (NULL for app-level) */
|
|
@@ -93,24 +89,6 @@ export interface RequestUploadUrlPayload {
|
|
|
93
89
|
/** Presigned URL expiry time (null if deduplicated) */
|
|
94
90
|
expiresAt: string | null;
|
|
95
91
|
}
|
|
96
|
-
/**
|
|
97
|
-
* Input for the confirmUpload mutation.
|
|
98
|
-
*/
|
|
99
|
-
export interface ConfirmUploadInput {
|
|
100
|
-
/** The file ID returned by requestUploadUrl */
|
|
101
|
-
fileId: string;
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Result of the confirmUpload mutation.
|
|
105
|
-
*/
|
|
106
|
-
export interface ConfirmUploadPayload {
|
|
107
|
-
/** The confirmed file ID */
|
|
108
|
-
fileId: string;
|
|
109
|
-
/** New file status (should be 'ready') */
|
|
110
|
-
status: string;
|
|
111
|
-
/** Whether confirmation succeeded */
|
|
112
|
-
success: boolean;
|
|
113
|
-
}
|
|
114
92
|
/**
|
|
115
93
|
* S3 configuration for the presigned URL plugin.
|
|
116
94
|
*/
|