zammad_schema.rb 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. class Gql::ZammadSchema < GraphQL::Schema
  3. mutation Gql::EntryPoints::Mutations
  4. query Gql::EntryPoints::Queries
  5. subscription Gql::EntryPoints::Subscriptions
  6. context_class Gql::Context::CurrentUserAware
  7. use GraphQL::Subscriptions::ActionCableSubscriptions, broadcast: true, default_broadcastable: false
  8. # Enable batch loading
  9. use GraphQL::Batch
  10. description 'This is the Zammad GraphQL API'
  11. # Set default limits to protect the system. Values may need to be adjusted in future.
  12. default_max_page_size 2000
  13. default_page_size 100
  14. max_complexity 10_000
  15. # Depth of 15 is needed for commmon introspection queries like Insomnia.
  16. max_depth 15
  17. TYPE_MAP = {
  18. ::Store => ::Gql::Types::StoredFileType,
  19. ::Taskbar => ::Gql::Types::User::TaskbarItemType,
  20. }.freeze
  21. ABSTRACT_TYPE_MAP = {
  22. ::Gql::Types::User::TaskbarItemEntityType => ::Gql::Types::User::TaskbarItemEntity::TicketCreateType,
  23. }.freeze
  24. # Union and Interface Resolution
  25. def self.resolve_type(abstract_type, obj, _ctx)
  26. TYPE_MAP[obj.class] || "Gql::Types::#{obj.class.name}Type".constantize
  27. rescue NameError
  28. ABSTRACT_TYPE_MAP[abstract_type]
  29. rescue
  30. raise GraphQL::RequiredImplementationMissingError, "Cannot resolve type for '#{obj.class.name}'."
  31. end
  32. # Relay-style Object Identification:
  33. # Return a string UUID for the internal ID.
  34. def self.id_from_internal_id(klass, internal_id)
  35. GlobalID.new(::URI::GID.build(app: GlobalID.app, model_name: klass.to_s, model_id: internal_id)).to_s
  36. end
  37. # Return a string UUID for `object`
  38. def self.id_from_object(object, _type_definition = nil, _query_ctx = nil)
  39. object.to_global_id.to_s
  40. end
  41. # Given a string UUID, find the object.
  42. def self.object_from_id(id, _query_ctx = nil, type: ActiveRecord::Base)
  43. GlobalID.find(id, only: type)
  44. end
  45. # Find the object, but also ensure its type and that it was actually found.
  46. def self.verified_object_from_id(id, type:)
  47. object_from_id(id, type: type) || raise(ActiveRecord::RecordNotFound, "Could not find #{type} #{id}")
  48. end
  49. # Like .verified_object_from_id, but with additional Pundit autorization.
  50. # This is only needed for objects where no validation takes place through their GraphQL type.
  51. def self.authorized_object_from_id(id, type:, user:, query: :show?)
  52. verified_object_from_id(id, type: type).tap do |object|
  53. Pundit.authorize user, object, query
  54. rescue Pundit::NotAuthorizedError => e
  55. # Map Pundit errors since we are not in a GraphQL built-in authorization context here.
  56. raise Exceptions::Forbidden, e.message
  57. end
  58. end
  59. def self.unauthorized_object(error)
  60. raise Exceptions::Forbidden, error.message # Add a top-level error to the response instead of returning nil.
  61. end
  62. def self.unauthorized_field(error)
  63. raise Exceptions::Forbidden, error.message # Add a top-level error to the response instead of returning nil.
  64. end
  65. RETHROWABLE_ERRORS = [GraphQL::ExecutionError, ArgumentError, IndexError, NameError, RangeError, RegexpError, SystemCallError, ThreadError, TypeError, ZeroDivisionError].freeze
  66. # Post-process errors and enrich them with meta information for processing on the client side.
  67. rescue_from(StandardError) do |err, _obj, _args, ctx, field|
  68. if field&.path&.start_with?('Mutations.')
  69. user_locale = ctx.current_user?&.locale
  70. case err
  71. when ActiveRecord::RecordInvalid
  72. next { errors: build_record_invalid_errors(err.record, user_locale) }
  73. when ActiveRecord::RecordNotUnique
  74. next { errors: [ message: Translation.translate(user_locale, 'This object already exists.') ] }
  75. end
  76. end
  77. # Re-throw built-in errors that point to programming errors rather than problems with input or data - causes GraphQL processing to be aborted.
  78. RETHROWABLE_ERRORS.each do |klass|
  79. raise err if err.instance_of?(klass)
  80. end
  81. extensions = {
  82. type: err.class.name,
  83. }
  84. if Rails.env.local?
  85. extensions[:backtrace] = Rails.backtrace_cleaner.clean(err.backtrace)
  86. end
  87. # We need to throw an ExecutionError, all others would cause the GraphQL processing to die.
  88. raise GraphQL::ExecutionError.new(err.message, extensions: extensions)
  89. end
  90. def self.build_record_invalid_errors(record, user_locale)
  91. record.errors.map do |e|
  92. field_name = e.attribute.to_s.camelize(:lower)
  93. {
  94. field: field_name == 'base' ? nil : field_name,
  95. message: e.localized_full_message(locale: user_locale, no_field_name: true)
  96. }
  97. end
  98. end
  99. private_class_method :build_record_invalid_errors
  100. end