From 177339a7f54d6714214015c62d482b2302298b29 Mon Sep 17 00:00:00 2001
From: Eugenia Grieff <egrieff@gitlab.com>
Date: Fri, 6 Oct 2023 00:42:03 +0000
Subject: [PATCH] Add work items related link restrictions table

Adds table which will be used for storing related links restrictions
for work item types.

Changelog: added
---
 app/models/concerns/enums/issuable_link.rb    | 12 +++++++
 app/models/concerns/issuable_link.rb          |  5 ++-
 .../work_items/related_link_restriction.rb    | 16 ++++++++++
 .../work_item_related_link_restrictions.yml   | 10 ++++++
 ...930094139_add_related_link_restrictions.rb | 21 +++++++++++++
 db/schema_migrations/20230930094139           |  1 +
 db/structure.sql                              | 31 +++++++++++++++++++
 ee/app/models/concerns/ee/issuable_link.rb    |  1 +
 .../work_items/related_link_restrictions.rb   |  9 ++++++
 .../related_link_restriction_spec.rb          | 22 +++++++++++++
 10 files changed, 125 insertions(+), 3 deletions(-)
 create mode 100644 app/models/concerns/enums/issuable_link.rb
 create mode 100644 app/models/work_items/related_link_restriction.rb
 create mode 100644 db/docs/work_item_related_link_restrictions.yml
 create mode 100644 db/migrate/20230930094139_add_related_link_restrictions.rb
 create mode 100644 db/schema_migrations/20230930094139
 create mode 100644 spec/factories/work_items/related_link_restrictions.rb
 create mode 100644 spec/models/work_items/related_link_restriction_spec.rb

diff --git a/app/models/concerns/enums/issuable_link.rb b/app/models/concerns/enums/issuable_link.rb
new file mode 100644
index 0000000000000..ca5728c26001e
--- /dev/null
+++ b/app/models/concerns/enums/issuable_link.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Enums
+  module IssuableLink
+    TYPE_RELATES_TO = 'relates_to'
+    TYPE_BLOCKS = 'blocks'
+
+    def self.link_types
+      { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1 }
+    end
+  end
+end
diff --git a/app/models/concerns/issuable_link.rb b/app/models/concerns/issuable_link.rb
index 4a922d3c2ea9e..dcd2705185f0c 100644
--- a/app/models/concerns/issuable_link.rb
+++ b/app/models/concerns/issuable_link.rb
@@ -10,8 +10,7 @@ module IssuableLink
   extend ActiveSupport::Concern
 
   MAX_LINKS_COUNT = 100
-  TYPE_RELATES_TO = 'relates_to'
-  TYPE_BLOCKS = 'blocks' ## EE-only. Kept here to be used on link_type enum.
+  TYPE_RELATES_TO = Enums::IssuableLink::TYPE_RELATES_TO
 
   class_methods do
     def inverse_link_type(type)
@@ -43,7 +42,7 @@ def available_link_types
 
     scope :for_source_or_target, ->(issuable) { where(source: issuable).or(where(target: issuable)) }
 
-    enum link_type: { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1 }
+    enum link_type: Enums::IssuableLink.link_types
 
     private
 
diff --git a/app/models/work_items/related_link_restriction.rb b/app/models/work_items/related_link_restriction.rb
new file mode 100644
index 0000000000000..d4a66c95ffb95
--- /dev/null
+++ b/app/models/work_items/related_link_restriction.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module WorkItems
+  class RelatedLinkRestriction < ApplicationRecord
+    self.table_name = 'work_item_related_link_restrictions'
+
+    belongs_to :source_type, class_name: 'WorkItems::Type'
+    belongs_to :target_type, class_name: 'WorkItems::Type'
+
+    validates :source_type, presence: true
+    validates :target_type, presence: true
+    validates :target_type, uniqueness: { scope: [:source_type_id, :link_type] }
+
+    enum link_type: Enums::IssuableLink.link_types
+  end
+end
diff --git a/db/docs/work_item_related_link_restrictions.yml b/db/docs/work_item_related_link_restrictions.yml
new file mode 100644
index 0000000000000..1f76b0482be01
--- /dev/null
+++ b/db/docs/work_item_related_link_restrictions.yml
@@ -0,0 +1,10 @@
+---
+table_name: work_item_related_link_restrictions
+classes:
+  - WorkItems::RelatedLinkRestriction
+feature_categories:
+  - portfolio_management
+description: Restrictions applied to related links.
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133044
+milestone: '16.5'
+gitlab_schema: gitlab_main
diff --git a/db/migrate/20230930094139_add_related_link_restrictions.rb b/db/migrate/20230930094139_add_related_link_restrictions.rb
new file mode 100644
index 0000000000000..e67f32b860cfb
--- /dev/null
+++ b/db/migrate/20230930094139_add_related_link_restrictions.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class AddRelatedLinkRestrictions < Gitlab::Database::Migration[2.1]
+  UNIQUE_INDEX_NAME = 'index_work_item_link_restrictions_on_source_link_type_target'
+
+  def up
+    create_table :work_item_related_link_restrictions do |t|
+      t.references :source_type, index: false, null: false,
+        foreign_key: { on_delete: :cascade, to_table: :work_item_types }
+      t.references :target_type, index: true, null: false,
+        foreign_key: { on_delete: :cascade, to_table: :work_item_types }
+      t.integer :link_type, null: false, limit: 2, default: 0
+
+      t.index [:source_type_id, :link_type, :target_type_id], unique: true, name: UNIQUE_INDEX_NAME
+    end
+  end
+
+  def down
+    drop_table :work_item_related_link_restrictions
+  end
+end
diff --git a/db/schema_migrations/20230930094139 b/db/schema_migrations/20230930094139
new file mode 100644
index 0000000000000..d076adbb7f781
--- /dev/null
+++ b/db/schema_migrations/20230930094139
@@ -0,0 +1 @@
+e5a56945b0f18c1014905534f6ac8cbd026582bb57b49368558435331f0746de
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index bc5b2b20a8482..3486dfa3d0c0c 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -25139,6 +25139,22 @@ CREATE TABLE work_item_progresses (
     last_reminder_sent_at timestamp with time zone
 );
 
+CREATE TABLE work_item_related_link_restrictions (
+    id bigint NOT NULL,
+    source_type_id bigint NOT NULL,
+    target_type_id bigint NOT NULL,
+    link_type smallint DEFAULT 0 NOT NULL
+);
+
+CREATE SEQUENCE work_item_related_link_restrictions_id_seq
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+ALTER SEQUENCE work_item_related_link_restrictions_id_seq OWNED BY work_item_related_link_restrictions.id;
+
 CREATE TABLE work_item_types (
     id bigint NOT NULL,
     base_type smallint DEFAULT 0 NOT NULL,
@@ -26923,6 +26939,8 @@ ALTER TABLE ONLY work_item_hierarchy_restrictions ALTER COLUMN id SET DEFAULT ne
 
 ALTER TABLE ONLY work_item_parent_links ALTER COLUMN id SET DEFAULT nextval('work_item_parent_links_id_seq'::regclass);
 
+ALTER TABLE ONLY work_item_related_link_restrictions ALTER COLUMN id SET DEFAULT nextval('work_item_related_link_restrictions_id_seq'::regclass);
+
 ALTER TABLE ONLY work_item_types ALTER COLUMN id SET DEFAULT nextval('work_item_types_id_seq'::regclass);
 
 ALTER TABLE ONLY work_item_widget_definitions ALTER COLUMN id SET DEFAULT nextval('work_item_widget_definitions_id_seq'::regclass);
@@ -29570,6 +29588,9 @@ ALTER TABLE ONLY work_item_parent_links
 ALTER TABLE ONLY work_item_progresses
     ADD CONSTRAINT work_item_progresses_pkey PRIMARY KEY (issue_id);
 
+ALTER TABLE ONLY work_item_related_link_restrictions
+    ADD CONSTRAINT work_item_related_link_restrictions_pkey PRIMARY KEY (id);
+
 ALTER TABLE ONLY work_item_types
     ADD CONSTRAINT work_item_types_pkey PRIMARY KEY (id);
 
@@ -34641,10 +34662,14 @@ CREATE UNIQUE INDEX index_work_item_hierarchy_restrictions_on_parent_and_child O
 
 CREATE INDEX index_work_item_hierarchy_restrictions_on_parent_type_id ON work_item_hierarchy_restrictions USING btree (parent_type_id);
 
+CREATE UNIQUE INDEX index_work_item_link_restrictions_on_source_link_type_target ON work_item_related_link_restrictions USING btree (source_type_id, link_type, target_type_id);
+
 CREATE UNIQUE INDEX index_work_item_parent_links_on_work_item_id ON work_item_parent_links USING btree (work_item_id);
 
 CREATE INDEX index_work_item_parent_links_on_work_item_parent_id ON work_item_parent_links USING btree (work_item_parent_id);
 
+CREATE INDEX index_work_item_related_link_restrictions_on_target_type_id ON work_item_related_link_restrictions USING btree (target_type_id);
+
 CREATE INDEX index_work_item_types_on_base_type_and_id ON work_item_types USING btree (base_type, id);
 
 CREATE UNIQUE INDEX index_work_item_widget_definitions_on_default_witype_and_name ON work_item_widget_definitions USING btree (work_item_type_id, name) WHERE (namespace_id IS NULL);
@@ -38175,6 +38200,9 @@ ALTER TABLE ONLY merge_request_assignees
 ALTER TABLE ONLY packages_dependency_links
     ADD CONSTRAINT fk_rails_4437bf4070 FOREIGN KEY (dependency_id) REFERENCES packages_dependencies(id) ON DELETE CASCADE;
 
+ALTER TABLE ONLY work_item_related_link_restrictions
+    ADD CONSTRAINT fk_rails_4513f0061c FOREIGN KEY (target_type_id) REFERENCES work_item_types(id) ON DELETE CASCADE;
+
 ALTER TABLE ONLY project_auto_devops
     ADD CONSTRAINT fk_rails_45436b12b2 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
 
@@ -38925,6 +38953,9 @@ ALTER TABLE ONLY pool_repositories
 ALTER TABLE ONLY vulnerability_statistics
     ADD CONSTRAINT fk_rails_af61a7df4c FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
 
+ALTER TABLE ONLY work_item_related_link_restrictions
+    ADD CONSTRAINT fk_rails_b013a0fa65 FOREIGN KEY (source_type_id) REFERENCES work_item_types(id) ON DELETE CASCADE;
+
 ALTER TABLE ONLY resource_label_events
     ADD CONSTRAINT fk_rails_b126799f57 FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE SET NULL;
 
diff --git a/ee/app/models/concerns/ee/issuable_link.rb b/ee/app/models/concerns/ee/issuable_link.rb
index 07808dc9efe7a..3a89dfb3e9469 100644
--- a/ee/app/models/concerns/ee/issuable_link.rb
+++ b/ee/app/models/concerns/ee/issuable_link.rb
@@ -8,6 +8,7 @@ module IssuableLink
       # we don't store is_blocked_by in the db but need it for displaying the relation
       # from the target
       TYPE_IS_BLOCKED_BY = 'is_blocked_by'
+      TYPE_BLOCKS = ::Enums::IssuableLink::TYPE_BLOCKS
     end
 
     class_methods do
diff --git a/spec/factories/work_items/related_link_restrictions.rb b/spec/factories/work_items/related_link_restrictions.rb
new file mode 100644
index 0000000000000..b1e2547f00428
--- /dev/null
+++ b/spec/factories/work_items/related_link_restrictions.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+  factory :related_link_restriction, class: 'WorkItems::RelatedLinkRestriction' do
+    source_type { association :work_item_type, :default }
+    target_type { association :work_item_type, :default }
+    link_type { 0 }
+  end
+end
diff --git a/spec/models/work_items/related_link_restriction_spec.rb b/spec/models/work_items/related_link_restriction_spec.rb
new file mode 100644
index 0000000000000..1dc2286c0bf61
--- /dev/null
+++ b/spec/models/work_items/related_link_restriction_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::RelatedLinkRestriction, feature_category: :portfolio_management do
+  describe 'associations' do
+    it { is_expected.to belong_to(:source_type) }
+    it { is_expected.to belong_to(:target_type) }
+  end
+
+  describe 'validations' do
+    subject { build(:related_link_restriction) }
+
+    it { is_expected.to validate_presence_of(:source_type) }
+    it { is_expected.to validate_presence_of(:target_type) }
+    it { is_expected.to validate_uniqueness_of(:target_type).scoped_to([:source_type_id, :link_type]) }
+  end
+
+  describe '.link_type' do
+    it { is_expected.to define_enum_for(:link_type).with_values(relates_to: 0, blocks: 1) }
+  end
+end
-- 
GitLab