Skip to content

Commit 0c090f0

Browse files
committed
fix: Refactor services to improve maintainability
1 parent 1d76209 commit 0c090f0

4 files changed

Lines changed: 266 additions & 310 deletions

File tree

app/controllers/api/v1/scouting/players_controller.rb

Lines changed: 71 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -56,31 +56,14 @@ def show
5656
# Creates/finds global target and adds to org watchlist
5757
def create
5858
ActiveRecord::Base.transaction do
59-
# Find or create global scouting target
6059
target = find_or_create_target!
61-
62-
# Create watchlist entry for this organization
63-
watchlist = target.scouting_watchlists.create!(
64-
organization: current_organization,
65-
added_by: current_user,
66-
priority: watchlist_params[:priority] || 'medium',
67-
status: watchlist_params[:status] || 'watching',
68-
notes: watchlist_params[:notes],
69-
assigned_to_id: watchlist_params[:assigned_to_id]
70-
)
71-
72-
log_user_action(
73-
action: 'create',
74-
entity_type: 'ScoutingWatchlist',
75-
entity_id: watchlist.id,
76-
new_values: watchlist.attributes
60+
watchlist = create_watchlist_for(target)
61+
log_user_action(action: 'create', entity_type: 'ScoutingWatchlist',
62+
entity_id: watchlist.id, new_values: watchlist.attributes)
63+
render_created(
64+
{ scouting_target: JSON.parse(ScoutingTargetSerializer.render(target, watchlist: watchlist)) },
65+
message: 'Scouting target added successfully'
7766
)
78-
79-
render_created({
80-
scouting_target: JSON.parse(
81-
ScoutingTargetSerializer.render(target, watchlist: watchlist)
82-
)
83-
}, message: 'Scouting target added successfully')
8467
end
8568
rescue ActiveRecord::RecordInvalid => e
8669
render_error(
@@ -95,36 +78,9 @@ def create
9578
# Updates global target data OR watchlist data
9679
def update
9780
ActiveRecord::Base.transaction do
98-
# Update global target fields if provided
99-
if target_params.any?
100-
@target.update!(target_params)
101-
end
102-
103-
# Update watchlist fields if provided
104-
if watchlist_params.any?
105-
watchlist = @target.scouting_watchlists.find_or_create_by!(organization: current_organization) do |w|
106-
w.added_by = current_user
107-
end
108-
109-
old_values = watchlist.attributes.dup
110-
watchlist.update!(watchlist_params)
111-
112-
log_user_action(
113-
action: 'update',
114-
entity_type: 'ScoutingWatchlist',
115-
entity_id: watchlist.id,
116-
old_values: old_values,
117-
new_values: watchlist.attributes
118-
)
119-
end
120-
121-
watchlist = @target.scouting_watchlists.find_by(organization: current_organization)
122-
123-
render_updated({
124-
scouting_target: JSON.parse(
125-
ScoutingTargetSerializer.render(@target, watchlist: watchlist)
126-
)
127-
})
81+
@target.update!(target_params) if target_params.any?
82+
update_watchlist_if_params_present
83+
render_updated(serialized_target_response)
12884
end
12985
rescue ActiveRecord::RecordInvalid => e
13086
render_error(
@@ -140,28 +96,17 @@ def update
14096
def destroy
14197
watchlist = @target.scouting_watchlists.find_by(organization: current_organization)
14298

143-
if watchlist
144-
watchlist.destroy
145-
146-
log_user_action(
147-
action: 'delete',
148-
entity_type: 'ScoutingWatchlist',
149-
entity_id: watchlist.id,
150-
old_values: watchlist.attributes
151-
)
152-
153-
render_deleted(message: 'Removed from watchlist')
154-
else
155-
render_error(
156-
message: 'Not in your watchlist',
157-
code: 'NOT_FOUND',
158-
status: :not_found
159-
)
99+
unless watchlist
100+
return render_error(message: 'Not in your watchlist', code: 'NOT_FOUND', status: :not_found)
160101
end
102+
103+
watchlist.destroy
104+
log_user_action(action: 'delete', entity_type: 'ScoutingWatchlist',
105+
entity_id: watchlist.id, old_values: watchlist.attributes)
106+
render_deleted(message: 'Removed from watchlist')
161107
end
162108

163109
def sync
164-
# Sync player data from Riot API
165110
unless @target.riot_puuid.present?
166111
return render_error(
167112
message: 'Cannot sync player without Riot PUUID',
@@ -170,62 +115,68 @@ def sync
170115
)
171116
end
172117

173-
riot_service = RiotApiService.new
174-
region = @target.region
175-
176-
begin
177-
# Fetch updated summoner data
178-
summoner_data = riot_service.get_summoner_by_puuid(
179-
puuid: @target.riot_puuid,
180-
region: region
181-
)
118+
perform_sync_from_riot
119+
rescue RiotApiService::NotFoundError
120+
render_error(message: 'Player not found in Riot API', code: 'PLAYER_NOT_FOUND', status: :not_found)
121+
rescue RiotApiService::RiotApiError => e
122+
render_error(message: "Failed to sync player data: #{e.message}", code: 'RIOT_API_ERROR',
123+
status: :service_unavailable)
124+
end
182125

183-
# Fetch ranked stats
184-
league_data = riot_service.get_league_entries(
185-
summoner_id: summoner_data[:summoner_id],
186-
region: region
187-
)
126+
private
188127

189-
# Fetch champion mastery
190-
mastery_data = riot_service.get_champion_mastery(
191-
puuid: @target.riot_puuid,
192-
region: region
193-
)
128+
def create_watchlist_for(target)
129+
target.scouting_watchlists.create!(
130+
organization: current_organization,
131+
added_by: current_user,
132+
priority: watchlist_params[:priority] || 'medium',
133+
status: watchlist_params[:status] || 'watching',
134+
notes: watchlist_params[:notes],
135+
assigned_to_id: watchlist_params[:assigned_to_id]
136+
)
137+
end
194138

195-
# Update target with fresh data
196-
@target.update!(
197-
riot_summoner_id: summoner_data[:summoner_id],
198-
summoner_name: summoner_data[:summoner_name],
199-
current_tier: league_data[:solo_queue]&.dig(:tier),
200-
current_rank: league_data[:solo_queue]&.dig(:rank),
201-
current_lp: league_data[:solo_queue]&.dig(:lp),
202-
champion_pool: extract_champion_pool(mastery_data),
203-
performance_trend: calculate_performance_trend(league_data)
204-
)
139+
def update_watchlist_if_params_present
140+
return unless watchlist_params.any?
205141

206-
watchlist = @target.scouting_watchlists.find_by(organization: current_organization)
207-
208-
render_success({
209-
scouting_target: JSON.parse(
210-
ScoutingTargetSerializer.render(@target, watchlist: watchlist)
211-
)
212-
}, message: 'Player data synced successfully')
213-
rescue RiotApiService::NotFoundError
214-
render_error(
215-
message: 'Player not found in Riot API',
216-
code: 'PLAYER_NOT_FOUND',
217-
status: :not_found
218-
)
219-
rescue RiotApiService::RiotApiError => e
220-
render_error(
221-
message: "Failed to sync player data: #{e.message}",
222-
code: 'RIOT_API_ERROR',
223-
status: :service_unavailable
224-
)
142+
watchlist = @target.scouting_watchlists.find_or_create_by!(organization: current_organization) do |w|
143+
w.added_by = current_user
225144
end
145+
old_values = watchlist.attributes.dup
146+
watchlist.update!(watchlist_params)
147+
log_user_action(action: 'update', entity_type: 'ScoutingWatchlist',
148+
entity_id: watchlist.id, old_values: old_values, new_values: watchlist.attributes)
226149
end
227150

228-
private
151+
def serialized_target_response
152+
watchlist = @target.scouting_watchlists.find_by(organization: current_organization)
153+
{ scouting_target: JSON.parse(ScoutingTargetSerializer.render(@target, watchlist: watchlist)) }
154+
end
155+
156+
def perform_sync_from_riot
157+
riot_service = RiotApiService.new
158+
region = @target.region
159+
160+
summoner_data = riot_service.get_summoner_by_puuid(puuid: @target.riot_puuid, region: region)
161+
league_data = riot_service.get_league_entries(summoner_id: summoner_data[:summoner_id], region: region)
162+
mastery_data = riot_service.get_champion_mastery(puuid: @target.riot_puuid, region: region)
163+
164+
@target.update!(
165+
riot_summoner_id: summoner_data[:summoner_id],
166+
summoner_name: summoner_data[:summoner_name],
167+
current_tier: league_data[:solo_queue]&.dig(:tier),
168+
current_rank: league_data[:solo_queue]&.dig(:rank),
169+
current_lp: league_data[:solo_queue]&.dig(:lp),
170+
champion_pool: extract_champion_pool(mastery_data),
171+
performance_trend: calculate_performance_trend(league_data)
172+
)
173+
174+
watchlist = @target.scouting_watchlists.find_by(organization: current_organization)
175+
render_success(
176+
{ scouting_target: JSON.parse(ScoutingTargetSerializer.render(@target, watchlist: watchlist)) },
177+
message: 'Player data synced successfully'
178+
)
179+
end
229180

230181
def find_or_create_target!
231182
if scouting_target_params[:riot_puuid].present?

app/controllers/concerns/row_level_security.rb

Lines changed: 38 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,53 +10,50 @@ module RowLevelSecurity
1010
def with_rls_context
1111
return yield unless current_user && current_organization
1212

13-
# Set thread-local variable for application-level scoping
14-
# This is the primary mechanism since RLS might not work with poolers
13+
set_thread_locals
14+
run_with_rls_transaction { yield }
15+
ensure
16+
clear_thread_locals
17+
end
18+
19+
private
20+
21+
def set_thread_locals
1522
Thread.current[:current_organization_id] = current_organization.id
1623
Thread.current[:current_user_id] = current_user.id
1724
Thread.current[:current_user_role] = current_user.role
25+
end
1826

19-
# Try to set PostgreSQL session variables for RLS
20-
# This works in direct connections but may fail with transaction-mode poolers
21-
ActiveRecord::Base.transaction do
22-
begin
23-
connection = ActiveRecord::Base.connection
24-
# Use parameterized queries to prevent SQL injection
25-
connection.exec_query(
26-
'SET LOCAL app.current_user_id = $1',
27-
'SET LOCAL',
28-
[[nil, current_user.id.to_s]]
29-
)
30-
connection.exec_query(
31-
'SET LOCAL app.current_organization_id = $1',
32-
'SET LOCAL',
33-
[[nil, current_organization.id.to_s]]
34-
)
35-
connection.exec_query(
36-
'SET LOCAL app.user_role = $1',
37-
'SET LOCAL',
38-
[[nil, current_user.role.to_s]]
39-
)
40-
rescue ActiveRecord::StatementInvalid => e
41-
# SET LOCAL might fail outside transactions on some poolers
42-
Rails.logger.warn("RLS SET LOCAL failed: #{e.message}. Using thread-local only.")
43-
end
44-
45-
yield
46-
47-
# Reset PostgreSQL variables within the same transaction
48-
begin
49-
connection.execute('RESET app.current_user_id;')
50-
connection.execute('RESET app.current_organization_id;')
51-
connection.execute('RESET app.user_role;')
52-
rescue ActiveRecord::StatementInvalid
53-
# Ignore reset errors
54-
end
55-
end
56-
ensure
57-
# Reset thread-local variables
27+
def clear_thread_locals
5828
Thread.current[:current_organization_id] = nil
5929
Thread.current[:current_user_id] = nil
6030
Thread.current[:current_user_role] = nil
6131
end
32+
33+
def run_with_rls_transaction
34+
ActiveRecord::Base.transaction do
35+
set_postgres_session_vars
36+
yield
37+
reset_postgres_session_vars
38+
end
39+
end
40+
41+
def set_postgres_session_vars
42+
connection = ActiveRecord::Base.connection
43+
connection.exec_query('SET LOCAL app.current_user_id = $1', 'SET LOCAL', [[nil, current_user.id.to_s]])
44+
connection.exec_query('SET LOCAL app.current_organization_id = $1', 'SET LOCAL',
45+
[[nil, current_organization.id.to_s]])
46+
connection.exec_query('SET LOCAL app.user_role = $1', 'SET LOCAL', [[nil, current_user.role.to_s]])
47+
rescue ActiveRecord::StatementInvalid => e
48+
Rails.logger.warn("RLS SET LOCAL failed: #{e.message}. Using thread-local only.")
49+
end
50+
51+
def reset_postgres_session_vars
52+
connection = ActiveRecord::Base.connection
53+
connection.execute('RESET app.current_user_id;')
54+
connection.execute('RESET app.current_organization_id;')
55+
connection.execute('RESET app.user_role;')
56+
rescue ActiveRecord::StatementInvalid
57+
# Ignore reset errors
58+
end
6259
end

0 commit comments

Comments
 (0)