Refactor Provider with indexed cache and CachedType delegator
What does this MR do?
Extends the work item types Provider with an id-indexed cache and a CachedType delegator class. This is the foundation for resolving custom work item types through the same Provider interface used for system-defined types.
Contributes to
- [BE] Return custom work item types through the ... (#590696 - closed)
- [BE] Create items of custom type (#581940)
- [WS6] Cascading work item type visibility setting (gitlab-org#20061)
CE Provider refactor
All public methods now delegate to two private methods: resolve_by_id and resolve_all. This gives EE a single pair of override points instead of needing to override every public method individually.
EE Provider: CachedType + indexed cache
- CachedType (SimpleDelegator) wraps types with an enabled attribute
- Indexed cache keyed by type ID, built per-request via SafeRequestStore
- resolve_by_id checks the cache first, falls back to CE super
- resolve_all returns all cached types (system-defined + custom)
- Identity method overrides (class, is_a?, instance_of?) keep CachedType transparent to FixedItemsModel equality checks
Why CachedType?
SystemDefined::Type instances are singletons (FixedItemsModel) -- the same Ruby object is returned every call. Adding state directly would leak across requests. CachedType wraps per-request so each request gets its own enabled state.
Spec boundary fix
Custom type finder specs moved from CE to ee/spec. The work_item_custom_type factory and Custom::Type model are EE-only and would fail in FOSS pipelines.
How to verify locally
Rails console smoke test:
group = Group.first
provider = WorkItems::TypesFramework::Provider.new(group)
# Flag off -- raw types, no wrapping
Feature.disable(:work_item_configurable_types)
type = provider.find_by_id(1)
type.class
# => WorkItems::TypesFramework::SystemDefined::Type
type.respond_to?(:enabled)
# => false
# Flag on -- CachedType wrapping
Feature.enable(:work_item_configurable_types, group)
RequestStore.clear!
type = provider.find_by_id(1)
type.class
# => WorkItems::TypesFramework::SystemDefined::Type (transparent)
type.respond_to?(:enabled)
# => true
type.enabled
# => true
# Equality still holds
raw = WorkItems::TypesFramework::SystemDefined::Type.find_by(id: 1)
type == raw
# => true
# All types sorted
provider.all_ordered_by_name.map(&:name)
# find_by_base_type and default_issue_type go through the cache
provider.find_by_base_type(:issue).respond_to?(:enabled)
# => true
provider.default_issue_type.respond_to?(:enabled)
# => true
# Nil namespace is safe
WorkItems::TypesFramework::Provider.new(nil).default_issue_type.name
# => "Issue"
Creating a custom type to test with
In a GDK rails console, FactoryBot is available:
group = Group.first
custom_type = FactoryBot.create(:work_item_custom_type, namespace: group, name: 'Bug Report')
custom_type.id # => 1001+
custom_type.base_type # => "issue" (delegates to Issue system type)
Or directly via the model:
group = Group.first
custom_type = WorkItems::TypesFramework::Custom::Type.create!(
name: 'Bug Report',
icon_name: 'bug',
namespace: group
)
Then verify the Provider picks it up:
Feature.enable(:work_item_configurable_types, group)
RequestStore.clear!
provider = WorkItems::TypesFramework::Provider.new(group)
# Custom type resolves by its own ID
provider.find_by_id(custom_type.id)
# => #<EE::WorkItems::TypesFramework::Provider::CachedType ...>
# Custom type resolves by name
provider.find_by_name('Bug Report')
# Custom type appears in the full list (no duplicates)
provider.all_ordered_by_name.map(&:name)
# => [..., "Bug Report", ..., "Issue", ...]
# No Custom type appears when filtering by base_type
provider.by_base_types_ordered_by_name([:issue]).map(&:name)
# => ["Issue"]
# find_by_base_type returns the system type (no conversion here)
provider.find_by_base_type(:issue).name
# => "Issue"
# Flag off -- custom type invisible
Feature.disable(:work_item_configurable_types)
RequestStore.clear!
provider = WorkItems::TypesFramework::Provider.new(group)
provider.find_by_id(custom_type.id)
# => nil
Converted types (system type replaced by custom type)
group = Group.first
Feature.enable(:work_item_configurable_types, group)
# Convert the Incident system type (id: 2) to a custom type
converted = FactoryBot.create(:work_item_custom_type, :converted_from_incident,
namespace: group, name: 'Production Incident')
RequestStore.clear!
provider = WorkItems::TypesFramework::Provider.new(group)
# Converted type is indexed under the system type ID it replaced
provider.find_by_id(2).name
# => "Production Incident" (not "Incident")
# find_by_base_type returns the converted type
provider.find_by_base_type(:incident).name
# => "Production Incident"
# The converted type's own custom ID is NOT in the cache
# (callers use the system type ID / GID, matching Custom::Type#to_global_id)
provider.find_by_id(converted.id)
# => nil
# No duplicates in all -- converted type replaces the system type
provider.all_ordered_by_name.select { |t| t.base_type == 'incident' }.map(&:name)
# => ["Production Incident"]
Things to check:
- Feature flag off: all methods return raw system types, no enabled attribute
- Feature flag on, no custom types: system types wrapped with enabled: true
- Feature flag on, with custom types: custom types appear in find_by_id, find_by_name, all_ordered_by_name, by_base_types_ordered_by_name
- CachedType transparency: == between wrapped and unwrapped holds
- UserNamespace: Provider falls through to CE (no cache built)
- Nil namespace: Provider.new(nil).default_issue_type works (no crash)
- by_base_types_ordered_by_name(:issue): includes non-converted custom types
- find_by_base_type returns the converted type when one exists
- Converted types indexed under system type ID only -- no duplicates in all
- find_by_id(converted.id) returns nil (use the system type ID instead)
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.