graphile-presigned-url-plugin 0.2.0 → 0.3.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
@@ -1,5 +1,17 @@
1
1
  # graphile-presigned-url-plugin
2
2
 
3
+ <p align="center" width="100%">
4
+ <img height="250" src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" />
5
+ </p>
6
+
7
+ <p align="center" width="100%">
8
+ <a href="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml">
9
+ <img height="20" src="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml/badge.svg" />
10
+ </a>
11
+ <a href="https://github.com/constructive-io/constructive/blob/main/LICENSE"><img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/></a>
12
+ <a href="https://www.npmjs.com/package/graphile-presigned-url-plugin"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/constructive?filename=graphile%2Fgraphile-presigned-url-plugin%2Fpackage.json"/></a>
13
+ </p>
14
+
3
15
  Presigned URL upload plugin for PostGraphile v5.
4
16
 
5
17
  ## Features
@@ -14,14 +14,5 @@
14
14
  */
15
15
  import type { GraphileConfig } from 'graphile-config';
16
16
  import type { PresignedUrlPluginOptions } from './types';
17
- /**
18
- * Creates the downloadUrl computed field plugin.
19
- *
20
- * This is a separate plugin from the main presigned URL plugin because it
21
- * uses the GraphQLObjectType_fields hook (low-level) rather than extendSchema.
22
- * The downloadUrl field needs to be added dynamically to whatever table is
23
- * the storage module's files table, which we discover at schema-build time
24
- * via the `@storageFiles` smart tag.
25
- */
26
17
  export declare function createDownloadUrlPlugin(options: PresignedUrlPluginOptions): GraphileConfig.Plugin;
27
18
  export default createDownloadUrlPlugin;
@@ -28,8 +28,19 @@ const log = new logger_1.Logger('graphile-presigned-url:download-url');
28
28
  * the storage module's files table, which we discover at schema-build time
29
29
  * via the `@storageFiles` smart tag.
30
30
  */
31
+ /**
32
+ * Resolve the S3 config from the options. If the option is a lazy getter
33
+ * function, call it (and cache the result).
34
+ */
35
+ function resolveS3(options) {
36
+ if (typeof options.s3 === 'function') {
37
+ const resolved = options.s3();
38
+ options.s3 = resolved;
39
+ return resolved;
40
+ }
41
+ return options.s3;
42
+ }
31
43
  function createDownloadUrlPlugin(options) {
32
- const { s3 } = options;
33
44
  return {
34
45
  name: 'PresignedUrlDownloadPlugin',
35
46
  version: '0.1.0',
@@ -65,6 +76,7 @@ function createDownloadUrlPlugin(options) {
65
76
  if (status !== 'ready' && status !== 'processed') {
66
77
  return null;
67
78
  }
79
+ const s3 = resolveS3(options);
68
80
  if (isPublic && s3.publicUrlPrefix) {
69
81
  // Public file: return direct URL
70
82
  return `${s3.publicUrlPrefix}/${key}`;
@@ -92,7 +104,7 @@ function createDownloadUrlPlugin(options) {
92
104
  // Fall back to default if config lookup fails
93
105
  }
94
106
  // Private file: generate presigned GET URL
95
- return (0, s3_signer_1.generatePresignedGetUrl)(s3, key, downloadUrlExpirySeconds, filename || undefined);
107
+ return (0, s3_signer_1.generatePresignedGetUrl)(resolveS3(options), key, downloadUrlExpirySeconds, filename || undefined);
96
108
  },
97
109
  }),
98
110
  }, 'PresignedUrlDownloadPlugin adding downloadUrl field');
@@ -14,14 +14,5 @@
14
14
  */
15
15
  import type { GraphileConfig } from 'graphile-config';
16
16
  import type { PresignedUrlPluginOptions } from './types';
17
- /**
18
- * Creates the downloadUrl computed field plugin.
19
- *
20
- * This is a separate plugin from the main presigned URL plugin because it
21
- * uses the GraphQLObjectType_fields hook (low-level) rather than extendSchema.
22
- * The downloadUrl field needs to be added dynamically to whatever table is
23
- * the storage module's files table, which we discover at schema-build time
24
- * via the `@storageFiles` smart tag.
25
- */
26
17
  export declare function createDownloadUrlPlugin(options: PresignedUrlPluginOptions): GraphileConfig.Plugin;
27
18
  export default createDownloadUrlPlugin;
@@ -25,8 +25,19 @@ const log = new Logger('graphile-presigned-url:download-url');
25
25
  * the storage module's files table, which we discover at schema-build time
26
26
  * via the `@storageFiles` smart tag.
27
27
  */
28
+ /**
29
+ * Resolve the S3 config from the options. If the option is a lazy getter
30
+ * function, call it (and cache the result).
31
+ */
32
+ function resolveS3(options) {
33
+ if (typeof options.s3 === 'function') {
34
+ const resolved = options.s3();
35
+ options.s3 = resolved;
36
+ return resolved;
37
+ }
38
+ return options.s3;
39
+ }
28
40
  export function createDownloadUrlPlugin(options) {
29
- const { s3 } = options;
30
41
  return {
31
42
  name: 'PresignedUrlDownloadPlugin',
32
43
  version: '0.1.0',
@@ -62,6 +73,7 @@ export function createDownloadUrlPlugin(options) {
62
73
  if (status !== 'ready' && status !== 'processed') {
63
74
  return null;
64
75
  }
76
+ const s3 = resolveS3(options);
65
77
  if (isPublic && s3.publicUrlPrefix) {
66
78
  // Public file: return direct URL
67
79
  return `${s3.publicUrlPrefix}/${key}`;
@@ -89,7 +101,7 @@ export function createDownloadUrlPlugin(options) {
89
101
  // Fall back to default if config lookup fails
90
102
  }
91
103
  // Private file: generate presigned GET URL
92
- return generatePresignedGetUrl(s3, key, downloadUrlExpirySeconds, filename || undefined);
104
+ return generatePresignedGetUrl(resolveS3(options), key, downloadUrlExpirySeconds, filename || undefined);
93
105
  },
94
106
  }),
95
107
  }, 'PresignedUrlDownloadPlugin adding downloadUrl field');
package/esm/index.d.ts CHANGED
@@ -31,4 +31,4 @@ export { createDownloadUrlPlugin } from './download-url-field';
31
31
  export { PresignedUrlPreset } from './preset';
32
32
  export { getStorageModuleConfig, getBucketConfig, clearStorageModuleCache, clearBucketCache } from './storage-module-cache';
33
33
  export { generatePresignedPutUrl, generatePresignedGetUrl, headObject } from './s3-signer';
34
- export type { BucketConfig, StorageModuleConfig, RequestUploadUrlInput, RequestUploadUrlPayload, ConfirmUploadInput, ConfirmUploadPayload, S3Config, PresignedUrlPluginOptions, } from './types';
34
+ export type { BucketConfig, StorageModuleConfig, RequestUploadUrlInput, RequestUploadUrlPayload, ConfirmUploadInput, ConfirmUploadPayload, S3Config, S3ConfigOrGetter, PresignedUrlPluginOptions, } from './types';
package/esm/plugin.js CHANGED
@@ -52,8 +52,21 @@ async function resolveDatabaseId(pgClient) {
52
52
  return result.rows[0]?.id ?? null;
53
53
  }
54
54
  // --- Plugin factory ---
55
+ /**
56
+ * Resolve the S3 config from the options. If the option is a lazy getter
57
+ * function, call it (and cache the result). This avoids reading env vars
58
+ * or constructing an S3Client at module-import time.
59
+ */
60
+ function resolveS3(options) {
61
+ if (typeof options.s3 === 'function') {
62
+ const resolved = options.s3();
63
+ // Cache so subsequent calls don't re-evaluate
64
+ options.s3 = resolved;
65
+ return resolved;
66
+ }
67
+ return options.s3;
68
+ }
55
69
  export function createPresignedUrlPlugin(options) {
56
- const { s3 } = options;
57
70
  return extendSchema(() => ({
58
71
  typeDefs: gql `
59
72
  input RequestUploadUrlInput {
@@ -229,7 +242,7 @@ export function createPresignedUrlPlugin(options) {
229
242
  ]);
230
243
  const fileId = fileResult.rows[0].id;
231
244
  // --- Generate presigned PUT URL ---
232
- const uploadUrl = await generatePresignedPutUrl(s3, s3Key, contentType, size, storageConfig.uploadUrlExpirySeconds);
245
+ const uploadUrl = await generatePresignedPutUrl(resolveS3(options), s3Key, contentType, size, storageConfig.uploadUrlExpirySeconds);
233
246
  const expiresAt = new Date(Date.now() + storageConfig.uploadUrlExpirySeconds * 1000).toISOString();
234
247
  // --- Track the upload request ---
235
248
  await pgClient.query(`INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
@@ -296,7 +309,7 @@ export function createPresignedUrlPlugin(options) {
296
309
  };
297
310
  }
298
311
  // --- Verify file exists in S3 ---
299
- const s3Head = await headObject(s3, file.key, file.content_type);
312
+ const s3Head = await headObject(resolveS3(options), file.key, file.content_type);
300
313
  if (!s3Head) {
301
314
  throw new Error('FILE_NOT_IN_S3: the file has not been uploaded yet');
302
315
  }
@@ -38,6 +38,9 @@ const STORAGE_MODULE_QUERY = `
38
38
  ft.name AS files_table,
39
39
  urs.schema_name AS upload_requests_schema,
40
40
  urt.name AS upload_requests_table,
41
+ sm.endpoint,
42
+ sm.public_url_prefix,
43
+ sm.provider,
41
44
  sm.upload_url_expiry_seconds,
42
45
  sm.download_url_expiry_seconds,
43
46
  sm.default_max_file_size,
@@ -83,6 +86,9 @@ export async function getStorageModuleConfig(pgClient, databaseId) {
83
86
  bucketsTableName: row.buckets_table,
84
87
  filesTableName: row.files_table,
85
88
  uploadRequestsTableName: row.upload_requests_table,
89
+ endpoint: row.endpoint,
90
+ publicUrlPrefix: row.public_url_prefix,
91
+ provider: row.provider,
86
92
  uploadUrlExpirySeconds: row.upload_url_expiry_seconds ?? DEFAULT_UPLOAD_URL_EXPIRY_SECONDS,
87
93
  downloadUrlExpirySeconds: row.download_url_expiry_seconds ?? DEFAULT_DOWNLOAD_URL_EXPIRY_SECONDS,
88
94
  defaultMaxFileSize: row.default_max_file_size ?? DEFAULT_MAX_FILE_SIZE,
package/esm/types.d.ts CHANGED
@@ -31,6 +31,12 @@ export interface StorageModuleConfig {
31
31
  filesTableName: string;
32
32
  /** Upload requests table name */
33
33
  uploadRequestsTableName: string;
34
+ /** S3-compatible API endpoint URL (per-database override) */
35
+ endpoint: string | null;
36
+ /** Public URL prefix for generating download URLs (per-database override) */
37
+ publicUrlPrefix: string | null;
38
+ /** Storage provider type: 'minio', 's3', 'gcs', etc. (per-database override) */
39
+ provider: string | null;
34
40
  /** Presigned PUT URL expiry in seconds (default: 900 = 15 min) */
35
41
  uploadUrlExpirySeconds: number;
36
42
  /** Presigned GET URL expiry in seconds (default: 3600 = 1 hour) */
@@ -107,10 +113,17 @@ export interface S3Config {
107
113
  /** Public URL prefix for generating download URLs */
108
114
  publicUrlPrefix?: string;
109
115
  }
116
+ /**
117
+ * S3 configuration or a lazy getter that returns it on first use.
118
+ * When a function is provided, it will only be called when the first
119
+ * mutation or resolver actually needs the S3 client — avoiding eager
120
+ * env-var reads and S3Client creation at module import time.
121
+ */
122
+ export type S3ConfigOrGetter = S3Config | (() => S3Config);
110
123
  /**
111
124
  * Plugin options for the presigned URL plugin.
112
125
  */
113
126
  export interface PresignedUrlPluginOptions {
114
- /** S3 configuration */
115
- s3: S3Config;
127
+ /** S3 configuration (concrete or lazy getter) */
128
+ s3: S3ConfigOrGetter;
116
129
  }
package/index.d.ts CHANGED
@@ -31,4 +31,4 @@ export { createDownloadUrlPlugin } from './download-url-field';
31
31
  export { PresignedUrlPreset } from './preset';
32
32
  export { getStorageModuleConfig, getBucketConfig, clearStorageModuleCache, clearBucketCache } from './storage-module-cache';
33
33
  export { generatePresignedPutUrl, generatePresignedGetUrl, headObject } from './s3-signer';
34
- export type { BucketConfig, StorageModuleConfig, RequestUploadUrlInput, RequestUploadUrlPayload, ConfirmUploadInput, ConfirmUploadPayload, S3Config, PresignedUrlPluginOptions, } from './types';
34
+ export type { BucketConfig, StorageModuleConfig, RequestUploadUrlInput, RequestUploadUrlPayload, ConfirmUploadInput, ConfirmUploadPayload, S3Config, S3ConfigOrGetter, PresignedUrlPluginOptions, } from './types';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphile-presigned-url-plugin",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Presigned URL upload plugin for PostGraphile v5 — requestUploadUrl, confirmUpload mutations and downloadUrl computed field",
5
5
  "author": "Constructive <developers@constructive.io>",
6
6
  "homepage": "https://github.com/constructive-io/constructive",
@@ -55,8 +55,9 @@
55
55
  "postgraphile": "5.0.0"
56
56
  },
57
57
  "devDependencies": {
58
+ "@constructive-io/s3-utils": "^2.10.2",
58
59
  "@types/node": "^22.19.11",
59
60
  "makage": "^0.1.10"
60
61
  },
61
- "gitHead": "fc23b83307d007a14e54b1d0fc36614b9650a5dc"
62
+ "gitHead": "fe60f7b81252eea53dce227bb581d5ae2ef0ec36"
62
63
  }
package/plugin.js CHANGED
@@ -56,8 +56,21 @@ async function resolveDatabaseId(pgClient) {
56
56
  return result.rows[0]?.id ?? null;
57
57
  }
58
58
  // --- Plugin factory ---
59
+ /**
60
+ * Resolve the S3 config from the options. If the option is a lazy getter
61
+ * function, call it (and cache the result). This avoids reading env vars
62
+ * or constructing an S3Client at module-import time.
63
+ */
64
+ function resolveS3(options) {
65
+ if (typeof options.s3 === 'function') {
66
+ const resolved = options.s3();
67
+ // Cache so subsequent calls don't re-evaluate
68
+ options.s3 = resolved;
69
+ return resolved;
70
+ }
71
+ return options.s3;
72
+ }
59
73
  function createPresignedUrlPlugin(options) {
60
- const { s3 } = options;
61
74
  return (0, graphile_utils_1.extendSchema)(() => ({
62
75
  typeDefs: (0, graphile_utils_1.gql) `
63
76
  input RequestUploadUrlInput {
@@ -233,7 +246,7 @@ function createPresignedUrlPlugin(options) {
233
246
  ]);
234
247
  const fileId = fileResult.rows[0].id;
235
248
  // --- Generate presigned PUT URL ---
236
- const uploadUrl = await (0, s3_signer_1.generatePresignedPutUrl)(s3, s3Key, contentType, size, storageConfig.uploadUrlExpirySeconds);
249
+ const uploadUrl = await (0, s3_signer_1.generatePresignedPutUrl)(resolveS3(options), s3Key, contentType, size, storageConfig.uploadUrlExpirySeconds);
237
250
  const expiresAt = new Date(Date.now() + storageConfig.uploadUrlExpirySeconds * 1000).toISOString();
238
251
  // --- Track the upload request ---
239
252
  await pgClient.query(`INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
@@ -300,7 +313,7 @@ function createPresignedUrlPlugin(options) {
300
313
  };
301
314
  }
302
315
  // --- Verify file exists in S3 ---
303
- const s3Head = await (0, s3_signer_1.headObject)(s3, file.key, file.content_type);
316
+ const s3Head = await (0, s3_signer_1.headObject)(resolveS3(options), file.key, file.content_type);
304
317
  if (!s3Head) {
305
318
  throw new Error('FILE_NOT_IN_S3: the file has not been uploaded yet');
306
319
  }
@@ -44,6 +44,9 @@ const STORAGE_MODULE_QUERY = `
44
44
  ft.name AS files_table,
45
45
  urs.schema_name AS upload_requests_schema,
46
46
  urt.name AS upload_requests_table,
47
+ sm.endpoint,
48
+ sm.public_url_prefix,
49
+ sm.provider,
47
50
  sm.upload_url_expiry_seconds,
48
51
  sm.download_url_expiry_seconds,
49
52
  sm.default_max_file_size,
@@ -89,6 +92,9 @@ async function getStorageModuleConfig(pgClient, databaseId) {
89
92
  bucketsTableName: row.buckets_table,
90
93
  filesTableName: row.files_table,
91
94
  uploadRequestsTableName: row.upload_requests_table,
95
+ endpoint: row.endpoint,
96
+ publicUrlPrefix: row.public_url_prefix,
97
+ provider: row.provider,
92
98
  uploadUrlExpirySeconds: row.upload_url_expiry_seconds ?? DEFAULT_UPLOAD_URL_EXPIRY_SECONDS,
93
99
  downloadUrlExpirySeconds: row.download_url_expiry_seconds ?? DEFAULT_DOWNLOAD_URL_EXPIRY_SECONDS,
94
100
  defaultMaxFileSize: row.default_max_file_size ?? DEFAULT_MAX_FILE_SIZE,
package/types.d.ts CHANGED
@@ -31,6 +31,12 @@ export interface StorageModuleConfig {
31
31
  filesTableName: string;
32
32
  /** Upload requests table name */
33
33
  uploadRequestsTableName: string;
34
+ /** S3-compatible API endpoint URL (per-database override) */
35
+ endpoint: string | null;
36
+ /** Public URL prefix for generating download URLs (per-database override) */
37
+ publicUrlPrefix: string | null;
38
+ /** Storage provider type: 'minio', 's3', 'gcs', etc. (per-database override) */
39
+ provider: string | null;
34
40
  /** Presigned PUT URL expiry in seconds (default: 900 = 15 min) */
35
41
  uploadUrlExpirySeconds: number;
36
42
  /** Presigned GET URL expiry in seconds (default: 3600 = 1 hour) */
@@ -107,10 +113,17 @@ export interface S3Config {
107
113
  /** Public URL prefix for generating download URLs */
108
114
  publicUrlPrefix?: string;
109
115
  }
116
+ /**
117
+ * S3 configuration or a lazy getter that returns it on first use.
118
+ * When a function is provided, it will only be called when the first
119
+ * mutation or resolver actually needs the S3 client — avoiding eager
120
+ * env-var reads and S3Client creation at module import time.
121
+ */
122
+ export type S3ConfigOrGetter = S3Config | (() => S3Config);
110
123
  /**
111
124
  * Plugin options for the presigned URL plugin.
112
125
  */
113
126
  export interface PresignedUrlPluginOptions {
114
- /** S3 configuration */
115
- s3: S3Config;
127
+ /** S3 configuration (concrete or lazy getter) */
128
+ s3: S3ConfigOrGetter;
116
129
  }