Skip to content

Commit 150d452

Browse files
committed
feat: move user flags to table
1 parent 49ac56f commit 150d452

20 files changed

Lines changed: 5957 additions & 362 deletions

File tree

apps/rpc/src/modules/event/attendance-repository.ts

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,21 @@ import {
1818
type EventId,
1919
type TaskId,
2020
type UserId,
21+
normalizeDbUser,
2122
} from "@dotkomonline/types"
2223
import invariant from "tiny-invariant"
2324
import { parseOrReport } from "../../invariant"
2425

26+
type WithUserFlagLinks = { user: Parameters<typeof normalizeDbUser>[0]; [k: string]: unknown }
27+
28+
function normalizeAttendee<T extends WithUserFlagLinks>(attendee: T) {
29+
return { ...attendee, user: normalizeDbUser(attendee.user) }
30+
}
31+
32+
function normalizeAttendance<T extends { attendees: WithUserFlagLinks[]; [k: string]: unknown }>(attendance: T) {
33+
return { ...attendance, attendees: attendance.attendees.map(normalizeAttendee) }
34+
}
35+
2536
export interface AttendanceRepository {
2637
createAttendance(handle: DBHandle, data: AttendanceWrite): Promise<Attendance>
2738
findAttendanceById(handle: DBHandle, attendanceId: AttendanceId): Promise<Attendance | null>
@@ -102,6 +113,11 @@ export function getAttendanceRepository(): AttendanceRepository {
102113
user: {
103114
include: {
104115
memberships: true,
116+
userFlagLinks: {
117+
include: {
118+
userFlag: true,
119+
},
120+
},
105121
},
106122
},
107123
},
@@ -112,7 +128,7 @@ export function getAttendanceRepository(): AttendanceRepository {
112128
},
113129
})
114130

115-
return parseOrReport(AttendanceSchema.nullable(), attendance)
131+
return parseOrReport(AttendanceSchema.nullable(), attendance ? normalizeAttendance(attendance) : null)
116132
},
117133

118134
async findAttendancesByIds(handle, attendanceIds) {
@@ -129,6 +145,11 @@ export function getAttendanceRepository(): AttendanceRepository {
129145
user: {
130146
include: {
131147
memberships: true,
148+
userFlagLinks: {
149+
include: {
150+
userFlag: true,
151+
},
152+
},
132153
},
133154
},
134155
},
@@ -139,7 +160,7 @@ export function getAttendanceRepository(): AttendanceRepository {
139160
},
140161
})
141162

142-
return attendances.map((attendance) => parseOrReport(AttendanceSchema, attendance))
163+
return attendances.map((attendance) => parseOrReport(AttendanceSchema, normalizeAttendance(attendance)))
143164
},
144165

145166
async findAttendanceSummariesByIds(handle, attendanceIds, userId) {
@@ -165,6 +186,11 @@ export function getAttendanceRepository(): AttendanceRepository {
165186
user: {
166187
include: {
167188
memberships: true,
189+
userFlagLinks: {
190+
include: {
191+
userFlag: true,
192+
},
193+
},
168194
},
169195
},
170196
},
@@ -184,7 +210,7 @@ export function getAttendanceRepository(): AttendanceRepository {
184210

185211
return {
186212
...attendance,
187-
currentUserAttendee: currentAttendee || null,
213+
currentUserAttendee: currentAttendee ? normalizeAttendee(currentAttendee) : null,
188214
reservedAttendeeCount: attendance._count.attendees,
189215
}
190216
})
@@ -208,6 +234,11 @@ export function getAttendanceRepository(): AttendanceRepository {
208234
user: {
209235
include: {
210236
memberships: true,
237+
userFlagLinks: {
238+
include: {
239+
userFlag: true,
240+
},
241+
},
211242
},
212243
},
213244
},
@@ -218,7 +249,7 @@ export function getAttendanceRepository(): AttendanceRepository {
218249
},
219250
})
220251

221-
return parseOrReport(AttendanceSchema.nullable(), attendance)
252+
return parseOrReport(AttendanceSchema.nullable(), attendance ? normalizeAttendance(attendance) : null)
222253
},
223254

224255
async findAttendanceByAttendeeId(handle, attendeeId) {
@@ -237,6 +268,11 @@ export function getAttendanceRepository(): AttendanceRepository {
237268
user: {
238269
include: {
239270
memberships: true,
271+
userFlagLinks: {
272+
include: {
273+
userFlag: true,
274+
},
275+
},
240276
},
241277
},
242278
},
@@ -247,7 +283,7 @@ export function getAttendanceRepository(): AttendanceRepository {
247283
},
248284
})
249285

250-
return parseOrReport(AttendanceSchema.nullable(), attendance)
286+
return parseOrReport(AttendanceSchema.nullable(), attendance ? normalizeAttendance(attendance) : null)
251287
},
252288

253289
async findAttendanceByAttendeePaymentId(handle, attendeePaymentId) {
@@ -266,6 +302,11 @@ export function getAttendanceRepository(): AttendanceRepository {
266302
user: {
267303
include: {
268304
memberships: true,
305+
userFlagLinks: {
306+
include: {
307+
userFlag: true,
308+
},
309+
},
269310
},
270311
},
271312
},
@@ -276,7 +317,7 @@ export function getAttendanceRepository(): AttendanceRepository {
276317
},
277318
})
278319

279-
return parseOrReport(AttendanceSchema.nullable(), attendance)
320+
return parseOrReport(AttendanceSchema.nullable(), attendance ? normalizeAttendance(attendance) : null)
280321
},
281322

282323
async findAttendanceByEventId(handle, eventId) {
@@ -295,6 +336,11 @@ export function getAttendanceRepository(): AttendanceRepository {
295336
user: {
296337
include: {
297338
memberships: true,
339+
userFlagLinks: {
340+
include: {
341+
userFlag: true,
342+
},
343+
},
298344
},
299345
},
300346
},
@@ -305,7 +351,7 @@ export function getAttendanceRepository(): AttendanceRepository {
305351
},
306352
})
307353

308-
return parseOrReport(AttendanceSchema.nullable(), attendance)
354+
return parseOrReport(AttendanceSchema.nullable(), attendance ? normalizeAttendance(attendance) : null)
309355
},
310356

311357
async updateAttendanceById(handle, attendanceId, data) {
@@ -375,12 +421,17 @@ export function getAttendanceRepository(): AttendanceRepository {
375421
user: {
376422
include: {
377423
memberships: true,
424+
userFlagLinks: {
425+
include: {
426+
userFlag: true,
427+
},
428+
},
378429
},
379430
},
380431
},
381432
})
382433

383-
return parseOrReport(AttendeeSchema, attendee)
434+
return parseOrReport(AttendeeSchema, normalizeAttendee(attendee))
384435
},
385436

386437
async deleteAttendeeById(handle, attendeeId) {
@@ -400,12 +451,17 @@ export function getAttendanceRepository(): AttendanceRepository {
400451
user: {
401452
include: {
402453
memberships: true,
454+
userFlagLinks: {
455+
include: {
456+
userFlag: true,
457+
},
458+
},
403459
},
404460
},
405461
},
406462
})
407463

408-
return parseOrReport(AttendeeSchema.nullable(), attendee)
464+
return parseOrReport(AttendeeSchema.nullable(), attendee ? normalizeAttendee(attendee) : null)
409465
},
410466

411467
async updateAttendeeById(handle, attendeeId, data) {
@@ -418,12 +474,17 @@ export function getAttendanceRepository(): AttendanceRepository {
418474
user: {
419475
include: {
420476
memberships: true,
477+
userFlagLinks: {
478+
include: {
479+
userFlag: true,
480+
},
481+
},
421482
},
422483
},
423484
},
424485
})
425486

426-
return parseOrReport(AttendeeSchema, attendee)
487+
return parseOrReport(AttendeeSchema, normalizeAttendee(attendee))
427488
},
428489

429490
async updateAttendeePaymentById(handle, attendeeId, data) {
@@ -436,12 +497,17 @@ export function getAttendanceRepository(): AttendanceRepository {
436497
user: {
437498
include: {
438499
memberships: true,
500+
userFlagLinks: {
501+
include: {
502+
userFlag: true,
503+
},
504+
},
439505
},
440506
},
441507
},
442508
})
443509

444-
return parseOrReport(AttendeeSchema, attendee)
510+
return parseOrReport(AttendeeSchema, normalizeAttendee(attendee))
445511
},
446512

447513
async createAttendancePool(handle, attendanceId, mergeAttendancePoolsTaskId, data) {

apps/rpc/src/modules/group/group-repository.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
GroupSchema,
1717
type GroupWrite,
1818
type UserId,
19+
normalizeDbUser,
1920
} from "@dotkomonline/types"
2021
import z from "zod"
2122
import { parseOrReport } from "../../invariant"
@@ -288,6 +289,11 @@ export function getGroupRepository(): GroupRepository {
288289
},
289290
include: {
290291
memberships: true,
292+
userFlagLinks: {
293+
include: {
294+
userFlag: true,
295+
},
296+
},
291297
groupMemberships: {
292298
where: {
293299
groupId: groupSlug,
@@ -311,7 +317,7 @@ export function getGroupRepository(): GroupRepository {
311317
})
312318

313319
const groupMembers = users.map(({ groupMemberships, ...user }) => ({
314-
...user,
320+
...normalizeDbUser(user),
315321
groupMemberships: groupMemberships.map(({ roles, ...membership }) => ({
316322
...membership,
317323
roles: roles.map((role) => role.role),

apps/rpc/src/modules/user/__test__/user-merging.spec.ts

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { randomUUID } from "node:crypto"
22
import type { DBHandle } from "@dotkomonline/db"
3-
import { GenderSchema, type Membership, type User } from "@dotkomonline/types"
3+
import { GenderSchema, type Membership, type User, type UserFlag } from "@dotkomonline/types"
44
import { beforeEach, describe, expect, it, type vi } from "vitest"
55
import { mockDeep } from "vitest-mock-extended"
66
import type { AttendanceService } from "../../event/attendance-service"
@@ -43,6 +43,18 @@ function makeMembership(userId: string, overrides: Partial<Membership> = {}): Me
4343
}
4444
}
4545

46+
function makeUserFlag(overrides: Partial<UserFlag> = {}): UserFlag {
47+
return {
48+
id: randomUUID(),
49+
name: randomUUID(),
50+
createdAt: new Date(),
51+
updatedAt: new Date(),
52+
description: null,
53+
imageUrl: null,
54+
...overrides,
55+
}
56+
}
57+
4658
describe("mergeUsers", () => {
4759
let handle: ReturnType<typeof mockDeep<DBHandle>>
4860
let groupRepository: ReturnType<typeof mockDeep<GroupRepository>>
@@ -120,15 +132,60 @@ describe("mergeUsers", () => {
120132
})
121133
})
122134

123-
describe("flags custom merger", () => {
124-
it("concatenates and deduplicates flags from both users", async () => {
125-
const survivor = makeUser({ flags: ["a", "b"] })
126-
const consumed = makeUser({ flags: ["b", "c"] })
135+
describe("flags custom relation merger", () => {
136+
it("connects consumed flags that are not already attached to survivor", async () => {
137+
const sharedFlag = makeUserFlag({ name: "shared" })
138+
const survivorFlag = makeUserFlag({ name: "survivor" })
139+
const consumedFlag = makeUserFlag({ name: "consumed" })
140+
const survivor = makeUser({ flags: [survivorFlag, sharedFlag] })
141+
const consumed = makeUser({ flags: [sharedFlag, consumedFlag] })
127142

128143
await mergeUsers(handle, deps, survivor, consumed)
129144

130-
const [[updateArgs]] = (handle.user.update as ReturnType<typeof vi.fn>).mock.calls
131-
expect(new Set(updateArgs.data.flags)).toEqual(new Set(["a", "b", "c"]))
145+
expect(handle.user.update).toHaveBeenCalledWith({
146+
where: {
147+
id: survivor.id,
148+
},
149+
data: {
150+
flags: {
151+
connect: [{ id: consumedFlag.id }],
152+
},
153+
},
154+
})
155+
})
156+
157+
it("does not connect flags when consumed only has duplicates", async () => {
158+
const sharedFlag = makeUserFlag({ name: "shared" })
159+
const survivor = makeUser({ flags: [sharedFlag] })
160+
const consumed = makeUser({ flags: [sharedFlag] })
161+
162+
await mergeUsers(handle, groupRepository, survivor, consumed)
163+
164+
const updateCalls = (handle.user.update as ReturnType<typeof vi.fn>).mock.calls
165+
expect(updateCalls).not.toEqual(
166+
expect.arrayContaining([
167+
[
168+
expect.objectContaining({
169+
data: expect.objectContaining({
170+
flags: expect.objectContaining({
171+
connect: expect.any(Array),
172+
}),
173+
}),
174+
}),
175+
],
176+
])
177+
)
178+
})
179+
180+
it("does not write flags as a scalar array", async () => {
181+
const survivor = makeUser({ flags: [makeUserFlag({ name: "survivor" })] })
182+
const consumed = makeUser({ flags: [makeUserFlag({ name: "consumed" })] })
183+
184+
await mergeUsers(handle, groupRepository, survivor, consumed)
185+
186+
for (const [updateArgs] of (handle.user.update as ReturnType<typeof vi.fn>).mock.calls) {
187+
expect(Array.isArray(updateArgs.data.flags)).toBe(false)
188+
}
132189
})
133190
})
134191

0 commit comments

Comments
 (0)