summaryrefslogtreecommitdiff
path: root/app/graphql
diff options
context:
space:
mode:
authorNick Thomas <nick@gitlab.com>2017-08-16 14:04:41 +0100
committerBob Van Landuyt <bob@vanlanduyt.co>2018-06-05 20:47:42 +0200
commit9c6c17cbcdb8bf8185fc1b873dcfd08f723e4df5 (patch)
tree624dba30e87ed0ea39afa0535d92c37c7718daef /app/graphql
parent67dc43db2f30095cce7fe01d7f475d084be936e8 (diff)
downloadgitlab-ce-9c6c17cbcdb8bf8185fc1b873dcfd08f723e4df5.tar.gz
Add a minimal GraphQL API
Diffstat (limited to 'app/graphql')
-rw-r--r--app/graphql/gitlab_schema.rb11
-rw-r--r--app/graphql/loaders/base_loader.rb24
-rw-r--r--app/graphql/loaders/full_path_loader.rb23
-rw-r--r--app/graphql/loaders/iid_loader.rb35
-rw-r--r--app/graphql/mutations/.keep0
-rw-r--r--app/graphql/types/merge_request_type.rb50
-rw-r--r--app/graphql/types/mutation_type.rb5
-rw-r--r--app/graphql/types/project_type.rb62
-rw-r--r--app/graphql/types/query_type.rb38
-rw-r--r--app/graphql/types/time_type.rb8
10 files changed, 256 insertions, 0 deletions
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
new file mode 100644
index 00000000000..7392bf6f503
--- /dev/null
+++ b/app/graphql/gitlab_schema.rb
@@ -0,0 +1,11 @@
+Gitlab::Graphql::Authorize.register!
+
+GitlabSchema = GraphQL::Schema.define do
+ use GraphQL::Batch
+
+ enable_preloading
+ enable_authorization
+
+ mutation(Types::MutationType)
+ query(Types::QueryType)
+end
diff --git a/app/graphql/loaders/base_loader.rb b/app/graphql/loaders/base_loader.rb
new file mode 100644
index 00000000000..c32c4daa91a
--- /dev/null
+++ b/app/graphql/loaders/base_loader.rb
@@ -0,0 +1,24 @@
+# Helper methods for all loaders
+class Loaders::BaseLoader < GraphQL::Batch::Loader
+ # Convert a class method into a resolver proc. The method should follow the
+ # (obj, args, ctx) calling convention
+ class << self
+ def [](sym)
+ resolver = method(sym)
+ raise ArgumentError.new("#{self}.#{sym} is not a resolver") unless resolver.arity == 3
+
+ resolver
+ end
+ end
+
+ # Fulfill all keys. Pass a block that converts each result into a key.
+ # Any keys not in results will be fulfilled with nil.
+ def fulfill_all(results, keys, &key_blk)
+ results.each do |result|
+ key = yield result
+ fulfill(key, result)
+ end
+
+ keys.each { |key| fulfill(key, nil) unless fulfilled?(key) }
+ end
+end
diff --git a/app/graphql/loaders/full_path_loader.rb b/app/graphql/loaders/full_path_loader.rb
new file mode 100644
index 00000000000..f99b487ce5d
--- /dev/null
+++ b/app/graphql/loaders/full_path_loader.rb
@@ -0,0 +1,23 @@
+class Loaders::FullPathLoader < Loaders::BaseLoader
+ class << self
+ def project(obj, args, ctx)
+ project_by_full_path(args[:full_path])
+ end
+
+ def project_by_full_path(full_path)
+ self.for(Project).load(full_path)
+ end
+ end
+
+ attr_reader :model
+
+ def initialize(model)
+ @model = model
+ end
+
+ def perform(keys)
+ # `with_route` prevents relation.all.map(&:full_path)` from being N+1
+ relation = model.where_full_path_in(keys).with_route
+ fulfill_all(relation, keys, &:full_path)
+ end
+end
diff --git a/app/graphql/loaders/iid_loader.rb b/app/graphql/loaders/iid_loader.rb
new file mode 100644
index 00000000000..e89031da0c2
--- /dev/null
+++ b/app/graphql/loaders/iid_loader.rb
@@ -0,0 +1,35 @@
+class Loaders::IidLoader < Loaders::BaseLoader
+ class << self
+ def merge_request(obj, args, ctx)
+ iid = args[:iid]
+ promise = Loaders::FullPathLoader.project_by_full_path(args[:project])
+
+ promise.then do |project|
+ if project
+ merge_request_by_project_and_iid(project.id, iid)
+ else
+ nil
+ end
+ end
+ end
+
+ def merge_request_by_project_and_iid(project_id, iid)
+ self.for(MergeRequest, target_project_id: project_id.to_s).load(iid.to_s)
+ end
+ end
+
+ attr_reader :model, :restrictions
+
+ def initialize(model, restrictions = {})
+ @model = model
+ @restrictions = restrictions
+ end
+
+ def perform(keys)
+ relation = model.where(iid: keys)
+ relation = relation.where(restrictions) if restrictions.present?
+
+ # IIDs are represented as the GraphQL `id` type, which is a string
+ fulfill_all(relation, keys) { |instance| instance.iid.to_s }
+ end
+end
diff --git a/app/graphql/mutations/.keep b/app/graphql/mutations/.keep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/app/graphql/mutations/.keep
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
new file mode 100644
index 00000000000..9b12f6f2bf3
--- /dev/null
+++ b/app/graphql/types/merge_request_type.rb
@@ -0,0 +1,50 @@
+Types::MergeRequestType = GraphQL::ObjectType.define do
+ name 'MergeRequest'
+
+ field :id, !types.ID
+ field :iid, !types.ID
+ field :title, types.String
+ field :description, types.String
+ field :state, types.String
+
+ field :created_at, Types::TimeType
+ field :updated_at, Types::TimeType
+
+ field :source_project, -> { Types::ProjectType }
+ field :target_project, -> { Types::ProjectType }
+
+ # Alias for target_project
+ field :project, -> { Types::ProjectType }
+
+ field :source_project_id, types.Int
+ field :target_project_id, types.Int
+ field :project_id, types.Int
+
+ field :source_branch, types.String
+ field :target_branch, types.String
+
+ field :work_in_progress, types.Boolean, property: :work_in_progress?
+ field :merge_when_pipeline_succeeds, types.Boolean
+
+ field :sha, types.String, property: :diff_head_sha
+ field :merge_commit_sha, types.String
+
+ field :user_notes_count, types.Int
+ field :should_remove_source_branch, types.Boolean, property: :should_remove_source_branch?
+ field :force_remove_source_branch, types.Boolean, property: :force_remove_source_branch?
+
+ field :merge_status, types.String
+
+ field :web_url, types.String do
+ resolve ->(merge_request, args, ctx) { Gitlab::UrlBuilder.build(merge_request) }
+ end
+
+ field :upvotes, types.Int
+ field :downvotes, types.Int
+
+ field :subscribed, types.Boolean do
+ resolve ->(merge_request, args, ctx) do
+ merge_request.subscribed?(ctx[:current_user], merge_request.target_project)
+ end
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
new file mode 100644
index 00000000000..c5061f10239
--- /dev/null
+++ b/app/graphql/types/mutation_type.rb
@@ -0,0 +1,5 @@
+Types::MutationType = GraphQL::ObjectType.define do
+ name "Mutation"
+
+ # TODO: Add Mutations as fields
+end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
new file mode 100644
index 00000000000..bfefc594896
--- /dev/null
+++ b/app/graphql/types/project_type.rb
@@ -0,0 +1,62 @@
+Types::ProjectType = GraphQL::ObjectType.define do
+ name 'Project'
+
+ field :id, !types.ID
+
+ field :full_path, !types.ID
+ field :path, !types.String
+
+ field :name_with_namespace, !types.String
+ field :name, !types.String
+
+ field :description, types.String
+
+ field :default_branch, types.String
+ field :tag_list, types.String
+
+ field :ssh_url_to_repo, types.String
+ field :http_url_to_repo, types.String
+ field :web_url, types.String
+
+ field :star_count, !types.Int
+ field :forks_count, !types.Int
+
+ field :created_at, Types::TimeType
+ field :last_activity_at, Types::TimeType
+
+ field :archived, types.Boolean
+
+ field :visibility, types.String
+
+ field :container_registry_enabled, types.Boolean
+ field :shared_runners_enabled, types.Boolean
+ field :lfs_enabled, types.Boolean
+
+ field :avatar_url, types.String do
+ resolve ->(project, args, ctx) { project.avatar_url(only_path: false) }
+ end
+
+ %i[issues merge_requests wiki snippets].each do |feature|
+ field "#{feature}_enabled", types.Boolean do
+ resolve ->(project, args, ctx) { project.feature_available?(feature, ctx[:current_user]) }
+ end
+ end
+
+ field :jobs_enabled, types.Boolean do
+ resolve ->(project, args, ctx) { project.feature_available?(:builds, ctx[:current_user]) }
+ end
+
+ field :public_jobs, types.Boolean, property: :public_builds
+
+ field :open_issues_count, types.Int do
+ resolve ->(project, args, ctx) { project.open_issues_count if project.feature_available?(:issues, ctx[:current_user]) }
+ end
+
+ field :import_status, types.String
+ field :ci_config_path, types.String
+
+ field :only_allow_merge_if_pipeline_succeeds, types.Boolean
+ field :request_access_enabled, types.Boolean
+ field :only_allow_merge_if_all_discussions_are_resolved, types.Boolean
+ field :printing_merge_request_link_enabled, types.Boolean
+end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
new file mode 100644
index 00000000000..029bbd098ad
--- /dev/null
+++ b/app/graphql/types/query_type.rb
@@ -0,0 +1,38 @@
+Types::QueryType = GraphQL::ObjectType.define do
+ name 'Query'
+
+ field :project, Types::ProjectType do
+ argument :full_path, !types.ID do
+ description 'The full path of the project, e.g., "gitlab-org/gitlab-ce"'
+ end
+
+ authorize :read_project
+
+ resolve Loaders::FullPathLoader[:project]
+ end
+
+ field :merge_request, Types::MergeRequestType do
+ argument :project, !types.ID do
+ description 'The full path of the target project, e.g., "gitlab-org/gitlab-ce"'
+ end
+
+ argument :iid, !types.ID do
+ description 'The IID of the merge request, e.g., "1"'
+ end
+
+ authorize :read_merge_request
+
+ resolve Loaders::IidLoader[:merge_request]
+ end
+
+ # Testing endpoint to validate the API with
+ field :echo, types.String do
+ argument :text, types.String
+
+ resolve -> (obj, args, ctx) do
+ username = ctx[:current_user]&.username
+
+ "#{username.inspect} says: #{args[:text]}"
+ end
+ end
+end
diff --git a/app/graphql/types/time_type.rb b/app/graphql/types/time_type.rb
new file mode 100644
index 00000000000..fb717eb3dc7
--- /dev/null
+++ b/app/graphql/types/time_type.rb
@@ -0,0 +1,8 @@
+# Taken from http://www.rubydoc.info/github/rmosolgo/graphql-ruby/GraphQL/ScalarType
+Types::TimeType = GraphQL::ScalarType.define do
+ name 'Time'
+ description 'Time since epoch in fractional seconds'
+
+ coerce_input ->(value, ctx) { Time.at(Float(value)) }
+ coerce_result ->(value, ctx) { value.to_f }
+end