Files
docmost/apps/server/src/database/repos/space/space-member.repo.ts
T
2025-04-22 19:37:06 +01:00

250 lines
7.1 KiB
TypeScript

import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectKysely } from 'nestjs-kysely';
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
import { dbOrTx } from '@docmost/db/utils';
import {
InsertableSpaceMember,
SpaceMember,
UpdatableSpaceMember,
} from '@docmost/db/types/entity.types';
import { PaginationOptions } from '../../pagination/pagination-options';
import { MemberInfo, UserSpaceRole } from './types';
import { executeWithPagination } from '@docmost/db/pagination/pagination';
import { GroupRepo } from '@docmost/db/repos/group/group.repo';
import { SpaceRepo } from '@docmost/db/repos/space/space.repo';
@Injectable()
export class SpaceMemberRepo {
constructor(
@InjectKysely() private readonly db: KyselyDB,
private readonly groupRepo: GroupRepo,
private readonly spaceRepo: SpaceRepo,
) {}
async insertSpaceMember(
insertableSpaceMember: InsertableSpaceMember,
trx?: KyselyTransaction,
): Promise<void> {
const db = dbOrTx(this.db, trx);
await db
.insertInto('spaceMembers')
.values(insertableSpaceMember)
.returningAll()
.execute();
}
async updateSpaceMember(
updatableSpaceMember: UpdatableSpaceMember,
spaceMemberId: string,
spaceId: string,
): Promise<void> {
await this.db
.updateTable('spaceMembers')
.set(updatableSpaceMember)
.where('id', '=', spaceMemberId)
.where('spaceId', '=', spaceId)
.execute();
}
async getSpaceMemberByTypeId(
spaceId: string,
opts: {
userId?: string;
groupId?: string;
},
trx?: KyselyTransaction,
): Promise<SpaceMember> {
const db = dbOrTx(this.db, trx);
let query = db
.selectFrom('spaceMembers')
.selectAll()
.where('spaceId', '=', spaceId);
if (opts.userId) {
query = query.where('userId', '=', opts.userId);
} else if (opts.groupId) {
query = query.where('groupId', '=', opts.groupId);
} else {
throw new BadRequestException('Please provide a userId or groupId');
}
return query.executeTakeFirst();
}
async removeSpaceMemberById(
memberId: string,
spaceId: string,
trx?: KyselyTransaction,
): Promise<void> {
const db = dbOrTx(this.db, trx);
await db
.deleteFrom('spaceMembers')
.where('id', '=', memberId)
.where('spaceId', '=', spaceId)
.execute();
}
async roleCountBySpaceId(role: string, spaceId: string): Promise<number> {
const { count } = await this.db
.selectFrom('spaceMembers')
.select((eb) => eb.fn.count('role').as('count'))
.where('role', '=', role)
.where('spaceId', '=', spaceId)
.executeTakeFirst();
return count as number;
}
async getSpaceMembersPaginated(
spaceId: string,
pagination: PaginationOptions,
) {
let query = this.db
.selectFrom('spaceMembers')
.leftJoin('users', 'users.id', 'spaceMembers.userId')
.leftJoin('groups', 'groups.id', 'spaceMembers.groupId')
.select([
'users.id as userId',
'users.name as userName',
'users.avatarUrl as userAvatarUrl',
'users.email as userEmail',
'groups.id as groupId',
'groups.name as groupName',
'groups.isDefault as groupIsDefault',
'spaceMembers.role',
'spaceMembers.createdAt',
])
.select((eb) => this.groupRepo.withMemberCount(eb))
.where('spaceId', '=', spaceId)
.orderBy((eb) => eb('groups.id', 'is not', null), 'desc')
.orderBy('spaceMembers.createdAt', 'asc');
if (pagination.query) {
query = query.where((eb) =>
eb('users.name', 'ilike', `%${pagination.query}%`)
.or('users.email', 'ilike', `%${pagination.query}%`)
.or('groups.name', 'ilike', `%${pagination.query}%`),
);
}
const result = await executeWithPagination(query, {
page: pagination.page,
perPage: pagination.limit,
});
let memberInfo: MemberInfo;
const members = result.items.map((member) => {
if (member.userId) {
memberInfo = {
id: member.userId,
name: member.userName,
email: member.userEmail,
avatarUrl: member.userAvatarUrl,
type: 'user',
};
} else if (member.groupId) {
memberInfo = {
id: member.groupId,
name: member.groupName,
memberCount: member.memberCount as number,
isDefault: member.groupIsDefault,
type: 'group',
};
}
return {
...memberInfo,
role: member.role,
createdAt: member.createdAt,
};
});
result.items = members as any;
return result;
}
/*
* we want to get a user's role in a space.
* they user can be a member either directly or via a group
* we will pass the user id and space id to return the user's roles
* if the user is a member of the space via multiple groups
* if the user has no space permission it should return an empty array,
* maybe we should throw an exception?
*/
async getUserSpaceRoles(
userId: string,
spaceId: string,
): Promise<UserSpaceRole[]> {
const roles = await this.db
.selectFrom('spaceMembers')
.select(['userId', 'role'])
.where('userId', '=', userId)
.where('spaceId', '=', spaceId)
.unionAll(
this.db
.selectFrom('spaceMembers')
.innerJoin('groupUsers', 'groupUsers.groupId', 'spaceMembers.groupId')
.select(['groupUsers.userId', 'spaceMembers.role'])
.where('groupUsers.userId', '=', userId)
.where('spaceMembers.spaceId', '=', spaceId),
)
.execute();
if (!roles || roles.length === 0) {
return undefined;
}
return roles;
}
async getUserSpaceIds(userId: string): Promise<string[]> {
const membership = await this.db
.selectFrom('spaceMembers')
.innerJoin('spaces', 'spaces.id', 'spaceMembers.spaceId')
.select(['spaces.id'])
.where('userId', '=', userId)
.union(
this.db
.selectFrom('spaceMembers')
.innerJoin('groupUsers', 'groupUsers.groupId', 'spaceMembers.groupId')
.innerJoin('spaces', 'spaces.id', 'spaceMembers.spaceId')
.select(['spaces.id'])
.where('groupUsers.userId', '=', userId),
)
.execute();
return membership.map((space) => space.id);
}
async getUserSpaces(userId: string, pagination: PaginationOptions) {
const userSpaceIds = await this.getUserSpaceIds(userId);
let query = this.db
.selectFrom('spaces')
.selectAll()
.select((eb) => [this.spaceRepo.withMemberCount(eb)])
//.where('workspaceId', '=', workspaceId)
.where('id', 'in', userSpaceIds)
.orderBy('createdAt', 'asc');
if (pagination.query) {
query = query.where((eb) =>
eb('name', 'ilike', `%${pagination.query}%`).or(
'description',
'ilike',
`%${pagination.query}%`,
),
);
}
const hasEmptyIds = userSpaceIds.length === 0;
const result = executeWithPagination(query, {
page: pagination.page,
perPage: pagination.limit,
hasEmptyIds,
});
return result;
}
}