diff --git a/apps/server-nestjs/src/modules/user/user-queries.utils.ts b/apps/server-nestjs/src/modules/user/user-queries.utils.ts index bc32cbfe6c..35d14ff0cc 100644 --- a/apps/server-nestjs/src/modules/user/user-queries.utils.ts +++ b/apps/server-nestjs/src/modules/user/user-queries.utils.ts @@ -1,49 +1,31 @@ import type { Prisma, User } from '@prisma/client' -import type { PrismaService } from '../infrastructure/database/prisma.service' -type UserCreate = Omit - -export const userSelect = { - id: true, - firstName: true, - lastName: true, - email: true, - createdAt: true, - updatedAt: true, - lastLogin: true, - adminRoleIds: true, - type: true, -} satisfies Prisma.UserSelect -export type UserRecord = Prisma.UserGetPayload<{ select: typeof userSelect }> - -export function getUsers(prisma: PrismaService, where?: Prisma.UserWhereInput) { - return prisma.user.findMany({ where }) +export function getUsers(client: Prisma.TransactionClient, where?: Prisma.UserWhereInput) { + return client.user.findMany({ where }) } -export function getMatchingUsers(prisma: PrismaService, where: Prisma.UserWhereInput) { - return prisma.user.findMany({ +export function getMatchingUsers(client: Prisma.TransactionClient, where: Prisma.UserWhereInput) { + return client.user.findMany({ where, take: 5, }) } -export function getUserByEmail(prisma: PrismaService, email: User['email']) { - return prisma.user.findUnique({ where: { email } }) +export function getUserByEmail(client: Prisma.TransactionClient, email: User['email']) { + return client.user.findUnique({ where: { email } }) } -export function getAdminRolesByName(prisma: PrismaService, names: string[]) { - return prisma.adminRole.findMany({ where: { name: { in: names } } }) +export function getAdminRolesByName(client: Prisma.TransactionClient, names: string[]) { + return client.adminRole.findMany({ where: { name: { in: names } } }) } -export function updateUserAdminRoleIds(prisma: PrismaService, id: User['id'], adminRoleIds: string[]) { - return prisma.user.update({ +export function updateUserAdminRoleIds(client: Prisma.TransactionClient, id: User['id'], adminRoleIds: string[]) { + return client.user.update({ where: { id }, data: { adminRoleIds }, }) } -export async function createUser(prisma: PrismaService, { id, email, firstName, lastName, type }: UserCreate) { - const user = await getUserByEmail(prisma, email) - if (user) throw new Error('Un utilisateur avec cette adresse e-mail existe déjà') - return prisma.user.create({ data: { id, email, firstName, lastName, type } }) +export function createUser(client: Prisma.TransactionClient, data: Pick) { + return client.user.create({ data }) } diff --git a/apps/server-nestjs/src/modules/user/user.module.ts b/apps/server-nestjs/src/modules/user/user.module.ts index 0be6ab177f..61320b6d33 100644 --- a/apps/server-nestjs/src/modules/user/user.module.ts +++ b/apps/server-nestjs/src/modules/user/user.module.ts @@ -1,18 +1,10 @@ import { Module } from '@nestjs/common' -import { AuthModule } from '../infrastructure/auth/auth.module' -import { DatabaseModule } from '../infrastructure/database/database.module' -import { EventsModule } from '../infrastructure/events/events.module' -import { UserPermissionModule } from '../infrastructure/permission/user/user.module' +import { InfrastructureModule } from '../infrastructure/infrastructure.module' import { UserController } from './user.controller' import { UserService } from './user.service' @Module({ - imports: [ - AuthModule, - DatabaseModule, - EventsModule, - UserPermissionModule, - ], + imports: [InfrastructureModule], controllers: [UserController], providers: [UserService], exports: [UserService], diff --git a/apps/server-nestjs/src/modules/user/user.service.spec.ts b/apps/server-nestjs/src/modules/user/user.service.spec.ts index 7bb73f7af4..c51901e11c 100644 --- a/apps/server-nestjs/src/modules/user/user.service.spec.ts +++ b/apps/server-nestjs/src/modules/user/user.service.spec.ts @@ -1,3 +1,4 @@ +import type { Prisma } from '@prisma/client' import type { DeepMockProxy } from 'vitest-mock-extended' import { EventEmitter2 } from '@nestjs/event-emitter' import { Test } from '@nestjs/testing' @@ -83,10 +84,13 @@ describe('userService', () => { const users = [makeUser()] prisma.user.update.mockResolvedValue(users[0]) prisma.user.findMany.mockResolvedValue(users) + const tx = mockDeep() + tx.user.update.mockResolvedValue(users[0]) + prisma.$transaction.mockImplementation(async cb => cb(tx)) const result = await service.patchUsers([{ id: users[0].id, adminRoleIds: ['role-1'] }]) - expect(prisma.user.update).toHaveBeenCalledWith({ + expect(tx.user.update).toHaveBeenCalledWith({ where: { id: users[0].id }, data: { adminRoleIds: ['role-1'] }, }) diff --git a/apps/server-nestjs/src/modules/user/user.service.ts b/apps/server-nestjs/src/modules/user/user.service.ts index b8a2a461c5..d941000d81 100644 --- a/apps/server-nestjs/src/modules/user/user.service.ts +++ b/apps/server-nestjs/src/modules/user/user.service.ts @@ -1,13 +1,14 @@ import type { userContract } from '@cpn-console/shared' import type { Prisma, User } from '@prisma/client' import type { ClientInferResponseBody } from '@ts-rest/core' -import { BadRequestException, Inject, Injectable } from '@nestjs/common' +import { BadRequestException, ConflictException, Inject, Injectable } from '@nestjs/common' import { EventEmitter2 } from '@nestjs/event-emitter' import { PrismaService } from '../infrastructure/database/prisma.service' import { - createUser as createUserQuery, + createUser, getAdminRolesByName, - getMatchingUsers as getMatchingUsersQuery, + getMatchingUsers, + getUserByEmail, getUsers, updateUserAdminRoleIds, } from './user-queries.utils' @@ -46,15 +47,7 @@ export class UserService { whereInputs.push({ adminRoleIds: { hasEvery: query.adminRoleIds } }) } if (query.adminRoles?.length) { - const roles = query.adminRoles - ? await getAdminRolesByName(this.prisma, query.adminRoles) - : [] - - const adminRoleNameNotFound = query.adminRoles?.find(nameQueried => !roles.some(({ name }) => name === nameQueried)) - if (adminRoleNameNotFound) { - throw new BadRequestException(`Unable to find adminRole ${adminRoleNameNotFound}`) - } - whereInputs.push({ adminRoleIds: { hasEvery: roles.map(({ id }) => id) } }) + whereInputs.push({ adminRoleIds: { hasEvery: await this.resolveAdminRoleIds(query.adminRoles) } }) } if (query.memberOfIds) { whereInputs.push({ @@ -92,13 +85,14 @@ export class UserService { AND.push({ type: 'human' }) } - return (await getMatchingUsersQuery(this.prisma, { AND })).map(toContractUser) + return (await getMatchingUsers(this.prisma, { AND })).map(toContractUser) } - async createUser( - data: Omit, - ): Promise { - return createUserQuery(this.prisma, data) + async createUser(data: Omit): Promise { + const existing = await getUserByEmail(this.prisma, data.email) + if (existing) throw new ConflictException('Un utilisateur avec cette adresse e-mail existe déjà') + + return this.prisma.$transaction(tx => createUser(tx, data)) } async patchUsers( @@ -106,14 +100,34 @@ export class UserService { ): Promise { const usersBefore = await getUsers(this.prisma, { id: { in: users.map(({ id }) => id) } }) - for (const user of users) { - if (user.adminRoleIds) { - await updateUserAdminRoleIds(this.prisma, user.id, user.adminRoleIds) - } + await this.prisma.$transaction(tx => this.patchUsersInTx(tx, users)) + + await this.emitImpactedRoleEvents(users, usersBefore) + + return (await getUsers(this.prisma, { id: { in: users.map(({ id }) => id) } })).map(toContractUser) + } + + private async resolveAdminRoleIds(names: string[]): Promise { + const roles = await getAdminRolesByName(this.prisma, names) + const adminRoleNameNotFound = names.find(nameQueried => !roles.some(({ name }) => name === nameQueried)) + if (adminRoleNameNotFound) { + throw new BadRequestException(`Unable to find adminRole ${adminRoleNameNotFound}`) } + return roles.map(({ id }) => id) + } + + private async patchUsersInTx(tx: Prisma.TransactionClient, users: { id: string, adminRoleIds: string[] | null }[]): Promise { + return Promise.all(users + .filter((user): user is typeof user & { adminRoleIds: string[] } => user.adminRoleIds !== null) + .map(user => updateUserAdminRoleIds(tx, user.id, user.adminRoleIds))) + } - // Mirror legacy: hook.adminRole.upsert per impacted user, over the union of - // before/after adminRoleIds so revocations are synced too + // Mirror legacy: hook.adminRole.upsert per impacted user, over the union of + // before/after adminRoleIds so revocations are synced too + private async emitImpactedRoleEvents( + users: { id: string, adminRoleIds: string[] | null }[], + usersBefore: User[], + ): Promise { const impactedRoleIds = new Set() for (const user of users) { usersBefore.find(({ id }) => id === user.id)?.adminRoleIds.forEach(roleId => impactedRoleIds.add(roleId)) @@ -122,7 +136,5 @@ export class UserService { for (const roleId of impactedRoleIds) { await this.eventEmitter.emitAsync('adminRole.upsert', { roleId }) } - - return (await getUsers(this.prisma, { id: { in: users.map(({ id }) => id) } })).map(toContractUser) } }