merge next-main, keep both modifier associations and orphan handling

This commit is contained in:
Justin Edmund 2025-12-31 22:16:08 -08:00
commit 4430331da9
19 changed files with 465 additions and 21 deletions

View file

@ -3,7 +3,7 @@
module Api
module V1
class GridArtifactBlueprint < ApiBlueprint
fields :level, :reroll_slot
fields :level, :reroll_slot, :orphaned
field :collection_artifact_id
field :out_of_sync, if: ->(_field, ga, _options) { ga.collection_artifact_id.present? } do |ga|

View file

@ -3,7 +3,7 @@
module Api
module V1
class GridSummonBlueprint < ApiBlueprint
fields :main, :friend, :position, :quick_summon, :uncap_level, :transcendence_step
fields :main, :friend, :position, :quick_summon, :uncap_level, :transcendence_step, :orphaned
field :collection_summon_id
field :out_of_sync, if: ->(_field, gs, _options) { gs.collection_summon_id.present? } do |gs|

View file

@ -3,7 +3,7 @@
module Api
module V1
class GridWeaponBlueprint < ApiBlueprint
fields :mainhand, :position, :uncap_level, :transcendence_step, :element
fields :mainhand, :position, :uncap_level, :transcendence_step, :element, :orphaned
field :collection_weapon_id
field :out_of_sync, if: ->(_field, gw, _options) { gw.collection_weapon_id.present? } do |gw|

View file

@ -36,6 +36,10 @@ module Api
end
end
field :has_orphaned_items do |party|
party.has_orphaned_items?
end
# For collection views
view :preview do
include_view :preview_objects # Characters, Weapons, Summons

View file

@ -9,7 +9,7 @@ module Api
before_action :set_collection_artifact_for_read, only: %i[show]
# Write actions: require auth, use current_user
before_action :restrict_access, only: %i[create update destroy batch batch_destroy import]
before_action :restrict_access, only: %i[create update destroy batch batch_destroy import preview_sync]
before_action :set_collection_artifact_for_write, only: %i[update destroy]
def index
@ -109,6 +109,8 @@ module Api
#
# @param data [Hash] Game data containing artifact list
# @param update_existing [Boolean] Whether to update existing artifacts (default: false)
# @param is_full_inventory [Boolean] Whether this represents the user's complete inventory (default: false)
# @param reconcile_deletions [Boolean] Whether to delete items not in the import (default: false)
def import
game_data = import_params[:data]
@ -119,7 +121,9 @@ module Api
service = ArtifactImportService.new(
current_user,
game_data,
update_existing: import_params[:update_existing] == true
update_existing: import_params[:update_existing] == true,
is_full_inventory: import_params[:is_full_inventory] == true,
reconcile_deletions: import_params[:reconcile_deletions] == true
)
result = service.import
@ -131,10 +135,41 @@ module Api
created: result.created&.size || 0,
updated: result.updated&.size || 0,
skipped: result.skipped&.size || 0,
errors: result.errors || []
errors: result.errors || [],
reconciliation: result.reconciliation
}, status: status
end
# POST /collection/artifacts/preview_sync
# Previews what would be deleted in a full sync operation
#
# @param data [Hash] Game data containing artifact list
# @return [JSON] List of items that would be deleted
def preview_sync
game_data = import_params[:data]
unless game_data.present?
return render json: { error: 'No data provided' }, status: :bad_request
end
service = ArtifactImportService.new(current_user, game_data)
items_to_delete = service.preview_deletions
render json: {
will_delete: items_to_delete.map do |ca|
{
id: ca.id,
game_id: ca.game_id,
name: ca.artifact&.name_en,
granblue_id: ca.artifact&.granblue_id,
element: ca.element,
level: ca.level
}
end,
count: items_to_delete.size
}
end
# DELETE /collection/artifacts/batch_destroy
# Deletes multiple collection artifacts in a single request
def batch_destroy
@ -197,6 +232,8 @@ module Api
def import_params
{
update_existing: params[:update_existing],
is_full_inventory: params[:is_full_inventory],
reconcile_deletions: params[:reconcile_deletions],
data: params[:data]&.to_unsafe_h
}
end

View file

@ -7,7 +7,7 @@ module Api
before_action :set_collection_summon_for_read, only: %i[show]
# Write actions: require auth, use current_user
before_action :restrict_access, only: %i[create update destroy batch batch_destroy import]
before_action :restrict_access, only: %i[create update destroy batch batch_destroy import preview_sync]
before_action :set_collection_summon_for_write, only: %i[update destroy]
def index
@ -113,6 +113,8 @@ module Api
#
# @param data [Hash] Game data containing summon list
# @param update_existing [Boolean] Whether to update existing summons (default: false)
# @param is_full_inventory [Boolean] Whether this represents the user's complete inventory (default: false)
# @param reconcile_deletions [Boolean] Whether to delete items not in the import (default: false)
def import
game_data = import_params[:data]
@ -123,7 +125,9 @@ module Api
service = SummonImportService.new(
current_user,
game_data,
update_existing: import_params[:update_existing] == true
update_existing: import_params[:update_existing] == true,
is_full_inventory: import_params[:is_full_inventory] == true,
reconcile_deletions: import_params[:reconcile_deletions] == true
)
result = service.import
@ -135,10 +139,41 @@ module Api
created: result.created.size,
updated: result.updated.size,
skipped: result.skipped.size,
errors: result.errors
errors: result.errors,
reconciliation: result.reconciliation
}, status: status
end
# POST /collection/summons/preview_sync
# Previews what would be deleted in a full sync operation
#
# @param data [Hash] Game data containing summon list
# @return [JSON] List of items that would be deleted
def preview_sync
game_data = import_params[:data]
unless game_data.present?
return render json: { error: 'No data provided' }, status: :bad_request
end
service = SummonImportService.new(current_user, game_data)
items_to_delete = service.preview_deletions
render json: {
will_delete: items_to_delete.map do |cs|
{
id: cs.id,
game_id: cs.game_id,
name: cs.summon&.name_en,
granblue_id: cs.summon&.granblue_id,
uncap_level: cs.uncap_level,
transcendence_step: cs.transcendence_step
}
end,
count: items_to_delete.size
}
end
private
def set_target_user
@ -181,6 +216,8 @@ module Api
def import_params
{
update_existing: params[:update_existing],
is_full_inventory: params[:is_full_inventory],
reconcile_deletions: params[:reconcile_deletions],
data: params[:data]&.to_unsafe_h
}
end

View file

@ -7,7 +7,7 @@ module Api
before_action :set_collection_weapon_for_read, only: %i[show]
# Write actions: require auth, use current_user
before_action :restrict_access, only: %i[create update destroy batch batch_destroy import]
before_action :restrict_access, only: %i[create update destroy batch batch_destroy import preview_sync]
before_action :set_collection_weapon_for_write, only: %i[update destroy]
def index
@ -121,6 +121,8 @@ module Api
#
# @param data [Hash] Game data containing weapon list
# @param update_existing [Boolean] Whether to update existing weapons (default: false)
# @param is_full_inventory [Boolean] Whether this represents the user's complete inventory (default: false)
# @param reconcile_deletions [Boolean] Whether to delete items not in the import (default: false)
def import
game_data = import_params[:data]
@ -131,7 +133,9 @@ module Api
service = WeaponImportService.new(
current_user,
game_data,
update_existing: import_params[:update_existing] == true
update_existing: import_params[:update_existing] == true,
is_full_inventory: import_params[:is_full_inventory] == true,
reconcile_deletions: import_params[:reconcile_deletions] == true
)
result = service.import
@ -143,10 +147,41 @@ module Api
created: result.created.size,
updated: result.updated.size,
skipped: result.skipped.size,
errors: result.errors
errors: result.errors,
reconciliation: result.reconciliation
}, status: status
end
# POST /collection/weapons/preview_sync
# Previews what would be deleted in a full sync operation
#
# @param data [Hash] Game data containing weapon list
# @return [JSON] List of items that would be deleted
def preview_sync
game_data = import_params[:data]
unless game_data.present?
return render json: { error: 'No data provided' }, status: :bad_request
end
service = WeaponImportService.new(current_user, game_data)
items_to_delete = service.preview_deletions
render json: {
will_delete: items_to_delete.map do |cw|
{
id: cw.id,
game_id: cw.game_id,
name: cw.weapon&.name_en,
granblue_id: cw.weapon&.granblue_id,
uncap_level: cw.uncap_level,
transcendence_step: cw.transcendence_step
}
end,
count: items_to_delete.size
}
end
private
def set_target_user
@ -199,6 +234,8 @@ module Api
def import_params
{
update_existing: params[:update_existing],
is_full_inventory: params[:is_full_inventory],
reconcile_deletions: params[:reconcile_deletions],
data: params[:data]&.to_unsafe_h
}
end

View file

@ -7,6 +7,10 @@ class CollectionArtifact < ApplicationRecord
belongs_to :user
belongs_to :artifact
has_many :grid_artifacts, dependent: :nullify
before_destroy :orphan_grid_items
# Enums - using GranblueEnums::ELEMENTS values (excluding Null)
# Wind: 1, Fire: 2, Water: 3, Earth: 4, Dark: 5, Light: 6
enum :element, {
@ -77,4 +81,12 @@ class CollectionArtifact < ApplicationRecord
def quirk_artifact?
artifact&.quirk?
end
##
# Marks all linked grid artifacts as orphaned before destroying this collection artifact.
#
# @return [void]
def orphan_grid_items
grid_artifacts.update_all(orphaned: true, collection_artifact_id: nil)
end
end

View file

@ -2,6 +2,10 @@ class CollectionSummon < ApplicationRecord
belongs_to :user
belongs_to :summon
has_many :grid_summons, dependent: :nullify
before_destroy :orphan_grid_items
validates :uncap_level, inclusion: { in: 0..5 }
validates :transcendence_step, inclusion: { in: 0..10 }
@ -31,4 +35,12 @@ class CollectionSummon < ApplicationRecord
errors.add(:transcendence_step, "not available for this summon")
end
end
##
# Marks all linked grid summons as orphaned before destroying this collection summon.
#
# @return [void]
def orphan_grid_items
grid_summons.update_all(orphaned: true, collection_summon_id: nil)
end
end

View file

@ -12,6 +12,10 @@ class CollectionWeapon < ApplicationRecord
belongs_to :ax_modifier2, class_name: 'WeaponStatModifier', optional: true
belongs_to :befoulment_modifier, class_name: 'WeaponStatModifier', optional: true
has_many :grid_weapons, dependent: :nullify
before_destroy :orphan_grid_items
# Set defaults before validation so database defaults don't cause validation failures
attribute :awakening_level, :integer, default: 1
@ -162,4 +166,12 @@ class CollectionWeapon < ApplicationRecord
errors.add(:transcendence_step, "not available for this weapon") if transcendence_step > 0
end
end
##
# Marks all linked grid weapons as orphaned before destroying this collection weapon.
#
# @return [void]
def orphan_grid_items
grid_weapons.update_all(orphaned: true, collection_weapon_id: nil)
end
end

View file

@ -11,6 +11,10 @@ class GridArtifact < ApplicationRecord
has_one :party, through: :grid_character
has_one :character, through: :grid_character
# Orphan status scopes
scope :orphaned, -> { where(orphaned: true) }
scope :not_orphaned, -> { where(orphaned: false) }
# Enums - using GranblueEnums::ELEMENTS values (excluding Null)
# Wind: 1, Fire: 2, Water: 3, Earth: 4, Dark: 5, Light: 6
enum :element, {
@ -55,6 +59,14 @@ class GridArtifact < ApplicationRecord
quirk_artifact? ? proficiency : artifact&.proficiency
end
##
# Marks this grid artifact as orphaned and clears its collection link.
#
# @return [Boolean] true if the update succeeded
def mark_orphaned!
update!(orphaned: true, collection_artifact_id: nil)
end
##
# Syncs customizations from the linked collection artifact.
#

View file

@ -19,6 +19,10 @@ class GridSummon < ApplicationRecord
belongs_to :collection_summon, optional: true
validates_presence_of :party
# Orphan status scopes
scope :orphaned, -> { where(orphaned: true) }
scope :not_orphaned, -> { where(orphaned: false) }
# Validate that position is provided.
validates :position, presence: true
validate :compatible_with_position, on: :create
@ -40,6 +44,14 @@ class GridSummon < ApplicationRecord
GridSummonBlueprint
end
##
# Marks this grid summon as orphaned and clears its collection link.
#
# @return [Boolean] true if the update succeeded
def mark_orphaned!
update!(orphaned: true, collection_summon_id: nil)
end
##
# Syncs customizations from the linked collection summon.
#

View file

@ -43,6 +43,10 @@ class GridWeapon < ApplicationRecord
belongs_to :ax_modifier2, class_name: 'WeaponStatModifier', optional: true
belongs_to :befoulment_modifier, class_name: 'WeaponStatModifier', optional: true
# Orphan status scopes
scope :orphaned, -> { where(orphaned: true) }
scope :not_orphaned, -> { where(orphaned: false) }
# Validate that uncap_level is present and numeric, transcendence_step is optional but must be numeric if present.
validates :uncap_level, presence: true, numericality: { only_integer: true }
validates :transcendence_step, numericality: { only_integer: true }, allow_nil: true
@ -100,6 +104,14 @@ class GridWeapon < ApplicationRecord
true
end
##
# Marks this grid weapon as orphaned and clears its collection link.
#
# @return [Boolean] true if the update succeeded
def mark_orphaned!
update!(orphaned: true, collection_weapon_id: nil)
end
##
# Checks if grid weapon is out of sync with collection.
#

View file

@ -274,6 +274,19 @@ class Party < ApplicationRecord
end
end
##
# Checks if the party has any orphaned grid items.
#
# An orphaned item is one whose linked collection item has been deleted,
# indicating the user no longer has that item in their game inventory.
#
# @return [Boolean] true if the party has orphaned weapons, summons, or artifacts.
def has_orphaned_items?
weapons.orphaned.exists? ||
summons.orphaned.exists? ||
characters.joins(:grid_artifact).where(grid_artifacts: { orphaned: true }).exists?
end
##
# Determines if the party meets the minimum requirements for preview generation.
#

View file

@ -12,7 +12,7 @@
# end
#
class ArtifactImportService
Result = Struct.new(:success?, :created, :updated, :skipped, :errors, keyword_init: true)
Result = Struct.new(:success?, :created, :updated, :skipped, :errors, :reconciliation, keyword_init: true)
# Game element values to our element enum values
# Game: 1=Fire, 2=Water, 3=Earth, 4=Wind, 5=Light, 6=Dark
@ -30,10 +30,38 @@ class ArtifactImportService
@user = user
@game_data = game_data
@update_existing = options[:update_existing] || false
@is_full_inventory = options[:is_full_inventory] || false
@reconcile_deletions = options[:reconcile_deletions] || false
@created = []
@updated = []
@skipped = []
@errors = []
@processed_game_ids = []
end
##
# Previews what would be deleted in a sync operation.
# Does not modify any data, just returns items that would be removed.
#
# @return [Array<CollectionArtifact>] Collection artifacts that would be deleted
def preview_deletions
items = extract_items
return [] if items.empty?
# Extract all game_ids from the import data
# Artifacts use 'id' directly (not nested in 'param')
game_ids = items.filter_map do |item|
data = item.is_a?(Hash) ? item.with_indifferent_access : item
data['id'].to_s if data['id'].present?
end
return [] if game_ids.empty?
# Find collection artifacts with game_ids NOT in the import
@user.collection_artifacts
.includes(:artifact)
.where.not(game_id: nil)
.where.not(game_id: game_ids)
end
##
@ -43,7 +71,14 @@ class ArtifactImportService
def import
items = extract_items
if items.empty?
return Result.new(success?: false, created: [], updated: [], skipped: [], errors: ['No artifact items found in data'])
return Result.new(
success?: false,
created: [],
updated: [],
skipped: [],
errors: ['No artifact items found in data'],
reconciliation: nil
)
end
# Preload artifacts and existing collection artifacts to avoid N+1 queries
@ -58,12 +93,19 @@ class ArtifactImportService
end
end
# Handle deletion reconciliation if requested
reconciliation_result = nil
if @reconcile_deletions && @is_full_inventory && @processed_game_ids.any?
reconciliation_result = reconcile_deletions
end
Result.new(
success?: @errors.empty?,
created: @created,
updated: @updated,
skipped: @skipped,
errors: @errors
errors: @errors,
reconciliation: reconciliation_result
)
end
@ -92,6 +134,10 @@ class ArtifactImportService
# Handle both string and symbol keys from params
data = item.is_a?(Hash) ? item.with_indifferent_access : item
# Track this game_id as processed (for reconciliation)
game_id = data['id']
@processed_game_ids << game_id.to_s if game_id.present?
artifact = find_artifact(data['artifact_id'])
unless artifact
@errors << { game_id: data['id'], artifact_id: data['artifact_id'], error: 'Artifact not found' }
@ -193,4 +239,34 @@ class ArtifactImportService
'level' => level.to_i
}
end
##
# Reconciles deletions by removing collection artifacts not in the processed list.
# Only called when @is_full_inventory and @reconcile_deletions are both true.
#
# @return [Hash] Reconciliation result with deleted count and orphaned grid item IDs
def reconcile_deletions
# Find collection artifacts with game_ids NOT in our processed list
missing = @user.collection_artifacts
.where.not(game_id: nil)
.where.not(game_id: @processed_game_ids)
deleted_count = 0
orphaned_grid_item_ids = []
missing.find_each do |coll_artifact|
# Collect IDs of grid items that will be orphaned
grid_artifact_ids = GridArtifact.where(collection_artifact_id: coll_artifact.id).pluck(:id)
orphaned_grid_item_ids.concat(grid_artifact_ids)
# The before_destroy callback on CollectionArtifact will mark grid items as orphaned
coll_artifact.destroy
deleted_count += 1
end
{
deleted: deleted_count,
orphaned_grid_items: orphaned_grid_item_ids
}
end
end

View file

@ -12,16 +12,43 @@
# end
#
class SummonImportService
Result = Struct.new(:success?, :created, :updated, :skipped, :errors, keyword_init: true)
Result = Struct.new(:success?, :created, :updated, :skipped, :errors, :reconciliation, keyword_init: true)
def initialize(user, game_data, options = {})
@user = user
@game_data = game_data
@update_existing = options[:update_existing] || false
@is_full_inventory = options[:is_full_inventory] || false
@reconcile_deletions = options[:reconcile_deletions] || false
@created = []
@updated = []
@skipped = []
@errors = []
@processed_game_ids = []
end
##
# Previews what would be deleted in a sync operation.
# Does not modify any data, just returns items that would be removed.
#
# @return [Array<CollectionSummon>] Collection summons that would be deleted
def preview_deletions
items = extract_items
return [] if items.empty?
# Extract all game_ids from the import data
game_ids = items.filter_map do |item|
param = item['param'] || {}
param['id'].to_s if param['id'].present?
end
return [] if game_ids.empty?
# Find collection summons with game_ids NOT in the import
@user.collection_summons
.includes(:summon)
.where.not(game_id: nil)
.where.not(game_id: game_ids)
end
##
@ -30,7 +57,16 @@ class SummonImportService
# @return [Result] Import result with counts and errors
def import
items = extract_items
return Result.new(success?: false, created: [], updated: [], skipped: [], errors: ['No summon items found in data']) if items.empty?
if items.empty?
return Result.new(
success?: false,
created: [],
updated: [],
skipped: [],
errors: ['No summon items found in data'],
reconciliation: nil
)
end
ActiveRecord::Base.transaction do
items.each_with_index do |item, index|
@ -40,12 +76,19 @@ class SummonImportService
end
end
# Handle deletion reconciliation if requested
reconciliation_result = nil
if @reconcile_deletions && @is_full_inventory && @processed_game_ids.any?
reconciliation_result = reconcile_deletions
end
Result.new(
success?: @errors.empty?,
created: @created,
updated: @updated,
skipped: @skipped,
errors: @errors
errors: @errors,
reconciliation: reconciliation_result
)
end
@ -68,6 +111,9 @@ class SummonImportService
granblue_id = image_id || master['id']
game_id = param['id']
# Track this game_id as processed (for reconciliation)
@processed_game_ids << game_id.to_s if game_id.present?
summon = find_summon(granblue_id)
unless summon
@errors << { game_id: game_id, granblue_id: granblue_id, error: 'Summon not found' }
@ -143,4 +189,34 @@ class SummonImportService
value = phase.to_i
value.clamp(0, 10)
end
##
# Reconciles deletions by removing collection summons not in the processed list.
# Only called when @is_full_inventory and @reconcile_deletions are both true.
#
# @return [Hash] Reconciliation result with deleted count and orphaned grid item IDs
def reconcile_deletions
# Find collection summons with game_ids NOT in our processed list
missing = @user.collection_summons
.where.not(game_id: nil)
.where.not(game_id: @processed_game_ids)
deleted_count = 0
orphaned_grid_item_ids = []
missing.find_each do |coll_summon|
# Collect IDs of grid items that will be orphaned
grid_summon_ids = GridSummon.where(collection_summon_id: coll_summon.id).pluck(:id)
orphaned_grid_item_ids.concat(grid_summon_ids)
# The before_destroy callback on CollectionSummon will mark grid items as orphaned
coll_summon.destroy
deleted_count += 1
end
{
deleted: deleted_count,
orphaned_grid_items: orphaned_grid_item_ids
}
end
end

View file

@ -12,7 +12,7 @@
# end
#
class WeaponImportService
Result = Struct.new(:success?, :created, :updated, :skipped, :errors, keyword_init: true)
Result = Struct.new(:success?, :created, :updated, :skipped, :errors, :reconciliation, keyword_init: true)
# Game awakening form to our slug mapping
AWAKENING_FORM_MAPPING = {
@ -28,12 +28,39 @@ class WeaponImportService
@user = user
@game_data = game_data
@update_existing = options[:update_existing] || false
@is_full_inventory = options[:is_full_inventory] || false
@reconcile_deletions = options[:reconcile_deletions] || false
@created = []
@updated = []
@skipped = []
@errors = []
@awakening_cache = {}
@modifier_cache = {}
@processed_game_ids = []
end
##
# Previews what would be deleted in a sync operation.
# Does not modify any data, just returns items that would be removed.
#
# @return [Array<CollectionWeapon>] Collection weapons that would be deleted
def preview_deletions
items = extract_items
return [] if items.empty?
# Extract all game_ids from the import data
game_ids = items.filter_map do |item|
param = item['param'] || {}
param['id'].to_s if param['id'].present?
end
return [] if game_ids.empty?
# Find collection weapons with game_ids NOT in the import
@user.collection_weapons
.includes(:weapon)
.where.not(game_id: nil)
.where.not(game_id: game_ids)
end
##
@ -42,7 +69,16 @@ class WeaponImportService
# @return [Result] Import result with counts and errors
def import
items = extract_items
return Result.new(success?: false, created: [], updated: [], skipped: [], errors: ['No weapon items found in data']) if items.empty?
if items.empty?
return Result.new(
success?: false,
created: [],
updated: [],
skipped: [],
errors: ['No weapon items found in data'],
reconciliation: nil
)
end
ActiveRecord::Base.transaction do
items.each_with_index do |item, index|
@ -52,12 +88,19 @@ class WeaponImportService
end
end
# Handle deletion reconciliation if requested
reconciliation_result = nil
if @reconcile_deletions && @is_full_inventory && @processed_game_ids.any?
reconciliation_result = reconcile_deletions
end
Result.new(
success?: @errors.empty?,
created: @created,
updated: @updated,
skipped: @skipped,
errors: @errors
errors: @errors,
reconciliation: reconciliation_result
)
end
@ -78,6 +121,9 @@ class WeaponImportService
granblue_id = param['image_id'] || master['id']
game_id = param['id']
# Track this game_id as processed (for reconciliation)
@processed_game_ids << game_id.to_s if game_id.present?
weapon = find_weapon(granblue_id)
unless weapon
@errors << { game_id: game_id, granblue_id: granblue_id, error: 'Weapon not found' }
@ -307,4 +353,34 @@ class WeaponImportService
nil
end
##
# Reconciles deletions by removing collection weapons not in the processed list.
# Only called when @is_full_inventory and @reconcile_deletions are both true.
#
# @return [Hash] Reconciliation result with deleted count and orphaned grid item IDs
def reconcile_deletions
# Find collection weapons with game_ids NOT in our processed list
missing = @user.collection_weapons
.where.not(game_id: nil)
.where.not(game_id: @processed_game_ids)
deleted_count = 0
orphaned_grid_item_ids = []
missing.find_each do |coll_weapon|
# Collect IDs of grid items that will be orphaned
grid_weapon_ids = GridWeapon.where(collection_weapon_id: coll_weapon.id).pluck(:id)
orphaned_grid_item_ids.concat(grid_weapon_ids)
# The before_destroy callback on CollectionWeapon will mark grid items as orphaned
coll_weapon.destroy
deleted_count += 1
end
{
deleted: deleted_count,
orphaned_grid_items: orphaned_grid_item_ids
}
end
end

View file

@ -276,6 +276,7 @@ Rails.application.routes.draw do
post :batch
delete :batch_destroy
post :import
post :preview_sync
end
end
resources :summons, only: [:create, :update, :destroy], controller: '/api/v1/collection_summons' do
@ -283,6 +284,7 @@ Rails.application.routes.draw do
post :batch
delete :batch_destroy
post :import
post :preview_sync
end
end
resources :job_accessories, controller: '/api/v1/collection_job_accessories',
@ -292,6 +294,7 @@ Rails.application.routes.draw do
post :batch
delete :batch_destroy
post :import
post :preview_sync
end
end
end

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
class AddOrphanedToGridItems < ActiveRecord::Migration[8.0]
def change
add_column :grid_weapons, :orphaned, :boolean, default: false, null: false
add_column :grid_summons, :orphaned, :boolean, default: false, null: false
add_column :grid_artifacts, :orphaned, :boolean, default: false, null: false
add_index :grid_weapons, :orphaned
add_index :grid_summons, :orphaned
add_index :grid_artifacts, :orphaned
end
end