feat: multi-scope bucket resolution (Option C: bucketKey + ownerId)#1009
Merged
pyramation merged 12 commits intomainfrom Apr 19, 2026
Merged
feat: multi-scope bucket resolution (Option C: bucketKey + ownerId)#1009pyramation merged 12 commits intomainfrom
pyramation merged 12 commits intomainfrom
Conversation
- presigned-url-plugin: Add optional ownerId to RequestUploadUrlInput - getStorageModuleConfig now filters to app-level (membership_type IS NULL) - New getStorageModuleConfigForOwner resolves entity-scoped modules by probing entity tables - New resolveStorageModuleByFileId for confirmUpload (probes all file tables by UUID) - getBucketConfig supports entity-scoped lookups with (owner_id, key) composite - File INSERT adapts to presence/absence of owner_id column per scope - bucket-provisioner-plugin: Add optional ownerId to ProvisionBucketInput - Replace LIMIT 1 query with scope-aware resolveStorageModule function - provisionBucket mutation resolves storage module via ownerId - Auto-provisioning hook uses app-level resolution (no ownerId context) - Backward compatible: omitting ownerId defaults to app-level storage - No DB changes required (builds on PR #876 membership_type + entity_table_id columns)
Contributor
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
…at code
- Replace raw string interpolation ("${schema}"."${table}") with
QuoteUtils.quoteQualifiedIdentifier() from @pgsql/quotes in both
presigned-url-plugin and bucket-provisioner-plugin
- Remove LEGACY_STORAGE_MODULE_QUERY constants from both plugins
- Remove schemaSupportsMultiScope module-level flags
- Remove all SAVEPOINT-based schema detection logic
- Simplify resolveStorageModule(), getStorageModuleConfig(),
getStorageModuleConfigForOwner(), and resolveStorageModuleByFileId()
to use direct queries without fallback
3 tasks
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
The test fixture setup.sql hardcodes the storage_module CREATE TABLE instead of using the published @pgpm/metaschema-modules package. Add the membership_type column and update table name defaults to match the 0.21.1 schema (app_buckets, app_files, app_upload_requests). Also add the unique scope index on (database_id, COALESCE(membership_type, -1)).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds multi-scope storage module resolution to the presigned-url and bucket-provisioner Graphile plugins. Previously, all storage module queries used
WHERE database_id = $1 LIMIT 1, which silently picked an arbitrary module when a database has multiple storage modules (app-level + entity-scoped fromhas_storage=trueentity types, as wired in constructive-db PR #876).Approach (Option C — Key + ownerId): Both
requestUploadUrlandprovisionBucketmutations accept an optionalownerId(entity UUID). When provided, the plugin probes entity tables to find the matching storage module. When omitted, it defaults to the app-level module (membership_type IS NULL).presigned-url-plugin
storage-module-cache.ts: Split the singleSTORAGE_MODULE_QUERYintoAPP_STORAGE_MODULE_QUERY(app-level, filtered bymembership_type IS NULL) andALL_STORAGE_MODULES_QUERY(all modules with entity table joins). Added three new resolution functions:getStorageModuleConfigForOwner()— probes entity tables to find the module for a given ownerIdresolveStorageModuleByFileId()— probes all file tables by UUID (forconfirmUpload)buildConfig()— shared helper to constructStorageModuleConfigfrom a DB row, usingQuoteUtils.quoteQualifiedIdentifier()for all qualified table namesplugin.ts:requestUploadUrlroutes to the correct resolver based on ownerId.confirmUploadusesresolveStorageModuleByFileIdto find the file across all scopes. File INSERT conditionally includesowner_idcolumn based on whether the storage module is entity-scoped.types.ts: AddedownerId?toRequestUploadUrlInput, scope identity fields (membershipType,entityTableId,entityQualifiedName) toStorageModuleConfig, changedBucketConfig.owner_idfromstringtostring | null.bucket-provisioner-plugin
plugin.ts: AddedresolveStorageModule()async function with app-level vs entity-probing pattern. UpdatedprovisionBucketmutation,provisionBucketForRow,updateBucketCors, and the auto-provisioning hook to use scope-aware resolution. Bucket lookups use(owner_id, key)composite when ownerId is present.Identifier quoting
Both plugins now use
QuoteUtils.quoteQualifiedIdentifier()from@pgsql/quotesfor all dynamic SQL table references, replacing raw string interpolation ("${schema}"."${table}"). This follows the established codebase convention used inPublicKeySignature.ts,query-builder.ts,upload.ts, anddump.ts.Test fixture & dependency updates
graphql/server-test/__fixtures__/seed/simple-seed-storage/setup.sql: Updated the hand-writtenstorage_moduleCREATE TABLE to match the 0.21.1 schema — addedmembership_type int DEFAULT NULLcolumn, renamed table defaults toapp_buckets/app_files/app_upload_requests, and added thestorage_module_unique_scopeindex on(database_id, COALESCE(membership_type, -1)).@pgpm/metaschema-modulesbumped from^0.18.0to^0.21.1(resolves to0.21.1in lockfile).Not changed
download-url-field.ts— uses smart-tag-driven type resolution; S3 config (bucket name, expiry) is the same across all scopes within a database, so no changes needed.graphile-settings/presigned-url-resolver.ts— config factory only (S3Client, bucket name resolver), no storage module queries.Review & Testing Checklist for Human
presigned-url-plugin/src/plugin.ts~lines 336-367): The two INSERT branches have different column counts (with/withoutowner_id). Verify the$Npositional parameters match thevaluesarrays in both paths — off-by-one here would silently corrupt data.getBucketConfig(storage-module-cache.ts~line 383): The SELECT conditionally interpolatesowner_id,into the column list via${storageConfig.membershipType !== null ? 'owner_id,' : ''}. Verify this doesn't produce malformed SQL in edge cases (e.g.,membershipTypeof0is truthy for!== null, which is correct, but worth confirming).getStorageModuleConfigForOwner,resolveStorageModuleByFileId, andresolveStorageModule(bucket-provisioner): All three probe entity/file tables sequentially. Acceptable for 2-3 entity types with storage, but consider whether this will scale. The ownerId→module mapping is cached after first resolution in the presigned-url-plugin.resolveStorageModule(bucket-provisioner ~line 135):mod.entity_schema!andmod.entity_table!are guarded by the.filter()above, but TypeScript can't narrow through the filter. Verify the filter condition (m.entity_schema && m.entity_table) is sufficient.setup.sqlhand-writes thestorage_moduletable rather than deploying via@pgpm/metaschema-modules. If the published package schema changes again, this fixture must be manually updated to match. Verify the fixture's column list matches the 0.21.1 schema.has_storage=true. Test: (1)requestUploadUrlwithout ownerId → uses app_files; (2)requestUploadUrlwith a valid entity ownerId → uses entity-scoped files; (3)requestUploadUrlwith an invalid ownerId → returnsSTORAGE_MODULE_NOT_FOUND_FOR_OWNER; (4)confirmUploadwith a fileId from each scope resolves correctly; (5)provisionBucketwith/without ownerId.Notes
membership_typeandentity_table_idcolumns tostorage_moduleand parameterizedapply_storage_security.@pgpm/metaschema-modules@0.21.1(published from pgpm-modules PR modules #58, merged) which includes themembership_typecolumn in the deployed table schema.graphql/test(introspection query) andgraphql/server-test(schema SDL) to include the newownerIdfields onRequestUploadUrlInputandProvisionBucketInput.Link to Devin session: https://app.devin.ai/sessions/18879be982854a40abe5c9b915aa4a84
Requested by: @pyramation