decidim-api 0.31.4 → 0.32.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -13
  3. data/app/controllers/decidim/api/sessions_controller.rb +1 -1
  4. data/app/models/decidim/api/api_user.rb +5 -1
  5. data/config/locales/ca-IT.yml +7 -0
  6. data/config/locales/ca.yml +7 -0
  7. data/config/locales/cs.yml +7 -0
  8. data/config/locales/el.yml +11 -0
  9. data/config/locales/en.yml +7 -0
  10. data/config/locales/es-MX.yml +7 -0
  11. data/config/locales/es-PY.yml +7 -0
  12. data/config/locales/es.yml +7 -0
  13. data/config/locales/eu.yml +7 -0
  14. data/config/locales/fi-plain.yml +7 -0
  15. data/config/locales/fi.yml +7 -0
  16. data/config/locales/fr-CA.yml +4 -0
  17. data/config/locales/fr.yml +4 -0
  18. data/config/locales/ja.yml +7 -0
  19. data/config/locales/no.yml +1 -0
  20. data/config/locales/pt-BR.yml +7 -0
  21. data/config/locales/ro-RO.yml +7 -0
  22. data/config/locales/sv.yml +7 -0
  23. data/decidim-api.gemspec +12 -15
  24. data/docs/usage.md +3 -634
  25. data/lib/decidim/api/component_mutation_type.rb +1 -2
  26. data/lib/decidim/api/errors/attribute_validation_error.rb +73 -0
  27. data/lib/decidim/api/errors/invalid_locale_error.rb +14 -0
  28. data/lib/decidim/api/errors/locale_error.rb +14 -0
  29. data/lib/decidim/api/errors/mutation_not_authorized_error.rb +14 -0
  30. data/lib/decidim/api/errors/not_found_error.rb +14 -0
  31. data/lib/decidim/api/errors/permission_not_set_error.rb +14 -0
  32. data/lib/decidim/api/errors/unauthorized_field_error.rb +14 -0
  33. data/lib/decidim/api/errors/unauthorized_object_error.rb +14 -0
  34. data/lib/decidim/api/errors/validation_error.rb +13 -0
  35. data/lib/decidim/api/graphiql/config.rb +1 -1
  36. data/lib/decidim/api/graphql_permissions.rb +17 -12
  37. data/lib/decidim/api/query_type.rb +91 -0
  38. data/lib/decidim/api/schema.rb +27 -1
  39. data/lib/decidim/api/test/component_context.rb +59 -51
  40. data/lib/decidim/api/test/shared_examples/commentable_interface_examples.rb +46 -0
  41. data/lib/decidim/api/test/shared_examples/followable_interface_examples.rb +12 -1
  42. data/lib/decidim/api/test/shared_examples/statistics_examples.rb +0 -2
  43. data/lib/decidim/api/test/type_context.rb +10 -2
  44. data/lib/decidim/api/test.rb +1 -0
  45. data/lib/decidim/api/types/access_mode_enum.rb +15 -0
  46. data/lib/decidim/api/types/base_mutation.rb +28 -0
  47. data/lib/decidim/api/types/base_object.rb +12 -0
  48. data/lib/decidim/api/types.rb +12 -1
  49. data/lib/decidim/api/version.rb +1 -1
  50. data/lib/decidim/api.rb +22 -32
  51. metadata +59 -34
  52. /data/lib/decidim/api/test/{mutation_context.rb → shared_examples/mutation_context.rb} +0 -0
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Api
5
+ module Errors
6
+ class AttributeValidationError < GraphQL::ExecutionError
7
+ def initialize(messages, ast_node: nil, options: nil, extensions: nil)
8
+ @ast_node = ast_node
9
+ @options = options
10
+ @extensions = extensions
11
+
12
+ @messages = messages
13
+
14
+ message_str =
15
+ if messages.is_a?(ActiveModel::Errors)
16
+ messages.full_messages.join(", ")
17
+ elsif messages.is_a?(Array)
18
+ messages.map { |a| a[:message] }.join(", ")
19
+ else
20
+ messages.to_s
21
+ end
22
+ super(message_str)
23
+ end
24
+
25
+ def to_h
26
+ hash = {}
27
+ if @messages.is_a?(ActiveModel::Errors)
28
+ hash["message"] = @messages.map do |error|
29
+ # This is the GraphQL argument which corresponds to the validation error:
30
+ local_path = ["attributes", error.attribute.to_s.camelize(:lower)]
31
+ {
32
+ path: local_path,
33
+ message: error.message
34
+ }
35
+ end
36
+ end
37
+
38
+ hash["message"] = @messages if @messages.is_a?(Array)
39
+
40
+ if ast_node
41
+ hash["locations"] = [
42
+ {
43
+ "line" => ast_node.line,
44
+ "column" => ast_node.col
45
+ }
46
+ ]
47
+ end
48
+
49
+ hash["path"] = path if path
50
+
51
+ hash.merge!(options) if options
52
+
53
+ if extensions
54
+ hash["extensions"] = extensions.transform_keys do |(key, value), ext|
55
+ ext[key.to_s] = value
56
+ end
57
+ end
58
+
59
+ hash.merge!({ "extensions" => { "code" => "ATTRIBUTE_VALIDATION_ERROR" } })
60
+
61
+ hash
62
+ end
63
+
64
+ def message
65
+ return @messages.full_messages.join(", ") if @messages.is_a?(ActiveModel::Errors)
66
+ return @messages.map { |a| [a[:path].last, a[:message]].join(": ") }.join(", ") if @messages.is_a?(Array)
67
+
68
+ @messages.to_s
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Api
5
+ module Errors
6
+ # i18n-tasks-use t("decidim.api.errors.invalid_locale")
7
+ class InvalidLocaleError < GraphQL::ExecutionError
8
+ def to_h
9
+ super.merge({ "extensions" => { "code" => "INVALID_LOCALE_ERROR" } })
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Api
5
+ module Errors
6
+ # i18n-tasks-use t("decidim.api.errors.locale_argument_error")
7
+ class LocaleError < GraphQL::ExecutionError
8
+ def to_h
9
+ super.merge({ "extensions" => { "code" => "LOCALE_ERROR" } })
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Api
5
+ module Errors
6
+ # i18n-tasks-use t("decidim.api.errors.unauthorized_mutation")
7
+ class MutationNotAuthorizedError < GraphQL::ExecutionError
8
+ def to_h
9
+ super.merge({ "extensions" => { "code" => "MUTATION_NOT_AUTHORIZED_ERROR" } })
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Api
5
+ module Errors
6
+ # i18n-tasks-use t("decidim.api.errors.not_found")
7
+ class NotFoundError < GraphQL::ExecutionError
8
+ def to_h
9
+ super.merge({ "extensions" => { "code" => "NOT_FOUND_ERROR" } })
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Api
5
+ module Errors
6
+ # i18n-tasks-use t("decidim.api.errors.permission_not_set")
7
+ class PermissionNotSetError < GraphQL::ExecutionError
8
+ def to_h
9
+ super.merge({ "extensions" => { "code" => "PERMISSION_NOT_SET_ERROR" } })
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Api
5
+ module Errors
6
+ # i18n-tasks-use t("decidim.api.errors.unauthorized_field")
7
+ class UnauthorizedFieldError < GraphQL::ExecutionError
8
+ def to_h
9
+ super.merge({ "extensions" => { "code" => "UNAUTHORIZED_FIELD_ERROR" } })
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Api
5
+ module Errors
6
+ # i18n-tasks-use t("decidim.api.errors.unauthorized_object")
7
+ class UnauthorizedObjectError < GraphQL::ExecutionError
8
+ def to_h
9
+ super.merge({ "extensions" => { "code" => "UNAUTHORIZED_OBJECT_ERROR" } })
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Api
5
+ module Errors
6
+ class ValidationError < GraphQL::ExecutionError
7
+ def to_h
8
+ super.merge({ "extensions" => { "code" => "VALIDATION_ERROR" } })
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -7,7 +7,7 @@ module Decidim
7
7
  # @example Adding a header to the request
8
8
  # config.headers["My-Header"] = -> (view_context) { "My-Value" }
9
9
  #
10
- # @return [Hash<String => Proc>] Keys are headers to include in GraphQL requests, values are `->(view_context) { ... }` procs to determine values
10
+ # @return [Hash{String => Proc}] Keys are headers to include in GraphQL requests, values are `->(view_context) { ... }` procs to determine values
11
11
  attr_accessor :headers
12
12
 
13
13
  attr_accessor :query_params, :initial_query, :csrf, :title, :logo
@@ -17,8 +17,8 @@ module Decidim
17
17
  subject = determine_subject_name(object)
18
18
  context[subject] = object
19
19
 
20
- chain.unshift(allowed_to?(:read, :participatory_space, object, context)) if object.respond_to?(:participatory_space)
21
- chain.unshift(allowed_to?(:read, :component, object, context)) if object.respond_to?(:component) && object.component.present?
20
+ chain.unshift(allowed_to?(:read, :participatory_space, object.participatory_space, context)) if object.respond_to?(:participatory_space)
21
+ chain.unshift(allowed_to?(:read, :component, object.component, context)) if object.respond_to?(:component) && object.component.present?
22
22
 
23
23
  super && chain.all?
24
24
  end
@@ -34,6 +34,7 @@ module Decidim
34
34
  # @param context [GraphQL::Query::Context] The GraphQL context
35
35
  #
36
36
  # @return Boolean
37
+ # @param [Symbol] scope
37
38
  def allowed_to?(action, subject, object, context, scope: :public)
38
39
  unless subject.is_a?(::Symbol)
39
40
  subject = determine_subject_name(object)
@@ -43,12 +44,7 @@ module Decidim
43
44
  permission_action = Decidim::PermissionAction.new(scope:, action:, subject:)
44
45
 
45
46
  permission_chain(object).inject(permission_action) do |current_permission_action, permission_class|
46
- permission_context =
47
- if scope == :admin
48
- local_admin_context(object, context)
49
- else
50
- local_context(object, context)
51
- end
47
+ permission_context = local_user_context(object, context)
52
48
 
53
49
  permission_class.new(
54
50
  context[:current_user],
@@ -56,6 +52,8 @@ module Decidim
56
52
  permission_context
57
53
  ).permissions
58
54
  end.allowed?
55
+ rescue Decidim::PermissionAction::PermissionNotSetError
56
+ false
59
57
  end
60
58
 
61
59
  # Injects into context object current_participatory_space and current_component keys as they are needed
@@ -65,8 +63,13 @@ module Decidim
65
63
  #
66
64
  # @return Hash
67
65
  def local_context(object, context)
68
- context[:current_participatory_space] = object.participatory_space if object.respond_to?(:participatory_space)
69
- context[:current_component] =
66
+ context[:current_participatory_space] ||=
67
+ if object.respond_to?(:participatory_space)
68
+ object.participatory_space
69
+ elsif object.is_a?(Decidim::Participable)
70
+ object
71
+ end
72
+ context[:current_component] ||=
70
73
  if object.is_a?(Decidim::Component)
71
74
  object
72
75
  elsif object.respond_to?(:component)
@@ -76,7 +79,7 @@ module Decidim
76
79
  context.to_h
77
80
  end
78
81
 
79
- def local_admin_context(object, context)
82
+ def local_user_context(object, context)
80
83
  context = local_context(object, context)
81
84
 
82
85
  component = context[:current_component]
@@ -103,7 +106,9 @@ module Decidim
103
106
  Decidim::Permissions
104
107
  ]
105
108
 
106
- if object.is_a?(Decidim::Component)
109
+ if object.is_a?(Decidim::Participable)
110
+ permissions.unshift(object.manifest.permissions_class)
111
+ elsif object.is_a?(Decidim::Component)
107
112
  permissions.unshift(object.participatory_space.manifest.permissions_class)
108
113
  permissions.unshift(object.manifest.permissions_class)
109
114
  else
@@ -5,6 +5,97 @@ module Decidim
5
5
  # This type represents the root query type of the whole API.
6
6
  class QueryType < Decidim::Api::Types::BaseObject
7
7
  description "The root query of this schema"
8
+
9
+ field :component, Decidim::Core::ComponentInterface, null: true do
10
+ description "Lists the components this space contains."
11
+ argument :id, GraphQL::Types::ID, required: true, description: "The ID of the component to be found"
12
+ end
13
+ field :decidim, Core::DecidimType, "Decidim's framework properties.", null: true
14
+ field :moderated_users, type: [Decidim::Core::UserModerationType], null: true,
15
+ description: "The moderated users for the current organization"
16
+ field :moderations, type: [Decidim::Core::ModerationType], null: true,
17
+ description: "The moderation for the current organization"
18
+ field :organization, Core::OrganizationType, "The current organization", null: true
19
+ field :participant_details, type: Decidim::Core::ParticipantDetailsType, null: true do
20
+ description "Participant details visible to admin users only"
21
+ argument :id, GraphQL::Types::ID, "The ID of the participant", required: true
22
+ argument :nickname, GraphQL::Types::String, "The @nickname of the participant", required: false
23
+ end
24
+ field :session, Core::SessionType, description: "Return's information about the logged in user", null: true
25
+ field :static_page_topics, type: [Decidim::Core::StaticPageTopicType], null: true,
26
+ description: "The static page topics for the current organization"
27
+ field :static_pages, type: [Decidim::Core::StaticPageType], null: true,
28
+ description: "The static pages for the current organization"
29
+ field :user,
30
+ type: Core::UserType, null: true,
31
+ description: "A participant (user or group) in the current organization" do
32
+ argument :id, GraphQL::Types::ID, "The ID of the participant", required: false
33
+ argument :nickname, GraphQL::Types::String, "The @nickname of the participant", required: false
34
+ end
35
+ field :users,
36
+ type: [Core::UserType], null: true,
37
+ description: "The participants (users or groups) for the current organization" do
38
+ argument :filter, Decidim::Core::UserEntityInputFilter, "Provides several methods to filter the results", required: false
39
+ argument :order, Decidim::Core::UserEntityInputSort, "Provides several methods to order the results", required: false
40
+ end
41
+
42
+ def component(id: {})
43
+ component = Decidim::Component.published.find_by(id:)
44
+ component&.organization == context[:current_organization] ? component : nil
45
+ end
46
+
47
+ def session
48
+ context[:current_user]
49
+ end
50
+
51
+ def decidim
52
+ Decidim
53
+ end
54
+
55
+ def organization
56
+ context[:current_organization]
57
+ end
58
+
59
+ def user(id: nil, nickname: nil)
60
+ Core::UserEntityFinder.new.call(object, { id:, nickname: }, context)
61
+ end
62
+
63
+ def users(filter: {}, order: {})
64
+ Core::UserEntityList.new.call(object, { filter:, order: }, context)
65
+ end
66
+
67
+ def participant_details(id: nil, nickname: nil)
68
+ participant = Decidim::Core::UserEntityFinder.new.call(object, { id:, nickname: }, context)
69
+ return nil unless participant
70
+
71
+ return nil unless Decidim::Core::ParticipantDetailsType.authorized?(participant, context)
72
+
73
+ Decidim::ActionLogger.log(
74
+ "read",
75
+ context[:current_user],
76
+ participant,
77
+ nil,
78
+ {}
79
+ )
80
+
81
+ participant
82
+ end
83
+
84
+ def static_pages
85
+ Decidim::StaticPage.accessible_for(organization, context[:current_user])
86
+ end
87
+
88
+ def static_page_topics
89
+ static_pages.collect(&:topic).uniq.compact_blank
90
+ end
91
+
92
+ def moderated_users
93
+ Decidim::UserModeration.joins(:user).where(decidim_users: { decidim_organization_id: organization&.id }).where.not(decidim_users: { blocked_at: nil })
94
+ end
95
+
96
+ def moderations
97
+ Decidim::Moderation.where(participatory_space: organization.participatory_spaces).includes(:reports).hidden
98
+ end
8
99
  end
9
100
  end
10
101
  end
@@ -7,7 +7,7 @@ module Decidim
7
7
  mutation(MutationType)
8
8
  query(QueryType)
9
9
 
10
- introspection IntrospectionAnalyzer
10
+ introspection(IntrospectionAnalyzer)
11
11
  query_analyzer RecursionAnalyzer
12
12
  query_analyzer AliasAnalyzer
13
13
 
@@ -16,6 +16,32 @@ module Decidim
16
16
  max_complexity Decidim::Api.schema_max_complexity
17
17
 
18
18
  orphan_types(Api.orphan_types)
19
+
20
+ def self.unauthorized_object(error)
21
+ # Add a top-level error to the response instead of returning nil:
22
+ raise Decidim::Api::Errors::UnauthorizedObjectError, I18n.t("decidim.api.errors.unauthorized_object", type: error.type.graphql_name)
23
+ end
24
+
25
+ def self.unauthorized_field(error)
26
+ # Add a top-level error to the response instead of returning nil:
27
+ raise Decidim::Api::Errors::UnauthorizedFieldError, I18n.t("decidim.api.errors.unauthorized_field", type: error.type.graphql_name, field: error.field.graphql_name)
28
+ end
29
+
30
+ rescue_from(ActiveRecord::RecordNotFound) do |_err, _obj, _args, _ctx, field|
31
+ raise Decidim::Api::Errors::NotFoundError, I18n.t("decidim.api.errors.not_found", type: field.type.unwrap.graphql_name)
32
+ end
33
+
34
+ rescue_from(Decidim::PermissionAction::PermissionNotSetError) do |_err, _obj, _args, _ctx, field|
35
+ raise Decidim::Api::Errors::PermissionNotSetError, I18n.t("decidim.api.errors.permission_not_set", type: field.type.unwrap.graphql_name)
36
+ end
37
+
38
+ rescue_from(I18n::InvalidLocale) do |_err, _obj, _args, _ctx, _field|
39
+ raise Decidim::Api::Errors::InvalidLocaleError, I18n.t("decidim.api.errors.invalid_locale")
40
+ end
41
+
42
+ rescue_from(I18n::ArgumentError) do |err, _obj, _args, _ctx, _field|
43
+ raise Decidim::Api::Errors::LocaleError, I18n.t("decidim.api.errors.locale_argument_error", message: err.message)
44
+ end
19
45
  end
20
46
  end
21
47
  end