Commit e7655a30 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'nfriend-update-individual-release-page-to-use-graphql-step-1' into 'master'

Step 1/3: Add GraphQL query to fetch single release

See merge request gitlab-org/gitlab!44766
parents 6c20d183 62f8280d
#import "./release.fragment.graphql"
query allReleases($fullPath: ID!, $first: Int, $last: Int, $before: String, $after: String) {
project(fullPath: $fullPath) {
releases(first: $first, last: $last, before: $before, after: $after) {
nodes {
name
tagName
tagPath
descriptionHtml
releasedAt
upcomingRelease
assets {
count
sources {
nodes {
format
url
}
}
links {
nodes {
id
name
url
directAssetUrl
linkType
external
}
}
}
evidences {
nodes {
filepath
collectedAt
sha
}
}
links {
editUrl
issuesUrl
mergeRequestsUrl
selfUrl
}
commit {
sha
webUrl
title
}
author {
webUrl
avatarUrl
username
}
milestones {
nodes {
id
title
description
webPath
stats {
totalIssuesCount
closedIssuesCount
}
}
}
...Release
}
pageInfo {
startCursor
......
#import "./release.fragment.graphql"
query oneRelease($fullPath: ID!, $tagName: String!) {
project(fullPath: $fullPath) {
release(tagName: $tagName) {
...Release
}
}
}
fragment Release on Release {
name
tagName
tagPath
descriptionHtml
releasedAt
upcomingRelease
assets {
count
sources {
nodes {
format
url
}
}
links {
nodes {
id
name
url
directAssetUrl
linkType
external
}
}
}
evidences {
nodes {
filepath
collectedAt
sha
}
}
links {
editUrl
issuesUrl
mergeRequestsUrl
selfUrl
}
commit {
sha
webUrl
title
}
author {
webUrl
avatarUrl
username
}
milestones {
nodes {
id
title
description
webPath
stats {
totalIssuesCount
closedIssuesCount
}
}
}
}
......@@ -8,7 +8,7 @@ import {
convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils';
import allReleasesQuery from '~/releases/queries/all_releases.query.graphql';
import { gqClient, convertGraphQLResponse } from '../../../util';
import { gqClient, convertAllReleasesGraphQLResponse } from '../../../util';
import { PAGE_SIZE } from '../../../constants';
/**
......@@ -64,7 +64,7 @@ export const fetchReleasesGraphQl = (
},
})
.then(response => {
const { data, paginationInfo: graphQlPageInfo } = convertGraphQLResponse(response);
const { data, paginationInfo: graphQlPageInfo } = convertAllReleasesGraphQLResponse(response);
commit(types.RECEIVE_RELEASES_SUCCESS, {
data,
......
......@@ -107,7 +107,24 @@ const convertMilestones = graphQLRelease => ({
});
/**
* Converts the response from the GraphQL endpoint into the
* Converts a single release object fetched from GraphQL
* into a release object that matches the shape of the REST API
* (the same shape that is returned by `apiJsonToRelease` above.)
*
* @param graphQLRelease The release object returned from a GraphQL query
*/
export const convertGraphQLRelease = graphQLRelease => ({
...convertScalarProperties(graphQLRelease),
...convertAssets(graphQLRelease),
...convertEvidences(graphQLRelease),
...convertLinks(graphQLRelease),
...convertCommit(graphQLRelease),
...convertAuthor(graphQLRelease),
...convertMilestones(graphQLRelease),
});
/**
* Converts the response from all_releases.query.graphql into the
* same shape as is returned from the Releases REST API.
*
* This allows the release components to use the response
......@@ -115,16 +132,8 @@ const convertMilestones = graphQLRelease => ({
*
* @param response The response received from the GraphQL endpoint
*/
export const convertGraphQLResponse = response => {
const releases = response.data.project.releases.nodes.map(r => ({
...convertScalarProperties(r),
...convertAssets(r),
...convertEvidences(r),
...convertLinks(r),
...convertCommit(r),
...convertAuthor(r),
...convertMilestones(r),
}));
export const convertAllReleasesGraphQLResponse = response => {
const releases = response.data.project.releases.nodes.map(convertGraphQLRelease);
const paginationInfo = {
...response.data.project.releases.pageInfo,
......@@ -132,3 +141,18 @@ export const convertGraphQLResponse = response => {
return { data: releases, paginationInfo };
};
/**
* Converts the response from one_release.query.graphql into the
* same shape as is returned from the Releases REST API.
*
* This allows the release components to use the response
* from either endpoint interchangeably.
*
* @param response The response received from the GraphQL endpoint
*/
export const convertOneReleaseGraphQLResponse = response => {
const release = convertGraphQLRelease(response.data.project.release);
return { data: release };
};
......@@ -116,21 +116,31 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
end
end
graphql_query_path = 'releases/queries/all_releases.query.graphql'
describe "~/#{graphql_query_path}", type: :request do
describe GraphQL::Query, type: :request do
include GraphqlHelpers
all_releases_query_path = 'releases/queries/all_releases.query.graphql'
one_release_query_path = 'releases/queries/one_release.query.graphql'
fragment_paths = ['releases/queries/release.fragment.graphql']
before(:all) do
clean_frontend_fixtures('graphql/releases/')
end
it "graphql/#{graphql_query_path}.json" do
query = File.read(File.join(Rails.root, '/app/assets/javascripts', graphql_query_path))
it "graphql/#{all_releases_query_path}.json" do
query = get_graphql_query_as_string(all_releases_query_path, fragment_paths)
post_graphql(query, current_user: admin, variables: { fullPath: project.full_path })
expect_graphql_errors_to_be_empty
end
it "graphql/#{one_release_query_path}.json" do
query = get_graphql_query_as_string(one_release_query_path, fragment_paths)
post_graphql(query, current_user: admin, variables: { fullPath: project.full_path, tagName: release.tag })
expect_graphql_errors_to_be_empty
end
end
end
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`releases/util.js convertGraphQLResponse matches snapshot 1`] = `
exports[`releases/util.js convertAllReleasesGraphQLResponse matches snapshot 1`] = `
Object {
"data": Array [
Object {
......@@ -125,3 +125,121 @@ Object {
},
}
`;
exports[`releases/util.js convertOneReleaseGraphQLResponse matches snapshot 1`] = `
Object {
"data": Object {
"_links": Object {
"editUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/edit",
"issuesUrl": "http://localhost/releases-namespace/releases-project/-/issues?release_tag=v1.1&scope=all&state=opened",
"mergeRequestsUrl": "http://localhost/releases-namespace/releases-project/-/merge_requests?release_tag=v1.1&scope=all&state=opened",
"self": "http://localhost/releases-namespace/releases-project/-/releases/v1.1",
"selfUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1",
},
"assets": Object {
"count": 8,
"links": Array [
Object {
"directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/binaries/awesome-app-3",
"external": true,
"id": "gid://gitlab/Releases::Link/13",
"linkType": "image",
"name": "Image",
"url": "https://example.com/image",
},
Object {
"directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/binaries/awesome-app-2",
"external": true,
"id": "gid://gitlab/Releases::Link/12",
"linkType": "package",
"name": "Package",
"url": "https://example.com/package",
},
Object {
"directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/binaries/awesome-app-1",
"external": false,
"id": "gid://gitlab/Releases::Link/11",
"linkType": "runbook",
"name": "Runbook",
"url": "http://localhost/releases-namespace/releases-project/runbook",
},
Object {
"directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/binaries/linux-amd64",
"external": true,
"id": "gid://gitlab/Releases::Link/10",
"linkType": "other",
"name": "linux-amd64 binaries",
"url": "https://downloads.example.com/bin/gitlab-linux-amd64",
},
],
"sources": Array [
Object {
"format": "zip",
"url": "http://localhost/releases-namespace/releases-project/-/archive/v1.1/releases-project-v1.1.zip",
},
Object {
"format": "tar.gz",
"url": "http://localhost/releases-namespace/releases-project/-/archive/v1.1/releases-project-v1.1.tar.gz",
},
Object {
"format": "tar.bz2",
"url": "http://localhost/releases-namespace/releases-project/-/archive/v1.1/releases-project-v1.1.tar.bz2",
},
Object {
"format": "tar",
"url": "http://localhost/releases-namespace/releases-project/-/archive/v1.1/releases-project-v1.1.tar",
},
],
},
"author": Object {
"avatarUrl": "https://www.gravatar.com/avatar/16f8e2050ce10180ca571c2eb19cfce2?s=80&d=identicon",
"username": "administrator",
"webUrl": "http://localhost/administrator",
},
"commit": Object {
"shortId": "b83d6e39",
"title": "Merge branch 'branch-merged' into 'master'",
},
"commitPath": "http://localhost/releases-namespace/releases-project/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0",
"descriptionHtml": "<p data-sourcepos=\\"1:1-1:33\\" dir=\\"auto\\">Best. Release. <strong>Ever.</strong> <gl-emoji title=\\"rocket\\" data-name=\\"rocket\\" data-unicode-version=\\"6.0\\">🚀</gl-emoji></p>",
"evidences": Array [
Object {
"collectedAt": "2018-12-03T00:00:00Z",
"filepath": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/evidences/1.json",
"sha": "760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d",
},
],
"milestones": Array [
Object {
"description": "The 12.4 milestone",
"id": "gid://gitlab/Milestone/124",
"issueStats": Object {
"closed": 1,
"total": 4,
},
"stats": undefined,
"title": "12.4",
"webPath": undefined,
"webUrl": "/releases-namespace/releases-project/-/milestones/2",
},
Object {
"description": "The 12.3 milestone",
"id": "gid://gitlab/Milestone/123",
"issueStats": Object {
"closed": 3,
"total": 5,
},
"stats": undefined,
"title": "12.3",
"webPath": undefined,
"webUrl": "/releases-namespace/releases-project/-/milestones/1",
},
],
"name": "The first release",
"releasedAt": "2018-12-10T00:00:00Z",
"tagName": "v1.1",
"tagPath": "/releases-namespace/releases-project/-/tags/v1.1",
"upcomingRelease": true,
},
}
`;
......@@ -10,7 +10,7 @@ import {
import createState from '~/releases/stores/modules/list/state';
import * as types from '~/releases/stores/modules/list/mutation_types';
import api from '~/api';
import { gqClient, convertGraphQLResponse } from '~/releases/util';
import { gqClient, convertAllReleasesGraphQLResponse } from '~/releases/util';
import {
normalizeHeaders,
parseIntPagination,
......@@ -164,7 +164,7 @@ describe('Releases State actions', () => {
});
it(`commits ${types.REQUEST_RELEASES} and ${types.RECEIVE_RELEASES_SUCCESS}`, () => {
const convertedResponse = convertGraphQLResponse(graphqlReleasesResponse);
const convertedResponse = convertAllReleasesGraphQLResponse(graphqlReleasesResponse);
return testAction(
fetchReleasesGraphQl,
......
......@@ -4,7 +4,7 @@ import mutations from '~/releases/stores/modules/list/mutations';
import * as types from '~/releases/stores/modules/list/mutation_types';
import { parseIntPagination, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { pageInfoHeadersWithoutPagination } from '../../../mock_data';
import { convertGraphQLResponse } from '~/releases/util';
import { convertAllReleasesGraphQLResponse } from '~/releases/util';
const originalRelease = getJSONFixture('api/releases/release.json');
const originalReleases = [originalRelease];
......@@ -22,7 +22,7 @@ describe('Releases Store Mutations', () => {
beforeEach(() => {
stateCopy = createState({});
restPageInfo = parseIntPagination(pageInfoHeadersWithoutPagination);
graphQlPageInfo = convertGraphQLResponse(graphqlReleasesResponse).paginationInfo;
graphQlPageInfo = convertAllReleasesGraphQLResponse(graphqlReleasesResponse).paginationInfo;
releases = convertObjectPropsToCamelCase(originalReleases, { deep: true });
});
......
import { cloneDeep } from 'lodash';
import { getJSONFixture } from 'helpers/fixtures';
import { releaseToApiJson, apiJsonToRelease, convertGraphQLResponse } from '~/releases/util';
const originalGraphqlReleasesResponse = getJSONFixture(
import {
releaseToApiJson,
apiJsonToRelease,
convertGraphQLRelease,
convertAllReleasesGraphQLResponse,
convertOneReleaseGraphQLResponse,
} from '~/releases/util';
const originalAllReleasesQueryResponse = getJSONFixture(
'graphql/releases/queries/all_releases.query.graphql.json',
);
const originalOneReleaseQueryResponse = getJSONFixture(
'graphql/releases/queries/one_release.query.graphql.json',
);
describe('releases/util.js', () => {
describe('releaseToApiJson', () => {
......@@ -107,54 +116,61 @@ describe('releases/util.js', () => {
});
});
describe('convertGraphQLResponse', () => {
let graphqlReleasesResponse;
let converted;
describe('convertGraphQLRelease', () => {
let releaseFromResponse;
let convertedRelease;
beforeEach(() => {
graphqlReleasesResponse = cloneDeep(originalGraphqlReleasesResponse);
converted = convertGraphQLResponse(graphqlReleasesResponse);
});
it('matches snapshot', () => {
expect(converted).toMatchSnapshot();
releaseFromResponse = cloneDeep(originalOneReleaseQueryResponse).data.project.release;
convertedRelease = convertGraphQLRelease(releaseFromResponse);
});
describe('assets', () => {
it("handles asset links that don't have a linkType", () => {
expect(converted.data[0].assets.links[0].linkType).not.toBeUndefined();
expect(convertedRelease.assets.links[0].linkType).not.toBeUndefined();
delete graphqlReleasesResponse.data.project.releases.nodes[0].assets.links.nodes[0]
.linkType;
delete releaseFromResponse.assets.links.nodes[0].linkType;
converted = convertGraphQLResponse(graphqlReleasesResponse);
convertedRelease = convertGraphQLRelease(releaseFromResponse);
expect(converted.data[0].assets.links[0].linkType).toBeUndefined();
expect(convertedRelease.assets.links[0].linkType).toBeUndefined();
});
});
describe('_links', () => {
it("handles releases that don't have any links", () => {
expect(converted.data[0]._links.selfUrl).not.toBeUndefined();
expect(convertedRelease._links.selfUrl).not.toBeUndefined();
delete graphqlReleasesResponse.data.project.releases.nodes[0].links;
delete releaseFromResponse.links;
converted = convertGraphQLResponse(graphqlReleasesResponse);
convertedRelease = convertGraphQLRelease(releaseFromResponse);
expect(converted.data[0]._links.selfUrl).toBeUndefined();
expect(convertedRelease._links.selfUrl).toBeUndefined();
});
});
describe('commit', () => {
it("handles releases that don't have any commit info", () => {
expect(converted.data[0].commit).not.toBeUndefined();
expect(convertedRelease.commit).not.toBeUndefined();
delete graphqlReleasesResponse.data.project.releases.nodes[0].commit;
delete releaseFromResponse.commit;
converted = convertGraphQLResponse(graphqlReleasesResponse);
convertedRelease = convertGraphQLRelease(releaseFromResponse);
expect(converted.data[0].commit).toBeUndefined();
expect(convertedRelease.commit).toBeUndefined();
});
});
});
describe('convertAllReleasesGraphQLResponse', () => {
it('matches snapshot', () => {
expect(convertAllReleasesGraphQLResponse(originalAllReleasesQueryResponse)).toMatchSnapshot();
});
});
describe('convertOneReleaseGraphQLResponse', () => {
it('matches snapshot', () => {
expect(convertOneReleaseGraphQLResponse(originalOneReleaseQueryResponse)).toMatchSnapshot();
});
});
});
......@@ -39,6 +39,17 @@ module JavaScriptFixturesHelpers
Gitlab::Shell.new.remove_repository(project.repository_storage, project.disk_path)
end
# Public: Reads a GraphQL query from the filesystem as a string
#
# query_path - file path to the GraphQL query, relative to `app/assets/javascripts`
# fragment_paths - an optional array of file paths to any fragments the query uses,
# also relative to `app/assets/javascripts`
def get_graphql_query_as_string(query_path, fragment_paths = [])
[query_path, *fragment_paths].map do |path|
File.read(File.join(Rails.root, '/app/assets/javascripts', path))
end.join("\n")
end
private
# Private: Store a response object as fixture file
......
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