|
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 |
|
16 | 22 | import type { GraphileConfig } from 'graphile-config'; |
| 23 | +import { context as grafastContext, lambda, object } from 'grafast'; |
17 | 24 | import { Logger } from '@pgpmjs/logger'; |
18 | 25 |
|
19 | 26 | import type { PresignedUrlPluginOptions, S3Config, StorageModuleConfig } from './types'; |
@@ -76,7 +83,7 @@ export function createDownloadUrlPlugin( |
76 | 83 |
|
77 | 84 | return { |
78 | 85 | name: 'PresignedUrlDownloadPlugin', |
79 | | - version: '0.1.0', |
| 86 | + version: '0.2.0', |
80 | 87 | description: 'Adds downloadUrl computed field to File types tagged with @storageFiles', |
81 | 88 |
|
82 | 89 | schema: { |
@@ -113,58 +120,71 @@ export function createDownloadUrlPlugin( |
113 | 120 | 'URL to download this file. For public files, returns the public URL. ' + |
114 | 121 | 'For private files, returns a time-limited presigned URL.', |
115 | 122 | type: GraphQLString, |
116 | | - async resolve(parent: any, _args: any, context: any) { |
117 | | - const key = parent.key || parent.get?.('key'); |
118 | | - const isPublic = parent.is_public ?? parent.get?.('is_public'); |
119 | | - const filename = parent.filename || parent.get?.('filename'); |
120 | | - const status = parent.status || parent.get?.('status'); |
121 | | - |
122 | | - if (!key) return null; |
123 | | - |
124 | | - // Only provide download URLs for ready/processed files |
125 | | - if (status !== 'ready' && status !== 'processed') { |
126 | | - return null; |
127 | | - } |
128 | | - |
129 | | - // Resolve per-database config (bucket, publicUrlPrefix, expiry) |
130 | | - let s3ForDb = resolveS3(options); // fallback to global |
131 | | - let downloadUrlExpirySeconds = 3600; // fallback default |
132 | | - try { |
133 | | - const withPgClient = context.pgSettings |
134 | | - ? context.withPgClient |
135 | | - : null; |
136 | | - if (withPgClient) { |
137 | | - const resolved = await withPgClient(null, async (pgClient: any) => { |
138 | | - const dbResult = await pgClient.query({ |
139 | | - text: `SELECT jwt_private.current_database_id() AS id`, |
| 123 | + plan($parent: any) { |
| 124 | + // Access file attributes from the parent PgSelectSingleStep |
| 125 | + const $key = $parent.get('key'); |
| 126 | + const $isPublic = $parent.get('is_public'); |
| 127 | + const $filename = $parent.get('filename'); |
| 128 | + const $status = $parent.get('status'); |
| 129 | + |
| 130 | + // Access GraphQL context for per-database config resolution |
| 131 | + const $withPgClient = (grafastContext() as any).get('withPgClient'); |
| 132 | + const $pgSettings = (grafastContext() as any).get('pgSettings'); |
| 133 | + |
| 134 | + const $combined = object({ |
| 135 | + key: $key, |
| 136 | + isPublic: $isPublic, |
| 137 | + filename: $filename, |
| 138 | + status: $status, |
| 139 | + withPgClient: $withPgClient, |
| 140 | + pgSettings: $pgSettings, |
| 141 | + }); |
| 142 | + |
| 143 | + return lambda($combined, async ({ key, isPublic, filename, status, withPgClient, pgSettings }: any) => { |
| 144 | + if (!key) return null; |
| 145 | + |
| 146 | + // Only provide download URLs for ready/processed files |
| 147 | + if (status !== 'ready' && status !== 'processed') { |
| 148 | + return null; |
| 149 | + } |
| 150 | + |
| 151 | + // Resolve per-database config (bucket, publicUrlPrefix, expiry) |
| 152 | + let s3ForDb = resolveS3(options); // fallback to global |
| 153 | + let downloadUrlExpirySeconds = 3600; // fallback default |
| 154 | + try { |
| 155 | + if (withPgClient && pgSettings) { |
| 156 | + const resolved = await withPgClient(null, async (pgClient: any) => { |
| 157 | + const dbResult = await pgClient.query({ |
| 158 | + text: `SELECT jwt_private.current_database_id() AS id`, |
| 159 | + }); |
| 160 | + const databaseId = dbResult.rows[0]?.id; |
| 161 | + if (!databaseId) return null; |
| 162 | + const config = await getStorageModuleConfig(pgClient, databaseId); |
| 163 | + if (!config) return null; |
| 164 | + return { config, databaseId }; |
140 | 165 | }); |
141 | | - const databaseId = dbResult.rows[0]?.id; |
142 | | - if (!databaseId) return null; |
143 | | - const config = await getStorageModuleConfig(pgClient, databaseId); |
144 | | - if (!config) return null; |
145 | | - return { config, databaseId }; |
146 | | - }); |
147 | | - if (resolved) { |
148 | | - downloadUrlExpirySeconds = resolved.config.downloadUrlExpirySeconds; |
149 | | - s3ForDb = resolveS3ForDatabase(options, resolved.config, resolved.databaseId); |
| 166 | + if (resolved) { |
| 167 | + downloadUrlExpirySeconds = resolved.config.downloadUrlExpirySeconds; |
| 168 | + s3ForDb = resolveS3ForDatabase(options, resolved.config, resolved.databaseId); |
| 169 | + } |
150 | 170 | } |
| 171 | + } catch { |
| 172 | + // Fall back to global config if lookup fails |
151 | 173 | } |
152 | | - } catch { |
153 | | - // Fall back to global config if lookup fails |
154 | | - } |
155 | | - |
156 | | - if (isPublic && s3ForDb.publicUrlPrefix) { |
157 | | - // Public file: return direct CDN URL (per-database prefix) |
158 | | - return `${s3ForDb.publicUrlPrefix}/${key}`; |
159 | | - } |
160 | | - |
161 | | - // Private file: generate presigned GET URL (per-database bucket) |
162 | | - return generatePresignedGetUrl( |
163 | | - s3ForDb, |
164 | | - key, |
165 | | - downloadUrlExpirySeconds, |
166 | | - filename || undefined, |
167 | | - ); |
| 174 | + |
| 175 | + if (isPublic && s3ForDb.publicUrlPrefix) { |
| 176 | + // Public file: return direct CDN URL (per-database prefix) |
| 177 | + return `${s3ForDb.publicUrlPrefix}/${key}`; |
| 178 | + } |
| 179 | + |
| 180 | + // Private file: generate presigned GET URL (per-database bucket) |
| 181 | + return generatePresignedGetUrl( |
| 182 | + s3ForDb, |
| 183 | + key, |
| 184 | + downloadUrlExpirySeconds, |
| 185 | + filename || undefined, |
| 186 | + ); |
| 187 | + }); |
168 | 188 | }, |
169 | 189 | }, |
170 | 190 | ), |
|
0 commit comments