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)