Unverified Commit a2358aaf authored by Francisco Javier López's avatar Francisco Javier López
Browse files

Add Snippet GraphQL resolver API

Added resolvers for project and user snippets.
parent 6c1f03fe
# frozen_string_literal: true
module ResolvesSnippets
extend ActiveSupport::Concern
included do
type Types::SnippetType, null: false
argument :ids, [GraphQL::ID_TYPE],
required: false,
description: 'Array of global snippet ids, e.g., "gid://gitlab/ProjectSnippet/1"'
argument :visibility, Types::Snippets::VisibilityScopesEnum,
required: false,
description: 'The visibility of the snippet'
end
def resolve(**args)
resolve_snippets(args)
end
private
def resolve_snippets(args)
SnippetsFinder.new(context[:current_user], snippet_finder_params(args)).execute
end
def snippet_finder_params(args)
{
ids: resolve_ids(args[:ids]),
scope: args[:visibility]
}.merge(options_by_type(args[:type]))
end
def resolve_ids(ids)
Array.wrap(ids).map { |id| resolve_gid(id, :id) }
end
def resolve_gid(gid, argument)
return unless gid.present?
GlobalID.parse(gid)&.model_id.tap do |id|
raise Gitlab::Graphql::Errors::ArgumentError, "Invalid global id format for param #{argument}" if id.nil?
end
end
def options_by_type(type)
case type
when 'personal'
{ only_personal: true }
when 'project'
{ only_project: true }
else
{}
end
end
end
# frozen_string_literal: true
module Resolvers
module Projects
class SnippetsResolver < BaseResolver
include ResolvesSnippets
alias_method :project, :object
def resolve(**args)
return Snippet.none if project.nil?
super
end
private
def snippet_finder_params(args)
super.merge(project: project)
end
end
end
end
# frozen_string_literal: true
module Resolvers
class SnippetsResolver < BaseResolver
include ResolvesSnippets
ERROR_MESSAGE = 'Filtering by both an author and a project is not supported'
alias_method :user, :object
argument :author_id, GraphQL::ID_TYPE,
required: false,
description: 'The ID of an author'
argument :project_id, GraphQL::ID_TYPE,
required: false,
description: 'The ID of a project'
argument :type, Types::Snippets::TypeEnum,
required: false,
description: 'The type of snippet'
argument :explore,
GraphQL::BOOLEAN_TYPE,
required: false,
description: 'Explore personal snippets'
def resolve(**args)
if args[:author_id].present? && args[:project_id].present?
raise Gitlab::Graphql::Errors::ArgumentError, ERROR_MESSAGE
end
super
end
private
def snippet_finder_params(args)
super
.merge(author: resolve_gid(args[:author_id], :author),
project: resolve_gid(args[:project_id], :project),
explore: args[:explore])
end
end
end
# frozen_string_literal: true
module Resolvers
module Users
class SnippetsResolver < BaseResolver
include ResolvesSnippets
alias_method :user, :object
argument :type, Types::Snippets::TypeEnum,
required: false,
description: 'The type of snippet'
private
def snippet_finder_params(args)
super.merge(author: user)
end
end
end
end
......@@ -15,6 +15,8 @@ module Types
Types::IssueType
when MergeRequest
Types::MergeRequestType
when Snippet
Types::SnippetType
else
raise "Unknown GraphQL type for #{object}"
end
......
......@@ -10,13 +10,19 @@ module Types
:remove_pages, :read_project, :create_merge_request_in,
:read_wiki, :read_project_member, :create_issue, :upload_file,
:read_cycle_analytics, :download_code, :download_wiki_code,
:fork_project, :create_project_snippet, :read_commit_status,
:fork_project, :read_commit_status,
:request_access, :create_pipeline, :create_pipeline_schedule,
:create_merge_request_from, :create_wiki, :push_code,
:create_deployment, :push_to_delete_protected_branch,
:admin_wiki, :admin_project, :update_pages,
:admin_remote_mirror, :create_label, :update_wiki, :destroy_wiki,
:create_pages, :destroy_pages, :read_pages_content, :admin_operations
permission_field :create_snippet
def create_snippet
Ability.allowed?(context[:current_user], :create_project_snippet, object)
end
end
end
end
......
# frozen_string_literal: true
module Types
module PermissionTypes
class Snippet < BasePermissionType
graphql_name 'SnippetPermissions'
abilities :create_note, :award_emoji
permission_field :read_snippet, method: :can_read_snippet?
permission_field :update_snippet, method: :can_update_snippet?
permission_field :admin_snippet, method: :can_admin_snippet?
end
end
end
# frozen_string_literal: true
module Types
module PermissionTypes
class User < BasePermissionType
graphql_name 'UserPermissions'
permission_field :create_snippet
def create_snippet
Ability.allowed?(context[:current_user], :create_personal_snippet)
end
end
end
end
......@@ -151,5 +151,11 @@ module Types
null: true,
description: 'Detailed version of a Sentry error on the project',
resolver: Resolvers::ErrorTracking::SentryDetailedErrorResolver
field :snippets,
Types::SnippetType.connection_type,
null: true,
description: 'Snippets of the project',
resolver: Resolvers::Projects::SnippetsResolver
end
end
......@@ -29,6 +29,12 @@ module Types
resolver: Resolvers::MetadataResolver,
description: 'Metadata about GitLab'
 
field :snippets,
Types::SnippetType.connection_type,
null: true,
resolver: Resolvers::SnippetsResolver,
description: 'Find Snippets visible to the current user'
field :echo, GraphQL::STRING_TYPE, null: false, resolver: Resolvers::EchoResolver # rubocop:disable Graphql/Descriptions
end
end
# frozen_string_literal: true
module Types
class SnippetType < BaseObject
graphql_name 'Snippet'
description 'Represents a snippet entry'
implements(Types::Notes::NoteableType)
present_using SnippetPresenter
authorize :read_snippet
expose_permissions Types::PermissionTypes::Snippet
field :id, GraphQL::ID_TYPE,
description: 'Id of the snippet',
null: false
field :title, GraphQL::STRING_TYPE,
description: 'Title of the snippet',
null: false
field :project, Types::ProjectType,
description: 'The project the snippet is associated with',
null: true,
authorize: :read_project,
resolve: -> (snippet, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, snippet.project_id).find }
field :author, Types::UserType,
description: 'The owner of the snippet',
null: false,
resolve: -> (snippet, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, snippet.author_id).find }
field :file_name, GraphQL::STRING_TYPE,
description: 'File Name of the snippet',
null: true
field :content, GraphQL::STRING_TYPE,
description: 'Content of the snippet',
null: false
field :description, GraphQL::STRING_TYPE,
description: 'Description of the snippet',
null: true
field :visibility, GraphQL::STRING_TYPE,
description: 'Visibility of the snippet',
null: false
field :created_at, Types::TimeType,
description: 'Timestamp this snippet was created',
null: false
field :updated_at, Types::TimeType,
description: 'Timestamp this snippet was updated',
null: false
field :web_url, type: GraphQL::STRING_TYPE,
description: 'Web URL of the snippet',
null: false
field :raw_url, type: GraphQL::STRING_TYPE,
description: 'Raw URL of the snippet',
null: false
markdown_field :description_html, null: true, method: :description
end
end
# frozen_string_literal: true
module Types
module Snippets
class TypeEnum < BaseEnum
value 'personal', value: 'personal'
value 'project', value: 'project'
end
end
end
# frozen_string_literal: true
module Types
module Snippets
class VisibilityScopesEnum < BaseEnum
value 'private', value: 'are_private'
value 'internal', value: 'are_internal'
value 'public', value: 'are_public'
end
end
end
......@@ -8,6 +8,8 @@ module Types
 
present_using UserPresenter
 
expose_permissions Types::PermissionTypes::User
field :name, GraphQL::STRING_TYPE, null: false,
description: 'Human-readable name of the user'
field :username, GraphQL::STRING_TYPE, null: false,
......@@ -19,5 +21,11 @@ module Types
field :todos, Types::TodoType.connection_type, null: false,
resolver: Resolvers::TodoResolver,
description: 'Todos of the user'
field :snippets,
Types::SnippetType.connection_type,
null: true,
description: 'Snippets authored by the user',
resolver: Resolvers::Users::SnippetsResolver
end
end
......@@ -27,4 +27,7 @@ class PersonalSnippetPolicy < BasePolicy
rule { can?(:create_note) }.enable :award_emoji
 
rule { can?(:read_all_resources) }.enable :read_personal_snippet
# Aliasing the ability to ease GraphQL permissions check
rule { can?(:read_personal_snippet) }.enable :read_snippet
end
......@@ -45,6 +45,9 @@ class ProjectSnippetPolicy < BasePolicy
end
 
rule { ~can?(:read_project_snippet) }.prevent :create_note
# Aliasing the ability to ease GraphQL permissions check
rule { can?(:read_project_snippet) }.enable :read_snippet
end
 
ProjectSnippetPolicy.prepend_if_ee('EE::ProjectSnippetPolicy')
# frozen_string_literal: true
class SnippetPresenter < Gitlab::View::Presenter::Delegated
presents :snippet
def web_url
Gitlab::UrlBuilder.build(snippet)
end
def raw_url
Gitlab::UrlBuilder.build(snippet, raw: true)
end
def can_read_snippet?
can_access_resource?("read")
end
def can_update_snippet?
can_access_resource?("update")
end
def can_admin_snippet?
can_access_resource?("admin")
end
private
def can_access_resource?(ability_prefix)
can?(current_user, ability_name(ability_prefix), snippet)
end
def ability_name(ability_prefix)
"#{ability_prefix}_#{snippet.class.underscore}".to_sym
end
end
---
title: Add Snippet GraphQL resolver endpoints
merge_request: 20613
author:
type: added
......@@ -61,6 +61,7 @@ The GraphQL API includes the following queries at the root level:
1. `namespace` : Within a namespace it is also possible to fetch `projects`.
1. `currentUser`: Information about the currently logged in user.
1. `metaData`: Metadata about GitLab and the GraphQL API.
1. `snippets`: Snippets visible to the currently logged in user.
 
Root-level queries are defined in
[`app/graphql/types/query_type.rb`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/graphql/types/query_type.rb).
......
......@@ -4512,6 +4512,41 @@ type Project {
"""
sharedRunnersEnabled: Boolean
 
"""
Snippets of the project
"""
snippets(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Array of global snippet ids, e.g., "gid://gitlab/ProjectSnippet/1"
"""
ids: [ID!]
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
The visibility of the snippet
"""
visibility: VisibilityScopesEnum
): SnippetConnection
"""
(deprecated) Does this project have snippets enabled?. Use `snippets_access_level` instead
"""
......@@ -4675,9 +4710,9 @@ type ProjectPermissions {
createPipelineSchedule: Boolean!
 
"""
Whether or not a user can perform `create_project_snippet` on this resource
Whether or not a user can perform `create_snippet` on this resource
"""
createProjectSnippet: Boolean!
createSnippet: Boolean!
 
"""
Whether or not a user can perform `create_wiki` on this resource
......@@ -4882,6 +4917,61 @@ type Query {
"""
fullPath: ID!
): Project
"""
Find Snippets visible to the current user
"""
snippets(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
The ID of an author
"""
authorId: ID
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Explore personal snippets
"""
explore: Boolean
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Array of global snippet ids, e.g., "gid://gitlab/ProjectSnippet/1"
"""
ids: [ID!]
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
The ID of a project
"""
projectId: ID
"""
The type of snippet
"""
type: TypeEnum
"""
The visibility of the snippet
"""
visibility: VisibilityScopesEnum
): SnippetConnection
}
 
"""
......@@ -5137,6 +5227,193 @@ enum SentryErrorStatus {
UNRESOLVED
}
 
"""
Represents a snippet entry
"""
type Snippet implements Noteable {
"""
The owner of the snippet
"""
author: User!
"""
Content of the snippet
"""
content: String!
"""
Timestamp this snippet was created
"""
createdAt: Time!
"""
Description of the snippet
"""
description: String
"""
The GitLab Flavored Markdown rendering of `description`
"""
descriptionHtml: String
"""
All discussions on this noteable
"""
discussions(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): DiscussionConnection!
"""