Commit 4a75e873 authored by Ash McKenzie's avatar Ash McKenzie

Merge branch 'psi-gql-type-iteration' into 'master'

Use stricter typed global IDs for iterations

See merge request gitlab-org/gitlab!47045
parents bc2864b8 f9e189f6
...@@ -23,8 +23,6 @@ FactoryBot/InlineAssociation: ...@@ -23,8 +23,6 @@ FactoryBot/InlineAssociation:
Graphql/IDType: Graphql/IDType:
Exclude: Exclude:
- 'ee/app/graphql/ee/mutations/issues/update.rb' - 'ee/app/graphql/ee/mutations/issues/update.rb'
- 'ee/app/graphql/mutations/iterations/update.rb'
- 'ee/app/graphql/resolvers/iterations_resolver.rb'
- 'app/graphql/mutations/boards/issues/issue_move_list.rb' - 'app/graphql/mutations/boards/issues/issue_move_list.rb'
- 'app/graphql/mutations/metrics/dashboard/annotations/delete.rb' - 'app/graphql/mutations/metrics/dashboard/annotations/delete.rb'
- 'app/graphql/resolvers/design_management/design_at_version_resolver.rb' - 'app/graphql/resolvers/design_management/design_at_version_resolver.rb'
......
...@@ -9159,17 +9159,17 @@ type Group { ...@@ -9159,17 +9159,17 @@ type Group {
first: Int first: Int
""" """
The ID of the Iteration to look up Global ID of the Iteration to look up.
""" """
id: ID id: ID
""" """
The internal ID of the Iteration to look up Internal ID of the Iteration to look up.
""" """
iid: ID iid: ID
""" """
Whether to include ancestor iterations. Defaults to true Whether to include ancestor iterations. Defaults to true.
""" """
includeAncestors: Boolean includeAncestors: Boolean
...@@ -9186,7 +9186,7 @@ type Group { ...@@ -9186,7 +9186,7 @@ type Group {
startDate: Time startDate: Time
""" """
Filter iterations by state Filter iterations by state.
""" """
state: IterationState state: IterationState
...@@ -9196,7 +9196,7 @@ type Group { ...@@ -9196,7 +9196,7 @@ type Group {
timeframe: Timeframe timeframe: Timeframe
""" """
Fuzzy search by title Fuzzy search by title.
""" """
title: String title: String
): IterationConnection ): IterationConnection
...@@ -15843,17 +15843,17 @@ type Project { ...@@ -15843,17 +15843,17 @@ type Project {
first: Int first: Int
""" """
The ID of the Iteration to look up Global ID of the Iteration to look up.
""" """
id: ID id: ID
""" """
The internal ID of the Iteration to look up Internal ID of the Iteration to look up.
""" """
iid: ID iid: ID
""" """
Whether to include ancestor iterations. Defaults to true Whether to include ancestor iterations. Defaults to true.
""" """
includeAncestors: Boolean includeAncestors: Boolean
...@@ -15870,7 +15870,7 @@ type Project { ...@@ -15870,7 +15870,7 @@ type Project {
startDate: Time startDate: Time
""" """
Filter iterations by state Filter iterations by state.
""" """
state: IterationState state: IterationState
...@@ -15880,7 +15880,7 @@ type Project { ...@@ -15880,7 +15880,7 @@ type Project {
timeframe: Timeframe timeframe: Timeframe
""" """
Fuzzy search by title Fuzzy search by title.
""" """
title: String title: String
): IterationConnection ): IterationConnection
...@@ -22557,32 +22557,32 @@ input UpdateIterationInput { ...@@ -22557,32 +22557,32 @@ input UpdateIterationInput {
clientMutationId: String clientMutationId: String
""" """
The description of the iteration Description of the iteration.
""" """
description: String description: String
""" """
The end date of the iteration End date of the iteration.
""" """
dueDate: String dueDate: String
""" """
The group of the iteration Group of the iteration.
""" """
groupPath: ID! groupPath: ID!
""" """
The id of the iteration Global ID of the iteration.
""" """
id: ID! id: ID!
""" """
The start date of the iteration Start date of the iteration.
""" """
startDate: String startDate: String
""" """
The title of the iteration Title of the iteration.
""" """
title: String title: String
} }
...@@ -22602,7 +22602,7 @@ type UpdateIterationPayload { ...@@ -22602,7 +22602,7 @@ type UpdateIterationPayload {
errors: [String!]! errors: [String!]!
""" """
The updated iteration Updated iteration.
""" """
iteration: Iteration iteration: Iteration
} }
......
...@@ -25140,7 +25140,7 @@ ...@@ -25140,7 +25140,7 @@
}, },
{ {
"name": "state", "name": "state",
"description": "Filter iterations by state", "description": "Filter iterations by state.",
"type": { "type": {
"kind": "ENUM", "kind": "ENUM",
"name": "IterationState", "name": "IterationState",
...@@ -25150,7 +25150,7 @@ ...@@ -25150,7 +25150,7 @@
}, },
{ {
"name": "title", "name": "title",
"description": "Fuzzy search by title", "description": "Fuzzy search by title.",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
...@@ -25160,7 +25160,7 @@ ...@@ -25160,7 +25160,7 @@
}, },
{ {
"name": "id", "name": "id",
"description": "The ID of the Iteration to look up", "description": "Global ID of the Iteration to look up.",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "ID", "name": "ID",
...@@ -25170,7 +25170,7 @@ ...@@ -25170,7 +25170,7 @@
}, },
{ {
"name": "iid", "name": "iid",
"description": "The internal ID of the Iteration to look up", "description": "Internal ID of the Iteration to look up.",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "ID", "name": "ID",
...@@ -25180,7 +25180,7 @@ ...@@ -25180,7 +25180,7 @@
}, },
{ {
"name": "includeAncestors", "name": "includeAncestors",
"description": "Whether to include ancestor iterations. Defaults to true", "description": "Whether to include ancestor iterations. Defaults to true.",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "Boolean", "name": "Boolean",
...@@ -46391,7 +46391,7 @@ ...@@ -46391,7 +46391,7 @@
}, },
{ {
"name": "state", "name": "state",
"description": "Filter iterations by state", "description": "Filter iterations by state.",
"type": { "type": {
"kind": "ENUM", "kind": "ENUM",
"name": "IterationState", "name": "IterationState",
...@@ -46401,7 +46401,7 @@ ...@@ -46401,7 +46401,7 @@
}, },
{ {
"name": "title", "name": "title",
"description": "Fuzzy search by title", "description": "Fuzzy search by title.",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
...@@ -46411,7 +46411,7 @@ ...@@ -46411,7 +46411,7 @@
}, },
{ {
"name": "id", "name": "id",
"description": "The ID of the Iteration to look up", "description": "Global ID of the Iteration to look up.",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "ID", "name": "ID",
...@@ -46421,7 +46421,7 @@ ...@@ -46421,7 +46421,7 @@
}, },
{ {
"name": "iid", "name": "iid",
"description": "The internal ID of the Iteration to look up", "description": "Internal ID of the Iteration to look up.",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "ID", "name": "ID",
...@@ -46431,7 +46431,7 @@ ...@@ -46431,7 +46431,7 @@
}, },
{ {
"name": "includeAncestors", "name": "includeAncestors",
"description": "Whether to include ancestor iterations. Defaults to true", "description": "Whether to include ancestor iterations. Defaults to true.",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "Boolean", "name": "Boolean",
...@@ -65527,7 +65527,7 @@ ...@@ -65527,7 +65527,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "groupPath", "name": "groupPath",
"description": "The group of the iteration", "description": "Group of the iteration.",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -65541,7 +65541,7 @@ ...@@ -65541,7 +65541,7 @@
}, },
{ {
"name": "id", "name": "id",
"description": "The id of the iteration", "description": "Global ID of the iteration.",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -65555,7 +65555,7 @@ ...@@ -65555,7 +65555,7 @@
}, },
{ {
"name": "title", "name": "title",
"description": "The title of the iteration", "description": "Title of the iteration.",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
...@@ -65565,7 +65565,7 @@ ...@@ -65565,7 +65565,7 @@
}, },
{ {
"name": "description", "name": "description",
"description": "The description of the iteration", "description": "Description of the iteration.",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
...@@ -65575,7 +65575,7 @@ ...@@ -65575,7 +65575,7 @@
}, },
{ {
"name": "startDate", "name": "startDate",
"description": "The start date of the iteration", "description": "Start date of the iteration.",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
...@@ -65585,7 +65585,7 @@ ...@@ -65585,7 +65585,7 @@
}, },
{ {
"name": "dueDate", "name": "dueDate",
"description": "The end date of the iteration", "description": "End date of the iteration.",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
...@@ -65655,7 +65655,7 @@ ...@@ -65655,7 +65655,7 @@
}, },
{ {
"name": "iteration", "name": "iteration",
"description": "The updated iteration", "description": "Updated iteration.",
"args": [ "args": [
], ],
...@@ -3392,7 +3392,7 @@ Autogenerated return type of UpdateIteration. ...@@ -3392,7 +3392,7 @@ Autogenerated return type of UpdateIteration.
| ----- | ---- | ----------- | | ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `iteration` | Iteration | The updated iteration | | `iteration` | Iteration | Updated iteration. |
### UpdateNotePayload ### UpdateNotePayload
......
...@@ -4,7 +4,6 @@ import { deprecatedCreateFlash as createFlash } from '~/flash'; ...@@ -4,7 +4,6 @@ import { deprecatedCreateFlash as createFlash } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale'; import { __ } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createIteration from '../queries/create_iteration.mutation.graphql'; import createIteration from '../queries/create_iteration.mutation.graphql';
import updateIteration from '../queries/update_iteration.mutation.graphql'; import updateIteration from '../queries/update_iteration.mutation.graphql';
import DueDateSelectors from '~/due_date_select'; import DueDateSelectors from '~/due_date_select';
...@@ -109,7 +108,7 @@ export default { ...@@ -109,7 +108,7 @@ export default {
variables: { variables: {
input: { input: {
...this.variables.input, ...this.variables.input,
id: getIdFromGraphQLId(this.iteration.id), id: this.iteration.id,
}, },
}, },
}) })
......
...@@ -13,39 +13,42 @@ module Mutations ...@@ -13,39 +13,42 @@ module Mutations
field :iteration, field :iteration,
Types::IterationType, Types::IterationType,
null: true, null: true,
description: 'The updated iteration' description: 'Updated iteration.'
argument :group_path, GraphQL::ID_TYPE, argument :group_path, GraphQL::ID_TYPE,
required: true, required: true,
description: "The group of the iteration" description: 'Group of the iteration.'
# rubocop:disable Graphql/IDType
argument :id, argument :id,
GraphQL::ID_TYPE, GraphQL::ID_TYPE,
required: true, required: true,
description: 'The id of the iteration' description: 'Global ID of the iteration.'
# rubocop:enable Graphql/IDType
argument :title, argument :title,
GraphQL::STRING_TYPE, GraphQL::STRING_TYPE,
required: false, required: false,
description: 'The title of the iteration' description: 'Title of the iteration.'
argument :description, argument :description,
GraphQL::STRING_TYPE, GraphQL::STRING_TYPE,
required: false, required: false,
description: 'The description of the iteration' description: 'Description of the iteration.'
argument :start_date, argument :start_date,
GraphQL::STRING_TYPE, GraphQL::STRING_TYPE,
required: false, required: false,
description: 'The start date of the iteration' description: 'Start date of the iteration.'
argument :due_date, argument :due_date,
GraphQL::STRING_TYPE, GraphQL::STRING_TYPE,
required: false, required: false,
description: 'The end date of the iteration' description: 'End date of the iteration.'
def resolve(args) def resolve(args)
validate_arguments!(args) validate_arguments!(args)
args[:id] = id_from_args(args)
parent = resolve_group(full_path: args[:group_path]).try(:sync) parent = resolve_group(full_path: args[:group_path]).try(:sync)
iteration = authorized_find!(parent: parent, id: args[:id]) iteration = authorized_find!(parent: parent, id: args[:id])
...@@ -64,12 +67,21 @@ module Mutations ...@@ -64,12 +67,21 @@ module Mutations
private private
def find_object(parent:, id:) def find_object(parent:, id:)
::Resolvers::IterationsResolver.new(object: parent, context: context, field: nil).resolve(id: id).items.first ::Resolvers::IterationsResolver.new(object: parent, context: context, field: nil)
.resolve(id: id).items.first
end end
def validate_arguments!(args) def validate_arguments!(args)
raise Gitlab::Graphql::Errors::ArgumentError, 'The list of iteration attributes is empty' if args.except(:group_path, :id).empty? raise Gitlab::Graphql::Errors::ArgumentError, 'The list of iteration attributes is empty' if args.except(:group_path, :id).empty?
end end
# Originally accepted a raw model id. Now accept a gid, but allow a raw id
# for backward compatibility
def id_from_args(args)
GitlabSchema.parse_gid(args[:id], expected_type: ::Iteration)
rescue Gitlab::Graphql::Errors::ArgumentError
::Gitlab::GlobalId.as_global_id(args[:id].to_i, model_name: 'Iteration')
end
end end
end end
end end
...@@ -7,27 +7,32 @@ module Resolvers ...@@ -7,27 +7,32 @@ module Resolvers
argument :state, Types::IterationStateEnum, argument :state, Types::IterationStateEnum,
required: false, required: false,
description: 'Filter iterations by state' description: 'Filter iterations by state.'
argument :title, GraphQL::STRING_TYPE, argument :title, GraphQL::STRING_TYPE,
required: false, required: false,
description: 'Fuzzy search by title' description: 'Fuzzy search by title.'
# rubocop:disable Graphql/IDType
argument :id, GraphQL::ID_TYPE, argument :id, GraphQL::ID_TYPE,
required: false, required: false,
description: 'The ID of the Iteration to look up' description: 'Global ID of the Iteration to look up.'
# rubocop:enable Graphql/IDType
argument :iid, GraphQL::ID_TYPE, argument :iid, GraphQL::ID_TYPE,
required: false, required: false,
description: 'The internal ID of the Iteration to look up' description: 'Internal ID of the Iteration to look up.'
argument :include_ancestors, GraphQL::BOOLEAN_TYPE, argument :include_ancestors, GraphQL::BOOLEAN_TYPE,
required: false, required: false,
description: 'Whether to include ancestor iterations. Defaults to true' description: 'Whether to include ancestor iterations. Defaults to true.'
type Types::IterationType, null: true type Types::IterationType.connection_type, null: true
def resolve(**args) def resolve(**args)
validate_timeframe_params!(args) validate_timeframe_params!(args)
authorize! authorize!
args[:id] = id_from_args(args)
args[:include_ancestors] = true if args[:include_ancestors].nil? && args[:iid].nil? args[:include_ancestors] = true if args[:include_ancestors].nil? && args[:iid].nil?
iterations = IterationsFinder.new(context[:current_user], iterations_finder_params(args)).execute iterations = IterationsFinder.new(context[:current_user], iterations_finder_params(args)).execute
...@@ -58,5 +63,15 @@ module Resolvers ...@@ -58,5 +63,15 @@ module Resolvers
def authorize! def authorize!
Ability.allowed?(context[:current_user], :read_iteration, parent) || raise_resource_not_available_error! Ability.allowed?(context[:current_user], :read_iteration, parent) || raise_resource_not_available_error!
end end
# Originally accepted a raw model id. Now accept a gid, but allow a raw id
# for backward compatibility
def id_from_args(args)
return unless args[:id].present?
GitlabSchema.parse_gid(args[:id], expected_type: ::Iteration).model_id
rescue Gitlab::Graphql::Errors::ArgumentError
args[:id]
end
end end
end end
...@@ -186,7 +186,7 @@ describe('Iteration Form', () => { ...@@ -186,7 +186,7 @@ describe('Iteration Form', () => {
variables: { variables: {
input: { input: {
groupPath, groupPath,
id, id: iteration.id,
title, title,
description, description,
startDate, startDate,
......
...@@ -43,13 +43,23 @@ RSpec.describe Resolvers::IterationsResolver do ...@@ -43,13 +43,23 @@ RSpec.describe Resolvers::IterationsResolver do
start_date = now start_date = now
end_date = start_date + 1.hour end_date = start_date + 1.hour
search = 'wow' search = 'wow'
id = 1 id = '1'
iid = 2 iid = 2
params = { id: id, iid: iid, group_ids: group.id, state: 'closed', start_date: start_date, end_date: end_date, search_title: search } params = { id: id, iid: iid, group_ids: group.id, state: 'closed', start_date: start_date, end_date: end_date, search_title: search }
expect(IterationsFinder).to receive(:new).with(current_user, params).and_call_original expect(IterationsFinder).to receive(:new).with(current_user, params).and_call_original
resolve_group_iterations(start_date: start_date, end_date: end_date, state: 'closed', title: search, id: id, iid: iid) resolve_group_iterations(start_date: start_date, end_date: end_date, state: 'closed', title: search, id: 'gid://gitlab/Iteration/1', iid: iid)
end
it 'accepts a raw model id for backward compatibility' do
id = 1
iid = 2
params = { id: id, iid: iid, group_ids: group.id, state: 'all', start_date: nil, end_date: nil, search_title: nil }
expect(IterationsFinder).to receive(:new).with(current_user, params).and_call_original
resolve_group_iterations(id: id, iid: iid)
end end
end end
......
...@@ -50,6 +50,7 @@ RSpec.describe 'Querying an Iteration' do ...@@ -50,6 +50,7 @@ RSpec.describe 'Querying an Iteration' do
let_it_be(:project_iteration) { create(:iteration, :skip_project_validation, project: project) } let_it_be(:project_iteration) { create(:iteration, :skip_project_validation, project: project) }
shared_examples 'scoped path' do shared_examples 'scoped path' do
let(:queried_iteration_id) { queried_iteration.to_global_id.to_s }
let(:iteration_nodes) do let(:iteration_nodes) do
nodes = <<~NODES nodes = <<~NODES
nodes { nodes {
...@@ -58,7 +59,7 @@ RSpec.describe 'Querying an Iteration' do ...@@ -58,7 +59,7 @@ RSpec.describe 'Querying an Iteration' do
} }
NODES NODES
query_graphql_field('iterations', { id: queried_iteration.id }, nodes) query_graphql_field('iterations', { id: queried_iteration_id }, nodes)
end end
before_all do before_all do
...@@ -68,6 +69,14 @@ RSpec.describe 'Querying an Iteration' do ...@@ -68,6 +69,14 @@ RSpec.describe 'Querying an Iteration' do
specify do specify do
expect(subject).to include('scopedPath' => expected_scope_path, 'scopedUrl' => expected_scope_url) expect(subject).to include('scopedPath' => expected_scope_path, 'scopedUrl' => expected_scope_url)
end end
context 'when given a raw model id (backward compatibility)' do
let(:queried_iteration_id) { queried_iteration.id }
specify do
expect(subject).to include('scopedPath' => expected_scope_path, 'scopedUrl' => expected_scope_url)
end
end
end end
context 'inside a project context' do context 'inside a project context' do
......
...@@ -21,7 +21,7 @@ RSpec.describe 'Updating an Iteration' do ...@@ -21,7 +21,7 @@ RSpec.describe 'Updating an Iteration' do
end end
let(:mutation) do let(:mutation) do
params = { group_path: group.full_path, id: iteration.id }.merge(attributes) params = { group_path: group.full_path, id: iteration.to_global_id.to_s }.merge(attributes)
graphql_mutation(:update_iteration, params) graphql_mutation(:update_iteration, params)
end end
...@@ -53,9 +53,12 @@ RSpec.describe 'Updating an Iteration' do ...@@ -53,9 +53,12 @@ RSpec.describe 'Updating an Iteration' do
stub_licensed_features(iterations: false) stub_licensed_features(iterations: false)
end end
it_behaves_like 'a mutation that returns top-level errors', it_behaves_like 'a mutation that returns top-level errors' do
errors: ['The resource that you are attempting to access does not '\ let(:match_errors) do
'exist or you don\'t have permission to perform this action'] include('The resource that you are attempting to access does not '\
'exist or you don\'t have permission to perform this action')
end
end
end end
context 'when iterations are enabled' do context 'when iterations are enabled' do
...@@ -92,6 +95,18 @@ RSpec.describe 'Updating an Iteration' do ...@@ -92,6 +95,18 @@ RSpec.describe 'Updating an Iteration' do
end end
end end
context 'when given a raw model id (backward compatibility)' do
let(:attributes) { { id: iteration.id, title: 'title' } }
it 'updates the iteration' do
post_graphql_mutation(mutation, current_user: current_user)
iteration_hash = mutation_response['iteration']
expect(iteration_hash['title']).to eq('title')
expect(iteration.reload.title).to eq('title')
end
end
context 'when the list of attributes is empty' do context 'when the list of attributes is empty' do
let(:attributes) { {} } let(:attributes) { {} }
......
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