fix(uploads): default an unregistered upload column by plane shape, not scope name; pin the database-scope upload surface - #1767
Conversation
…ot scope name Pin the database-scope upload surface end to end: an unprefixed buckets/files plane emits uploadFile alongside the prefixed uploadAppFile, and a presigned PUT lands in a real physical bucket recorded on the tenant's own row.
🤖 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:
|
|
Runtime verification: the database-scope tenant upload path, proved end to end (local Postgres 18 + MinIO) A database-scope tenant obtains a presigned URL, uploads for real, gets a real per-tenant physical bucket, and cannot see another tenant's buckets. db-scope upload surface — 6/6, real PUT,
|







Summary
Track T6 of constructive-io/constructive-planning#1782 — the upload half of tenant static/SSG sites. Two of the three diagnosed defects do not reproduce on current
main; this PR pins them with tests and fixes the one surviving instance of the pattern behind defect 2.Defect 1 (db-scope upload mutations never generated) — does not reproduce. Pairing is registry/tag/FK based, not name based, since #1764:
pairStoragePlanereads the@storageBuckets/@storageFilestags and the files→buckets FK relation (graphile/graphile-storage-registry/src/pairing.ts), andgraphile-presigned-url-plugin/src/plugin.ts:27builds a surface per discovered plane viadiscoverStoragePlanes+uploadSurfaceNames. No regex, no prefix assumption. Now pinned at the GraphQL layer, not just the unit layer (below).Defect 2 (
WHERE scope = 'app') — the provisioner query is already fixed; one hard-coded'app'survived.ALL_STORAGE_MODULES_QUERY(storage-module-cache.ts:41-74) selects every module for the database with no scope filter, andgraphile-bucket-provisioner-pluginresolves modules by bucket-table identity, so explicitprovisionBucketwas fine. The remaining one was the managed-upload fallback for an unregistered upload column:Selection is now by shape — a global plane is one with no entity key — which is what a database-wide plane is whether it registered as
database,platform, orapp. No hiding: two global planes and no registry row throwsSTORAGE_MODULE_AMBIGUOUSnaming both scopes, and zero still throwsSTORAGE_MODULE_NOT_FOUND.Defect 3 (
buckets.auth_sel_pub USING (is_public = true)) — reproduces; fixed in the generator in constructive-io/constructive-db#3269 (separate repo, separate PR). Verified there that public object serving does not depend on that policy: the static gateway resolves throughresolve_route(), aSTABLE SECURITY DEFINERresolver, and readsphysical_namefrom the catalog buckets projection, never the tenant buckets table.Tests
graphql/server-test/__tests__/db-scope-upload.integration.test.tsbuilds one schema over both an app-scope plane (app_buckets/app_files) and a new database-scope fixture whose tables are the unprefixedbuckets/files, and assertsuploadAppFileanduploadFileboth exist — a name-based pairing rule fails this. It then proves the db-scope path end to end against real MinIO: presigned PUT → 200,physical_namerecorded on the tenant's own bucket row and equal to the bucket in the presigned URL, files row on the tenant plane only.Unit coverage for the fallback: an unregistered column on a
scope='database'plane resolves (passing'database'toresolve_default_bucket), and two global planes refuse.Verification
graphile/graphile-presigned-url-plugin: 71/71 passgraphql/server-test/__tests__/upload.integration.test.ts: 45/45 (unchanged baseline)graphql/server-test/__tests__/db-scope-upload.integration.test.ts: 6/6, real MinIOLink to Devin session: https://app.devin.ai/sessions/d86629cb5b2947c7b6ab15546906ce80
Requested by: @pyramation