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 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;
@@ -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, status
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 (status=pending) ---
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, status)
297
- 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)
298
263
  RETURNING id`
299
264
  : `INSERT INTO ${storageConfig.filesQualifiedName}
300
- (bucket_id, key, mime_type, size, filename, is_public, status)
301
- 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)
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
- * 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) */
@@ -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, 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.6.5",
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": "a8ccaed4f3a7c6c82495241ee1581700db9dd2dd"
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).
@@ -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, status
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 (status=pending) ---
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, status)
301
- 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)
302
267
  RETURNING id`
303
268
  : `INSERT INTO ${storageConfig.filesQualifiedName}
304
- (bucket_id, key, mime_type, size, filename, is_public, status)
305
- 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)
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
- * 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) */
@@ -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
  */