graphile-presigned-url-plugin 0.7.0 → 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 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
 
@@ -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, status, withPgClient, pgSettings }) => {
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, status, withPgClient, pgSettings }) => {
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, ConfirmUploadInput, ConfirmUploadPayload, S3Config, S3ConfigOrGetter, PresignedUrlPluginOptions, BucketNameResolver, EnsureBucketProvisioned, } from './types';
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, tracks the request in upload_requests.
8
+ * content hash via UNIQUE(bucket_id, key) constraint.
9
9
  *
10
- * 2. `confirmUpload` mutation confirms a file was uploaded to S3, verifies
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, tracks the request in upload_requests.
8
+ * content hash via UNIQUE(bucket_id, key) constraint.
9
9
  *
10
- * 2. `confirmUpload` mutation confirms a file was uploaded to S3, verifies
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, resolveStorageModuleByFileId, isS3BucketProvisioned, markS3BucketProvisioned } from './storage-module-cache';
23
- import { generatePresignedPutUrl, headObject } from './s3-signer';
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;
@@ -145,22 +141,6 @@ export function createPresignedUrlPlugin(options) {
145
141
  deduplicated: Boolean!
146
142
  """Presigned URL expiry time (null if deduplicated)"""
147
143
  expiresAt: Datetime
148
- """File status — 'pending' for fresh uploads, 'ready' or 'processed' for deduplicated files. Clients can use this to know immediately whether the file is usable."""
149
- status: String!
150
- }
151
-
152
- input ConfirmUploadInput {
153
- """The file ID returned by requestUploadUrl"""
154
- fileId: UUID!
155
- }
156
-
157
- type ConfirmUploadPayload {
158
- """The confirmed file ID"""
159
- fileId: UUID!
160
- """New file status"""
161
- status: String!
162
- """Whether confirmation succeeded"""
163
- success: Boolean!
164
144
  }
165
145
 
166
146
  extend type Mutation {
@@ -173,15 +153,6 @@ export function createPresignedUrlPlugin(options) {
173
153
  requestUploadUrl(
174
154
  input: RequestUploadUrlInput!
175
155
  ): RequestUploadUrlPayload
176
-
177
- """
178
- Confirm that a file has been uploaded to S3.
179
- Verifies the object exists in S3, checks content-type,
180
- and transitions the file status from 'pending' to 'ready'.
181
- """
182
- confirmUpload(
183
- input: ConfirmUploadInput!
184
- ): ConfirmUploadPayload
185
156
  }
186
157
  `,
187
158
  plans: {
@@ -263,45 +234,36 @@ export function createPresignedUrlPlugin(options) {
263
234
  const s3Key = buildS3Key(contentHash);
264
235
  // --- Dedup check: look for existing file with same key (content hash) in this bucket ---
265
236
  const dedupResult = await txClient.query({
266
- text: `SELECT id, status
237
+ text: `SELECT id
267
238
  FROM ${storageConfig.filesQualifiedName}
268
239
  WHERE key = $1
269
240
  AND bucket_id = $2
270
- AND status IN ('ready', 'processed')
271
241
  LIMIT 1`,
272
242
  values: [s3Key, bucket.id],
273
243
  });
274
244
  if (dedupResult.rows.length > 0) {
275
245
  const existingFile = dedupResult.rows[0];
276
246
  log.info(`Dedup hit: file ${existingFile.id} for hash ${contentHash}`);
277
- // Track the dedup request
278
- await txClient.query({
279
- text: `INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
280
- (file_id, bucket_id, key, content_type, content_hash, status, expires_at)
281
- VALUES ($1, $2, $3, $4, $5, 'confirmed', NOW())`,
282
- values: [existingFile.id, bucket.id, s3Key, contentType, contentHash],
283
- });
284
247
  return {
285
248
  uploadUrl: null,
286
249
  fileId: existingFile.id,
287
250
  key: s3Key,
288
251
  deduplicated: true,
289
252
  expiresAt: null,
290
- status: existingFile.status,
291
253
  };
292
254
  }
293
- // --- Create file record (status=pending) ---
255
+ // --- Create file record ---
294
256
  // For app-level storage (no owner_id column), omit owner_id from the INSERT.
295
257
  const hasOwnerColumn = storageConfig.membershipType !== null;
296
258
  const fileResult = await txClient.query({
297
259
  text: hasOwnerColumn
298
260
  ? `INSERT INTO ${storageConfig.filesQualifiedName}
299
- (bucket_id, key, mime_type, size, filename, owner_id, is_public, status)
300
- VALUES ($1, $2, $3, $4, $5, $6, $7, 'pending')
261
+ (bucket_id, key, mime_type, size, filename, owner_id, is_public)
262
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
301
263
  RETURNING id`
302
264
  : `INSERT INTO ${storageConfig.filesQualifiedName}
303
- (bucket_id, key, mime_type, size, filename, is_public, status)
304
- VALUES ($1, $2, $3, $4, $5, $6, 'pending')
265
+ (bucket_id, key, mime_type, size, filename, is_public)
266
+ VALUES ($1, $2, $3, $4, $5, $6)
305
267
  RETURNING id`,
306
268
  values: hasOwnerColumn
307
269
  ? [
@@ -329,94 +291,12 @@ export function createPresignedUrlPlugin(options) {
329
291
  // --- Generate presigned PUT URL (per-database bucket) ---
330
292
  const uploadUrl = await generatePresignedPutUrl(s3ForDb, s3Key, contentType, size, storageConfig.uploadUrlExpirySeconds);
331
293
  const expiresAt = new Date(Date.now() + storageConfig.uploadUrlExpirySeconds * 1000).toISOString();
332
- // --- Track the upload request ---
333
- await txClient.query({
334
- text: `INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
335
- (file_id, bucket_id, key, content_type, content_hash, status, expires_at)
336
- VALUES ($1, $2, $3, $4, $5, 'issued', $6)`,
337
- values: [fileId, bucket.id, s3Key, contentType, contentHash, expiresAt],
338
- });
339
294
  return {
340
295
  uploadUrl,
341
296
  fileId,
342
297
  key: s3Key,
343
298
  deduplicated: false,
344
299
  expiresAt,
345
- status: 'pending',
346
- };
347
- });
348
- });
349
- });
350
- },
351
- confirmUpload(_$mutation, fieldArgs) {
352
- const $input = fieldArgs.getRaw('input');
353
- const $withPgClient = grafastContext().get('withPgClient');
354
- const $pgSettings = grafastContext().get('pgSettings');
355
- const $combined = object({
356
- input: $input,
357
- withPgClient: $withPgClient,
358
- pgSettings: $pgSettings,
359
- });
360
- return 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 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 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
300
  };
421
301
  });
422
302
  });
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
- * confirmUpload) with the downloadUrl computed field plugin.
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
- * confirmUpload) with the downloadUrl computed field plugin.
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';
@@ -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
- * Used by confirmUpload to verify the file was actually uploaded to S3
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
- * Used by confirmUpload to verify the file was actually uploaded to S3
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
- * upload_requests) and their schemas. This cache avoids re-querying metaschema
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, status, bucket_id
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) */
@@ -92,26 +88,6 @@ export interface RequestUploadUrlPayload {
92
88
  deduplicated: boolean;
93
89
  /** Presigned URL expiry time (null if deduplicated) */
94
90
  expiresAt: string | null;
95
- /** File status — 'pending' for fresh uploads, 'ready' or 'processed' for deduplicated files */
96
- status: string;
97
- }
98
- /**
99
- * Input for the confirmUpload mutation.
100
- */
101
- export interface ConfirmUploadInput {
102
- /** The file ID returned by requestUploadUrl */
103
- fileId: string;
104
- }
105
- /**
106
- * Result of the confirmUpload mutation.
107
- */
108
- export interface ConfirmUploadPayload {
109
- /** The confirmed file ID */
110
- fileId: string;
111
- /** New file status (should be 'ready') */
112
- status: string;
113
- /** Whether confirmation succeeded */
114
- success: boolean;
115
91
  }
116
92
  /**
117
93
  * S3 configuration for the presigned URL plugin.
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, ConfirmUploadInput, ConfirmUploadPayload, S3Config, S3ConfigOrGetter, PresignedUrlPluginOptions, BucketNameResolver, EnsureBucketProvisioned, } from './types';
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.7.0",
4
- "description": "Presigned URL upload plugin for PostGraphile v5 — requestUploadUrl, confirmUpload mutations and downloadUrl computed field",
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": "058b8200e99eb505477d1599ee0d5ab795aa0123"
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, tracks the request in upload_requests.
8
+ * content hash via UNIQUE(bucket_id, key) constraint.
9
9
  *
10
- * 2. `confirmUpload` mutation confirms a file was uploaded to S3, verifies
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, tracks the request in upload_requests.
9
+ * content hash via UNIQUE(bucket_id, key) constraint.
10
10
  *
11
- * 2. `confirmUpload` mutation confirms a file was uploaded to S3, verifies
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).
@@ -149,22 +145,6 @@ function createPresignedUrlPlugin(options) {
149
145
  deduplicated: Boolean!
150
146
  """Presigned URL expiry time (null if deduplicated)"""
151
147
  expiresAt: Datetime
152
- """File status — 'pending' for fresh uploads, 'ready' or 'processed' for deduplicated files. Clients can use this to know immediately whether the file is usable."""
153
- status: String!
154
- }
155
-
156
- input ConfirmUploadInput {
157
- """The file ID returned by requestUploadUrl"""
158
- fileId: UUID!
159
- }
160
-
161
- type ConfirmUploadPayload {
162
- """The confirmed file ID"""
163
- fileId: UUID!
164
- """New file status"""
165
- status: String!
166
- """Whether confirmation succeeded"""
167
- success: Boolean!
168
148
  }
169
149
 
170
150
  extend type Mutation {
@@ -177,15 +157,6 @@ function createPresignedUrlPlugin(options) {
177
157
  requestUploadUrl(
178
158
  input: RequestUploadUrlInput!
179
159
  ): RequestUploadUrlPayload
180
-
181
- """
182
- Confirm that a file has been uploaded to S3.
183
- Verifies the object exists in S3, checks content-type,
184
- and transitions the file status from 'pending' to 'ready'.
185
- """
186
- confirmUpload(
187
- input: ConfirmUploadInput!
188
- ): ConfirmUploadPayload
189
160
  }
190
161
  `,
191
162
  plans: {
@@ -267,45 +238,36 @@ function createPresignedUrlPlugin(options) {
267
238
  const s3Key = buildS3Key(contentHash);
268
239
  // --- Dedup check: look for existing file with same key (content hash) in this bucket ---
269
240
  const dedupResult = await txClient.query({
270
- text: `SELECT id, status
241
+ text: `SELECT id
271
242
  FROM ${storageConfig.filesQualifiedName}
272
243
  WHERE key = $1
273
244
  AND bucket_id = $2
274
- AND status IN ('ready', 'processed')
275
245
  LIMIT 1`,
276
246
  values: [s3Key, bucket.id],
277
247
  });
278
248
  if (dedupResult.rows.length > 0) {
279
249
  const existingFile = dedupResult.rows[0];
280
250
  log.info(`Dedup hit: file ${existingFile.id} for hash ${contentHash}`);
281
- // Track the dedup request
282
- await txClient.query({
283
- text: `INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
284
- (file_id, bucket_id, key, content_type, content_hash, status, expires_at)
285
- VALUES ($1, $2, $3, $4, $5, 'confirmed', NOW())`,
286
- values: [existingFile.id, bucket.id, s3Key, contentType, contentHash],
287
- });
288
251
  return {
289
252
  uploadUrl: null,
290
253
  fileId: existingFile.id,
291
254
  key: s3Key,
292
255
  deduplicated: true,
293
256
  expiresAt: null,
294
- status: existingFile.status,
295
257
  };
296
258
  }
297
- // --- Create file record (status=pending) ---
259
+ // --- Create file record ---
298
260
  // For app-level storage (no owner_id column), omit owner_id from the INSERT.
299
261
  const hasOwnerColumn = storageConfig.membershipType !== null;
300
262
  const fileResult = await txClient.query({
301
263
  text: hasOwnerColumn
302
264
  ? `INSERT INTO ${storageConfig.filesQualifiedName}
303
- (bucket_id, key, mime_type, size, filename, owner_id, is_public, status)
304
- VALUES ($1, $2, $3, $4, $5, $6, $7, 'pending')
265
+ (bucket_id, key, mime_type, size, filename, owner_id, is_public)
266
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
305
267
  RETURNING id`
306
268
  : `INSERT INTO ${storageConfig.filesQualifiedName}
307
- (bucket_id, key, mime_type, size, filename, is_public, status)
308
- VALUES ($1, $2, $3, $4, $5, $6, 'pending')
269
+ (bucket_id, key, mime_type, size, filename, is_public)
270
+ VALUES ($1, $2, $3, $4, $5, $6)
309
271
  RETURNING id`,
310
272
  values: hasOwnerColumn
311
273
  ? [
@@ -333,94 +295,12 @@ function createPresignedUrlPlugin(options) {
333
295
  // --- Generate presigned PUT URL (per-database bucket) ---
334
296
  const uploadUrl = await (0, s3_signer_1.generatePresignedPutUrl)(s3ForDb, s3Key, contentType, size, storageConfig.uploadUrlExpirySeconds);
335
297
  const expiresAt = new Date(Date.now() + storageConfig.uploadUrlExpirySeconds * 1000).toISOString();
336
- // --- Track the upload request ---
337
- await txClient.query({
338
- text: `INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
339
- (file_id, bucket_id, key, content_type, content_hash, status, expires_at)
340
- VALUES ($1, $2, $3, $4, $5, 'issued', $6)`,
341
- values: [fileId, bucket.id, s3Key, contentType, contentHash, expiresAt],
342
- });
343
298
  return {
344
299
  uploadUrl,
345
300
  fileId,
346
301
  key: s3Key,
347
302
  deduplicated: false,
348
303
  expiresAt,
349
- status: 'pending',
350
- };
351
- });
352
- });
353
- });
354
- },
355
- confirmUpload(_$mutation, fieldArgs) {
356
- const $input = fieldArgs.getRaw('input');
357
- const $withPgClient = (0, grafast_1.context)().get('withPgClient');
358
- const $pgSettings = (0, grafast_1.context)().get('pgSettings');
359
- const $combined = (0, grafast_1.object)({
360
- input: $input,
361
- withPgClient: $withPgClient,
362
- pgSettings: $pgSettings,
363
- });
364
- return (0, grafast_1.lambda)($combined, async ({ input, withPgClient, pgSettings }) => {
365
- const { fileId } = input;
366
- if (!fileId || typeof fileId !== 'string') {
367
- throw new Error('INVALID_FILE_ID');
368
- }
369
- return withPgClient(pgSettings, async (pgClient) => {
370
- return pgClient.withTransaction(async (txClient) => {
371
- // --- Resolve storage module by file ID (probes all file tables) ---
372
- const databaseId = await resolveDatabaseId(txClient);
373
- if (!databaseId) {
374
- throw new Error('DATABASE_NOT_FOUND');
375
- }
376
- const resolved = await (0, storage_module_cache_1.resolveStorageModuleByFileId)(txClient, databaseId, fileId);
377
- if (!resolved) {
378
- throw new Error('FILE_NOT_FOUND');
379
- }
380
- const { storageConfig, file } = resolved;
381
- if (file.status !== 'pending') {
382
- // File is already confirmed or processed — idempotent success
383
- return {
384
- fileId: file.id,
385
- status: file.status,
386
- success: true,
387
- };
388
- }
389
- // --- Verify file exists in S3 (per-database bucket) ---
390
- const s3ForDb = resolveS3ForDatabase(options, storageConfig, databaseId);
391
- const s3Head = await (0, s3_signer_1.headObject)(s3ForDb, file.key, file.mime_type);
392
- if (!s3Head) {
393
- throw new Error('FILE_NOT_IN_S3: the file has not been uploaded yet');
394
- }
395
- // --- Content-type verification ---
396
- if (s3Head.contentType && s3Head.contentType !== file.mime_type) {
397
- // Mark upload_request as rejected
398
- await txClient.query({
399
- text: `UPDATE ${storageConfig.uploadRequestsQualifiedName}
400
- SET status = 'rejected'
401
- WHERE file_id = $1 AND status = 'issued'`,
402
- values: [fileId],
403
- });
404
- throw new Error(`CONTENT_TYPE_MISMATCH: expected ${file.mime_type}, got ${s3Head.contentType}`);
405
- }
406
- // --- Transition file to 'ready' ---
407
- await txClient.query({
408
- text: `UPDATE ${storageConfig.filesQualifiedName}
409
- SET status = 'ready'
410
- WHERE id = $1`,
411
- values: [fileId],
412
- });
413
- // --- Update upload_request to 'confirmed' ---
414
- await txClient.query({
415
- text: `UPDATE ${storageConfig.uploadRequestsQualifiedName}
416
- SET status = 'confirmed', confirmed_at = NOW()
417
- WHERE file_id = $1 AND status = 'issued'`,
418
- values: [fileId],
419
- });
420
- return {
421
- fileId: file.id,
422
- status: 'ready',
423
- success: true,
424
304
  };
425
305
  });
426
306
  });
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
- * confirmUpload) with the downloadUrl computed field plugin.
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
- * confirmUpload) with the downloadUrl computed field plugin.
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
- * Used by confirmUpload to verify the file was actually uploaded to S3
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
- * Used by confirmUpload to verify the file was actually uploaded to S3
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>;
@@ -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
- * upload_requests) and their schemas. This cache avoids re-querying metaschema
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, status, bucket_id
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) */
@@ -92,26 +88,6 @@ export interface RequestUploadUrlPayload {
92
88
  deduplicated: boolean;
93
89
  /** Presigned URL expiry time (null if deduplicated) */
94
90
  expiresAt: string | null;
95
- /** File status — 'pending' for fresh uploads, 'ready' or 'processed' for deduplicated files */
96
- status: string;
97
- }
98
- /**
99
- * Input for the confirmUpload mutation.
100
- */
101
- export interface ConfirmUploadInput {
102
- /** The file ID returned by requestUploadUrl */
103
- fileId: string;
104
- }
105
- /**
106
- * Result of the confirmUpload mutation.
107
- */
108
- export interface ConfirmUploadPayload {
109
- /** The confirmed file ID */
110
- fileId: string;
111
- /** New file status (should be 'ready') */
112
- status: string;
113
- /** Whether confirmation succeeded */
114
- success: boolean;
115
91
  }
116
92
  /**
117
93
  * S3 configuration for the presigned URL plugin.