diff --git a/app/graphql/mutations/ml/model_versions/edit.rb b/app/graphql/mutations/ml/model_versions/edit.rb new file mode 100644 index 0000000000000000000000000000000000000000..f6bc85cc1cbf1f491273ab3fd8bed30702ea8b3b --- /dev/null +++ b/app/graphql/mutations/ml/model_versions/edit.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Mutations + module Ml + module ModelVersions + class Edit < BaseMutation + graphql_name 'MlModelVersionEdit' + include FindsProject + + authorize :write_model_registry + + argument :project_path, GraphQL::Types::ID, + required: true, + description: "Project the model to mutate is in." + + argument :model_id, ::Types::GlobalIDType[::Ml::Model], + required: true, + description: 'Global ID of the model the version belongs to.' + + argument :version, GraphQL::Types::String, + required: true, + description: 'Model version.' + + argument :description, GraphQL::Types::String, + required: true, + description: 'Description of the model version.' + + field :model_version, + Types::Ml::ModelVersionType, + null: true, + description: 'Model after mutation.' + + def resolve(**args) + project = authorized_find!(args[:project_path]) + model = ::Ml::Model.by_project_id_and_id(project.id, args[:model_id].model_id) + + return { errors: ['Model not found'] } unless model + + service_response = ::Ml::ModelVersions::UpdateModelVersionService.new(project, model.name, args[:version], + args[:description]).execute + + if service_response.success? + { + model_version: service_response.payload, + errors: [] + } + else + { + model_version: nil, + errors: service_response.errors + } + end + end + end + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 1ee9f8fa275e2ff475329855fcb6bad995cdd090..b4ab430ecf2a0c82fdd6b2dccf67ee939faabfda 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -223,6 +223,7 @@ class MutationType < BaseObject mount_mutation Mutations::Ml::Models::Destroy, alpha: { milestone: '16.10' } mount_mutation Mutations::Ml::Models::Delete, alpha: { milestone: '17.0' } mount_mutation Mutations::Ml::ModelVersions::Create, alpha: { milestone: '17.1' } + mount_mutation Mutations::Ml::ModelVersions::Edit, alpha: { milestone: '17.4' } mount_mutation Mutations::Ml::ModelVersions::Delete, alpha: { milestone: '17.0' } mount_mutation Mutations::BranchRules::Delete, alpha: { milestone: '16.9' } mount_mutation Mutations::Pages::Deployment::Delete, alpha: { milestone: '17.1' } diff --git a/app/helpers/projects/ml/model_registry_helper.rb b/app/helpers/projects/ml/model_registry_helper.rb index ee5303d8710f935c29b45296c12e3db25d3018e2..e7d35baf36fb5daea6b7831615fb8ca525f09afc 100644 --- a/app/helpers/projects/ml/model_registry_helper.rb +++ b/app/helpers/projects/ml/model_registry_helper.rb @@ -48,7 +48,8 @@ def show_ml_model_version_data(model_version, user) can_write_model_registry: can_write_model_registry?(user, project), import_path: model_version_artifact_import_path(project.id, model_version.id), model_path: project_ml_model_path(project, model_version.model), - max_allowed_file_size: max_allowed_file_size(project) + max_allowed_file_size: max_allowed_file_size(project), + markdown_preview_path: preview_markdown_path(project) } to_json(data) diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 9e9c1432a1483777a8b116daec2b98e70318d719..4df7198a5074082b73327cd2960e743b6ab5fadd 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -7123,6 +7123,32 @@ Input type: `MlModelVersionDeleteInput` | <a id="mutationmlmodelversiondeleteerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationmlmodelversiondeletemodelversion"></a>`modelVersion` | [`MlModelVersion`](#mlmodelversion) | Deleted model version. | +### `Mutation.mlModelVersionEdit` + +DETAILS: +**Introduced** in GitLab 17.4. +**Status**: Experiment. + +Input type: `MlModelVersionEditInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationmlmodelversioneditclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationmlmodelversioneditdescription"></a>`description` | [`String!`](#string) | Description of the model version. | +| <a id="mutationmlmodelversioneditmodelid"></a>`modelId` | [`MlModelID!`](#mlmodelid) | Global ID of the model the version belongs to. | +| <a id="mutationmlmodelversioneditprojectpath"></a>`projectPath` | [`ID!`](#id) | Project the model to mutate is in. | +| <a id="mutationmlmodelversioneditversion"></a>`version` | [`String!`](#string) | Model version. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationmlmodelversioneditclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationmlmodelversionediterrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| <a id="mutationmlmodelversioneditmodelversion"></a>`modelVersion` | [`MlModelVersion`](#mlmodelversion) | Model after mutation. | + ### `Mutation.namespaceBanDestroy` Input type: `NamespaceBanDestroyInput` diff --git a/spec/helpers/projects/ml/model_registry_helper_spec.rb b/spec/helpers/projects/ml/model_registry_helper_spec.rb index 415e98741698bd81fae15ae5a0f6046a873247a8..ec4e73ca122b02b2e46ec2b1c33945b702561926 100644 --- a/spec/helpers/projects/ml/model_registry_helper_spec.rb +++ b/spec/helpers/projects/ml/model_registry_helper_spec.rb @@ -99,7 +99,8 @@ "canWriteModelRegistry" => true, 'maxAllowedFileSize' => 10737418240, "importPath" => "/api/v4/projects/#{project.id}/packages/ml_models/#{model_version.id}/files/", - "modelPath" => "/#{project.full_path}/-/ml/models/1" + "modelPath" => "/#{project.full_path}/-/ml/models/1", + "markdownPreviewPath" => "/#{project.full_path}/-/preview_markdown" }) end diff --git a/spec/requests/api/graphql/mutations/ml/model_versions/edit_spec.rb b/spec/requests/api/graphql/mutations/ml/model_versions/edit_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..b1e6128463b20550722fdb7d3bc2d1e851545bbc --- /dev/null +++ b/spec/requests/api/graphql/mutations/ml/model_versions/edit_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Editing of a machine learning model version', feature_category: :mlops do + include GraphqlHelpers + let_it_be(:model_version) { create(:ml_model_versions, :with_package, description: 'A **description**') } + let_it_be(:project) { model_version.project } + let_it_be(:current_user) { project.owner } + let_it_be(:guest) { create(:user) } + + let(:model_id) { model_version.model.to_gid } + let(:version) { model_version.version } + let(:description) { 'A description' } + let(:new_description) { 'A **new** description' } + + let(:edit_input) do + { project_path: project.full_path, description: new_description, model_id: model_id, version: version } + end + + let(:mutation) { graphql_mutation(:ml_model_version_edit, edit_input, nil, ['version']) } + let(:mutation_response) { graphql_mutation_response(:ml_model_version_edit) } + + context 'when user is not allowed write changes' do + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(current_user, :write_model_registry, project) + .and_return(false) + end + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when the user is not part of the project' do + it 'does not update description' do + post_graphql_mutation(mutation, current_user: guest) + expect { mutation }.to not_change { model_version.reload.description } + expect(mutation_response).to be_nil + end + end + + context 'when the user is authenticated' do + context 'when the model does not exist' do + let(:model_id) { "gid://gitlab/Ml::Model/0" } + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['errors']).to match_array(['Model not found']) + end + end + + context 'when the model exists' do + it 'updates the model description' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['errors']).to be_empty + + model_version.reload + expect(model_version.description).to eq(new_description) + end + end + + context 'when the model is not part of the project' do + let(:project) { create(:project) } + + before do + post_graphql_mutation(mutation, current_user: current_user) + end + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when the update service fails' do + before do + allow_next_instance_of(::Ml::ModelVersions::UpdateModelVersionService) do |instance| + allow(instance).to receive(:execute).and_return( + ServiceResponse.error(message: 'Model update failed') + ) + end + end + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['errors']).to match_array(['Model update failed']) + expect(mutation_response['modelVersion']).to be_nil + end + end + end +end