Skip to content

Commit e1c97f2

Browse files
kronosapiensclaude
andauthored
refactor: simplify database schema and remove role history (#7)
- Change primary keys from UUID to serial integers - Simplify reactions table columns: - message_author_id → author_id - reactor_role_at_time → reactor_role - Remove role_history table and all related code (auditing not needed) - Rewrite initial migration for clean slate BREAKING CHANGE: Database schema incompatible with previous version. Requires dropping and recreating tables. Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a43b647 commit e1c97f2

12 files changed

Lines changed: 67 additions & 209 deletions

File tree

backend/__tests__/integration/commands.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { createMockInteraction, createMockUser } from '../mocks/discord.js';
99
// Mock modules BEFORE importing them
1010
const mockGetReactionBreakdown = jest.fn();
1111
const mockGetLeaderboard = jest.fn();
12-
const mockGetRoleHistory = jest.fn();
1312
const mockGetReactionsForUser = jest.fn();
1413
const mockGetUserStats = jest.fn();
1514
const mockCalculateSenpaiScore = jest.fn();
@@ -21,7 +20,6 @@ const mockCheckSenseiDecay = jest.fn();
2120
jest.unstable_mockModule('../../src/services/database.js', () => ({
2221
getReactionBreakdown: mockGetReactionBreakdown,
2322
getLeaderboard: mockGetLeaderboard,
24-
getRoleHistory: mockGetRoleHistory,
2523
getReactionsForUser: mockGetReactionsForUser,
2624
}));
2725

backend/__tests__/mocks/database.ts

Lines changed: 12 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -15,40 +15,26 @@ export async function createTestDatabase(): Promise<PGlite> {
1515
// Create tables
1616
await db.exec(`
1717
CREATE TABLE IF NOT EXISTS reactions (
18-
id TEXT PRIMARY KEY,
18+
id SERIAL PRIMARY KEY,
1919
message_id TEXT NOT NULL,
20-
message_author_id TEXT NOT NULL,
20+
author_id TEXT NOT NULL,
2121
reactor_id TEXT NOT NULL,
22-
reactor_role_at_time TEXT NOT NULL CHECK(reactor_role_at_time IN ('Kohai', 'Senpai', 'Sensei')),
22+
reactor_role TEXT NOT NULL CHECK(reactor_role IN ('Kohai', 'Senpai', 'Sensei')),
2323
timestamp BIGINT NOT NULL,
2424
UNIQUE(message_id, reactor_id)
2525
)
2626
`);
2727

2828
await db.exec(`
29-
CREATE INDEX IF NOT EXISTS idx_reactions_author ON reactions(message_author_id);
29+
CREATE INDEX IF NOT EXISTS idx_reactions_author ON reactions(author_id);
3030
CREATE INDEX IF NOT EXISTS idx_reactions_reactor ON reactions(reactor_id);
3131
CREATE INDEX IF NOT EXISTS idx_reactions_timestamp ON reactions(timestamp);
3232
`);
3333

34-
await db.exec(`
35-
CREATE TABLE IF NOT EXISTS role_history (
36-
id TEXT PRIMARY KEY,
37-
user_id TEXT NOT NULL,
38-
role TEXT NOT NULL CHECK(role IN ('Kohai', 'Senpai', 'Sensei')),
39-
reason TEXT NOT NULL CHECK(reason IN ('promotion', 'demotion', 'decay', 'manual')),
40-
timestamp BIGINT NOT NULL
41-
)
42-
`);
43-
44-
await db.exec(`
45-
CREATE INDEX IF NOT EXISTS idx_role_history_user ON role_history(user_id);
46-
`);
47-
4834
// Content pipeline tables
4935
await db.exec(`
5036
CREATE TABLE IF NOT EXISTS content_stories (
51-
id TEXT PRIMARY KEY,
37+
id SERIAL PRIMARY KEY,
5238
title TEXT NOT NULL,
5339
summary TEXT NOT NULL,
5440
source_message_ids TEXT NOT NULL,
@@ -64,8 +50,8 @@ export async function createTestDatabase(): Promise<PGlite> {
6450

6551
await db.exec(`
6652
CREATE TABLE IF NOT EXISTS content_drafts (
67-
id TEXT PRIMARY KEY,
68-
story_id TEXT NOT NULL REFERENCES content_stories(id) ON DELETE CASCADE,
53+
id SERIAL PRIMARY KEY,
54+
story_id INTEGER NOT NULL REFERENCES content_stories(id) ON DELETE CASCADE,
6955
tweets TEXT NOT NULL,
7056
image_prompt TEXT NOT NULL,
7157
image_url TEXT,
@@ -82,7 +68,7 @@ export async function createTestDatabase(): Promise<PGlite> {
8268

8369
await db.exec(`
8470
CREATE TABLE IF NOT EXISTS content_pipeline_runs (
85-
id TEXT PRIMARY KEY,
71+
id SERIAL PRIMARY KEY,
8672
started_at BIGINT NOT NULL,
8773
completed_at BIGINT,
8874
messages_scanned INTEGER NOT NULL DEFAULT 0,
@@ -107,31 +93,10 @@ export async function insertTestReaction(
10793
reactorRole: Role,
10894
timestamp: number = Date.now()
10995
): Promise<void> {
110-
const id = `test-reaction-${messageId}-${reactorId}`;
111-
112-
await db.query(
113-
`INSERT INTO reactions (id, message_id, message_author_id, reactor_id, reactor_role_at_time, timestamp)
114-
VALUES ($1, $2, $3, $4, $5, $6)`,
115-
[id, messageId, messageAuthorId, reactorId, reactorRole, timestamp]
116-
);
117-
}
118-
119-
/**
120-
* Insert test role history into database
121-
*/
122-
export async function insertTestRoleHistory(
123-
db: PGlite,
124-
userId: string,
125-
role: Role,
126-
reason: 'promotion' | 'demotion' | 'decay' | 'manual',
127-
timestamp: number = Date.now()
128-
): Promise<void> {
129-
const id = `test-history-${userId}-${timestamp}`;
130-
13196
await db.query(
132-
`INSERT INTO role_history (id, user_id, role, reason, timestamp)
97+
`INSERT INTO reactions (message_id, author_id, reactor_id, reactor_role, timestamp)
13398
VALUES ($1, $2, $3, $4, $5)`,
134-
[id, userId, role, reason, timestamp]
99+
[messageId, messageAuthorId, reactorId, reactorRole, timestamp]
135100
);
136101
}
137102

@@ -146,8 +111,8 @@ export async function getTestReactionCount(
146111
const result = await db.query<{ count: string }>(
147112
`SELECT COUNT(*) as count
148113
FROM reactions
149-
WHERE message_author_id = $1
150-
AND reactor_role_at_time = ANY($2)`,
114+
WHERE author_id = $1
115+
AND reactor_role = ANY($2)`,
151116
[userId, roles]
152117
);
153118

@@ -162,7 +127,6 @@ export async function clearTestDatabase(db: PGlite): Promise<void> {
162127
await db.exec('DELETE FROM content_stories');
163128
await db.exec('DELETE FROM content_pipeline_runs');
164129
await db.exec('DELETE FROM reactions');
165-
await db.exec('DELETE FROM role_history');
166130
}
167131

168132
/**

backend/__tests__/unit/decay.test.ts

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,13 @@ import { createMockGuild, createMockMember } from '../mocks/discord.js';
88

99
// Mock modules BEFORE importing them
1010
const mockGetRecentSenseiReactions = jest.fn();
11-
const mockInsertRoleHistory = jest.fn();
1211
const mockGetUserRole = jest.fn();
1312
const mockAssignRole = jest.fn();
1413
const mockSendDM = jest.fn();
1514
const mockFormatDemotionMessage = jest.fn();
1615

1716
jest.unstable_mockModule('../../src/services/database.js', () => ({
1817
getRecentSenseiReactions: mockGetRecentSenseiReactions,
19-
insertRoleHistory: mockInsertRoleHistory,
2018
}));
2119

2220
jest.unstable_mockModule('../../src/services/roleManager.js', () => ({
@@ -54,9 +52,9 @@ describe('Decay Service', () => {
5452
const recentReactions = Array.from({ length: 35 }, (_, i) => ({
5553
id: `reaction-${i}`,
5654
message_id: `msg-${i}`,
57-
message_author_id: 'user-1',
55+
author_id: 'user-1',
5856
reactor_id: `reactor-${i}`,
59-
reactor_role_at_time: Role.Sensei,
57+
reactor_role: Role.Sensei,
6058
timestamp: Date.now() - i * 1000,
6159
}));
6260

@@ -66,7 +64,6 @@ describe('Decay Service', () => {
6664

6765
expect(result.demoted).toBe(false);
6866
expect(mockAssignRole).not.toHaveBeenCalled();
69-
expect(mockInsertRoleHistory).not.toHaveBeenCalled();
7067
});
7168

7269
test('should demote Sensei with insufficient recent reactions', async () => {
@@ -80,9 +77,9 @@ describe('Decay Service', () => {
8077
const recentReactions = Array.from({ length: 25 }, (_, i) => ({
8178
id: `reaction-${i}`,
8279
message_id: `msg-${i}`,
83-
message_author_id: 'user-1',
80+
author_id: 'user-1',
8481
reactor_id: `reactor-${i}`,
85-
reactor_role_at_time: Role.Sensei,
82+
reactor_role: Role.Sensei,
8683
timestamp: Date.now() - i * 1000,
8784
}));
8885

@@ -94,7 +91,6 @@ describe('Decay Service', () => {
9491
expect(result.oldRole).toBe(Role.Sensei);
9592
expect(result.newRole).toBe(Role.Senpai);
9693
expect(mockAssignRole).toHaveBeenCalledWith(guild, 'user-1', Role.Senpai);
97-
expect(mockInsertRoleHistory).toHaveBeenCalledWith('user-1', Role.Senpai, 'decay');
9894
});
9995

10096
test('should not check decay for non-Sensei users', async () => {
@@ -118,9 +114,9 @@ describe('Decay Service', () => {
118114
const recentReactions = Array.from({ length: 29 }, (_, i) => ({
119115
id: `reaction-${i}`,
120116
message_id: `msg-${i}`,
121-
message_author_id: 'user-1',
117+
author_id: 'user-1',
122118
reactor_id: `reactor-${i}`,
123-
reactor_role_at_time: Role.Sensei,
119+
reactor_role: Role.Sensei,
124120
timestamp: Date.now() - i * 1000,
125121
}));
126122

@@ -139,9 +135,9 @@ describe('Decay Service', () => {
139135
const recentReactions = Array.from({ length: 30 }, (_, i) => ({
140136
id: `reaction-${i}`,
141137
message_id: `msg-${i}`,
142-
message_author_id: 'user-1',
138+
author_id: 'user-1',
143139
reactor_id: `reactor-${i}`,
144-
reactor_role_at_time: Role.Sensei,
140+
reactor_role: Role.Sensei,
145141
timestamp: Date.now() - i * 1000,
146142
}));
147143

@@ -181,7 +177,6 @@ describe('Decay Service', () => {
181177

182178
expect(result.demoted).toBe(false);
183179
expect(mockAssignRole).not.toHaveBeenCalled();
184-
expect(mockInsertRoleHistory).not.toHaveBeenCalled();
185180
expect(mockGetRecentSenseiReactions).not.toHaveBeenCalled();
186181
});
187182

@@ -201,7 +196,6 @@ describe('Decay Service', () => {
201196

202197
expect(result.demoted).toBe(false);
203198
expect(mockAssignRole).not.toHaveBeenCalled();
204-
expect(mockInsertRoleHistory).not.toHaveBeenCalled();
205199
expect(mockGetRecentSenseiReactions).not.toHaveBeenCalled();
206200
});
207201
});
@@ -211,9 +205,9 @@ describe('Decay Service', () => {
211205
const recentReactions = Array.from({ length: 35 }, (_, i) => ({
212206
id: `reaction-${i}`,
213207
message_id: `msg-${i}`,
214-
message_author_id: 'user-1',
208+
author_id: 'user-1',
215209
reactor_id: `reactor-${i}`,
216-
reactor_role_at_time: Role.Sensei,
210+
reactor_role: Role.Sensei,
217211
timestamp: Date.now() - i * 1000,
218212
}));
219213

@@ -246,18 +240,18 @@ describe('Decay Service', () => {
246240
...Array.from({ length: 25 }, (_, i) => ({
247241
id: `recent-${i}`,
248242
message_id: `msg-${i}`,
249-
message_author_id: 'user-1',
243+
author_id: 'user-1',
250244
reactor_id: `reactor-${i}`,
251-
reactor_role_at_time: Role.Sensei,
245+
reactor_role: Role.Sensei,
252246
timestamp: now - 300 * 24 * 60 * 60 * 1000, // 300 days ago
253247
})),
254248
// Outside window (should not be counted)
255249
...Array.from({ length: 20 }, (_, i) => ({
256250
id: `old-${i}`,
257251
message_id: `msg-old-${i}`,
258-
message_author_id: 'user-1',
252+
author_id: 'user-1',
259253
reactor_id: `reactor-old-${i}`,
260-
reactor_role_at_time: Role.Sensei,
254+
reactor_role: Role.Sensei,
261255
timestamp: now - 400 * 24 * 60 * 60 * 1000, // 400 days ago
262256
})),
263257
];

backend/__tests__/unit/reputation.test.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { createMockGuild } from '../mocks/discord.js';
99
const mockGetReactionCount = jest.fn();
1010
const mockGetUniqueReactors = jest.fn();
1111
const mockGetReactionBreakdown = jest.fn();
12-
const mockInsertRoleHistory = jest.fn();
1312
const mockGetUserRole = jest.fn();
1413
const mockAssignRole = jest.fn();
1514
const mockGetRoleCounts = jest.fn();
@@ -20,7 +19,6 @@ jest.unstable_mockModule('../../src/services/database.js', () => ({
2019
getReactionCount: mockGetReactionCount,
2120
getUniqueReactors: mockGetUniqueReactors,
2221
getReactionBreakdown: mockGetReactionBreakdown,
23-
insertRoleHistory: mockInsertRoleHistory,
2422
}));
2523

2624
jest.unstable_mockModule('../../src/services/roleManager.js', () => ({
@@ -306,7 +304,6 @@ describe('Reputation Service', () => {
306304
mockGetReactionCount.mockResolvedValue(60);
307305
mockGetUniqueReactors.mockResolvedValue(Array.from({ length: 15 }, (_, i) => `reactor-${i}`));
308306
mockAssignRole.mockResolvedValue(undefined);
309-
mockInsertRoleHistory.mockResolvedValue(undefined);
310307
mockSendDM.mockResolvedValue(true);
311308

312309
const result = await checkPromotion(guild as any, 'user-1');
@@ -315,7 +312,6 @@ describe('Reputation Service', () => {
315312
expect(result.oldRole).toBe(Role.Kohai);
316313
expect(result.newRole).toBe(Role.Senpai);
317314
expect(mockAssignRole).toHaveBeenCalledWith(guild, 'user-1', Role.Senpai);
318-
expect(mockInsertRoleHistory).toHaveBeenCalledWith('user-1', Role.Senpai, 'promotion');
319315
expect(mockSendDM).toHaveBeenCalled();
320316
});
321317

@@ -332,7 +328,6 @@ describe('Reputation Service', () => {
332328
mockGetReactionCount.mockResolvedValue(35);
333329
mockGetUniqueReactors.mockResolvedValue(Array.from({ length: 12 }, (_, i) => `sensei-${i}`));
334330
mockAssignRole.mockResolvedValue(undefined);
335-
mockInsertRoleHistory.mockResolvedValue(undefined);
336331
mockSendDM.mockResolvedValue(true);
337332

338333
const result = await checkPromotion(guild as any, 'user-1');
@@ -341,7 +336,6 @@ describe('Reputation Service', () => {
341336
expect(result.oldRole).toBe(Role.Senpai);
342337
expect(result.newRole).toBe(Role.Sensei);
343338
expect(mockAssignRole).toHaveBeenCalledWith(guild, 'user-1', Role.Sensei);
344-
expect(mockInsertRoleHistory).toHaveBeenCalledWith('user-1', Role.Sensei, 'promotion');
345339
});
346340

347341
test('should not promote Kohai when threshold not met', async () => {
@@ -356,7 +350,6 @@ describe('Reputation Service', () => {
356350

357351
expect(result.promoted).toBe(false);
358352
expect(mockAssignRole).not.toHaveBeenCalled();
359-
expect(mockInsertRoleHistory).not.toHaveBeenCalled();
360353
});
361354

362355
test('should not promote Kohai when unique requirement not met', async () => {

0 commit comments

Comments
 (0)