From 99292f20effc174036db335388851dde758d12fb Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Wed, 3 Dec 2025 09:03:37 -0800 Subject: [PATCH] add batch endpoints for collection items POST /collection/{characters,weapons,summons}/batch --- .../v1/collection_characters_controller.rb | 53 +++++++++++- .../api/v1/collection_summons_controller.rb | 74 +++++++++++++++-- .../api/v1/collection_weapons_controller.rb | 80 +++++++++++++++++-- config/routes.rb | 18 ++++- 4 files changed, 210 insertions(+), 15 deletions(-) diff --git a/app/controllers/api/v1/collection_characters_controller.rb b/app/controllers/api/v1/collection_characters_controller.rb index f59bd3d..0662261 100644 --- a/app/controllers/api/v1/collection_characters_controller.rb +++ b/app/controllers/api/v1/collection_characters_controller.rb @@ -7,7 +7,7 @@ module Api before_action :set_collection_character_for_read, only: %i[show] # Write actions: require auth, use current_user - before_action :restrict_access, only: %i[create update destroy] + before_action :restrict_access, only: %i[create update destroy batch] before_action :set_collection_character_for_write, only: %i[update destroy] def index @@ -74,6 +74,45 @@ module Api head :no_content end + # POST /collection/characters/batch + # Creates multiple collection characters in a single request + def batch + items = batch_character_params[:collection_characters] || [] + created = [] + skipped = [] + errors = [] + + ActiveRecord::Base.transaction do + items.each_with_index do |item_params, index| + # Check if already exists (skip duplicates) + if current_user.collection_characters.exists?(character_id: item_params[:character_id]) + skipped << { index: index, character_id: item_params[:character_id], reason: 'already_exists' } + next + end + + collection_character = current_user.collection_characters.build(item_params) + + if collection_character.save + created << collection_character + else + errors << { + index: index, + character_id: item_params[:character_id], + error: collection_character.errors.full_messages.join(', ') + } + end + end + end + + status = errors.any? ? :multi_status : :created + + render json: Api::V1::CollectionCharacterBlueprint.render( + created, + root: :characters, + meta: { created: created.size, skipped: skipped.size, skipped_items: skipped, errors: errors } + ), status: status + end + private def set_target_user @@ -112,6 +151,18 @@ module Api earring: %i[modifier strength] ) end + + def batch_character_params + params.permit(collection_characters: [ + :character_id, :uncap_level, :transcendence_step, :perpetuity, + :awakening_id, :awakening_level, + ring1: %i[modifier strength], + ring2: %i[modifier strength], + ring3: %i[modifier strength], + ring4: %i[modifier strength], + earring: %i[modifier strength] + ]) + end end end end diff --git a/app/controllers/api/v1/collection_summons_controller.rb b/app/controllers/api/v1/collection_summons_controller.rb index 4d86952..6a68ecb 100644 --- a/app/controllers/api/v1/collection_summons_controller.rb +++ b/app/controllers/api/v1/collection_summons_controller.rb @@ -1,11 +1,17 @@ module Api module V1 class CollectionSummonsController < ApiController - before_action :restrict_access - before_action :set_collection_summon, only: %i[show update destroy] + # Read actions: look up user from params, check privacy + before_action :set_target_user, only: %i[index show] + before_action :check_collection_access, only: %i[index show] + 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] + before_action :set_collection_summon_for_write, only: %i[update destroy] def index - @collection_summons = current_user.collection_summons + @collection_summons = @target_user.collection_summons .includes(:summon) @collection_summons = @collection_summons.by_summon(params[:summon_id]) if params[:summon_id] @@ -16,7 +22,7 @@ module Api render json: Api::V1::CollectionSummonBlueprint.render( @collection_summons, - root: :collection_summons, + root: :summons, meta: pagination_meta(@collection_summons) ) end @@ -57,9 +63,61 @@ module Api head :no_content end + # POST /collection/summons/batch + # Creates multiple collection summons in a single request + # Unlike characters, summons can have duplicates (user can own multiple copies) + def batch + items = batch_summon_params[:collection_summons] || [] + created = [] + errors = [] + + ActiveRecord::Base.transaction do + items.each_with_index do |item_params, index| + collection_summon = current_user.collection_summons.build(item_params) + + if collection_summon.save + created << collection_summon + else + errors << { + index: index, + summon_id: item_params[:summon_id], + error: collection_summon.errors.full_messages.join(', ') + } + end + end + end + + status = errors.any? ? :multi_status : :created + + render json: Api::V1::CollectionSummonBlueprint.render( + created, + root: :summons, + meta: { created: created.size, errors: errors } + ), status: status + end + private - def set_collection_summon + def set_target_user + @target_user = User.find(params[:user_id]) + rescue ActiveRecord::RecordNotFound + render json: { error: "User not found" }, status: :not_found + end + + def check_collection_access + return if @target_user.nil? # Already handled by set_target_user + unless @target_user.collection_viewable_by?(current_user) + render json: { error: "You do not have permission to view this collection" }, status: :forbidden + end + end + + def set_collection_summon_for_read + @collection_summon = @target_user.collection_summons.find(params[:id]) + rescue ActiveRecord::RecordNotFound + raise CollectionErrors::CollectionItemNotFound.new('summon', params[:id]) + end + + def set_collection_summon_for_write @collection_summon = current_user.collection_summons.find(params[:id]) rescue ActiveRecord::RecordNotFound raise CollectionErrors::CollectionItemNotFound.new('summon', params[:id]) @@ -70,6 +128,12 @@ module Api :summon_id, :uncap_level, :transcendence_step ) end + + def batch_summon_params + params.permit(collection_summons: [ + :summon_id, :uncap_level, :transcendence_step + ]) + end end end end diff --git a/app/controllers/api/v1/collection_weapons_controller.rb b/app/controllers/api/v1/collection_weapons_controller.rb index 773c376..f0d814b 100644 --- a/app/controllers/api/v1/collection_weapons_controller.rb +++ b/app/controllers/api/v1/collection_weapons_controller.rb @@ -1,11 +1,17 @@ module Api module V1 class CollectionWeaponsController < ApiController - before_action :restrict_access - before_action :set_collection_weapon, only: [:show, :update, :destroy] + # Read actions: look up user from params, check privacy + before_action :set_target_user, only: %i[index show] + before_action :check_collection_access, only: %i[index show] + 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] + before_action :set_collection_weapon_for_write, only: %i[update destroy] def index - @collection_weapons = current_user.collection_weapons + @collection_weapons = @target_user.collection_weapons .includes(:weapon, :awakening, :weapon_key1, :weapon_key2, :weapon_key3, :weapon_key4) @@ -18,7 +24,7 @@ module Api render json: Api::V1::CollectionWeaponBlueprint.render( @collection_weapons, - root: :collection_weapons, + root: :weapons, meta: pagination_meta(@collection_weapons) ) end @@ -59,9 +65,61 @@ module Api head :no_content end + # POST /collection/weapons/batch + # Creates multiple collection weapons in a single request + # Unlike characters, weapons can have duplicates (user can own multiple copies) + def batch + items = batch_weapon_params[:collection_weapons] || [] + created = [] + errors = [] + + ActiveRecord::Base.transaction do + items.each_with_index do |item_params, index| + collection_weapon = current_user.collection_weapons.build(item_params) + + if collection_weapon.save + created << collection_weapon + else + errors << { + index: index, + weapon_id: item_params[:weapon_id], + error: collection_weapon.errors.full_messages.join(', ') + } + end + end + end + + status = errors.any? ? :multi_status : :created + + render json: Api::V1::CollectionWeaponBlueprint.render( + created, + root: :weapons, + meta: { created: created.size, errors: errors } + ), status: status + end + private - def set_collection_weapon + def set_target_user + @target_user = User.find(params[:user_id]) + rescue ActiveRecord::RecordNotFound + render json: { error: "User not found" }, status: :not_found + end + + def check_collection_access + return if @target_user.nil? # Already handled by set_target_user + unless @target_user.collection_viewable_by?(current_user) + render json: { error: "You do not have permission to view this collection" }, status: :forbidden + end + end + + def set_collection_weapon_for_read + @collection_weapon = @target_user.collection_weapons.find(params[:id]) + rescue ActiveRecord::RecordNotFound + raise CollectionErrors::CollectionItemNotFound.new('weapon', params[:id]) + end + + def set_collection_weapon_for_write @collection_weapon = current_user.collection_weapons.find(params[:id]) rescue ActiveRecord::RecordNotFound raise CollectionErrors::CollectionItemNotFound.new('weapon', params[:id]) @@ -76,6 +134,16 @@ module Api :element ) end + + def batch_weapon_params + params.permit(collection_weapons: [ + :weapon_id, :uncap_level, :transcendence_step, + :weapon_key1_id, :weapon_key2_id, :weapon_key3_id, :weapon_key4_id, + :awakening_id, :awakening_level, + :ax_modifier1, :ax_strength1, :ax_modifier2, :ax_strength2, + :element + ]) + end end end -end \ No newline at end of file +end diff --git a/config/routes.rb b/config/routes.rb index ffc75b5..4e2c9da 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -137,9 +137,21 @@ Rails.application.routes.draw do # Writing to collections - requires auth, operates on current_user namespace :collection do - resources :characters, only: [:create, :update, :destroy], controller: '/api/v1/collection_characters' - resources :weapons, only: [:create, :update, :destroy], controller: '/api/v1/collection_weapons' - resources :summons, only: [:create, :update, :destroy], controller: '/api/v1/collection_summons' + resources :characters, only: [:create, :update, :destroy], controller: '/api/v1/collection_characters' do + collection do + post :batch + end + end + resources :weapons, only: [:create, :update, :destroy], controller: '/api/v1/collection_weapons' do + collection do + post :batch + end + end + resources :summons, only: [:create, :update, :destroy], controller: '/api/v1/collection_summons' do + collection do + post :batch + end + end resources :job_accessories, controller: '/api/v1/collection_job_accessories', only: [:index, :show, :create, :destroy] end