I have an article already on File Uploading in GraphQL API in Rails with ActiveStorage. After that article, codelion
asked how to unit test the Upload!
Type in GraphQL. I was looking for the answers of the unit test myself and meanwhile Rails 7 launched. So, I started working on ActiveStorage with Rails 7 and I finally got the answers of both. So writing the article again to work with Rails 7.
Document Model:
I kept the document model same as before:
# app/models/document.rb class Document < ApplicationRecord ## RELATIONSHIPS has_one_attached :doc belongs_to :documentable, polymorphic: true, optional: true belongs_to :user, optional: true ## VALIDATIONS validate :doc_presence, on: :create def doc_presence pattern = %r{^(image|application|text)/(.)+$} unless doc.attached? && pattern.match?(doc.attachment.blob.content_type) errors.add(:doc, I18n.t("errors.models.document.file_presence")) end end end
GraphQL Document Type:
The Document Type for the GraphQL remains the same as well:
# app/graphql/types/objects/document_type.rb module Types module Objects class DocumentType < Types::BaseObject field :id, Integer, null: false field :documentable_type, String, null: true field :documentable_id, Integer, null: true field :content_type, String, null: true field :url, String, null: false field :created_at, GraphQL::Types::ISO8601DateTime, null: false field :updated_at, GraphQL::Types::ISO8601DateTime, null: false def url (Rails.env.development? || Rails.env.test?) ? Rails.application.routes.url_helpers.rails_blob_path(object.doc) : object.doc.service_url end def content_type object.doc.present? ? object.doc.blob.content_type : nil end end end end
I have url
, and I am using s3 service so showing the service url. But in other environment I am using local storage, that is why I have different document URLs for the environment. All other parameters are fairly descriptive.
Apollo Upload Server:
Now, comes the point to create a mutation resolver for uploading the file to the system. The complete documentation of ApolloUploadServer is here. After adding the gem to the Gemfile now we can create our mutation. My mutation looks like this: (It is changed from previous implementation)
# app/graphql/mutations/create_document.rb module Mutations class CreateDocument < Mutations::BaseMutation description "Create a document" argument :doc, ApolloUploadServer::Upload, required: true, description: "The document to upload" field :document, Types::Objects::DocumentType, null: false field :code, Types::Enums::CodeEnum, null: false def resolve(doc:) authenticate_user # First you need to create blob file in case of active storage. https://stackoverflow.com/q/70550808/4011757 attachment = ActiveStorage::Blob::create_and_upload!(io: doc, filename: doc.original_filename, content_type: doc.content_type) document = context[:current_user].documents.build(doc: attachment) if document.save {document: document, code: "SUCCESS"} else raise GraphQL::ExecutionError.new(document.errors.full_messages.join(", "), extensions: {code: "UNPROCESSABLE_ENTITY", errors: document.errors}) end end end end
Here, doc
is the active storage parameter saving the file, so, it is being used of ApolloUploadServer. The output will be the type we created before.
Now, here I changed according to the Rails 7 ActiveStorage. So, we have to create a blob before building the document.
ActiveStorage::Blob::create_and_upload!(io: doc, filename: doc.original_filename, content_type: doc.content_type)
The rails 7 has method of create_and_upload!
But before rails 7 it was create_after_upload!
.
Documents Factory:
I use FactoryBot for creating the factory, for stubbing the factory. My factory is containing an image that has to be uploaded and attached as doc every time a factory created:
# spec/factories/documents.rb FactoryBot.define do factory :document do association :user trait :with_image do after :build do |document| file_name = 'image.png' file_path = Rails.root.join('spec', 'support', 'fixtures', file_name) document.doc.attach(io: File.open(file_path), filename: file_name, content_type: 'image/png') end end trait :with_video do after :build do |document| file_name = 'video.mp4' file_path = Rails.root.join('spec', 'support', 'fixtures', file_name) document.doc.attach(io: File.open(file_path), filename: file_name, content_type: 'video/mp4') end end trait :with_pdf do after :build do |document| file_name = 'pdf.pdf' file_path = Rails.root.join('spec', 'support', 'fixtures', file_name) document.doc.attach(io: File.open(file_path), filename: file_name, content_type: 'application/pdf') end end trait :with_csv do after :build do |document| file_name = 'import_csv.csv' file_path = Rails.root.join('spec', 'support', 'fixtures', file_name) document.doc.attach(io: File.open(file_path), filename: file_name, content_type: 'text/csv') end end end end
Document Spec
Now the part of testing the document model:
# spec/models/document_spec.rb require 'rails_helper' RSpec.describe Document, type: :model do it 'has a valid factory' do document = FactoryBot.build(:document) expect(document.valid?).to be_falsey document = FactoryBot.build(:document, :with_image) expect(document.valid?).to be_truthy expect(document.doc.attached?).to be_truthy document.save! expect(document.doc.attached?).to be_truthy document = FactoryBot.build(:document, :with_video) expect(document.valid?).to be_truthy expect(document.doc.attached?).to be_truthy document.save! expect(document.doc.attached?).to be_truthy document = FactoryBot.build(:document, :with_pdf) expect(document.valid?).to be_truthy document = FactoryBot.build(:document, :with_csv) expect(document.valid?).to be_truthy end it 'must have user' do document = FactoryBot.build(:document, user: nil) expect(document.valid?).to be_falsey end it 'must contain file' do document = FactoryBot.build(:document, doc: nil) expect(document.valid?).to be_falsey end end
All the tests should pass.
Create Document Spec:
To test the above mutation, I had to use the factory of fixture, but that was changing the type of uploader type. So, I had to do the direct method:
# spec/graphql/mutations/create_document_spec.rb require 'rails_helper' module Mutations RSpec.describe CreateDocument, type: :request do describe '.resolve' do let(:session) {FactoryBot.create(:session)} it 'create a document for the user' do session.user.confirm headers = sign_in_test_headers session params = FactoryBot.attributes_for(:document, :with_image, user_id: session.user_id) query = <<-GRAPHQL mutation ($input: CreateDocumentInput!) { createDocument(input: $input) { document { id url } } } GRAPHQL variables = {input: {doc: ::ApolloUploadServer::Wrappers::UploadedFile.new(ActionDispatch::Http::UploadedFile.new(filename: "image.png", type: "image/png", tempfile: File.new("spec/support/fixtures/image.png")))}} response = ApiSchema.execute(query, variables: variables, context: {current_user: session.user}) expect(response.to_h["data"]["createDocument"]["document"]["url"]).not_to be_nil end end end end
This test should return success using the gem’s method ApolloUploadServer::Wrappers::UploadedFile
and uploaded file method ActionDispatch::Http::UploadedFile
.
The code is at https://gist.github.com/sulmanweb/64c878292de356c62481ea3a81ed3ff5
Happy Coding!
Top comments (0)