Skip to content

feat: per-table storage middleware — replace global mutations with typed per-table hooks#1064

Merged
pyramation merged 4 commits intomainfrom
feat/per-table-storage-middleware
May 7, 2026
Merged

feat: per-table storage middleware — replace global mutations with typed per-table hooks#1064
pyramation merged 4 commits intomainfrom
feat/per-table-storage-middleware

Conversation

@pyramation
Copy link
Copy Markdown
Contributor

@pyramation pyramation commented May 6, 2026

Summary

Replaces the global requestUploadUrl / requestBulkUploadUrls / deleteFile mutations with per-table PostGraphile hooks, keyed by @storageBuckets and @storageFiles smart tags.

graphile-presigned-url-plugin:

  • plugin.ts: Rewritten — requestUploadUrl and requestBulkUploadUrls added as fields on any type tagged @storageBuckets; delete middleware wraps delete* mutations on @storageFiles tables with S3 cleanup
  • storage-module-cache.ts: New loadAllStorageModules() + resolveStorageConfigFromCodec() helpers — matches codec's schema+table name against cached storage modules (no ownerId/probing needed)
  • download-url-field.ts: Uses codec-based resolution for entity-scoped files
  • s3-signer.ts: Added deleteS3Object()
  • Global mutations removed entirely — no backward compatibility wrappers

upload-client:

  • Updated to query bucket by key via collection query, then call bucket.requestUploadUrl(...) field
  • New bucketQueryField option for custom bucket collection field name

Tests:

  • Upload integration test uses buckets(condition: { key: $key }) { nodes { ... } } (not bucketByKey — disabled by NoUniqueLookupPreset)
  • Schema snapshots deleted and regenerated (global mutations removed from introspection)

Design: Each storage module (app-level, org-scoped, room-scoped, etc.) gets its own typed GraphQL API. No scope resolution needed — PostGraphile already knows which table each type comes from. The plugin detects smart tags at schema build time and augments the appropriate types.

Depends on: constructive-db PR #1037 (adds @storageBuckets smart tag via new append_smart_tags helper)

Review & Testing Checklist for Human

  • Verify requestUploadUrl field appears on bucket types in GraphiQL (on the Bucket type, not as a root mutation)
  • Test upload flow end-to-end: query bucket → call requestUploadUrl field → PUT to presigned URL
  • Verify delete middleware fires: delete a file via generated deleteAppFile mutation and check S3 object cleanup
  • Confirm downloadUrl still works for entity-scoped files (not just app-level)
  • Check that old global mutations (requestUploadUrl, requestBulkUploadUrls) are gone from the schema

Notes

  • The upload-client now takes a bucketQueryField option (defaults to 'buckets') for apps with custom bucket table names
  • Delete middleware reads the file row BEFORE PostGraphile's delete executes (to get the S3 key), then does S3 cleanup after
  • The AFTER DELETE trigger in constructive-db is still the async safety net for S3 GC"

Link to Devin session: https://app.devin.ai/sessions/ffa3ed8652fc412f976accbdc229c88d
Requested by: @pyramation

pyramation added 2 commits May 6, 2026 23:30
…elete middleware on file tables

Replace global requestUploadUrl/requestBulkUploadUrls mutations with per-table
fields on @storageBuckets-tagged types. Replace global deleteFile mutation with
middleware that wraps PostGraphile's generated delete* mutations on @storageFiles-
tagged tables with S3 cleanup.

Key changes:
- plugin.ts: rewritten with GraphQLObjectType_fields hook for upload fields and
  GraphQLObjectType_fields_field hook for delete middleware
- storage-module-cache.ts: add loadAllStorageModules() and resolveStorageConfigFromCodec()
  for codec-based scope resolution (no probing)
- download-url-field.ts: use codec-based resolution instead of app-level only
- s3-signer.ts: add deleteS3Object() function
- upload.integration.test.ts: update to use per-table bucket query pattern
- s3-signer.integration.test.ts: add deleteS3Object tests
- queries.ts: replace global mutation with buildRequestUploadUrlQuery() builder
- upload.ts: query bucket by key, extract upload payload from nested response
- types.ts: add bucketQueryField option, replace status with previousVersionId
- index.ts: export new query builder + DEFAULT_BUCKET_QUERY_FIELD
- upload.test.ts: mock executor returns per-table nested bucket response
@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

pyramation added 2 commits May 7, 2026 00:15
- Replace bucketByKey (disabled by NoUniqueLookupPreset) with
  buckets(condition: { key: $key }) { nodes { ... } } collection query
- Delete stale schema snapshots that referenced removed global mutations
  (requestUploadUrl, requestBulkUploadUrls) — jest will regenerate them
- Replace condition (not available) with where: { key: { equalTo: ... } }
  connection filter for bucket lookup in upload integration test
- Regenerate schema-snapshot and graphile-test introspection snapshots
  (global mutations removed, per-table upload fields added)
@pyramation pyramation merged commit dcf166d into main May 7, 2026
53 checks passed
@pyramation pyramation deleted the feat/per-table-storage-middleware branch May 7, 2026 00:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant