graphile-presigned-url-plugin 0.6.1 → 0.6.3

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.
@@ -11,6 +11,12 @@
11
11
  * COMMENT ON TABLE files IS E'@storageFiles\nStorage files table';
12
12
  *
13
13
  * This is explicit and reliable — no duck-typing on column names.
14
+ *
15
+ * IMPORTANT: Uses Grafast plan() instead of traditional resolve().
16
+ * In PostGraphile V5, Grafast's planning system does not invoke traditional
17
+ * resolve functions on PG table type fields — it plans them as column
18
+ * lookups. Since downloadUrl is a computed field (not a real column),
19
+ * the plan() function is required for Grafast to execute the S3 signing.
14
20
  */
15
21
  import type { GraphileConfig } from 'graphile-config';
16
22
  import type { PresignedUrlPluginOptions } from './types';
@@ -12,9 +12,16 @@
12
12
  * COMMENT ON TABLE files IS E'@storageFiles\nStorage files table';
13
13
  *
14
14
  * This is explicit and reliable — no duck-typing on column names.
15
+ *
16
+ * IMPORTANT: Uses Grafast plan() instead of traditional resolve().
17
+ * In PostGraphile V5, Grafast's planning system does not invoke traditional
18
+ * resolve functions on PG table type fields — it plans them as column
19
+ * lookups. Since downloadUrl is a computed field (not a real column),
20
+ * the plan() function is required for Grafast to execute the S3 signing.
15
21
  */
16
22
  Object.defineProperty(exports, "__esModule", { value: true });
17
23
  exports.createDownloadUrlPlugin = createDownloadUrlPlugin;
24
+ const grafast_1 = require("grafast");
18
25
  const logger_1 = require("@pgpmjs/logger");
19
26
  const s3_signer_1 = require("./s3-signer");
20
27
  const storage_module_cache_1 = require("./storage-module-cache");
@@ -62,7 +69,7 @@ function resolveS3ForDatabase(options, storageConfig, databaseId) {
62
69
  function createDownloadUrlPlugin(options) {
63
70
  return {
64
71
  name: 'PresignedUrlDownloadPlugin',
65
- version: '0.1.0',
72
+ version: '0.2.0',
66
73
  description: 'Adds downloadUrl computed field to File types tagged with @storageFiles',
67
74
  schema: {
68
75
  hooks: {
@@ -84,52 +91,63 @@ function createDownloadUrlPlugin(options) {
84
91
  description: 'URL to download this file. For public files, returns the public URL. ' +
85
92
  'For private files, returns a time-limited presigned URL.',
86
93
  type: GraphQLString,
87
- async resolve(parent, _args, context) {
88
- const key = parent.key || parent.get?.('key');
89
- const isPublic = parent.is_public ?? parent.get?.('is_public');
90
- const filename = parent.filename || parent.get?.('filename');
91
- const status = parent.status || parent.get?.('status');
92
- if (!key)
93
- return null;
94
- // Only provide download URLs for ready/processed files
95
- if (status !== 'ready' && status !== 'processed') {
96
- return null;
97
- }
98
- // Resolve per-database config (bucket, publicUrlPrefix, expiry)
99
- let s3ForDb = resolveS3(options); // fallback to global
100
- let downloadUrlExpirySeconds = 3600; // fallback default
101
- try {
102
- const withPgClient = context.pgSettings
103
- ? context.withPgClient
104
- : null;
105
- if (withPgClient) {
106
- const resolved = await withPgClient(null, async (pgClient) => {
107
- const dbResult = await pgClient.query({
108
- text: `SELECT jwt_private.current_database_id() AS id`,
94
+ plan($parent) {
95
+ // Access file attributes from the parent PgSelectSingleStep
96
+ const $key = $parent.get('key');
97
+ const $isPublic = $parent.get('is_public');
98
+ const $filename = $parent.get('filename');
99
+ const $status = $parent.get('status');
100
+ // Access GraphQL context for per-database config resolution
101
+ const $withPgClient = (0, grafast_1.context)().get('withPgClient');
102
+ const $pgSettings = (0, grafast_1.context)().get('pgSettings');
103
+ const $combined = (0, grafast_1.object)({
104
+ key: $key,
105
+ isPublic: $isPublic,
106
+ filename: $filename,
107
+ status: $status,
108
+ withPgClient: $withPgClient,
109
+ pgSettings: $pgSettings,
110
+ });
111
+ return (0, grafast_1.lambda)($combined, async ({ key, isPublic, filename, status, withPgClient, pgSettings }) => {
112
+ if (!key)
113
+ return null;
114
+ // Only provide download URLs for ready/processed files
115
+ if (status !== 'ready' && status !== 'processed') {
116
+ return null;
117
+ }
118
+ // Resolve per-database config (bucket, publicUrlPrefix, expiry)
119
+ let s3ForDb = resolveS3(options); // fallback to global
120
+ let downloadUrlExpirySeconds = 3600; // fallback default
121
+ try {
122
+ if (withPgClient && pgSettings) {
123
+ const resolved = await withPgClient(null, async (pgClient) => {
124
+ const dbResult = await pgClient.query({
125
+ text: `SELECT jwt_private.current_database_id() AS id`,
126
+ });
127
+ const databaseId = dbResult.rows[0]?.id;
128
+ if (!databaseId)
129
+ return null;
130
+ const config = await (0, storage_module_cache_1.getStorageModuleConfig)(pgClient, databaseId);
131
+ if (!config)
132
+ return null;
133
+ return { config, databaseId };
109
134
  });
110
- const databaseId = dbResult.rows[0]?.id;
111
- if (!databaseId)
112
- return null;
113
- const config = await (0, storage_module_cache_1.getStorageModuleConfig)(pgClient, databaseId);
114
- if (!config)
115
- return null;
116
- return { config, databaseId };
117
- });
118
- if (resolved) {
119
- downloadUrlExpirySeconds = resolved.config.downloadUrlExpirySeconds;
120
- s3ForDb = resolveS3ForDatabase(options, resolved.config, resolved.databaseId);
135
+ if (resolved) {
136
+ downloadUrlExpirySeconds = resolved.config.downloadUrlExpirySeconds;
137
+ s3ForDb = resolveS3ForDatabase(options, resolved.config, resolved.databaseId);
138
+ }
121
139
  }
122
140
  }
123
- }
124
- catch {
125
- // Fall back to global config if lookup fails
126
- }
127
- if (isPublic && s3ForDb.publicUrlPrefix) {
128
- // Public file: return direct CDN URL (per-database prefix)
129
- return `${s3ForDb.publicUrlPrefix}/${key}`;
130
- }
131
- // Private file: generate presigned GET URL (per-database bucket)
132
- return (0, s3_signer_1.generatePresignedGetUrl)(s3ForDb, key, downloadUrlExpirySeconds, filename || undefined);
141
+ catch {
142
+ // Fall back to global config if lookup fails
143
+ }
144
+ if (isPublic && s3ForDb.publicUrlPrefix) {
145
+ // Public file: return direct CDN URL (per-database prefix)
146
+ return `${s3ForDb.publicUrlPrefix}/${key}`;
147
+ }
148
+ // Private file: generate presigned GET URL (per-database bucket)
149
+ return (0, s3_signer_1.generatePresignedGetUrl)(s3ForDb, key, downloadUrlExpirySeconds, filename || undefined);
150
+ });
133
151
  },
134
152
  }),
135
153
  }, 'PresignedUrlDownloadPlugin adding downloadUrl field');
@@ -11,6 +11,12 @@
11
11
  * COMMENT ON TABLE files IS E'@storageFiles\nStorage files table';
12
12
  *
13
13
  * This is explicit and reliable — no duck-typing on column names.
14
+ *
15
+ * IMPORTANT: Uses Grafast plan() instead of traditional resolve().
16
+ * In PostGraphile V5, Grafast's planning system does not invoke traditional
17
+ * resolve functions on PG table type fields — it plans them as column
18
+ * lookups. Since downloadUrl is a computed field (not a real column),
19
+ * the plan() function is required for Grafast to execute the S3 signing.
14
20
  */
15
21
  import type { GraphileConfig } from 'graphile-config';
16
22
  import type { PresignedUrlPluginOptions } from './types';
@@ -11,7 +11,14 @@
11
11
  * COMMENT ON TABLE files IS E'@storageFiles\nStorage files table';
12
12
  *
13
13
  * This is explicit and reliable — no duck-typing on column names.
14
+ *
15
+ * IMPORTANT: Uses Grafast plan() instead of traditional resolve().
16
+ * In PostGraphile V5, Grafast's planning system does not invoke traditional
17
+ * resolve functions on PG table type fields — it plans them as column
18
+ * lookups. Since downloadUrl is a computed field (not a real column),
19
+ * the plan() function is required for Grafast to execute the S3 signing.
14
20
  */
21
+ import { context as grafastContext, lambda, object } from 'grafast';
15
22
  import { Logger } from '@pgpmjs/logger';
16
23
  import { generatePresignedGetUrl } from './s3-signer';
17
24
  import { getStorageModuleConfig } from './storage-module-cache';
@@ -59,7 +66,7 @@ function resolveS3ForDatabase(options, storageConfig, databaseId) {
59
66
  export function createDownloadUrlPlugin(options) {
60
67
  return {
61
68
  name: 'PresignedUrlDownloadPlugin',
62
- version: '0.1.0',
69
+ version: '0.2.0',
63
70
  description: 'Adds downloadUrl computed field to File types tagged with @storageFiles',
64
71
  schema: {
65
72
  hooks: {
@@ -81,52 +88,63 @@ export function createDownloadUrlPlugin(options) {
81
88
  description: 'URL to download this file. For public files, returns the public URL. ' +
82
89
  'For private files, returns a time-limited presigned URL.',
83
90
  type: GraphQLString,
84
- async resolve(parent, _args, context) {
85
- const key = parent.key || parent.get?.('key');
86
- const isPublic = parent.is_public ?? parent.get?.('is_public');
87
- const filename = parent.filename || parent.get?.('filename');
88
- const status = parent.status || parent.get?.('status');
89
- if (!key)
90
- return null;
91
- // Only provide download URLs for ready/processed files
92
- if (status !== 'ready' && status !== 'processed') {
93
- return null;
94
- }
95
- // Resolve per-database config (bucket, publicUrlPrefix, expiry)
96
- let s3ForDb = resolveS3(options); // fallback to global
97
- let downloadUrlExpirySeconds = 3600; // fallback default
98
- try {
99
- const withPgClient = context.pgSettings
100
- ? context.withPgClient
101
- : null;
102
- if (withPgClient) {
103
- const resolved = await withPgClient(null, async (pgClient) => {
104
- const dbResult = await pgClient.query({
105
- text: `SELECT jwt_private.current_database_id() AS id`,
91
+ plan($parent) {
92
+ // Access file attributes from the parent PgSelectSingleStep
93
+ const $key = $parent.get('key');
94
+ const $isPublic = $parent.get('is_public');
95
+ const $filename = $parent.get('filename');
96
+ const $status = $parent.get('status');
97
+ // Access GraphQL context for per-database config resolution
98
+ const $withPgClient = grafastContext().get('withPgClient');
99
+ const $pgSettings = grafastContext().get('pgSettings');
100
+ const $combined = object({
101
+ key: $key,
102
+ isPublic: $isPublic,
103
+ filename: $filename,
104
+ status: $status,
105
+ withPgClient: $withPgClient,
106
+ pgSettings: $pgSettings,
107
+ });
108
+ return lambda($combined, async ({ key, isPublic, filename, status, withPgClient, pgSettings }) => {
109
+ if (!key)
110
+ return null;
111
+ // Only provide download URLs for ready/processed files
112
+ if (status !== 'ready' && status !== 'processed') {
113
+ return null;
114
+ }
115
+ // Resolve per-database config (bucket, publicUrlPrefix, expiry)
116
+ let s3ForDb = resolveS3(options); // fallback to global
117
+ let downloadUrlExpirySeconds = 3600; // fallback default
118
+ try {
119
+ if (withPgClient && pgSettings) {
120
+ const resolved = await withPgClient(null, async (pgClient) => {
121
+ const dbResult = await pgClient.query({
122
+ text: `SELECT jwt_private.current_database_id() AS id`,
123
+ });
124
+ const databaseId = dbResult.rows[0]?.id;
125
+ if (!databaseId)
126
+ return null;
127
+ const config = await getStorageModuleConfig(pgClient, databaseId);
128
+ if (!config)
129
+ return null;
130
+ return { config, databaseId };
106
131
  });
107
- const databaseId = dbResult.rows[0]?.id;
108
- if (!databaseId)
109
- return null;
110
- const config = await getStorageModuleConfig(pgClient, databaseId);
111
- if (!config)
112
- return null;
113
- return { config, databaseId };
114
- });
115
- if (resolved) {
116
- downloadUrlExpirySeconds = resolved.config.downloadUrlExpirySeconds;
117
- s3ForDb = resolveS3ForDatabase(options, resolved.config, resolved.databaseId);
132
+ if (resolved) {
133
+ downloadUrlExpirySeconds = resolved.config.downloadUrlExpirySeconds;
134
+ s3ForDb = resolveS3ForDatabase(options, resolved.config, resolved.databaseId);
135
+ }
118
136
  }
119
137
  }
120
- }
121
- catch {
122
- // Fall back to global config if lookup fails
123
- }
124
- if (isPublic && s3ForDb.publicUrlPrefix) {
125
- // Public file: return direct CDN URL (per-database prefix)
126
- return `${s3ForDb.publicUrlPrefix}/${key}`;
127
- }
128
- // Private file: generate presigned GET URL (per-database bucket)
129
- return generatePresignedGetUrl(s3ForDb, key, downloadUrlExpirySeconds, filename || undefined);
138
+ catch {
139
+ // Fall back to global config if lookup fails
140
+ }
141
+ if (isPublic && s3ForDb.publicUrlPrefix) {
142
+ // Public file: return direct CDN URL (per-database prefix)
143
+ return `${s3ForDb.publicUrlPrefix}/${key}`;
144
+ }
145
+ // Private file: generate presigned GET URL (per-database bucket)
146
+ return generatePresignedGetUrl(s3ForDb, key, downloadUrlExpirySeconds, filename || undefined);
147
+ });
130
148
  },
131
149
  }),
132
150
  }, 'PresignedUrlDownloadPlugin adding downloadUrl field');
package/esm/plugin.js CHANGED
@@ -275,9 +275,9 @@ export function createPresignedUrlPlugin(options) {
275
275
  // Track the dedup request
276
276
  await txClient.query({
277
277
  text: `INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
278
- (file_id, bucket_id, key, content_type, content_hash, size, status, expires_at)
279
- VALUES ($1, $2, $3, $4, $5, $6, 'confirmed', NOW())`,
280
- values: [existingFile.id, bucket.id, s3Key, contentType, contentHash, size],
278
+ (file_id, bucket_id, key, content_type, content_hash, status, expires_at)
279
+ VALUES ($1, $2, $3, $4, $5, 'confirmed', NOW())`,
280
+ values: [existingFile.id, bucket.id, s3Key, contentType, contentHash],
281
281
  });
282
282
  return {
283
283
  uploadUrl: null,
@@ -329,9 +329,9 @@ export function createPresignedUrlPlugin(options) {
329
329
  // --- Track the upload request ---
330
330
  await txClient.query({
331
331
  text: `INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
332
- (file_id, bucket_id, key, content_type, content_hash, size, status, expires_at)
333
- VALUES ($1, $2, $3, $4, $5, $6, 'issued', $7)`,
334
- values: [fileId, bucket.id, s3Key, contentType, contentHash, size, expiresAt],
332
+ (file_id, bucket_id, key, content_type, content_hash, status, expires_at)
333
+ VALUES ($1, $2, $3, $4, $5, 'issued', $6)`,
334
+ values: [fileId, bucket.id, s3Key, contentType, contentHash, expiresAt],
335
335
  });
336
336
  return {
337
337
  uploadUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphile-presigned-url-plugin",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
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",
@@ -60,5 +60,5 @@
60
60
  "@types/node": "^22.19.11",
61
61
  "makage": "^0.1.10"
62
62
  },
63
- "gitHead": "b816000e463c796c41160a4ed5d34ad3356a2ad5"
63
+ "gitHead": "a6424a875afd997535ca6b1636542a5158991003"
64
64
  }
package/plugin.js CHANGED
@@ -279,9 +279,9 @@ function createPresignedUrlPlugin(options) {
279
279
  // Track the dedup request
280
280
  await txClient.query({
281
281
  text: `INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
282
- (file_id, bucket_id, key, content_type, content_hash, size, status, expires_at)
283
- VALUES ($1, $2, $3, $4, $5, $6, 'confirmed', NOW())`,
284
- values: [existingFile.id, bucket.id, s3Key, contentType, contentHash, size],
282
+ (file_id, bucket_id, key, content_type, content_hash, status, expires_at)
283
+ VALUES ($1, $2, $3, $4, $5, 'confirmed', NOW())`,
284
+ values: [existingFile.id, bucket.id, s3Key, contentType, contentHash],
285
285
  });
286
286
  return {
287
287
  uploadUrl: null,
@@ -333,9 +333,9 @@ function createPresignedUrlPlugin(options) {
333
333
  // --- Track the upload request ---
334
334
  await txClient.query({
335
335
  text: `INSERT INTO ${storageConfig.uploadRequestsQualifiedName}
336
- (file_id, bucket_id, key, content_type, content_hash, size, status, expires_at)
337
- VALUES ($1, $2, $3, $4, $5, $6, 'issued', $7)`,
338
- values: [fileId, bucket.id, s3Key, contentType, contentHash, size, expiresAt],
336
+ (file_id, bucket_id, key, content_type, content_hash, status, expires_at)
337
+ VALUES ($1, $2, $3, $4, $5, 'issued', $6)`,
338
+ values: [fileId, bucket.id, s3Key, contentType, contentHash, expiresAt],
339
339
  });
340
340
  return {
341
341
  uploadUrl,