Skip to content

Commit

Permalink
feat: Expose ActiveRecord validation errors in GraphQL (#102)
Browse files Browse the repository at this point in the history
  • Loading branch information
vincent-pochet authored Apr 26, 2022
1 parent eb20265 commit 9c7cdfd
Show file tree
Hide file tree
Showing 19 changed files with 58 additions and 57 deletions.
23 changes: 21 additions & 2 deletions app/graphql/concerns/execution_error_responder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,19 @@ module ExecutionErrorResponder

private

def execution_error(message: 'Internal Error', status: 422, code: 'internal_error')
GraphQL::ExecutionError.new(message, extensions: { status: status, code: code })
def execution_error(message: 'Internal Error', status: 422, code: 'internal_error', details: nil)
payload = {
status: status,
code: code,
}

if code == 'unprocessable_entity'
payload[:details] = details&.transform_keys do |key|
key.to_s.camelize(:lower)
end
end

GraphQL::ExecutionError.new(message, extensions: payload)
end

def not_found_error
Expand All @@ -17,4 +28,12 @@ def not_found_error
code: 'not_found',
)
end

def result_error(service_result)
execution_error(
code: service_result.error_code,
message: service_result.error,
details: service_result.error_details,
)
end
end
2 changes: 1 addition & 1 deletion app/graphql/mutations/billable_metrics/create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def resolve(**args)
.new(context[:current_user])
.create(**args.merge(organization_id: current_organization.id))

result.success? ? result.billable_metric : execution_error(code: result.error_code, message: result.error)
result.success? ? result.billable_metric : result_error(result)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/graphql/mutations/billable_metrics/destroy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Destroy < BaseMutation
def resolve(id:)
result = BillableMetricsService.new(context[:current_user]).destroy(id)

result.success? ? result.billable_metric : execution_error(code: result.error_code, message: result.error)
result.success? ? result.billable_metric : result_error(result)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/graphql/mutations/billable_metrics/update.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class Update < BaseMutation
def resolve(**args)
result = BillableMetricsService.new(context[:current_user]).update(**args)

result.success? ? result.billable_metric : execution_error(code: result.error_code, message: result.error)
result.success? ? result.billable_metric : result_error(result)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/graphql/mutations/customers/create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def resolve(**args)
.new(context[:current_user])
.create(**args.merge(organization_id: current_organization.id))

result.success? ? result.customer : execution_error(code: result.error_code, message: result.error)
result.success? ? result.customer : result_error(result)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/graphql/mutations/customers/destroy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Destroy < BaseMutation
def resolve(id:)
result = CustomersService.new(context[:current_user]).destroy(id: id)

result.success? ? result.customer : execution_error(code: result.error, message: result.error)
result.success? ? result.customer : result_error(result)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/graphql/mutations/customers/update.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class Update < BaseMutation
def resolve(**args)
result = CustomersService.new(context[:current_user]).update(**args)

result.success? ? result.customer : execution_error(code: result.error_code, message: result.message)
result.success? ? result.customer : result_error(result)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/graphql/mutations/login_user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class LoginUser < BaseMutation

def resolve(email:, password:)
result = UsersService.new.login(email, password)
result.success? ? result : execution_error(code: result.error_code, message: result.error)
result.success? ? result : result_error(result)
end
end
end
2 changes: 1 addition & 1 deletion app/graphql/mutations/plans/create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def resolve(**args)
.new(context[:current_user])
.create(**args.merge(organization_id: current_organization.id))

result.success? ? result.plan : execution_error(code: result.error_code, message: result.error)
result.success? ? result.plan : result_error(result)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/graphql/mutations/plans/destroy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Destroy < BaseMutation
def resolve(id:)
result = PlansService.new(context[:current_user]).destroy(id)

result.success? ? result.plan : execution_error(code: result.error, message: result.error)
result.success? ? result.plan : result_error(result)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/graphql/mutations/plans/update.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class Update < BaseMutation
def resolve(**args)
result = PlansService.new(context[:current_user]).update(**args)

result.success? ? result.plan : execution_error(code: result.error_code, message: result.message)
result.success? ? result.plan : result_error(result)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/graphql/mutations/register_user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def resolve(email:, password:, organization_name:)
organization_name
)

result.success? ? result : execution_error(code: result.error_code, message: result.error)
result.success? ? result : result_error(result)
end
end
end
2 changes: 1 addition & 1 deletion app/graphql/mutations/subscriptions/create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def resolve(**args)
params: args,
)

result.success? ? result.subscription : execution_error(code: result.error_code, message: result.error)
result.success? ? result.subscription : result_error(result)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/graphql/mutations/subscriptions/terminate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def resolve(**args)

result = ::Subscriptions::TerminateService.new(args[:id]).terminate

result.success? ? result.subscription : execution_error(code: result.error_code, message: result.error)
result.success? ? result.subscription : result_error(result)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/models/invoice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ class Invoice < ApplicationRecord
private

def validate_date_bounds
errors.add(:from_date, 'from_date must be before to_date') if from_date > to_date
errors.add(:from_date, :invalid_date_range) if from_date > to_date
end
end
11 changes: 8 additions & 3 deletions app/services/base_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class BaseService
class FailedResult < StandardError; end

class Result < OpenStruct
attr_reader :error, :error_code
attr_reader :error, :error_code, :error_details

def initialize
super
Expand All @@ -17,10 +17,11 @@ def success?
!failure
end

def fail!(code, message = nil)
def fail!(code, message = nil, details = nil)
@failure = true
@error_code = code
@error = message || code
@error_details = details

# Return self to return result immediately in case of failure:
# ```
Expand All @@ -30,7 +31,11 @@ def fail!(code, message = nil)
end

def fail_with_validations!(record)
fail!('unprocessable_entity', record.errors.full_messages)
fail!(
'unprocessable_entity',
'Validation error on the record',
record.errors.messages,
)
end

def throw_error
Expand Down
4 changes: 2 additions & 2 deletions app/services/billable_metrics_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def create(**args)
result.billable_metric = metric
result
rescue ActiveRecord::RecordInvalid => e
result.fail!('unprocessable_entity', e.record.errors.full_messages.join('\n'))
result.fail_with_validations!(e.record)
end

def update(**args)
Expand All @@ -37,7 +37,7 @@ def update(**args)
result.billable_metric = metric
result
rescue ActiveRecord::RecordInvalid => e
result.fail!('unprocessable_entity', e.record.errors.full_messages.join('\n'))
result.fail_with_validations!(e.record)
end

def destroy(id)
Expand Down
8 changes: 4 additions & 4 deletions app/services/plans_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def create(**args)
# Validates billable metrics
metric_ids = args[:charges].map { |c| c[:billable_metric_id] }.uniq
if metric_ids.present? && plan.organization.billable_metrics.where(id: metric_ids).count != metric_ids.count
return result.fail!('unprocessable_entity', 'Billable metrics does not exists')
return result.fail!('not_found', 'Billable metrics does not exists')
end

ActiveRecord::Base.transaction do
Expand All @@ -30,7 +30,7 @@ def create(**args)
result.plan = plan
result
rescue ActiveRecord::RecordInvalid => e
result.fail!('unprocessable_entity', e.record.errors.full_messages.join('\n'))
result.fail_with_validations!(e.record)
end

def update(**args)
Expand All @@ -54,7 +54,7 @@ def update(**args)

metric_ids = args[:charges].map { |c| c[:billable_metric_id] }.uniq
if metric_ids.present? && plan.organization.billable_metrics.where(id: metric_ids).count != metric_ids.count
return result.fail!('unprocessable_entity', 'Billable metrics does not exists')
return result.fail!('not_found', 'Billable metrics does not exists')
end

ActiveRecord::Base.transaction do
Expand All @@ -81,7 +81,7 @@ def update(**args)
result.plan = plan
result
rescue ActiveRecord::RecordInvalid => e
result.fail!('unprocessable_entity', e.record.errors.full_messages.join('\n'))
result.fail_with_validations!(e.record)
end

def destroy(id)
Expand Down
41 changes: 9 additions & 32 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
@@ -1,33 +1,10 @@
# Files in the config/locales directory are used for internationalization
# and are automatically loaded by Rails. If you want to use locales other
# than English, add the necessary files in this directory.
#
# To use the locales, use `I18n.t`:
#
# I18n.t "hello"
#
# In views, this is aliased to just `t`:
#
# <%= t("hello") %>
#
# To use a different locale, set it with `I18n.locale`:
#
# I18n.locale = :es
#
# This would use the information in config/locales/es.yml.
#
# The following keys must be escaped otherwise they will not be retrieved by
# the default I18n backend:
#
# true, false, on, off, yes, no
#
# Instead, surround them with single quotes.
#
# en:
# "true": "foo"
#
# To learn more, please read the Rails Internationalization guide
# available at https://guides.rubyonrails.org/i18n.html.

en:
hello: "Hello world"
activerecord:
errors:
messages:
blank: value_is_mandatory
country_code_invalid: not_a_valid_country_code
inclusion: value_is_invalid
invalid_date_range: invalid_date_range
required: relation_must_exist
taken: value_already_exist

0 comments on commit 9c7cdfd

Please sign in to comment.