diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index b1f43f38e510775befacc5c78353a2f38dd7cfcc..6af179c5718a9a7b99246e8d5dc664a9598ed4ce 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -9499,6 +9499,11 @@ type GeoNode { """ first: Int + """ + Global ID of a specific compliance framework to return. + """ + id: ComplianceManagementFrameworkID + """ Returns the last _n_ elements from the list. """ @@ -9715,7 +9720,7 @@ type Group { ): CodeCoverageActivityConnection """ - Compliance frameworks available to projects in this namespace Available only + Compliance frameworks available to projects in this namespace. Available only when feature flag `ff_custom_compliance_frameworks` is enabled. """ complianceFrameworks( @@ -15368,7 +15373,7 @@ type Namespace { additionalPurchasedStorageSize: Float """ - Compliance frameworks available to projects in this namespace Available only + Compliance frameworks available to projects in this namespace. Available only when feature flag `ff_custom_compliance_frameworks` is enabled. """ complianceFrameworks( @@ -15387,6 +15392,11 @@ type Namespace { """ first: Int + """ + Global ID of a specific compliance framework to return. + """ + id: ComplianceManagementFrameworkID + """ Returns the last _n_ elements from the list. """ diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 34d80e450f4cfeca1aad1b231383aafdef48a9c0..0e72319c4b0ca0d76474361f2bfc330b23a4dbfd 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -26177,6 +26177,16 @@ "ofType": null }, "defaultValue": null + }, + { + "name": "id", + "description": "Global ID of a specific compliance framework to return.", + "type": { + "kind": "SCALAR", + "name": "ComplianceManagementFrameworkID", + "ofType": null + }, + "defaultValue": null } ], "type": { @@ -26928,7 +26938,7 @@ }, { "name": "complianceFrameworks", - "description": "Compliance frameworks available to projects in this namespace Available only when feature flag `ff_custom_compliance_frameworks` is enabled.", + "description": "Compliance frameworks available to projects in this namespace. Available only when feature flag `ff_custom_compliance_frameworks` is enabled.", "args": [ { "name": "after", @@ -45542,7 +45552,7 @@ }, { "name": "complianceFrameworks", - "description": "Compliance frameworks available to projects in this namespace Available only when feature flag `ff_custom_compliance_frameworks` is enabled.", + "description": "Compliance frameworks available to projects in this namespace. Available only when feature flag `ff_custom_compliance_frameworks` is enabled.", "args": [ { "name": "after", @@ -45583,6 +45593,16 @@ "ofType": null }, "defaultValue": null + }, + { + "name": "id", + "description": "Global ID of a specific compliance framework to return.", + "type": { + "kind": "SCALAR", + "name": "ComplianceManagementFrameworkID", + "ofType": null + }, + "defaultValue": null } ], "type": { diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 439b3f8527a8bff15d6d2cd421df40db4fd41bff..720c7ca557f2d4d3fd7b297c599ccb651f390bc6 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1579,7 +1579,7 @@ Represents an external issue. | `board` | Board | A single board of the group | | `boards` | BoardConnection | Boards of the group | | `codeCoverageActivities` | CodeCoverageActivityConnection | Represents the code coverage activity for this group | -| `complianceFrameworks` | ComplianceFrameworkConnection | Compliance frameworks available to projects in this namespace Available only when feature flag `ff_custom_compliance_frameworks` is enabled. | +| `complianceFrameworks` | ComplianceFrameworkConnection | Compliance frameworks available to projects in this namespace. Available only when feature flag `ff_custom_compliance_frameworks` is enabled. | | `containerRepositories` | ContainerRepositoryConnection | Container repositories of the group | | `containerRepositoriesCount` | Int! | Number of container repositories in the group | | `containsLockedProjects` | Boolean! | Includes at least one project where the repository size exceeds the limit | @@ -2329,7 +2329,7 @@ Contains statistics about a milestone. | ----- | ---- | ----------- | | `actualRepositorySizeLimit` | Float | Size limit for repositories in the namespace in bytes | | `additionalPurchasedStorageSize` | Float | Additional storage purchased for the root namespace in bytes | -| `complianceFrameworks` | ComplianceFrameworkConnection | Compliance frameworks available to projects in this namespace Available only when feature flag `ff_custom_compliance_frameworks` is enabled. | +| `complianceFrameworks` | ComplianceFrameworkConnection | Compliance frameworks available to projects in this namespace. Available only when feature flag `ff_custom_compliance_frameworks` is enabled. | | `containsLockedProjects` | Boolean! | Includes at least one project where the repository size exceeds the limit | | `description` | String | Description of the namespace | | `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` | diff --git a/ee/app/graphql/ee/types/namespace_type.rb b/ee/app/graphql/ee/types/namespace_type.rb index d2156496cc3a8dfee8848dc0fd2dc2c130d79e2a..e087a5d7bdc6203af65952e8a81016de3a5ed6a0 100644 --- a/ee/app/graphql/ee/types/namespace_type.rb +++ b/ee/app/graphql/ee/types/namespace_type.rb @@ -57,8 +57,12 @@ module NamespaceType field :compliance_frameworks, ::Types::ComplianceManagement::ComplianceFrameworkType.connection_type, null: true, - description: 'Compliance frameworks available to projects in this namespace', - feature_flag: :ff_custom_compliance_frameworks + description: 'Compliance frameworks available to projects in this namespace.', + feature_flag: :ff_custom_compliance_frameworks do + argument :id, ::Types::GlobalIDType[::ComplianceManagement::Framework], + description: 'Global ID of a specific compliance framework to return.', + required: false + end def additional_purchased_storage_size object.additional_purchased_storage_size.megabytes @@ -68,12 +72,22 @@ def storage_size_limit object.root_storage_size.limit end - def compliance_frameworks - BatchLoader::GraphQL.for(object.id).batch(default_value: []) do |namespace_ids, loader| - results = ::ComplianceManagement::Framework.with_namespaces(namespace_ids) + def compliance_frameworks(id: nil) + id = ::Types::GlobalIDType[::ComplianceManagement::Framework].coerce_isolated_input(id) unless id.nil? + BatchLoader::GraphQL + .for([object.id, id&.model_id]) + .batch(default_value: []) do |keys, loader| + namespace_ids = keys.map(&:first).uniq + by_namespace_id = keys.group_by(&:first).transform_values { |k| k.map(&:second) } + frameworks = ::ComplianceManagement::Framework.with_namespaces(namespace_ids) + frameworks.group_by(&:namespace_id).each do |ns_id, group| + by_namespace_id[ns_id].each do |fw_id| + group.each do |fw| + next unless fw_id.nil? || fw_id.to_i == fw.id - results.each do |framework| - loader.call(framework.namespace.id) { |xs| xs << framework } + loader.call([ns_id, fw_id]) { |array| array << fw } + end + end end end end diff --git a/ee/changelogs/unreleased/289846-graphql-namespace-frameworks-filter-by-id.yml b/ee/changelogs/unreleased/289846-graphql-namespace-frameworks-filter-by-id.yml new file mode 100644 index 0000000000000000000000000000000000000000..76a908fbf8e70eb8ead031a0f0c1efa5c51f0426 --- /dev/null +++ b/ee/changelogs/unreleased/289846-graphql-namespace-frameworks-filter-by-id.yml @@ -0,0 +1,5 @@ +--- +title: Add ID filter to Namespace -> ComplianceFramework GraphQL +merge_request: 49108 +author: +type: added diff --git a/ee/spec/requests/api/graphql/namespace/compliance_frameworks_spec.rb b/ee/spec/requests/api/graphql/namespace/compliance_frameworks_spec.rb index 84caa279a1c4dc63b1485f39b8b2d70f81854c4e..40db43da11eb5d373b6886ef3534916a7097fea2 100644 --- a/ee/spec/requests/api/graphql/namespace/compliance_frameworks_spec.rb +++ b/ee/spec/requests/api/graphql/namespace/compliance_frameworks_spec.rb @@ -28,8 +28,51 @@ ) end + context 'when querying a specific framework ID' do + let(:query) do + graphql_query_for( + :namespace, { full_path: namespace.full_path }, query_nodes(:compliance_frameworks, nil, args: { id: global_id_of(compliance_framework_1) }) + ) + end + + it 'returns only a single compliance framework' do + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(:namespace, :complianceFrameworks, :nodes).map { |n| n['id'] }).to contain_exactly(global_id_of(compliance_framework_1)) + end + end + + context 'when querying an invalid object ID' do + let(:query) do + graphql_query_for( + :namespace, { full_path: namespace.full_path }, query_nodes(:compliance_frameworks, nil, args: { id: global_id_of(namespace) }) + ) + end + + it 'returns an error message' do + post_graphql(query, current_user: current_user) + + expect(graphql_errors).to contain_exactly(include('message' => "\"#{global_id_of(namespace)}\" does not represent an instance of ComplianceManagement::Framework")) + end + end + + context 'when querying a specific framework that current_user has no access to' do + let(:query) do + graphql_query_for( + :namespace, { full_path: namespace.full_path }, query_nodes(:compliance_frameworks, nil, args: { id: global_id_of(create(:compliance_framework)) }) + ) + end + + it 'does not return the framework' do + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(:namespace, :complianceFrameworks, :nodes)).to be_empty + end + end + context 'when querying multiple namespaces' do let(:group) { create(:group) } + let(:sox_framework) { create(:compliance_framework, namespace: group, name: 'SOX') } let(:multiple_namespace_query) do <<~QUERY query { @@ -39,26 +82,33 @@ b: namespace(fullPath: "#{group.full_path}") { complianceFrameworks { nodes { id name } } } + c: namespace(fullPath: "#{group.full_path}") { + complianceFrameworks(id: "#{sox_framework.to_global_id}") { nodes { id name } } + } } QUERY end before do - create(:compliance_framework, namespace: group) + create(:compliance_framework, namespace: group, name: 'GDPR') group.add_owner(current_user) end it 'avoids N+1 queries' do + post_graphql(query, current_user: current_user) + post_graphql(multiple_namespace_query, current_user: current_user) + query_count = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }.count - expect { post_graphql(multiple_namespace_query, current_user: current_user) }.not_to exceed_query_limit(query_count + 4) + expect { post_graphql(multiple_namespace_query, current_user: current_user) }.not_to exceed_query_limit(query_count + 2) end it 'responds with the expected list of compliance frameworks' do post_graphql(multiple_namespace_query, current_user: current_user) - expect(graphql_data_at(:a, :complianceFrameworks, :nodes).map { |f| f['name'] }).to contain_exactly('Test1', 'Test2') - expect(graphql_data_at(:b, :complianceFrameworks, :nodes).map { |f| f['name'] }).to contain_exactly('GDPR') + expect(graphql_data_at(:a, :complianceFrameworks, :nodes, :name)).to contain_exactly('Test1', 'Test2') + expect(graphql_data_at(:b, :complianceFrameworks, :nodes, :name)).to contain_exactly('GDPR', 'SOX') + expect(graphql_data_at(:c, :complianceFrameworks, :nodes, :name)).to contain_exactly('SOX') end end