Commit 02d5e9a3 authored by Nathan Friend's avatar Nathan Friend

Add Release assets to GraphQL endpoint

This commit adds Release asset data to the GraphQL endpoint.
parent a57593b7
# frozen_string_literal: true
module Types
class ReleaseAssetsType < BaseObject
graphql_name 'ReleaseAssets'
authorize :read_release
alias_method :release, :object
present_using ReleasePresenter
field :assets_count, GraphQL::INT_TYPE, null: true,
description: 'Number of assets of the release'
field :links, Types::ReleaseLinkType.connection_type, null: true,
description: 'Asset links of the release'
field :sources, Types::ReleaseSourceType.connection_type, null: true,
description: 'Sources of the release'
end
end
# frozen_string_literal: true
module Types
class ReleaseLinkType < BaseObject
graphql_name 'ReleaseLink'
authorize :read_release
field :id, GraphQL::ID_TYPE, null: false,
description: 'ID of the link'
field :name, GraphQL::STRING_TYPE, null: true,
description: 'Name of the link'
field :url, GraphQL::STRING_TYPE, null: true,
description: 'URL of the link'
field :external, GraphQL::BOOLEAN_TYPE, null: true, method: :external?,
description: 'Indicates the link points to an external resource'
end
end
# frozen_string_literal: true
module Types
class ReleaseSourceType < BaseObject
graphql_name 'ReleaseSource'
authorize :read_release_sources
field :format, GraphQL::STRING_TYPE, null: true,
description: 'Format of the source'
field :url, GraphQL::STRING_TYPE, null: true,
description: 'Download URL of the source'
end
end
...@@ -23,6 +23,8 @@ module Types ...@@ -23,6 +23,8 @@ module Types
description: 'Timestamp of when the release was created' description: 'Timestamp of when the release was created'
field :released_at, Types::TimeType, null: true, field :released_at, Types::TimeType, null: true,
description: 'Timestamp of when the release was released' description: 'Timestamp of when the release was released'
field :assets, Types::ReleaseAssetsType, null: true, method: :itself,
description: 'Assets of the release'
field :milestones, Types::MilestoneType.connection_type, null: true, field :milestones, Types::MilestoneType.connection_type, null: true,
description: 'Milestones associated to the release' description: 'Milestones associated to the release'
......
# frozen_string_literal: true
module Releases
class LinkPolicy < BasePolicy
delegate { @subject.release.project }
end
end
# frozen_string_literal: true
module Releases
class SourcePolicy < BasePolicy
delegate { @subject.project }
rule { can?(:public_access) | can?(:reporter_access) }.policy do
enable :read_release_sources
end
rule { ~can?(:read_release) }.prevent :read_release_sources
end
end
...@@ -5,7 +5,7 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated ...@@ -5,7 +5,7 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
presents :release presents :release
delegate :project, :tag, to: :release delegate :project, :tag, :assets_count, to: :release
def commit_path def commit_path
return unless release.commit && can_download_code? return unless release.commit && can_download_code?
......
...@@ -9022,6 +9022,11 @@ enum RegistryState { ...@@ -9022,6 +9022,11 @@ enum RegistryState {
} }
type Release { type Release {
"""
Assets of the release
"""
assets: ReleaseAssets
""" """
User that created the release User that created the release
""" """
...@@ -9093,6 +9098,63 @@ type Release { ...@@ -9093,6 +9098,63 @@ type Release {
tagPath: String tagPath: String
} }
type ReleaseAssets {
"""
Number of assets of the release
"""
assetsCount: Int
"""
Asset links of the release
"""
links(
"""
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
): ReleaseLinkConnection
"""
Sources of the release
"""
sources(
"""
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
): ReleaseSourceConnection
}
""" """
The connection type for Release. The connection type for Release.
""" """
...@@ -9128,6 +9190,110 @@ type ReleaseEdge { ...@@ -9128,6 +9190,110 @@ type ReleaseEdge {
node: Release node: Release
} }
type ReleaseLink {
"""
Indicates the link points to an external resource
"""
external: Boolean
"""
ID of the link
"""
id: ID!
"""
Name of the link
"""
name: String
"""
URL of the link
"""
url: String
}
"""
The connection type for ReleaseLink.
"""
type ReleaseLinkConnection {
"""
A list of edges.
"""
edges: [ReleaseLinkEdge]
"""
A list of nodes.
"""
nodes: [ReleaseLink]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type ReleaseLinkEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: ReleaseLink
}
type ReleaseSource {
"""
Format of the source
"""
format: String
"""
Download URL of the source
"""
url: String
}
"""
The connection type for ReleaseSource.
"""
type ReleaseSourceConnection {
"""
A list of edges.
"""
edges: [ReleaseSourceEdge]
"""
A list of nodes.
"""
nodes: [ReleaseSource]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type ReleaseSourceEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: ReleaseSource
}
""" """
Autogenerated input type of RemoveAwardEmoji Autogenerated input type of RemoveAwardEmoji
""" """
......
...@@ -26504,6 +26504,20 @@ ...@@ -26504,6 +26504,20 @@
"name": "Release", "name": "Release",
"description": null, "description": null,
"fields": [ "fields": [
{
"name": "assets",
"description": "Assets of the release",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "ReleaseAssets",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "author", "name": "author",
"description": "User that created the release", "description": "User that created the release",
...@@ -26695,6 +26709,139 @@ ...@@ -26695,6 +26709,139 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "ReleaseAssets",
"description": null,
"fields": [
{
"name": "assetsCount",
"description": "Number of assets of the release",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "links",
"description": "Asset links of the release",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "ReleaseLinkConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "sources",
"description": "Sources of the release",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "ReleaseSourceConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "ReleaseConnection", "name": "ReleaseConnection",
...@@ -26807,6 +26954,344 @@ ...@@ -26807,6 +26954,344 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "ReleaseLink",
"description": null,
"fields": [
{
"name": "external",
"description": "Indicates the link points to an external resource",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "ID of the link",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "Name of the link",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "url",
"description": "URL of the link",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ReleaseLinkConnection",
"description": "The connection type for ReleaseLink.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "ReleaseLinkEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "ReleaseLink",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ReleaseLinkEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "ReleaseLink",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ReleaseSource",
"description": null,
"fields": [
{
"name": "format",
"description": "Format of the source",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "url",
"description": "Download URL of the source",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ReleaseSourceConnection",
"description": "The connection type for ReleaseSource.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "ReleaseSourceEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "ReleaseSource",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ReleaseSourceEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "ReleaseSource",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "INPUT_OBJECT", "kind": "INPUT_OBJECT",
"name": "RemoveAwardEmojiInput", "name": "RemoveAwardEmojiInput",
...@@ -1263,6 +1263,7 @@ Information about pagination in a connection. ...@@ -1263,6 +1263,7 @@ Information about pagination in a connection.
| Name | Type | Description | | Name | Type | Description |
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `assets` | ReleaseAssets | Assets of the release |
| `author` | User | User that created the release | | `author` | User | User that created the release |
| `commit` | Commit | The commit associated with the release | | `commit` | Commit | The commit associated with the release |
| `createdAt` | Time | Timestamp of when the release was created | | `createdAt` | Time | Timestamp of when the release was created |
...@@ -1273,6 +1274,28 @@ Information about pagination in a connection. ...@@ -1273,6 +1274,28 @@ Information about pagination in a connection.
| `tagName` | String! | Name of the tag associated with the release | | `tagName` | String! | Name of the tag associated with the release |
| `tagPath` | String | Relative web path to the tag associated with the release | | `tagPath` | String | Relative web path to the tag associated with the release |
## ReleaseAssets
| Name | Type | Description |
| --- | ---- | ---------- |
| `assetsCount` | Int | Number of assets of the release |
## ReleaseLink
| Name | Type | Description |
| --- | ---- | ---------- |
| `external` | Boolean | Indicates the link points to an external resource |
| `id` | ID! | ID of the link |
| `name` | String | Name of the link |
| `url` | String | URL of the link |
## ReleaseSource
| Name | Type | Description |
| --- | ---- | ---------- |
| `format` | String | Format of the source |
| `url` | String | Download URL of the source |
## RemoveAwardEmojiPayload ## RemoveAwardEmojiPayload
Autogenerated return type of RemoveAwardEmoji Autogenerated return type of RemoveAwardEmoji
......
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['ReleaseAssets'] do
it { expect(described_class).to require_graphql_authorizations(:read_release) }
it 'has the expected fields' do
expected_fields = %w[
assets_count links sources
]
expect(described_class).to include_graphql_fields(*expected_fields)
end
describe 'links field' do
subject { described_class.fields['links'] }
it { is_expected.to have_graphql_type(Types::ReleaseLinkType.connection_type) }
end
describe 'sources field' do
subject { described_class.fields['sources'] }
it { is_expected.to have_graphql_type(Types::ReleaseSourceType.connection_type) }
end
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['ReleaseLink'] do
it { expect(described_class).to require_graphql_authorizations(:read_release) }
it 'has the expected fields' do
expected_fields = %w[
id name url external
]
expect(described_class).to include_graphql_fields(*expected_fields)
end
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['ReleaseSource'] do
it { expect(described_class).to require_graphql_authorizations(:read_release_sources) }
it 'has the expected fields' do
expected_fields = %w[
format url
]
expect(described_class).to include_graphql_fields(*expected_fields)
end
end
...@@ -9,13 +9,19 @@ describe GitlabSchema.types['Release'] do ...@@ -9,13 +9,19 @@ describe GitlabSchema.types['Release'] do
expected_fields = %w[ expected_fields = %w[
tag_name tag_path tag_name tag_path
description description_html description description_html
name milestones author commit name assets milestones author commit
created_at released_at created_at released_at
] ]
expect(described_class).to include_graphql_fields(*expected_fields) expect(described_class).to include_graphql_fields(*expected_fields)
end end
describe 'assets field' do
subject { described_class.fields['assets'] }
it { is_expected.to have_graphql_type(Types::ReleaseAssetsType) }
end
describe 'milestones field' do describe 'milestones field' do
subject { described_class.fields['milestones'] } subject { described_class.fields['milestones'] }
......
# frozen_string_literal: true
require 'spec_helper'
describe Releases::SourcePolicy do
using RSpec::Parameterized::TableSyntax
let(:policy) { described_class.new(user, source) }
let_it_be(:public_user) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:reporter) { create(:user) }
let(:release) { create(:release, project: project) }
let(:source) { release.sources.first }
shared_examples 'source code access' do
it "allows access a release's source code" do
expect(policy).to be_allowed(:read_release_sources)
end
end
shared_examples 'no source code access' do
it "does not allow access a release's source code" do
expect(policy).to be_disallowed(:read_release_sources)
end
end
context 'a private project' do
let_it_be(:project) { create(:project, :private) }
context 'accessed by a public user' do
let(:user) { public_user }
it_behaves_like 'no source code access'
end
context 'accessed by a user with Guest permissions' do
let(:user) { guest }
before do
project.add_guest(user)
end
it_behaves_like 'no source code access'
end
context 'accessed by a user with Reporter permissions' do
let(:user) { reporter }
before do
project.add_reporter(user)
end
it_behaves_like 'source code access'
end
end
context 'a public project' do
let_it_be(:project) { create(:project, :public) }
context 'accessed by a public user' do
let(:user) { public_user }
it_behaves_like 'source code access'
end
context 'accessed by a user with Guest permissions' do
let(:user) { guest }
before do
project.add_guest(user)
end
it_behaves_like 'source code access'
end
context 'accessed by a user with Reporter permissions' do
let(:user) { reporter }
before do
project.add_reporter(user)
end
it_behaves_like 'source code access'
end
end
end
# frozen_string_literal: true
require 'spec_helper'
require 'pp'
describe 'Query.project(fullPath).release(tagName)' do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:milestone_1) { create(:milestone, project: project) }
let_it_be(:milestone_2) { create(:milestone, project: project) }
let_it_be(:release) { create(:release, project: project, milestones: [milestone_1, milestone_2]) }
let_it_be(:release_link_1) { create(:release_link, release: release) }
let_it_be(:release_link_2) { create(:release_link, release: release) }
let_it_be(:developer) { create(:user) }
let(:current_user) { developer }
def query(rq = release_fields)
graphql_query_for(:project, { fullPath: project.full_path },
query_graphql_field(:release, { tagName: release.tag }, rq))
end
let(:post_query) { post_graphql(query, current_user: current_user) }
let(:path_prefix) { %w[project release] }
let(:data) { graphql_data.dig(*path) }
before do
project.add_developer(developer)
end
describe 'scalar fields' do
let(:path) { path_prefix }
let(:release_fields) do
query_graphql_field(%{
tagName
tagPath
description
descriptionHtml
name
createdAt
releasedAt
})
end
before do
post_query
end
it 'finds all release data' do
expect(data).to eq({
'tagName' => release.tag,
'tagPath' => project_tag_path(project, release.tag),
'description' => release.description,
'descriptionHtml' => release.description_html,
'name' => release.name,
'createdAt' => release.created_at.iso8601,
'releasedAt' => release.released_at.iso8601
})
end
end
describe 'milestones' do
let(:path) { path_prefix + %w[milestones nodes] }
let(:release_fields) do
query_graphql_field(:milestones, nil, 'nodes { id title }')
end
it 'finds all milestones associated to a release' do
post_query
expected = release.milestones.map do |milestone|
{ 'id' => global_id_of(milestone), 'title' => milestone.title }
end
expect(data).to match_array(expected)
end
end
describe 'author' do
let(:path) { path_prefix + %w[author] }
let(:release_fields) do
query_graphql_field(:author, nil, 'id username')
end
it 'finds the author of the release' do
post_query
expect(data).to eq({
'id' => global_id_of(release.author),
'username' => release.author.username
})
end
end
describe 'commit' do
let(:path) { path_prefix + %w[commit] }
let(:release_fields) do
query_graphql_field(:commit, nil, 'sha')
end
it 'finds the commit associated with the release' do
post_query
expect(data).to eq({ 'sha' => release.commit.sha })
end
end
describe 'assets' do
describe 'assetsCount' do
let(:path) { path_prefix + %w[assets] }
let(:release_fields) do
query_graphql_field(:assets, nil, 'assetsCount')
end
it 'returns the number of assets associated to the release' do
post_query
expect(data).to eq({ 'assetsCount' => release.sources.size + release.links.size })
end
end
describe 'links' do
let(:path) { path_prefix + %w[assets links nodes] }
let(:release_fields) do
query_graphql_field(:assets, nil,
query_graphql_field(:links, nil, 'nodes { id name url external }'))
end
it 'finds all release links' do
post_query
expected = release.links.map do |link|
{
'id' => global_id_of(link),
'name' => link.name,
'url' => link.url,
'external' => link.external?
}
end
expect(data).to match_array(expected)
end
end
describe 'sources' do
let(:path) { path_prefix + %w[assets sources nodes] }
let(:release_fields) do
query_graphql_field(:assets, nil,
query_graphql_field(:sources, nil, 'nodes { format url }'))
end
it 'finds all release sources' do
post_query
expected = release.sources.map do |source|
{
'format' => source.format,
'url' => source.url
}
end
expect(data).to match_array(expected)
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment