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/README.md +0 -2
- package/download-url-field.js +1 -7
- package/esm/download-url-field.js +1 -7
- package/esm/index.d.ts +2 -3
- package/esm/index.js +1 -2
- package/esm/plugin.d.ts +2 -6
- package/esm/plugin.js +338 -220
- package/esm/preset.d.ts +2 -2
- package/esm/preset.js +2 -2
- package/esm/s3-signer.d.ts +1 -2
- package/esm/s3-signer.js +1 -2
- package/esm/storage-module-cache.d.ts +0 -2
- package/esm/storage-module-cache.js +17 -16
- package/esm/types.d.ts +16 -24
- package/index.d.ts +2 -3
- package/index.js +1 -2
- package/package.json +3 -3
- package/plugin.d.ts +2 -6
- package/plugin.js +336 -218
- package/preset.d.ts +2 -2
- package/preset.js +2 -2
- package/s3-signer.d.ts +1 -2
- package/s3-signer.js +1 -2
- package/storage-module-cache.d.ts +0 -2
- package/storage-module-cache.js +17 -16
- package/types.d.ts +16 -24
package/preset.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* PostGraphile v5 Presigned URL Preset
|
|
4
4
|
*
|
|
5
5
|
* Provides a convenient preset for including presigned URL upload support
|
|
6
|
-
* in PostGraphile. Combines the main mutation plugin (requestUploadUrl
|
|
7
|
-
*
|
|
6
|
+
* in PostGraphile. Combines the main mutation plugin (requestUploadUrl)
|
|
7
|
+
* with the downloadUrl computed field plugin.
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.PresignedUrlPreset = PresignedUrlPreset;
|
package/s3-signer.d.ts
CHANGED
|
@@ -30,8 +30,7 @@ export declare function generatePresignedGetUrl(s3Config: S3Config, key: string,
|
|
|
30
30
|
/**
|
|
31
31
|
* Check if an object exists in S3 and optionally verify its content-type.
|
|
32
32
|
*
|
|
33
|
-
*
|
|
34
|
-
* and that the content-type matches what was declared.
|
|
33
|
+
* Checks whether an object exists in S3 and retrieves its content-type.
|
|
35
34
|
*
|
|
36
35
|
* @param s3Config - S3 client and bucket configuration
|
|
37
36
|
* @param key - S3 object key
|
package/s3-signer.js
CHANGED
|
@@ -61,8 +61,7 @@ async function generatePresignedGetUrl(s3Config, key, expiresIn = 3600, filename
|
|
|
61
61
|
/**
|
|
62
62
|
* Check if an object exists in S3 and optionally verify its content-type.
|
|
63
63
|
*
|
|
64
|
-
*
|
|
65
|
-
* and that the content-type matches what was declared.
|
|
64
|
+
* Checks whether an object exists in S3 and retrieves its content-type.
|
|
66
65
|
*
|
|
67
66
|
* @param s3Config - S3 client and bucket configuration
|
|
68
67
|
* @param key - S3 object key
|
|
@@ -43,7 +43,6 @@ export declare function getStorageModuleConfigForOwner(pgClient: {
|
|
|
43
43
|
/**
|
|
44
44
|
* Resolve the storage module that owns a specific file by probing all file tables.
|
|
45
45
|
*
|
|
46
|
-
* Used by confirmUpload when only a fileId (UUID) is available.
|
|
47
46
|
* Since UUIDs are globally unique, exactly one table will contain the file.
|
|
48
47
|
*
|
|
49
48
|
* @param pgClient - A pg client from the Graphile context
|
|
@@ -64,7 +63,6 @@ export declare function resolveStorageModuleByFileId(pgClient: {
|
|
|
64
63
|
id: string;
|
|
65
64
|
key: string;
|
|
66
65
|
mime_type: string;
|
|
67
|
-
status: string;
|
|
68
66
|
bucket_id: string;
|
|
69
67
|
};
|
|
70
68
|
} | null>;
|
package/storage-module-cache.js
CHANGED
|
@@ -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
|
-
*
|
|
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,
|
|
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
|
-
/**
|
|
96
|
-
|
|
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.
|