Skip to content
Snippets Groups Projects
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
No related branches found
No related tags found
No related merge requests found
Showing
with 684 additions and 3 deletions
# 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
Loading
Loading
@@ -15,6 +15,8 @@ def resolve_type(object, context)
Types::IssueType
when MergeRequest
Types::MergeRequestType
when Snippet
Types::SnippetType
else
raise "Unknown GraphQL type for #{object}"
end
Loading
Loading
Loading
Loading
@@ -10,13 +10,19 @@ class Project < BasePermissionType
: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
Loading
Loading
# 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
Loading
Loading
@@ -151,5 +151,11 @@ class ProjectType < BaseObject
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
Loading
Loading
@@ -29,6 +29,12 @@ class QueryType < ::Types::BaseObject
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
Loading
Loading
@@ -8,6 +8,8 @@ class UserType < BaseObject
 
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,
Loading
Loading
@@ -19,5 +21,11 @@ class UserType < BaseObject
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
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
@@ -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).
Loading
Loading
Loading
Loading
@@ -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
"""
Loading
Loading
@@ -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
Loading
Loading
@@ -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
}
 
"""
Loading
Loading
@@ -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!
"""
File Name of the snippet
"""
fileName: String
"""
Id of the snippet
"""
id: ID!
"""
All notes on this noteable
"""
notes(
"""
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
): NoteConnection!
"""
The project the snippet is associated with
"""
project: Project
"""
Raw URL of the snippet
"""
rawUrl: String!
"""
Title of the snippet
"""
title: String!
"""
Timestamp this snippet was updated
"""
updatedAt: Time!
"""
Permissions for the current user on the resource
"""
userPermissions: SnippetPermissions!
"""
Visibility of the snippet
"""
visibility: String!
"""
Web URL of the snippet
"""
webUrl: String!
}
"""
The connection type for Snippet.
"""
type SnippetConnection {
"""
A list of edges.
"""
edges: [SnippetEdge]
"""
A list of nodes.
"""
nodes: [Snippet]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type SnippetEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: Snippet
}
type SnippetPermissions {
"""
Whether or not a user can perform `admin_snippet` on this resource
"""
adminSnippet: Boolean!
"""
Whether or not a user can perform `award_emoji` on this resource
"""
awardEmoji: Boolean!
"""
Whether or not a user can perform `create_note` on this resource
"""
createNote: Boolean!
"""
Whether or not a user can perform `read_snippet` on this resource
"""
readSnippet: Boolean!
"""
Whether or not a user can perform `update_snippet` on this resource
"""
updateSnippet: Boolean!
}
type Submodule implements Entry {
flatPath: String!
id: ID!
Loading
Loading
@@ -5602,6 +5879,11 @@ type TreeEntryEdge {
node: TreeEntry
}
 
enum TypeEnum {
personal
project
}
"""
Autogenerated input type of UpdateEpic
"""
Loading
Loading
@@ -5740,6 +6022,46 @@ type User {
"""
name: String!
 
"""
Snippets authored by the user
"""
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 type of snippet
"""
type: TypeEnum
"""
The visibility of the snippet
"""
visibility: VisibilityScopesEnum
): SnippetConnection
"""
Todos of the user
"""
Loading
Loading
@@ -5795,6 +6117,11 @@ type User {
type: [TodoTargetEnum!]
): TodoConnection!
 
"""
Permissions for the current user on the resource
"""
userPermissions: UserPermissions!
"""
Username of the user. Unique within this instance of GitLab
"""
Loading
Loading
@@ -5839,4 +6166,17 @@ type UserEdge {
The item at the end of the edge.
"""
node: User
}
type UserPermissions {
"""
Whether or not a user can perform `create_snippet` on this resource
"""
createSnippet: Boolean!
}
enum VisibilityScopesEnum {
internal
private
public
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment