Lighthouse GraphQL RailsLighthouse GraphQL Rails
Docs
Directives
Example app
Changelog
RubyGems
GitHub
Docs
Directives
Example app
Changelog
RubyGems
GitHub
  • Getting Started

    • Motivation
    • Getting Started
    • Configuration
  • The Basics

    • Resolvers, Mutations and Builders
    • Generators
    • Unions, Interfaces and Scalars
    • Relay: Global Object Identification
    • Relationships
    • Filtering & Ordering
    • Mutations
    • File Uploads
    • Validation
    • Subscriptions
  • Security

    • Authentication & Authorization
    • Query Complexity & Depth
  • Performance

    • Performance at scale
    • Caching
  • Digging Deeper

    • Schema Organisation
    • Custom Directives
    • Events
    • Apollo Federation
    • Best Practices
  • API Reference

    • Directives Reference

File Uploads

Clients send files with the GraphQL multipart request specification, which Apollo Upload Client, urql and most GraphQL clients speak: a multipart form with the operation as JSON, a map of where each file goes, and the files. Lighthouse::GraphQL::Controller reads it, so the endpoint needs nothing more.

The Upload scalar

Declare it, and use it for arguments:

"A file sent with a multipart request."
scalar Upload

type Mutation {
  updateAvatar(avatar: Upload!): User! @update @inject(context: "current_user.id", name: "id")
}

Lighthouse reads its values itself (Lighthouse::GraphQL::Upload): the resolver gets the uploaded file, an ActionDispatch::Http::UploadedFile. A value that is not a file makes the argument invalid. Upload is for arguments only, never a field's result.

An ActiveStorage attachment takes the file as it is: with has_one_attached :avatar on User, the mutation above attaches it.

@upload

@upload stores the file first, and passes along what was stored instead, as PHP Lighthouse's @upload does on a Laravel disk. The file goes to your app's ActiveStorage, the way the rest of the app stores files:

type Mutation {
  updateAvatar(avatar: Upload! @upload(path: "avatars")): User!
    @guard
    @update
    @inject(context: "current_user.id", name: "id")
}
PHP Lighthouse (Laravel)lighthouse-graphql (ActiveStorage)
diska disk of config/filesystems.php; filesystems.default without ita service of config/storage.yml; config.active_storage.service without it
path ("/")the directory the file is stored inthe directory the blob's key starts with (avatars/<token>); at "/", ActiveStorage's own key
public (false)the file's visibilitymust agree with the service's (see below)
the argument holdsthe stored paththe blob's signed id

The signed id is what an attachment attribute attaches (user.avatar = signed_id) and ActiveStorage::Blob.find_signed finds; the blob keeps the file's name and content type. Lists of files are stored one by one, and null stays null.

ActiveStorage makes a whole service public (public: true in config/storage.yml) or private, never one file. So public: must agree with the service's, and the schema never says a file is private that anyone can read: @upload(public: true) on a private service fails, and so does @upload (public: false) on a public one. A service config/storage.yml does not declare fails too.

To store files somewhere else (S3, GCS, Azure or a service of your own), declare an ActiveStorage service for it and name it with disk:, as a Laravel app declares a disk:

# config/storage.yml
avatars:
  service: S3
  bucket: my-app-avatars
  region: us-east-1
  public: true
updateAvatar(avatar: Upload! @upload(disk: "avatars", path: "users", public: true)): User!

@upload runs once @guard, the @can* family and @validator have let the call through, so a refused call stores nothing.

The request

curl localhost:3000/graphql \
  -H 'Authorization: Bearer <token>' \
  -F operations='{ "query": "mutation($avatar: Upload!) { updateAvatar(avatar: $avatar) { avatarUrl } }", "variables": { "avatar": null } }' \
  -F map='{ "0": ["variables.avatar"] }' \
  -F 0=@me.png

A batch sends an array as operations, and maps files to "0.variables.avatar", "1.variables.files.0", and so on.

A multipart request is also what an HTML form on another site can send. If the GraphQL controller authenticates with the session cookie, keep Rails' CSRF protection on it (protect_from_forgery, on by default in ActionController::Base) and have clients send the X-CSRF-Token header, as PHP Lighthouse has them send X-Requested-With for its EnsureXHR middleware. A controller that authenticates with a token header is not exposed.

In a request spec:

avatar = Rack::Test::UploadedFile.new('spec/fixtures/me.png', 'image/png')
post '/graphql', params: {
  operations: { query: mutation, variables: { avatar: nil } }.to_json,
  map: { '0' => ['variables.avatar'] }.to_json,
  '0' => avatar
}
Edit this page on GitHub
Prev
Mutations
Next
Validation