Job accessories backend support (#206)

* add jobs search endpoint with pg_search

- add en_search and ja_search scopes to Job model
- add jobs action to SearchController with filtering
- supports row, proficiency, master_level, ultimate_mastery, accessory filters

* add jobs create endpoint

* add job accessories CRUD

- add accessory_type to blueprint
- add index, show, create, update, destroy actions
- editors only for mutations

* add routes for jobs search, create, and accessories CRUD
This commit is contained in:
Justin Edmund 2026-01-04 14:47:27 -08:00 committed by GitHub
parent 34e3bbd03b
commit c3d9efa349
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 180 additions and 4 deletions

View file

@ -14,7 +14,7 @@ module Api
name: :job, name: :job,
blueprint: JobBlueprint blueprint: JobBlueprint
fields :granblue_id, :rarity, :release_date fields :granblue_id, :rarity, :release_date, :accessory_type
end end
end end
end end

View file

@ -3,10 +3,84 @@
module Api module Api
module V1 module V1
class JobAccessoriesController < Api::V1::ApiController class JobAccessoriesController < Api::V1::ApiController
def job before_action :doorkeeper_authorize!, only: %i[create update destroy]
accessories = JobAccessory.where('job_id = ?', params[:id]) before_action :ensure_editor_role, only: %i[create update destroy]
# GET /job_accessories
# Optional filter: ?accessory_type=1 (1=Shield, 2=Manatura)
def index
accessories = JobAccessory.includes(:job).all
accessories = accessories.where(accessory_type: params[:accessory_type]) if params[:accessory_type].present?
accessories = accessories.order(:accessory_type, :granblue_id)
render json: JobAccessoryBlueprint.render(accessories) render json: JobAccessoryBlueprint.render(accessories)
end end
# GET /job_accessories/:id
# Supports lookup by granblue_id or uuid
def show
accessory = find_accessory
return render_not_found_response('job_accessory') unless accessory
render json: JobAccessoryBlueprint.render(accessory)
end
# POST /job_accessories
def create
accessory = JobAccessory.new(job_accessory_params)
if accessory.save
render json: JobAccessoryBlueprint.render(accessory), status: :created
else
render_validation_error_response(accessory)
end
end
# PUT /job_accessories/:id
def update
accessory = find_accessory
return render_not_found_response('job_accessory') unless accessory
if accessory.update(job_accessory_params)
render json: JobAccessoryBlueprint.render(accessory)
else
render_validation_error_response(accessory)
end
end
# DELETE /job_accessories/:id
def destroy
accessory = find_accessory
return render_not_found_response('job_accessory') unless accessory
accessory.destroy
head :no_content
end
# GET /jobs/:id/accessories
# Legacy endpoint - get accessories for a specific job
def job
job = Job.find_by(granblue_id: params[:id]) || Job.find_by(id: params[:id])
return render_not_found_response('job') unless job
accessories = JobAccessory.where(job_id: job.id)
render json: JobAccessoryBlueprint.render(accessories)
end
private
def find_accessory
JobAccessory.find_by(granblue_id: params[:id]) || JobAccessory.find_by(id: params[:id])
end
def job_accessory_params
params.permit(:name_en, :name_jp, :granblue_id, :rarity, :release_date, :accessory_type, :job_id)
end
def ensure_editor_role
return if current_user&.role && current_user.role >= 7
Rails.logger.warn "[JOB_ACCESSORIES] Unauthorized access attempt by user #{current_user&.id}"
render json: { error: 'Unauthorized - Editor role required' }, status: :unauthorized
end
end end
end end
end end

View file

@ -6,7 +6,7 @@ module Api
before_action :set_party, only: %w[update_job update_job_skills destroy_job_skill] before_action :set_party, only: %w[update_job update_job_skills destroy_job_skill]
before_action :authorize_party, only: %w[update_job update_job_skills destroy_job_skill] before_action :authorize_party, only: %w[update_job update_job_skills destroy_job_skill]
before_action :set_job, only: %w[update] before_action :set_job, only: %w[update]
before_action :ensure_editor_role, only: %w[update] before_action :ensure_editor_role, only: %w[create update]
def all def all
render json: JobBlueprint.render(Job.all) render json: JobBlueprint.render(Job.all)
@ -16,6 +16,18 @@ module Api
render json: JobBlueprint.render(Job.find_by(granblue_id: params[:id])) render json: JobBlueprint.render(Job.find_by(granblue_id: params[:id]))
end end
# POST /jobs
# Creates a new job record
def create
@job = Job.new(job_update_params)
if @job.save
render json: JobBlueprint.render(@job), status: :created
else
render_validation_error_response(@job)
end
end
# PATCH/PUT /jobs/:id # PATCH/PUT /jobs/:id
# Updates an existing job record # Updates an existing job record
def update def update

View file

@ -284,6 +284,58 @@ module Api
meta: pagination_meta(paginated).merge(count: count)) meta: pagination_meta(paginated).merge(count: count))
end end
def jobs
filters = search_params[:filters]
locale = search_params[:locale] || 'en'
conditions = {}
if filters
conditions[:row] = filters['row'] unless filters['row'].blank? || filters['row'].empty?
unless filters['proficiency'].blank? || filters['proficiency'].empty?
# Filter by either proficiency1 or proficiency2 matching
proficiency_values = Array(filters['proficiency']).map(&:to_i)
conditions[:proficiency1] = proficiency_values
end
end
jobs = if search_params[:query].present? && search_params[:query].length >= 2
if locale == 'ja'
Job.ja_search(search_params[:query]).where(conditions)
else
Job.en_search(search_params[:query]).where(conditions)
end
else
Job.where(conditions)
end
# Filter by proficiency2 as well (OR condition)
if filters && filters['proficiency'].present? && !filters['proficiency'].empty?
proficiency_values = Array(filters['proficiency']).map(&:to_i)
jobs = jobs.or(Job.where(proficiency2: proficiency_values))
end
# Apply feature filters
if filters
jobs = jobs.where(master_level: true) if filters['masterLevel'] == true || filters['masterLevel'] == 'true'
jobs = jobs.where(ultimate_mastery: true) if filters['ultimateMastery'] == true || filters['ultimateMastery'] == 'true'
jobs = jobs.where(accessory: true) if filters['accessory'] == true || filters['accessory'] == 'true'
end
# Apply sorting if specified, otherwise use default (row, then order)
if search_params[:sort].present?
jobs = apply_job_sort(jobs, search_params[:sort], search_params[:order], locale)
else
jobs = jobs.order(:row, :order)
end
count = jobs.length
paginated = jobs.paginate(page: search_params[:page], per_page: search_page_size)
render json: JobBlueprint.render(paginated,
root: :results,
meta: pagination_meta(paginated).merge(count: count))
end
def guidebooks def guidebooks
# Perform the query # Perform the query
books = if search_params[:query].present? && search_params[:query].length >= 2 books = if search_params[:query].present? && search_params[:query].length >= 2
@ -326,6 +378,23 @@ module Api
scope scope
end end
end end
# Apply sorting for jobs
def apply_job_sort(scope, column, order, locale)
sort_dir = order == 'desc' ? :desc : :asc
case column
when 'name'
name_col = locale == 'ja' ? :name_ja : :name_en
scope.order(name_col => sort_dir)
when 'row'
scope.order(row: sort_dir, order: :asc)
when 'proficiency'
scope.order(proficiency1: sort_dir)
else
scope.order(:row, :order)
end
end
end end
end end
end end

View file

@ -16,6 +16,22 @@ class Job < ApplicationRecord
} }
} }
pg_search_scope :en_search,
against: :name_en,
using: {
tsearch: {
prefix: true
}
}
pg_search_scope :ja_search,
against: :name_jp,
using: {
tsearch: {
prefix: true
}
}
belongs_to :base_job, belongs_to :base_job,
foreign_key: 'base_job_id', foreign_key: 'base_job_id',
class_name: 'Job', class_name: 'Job',

View file

@ -82,9 +82,11 @@ Rails.application.routes.draw do
post 'search/weapons', to: 'search#weapons' post 'search/weapons', to: 'search#weapons'
post 'search/summons', to: 'search#summons' post 'search/summons', to: 'search#summons'
post 'search/job_skills', to: 'search#job_skills' post 'search/job_skills', to: 'search#job_skills'
post 'search/jobs', to: 'search#jobs'
post 'search/guidebooks', to: 'search#guidebooks' post 'search/guidebooks', to: 'search#guidebooks'
get 'jobs', to: 'jobs#all' get 'jobs', to: 'jobs#all'
post 'jobs', to: 'jobs#create'
get 'jobs/skills', to: 'job_skills#all' get 'jobs/skills', to: 'job_skills#all'
get 'jobs/:id', to: 'jobs#show' get 'jobs/:id', to: 'jobs#show'
@ -97,6 +99,9 @@ Rails.application.routes.draw do
post 'jobs/:job_id/skills/:id/download_image', to: 'job_skills#download_image' post 'jobs/:job_id/skills/:id/download_image', to: 'job_skills#download_image'
get 'jobs/:id/accessories', to: 'job_accessories#job' get 'jobs/:id/accessories', to: 'job_accessories#job'
# Job Accessories (database management)
resources :job_accessories, only: %i[index show create update destroy]
get 'characters/:id/related', to: 'characters#related' get 'characters/:id/related', to: 'characters#related'
get 'guidebooks', to: 'guidebooks#all' get 'guidebooks', to: 'guidebooks#all'