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/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
|
-
*
|
|
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
|
-
*
|
|
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';
|
package/esm/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/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
|
-
*
|
|
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>;
|
|
@@ -8,14 +8,16 @@ const DEFAULT_DOWNLOAD_URL_EXPIRY_SECONDS = 3600; // 1 hour
|
|
|
8
8
|
const DEFAULT_MAX_FILE_SIZE = 200 * 1024 * 1024; // 200MB
|
|
9
9
|
const DEFAULT_MAX_FILENAME_LENGTH = 1024;
|
|
10
10
|
const DEFAULT_CACHE_TTL_SECONDS = process.env.NODE_ENV === 'development' ? 300 : 3600;
|
|
11
|
+
const DEFAULT_MAX_BULK_FILES = 100;
|
|
12
|
+
const DEFAULT_MAX_BULK_TOTAL_SIZE = 1073741824; // 1GB
|
|
11
13
|
const FIVE_MINUTES_MS = 1000 * 60 * 5;
|
|
12
14
|
const ONE_HOUR_MS = 1000 * 60 * 60;
|
|
13
15
|
/**
|
|
14
16
|
* LRU cache for per-database StorageModuleConfig.
|
|
15
17
|
*
|
|
16
18
|
* Each PostGraphile instance serves a single database, but the presigned URL
|
|
17
|
-
* plugin needs to know the generated table names (buckets, files
|
|
18
|
-
*
|
|
19
|
+
* plugin needs to know the generated table names (buckets, files)
|
|
20
|
+
* and their schemas. This cache avoids re-querying metaschema
|
|
19
21
|
* on every request.
|
|
20
22
|
*
|
|
21
23
|
* Pattern: same as graphile-cache's LRU with TTL-based eviction.
|
|
@@ -42,8 +44,6 @@ const APP_STORAGE_MODULE_QUERY = `
|
|
|
42
44
|
bt.name AS buckets_table,
|
|
43
45
|
fs.schema_name AS files_schema,
|
|
44
46
|
ft.name AS files_table,
|
|
45
|
-
urs.schema_name AS upload_requests_schema,
|
|
46
|
-
urt.name AS upload_requests_table,
|
|
47
47
|
sm.endpoint,
|
|
48
48
|
sm.public_url_prefix,
|
|
49
49
|
sm.provider,
|
|
@@ -53,6 +53,9 @@ const APP_STORAGE_MODULE_QUERY = `
|
|
|
53
53
|
sm.default_max_file_size,
|
|
54
54
|
sm.max_filename_length,
|
|
55
55
|
sm.cache_ttl_seconds,
|
|
56
|
+
sm.max_bulk_files,
|
|
57
|
+
sm.max_bulk_total_size,
|
|
58
|
+
sm.has_path_shares,
|
|
56
59
|
NULL AS entity_schema,
|
|
57
60
|
NULL AS entity_table
|
|
58
61
|
FROM metaschema_modules_public.storage_module sm
|
|
@@ -60,8 +63,6 @@ const APP_STORAGE_MODULE_QUERY = `
|
|
|
60
63
|
JOIN metaschema_public.schema bs ON bs.id = bt.schema_id
|
|
61
64
|
JOIN metaschema_public.table ft ON ft.id = sm.files_table_id
|
|
62
65
|
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
66
|
WHERE sm.database_id = $1
|
|
66
67
|
AND sm.membership_type IS NULL
|
|
67
68
|
LIMIT 1
|
|
@@ -81,8 +82,6 @@ const ALL_STORAGE_MODULES_QUERY = `
|
|
|
81
82
|
bt.name AS buckets_table,
|
|
82
83
|
fs.schema_name AS files_schema,
|
|
83
84
|
ft.name AS files_table,
|
|
84
|
-
urs.schema_name AS upload_requests_schema,
|
|
85
|
-
urt.name AS upload_requests_table,
|
|
86
85
|
sm.endpoint,
|
|
87
86
|
sm.public_url_prefix,
|
|
88
87
|
sm.provider,
|
|
@@ -92,6 +91,9 @@ const ALL_STORAGE_MODULES_QUERY = `
|
|
|
92
91
|
sm.default_max_file_size,
|
|
93
92
|
sm.max_filename_length,
|
|
94
93
|
sm.cache_ttl_seconds,
|
|
94
|
+
sm.max_bulk_files,
|
|
95
|
+
sm.max_bulk_total_size,
|
|
96
|
+
sm.has_path_shares,
|
|
95
97
|
es.schema_name AS entity_schema,
|
|
96
98
|
et.name AS entity_table
|
|
97
99
|
FROM metaschema_modules_public.storage_module sm
|
|
@@ -99,8 +101,6 @@ const ALL_STORAGE_MODULES_QUERY = `
|
|
|
99
101
|
JOIN metaschema_public.schema bs ON bs.id = bt.schema_id
|
|
100
102
|
JOIN metaschema_public.table ft ON ft.id = sm.files_table_id
|
|
101
103
|
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
104
|
LEFT JOIN metaschema_public.table et ON et.id = sm.entity_table_id
|
|
105
105
|
LEFT JOIN metaschema_public.schema es ON es.id = et.schema_id
|
|
106
106
|
WHERE sm.database_id = $1
|
|
@@ -114,11 +114,9 @@ function buildConfig(row) {
|
|
|
114
114
|
id: row.id,
|
|
115
115
|
bucketsQualifiedName: QuoteUtils.quoteQualifiedIdentifier(row.buckets_schema, row.buckets_table),
|
|
116
116
|
filesQualifiedName: QuoteUtils.quoteQualifiedIdentifier(row.files_schema, row.files_table),
|
|
117
|
-
uploadRequestsQualifiedName: QuoteUtils.quoteQualifiedIdentifier(row.upload_requests_schema, row.upload_requests_table),
|
|
118
117
|
schemaName: row.buckets_schema,
|
|
119
118
|
bucketsTableName: row.buckets_table,
|
|
120
119
|
filesTableName: row.files_table,
|
|
121
|
-
uploadRequestsTableName: row.upload_requests_table,
|
|
122
120
|
membershipType: row.membership_type,
|
|
123
121
|
entityTableId: row.entity_table_id,
|
|
124
122
|
entityQualifiedName: row.entity_schema && row.entity_table
|
|
@@ -133,6 +131,9 @@ function buildConfig(row) {
|
|
|
133
131
|
defaultMaxFileSize: row.default_max_file_size ?? DEFAULT_MAX_FILE_SIZE,
|
|
134
132
|
maxFilenameLength: row.max_filename_length ?? DEFAULT_MAX_FILENAME_LENGTH,
|
|
135
133
|
cacheTtlSeconds,
|
|
134
|
+
hasPathShares: row.has_path_shares ?? false,
|
|
135
|
+
maxBulkFiles: row.max_bulk_files ?? DEFAULT_MAX_BULK_FILES,
|
|
136
|
+
maxBulkTotalSize: row.max_bulk_total_size ?? DEFAULT_MAX_BULK_TOTAL_SIZE,
|
|
136
137
|
};
|
|
137
138
|
}
|
|
138
139
|
/**
|
|
@@ -229,7 +230,6 @@ export async function getStorageModuleConfigForOwner(pgClient, databaseId, owner
|
|
|
229
230
|
/**
|
|
230
231
|
* Resolve the storage module that owns a specific file by probing all file tables.
|
|
231
232
|
*
|
|
232
|
-
* Used by confirmUpload when only a fileId (UUID) is available.
|
|
233
233
|
* Since UUIDs are globally unique, exactly one table will contain the file.
|
|
234
234
|
*
|
|
235
235
|
* @param pgClient - A pg client from the Graphile context
|
|
@@ -244,7 +244,7 @@ export async function resolveStorageModuleByFileId(pgClient, databaseId, fileId)
|
|
|
244
244
|
// Probe each module's files table for the fileId
|
|
245
245
|
for (const config of allConfigs) {
|
|
246
246
|
const fileResult = await pgClient.query({
|
|
247
|
-
text: `SELECT id, key, mime_type,
|
|
247
|
+
text: `SELECT id, key, mime_type, bucket_id
|
|
248
248
|
FROM ${config.filesQualifiedName}
|
|
249
249
|
WHERE id = $1
|
|
250
250
|
LIMIT 1`,
|
|
@@ -302,11 +302,11 @@ export async function getBucketConfig(pgClient, storageConfig, databaseId, bucke
|
|
|
302
302
|
const hasOwner = ownerId && storageConfig.membershipType !== null;
|
|
303
303
|
const result = await pgClient.query({
|
|
304
304
|
text: hasOwner
|
|
305
|
-
? `SELECT id, key, type, is_public, owner_id, allowed_mime_types, max_file_size
|
|
305
|
+
? `SELECT id, key, type, is_public, owner_id, allowed_mime_types, max_file_size, allow_custom_keys
|
|
306
306
|
FROM ${storageConfig.bucketsQualifiedName}
|
|
307
307
|
WHERE key = $1 AND owner_id = $2
|
|
308
308
|
LIMIT 1`
|
|
309
|
-
: `SELECT id, key, type, is_public, ${storageConfig.membershipType !== null ? 'owner_id,' : ''} allowed_mime_types, max_file_size
|
|
309
|
+
: `SELECT id, key, type, is_public, ${storageConfig.membershipType !== null ? 'owner_id,' : ''} allowed_mime_types, max_file_size, allow_custom_keys
|
|
310
310
|
FROM ${storageConfig.bucketsQualifiedName}
|
|
311
311
|
WHERE key = $1
|
|
312
312
|
LIMIT 1`,
|
|
@@ -324,6 +324,7 @@ export async function getBucketConfig(pgClient, storageConfig, databaseId, bucke
|
|
|
324
324
|
owner_id: row.owner_id ?? null,
|
|
325
325
|
allowed_mime_types: row.allowed_mime_types,
|
|
326
326
|
max_file_size: row.max_file_size,
|
|
327
|
+
allow_custom_keys: row.allow_custom_keys ?? false,
|
|
327
328
|
};
|
|
328
329
|
bucketCache.set(cacheKey, config);
|
|
329
330
|
log.debug(`Cached bucket config for ${databaseId}:${bucketKey} (id=${config.id}, scope=${storageConfig.membershipType ?? 'app'})`);
|
package/esm/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.
|
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,
|
|
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.
|
|
4
|
-
"description": "Presigned URL upload plugin for PostGraphile v5 — requestUploadUrl
|
|
3
|
+
"version": "0.9.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": "
|
|
63
|
+
"gitHead": "caf38236170287e2ad06d5c47482ce246fb3233e"
|
|
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
|
|
8
|
+
* content hash via UNIQUE(bucket_id, key) constraint.
|
|
9
9
|
*
|
|
10
|
-
* 2. `
|
|
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).
|