diff --git a/server/app/graphql/mutations/create_fusion_feature.rb b/server/app/graphql/mutations/create_fusion_feature.rb index 142422af1..79fc96303 100644 --- a/server/app/graphql/mutations/create_fusion_feature.rb +++ b/server/app/graphql/mutations/create_fusion_feature.rb @@ -36,14 +36,22 @@ def ready?(organization_id: nil, five_prime_gene:, three_prime_gene:,**kwargs) #check that partner status matches gene_id presence [five_prime_gene, three_prime_gene].each do |gene_input| - if gene_input.gene_id.present? && gene_input.partner_status != 'known' - raise GraphQL::ExecutionError, "Partner status needs to be 'known' if a gene_id is set" + if gene_input.gene_id.present? && (gene_input.partner_status != 'known' || gene_input.partner_status != 'regulatory') + raise GraphQL::ExecutionError, "Partner status needs to be 'known' or 'regulatory' if a gene_id is set" end - if gene_input.gene_id.blank? && gene_input.partner_status == 'known' - raise GraphQL::ExecutionError, "Partner status can't be 'known' if a gene_id is not set" + if gene_input.gene_id.blank? && (gene_input.partner_status == 'known' && gene_input.partner_status != 'regulatory') + raise GraphQL::ExecutionError, "Partner status can't be 'known' or 'regulatory' if a gene_id is not set" end end + #check that maximuim one gene has regulatory_fusion_type set + if five_prime_gene.partner_status == 'regulatory' && three_prime_gene.partner_status == 'regulatory' + raise GraphQL::ExecutionError, "Only one Fusion partner can be marked 'regulatory'" + end + if five_prime_gene.regulatory_fusion_type.present? && three_prime_gene.regulatory_fusion_type.present? + raise GraphQL::ExecutionError, "Only one Fusion partner can have a regulatory fusion type set." + end + return true end @@ -54,12 +62,16 @@ def authorized?(organization_id: nil, **kwargs) def resolve(five_prime_gene:, three_prime_gene:, organization_id: nil) + #only one can be set + regulatory_fusion_type = five_prime_partner_status.regulatory_fusion_type || three_prime_partner_status.regulatory_fusion_type + existing_feature_instance = Features::Fusion .find_by( five_prime_gene_id: five_prime_gene.gene_id, three_prime_gene_id: three_prime_gene.gene_id, five_prime_partner_status: five_prime_gene.partner_status, three_prime_partner_status: three_prime_gene.partner_status, + regulatory_fusion_type: regulatory_fusion_type ) if existing_feature_instance.present? @@ -74,6 +86,7 @@ def resolve(five_prime_gene:, three_prime_gene:, organization_id: nil) three_prime_gene_id: three_prime_gene.gene_id, five_prime_partner_status: five_prime_gene.partner_status, three_prime_partner_status: three_prime_gene.partner_status, + regulatory_fusion_type: regulatory_fusion_type, originating_user: context[:current_user], organization_id: organization_id, ) diff --git a/server/app/graphql/types/entities/fusion_type.rb b/server/app/graphql/types/entities/fusion_type.rb index f47507f0c..97c5c6021 100644 --- a/server/app/graphql/types/entities/fusion_type.rb +++ b/server/app/graphql/types/entities/fusion_type.rb @@ -7,6 +7,8 @@ class FusionType < Types::Entities::FeatureType field :five_prime_partner_status, Types::Fusion::FusionPartnerStatus, null: false field :three_prime_partner_status, Types::Fusion::FusionPartnerStatus, null: false + field :regulatory_fusion_type, Types::Fusion::RegulatoryFusionTypeType, null: true + def five_prime_gene Loaders::AssociationLoader.for(Features::Fusion, :five_prime_gene).load(object) end diff --git a/server/app/graphql/types/fusion/fusion_partner_input_type.rb b/server/app/graphql/types/fusion/fusion_partner_input_type.rb index 026f59e73..dd09084fb 100644 --- a/server/app/graphql/types/fusion/fusion_partner_input_type.rb +++ b/server/app/graphql/types/fusion/fusion_partner_input_type.rb @@ -6,5 +6,7 @@ class FusionPartnerInputType < Types::BaseInputObject description: 'The status of the fusion partner' argument :gene_id, Int, required: false, description: 'The CIViC gene ID of the partner, if known' + argument :regulatory_fusion_type, Types::Fusion::RegulatoryFusionTypeType, required: false, + description: "If the fusion partner status is set to regulatory, what type of regulatory fusion is it?" end end diff --git a/server/app/graphql/types/fusion/fusion_partner_status.rb b/server/app/graphql/types/fusion/fusion_partner_status.rb index af4e7d6ab..2c081d4a9 100644 --- a/server/app/graphql/types/fusion/fusion_partner_status.rb +++ b/server/app/graphql/types/fusion/fusion_partner_status.rb @@ -3,5 +3,6 @@ class FusionPartnerStatus < Types::BaseEnum value 'KNOWN', value: 'known' value 'UNKNOWN', value: 'unknown' value 'MULTIPLE', value: 'multiple' + value 'REGULATORY', value: 'regulatory' end end diff --git a/server/app/graphql/types/fusion/regulatory_fusion_type_type.rb b/server/app/graphql/types/fusion/regulatory_fusion_type_type.rb new file mode 100644 index 000000000..ada414318 --- /dev/null +++ b/server/app/graphql/types/fusion/regulatory_fusion_type_type.rb @@ -0,0 +1,7 @@ +module Types::Fusion + class RegulatoryFusionTypeType < Types::BaseEnum + Constants::REGULATORY_FUSION_ENUM_TYPES.each do |(name, _)| + value name.upcase, value: name + end + end +end diff --git a/server/app/models/actions/create_fusion_feature.rb b/server/app/models/actions/create_fusion_feature.rb index 5ee6e8c7e..2391fe6a6 100644 --- a/server/app/models/actions/create_fusion_feature.rb +++ b/server/app/models/actions/create_fusion_feature.rb @@ -2,9 +2,9 @@ module Actions class CreateFusionFeature include Actions::Transactional - attr_reader :feature, :originating_user, :organization_id, :create_variant, :five_prime_partner_status, :three_prime_partner_status + attr_reader :feature, :originating_user, :organization_id, :create_variant, :five_prime_partner_status, :three_prime_partner_status, :regulatory_fusion_type - def initialize(originating_user:, five_prime_gene_id:, three_prime_gene_id:, five_prime_partner_status:, three_prime_partner_status:, organization_id: nil, create_variant: true) + def initialize(originating_user:, five_prime_gene_id:, three_prime_gene_id:, five_prime_partner_status:, three_prime_partner_status:, regulatory_fusion_type:, organization_id: nil, create_variant: true) feature_name = "#{construct_fusion_partner_name(five_prime_gene_id, five_prime_partner_status)}::#{construct_fusion_partner_name(three_prime_gene_id, three_prime_partner_status)}" @feature = Feature.new( name: feature_name, @@ -14,10 +14,12 @@ def initialize(originating_user:, five_prime_gene_id:, three_prime_gene_id:, fiv three_prime_gene_id: three_prime_gene_id, five_prime_partner_status: five_prime_partner_status, three_prime_partner_status: three_prime_partner_status, + regulatory_fusion_type: regulatory_fusion_type, feature: feature, ) @five_prime_partner_status = five_prime_partner_status @three_prime_partner_status = three_prime_partner_status + @regulatory_fusion_type = regulatory_fusion_type @originating_user = originating_user @organization_id = organization_id @create_variant = create_variant @@ -26,6 +28,10 @@ def initialize(originating_user:, five_prime_gene_id:, three_prime_gene_id:, fiv def construct_fusion_partner_name(gene_id, partner_status) if partner_status == 'known' Features::Gene.find(gene_id).name + elseif partner_status == 'regulatory' + gene_name = Features::Gene.find(gene_id).name + rft = Features::Fusion.format_regulatory_fusion_type(regulatory_fusion_type) + "#{rft}@#{gene_name}" elsif partner_status == 'unknown' '?' elsif partner_status == 'multiple' diff --git a/server/app/models/activities/create_fusion_feature.rb b/server/app/models/activities/create_fusion_feature.rb index 1aa497f5b..6f780a69b 100644 --- a/server/app/models/activities/create_fusion_feature.rb +++ b/server/app/models/activities/create_fusion_feature.rb @@ -1,13 +1,14 @@ module Activities class CreateFusionFeature < Base - attr_reader :feature, :five_prime_gene_id, :three_prime_gene_id, :five_prime_partner_status, :three_prime_partner_status, :create_variant + attr_reader :feature, :five_prime_gene_id, :three_prime_gene_id, :five_prime_partner_status, :three_prime_partner_status, :create_variant, :regulatory_fusion_type - def initialize(originating_user:, organization_id:, five_prime_gene_id:, three_prime_gene_id:, five_prime_partner_status:, three_prime_partner_status:, create_variant: true) + def initialize(originating_user:, organization_id:, five_prime_gene_id:, three_prime_gene_id:, five_prime_partner_status:, three_prime_partner_status:, regulatory_fusion_type:, create_variant: true) super(organization_id: organization_id, user: originating_user) @five_prime_gene_id = five_prime_gene_id @three_prime_gene_id = three_prime_gene_id @five_prime_partner_status = five_prime_partner_status @three_prime_partner_status = three_prime_partner_status + @regulatory_fusion_type = regulatory_fusion_type @create_variant = create_variant end @@ -25,6 +26,7 @@ def call_actions three_prime_gene_id: three_prime_gene_id, five_prime_partner_status: five_prime_partner_status, three_prime_partner_status: three_prime_partner_status, + regulatory_fusion_type: regulatory_fusion_type, originating_user: user, organization_id: organization&.id, create_variant: create_variant diff --git a/server/app/models/constants.rb b/server/app/models/constants.rb index 7ee1161b2..afd57fe03 100644 --- a/server/app/models/constants.rb +++ b/server/app/models/constants.rb @@ -138,4 +138,38 @@ module Constants ENSEMBL_TRANSCRIPT_ID_FORMAT = /\AENST\d{11}\.\d{1,2}\z/ REPRESENTATIVE_FUSION_VARIANT_NAME = 'Fusion' + + # INSDC regulatory class vocabulary as required here: https://fusions.cancervariants.org/en/latest/nomenclature.html#regulatory-nomenclature + REGULATORY_FUSION_TYPES = [ + ["attenuator", "SO:0000140"], + ["CAAT_signal", "SO:0000172"], + ["DNase_I_hypersensitive_site", "SO:0000685"], + ["enhancer", "SO:0000165"], + ["enhancer_blocking_element", nil], + ["GC_signal", "SO:0000173"], + ["imprinting_control_region", nil], + ["insulator", "SO:0000627"], + ["locus_control_region", "SO:0000037"], + ["matrix_attachment_region", "SO:0000036"], + ["minus_35_signal", "SO:0000176"], + ["minus_10_signal", "SO:0000175"], + ["polyA_signal_sequence", "SO:0000551"], + ["promoter", "SO:0000167"], + ["recoding_stimulatory_region", "SO:1001268"], + ["recombination_enhancer", "SO:0002059"], + ["replication_regulatory_region", "SO:0001682"], + ["response_element", nil], + ["ribosome_binding_site", "SO:0000552"], + ["riboswitch", "SO:0000035"], + ["silencer", "SO:0000625"], + ["TATA_box", "SO:0000174"], + ["terminator", "SO:0000141"], + ["transcriptional_cis_regulatory_region", "SO:0001055"], + ["uORF", "SO:0002027"], + ["other", nil] + ].map { |(type, soid)| ["reg_#{type}", soid] } + + REGULATORY_FUSION_ENUM_TYPES = REGULATORY_FUSION_TYPES.map { |(type, _)| [type, type] }.to_h + + FUSION_PARTNER_STATUSES = [ 'known', 'unknown', 'multiple', 'regulatory' ].map { [_1, _1] }.to_h end diff --git a/server/app/models/features/fusion.rb b/server/app/models/features/fusion.rb index f8d3cdcdc..34201c6fa 100644 --- a/server/app/models/features/fusion.rb +++ b/server/app/models/features/fusion.rb @@ -6,17 +6,10 @@ class Fusion < ActiveRecord::Base belongs_to :five_prime_gene, class_name: 'Features::Gene', optional: true belongs_to :three_prime_gene, class_name: 'Features::Gene', optional: true - enum five_prime_partner_status: { - known: 'known', - unknown: 'unknown', - multiple: 'multiple', - }, _prefix: true + enum five_prime_partner_status: Constants::FUSION_PARTNER_STATUSES, _prefix: true + enum three_prime_partner_status: Constants::FUSION_PARTNER_STATUSES, _prefix: true - enum three_prime_partner_status: { - known: 'known', - unknown: 'unknown', - multiple: 'multiple', - }, _prefix: true + enum regulatory_fusion_type: Constants::REGULATORY_FUSION_ENUM_TYPES has_many :variant_groups has_many :source_suggestions @@ -24,31 +17,22 @@ class Fusion < ActiveRecord::Base #TODO - move to feature? has_many :comment_mentions, foreign_key: :comment_id, class_name: 'EntityMention' - validate :partner_status_valid_for_gene_ids - validate :at_least_one_gene_id + validates_with FusionFeatureValidator - def partner_status_valid_for_gene_ids - if !self.in_revision_validation_context - [self.five_prime_gene, self.three_prime_gene].zip([self.five_prime_partner_status, self.three_prime_partner_status], [:five_prime_gene, :three_prime_gene]).each do |gene, status, fk| - if gene.nil? && status == 'known' - errors.add(fk, "Partner status cannot be 'known' if the gene isn't set") - elsif !gene.nil? && status != 'known' - errors.add(fk, "Partner status has to be 'known' if gene is set") - end - end - end + def display_name + name end - def at_least_one_gene_id - if !self.in_revision_validation_context && self.five_prime_gene_id.nil? && self.three_prime_gene_id.nil? - errors.add(:base, "One or both of the genes need to be set") + def self.format_regulatory_fusion_type(rft) + if rft == 'reg_enhancer' + 'reg_e' + elsif rft == 'reg_promoter' + 'reg_p' + else + rft end end - def display_name - name - end - def editable_fields [ :description, diff --git a/server/app/models/variant_type.rb b/server/app/models/variant_type.rb index fe029b4ef..544ebe15f 100644 --- a/server/app/models/variant_type.rb +++ b/server/app/models/variant_type.rb @@ -3,6 +3,7 @@ class VariantType < ActiveRecord::Base has_and_belongs_to_many :variants has_and_belongs_to_many :pipeline_types + enum regulatory_fusion_type: Constants::REGULATORY_FUSION_ENUM_TYPES def url if self.soid != "N/A" diff --git a/server/db/migrate/20250102170055_add_regulatory_fusion_types_enum.rb b/server/db/migrate/20250102170055_add_regulatory_fusion_types_enum.rb new file mode 100644 index 000000000..36fd399ce --- /dev/null +++ b/server/db/migrate/20250102170055_add_regulatory_fusion_types_enum.rb @@ -0,0 +1,26 @@ +class AddRegulatoryFusionTypesEnum < ActiveRecord::Migration[7.1] + def up + create_enum :regulatory_fusion_types, Constants::REGULATORY_FUSION_TYPES.map(&:first) + add_enum_value :fusion_partner_status, "regulatory" + + add_column :variant_types, :regulatory_fusion_type, :enum, enum_type: :regulatory_fusion_types, null: true + add_column :fusions, :regulatory_fusion_type, :enum, enum_type: :regulatory_fusion_types, null: true + add_index :variant_types, :regulatory_fusion_type + + Constants::REGULATORY_FUSION_TYPES.each do |(type, soid)| + if soid.present? + vt = VariantType.find_by!(soid: soid) + vt.regulatory_fusion_type = type + vt.save! + end + end + end + + def down + remove_column :variant_types, :regulatory_fusion_type + remove_column :fusions, :regulatory_fusion_type + execute <<-SQL + DROP TYPE regulatory_fusion_types; + SQL + end +end diff --git a/server/db/schema.rb b/server/db/schema.rb index 761d1e078..5574c3c4c 100644 --- a/server/db/schema.rb +++ b/server/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_09_14_154057) do +ActiveRecord::Schema[7.1].define(version: 2025_01_02_170055) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -18,7 +18,9 @@ # Note that some types may not work with other database engines. Be careful if changing database. create_enum "exon_coordinate_record_state", ["stub", "exons_provided", "fully_curated"] create_enum "exon_offset_direction", ["positive", "negative"] - create_enum "fusion_partner_status", ["known", "unknown", "multiple"] + create_enum "fusion_partner_status", ["known", "unknown", "multiple", "regulatory"] + create_enum "regulatory_fusion_partner", ["five_prime", "three_prime"] + create_enum "regulatory_fusion_types", ["reg_attenuator", "reg_CAAT_signal", "reg_DNase_I_hypersensitive_site", "reg_enhancer", "reg_enhancer_blocking_element", "reg_GC_signal", "reg_imprinting_control_region", "reg_insulator", "reg_locus_control_region", "reg_matrix_attachment_region", "reg_minus_35_signal", "reg_minus_10_signal", "reg_polyA_signal_sequence", "reg_promoter", "reg_recoding_stimulatory_region", "reg_recombination_enhancer", "reg_replication_regulatory_region", "reg_response_element", "reg_ribosome_binding_site", "reg_riboswitch", "reg_silencer", "reg_TATA_box", "reg_terminator", "reg_transcriptional_cis_regulatory_region", "reg_uORF", "reg_other"] create_enum "variant_coordinate_record_state", ["stub", "fully_curated"] create_table "acmg_codes", id: :serial, force: :cascade do |t| @@ -106,6 +108,20 @@ t.index ["user_id"], name: "index_affiliations_on_user_id" end + create_table "api_keys", force: :cascade do |t| + t.string "bearer_type" + t.bigint "bearer_id" + t.text "token_prefix", null: false + t.text "token_suffix", null: false + t.text "token_digest", null: false + t.boolean "revoked", default: false, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["bearer_type", "bearer_id"], name: "index_api_keys_on_bearer" + t.index ["revoked"], name: "index_api_keys_on_revoked" + t.index ["token_digest"], name: "index_api_keys_on_token_digest", unique: true + end + create_table "assertions", id: :serial, force: :cascade do |t| t.text "description" t.datetime "created_at", precision: nil @@ -536,6 +552,7 @@ t.enum "three_prime_partner_status", default: "unknown", null: false, enum_type: "fusion_partner_status" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.enum "regulatory_fusion_type", enum_type: "regulatory_fusion_types" t.index ["five_prime_gene_id"], name: "index_fusions_on_five_prime_gene_id" t.index ["three_prime_gene_id"], name: "index_fusions_on_three_prime_gene_id" end @@ -963,8 +980,10 @@ t.integer "parent_id" t.integer "lft" t.integer "rgt" + t.enum "regulatory_fusion_type", enum_type: "regulatory_fusion_types" t.index ["display_name"], name: "index_variant_types_on_display_name" t.index ["name"], name: "index_variant_types_on_name" + t.index ["regulatory_fusion_type"], name: "index_variant_types_on_regulatory_fusion_type" t.index ["soid"], name: "index_variant_types_on_soid" end diff --git a/server/test/fixtures/feature_fusions.yml b/server/test/fixtures/feature_fusions.yml new file mode 100644 index 000000000..ea16356f1 --- /dev/null +++ b/server/test/fixtures/feature_fusions.yml @@ -0,0 +1,27 @@ +braf_vhl_fusion: + five_prime_gene: braf + three_prime_gene: vhl + five_prime_partner_status: known + three_prime_partner_status: known + regulatory_fusion_type: reg_enhancer + +braf_fusion: + five_prime_gene: braf + three_prime_gene: null + five_prime_partner_status: known + three_prime_partner_status: unknown + regulatory_fusion_type: null + +vhl_fusion: + five_prime_gene: null + three_prime_gene: vhl + five_prime_partner_status: unknown + three_prime_partner_status: known + regulatory_fusion_type: null + +regulatory_fusion: + five_prime_gene: braf + three_prime_gene: vhl + five_prime_partner_status: regulatory + three_prime_partner_status: known + regulatory_fusion_type: reg_enhancer diff --git a/server/test/fixtures/features.yml b/server/test/fixtures/features.yml index 86ffe0e40..816423435 100644 --- a/server/test/fixtures/features.yml +++ b/server/test/fixtures/features.yml @@ -13,3 +13,8 @@ msi: full_name: Microsatellite Instability description: The description for the MSI factor feature_instance: msi (Features::Factor) + +braf_fusion: + name: BRAF::? + description: BRAF and unknown partner + feature_instance: braf_fusion (Features::Fusion) diff --git a/server/test/models/fusion_feature_test.rb b/server/test/models/fusion_feature_test.rb new file mode 100644 index 000000000..fa2baf60d --- /dev/null +++ b/server/test/models/fusion_feature_test.rb @@ -0,0 +1,47 @@ +require "test_helper" + +class FusionFeatureTest < ActiveSupport::TestCase + setup do + feature = features(:braf_fusion) + @braf_vhl_fusion = feature_fusions(:braf_vhl_fusion) + @braf_vhl_fusion.feature = feature + @braf_fusion = feature_fusions(:braf_fusion) + @braf_fusion.feature = feature + @vhl_fusion = feature_fusions(:vhl_fusion) + @vhl_fusion.feature = feature + @regulatory_fusion = feature_fusions(:regulatory_fusion) + @regulatory_fusion.feature = feature + end + + test "valid fusion with both genes set and known partner status" do + assert @braf_vhl_fusion.valid? + end + + test "invalid fusion with one gene set and known partner status" do + @braf_fusion.three_prime_partner_status = 'known' + assert_not @braf_fusion.valid? + assert_includes @braf_fusion.errors[:three_prime_gene], "Partner status cannot be 'known' or 'regulatory' if the gene isn't set" + end + + test "invalid fusion with both genes unset" do + @braf_fusion.five_prime_gene = nil + assert_not @braf_fusion.valid? + assert_includes @braf_fusion.errors[:base], "One or both of the genes need to be set" + end + + test "invalid fusion with both partners marked as regulatory" do + @regulatory_fusion.three_prime_partner_status = 'regulatory' + assert_not @regulatory_fusion.valid? + assert_includes @regulatory_fusion.errors[:base], "Only one fusion partner may be marked as regulatory" + end + + test "invalid fusion with regulatory partner but no regulatory fusion type" do + @regulatory_fusion.regulatory_fusion_type = nil + assert_not @regulatory_fusion.valid? + assert_includes @regulatory_fusion.errors[:regulatory_fusion_type], "You must select a regulatory fusion type if one of the fusion partners is marked as regulatory" + end + + test "valid fusion with regulatory partner and regulatory fusion type" do + assert @regulatory_fusion.valid? + end +end diff --git a/server/test/test_helper.rb b/server/test/test_helper.rb index b96f9cff2..cfe56486c 100644 --- a/server/test/test_helper.rb +++ b/server/test/test_helper.rb @@ -8,6 +8,7 @@ class ActiveSupport::TestCase set_fixture_class feature_genes: Features::Gene set_fixture_class feature_factors: Features::Factor + set_fixture_class feature_fusions: Features::Fusion # Add more helper methods to be used by all tests here... end