Skip to content

Commit ac3ee50

Browse files
authored
Merge pull request #1049 from constructive-io/feat/storage-simplification
feat: remove confirmUpload, upload_requests, and files.status — simplify storage
2 parents 66f9e77 + 9b1a48a commit ac3ee50

18 files changed

Lines changed: 31 additions & 491 deletions

File tree

graphile/graphile-presigned-url-plugin/README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,9 @@ Presigned URL upload plugin for PostGraphile v5.
1717
## Features
1818

1919
- `requestUploadUrl` mutation — generates presigned PUT URLs for direct client-to-S3 upload
20-
- `confirmUpload` mutation — verifies upload and transitions file status to 'ready'
2120
- `downloadUrl` computed field — presigned GET URLs for private files, public URLs for public files
2221
- Content-hash based S3 keys (SHA-256) with automatic deduplication
2322
- Per-bucket MIME type and file size validation
24-
- Upload request tracking for audit and rate limiting
2523

2624
## Usage
2725

graphile/graphile-presigned-url-plugin/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "graphile-presigned-url-plugin",
33
"version": "0.7.0",
4-
"description": "Presigned URL upload plugin for PostGraphile v5 — requestUploadUrl, confirmUpload mutations and downloadUrl computed field",
4+
"description": "Presigned URL upload plugin for PostGraphile v5 — requestUploadUrl mutation and downloadUrl computed field",
55
"author": "Constructive <developers@constructive.io>",
66
"homepage": "https://github.com/constructive-io/constructive",
77
"license": "MIT",

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ export function createDownloadUrlPlugin(
125125
const $key = $parent.get('key');
126126
const $isPublic = $parent.get('is_public');
127127
const $filename = $parent.get('filename');
128-
const $status = $parent.get('status');
129128

130129
// Access GraphQL context for per-database config resolution
131130
const $withPgClient = (grafastContext() as any).get('withPgClient');
@@ -135,19 +134,13 @@ export function createDownloadUrlPlugin(
135134
key: $key,
136135
isPublic: $isPublic,
137136
filename: $filename,
138-
status: $status,
139137
withPgClient: $withPgClient,
140138
pgSettings: $pgSettings,
141139
});
142140

143-
return lambda($combined, async ({ key, isPublic, filename, status, withPgClient, pgSettings }: any) => {
141+
return lambda($combined, async ({ key, isPublic, filename, withPgClient, pgSettings }: any) => {
144142
if (!key) return null;
145143

146-
// Only provide download URLs for ready/processed files
147-
if (status !== 'ready' && status !== 'processed') {
148-
return null;
149-
}
150-
151144
// Resolve per-database config (bucket, publicUrlPrefix, expiry)
152145
let s3ForDb = resolveS3(options); // fallback to global
153146
let downloadUrlExpirySeconds = 3600; // fallback default

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
* Presigned URL Plugin for PostGraphile v5
33
*
44
* 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)
76
* - downloadUrl computed field (presigned GET URL / public URL)
87
*
98
* @example
@@ -37,8 +36,6 @@ export type {
3736
StorageModuleConfig,
3837
RequestUploadUrlInput,
3938
RequestUploadUrlPayload,
40-
ConfirmUploadInput,
41-
ConfirmUploadPayload,
4239
S3Config,
4340
S3ConfigOrGetter,
4441
PresignedUrlPluginOptions,

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

Lines changed: 10 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,9 @@
55
*
66
* 1. `requestUploadUrl` mutation — generates a presigned PUT URL for direct
77
* 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.
99
*
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
1511
* for private files, returns public URL prefix + key for public files.
1612
*
1713
* Uses the extendSchema + grafast plan pattern (same as PublicKeySignature).
@@ -23,8 +19,8 @@ import { extendSchema, gql } from 'graphile-utils';
2319
import { Logger } from '@pgpmjs/logger';
2420

2521
import type { PresignedUrlPluginOptions, S3Config, StorageModuleConfig, BucketConfig } from './types';
26-
import { getStorageModuleConfig, getStorageModuleConfigForOwner, getBucketConfig, resolveStorageModuleByFileId, isS3BucketProvisioned, markS3BucketProvisioned } from './storage-module-cache';
27-
import { generatePresignedPutUrl, headObject } from './s3-signer';
22+
import { getStorageModuleConfig, getStorageModuleConfigForOwner, getBucketConfig, isS3BucketProvisioned, markS3BucketProvisioned } from './storage-module-cache';
23+
import { generatePresignedPutUrl } from './s3-signer';
2824

2925
const log = new Logger('graphile-presigned-url:plugin');
3026

@@ -175,22 +171,6 @@ export function createPresignedUrlPlugin(
175171
deduplicated: Boolean!
176172
"""Presigned URL expiry time (null if deduplicated)"""
177173
expiresAt: Datetime
178-
"""File status — 'pending' for fresh uploads, 'ready' or 'processed' for deduplicated files. Clients can use this to know immediately whether the file is usable."""
179-
status: String!
180-
}
181-
182-
input ConfirmUploadInput {
183-
"""The file ID returned by requestUploadUrl"""
184-
fileId: UUID!
185-
}
186-
187-
type ConfirmUploadPayload {
188-
"""The confirmed file ID"""
189-
fileId: UUID!
190-
"""New file status"""
191-
status: String!
192-
"""Whether confirmation succeeded"""
193-
success: Boolean!
194174
}
195175
196176
extend type Mutation {
@@ -203,15 +183,6 @@ export function createPresignedUrlPlugin(
203183
requestUploadUrl(
204184
input: RequestUploadUrlInput!
205185
): RequestUploadUrlPayload
206-
207-
"""
208-
Confirm that a file has been uploaded to S3.
209-
Verifies the object exists in S3, checks content-type,
210-
and transitions the file status from 'pending' to 'ready'.
211-
"""
212-
confirmUpload(
213-
input: ConfirmUploadInput!
214-
): ConfirmUploadPayload
215186
}
216187
`,
217188
plans: {
@@ -304,11 +275,10 @@ export function createPresignedUrlPlugin(
304275

305276
// --- Dedup check: look for existing file with same key (content hash) in this bucket ---
306277
const dedupResult = await txClient.query({
307-
text: `SELECT id, status
278+
text: `SELECT id
308279
FROM ${storageConfig.filesQualifiedName}
309280
WHERE key = $1
310281
AND bucket_id = $2
311-
AND status IN ('ready', 'processed')
312282
LIMIT 1`,
313283
values: [s3Key, bucket.id],
314284
});
@@ -317,36 +287,27 @@ export function createPresignedUrlPlugin(
317287
const existingFile = dedupResult.rows[0];
318288
log.info(`Dedup hit: file ${existingFile.id} for hash ${contentHash}`);
319289

320-
// Track the dedup request
321-
await txClient.query({
322-
text: `INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
323-
(file_id, bucket_id, key, content_type, content_hash, status, expires_at)
324-
VALUES ($1, $2, $3, $4, $5, 'confirmed', NOW())`,
325-
values: [existingFile.id, bucket.id, s3Key, contentType, contentHash],
326-
});
327-
328290
return {
329291
uploadUrl: null as string | null,
330292
fileId: existingFile.id as string,
331293
key: s3Key,
332294
deduplicated: true,
333295
expiresAt: null as string | null,
334-
status: existingFile.status as string,
335296
};
336297
}
337298

338-
// --- Create file record (status=pending) ---
299+
// --- Create file record ---
339300
// For app-level storage (no owner_id column), omit owner_id from the INSERT.
340301
const hasOwnerColumn = storageConfig.membershipType !== null;
341302
const fileResult = await txClient.query({
342303
text: hasOwnerColumn
343304
? `INSERT INTO ${storageConfig.filesQualifiedName}
344-
(bucket_id, key, mime_type, size, filename, owner_id, is_public, status)
345-
VALUES ($1, $2, $3, $4, $5, $6, $7, 'pending')
305+
(bucket_id, key, mime_type, size, filename, owner_id, is_public)
306+
VALUES ($1, $2, $3, $4, $5, $6, $7)
346307
RETURNING id`
347308
: `INSERT INTO ${storageConfig.filesQualifiedName}
348-
(bucket_id, key, mime_type, size, filename, is_public, status)
349-
VALUES ($1, $2, $3, $4, $5, $6, 'pending')
309+
(bucket_id, key, mime_type, size, filename, is_public)
310+
VALUES ($1, $2, $3, $4, $5, $6)
350311
RETURNING id`,
351312
values: hasOwnerColumn
352313
? [
@@ -385,111 +346,12 @@ export function createPresignedUrlPlugin(
385346

386347
const expiresAt = new Date(Date.now() + storageConfig.uploadUrlExpirySeconds * 1000).toISOString();
387348

388-
// --- Track the upload request ---
389-
await txClient.query({
390-
text: `INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
391-
(file_id, bucket_id, key, content_type, content_hash, status, expires_at)
392-
VALUES ($1, $2, $3, $4, $5, 'issued', $6)`,
393-
values: [fileId, bucket.id, s3Key, contentType, contentHash, expiresAt],
394-
});
395-
396349
return {
397350
uploadUrl,
398351
fileId,
399352
key: s3Key,
400353
deduplicated: false,
401354
expiresAt,
402-
status: 'pending',
403-
};
404-
});
405-
});
406-
});
407-
},
408-
409-
confirmUpload(_$mutation: any, fieldArgs: any) {
410-
const $input = fieldArgs.getRaw('input');
411-
const $withPgClient = (grafastContext() as any).get('withPgClient');
412-
const $pgSettings = (grafastContext() as any).get('pgSettings');
413-
const $combined = object({
414-
input: $input,
415-
withPgClient: $withPgClient,
416-
pgSettings: $pgSettings,
417-
});
418-
419-
return lambda($combined, async ({ input, withPgClient, pgSettings }: any) => {
420-
const { fileId } = input;
421-
422-
if (!fileId || typeof fileId !== 'string') {
423-
throw new Error('INVALID_FILE_ID');
424-
}
425-
426-
return withPgClient(pgSettings, async (pgClient: any) => {
427-
return pgClient.withTransaction(async (txClient: any) => {
428-
// --- Resolve storage module by file ID (probes all file tables) ---
429-
const databaseId = await resolveDatabaseId(txClient);
430-
if (!databaseId) {
431-
throw new Error('DATABASE_NOT_FOUND');
432-
}
433-
434-
const resolved = await resolveStorageModuleByFileId(txClient, databaseId, fileId);
435-
if (!resolved) {
436-
throw new Error('FILE_NOT_FOUND');
437-
}
438-
439-
const { storageConfig, file } = resolved;
440-
441-
if (file.status !== 'pending') {
442-
// File is already confirmed or processed — idempotent success
443-
return {
444-
fileId: file.id,
445-
status: file.status,
446-
success: true,
447-
};
448-
}
449-
450-
// --- Verify file exists in S3 (per-database bucket) ---
451-
const s3ForDb = resolveS3ForDatabase(options, storageConfig, databaseId);
452-
const s3Head = await headObject(s3ForDb, file.key, file.mime_type as string);
453-
454-
if (!s3Head) {
455-
throw new Error('FILE_NOT_IN_S3: the file has not been uploaded yet');
456-
}
457-
458-
// --- Content-type verification ---
459-
if (s3Head.contentType && s3Head.contentType !== file.mime_type) {
460-
// Mark upload_request as rejected
461-
await txClient.query({
462-
text: `UPDATE ${storageConfig.uploadRequestsQualifiedName}
463-
SET status = 'rejected'
464-
WHERE file_id = $1 AND status = 'issued'`,
465-
values: [fileId],
466-
});
467-
468-
throw new Error(
469-
`CONTENT_TYPE_MISMATCH: expected ${file.mime_type}, got ${s3Head.contentType}`,
470-
);
471-
}
472-
473-
// --- Transition file to 'ready' ---
474-
await txClient.query({
475-
text: `UPDATE ${storageConfig.filesQualifiedName}
476-
SET status = 'ready'
477-
WHERE id = $1`,
478-
values: [fileId],
479-
});
480-
481-
// --- Update upload_request to 'confirmed' ---
482-
await txClient.query({
483-
text: `UPDATE ${storageConfig.uploadRequestsQualifiedName}
484-
SET status = 'confirmed', confirmed_at = NOW()
485-
WHERE file_id = $1 AND status = 'issued'`,
486-
values: [fileId],
487-
});
488-
489-
return {
490-
fileId: file.id,
491-
status: 'ready',
492-
success: true,
493355
};
494356
});
495357
});

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
* PostGraphile v5 Presigned URL Preset
33
*
44
* 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.
77
*/
88

99
import type { GraphileConfig } from 'graphile-config';

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,7 @@ export async function generatePresignedGetUrl(
8080
/**
8181
* Check if an object exists in S3 and optionally verify its content-type.
8282
*
83-
* Used by confirmUpload to verify the file was actually uploaded to S3
84-
* and that the content-type matches what was declared.
83+
* Checks whether an object exists in S3 and retrieves its content-type.
8584
*
8685
* @param s3Config - S3 client and bucket configuration
8786
* @param key - S3 object key

0 commit comments

Comments
 (0)