Skip to content

Commit 59d4a78

Browse files
committed
fix: runtime issues in Phases 20-24 — SQL conflicts, HTTP methods, route paths, FK constraints
Fixes: - Repo: dismiss_recommendation now dismisses by rec_id (was using server_id as WHERE clause) - Repo: award_achievement ON CONFLICT now uses explicit columns + DO UPDATE to always return row - Repo: cast_vote ON CONFLICT now uses explicit columns + DO UPDATE to always return row - Repo: added get_sync_cursor() read-only function (GET handler was calling upsert) - Routes: award_achievement, dismiss_recommendation, cast_vote changed from GET to POST - Routes: award_contributor_badge removed GET handler (POST-only, expects JSON body) - Routes: cast_vote handler changed from Query to Json body extraction - Routes: get_sync_cursor now calls read-only get_sync_cursor() instead of upsert - invoke.ts: scaling-configs paths fixed from /servers/ to /admin/ - invoke.ts: voice-quality-logs, slow-mode-overrides fixed from /servers/ to /channels/ - invoke.ts: ai-suggestions, ai-consent, ai-audit-log paths fixed to match route definitions - invoke.ts: live-streams now includes required channel_id query param - invoke.ts: dismiss, award, vote calls changed from GET to POST - Migration 000030: added FK constraints and indexes for all Phase 20-24 tables
1 parent 2472299 commit 59d4a78

6 files changed

Lines changed: 236 additions & 32 deletions

File tree

crates/nexus-api/src/routes/growth.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use axum::{
77
extract::{Extension, Path, Query, State},
88
middleware,
9-
routing::get,
9+
routing::{get, post},
1010
Json, Router,
1111
};
1212
use nexus_common::error::{NexusError, NexusResult};
@@ -30,7 +30,7 @@ pub fn router() -> Router<Arc<AppState>> {
3030
)
3131
.route(
3232
"/users/@me/recommendations/:rec_id/dismiss",
33-
get(dismiss_recommendation),
33+
post(dismiss_recommendation),
3434
)
3535
// Onboarding flows
3636
.route(
@@ -57,7 +57,7 @@ pub fn router() -> Router<Arc<AppState>> {
5757
)
5858
.route(
5959
"/servers/:server_id/achievements/:achievement_id/award",
60-
get(award_achievement),
60+
post(award_achievement),
6161
)
6262
// Sync cursors
6363
.route("/users/@me/sync-cursors", get(get_sync_cursor).post(upsert_sync_cursor))
@@ -324,9 +324,9 @@ async fn get_sync_cursor(
324324
Extension(ctx): Extension<AuthContext>,
325325
Query(q): Query<SyncCursorQuery>,
326326
) -> NexusResult<Json<Option<SyncCursor>>> {
327-
let row = growth::upsert_sync_cursor(
328-
&state.db.pool, ctx.user_id, &q.device_id, q.channel_id, None,
329-
).await.ok();
327+
let row = growth::get_sync_cursor(
328+
&state.db.pool, ctx.user_id, &q.device_id, q.channel_id,
329+
).await.map_err(|e| NexusError::Internal(e.into()))?;
330330
Ok(Json(row))
331331
}
332332

crates/nexus-api/src/routes/sustainability.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use axum::{
77
extract::{Extension, Path, Query, State},
88
middleware,
9-
routing::get,
9+
routing::{get, post},
1010
Json, Router,
1111
};
1212
use nexus_common::error::{NexusError, NexusResult};
@@ -33,7 +33,7 @@ pub fn router() -> Router<Arc<AppState>> {
3333
)
3434
.route(
3535
"/servers/:server_id/governance/polls/:poll_id/vote",
36-
get(cast_vote),
36+
post(cast_vote),
3737
)
3838
// Governance proposals
3939
.route(
@@ -47,7 +47,7 @@ pub fn router() -> Router<Arc<AppState>> {
4747
)
4848
.route(
4949
"/users/:user_id/contributor-badges/award",
50-
get(award_contributor_badge).post(award_contributor_badge),
50+
post(award_contributor_badge),
5151
)
5252
// Security audits (admin)
5353
.route("/admin/security-audits", get(list_security_audits).post(create_security_audit))
@@ -199,13 +199,13 @@ async fn cast_vote(
199199
State(state): State<Arc<AppState>>,
200200
Extension(ctx): Extension<AuthContext>,
201201
Path((server_id, poll_id)): Path<(Uuid, Uuid)>,
202-
Query(q): Query<CastVoteReq>,
202+
Json(body): Json<CastVoteReq>,
203203
) -> NexusResult<Json<PollVote>> {
204204
let _m: Option<Member> = members::find_member(&state.db.pool, ctx.user_id, server_id)
205205
.await.map_err(|e| NexusError::Internal(e.into()))?;
206206
if _m.is_none() { return Err(NexusError::Forbidden); }
207207

208-
let row = sustainability::cast_vote(&state.db.pool, poll_id, ctx.user_id, q.option_index)
208+
let row = sustainability::cast_vote(&state.db.pool, poll_id, ctx.user_id, body.option_index)
209209
.await.map_err(|e| NexusError::Internal(e.into()))?;
210210
Ok(Json(row))
211211
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
-- ============================================================================
2+
-- Phase 20-24: Add missing foreign key constraints and indexes
3+
-- ============================================================================
4+
5+
-- ── Migration 25: Scalability Hardening ────────────────────────────────────
6+
7+
ALTER TABLE voice_quality_logs
8+
ADD CONSTRAINT fk_vql_channel FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE,
9+
ADD CONSTRAINT fk_vql_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
10+
11+
ALTER TABLE member_prune_rules
12+
ADD CONSTRAINT fk_mpr_server FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE;
13+
14+
ALTER TABLE slow_mode_overrides
15+
ADD CONSTRAINT fk_smo_channel FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE;
16+
17+
CREATE INDEX IF NOT EXISTS idx_vql_channel ON voice_quality_logs(channel_id, created_at DESC);
18+
CREATE INDEX IF NOT EXISTS idx_vql_user ON voice_quality_logs(user_id);
19+
CREATE INDEX IF NOT EXISTS idx_mpr_server ON member_prune_rules(server_id);
20+
CREATE INDEX IF NOT EXISTS idx_smo_channel ON slow_mode_overrides(channel_id);
21+
22+
-- ── Migration 26: AI Intelligence ─────────────────────────────────────────
23+
24+
ALTER TABLE search_embeddings
25+
ADD CONSTRAINT fk_se_message FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE,
26+
ADD CONSTRAINT fk_se_channel FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE;
27+
28+
ALTER TABLE search_queries
29+
ADD CONSTRAINT fk_sq_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
30+
31+
ALTER TABLE ai_suggestions
32+
ADD CONSTRAINT fk_as_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
33+
ADD CONSTRAINT fk_as_channel FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE;
34+
35+
ALTER TABLE thread_summaries
36+
ADD CONSTRAINT fk_ts_channel FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE;
37+
38+
ALTER TABLE toxicity_scores
39+
ADD CONSTRAINT fk_tox_message FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE,
40+
ADD CONSTRAINT fk_tox_server FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE;
41+
42+
ALTER TABLE raid_detections
43+
ADD CONSTRAINT fk_rd_server FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE;
44+
45+
ALTER TABLE voice_transcripts
46+
ADD CONSTRAINT fk_vt_channel FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE;
47+
48+
ALTER TABLE voice_commands
49+
ADD CONSTRAINT fk_vc_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
50+
ADD CONSTRAINT fk_vc_channel FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE;
51+
52+
ALTER TABLE ai_consent
53+
ADD CONSTRAINT fk_ac_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
54+
ADD CONSTRAINT fk_ac_server FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE;
55+
56+
ALTER TABLE ai_audit_log
57+
ADD CONSTRAINT fk_aal_server FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE;
58+
59+
CREATE INDEX IF NOT EXISTS idx_se_message ON search_embeddings(message_id);
60+
CREATE INDEX IF NOT EXISTS idx_se_channel ON search_embeddings(channel_id);
61+
CREATE INDEX IF NOT EXISTS idx_sq_user ON search_queries(user_id);
62+
CREATE INDEX IF NOT EXISTS idx_as_user ON ai_suggestions(user_id, channel_id);
63+
CREATE INDEX IF NOT EXISTS idx_ts_channel ON thread_summaries(channel_id);
64+
CREATE INDEX IF NOT EXISTS idx_tox_message ON toxicity_scores(message_id);
65+
CREATE INDEX IF NOT EXISTS idx_tox_server ON toxicity_scores(server_id);
66+
CREATE INDEX IF NOT EXISTS idx_rd_server ON raid_detections(server_id, detected_at DESC);
67+
CREATE INDEX IF NOT EXISTS idx_vt_channel ON voice_transcripts(channel_id, created_at DESC);
68+
CREATE INDEX IF NOT EXISTS idx_vc_user ON voice_commands(user_id, channel_id);
69+
CREATE INDEX IF NOT EXISTS idx_ac_user ON ai_consent(user_id, server_id);
70+
CREATE INDEX IF NOT EXISTS idx_aal_server ON ai_audit_log(server_id, created_at DESC);
71+
72+
-- ── Migration 27: Voice & Collaboration ───────────────────────────────────
73+
74+
ALTER TABLE video_layouts
75+
ADD CONSTRAINT fk_vl_channel FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE,
76+
ADD CONSTRAINT fk_vl_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
77+
78+
ALTER TABLE virtual_backgrounds
79+
ADD CONSTRAINT fk_vb_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
80+
81+
ALTER TABLE live_streams
82+
ADD CONSTRAINT fk_ls_channel FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE,
83+
ADD CONSTRAINT fk_ls_streamer FOREIGN KEY (streamer_id) REFERENCES users(id) ON DELETE CASCADE;
84+
85+
ALTER TABLE stream_viewers
86+
ADD CONSTRAINT fk_sv_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
87+
88+
ALTER TABLE breakout_rooms
89+
ADD CONSTRAINT fk_br_parent FOREIGN KEY (parent_channel) REFERENCES channels(id) ON DELETE CASCADE,
90+
ADD CONSTRAINT fk_br_creator FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL;
91+
92+
ALTER TABLE collab_sessions
93+
ADD CONSTRAINT fk_cs_channel FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE;
94+
95+
ALTER TABLE spatial_audio_configs
96+
ADD CONSTRAINT fk_sac_channel FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE;
97+
98+
CREATE INDEX IF NOT EXISTS idx_vl_channel ON video_layouts(channel_id);
99+
CREATE INDEX IF NOT EXISTS idx_vl_user ON video_layouts(user_id);
100+
CREATE INDEX IF NOT EXISTS idx_vb_user ON virtual_backgrounds(user_id);
101+
CREATE INDEX IF NOT EXISTS idx_ls_channel ON live_streams(channel_id);
102+
CREATE INDEX IF NOT EXISTS idx_ls_streamer ON live_streams(streamer_id);
103+
CREATE INDEX IF NOT EXISTS idx_sv_user ON stream_viewers(user_id);
104+
CREATE INDEX IF NOT EXISTS idx_br_parent ON breakout_rooms(parent_channel);
105+
CREATE INDEX IF NOT EXISTS idx_cs_channel ON collab_sessions(channel_id);
106+
107+
-- ── Migration 28: Growth & Retention ──────────────────────────────────────
108+
109+
ALTER TABLE server_recommendations
110+
ADD CONSTRAINT fk_sr_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
111+
ADD CONSTRAINT fk_sr_server FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE;
112+
113+
ALTER TABLE onboarding_flows
114+
ADD CONSTRAINT fk_of_server FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE;
115+
116+
ALTER TABLE device_sessions
117+
ADD CONSTRAINT fk_ds_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
118+
119+
ALTER TABLE clipboard_sync
120+
ADD CONSTRAINT fk_cbs_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
121+
122+
ALTER TABLE user_xp
123+
ADD CONSTRAINT fk_ux_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
124+
ADD CONSTRAINT fk_ux_server FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE;
125+
126+
ALTER TABLE gamification_configs
127+
ADD CONSTRAINT fk_gc_server FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE;
128+
129+
ALTER TABLE achievements
130+
ADD CONSTRAINT fk_ach_server FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE;
131+
132+
ALTER TABLE user_achievements
133+
ADD CONSTRAINT fk_ua_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
134+
135+
ALTER TABLE activity_streaks
136+
ADD CONSTRAINT fk_ast_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
137+
ADD CONSTRAINT fk_ast_server FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE;
138+
139+
ALTER TABLE sync_cursors
140+
ADD CONSTRAINT fk_sc_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
141+
ADD CONSTRAINT fk_sc_channel FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE;
142+
143+
ALTER TABLE offline_queue
144+
ADD CONSTRAINT fk_oq_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
145+
146+
CREATE INDEX IF NOT EXISTS idx_sr_user ON server_recommendations(user_id);
147+
CREATE INDEX IF NOT EXISTS idx_sr_server ON server_recommendations(server_id);
148+
CREATE INDEX IF NOT EXISTS idx_of_server ON onboarding_flows(server_id);
149+
CREATE INDEX IF NOT EXISTS idx_ds_user ON device_sessions(user_id);
150+
CREATE INDEX IF NOT EXISTS idx_ux_user ON user_xp(user_id, server_id);
151+
CREATE INDEX IF NOT EXISTS idx_ach_server ON achievements(server_id);
152+
CREATE INDEX IF NOT EXISTS idx_ast_user ON activity_streaks(user_id, server_id);
153+
CREATE INDEX IF NOT EXISTS idx_sc_user ON sync_cursors(user_id, channel_id);
154+
155+
-- ── Migration 29: Sustainability ──────────────────────────────────────────
156+
157+
ALTER TABLE governance_polls
158+
ADD CONSTRAINT fk_gp_server FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE,
159+
ADD CONSTRAINT fk_gp_creator FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL;
160+
161+
ALTER TABLE poll_votes
162+
ADD CONSTRAINT fk_pv_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
163+
164+
ALTER TABLE governance_proposals
165+
ADD CONSTRAINT fk_gprop_server FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE,
166+
ADD CONSTRAINT fk_gprop_author FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL;
167+
168+
ALTER TABLE contributor_badges
169+
ADD CONSTRAINT fk_cb_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
170+
171+
ALTER TABLE vulnerability_records
172+
DROP CONSTRAINT IF EXISTS vulnerability_records_audit_id_fkey,
173+
ADD CONSTRAINT fk_vr_audit FOREIGN KEY (audit_id) REFERENCES security_audits(id) ON DELETE CASCADE;
174+
175+
ALTER TABLE tutorial_progress
176+
ADD CONSTRAINT fk_tp_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
177+
178+
CREATE INDEX IF NOT EXISTS idx_gp_server ON governance_polls(server_id);
179+
CREATE INDEX IF NOT EXISTS idx_gp_creator ON governance_polls(created_by);
180+
CREATE INDEX IF NOT EXISTS idx_pv_user ON poll_votes(user_id);
181+
CREATE INDEX IF NOT EXISTS idx_gprop_server ON governance_proposals(server_id);
182+
CREATE INDEX IF NOT EXISTS idx_gprop_author ON governance_proposals(author_id);
183+
CREATE INDEX IF NOT EXISTS idx_cb_user ON contributor_badges(user_id);
184+
CREATE INDEX IF NOT EXISTS idx_tp_user ON tutorial_progress(user_id);

crates/nexus-db/src/repository/growth.rs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@ pub async fn list_recommendations(
5252
pub async fn dismiss_recommendation(
5353
pool: &AnyPool,
5454
user_id: Uuid,
55-
server_id: Uuid,
55+
rec_id: Uuid,
5656
) -> Result<(), sqlx::Error> {
57-
sqlx::query("UPDATE server_recommendations SET dismissed = TRUE WHERE user_id = $1 AND server_id = $2")
57+
sqlx::query("UPDATE server_recommendations SET dismissed = TRUE WHERE id = $1 AND user_id = $2")
58+
.bind(rec_id.to_string())
5859
.bind(user_id.to_string())
59-
.bind(server_id.to_string())
6060
.execute(pool)
6161
.await?;
6262
Ok(())
@@ -285,7 +285,8 @@ pub async fn award_achievement(
285285
) -> Result<UserAchievement, sqlx::Error> {
286286
let q = format!(
287287
"INSERT INTO user_achievements (user_id, achievement_id) \
288-
VALUES ($1, $2) ON CONFLICT DO NOTHING \
288+
VALUES ($1, $2) \
289+
ON CONFLICT (user_id, achievement_id) DO UPDATE SET earned_at = user_achievements.earned_at \
289290
RETURNING {USER_ACHIEVEMENT_COLS}"
290291
);
291292
sqlx::query_as::<_, UserAchievement>(&q)
@@ -297,6 +298,24 @@ pub async fn award_achievement(
297298

298299
// ── 23-04: Sync Cursors / Offline Queue ───────────────────────────────────
299300

301+
pub async fn get_sync_cursor(
302+
pool: &AnyPool,
303+
user_id: Uuid,
304+
device_id: &str,
305+
channel_id: Uuid,
306+
) -> Result<Option<SyncCursor>, sqlx::Error> {
307+
let q = format!(
308+
"SELECT {SYNC_CURSOR_COLS} FROM sync_cursors \
309+
WHERE user_id = $1 AND device_id = $2 AND channel_id = $3"
310+
);
311+
sqlx::query_as::<_, SyncCursor>(&q)
312+
.bind(user_id.to_string())
313+
.bind(device_id)
314+
.bind(channel_id.to_string())
315+
.fetch_optional(pool)
316+
.await
317+
}
318+
300319
pub async fn upsert_sync_cursor(
301320
pool: &AnyPool,
302321
user_id: Uuid,

crates/nexus-db/src/repository/sustainability.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ pub async fn cast_vote(
139139
) -> Result<PollVote, sqlx::Error> {
140140
let q = format!(
141141
"INSERT INTO poll_votes (poll_id, user_id, option_index) \
142-
VALUES ($1, $2, $3) ON CONFLICT DO NOTHING \
142+
VALUES ($1, $2, $3) \
143+
ON CONFLICT (poll_id, user_id, option_index) DO UPDATE SET voted_at = poll_votes.voted_at \
143144
RETURNING {POLL_VOTE_COLS}"
144145
);
145146
sqlx::query_as::<_, PollVote>(&q)

0 commit comments

Comments
 (0)