feat(storage): managed upload lane — tenant-resolved buckets, files rows, projection documents - #1679
Merged
Merged
Conversation
…solved buckets Both upload transports now resolve the tenant's logical bucket in the database and create an authoritative files row, and return the projection document whose id is what storage GC counts before collecting an object. - upload field identity (schema/table/column) is reported to the resolver - file_ref_field registry lookup supplies storage module + bucket intent - omitted bucketKey resolves the reserved default tag via function_resolution.resolve_default_bucket instead of an env bucket - multipart uploads stage, hash, promote to the content key, dedup, and insert a files row; image/upload columns keep url/filename/mime - removes the global BUCKET_NAME upload lane Refs constructive-planning#1476 (item 3 of §6)
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:
|
A bucket with allow_custom_keys=true (e.g. a static site's) is addressed by publisher-chosen paths; the multipart lane only mints content-hash keys, which would pollute it with unreachable objects. Path-keyed uploads belong to the presigned lane, which accepts an explicit key.
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
Item 3 of constructive-planning#1476 §6: both upload transports now resolve the tenant's bucket in the database and produce an authoritative files row, and the value stored in an
image/uploadcolumn becomes a projection of that row. Itsidis the load-bearing field — it is what the document-reference refcount shipped in constructive-db#2879 counts before storage GC collects an S3 object. Until this lands, nothing writes that id, so that refcount has no producer.The multipart
@uploadlane was the second, unmanaged storage model:Bucket resolution is logical, never environmental. A field's
file_ref_fieldrow supplies its storage module and declaredbucket_key/publicness; with no declared key,function_resolution.resolve_default_bucket(database_id, scope, entity_id, public_access, NULL)resolves the tenant's reserved default tag and raises in SQL on none/ambiguous.mintPhysicalBucketNameno longer falls back to the globally configured S3 bucket — a deployment withoutresolveBucketNamenow fails loudly instead of writing a tenant's object into a shared env bucket.Streaming needs staging. The final key is the content hash, which isn't known until the last byte: bytes stream through a hashing
Transformto.staging/<uuid>, thenfinalizeStagedUploadeither finds an existing row on(content_hash, bucket_id)and drops the staged object (dedup — a property of the object, so it holds across transports), or server-side-copies to the content key and inserts the row. Every failure path leaves S3 as it found it: bytes no row names are bytes GC can never reach, so an insert failure drops both the promoted and the staged object.Frontend compatibility. Presigned mutations are additive: all existing payload fields stay,
file(the projection) is added, andbucketKeyis relaxed to nullable (isPublicselects which default) — relaxing a required input is backwards compatible. For multipart,image/uploadkeepurl/filename/mimealongside the new fields, so existingphoto.urlreaders keep working;urlis marked deprecated in favour ofid+ the files row's late-bounddownloadUrl, which also works for private buckets.Two deliberate calls worth reviewing:
attachmentis atextdomain, so it cannot hold a projection. It still stores a plain public URL, and a resolved private bucket raises (ATTACHMENT_BUCKET_NOT_PUBLIC) rather than persisting an expiring presigned URL in a column. Consequence: an attachment column is not a countable document reference — its files row keeps the object alive, but the column contributes nothing to the refcount. Migrating those columns touploadis the fix, and is not in this PR.STORAGE_SCOPE_UNSUPPORTED) — that transport carries no owner row to resolve an owner-scoped bucket against.New:
file-ref-registry.ts(registry lookup, LRU-cached, misses never cached),default-bucket.ts,physical-bucket.ts(name minting + first-provision recording, moved out ofplugin.ts),managed-upload.ts(the lifecycle both transports share).Tests:
graphile-presigned-url-plugin/__tests__/managed-upload.test.tsand a rewrittengraphile-settings/__tests__/upload-resolver.test.tsfake the database at the query boundary — unanswered statements throw, so a path that silently proceeds fails the test. They cover default-tag resolution, declared bucket keys, first-provision recording, dedup, projection shape, compaturl, staged-object cleanup, the private-attachment refusal, and each raise path.Verified: package tests for all four touched packages (MinIO integration suites included),
pnpm build,pnpm lint(0 errors).Link to Devin session: https://app.devin.ai/sessions/739f6e40a0dc44c4a49b8d84390a2268
Requested by: @pyramation