Merge pull request #8918 from tchak/graphql-add-error-codes
ETQ intégrateur API, je voudrais avoir des codes d'erreur plus précis
This commit is contained in:
commit
a286af8a70
10 changed files with 125 additions and 22 deletions
|
@ -5,7 +5,9 @@ class API::V2::GraphqlController < API::V2::BaseController
|
||||||
|
|
||||||
render json: result
|
render json: result
|
||||||
rescue GraphQL::ParseError, JSON::ParserError => exception
|
rescue GraphQL::ParseError, JSON::ParserError => exception
|
||||||
handle_parse_error(exception)
|
handle_parse_error(exception, :graphql_parse_failed)
|
||||||
|
rescue ArgumentError => exception
|
||||||
|
handle_parse_error(exception, :bad_request)
|
||||||
rescue => exception
|
rescue => exception
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
handle_error_in_production(exception)
|
handle_error_in_production(exception)
|
||||||
|
@ -33,7 +35,12 @@ class API::V2::GraphqlController < API::V2::BaseController
|
||||||
rescue ActionDispatch::Http::Parameters::ParseError => exception
|
rescue ActionDispatch::Http::Parameters::ParseError => exception
|
||||||
render json: {
|
render json: {
|
||||||
errors: [
|
errors: [
|
||||||
{ message: exception.cause.message }
|
{
|
||||||
|
message: exception.cause.message,
|
||||||
|
extensions: {
|
||||||
|
code: :bad_request
|
||||||
|
}
|
||||||
|
}
|
||||||
],
|
],
|
||||||
data: nil
|
data: nil
|
||||||
}, status: 400
|
}, status: 400
|
||||||
|
@ -75,10 +82,13 @@ class API::V2::GraphqlController < API::V2::BaseController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_parse_error(exception)
|
def handle_parse_error(exception, code)
|
||||||
render json: {
|
render json: {
|
||||||
errors: [
|
errors: [
|
||||||
{ message: exception.message }
|
{
|
||||||
|
message: exception.message,
|
||||||
|
extensions: { code: }
|
||||||
|
}
|
||||||
],
|
],
|
||||||
data: nil
|
data: nil
|
||||||
}, status: 400
|
}, status: 400
|
||||||
|
@ -90,22 +100,32 @@ class API::V2::GraphqlController < API::V2::BaseController
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
errors: [
|
errors: [
|
||||||
{ message: exception.message, backtrace: exception.backtrace }
|
{
|
||||||
|
message: exception.message,
|
||||||
|
extensions: {
|
||||||
|
code: :internal_server_error,
|
||||||
|
backtrace: exception.backtrace
|
||||||
|
}
|
||||||
|
}
|
||||||
],
|
],
|
||||||
data: nil
|
data: nil
|
||||||
}, status: 500
|
}, status: 500
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_error_in_production(exception)
|
def handle_error_in_production(exception)
|
||||||
extra = { exception_id: SecureRandom.uuid }
|
exception_id = SecureRandom.uuid
|
||||||
Sentry.capture_exception(exception, extra:)
|
Sentry.with_scope do |scope|
|
||||||
|
scope.set_tags(exception_id:)
|
||||||
|
Sentry.capture_exception(exception)
|
||||||
|
end
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
errors: [
|
errors: [
|
||||||
{
|
{
|
||||||
message: "Internal Server Error",
|
message: "Internal Server Error",
|
||||||
extensions: {
|
extensions: {
|
||||||
exception: { id: extra[:exception_id] }
|
code: :internal_server_error,
|
||||||
|
exception_id:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -25,8 +25,6 @@ class API::V2::Schema < GraphQL::Schema
|
||||||
|
|
||||||
def self.object_from_id(id, ctx)
|
def self.object_from_id(id, ctx)
|
||||||
ApplicationRecord.record_from_typed_id(id)
|
ApplicationRecord.record_from_typed_id(id)
|
||||||
rescue => e
|
|
||||||
raise GraphQL::ExecutionError.new(e.message, extensions: { code: :not_found })
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.resolve_type(type_definition, object, ctx)
|
def self.resolve_type(type_definition, object, ctx)
|
||||||
|
@ -50,7 +48,7 @@ class API::V2::Schema < GraphQL::Schema
|
||||||
when GroupeInstructeur
|
when GroupeInstructeur
|
||||||
Types::GroupeInstructeurType
|
Types::GroupeInstructeurType
|
||||||
else
|
else
|
||||||
raise GraphQL::ExecutionError.new("Unexpected object: #{object}")
|
type_definition
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -129,8 +127,13 @@ class API::V2::Schema < GraphQL::Schema
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
rescue_from(ActiveRecord::RecordNotFound) do |_error, _object, _args, _ctx, field|
|
||||||
|
raise GraphQL::ExecutionError.new("#{field.type.unwrap.graphql_name} not found", extensions: { code: :not_found })
|
||||||
|
end
|
||||||
|
|
||||||
class Timeout < GraphQL::Schema::Timeout
|
class Timeout < GraphQL::Schema::Timeout
|
||||||
def handle_timeout(error, query)
|
def handle_timeout(error, query)
|
||||||
|
error.extensions = { code: :timeout }
|
||||||
Sentry.capture_exception(error, extra: query.context.query_info)
|
Sentry.capture_exception(error, extra: query.context.query_info)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,7 +8,7 @@ class API::V2::StoredQuery
|
||||||
when 'introspection'
|
when 'introspection'
|
||||||
GraphQL::Introspection::INTROSPECTION_QUERY
|
GraphQL::Introspection::INTROSPECTION_QUERY
|
||||||
else
|
else
|
||||||
raise GraphQL::ExecutionError.new("No query with id \"#{query_id}\"")
|
raise GraphQL::ExecutionError.new("No query with id \"#{query_id}\"", extensions: { code: :bad_request })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ module Loaders
|
||||||
|
|
||||||
def perform(keys)
|
def perform(keys)
|
||||||
query(keys).each { |record| fulfill(record.stable_id, [record].compact) }
|
query(keys).each { |record| fulfill(record.stable_id, [record].compact) }
|
||||||
keys.each { |key| fulfill(key, nil) unless fulfilled?(key) }
|
keys.each { |key| fulfill(key, []) unless fulfilled?(key) }
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -21,7 +21,7 @@ module Loaders
|
||||||
fulfilled_value = @array ? [record].compact : record
|
fulfilled_value = @array ? [record].compact : record
|
||||||
fulfill(record.public_send(@column), fulfilled_value)
|
fulfill(record.public_send(@column), fulfilled_value)
|
||||||
end
|
end
|
||||||
keys.each { |key| fulfill(key, nil) unless fulfilled?(key) }
|
keys.each { |key| fulfill(key, @array ? [] : nil) unless fulfilled?(key) }
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
module Types
|
module Types
|
||||||
class BaseObject < GraphQL::Schema::Object
|
class BaseObject < GraphQL::Schema::Object
|
||||||
field_class BaseField
|
field_class BaseField
|
||||||
|
|
||||||
|
class InvalidNullError < GraphQL::InvalidNullError
|
||||||
|
def to_h
|
||||||
|
super.merge(extensions: { code: :invalid_null })
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,13 +26,11 @@ module Types
|
||||||
demarche_number = demarche.number.presence || ApplicationRecord.id_from_typed_id(demarche.id)
|
demarche_number = demarche.number.presence || ApplicationRecord.id_from_typed_id(demarche.id)
|
||||||
Procedure
|
Procedure
|
||||||
.includes(draft_revision: :procedure, published_revision: :procedure)
|
.includes(draft_revision: :procedure, published_revision: :procedure)
|
||||||
.find_by(id: demarche_number)
|
.find(demarche_number)
|
||||||
end
|
end
|
||||||
|
|
||||||
def demarche(number:)
|
def demarche(number:)
|
||||||
Procedure.for_api_v2.find(number)
|
Procedure.for_api_v2.find(number)
|
||||||
rescue => e
|
|
||||||
raise GraphQL::ExecutionError.new(e.message, extensions: { code: :not_found })
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def dossier(number:)
|
def dossier(number:)
|
||||||
|
@ -42,14 +40,10 @@ module Types
|
||||||
Dossier.visible_by_administration.for_api_v2.find(number)
|
Dossier.visible_by_administration.for_api_v2.find(number)
|
||||||
end
|
end
|
||||||
DossierPreloader.load_one(dossier)
|
DossierPreloader.load_one(dossier)
|
||||||
rescue => e
|
|
||||||
raise GraphQL::ExecutionError.new(e.message, extensions: { code: :not_found })
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def groupe_instructeur(number:)
|
def groupe_instructeur(number:)
|
||||||
GroupeInstructeur.for_api_v2.find(number)
|
GroupeInstructeur.for_api_v2.find(number)
|
||||||
rescue => e
|
|
||||||
raise GraphQL::ExecutionError.new(e.message, extensions: { code: :not_found })
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.accessible?(context)
|
def self.accessible?(context)
|
||||||
|
|
|
@ -684,7 +684,7 @@ describe API::V2::GraphqlController do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should return an error" do
|
it "should return an error" do
|
||||||
expect(gql_errors).to eq([{ message: "Cannot return null for non-nullable field PersonneMorale.siegeSocial" }])
|
expect(gql_errors).to eq([{ message: "Cannot return null for non-nullable field PersonneMorale.siegeSocial", extensions: { code: "invalid_null" } }])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -64,6 +64,18 @@ describe API::V2::GraphqlController do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'timeout' do
|
||||||
|
let(:variables) { { dossierNumber: dossier.id } }
|
||||||
|
let(:operation_name) { 'getDossier' }
|
||||||
|
|
||||||
|
before { allow_any_instance_of(API::V2::Schema::Timeout).to receive(:max_seconds).and_return(0) }
|
||||||
|
|
||||||
|
it {
|
||||||
|
expect(gql_errors.first[:message]).to eq('Timeout on Query.dossier')
|
||||||
|
expect(gql_errors.first[:extensions]).to eq({ code: 'timeout' })
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
context 'getDossier' do
|
context 'getDossier' do
|
||||||
let(:variables) { { dossierNumber: dossier.id } }
|
let(:variables) { { dossierNumber: dossier.id } }
|
||||||
let(:operation_name) { 'getDossier' }
|
let(:operation_name) { 'getDossier' }
|
||||||
|
@ -76,6 +88,15 @@ describe API::V2::GraphqlController do
|
||||||
expect(gql_data[:dossier][:demandeur][:prenom]).to eq(dossier.individual.prenom)
|
expect(gql_data[:dossier][:demandeur][:prenom]).to eq(dossier.individual.prenom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context 'not found' do
|
||||||
|
let(:variables) { { dossierNumber: 0 } }
|
||||||
|
|
||||||
|
it {
|
||||||
|
expect(gql_errors.first[:message]).to eq('Dossier not found')
|
||||||
|
expect(gql_errors.first[:extensions]).to eq({ code: 'not_found' })
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
context 'with entreprise' do
|
context 'with entreprise' do
|
||||||
let(:procedure) { create(:procedure, :published, :with_service, administrateurs: [admin], types_de_champ_public:) }
|
let(:procedure) { create(:procedure, :published, :with_service, administrateurs: [admin], types_de_champ_public:) }
|
||||||
let(:dossier) { create(:dossier, :en_construction, :with_entreprise, procedure: procedure) }
|
let(:dossier) { create(:dossier, :en_construction, :with_entreprise, procedure: procedure) }
|
||||||
|
@ -114,6 +135,15 @@ describe API::V2::GraphqlController do
|
||||||
expect(gql_data[:demarche][:dossiers]).to be_nil
|
expect(gql_data[:demarche][:dossiers]).to be_nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context 'not found' do
|
||||||
|
let(:variables) { { demarcheNumber: 0 } }
|
||||||
|
|
||||||
|
it {
|
||||||
|
expect(gql_errors.first[:message]).to eq('Demarche not found')
|
||||||
|
expect(gql_errors.first[:extensions]).to eq({ code: 'not_found' })
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
context 'include Dossiers' do
|
context 'include Dossiers' do
|
||||||
let(:variables) { { demarcheNumber: procedure.id, includeDossiers: true } }
|
let(:variables) { { demarcheNumber: procedure.id, includeDossiers: true } }
|
||||||
|
|
||||||
|
@ -182,6 +212,15 @@ describe API::V2::GraphqlController do
|
||||||
expect(gql_data[:groupeInstructeur][:dossiers]).to be_nil
|
expect(gql_data[:groupeInstructeur][:dossiers]).to be_nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context 'not found' do
|
||||||
|
let(:variables) { { groupeInstructeurNumber: 0 } }
|
||||||
|
|
||||||
|
it {
|
||||||
|
expect(gql_errors.first[:message]).to eq('GroupeInstructeurWithDossiers not found')
|
||||||
|
expect(gql_errors.first[:extensions]).to eq({ code: 'not_found' })
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
context 'include Dossiers' do
|
context 'include Dossiers' do
|
||||||
let(:variables) { { groupeInstructeurNumber: groupe_instructeur.id, includeDossiers: true } }
|
let(:variables) { { groupeInstructeurNumber: groupe_instructeur.id, includeDossiers: true } }
|
||||||
|
|
||||||
|
@ -241,6 +280,15 @@ describe API::V2::GraphqlController do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'not found' do
|
||||||
|
let(:variables) { { demarche: { number: 0 } } }
|
||||||
|
|
||||||
|
it {
|
||||||
|
expect(gql_errors.first[:message]).to eq('DemarcheDescriptor not found')
|
||||||
|
expect(gql_errors.first[:extensions]).to eq({ code: 'not_found' })
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
context 'find by id' do
|
context 'find by id' do
|
||||||
let(:variables) { { demarche: { id: procedure.to_typed_id } } }
|
let(:variables) { { demarche: { id: procedure.to_typed_id } } }
|
||||||
|
|
||||||
|
|
|
@ -75,6 +75,27 @@ RSpec.describe Types::DossierType, type: :graphql do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'dossier with selected champ' do
|
||||||
|
let(:procedure) { create(:procedure, types_de_champ_public: [{ libelle: 'yolo' }, { libelle: 'toto' }]) }
|
||||||
|
let(:dossier) { create(:dossier, :en_construction, :with_populated_champs, procedure:) }
|
||||||
|
let(:query) { DOSSIER_WITH_SELECTED_CHAMP_QUERY }
|
||||||
|
let(:variables) { { number: dossier.id, id: champ.to_typed_id } }
|
||||||
|
let(:champ) { dossier.champs_public.last }
|
||||||
|
|
||||||
|
context 'when champ exists' do
|
||||||
|
it {
|
||||||
|
expect(data[:dossier][:champs].size).to eq 1
|
||||||
|
expect(data[:dossier][:champs][0][:label]).to eq "toto"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when champ dosen't exists" do
|
||||||
|
let(:variables) { { number: dossier.id, id: '1234' } }
|
||||||
|
|
||||||
|
it { expect(data[:dossier][:champs].size).to eq 0 }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'dossier with conditional champs' do
|
describe 'dossier with conditional champs' do
|
||||||
include Logic
|
include Logic
|
||||||
let(:stable_id) { 1234 }
|
let(:stable_id) { 1234 }
|
||||||
|
@ -390,4 +411,15 @@ RSpec.describe Types::DossierType, type: :graphql do
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
GRAPHQL
|
GRAPHQL
|
||||||
|
|
||||||
|
DOSSIER_WITH_SELECTED_CHAMP_QUERY = <<-GRAPHQL
|
||||||
|
query($number: Int!, $id: ID!) {
|
||||||
|
dossier(number: $number) {
|
||||||
|
champs(id: $id) {
|
||||||
|
id
|
||||||
|
label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GRAPHQL
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue