graphile-presigned-url-plugin 0.7.0 → 0.9.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/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>;
@@ -18,14 +18,16 @@ const DEFAULT_DOWNLOAD_URL_EXPIRY_SECONDS = 3600; // 1 hour
18
18
  const DEFAULT_MAX_FILE_SIZE = 200 * 1024 * 1024; // 200MB
19
19
  const DEFAULT_MAX_FILENAME_LENGTH = 1024;
20
20
  const DEFAULT_CACHE_TTL_SECONDS = process.env.NODE_ENV === 'development' ? 300 : 3600;
21
+ const DEFAULT_MAX_BULK_FILES = 100;
22
+ const DEFAULT_MAX_BULK_TOTAL_SIZE = 1073741824; // 1GB
21
23
  const FIVE_MINUTES_MS = 1000 * 60 * 5;
22
24
  const ONE_HOUR_MS = 1000 * 60 * 60;
23
25
  /**
24
26
  * LRU cache for per-database StorageModuleConfig.
25
27
  *
26
28
  * 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
29
+ * plugin needs to know the generated table names (buckets, files)
30
+ * and their schemas. This cache avoids re-querying metaschema
29
31
  * on every request.
30
32
  *
31
33
  * Pattern: same as graphile-cache's LRU with TTL-based eviction.
@@ -52,8 +54,6 @@ const APP_STORAGE_MODULE_QUERY = `
52
54
  bt.name AS buckets_table,
53
55
  fs.schema_name AS files_schema,
54
56
  ft.name AS files_table,
55
- urs.schema_name AS upload_requests_schema,
56
- urt.name AS upload_requests_table,
57
57
  sm.endpoint,
58
58
  sm.public_url_prefix,
59
59
  sm.provider,
@@ -63,6 +63,9 @@ const APP_STORAGE_MODULE_QUERY = `
63
63
  sm.default_max_file_size,
64
64
  sm.max_filename_length,
65
65
  sm.cache_ttl_seconds,
66
+ sm.max_bulk_files,
67
+ sm.max_bulk_total_size,
68
+ sm.has_path_shares,
66
69
  NULL AS entity_schema,
67
70
  NULL AS entity_table
68
71
  FROM metaschema_modules_public.storage_module sm
@@ -70,8 +73,6 @@ const APP_STORAGE_MODULE_QUERY = `
70
73
  JOIN metaschema_public.schema bs ON bs.id = bt.schema_id
71
74
  JOIN metaschema_public.table ft ON ft.id = sm.files_table_id
72
75
  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
76
  WHERE sm.database_id = $1
76
77
  AND sm.membership_type IS NULL
77
78
  LIMIT 1
@@ -91,8 +92,6 @@ const ALL_STORAGE_MODULES_QUERY = `
91
92
  bt.name AS buckets_table,
92
93
  fs.schema_name AS files_schema,
93
94
  ft.name AS files_table,
94
- urs.schema_name AS upload_requests_schema,
95
- urt.name AS upload_requests_table,
96
95
  sm.endpoint,
97
96
  sm.public_url_prefix,
98
97
  sm.provider,
@@ -102,6 +101,9 @@ const ALL_STORAGE_MODULES_QUERY = `
102
101
  sm.default_max_file_size,
103
102
  sm.max_filename_length,
104
103
  sm.cache_ttl_seconds,
104
+ sm.max_bulk_files,
105
+ sm.max_bulk_total_size,
106
+ sm.has_path_shares,
105
107
  es.schema_name AS entity_schema,
106
108
  et.name AS entity_table
107
109
  FROM metaschema_modules_public.storage_module sm
@@ -109,8 +111,6 @@ const ALL_STORAGE_MODULES_QUERY = `
109
111
  JOIN metaschema_public.schema bs ON bs.id = bt.schema_id
110
112
  JOIN metaschema_public.table ft ON ft.id = sm.files_table_id
111
113
  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
114
  LEFT JOIN metaschema_public.table et ON et.id = sm.entity_table_id
115
115
  LEFT JOIN metaschema_public.schema es ON es.id = et.schema_id
116
116
  WHERE sm.database_id = $1
@@ -124,11 +124,9 @@ function buildConfig(row) {
124
124
  id: row.id,
125
125
  bucketsQualifiedName: quotes_1.QuoteUtils.quoteQualifiedIdentifier(row.buckets_schema, row.buckets_table),
126
126
  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
127
  schemaName: row.buckets_schema,
129
128
  bucketsTableName: row.buckets_table,
130
129
  filesTableName: row.files_table,
131
- uploadRequestsTableName: row.upload_requests_table,
132
130
  membershipType: row.membership_type,
133
131
  entityTableId: row.entity_table_id,
134
132
  entityQualifiedName: row.entity_schema && row.entity_table
@@ -143,6 +141,9 @@ function buildConfig(row) {
143
141
  defaultMaxFileSize: row.default_max_file_size ?? DEFAULT_MAX_FILE_SIZE,
144
142
  maxFilenameLength: row.max_filename_length ?? DEFAULT_MAX_FILENAME_LENGTH,
145
143
  cacheTtlSeconds,
144
+ hasPathShares: row.has_path_shares ?? false,
145
+ maxBulkFiles: row.max_bulk_files ?? DEFAULT_MAX_BULK_FILES,
146
+ maxBulkTotalSize: row.max_bulk_total_size ?? DEFAULT_MAX_BULK_TOTAL_SIZE,
146
147
  };
147
148
  }
148
149
  /**
@@ -239,7 +240,6 @@ async function getStorageModuleConfigForOwner(pgClient, databaseId, ownerId) {
239
240
  /**
240
241
  * Resolve the storage module that owns a specific file by probing all file tables.
241
242
  *
242
- * Used by confirmUpload when only a fileId (UUID) is available.
243
243
  * Since UUIDs are globally unique, exactly one table will contain the file.
244
244
  *
245
245
  * @param pgClient - A pg client from the Graphile context
@@ -254,7 +254,7 @@ async function resolveStorageModuleByFileId(pgClient, databaseId, fileId) {
254
254
  // Probe each module's files table for the fileId
255
255
  for (const config of allConfigs) {
256
256
  const fileResult = await pgClient.query({
257
- text: `SELECT id, key, mime_type, status, bucket_id
257
+ text: `SELECT id, key, mime_type, bucket_id
258
258
  FROM ${config.filesQualifiedName}
259
259
  WHERE id = $1
260
260
  LIMIT 1`,
@@ -312,11 +312,11 @@ async function getBucketConfig(pgClient, storageConfig, databaseId, bucketKey, o
312
312
  const hasOwner = ownerId && storageConfig.membershipType !== null;
313
313
  const result = await pgClient.query({
314
314
  text: hasOwner
315
- ? `SELECT id, key, type, is_public, owner_id, allowed_mime_types, max_file_size
315
+ ? `SELECT id, key, type, is_public, owner_id, allowed_mime_types, max_file_size, allow_custom_keys
316
316
  FROM ${storageConfig.bucketsQualifiedName}
317
317
  WHERE key = $1 AND owner_id = $2
318
318
  LIMIT 1`
319
- : `SELECT id, key, type, is_public, ${storageConfig.membershipType !== null ? 'owner_id,' : ''} allowed_mime_types, max_file_size
319
+ : `SELECT id, key, type, is_public, ${storageConfig.membershipType !== null ? 'owner_id,' : ''} allowed_mime_types, max_file_size, allow_custom_keys
320
320
  FROM ${storageConfig.bucketsQualifiedName}
321
321
  WHERE key = $1
322
322
  LIMIT 1`,
@@ -334,6 +334,7 @@ async function getBucketConfig(pgClient, storageConfig, databaseId, bucketKey, o
334
334
  owner_id: row.owner_id ?? null,
335
335
  allowed_mime_types: row.allowed_mime_types,
336
336
  max_file_size: row.max_file_size,
337
+ allow_custom_keys: row.allow_custom_keys ?? false,
337
338
  };
338
339
  bucketCache.set(cacheKey, config);
339
340
  log.debug(`Cached bucket config for ${databaseId}:${bucketKey} (id=${config.id}, scope=${storageConfig.membershipType ?? 'app'})`);
package/types.d.ts CHANGED
@@ -10,6 +10,7 @@ export interface BucketConfig {
10
10
  owner_id: string | null;
11
11
  allowed_mime_types: string[] | null;
12
12
  max_file_size: number | null;
13
+ allow_custom_keys: boolean;
13
14
  }
14
15
  /**
15
16
  * Storage module configuration resolved from metaschema for a given database.
@@ -21,16 +22,12 @@ export interface StorageModuleConfig {
21
22
  bucketsQualifiedName: string;
22
23
  /** Resolved schema.table for files */
23
24
  filesQualifiedName: string;
24
- /** Resolved schema.table for upload_requests */
25
- uploadRequestsQualifiedName: string;
26
25
  /** Schema name (e.g., "app_public") */
27
26
  schemaName: string;
28
27
  /** Buckets table name */
29
28
  bucketsTableName: string;
30
29
  /** Files table name */
31
30
  filesTableName: string;
32
- /** Upload requests table name */
33
- uploadRequestsTableName: string;
34
31
  /** Membership type (NULL for app-level, non-NULL for entity-scoped) */
35
32
  membershipType: number | null;
36
33
  /** Entity table ID for entity-scoped storage (NULL for app-level) */
@@ -55,6 +52,12 @@ export interface StorageModuleConfig {
55
52
  maxFilenameLength: number;
56
53
  /** Cache TTL in seconds for this config entry (default: 300 dev / 3600 prod) */
57
54
  cacheTtlSeconds: number;
55
+ /** Whether this storage module uses ltree path + path shares (determines if path column exists on files) */
56
+ hasPathShares: boolean;
57
+ /** Max files per requestBulkUploadUrls batch (default: 100) */
58
+ maxBulkFiles: number;
59
+ /** Max total size per bulk upload batch in bytes (default: 1GB) */
60
+ maxBulkTotalSize: number;
58
61
  }
59
62
  /**
60
63
  * Input for the requestUploadUrl mutation.
@@ -77,6 +80,13 @@ export interface RequestUploadUrlInput {
77
80
  size: number;
78
81
  /** Original filename (optional, for display/Content-Disposition) */
79
82
  filename?: string;
83
+ /**
84
+ * Custom S3 key for the file (only allowed when bucket has allow_custom_keys=true).
85
+ * When omitted, key defaults to contentHash (content-addressed dedup).
86
+ * When provided, the file is stored at this key; dedup is bypassed.
87
+ * Max 1024 chars. Must not contain path traversal (.. or leading /).
88
+ */
89
+ key?: string;
80
90
  }
81
91
  /**
82
92
  * Result of the requestUploadUrl mutation.
@@ -92,26 +102,8 @@ export interface RequestUploadUrlPayload {
92
102
  deduplicated: boolean;
93
103
  /** Presigned URL expiry time (null if deduplicated) */
94
104
  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;
105
+ /** ID of the previous version (set when re-uploading to an existing custom key) */
106
+ previousVersionId: string | null;
115
107
  }
116
108
  /**
117
109
  * S3 configuration for the presigned URL plugin.