Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 12 additions & 30 deletions apps/server-nestjs/src/modules/user/user-queries.utils.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,31 @@
import type { Prisma, User } from '@prisma/client'
import type { PrismaService } from '../infrastructure/database/prisma.service'

type UserCreate = Omit<User, 'createdAt' | 'updatedAt'>

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<User, 'id' | 'email' | 'firstName' | 'lastName' | 'type'>) {
return client.user.create({ data })
}
12 changes: 2 additions & 10 deletions apps/server-nestjs/src/modules/user/user.module.ts
Original file line number Diff line number Diff line change
@@ -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],
Expand Down
6 changes: 5 additions & 1 deletion apps/server-nestjs/src/modules/user/user.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -83,10 +84,13 @@ describe('userService', () => {
const users = [makeUser()]
prisma.user.update.mockResolvedValue(users[0])
prisma.user.findMany.mockResolvedValue(users)
const tx = mockDeep<Prisma.TransactionClient>()
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'] },
})
Expand Down
62 changes: 37 additions & 25 deletions apps/server-nestjs/src/modules/user/user.service.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -46,15 +47,7 @@
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({
Expand All @@ -76,7 +69,7 @@
const AND: Prisma.UserWhereInput[] = []
if (query.notInProjectId) {
AND.push({ projectMembers: { none: { projectId: query.notInProjectId } } })
AND.push({ projectsOwned: { none: { id: query.notInProjectId } } })

Check notice on line 72 in apps/server-nestjs/src/modules/user/user.service.ts

View check run for this annotation

cloud-pi-native-sonarqube / SonarQube Code Analysis

apps/server-nestjs/src/modules/user/user.service.ts#L72

Do not call `Array#push()` multiple times.
}
const filter = { contains: query.letters, mode: 'insensitive' } as const
if (query.letters) {
Expand All @@ -89,31 +82,52 @@
lastName: filter,
}],
})
AND.push({ type: 'human' })

Check notice on line 85 in apps/server-nestjs/src/modules/user/user.service.ts

View check run for this annotation

cloud-pi-native-sonarqube / SonarQube Code Analysis

apps/server-nestjs/src/modules/user/user.service.ts#L85

Do not call `Array#push()` multiple times.
}

return (await getMatchingUsersQuery(this.prisma, { AND })).map(toContractUser)
return (await getMatchingUsers(this.prisma, { AND })).map(toContractUser)
}

async createUser(
data: Omit<User, 'createdAt' | 'updatedAt'>,
): Promise<User> {
return createUserQuery(this.prisma, data)
async createUser(data: Omit<User, 'createdAt' | 'updatedAt'>): Promise<User> {
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(
users: { id: string, adminRoleIds: string[] | null }[],
): Promise<PatchUsersResponse> {
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<string[]> {
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<User[]> {
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<void> {
const impactedRoleIds = new Set<string>()
for (const user of users) {
usersBefore.find(({ id }) => id === user.id)?.adminRoleIds.forEach(roleId => impactedRoleIds.add(roleId))
Expand All @@ -122,7 +136,5 @@
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)
}
}
Loading