Use polymorphic table for space membership

This commit is contained in:
Philipinho
2024-03-22 00:40:13 +00:00
parent 639842182c
commit 51baf30f0d
25 changed files with 471 additions and 452 deletions

View File

@ -8,7 +8,7 @@ import { WorkspaceRepository } from '../../workspace/repositories/workspace.repo
import { WorkspaceService } from '../../workspace/services/workspace.service';
import { CreateWorkspaceDto } from '../../workspace/dto/create-workspace.dto';
import { Workspace } from '../../workspace/entities/workspace.entity';
import { SpaceService } from '../../space/space.service';
import { SpaceService } from '../../space/services/space.service';
import { CreateAdminUserDto } from '../dto/create-admin-user.dto';
import { GroupUserService } from '../../group/services/group-user.service';

View File

@ -15,16 +15,16 @@ import { Group } from '../../group/entities/group.entity';
import { GroupUser } from '../../group/entities/group-user.entity';
import { Attachment } from '../../attachment/entities/attachment.entity';
import { Space } from '../../space/entities/space.entity';
import { SpaceUser } from '../../space/entities/space-user.entity';
import { Page } from '../../page/entities/page.entity';
import { Comment } from '../../comment/entities/comment.entity';
import { SpaceMember } from '../../space/entities/space-member.entity';
export type Subjects =
| InferSubjects<
| typeof Workspace
| typeof WorkspaceInvitation
| typeof Space
| typeof SpaceUser
| typeof SpaceMember
| typeof Group
| typeof GroupUser
| typeof Attachment

View File

@ -12,7 +12,7 @@ import { GroupUser } from './group-user.entity';
import { Workspace } from '../../workspace/entities/workspace.entity';
import { User } from '../../user/entities/user.entity';
import { Unique } from 'typeorm';
import { SpaceGroup } from '../../space/entities/space-group.entity';
import { SpaceMember } from '../../space/entities/space-member.entity';
@Entity('groups')
@Unique(['name', 'workspaceId'])
@ -54,8 +54,8 @@ export class Group {
@OneToMany(() => GroupUser, (groupUser) => groupUser.group)
groupUsers: GroupUser[];
@OneToMany(() => SpaceGroup, (spaceGroup) => spaceGroup.group)
spaces: SpaceGroup[];
@OneToMany(() => SpaceMember, (spaceMembership) => spaceMembership.group)
spaces: SpaceMember[];
userCount?: number;
memberCount?: number;
}

View File

@ -89,12 +89,6 @@ export class GroupUserService {
throw new NotFoundException('Group not found');
}
const find = await manager.findOne(User, {
where: { id: userId },
});
console.log(find);
const userExists = await manager.exists(User, {
where: { id: userId, workspaceId },
});

View File

@ -119,7 +119,7 @@ export class GroupService {
.where('group.id = :groupId', { groupId })
.andWhere('group.workspaceId = :workspaceId', { workspaceId })
.loadRelationCountAndMap(
'group.userCount',
'group.memberCount',
'group.groupUsers',
'groupUsers',
)
@ -140,7 +140,7 @@ export class GroupService {
.createQueryBuilder('group')
.where('group.workspaceId = :workspaceId', { workspaceId })
.loadRelationCountAndMap(
'group.userCount',
'group.memberCount',
'group.groupUsers',
'groupUsers',
)

View File

@ -1,45 +0,0 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
Unique,
} from 'typeorm';
import { Space } from './space.entity';
import { Group } from '../../group/entities/group.entity';
@Entity('space_groups')
@Unique(['spaceId', 'groupId'])
export class SpaceGroup {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
groupId: string;
@ManyToOne(() => Group, (group) => group.spaces, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'groupId' })
group: Group;
@Column()
spaceId: string;
@ManyToOne(() => Space, (space) => space.spaceGroups, {
onDelete: 'CASCADE',
})
space: Space;
@Column({ length: 100, nullable: true })
role: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,69 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
Unique,
Check,
} from 'typeorm';
import { User } from '../../user/entities/user.entity';
import { Space } from './space.entity';
import { Group } from '../../group/entities/group.entity';
@Entity('space_members')
// allow either userId or groupId
@Check(
'CHK_allow_userId_or_groupId',
`("userId" IS NOT NULL AND "groupId" IS NULL) OR ("userId" IS NULL AND "groupId" IS NOT NULL)`,
)
@Unique(['spaceId', 'userId'])
@Unique(['spaceId', 'groupId'])
export class SpaceMember {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: true })
userId: string;
@ManyToOne(() => User, (user) => user.spaces, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'userId' })
user: User;
@Column({ nullable: true })
groupId: string;
@ManyToOne(() => Group, (group) => group.spaces, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'groupId' })
group: Group;
@Column()
spaceId: string;
@ManyToOne(() => Space, (space) => space.spaceMembers, {
onDelete: 'CASCADE',
})
space: Space;
@Column({ length: 100 })
role: string;
@Column({ nullable: true })
creatorId: string;
@ManyToOne(() => User)
@JoinColumn({ name: 'creatorId' })
creator: User;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -1,45 +0,0 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
Unique,
} from 'typeorm';
import { User } from '../../user/entities/user.entity';
import { Space } from './space.entity';
@Entity('space_users')
@Unique(['spaceId', 'userId'])
export class SpaceUser {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
userId: string;
@ManyToOne(() => User, (user) => user.spaces, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'userId' })
user: User;
@Column()
spaceId: string;
@ManyToOne(() => Space, (space) => space.spaceUsers, {
onDelete: 'CASCADE',
})
space: Space;
@Column({ length: 100, nullable: true })
role: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -11,10 +11,9 @@ import {
} from 'typeorm';
import { User } from '../../user/entities/user.entity';
import { Workspace } from '../../workspace/entities/workspace.entity';
import { SpaceUser } from './space-user.entity';
import { Page } from '../../page/entities/page.entity';
import { SpaceVisibility, SpaceRole } from '../../../helpers/types/permission';
import { SpaceGroup } from './space-group.entity';
import { SpaceMember } from './space-member.entity';
@Entity('spaces')
@Unique(['slug', 'workspaceId'])
@ -56,11 +55,8 @@ export class Space {
@JoinColumn({ name: 'workspaceId' })
workspace: Workspace;
@OneToMany(() => SpaceUser, (spaceUser) => spaceUser.space)
spaceUsers: SpaceUser[];
@OneToMany(() => SpaceGroup, (spaceGroup) => spaceGroup.space)
spaceGroups: SpaceGroup[];
@OneToMany(() => SpaceMember, (spaceMember) => spaceMember.space)
spaceMembers: SpaceMember[];
@OneToMany(() => Page, (page) => page.space)
pages: Page[];

View File

@ -1,10 +0,0 @@
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { SpaceGroup } from '../entities/space-group.entity';
@Injectable()
export class SpaceGroupRepository extends Repository<SpaceGroup> {
constructor(private dataSource: DataSource) {
super(SpaceGroup, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1,10 @@
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { SpaceMember } from '../entities/space-member.entity';
@Injectable()
export class SpaceMemberRepository extends Repository<SpaceMember> {
constructor(private dataSource: DataSource) {
super(SpaceMember, dataSource.createEntityManager());
}
}

View File

@ -1,10 +0,0 @@
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { SpaceUser } from '../entities/space-user.entity';
@Injectable()
export class SpaceUserRepository extends Repository<SpaceUser> {
constructor(private dataSource: DataSource) {
super(SpaceUser, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1,230 @@
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { SpaceRepository } from '../repositories/space.repository';
import { transactionWrapper } from '../../../helpers/db.helper';
import { DataSource, EntityManager, IsNull, Not } from 'typeorm';
import { User } from '../../user/entities/user.entity';
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto';
import { PaginatedResult } from '../../../helpers/pagination/paginated-result';
import { Group } from '../../group/entities/group.entity';
import { SpaceMemberRepository } from '../repositories/space-member.repository';
import { SpaceMember } from '../entities/space-member.entity';
@Injectable()
export class SpaceMemberService {
constructor(
private spaceRepository: SpaceRepository,
private spaceMemberRepository: SpaceMemberRepository,
private dataSource: DataSource,
) {}
async addUserToSpace(
userId: string,
spaceId: string,
role: string,
workspaceId,
manager?: EntityManager,
): Promise<SpaceMember> {
return await transactionWrapper(
async (manager: EntityManager) => {
const userExists = await manager.exists(User, {
where: { id: userId, workspaceId },
});
if (!userExists) {
throw new NotFoundException('User not found');
}
const existingSpaceUser = await manager.findOneBy(SpaceMember, {
userId: userId,
spaceId: spaceId,
});
if (existingSpaceUser) {
throw new BadRequestException('User already added to this space');
}
const spaceMember = new SpaceMember();
spaceMember.userId = userId;
spaceMember.spaceId = spaceId;
spaceMember.role = role;
await manager.save(spaceMember);
return spaceMember;
},
this.dataSource,
manager,
);
}
async getUserSpaces(
userId: string,
workspaceId: string,
paginationOptions: PaginationOptions,
) {
const [userSpaces, count] = await this.spaceMemberRepository
.createQueryBuilder('spaceMember')
.leftJoinAndSelect('spaceMember.space', 'space')
.where('spaceMember.userId = :userId', { userId })
.andWhere('space.workspaceId = :workspaceId', { workspaceId })
.loadRelationCountAndMap(
'space.memberCount',
'space.spaceMembers',
'spaceMembers',
)
.take(paginationOptions.limit)
.skip(paginationOptions.skip)
.getManyAndCount();
/*
const getUserSpacesViaGroup = this.spaceRepository
.createQueryBuilder('space')
.leftJoin('space.spaceGroups', 'spaceGroup')
.leftJoin('spaceGroup.group', 'group')
.leftJoin('group.groupUsers', 'groupUser')
.where('groupUser.userId = :userId', { userId })
.andWhere('space.workspaceId = :workspaceId', { workspaceId })
.getManyAndCount();
console.log(await getUserSpacesViaGroup);
*/
const spaces = userSpaces.map((userSpace) => userSpace.space);
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
return new PaginatedResult(spaces, paginationMeta);
}
async getSpaceMembers(
spaceId: string,
workspaceId: string,
paginationOptions: PaginationOptions,
) {
const [spaceMembers, count] = await this.spaceMemberRepository.findAndCount(
{
relations: ['user', 'group'],
where: {
space: {
id: spaceId,
workspaceId,
},
},
order: {
createdAt: 'ASC',
},
take: paginationOptions.limit,
skip: paginationOptions.skip,
},
);
const members = await Promise.all(
spaceMembers.map(async (member) => {
let memberInfo = {};
if (member.user) {
memberInfo = {
id: member.user.id,
name: member.user.name,
email: member.user.email,
avatarUrl: member.user.avatarUrl,
type: 'user',
};
} else if (member.group) {
const memberCount = await this.dataSource.getRepository(Group).count({
where: {
id: member.groupId,
workspaceId,
},
});
memberInfo = {
id: member.group.id,
name: member.group.name,
isDefault: member.group.isDefault,
memberCount: memberCount,
type: 'group',
};
}
return {
...memberInfo,
role: member.role,
};
}),
);
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
return new PaginatedResult(members, paginationMeta);
}
async addGroupToSpace(
groupId: string,
spaceId: string,
role: string,
workspaceId,
manager?: EntityManager,
): Promise<SpaceMember> {
return await transactionWrapper(
async (manager: EntityManager) => {
const groupExists = await manager.exists(Group, {
where: { id: groupId, workspaceId },
});
if (!groupExists) {
throw new NotFoundException('Group not found');
}
const existingSpaceGroup = await manager.findOneBy(SpaceMember, {
groupId: groupId,
spaceId: spaceId,
});
if (existingSpaceGroup) {
throw new BadRequestException('Group already added to this space');
}
const spaceMember = new SpaceMember();
spaceMember.groupId = groupId;
spaceMember.spaceId = spaceId;
spaceMember.role = role;
await manager.save(spaceMember);
return spaceMember;
},
this.dataSource,
manager,
);
}
async getSpaceGroup(
spaceId: string,
workspaceId: string,
paginationOptions: PaginationOptions,
) {
const [spaceGroups, count] = await this.spaceMemberRepository.findAndCount({
relations: ['group'],
where: {
groupId: Not(IsNull()),
space: {
id: spaceId,
workspaceId,
},
},
take: paginationOptions.limit,
skip: paginationOptions.skip,
});
// TODO: add group memberCount
const groups = spaceGroups.map((spaceGroup) => {
return {
...spaceGroup.group,
spaceRole: spaceGroup.role,
};
});
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
return new PaginatedResult(groups, paginationMeta);
}
}

View File

@ -0,0 +1,84 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateSpaceDto } from '../dto/create-space.dto';
import { Space } from '../entities/space.entity';
import { SpaceRepository } from '../repositories/space.repository';
import { transactionWrapper } from '../../../helpers/db.helper';
import { DataSource, EntityManager } from 'typeorm';
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto';
import { PaginatedResult } from '../../../helpers/pagination/paginated-result';
import { SpaceMemberRepository } from '../repositories/space-member.repository';
import slugify from 'slugify';
@Injectable()
export class SpaceService {
constructor(
private spaceRepository: SpaceRepository,
private spaceMemberRepository: SpaceMemberRepository,
private dataSource: DataSource,
) {}
async create(
userId: string,
workspaceId: string,
createSpaceDto?: CreateSpaceDto,
manager?: EntityManager,
): Promise<Space> {
return await transactionWrapper(
async (manager: EntityManager) => {
const space = new Space();
space.name = createSpaceDto.name ?? 'untitled space ';
space.description = createSpaceDto.description ?? '';
space.creatorId = userId;
space.workspaceId = workspaceId;
space.slug = slugify(space.name.toLowerCase()); // TODO: check for duplicate
await manager.save(space);
return space;
},
this.dataSource,
manager,
);
}
async getSpaceInfo(spaceId: string, workspaceId: string): Promise<Space> {
const space = await this.spaceRepository
.createQueryBuilder('space')
.where('space.id = :spaceId', { spaceId })
.andWhere('space.workspaceId = :workspaceId', { workspaceId })
.loadRelationCountAndMap(
'space.memberCount',
'space.spaceMembers',
'spaceMembers',
) // TODO: add groups to memberCount
.getOne();
if (!space) {
throw new NotFoundException('Space not found');
}
return space;
}
async getWorkspaceSpaces(
workspaceId: string,
paginationOptions: PaginationOptions,
): Promise<PaginatedResult<Space>> {
const [spaces, count] = await this.spaceRepository
.createQueryBuilder('space')
.where('space.workspaceId = :workspaceId', { workspaceId })
.loadRelationCountAndMap(
'space.memberCount',
'space.spaceMembers',
'spaceMembers',
) // TODO: add groups to memberCount
.take(paginationOptions.limit)
.skip(paginationOptions.skip)
.getManyAndCount();
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
return new PaginatedResult(spaces, paginationMeta);
}
}

View File

@ -1,6 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SpaceController } from './space.controller';
import { SpaceService } from './space.service';
import { SpaceService } from './services/space.service';
describe('SpaceController', () => {
let controller: SpaceController;

View File

@ -6,7 +6,7 @@ import {
Post,
UseGuards,
} from '@nestjs/common';
import { SpaceService } from './space.service';
import { SpaceService } from './services/space.service';
import { AuthUser } from '../../decorators/auth-user.decorator';
import { User } from '../user/entities/user.entity';
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
@ -14,11 +14,15 @@ import { Workspace } from '../workspace/entities/workspace.entity';
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
import { SpaceIdDto } from './dto/space-id.dto';
import { PaginationOptions } from '../../helpers/pagination/pagination-options';
import { SpaceMemberService } from './services/space-member.service';
@UseGuards(JwtAuthGuard)
@Controller('spaces')
export class SpaceController {
constructor(private readonly spaceService: SpaceService) {}
constructor(
private readonly spaceService: SpaceService,
private readonly spaceMemberService: SpaceMemberService,
) {}
@HttpCode(HttpStatus.OK)
@Post('/')
@ -41,7 +45,11 @@ export class SpaceController {
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
) {
return this.spaceService.getUserSpaces(user.id, workspace.id, pagination);
return this.spaceMemberService.getUserSpaces(
user.id,
workspace.id,
pagination,
);
}
@HttpCode(HttpStatus.OK)
@ -64,7 +72,7 @@ export class SpaceController {
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
) {
return this.spaceService.getSpaceUsers(
return this.spaceMemberService.getSpaceMembers(
spaceIdDto.spaceId,
workspace.id,
pagination,

View File

@ -1,23 +1,22 @@
import { Module } from '@nestjs/common';
import { SpaceService } from './space.service';
import { SpaceService } from './services/space.service';
import { SpaceController } from './space.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Space } from './entities/space.entity';
import { SpaceUser } from './entities/space-user.entity';
import { SpaceRepository } from './repositories/space.repository';
import { SpaceUserRepository } from './repositories/space-user.repository';
import { SpaceGroup } from './entities/space-group.entity';
import { SpaceGroupRepository } from './repositories/space-group.repository';
import { SpaceMember } from './entities/space-member.entity';
import { SpaceMemberRepository } from './repositories/space-member.repository';
import { SpaceMemberService } from './services/space-member.service';
@Module({
imports: [TypeOrmModule.forFeature([Space, SpaceUser, SpaceGroup])],
imports: [TypeOrmModule.forFeature([Space, SpaceMember])],
controllers: [SpaceController],
providers: [
SpaceService,
SpaceMemberService,
SpaceRepository,
SpaceUserRepository,
SpaceGroupRepository,
SpaceMemberRepository,
],
exports: [SpaceService],
exports: [SpaceService, SpaceMemberService],
})
export class SpaceModule {}

View File

@ -1,263 +0,0 @@
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { CreateSpaceDto } from './dto/create-space.dto';
import { Space } from './entities/space.entity';
import { SpaceRepository } from './repositories/space.repository';
import { SpaceUserRepository } from './repositories/space-user.repository';
import { SpaceUser } from './entities/space-user.entity';
import { transactionWrapper } from '../../helpers/db.helper';
import { DataSource, EntityManager } from 'typeorm';
import { User } from '../user/entities/user.entity';
import { PaginationOptions } from '../../helpers/pagination/pagination-options';
import { PaginationMetaDto } from '../../helpers/pagination/pagination-meta-dto';
import { PaginatedResult } from '../../helpers/pagination/paginated-result';
import { SpaceGroupRepository } from './repositories/space-group.repository';
import { Group } from '../group/entities/group.entity';
import { SpaceGroup } from './entities/space-group.entity';
@Injectable()
export class SpaceService {
constructor(
private spaceRepository: SpaceRepository,
private spaceUserRepository: SpaceUserRepository,
private spaceGroupRepository: SpaceGroupRepository,
private dataSource: DataSource,
) {}
async create(
userId: string,
workspaceId: string,
createSpaceDto?: CreateSpaceDto,
manager?: EntityManager,
): Promise<Space> {
return await transactionWrapper(
async (manager: EntityManager) => {
const space = new Space();
space.name = createSpaceDto.name ?? 'untitled space ';
space.description = createSpaceDto.description ?? '';
space.creatorId = userId;
space.workspaceId = workspaceId;
space.slug = space.name.toLowerCase(); // TODO: fix
await manager.save(space);
return space;
},
this.dataSource,
manager,
);
}
async addUserToSpace(
userId: string,
spaceId: string,
role: string,
workspaceId,
manager?: EntityManager,
): Promise<SpaceUser> {
return await transactionWrapper(
async (manager: EntityManager) => {
const userExists = await manager.exists(User, {
where: { id: userId, workspaceId },
});
if (!userExists) {
throw new NotFoundException('User not found');
}
const existingSpaceUser = await manager.findOneBy(SpaceUser, {
userId: userId,
spaceId: spaceId,
});
if (existingSpaceUser) {
throw new BadRequestException('User already added to this space');
}
const spaceUser = new SpaceUser();
spaceUser.userId = userId;
spaceUser.spaceId = spaceId;
spaceUser.role = role;
await manager.save(spaceUser);
return spaceUser;
},
this.dataSource,
manager,
);
}
async getSpaceInfo(spaceId: string, workspaceId: string): Promise<Space> {
const space = await this.spaceRepository
.createQueryBuilder('space')
.where('space.id = :spaceId', { spaceId })
.andWhere('space.workspaceId = :workspaceId', { workspaceId })
.loadRelationCountAndMap(
'space.userCount',
'space.spaceUsers',
'spaceUsers',
) // TODO: add groups to userCount
.getOne();
if (!space) {
throw new NotFoundException('Space not found');
}
return space;
}
async getWorkspaceSpaces(
workspaceId: string,
paginationOptions: PaginationOptions,
): Promise<PaginatedResult<Space>> {
const [spaces, count] = await this.spaceRepository
.createQueryBuilder('space')
.where('space.workspaceId = :workspaceId', { workspaceId })
.loadRelationCountAndMap(
'space.userCount',
'space.spaceUsers',
'spaceUsers',
) // TODO: add groups to userCount
.take(paginationOptions.limit)
.skip(paginationOptions.skip)
.getManyAndCount();
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
return new PaginatedResult(spaces, paginationMeta);
}
async getUserSpaces(
userId: string,
workspaceId: string,
paginationOptions: PaginationOptions,
) {
const [userSpaces, count] = await this.spaceUserRepository
.createQueryBuilder('spaceUser')
.leftJoinAndSelect('spaceUser.space', 'space')
.where('spaceUser.userId = :userId', { userId })
.andWhere('space.workspaceId = :workspaceId', { workspaceId })
.loadRelationCountAndMap(
'space.userCount',
'space.spaceUsers',
'spaceUsers',
)
.take(paginationOptions.limit)
.skip(paginationOptions.skip)
.getManyAndCount();
const getUserSpacesViaGroup = this.spaceRepository
.createQueryBuilder('space')
.leftJoin('space.spaceGroups', 'spaceGroup')
.leftJoin('spaceGroup.group', 'group')
.leftJoin('group.groupUsers', 'groupUser')
.where('groupUser.userId = :userId', { userId })
.andWhere('space.workspaceId = :workspaceId', { workspaceId }).getManyAndCount();
console.log(await getUserSpacesViaGroup);
const spaces = userSpaces.map((userSpace) => userSpace.space);
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
return new PaginatedResult(spaces, paginationMeta);
}
async getSpaceUsers(
spaceId: string,
workspaceId: string,
paginationOptions: PaginationOptions,
) {
const [spaceUsers, count] = await this.spaceUserRepository.findAndCount({
relations: ['user'],
where: {
space: {
id: spaceId,
workspaceId,
},
},
take: paginationOptions.limit,
skip: paginationOptions.skip,
});
const users = spaceUsers.map((spaceUser) => {
delete spaceUser.user.password;
return {
...spaceUser.user,
spaceRole: spaceUser.role,
};
});
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
return new PaginatedResult(users, paginationMeta);
}
async addGroupToSpace(
groupId: string,
spaceId: string,
role: string,
workspaceId,
manager?: EntityManager,
): Promise<SpaceGroup> {
return await transactionWrapper(
async (manager: EntityManager) => {
const groupExists = await manager.exists(Group, {
where: { id: groupId, workspaceId },
});
if (!groupExists) {
throw new NotFoundException('Group not found');
}
const existingSpaceGroup = await manager.findOneBy(SpaceGroup, {
groupId: groupId,
spaceId: spaceId,
});
if (existingSpaceGroup) {
throw new BadRequestException('Group already added to this space');
}
const spaceGroup = new SpaceGroup();
spaceGroup.groupId = groupId;
spaceGroup.spaceId = spaceId;
spaceGroup.role = role;
await manager.save(spaceGroup);
return spaceGroup;
},
this.dataSource,
manager,
);
}
async getSpaceGroups(
spaceId: string,
workspaceId: string,
paginationOptions: PaginationOptions,
) {
const [spaceGroups, count] = await this.spaceGroupRepository.findAndCount({
relations: ['group'],
where: {
space: {
id: spaceId,
workspaceId,
},
},
take: paginationOptions.limit,
skip: paginationOptions.skip,
});
// TODO: add group userCount
const groups = spaceGroups.map((spaceGroup) => {
return {
...spaceGroup.group,
spaceRole: spaceGroup.role,
};
});
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
return new PaginatedResult(groups, paginationMeta);
}
}

View File

@ -14,7 +14,7 @@ import { Workspace } from '../../workspace/entities/workspace.entity';
import { Page } from '../../page/entities/page.entity';
import { Comment } from '../../comment/entities/comment.entity';
import { Space } from '../../space/entities/space.entity';
import { SpaceUser } from '../../space/entities/space-user.entity';
import { SpaceMember } from '../../space/entities/space-member.entity';
@Entity('users')
@Unique(['email', 'workspaceId'])
@ -78,8 +78,8 @@ export class User {
@OneToMany(() => Space, (space) => space.creator)
createdSpaces: Space[];
@OneToMany(() => SpaceUser, (spaceUser) => spaceUser.user)
spaces: SpaceUser[];
@OneToMany(() => SpaceMember, (spaceMembership) => spaceMembership.user)
spaces: SpaceMember[];
toJSON() {
delete this.password;

View File

@ -9,7 +9,7 @@ import { Workspace } from '../entities/workspace.entity';
import { v4 as uuidv4 } from 'uuid';
import { UpdateWorkspaceDto } from '../dto/update-workspace.dto';
import { DeleteWorkspaceDto } from '../dto/delete-workspace.dto';
import { SpaceService } from '../../space/space.service';
import { SpaceService } from '../../space/services/space.service';
import { DataSource, EntityManager } from 'typeorm';
import { transactionWrapper } from '../../../helpers/db.helper';
import { CreateSpaceDto } from '../../space/dto/create-space.dto';
@ -19,6 +19,7 @@ import { User } from '../../user/entities/user.entity';
import { EnvironmentService } from '../../../integrations/environment/environment.service';
import { GroupService } from '../../group/services/group.service';
import { GroupUserService } from '../../group/services/group-user.service';
import { SpaceMemberService } from '../../space/services/space-member.service';
@Injectable()
export class WorkspaceService {
@ -26,6 +27,7 @@ export class WorkspaceService {
private workspaceRepository: WorkspaceRepository,
private userRepository: UserRepository,
private spaceService: SpaceService,
private spaceMemberService: SpaceMemberService,
private groupService: GroupService,
private groupUserService: GroupUserService,
private environmentService: EnvironmentService,
@ -42,7 +44,7 @@ export class WorkspaceService {
.createQueryBuilder('workspace')
.where('workspace.id = :workspaceId', { workspaceId })
.loadRelationCountAndMap(
'workspace.userCount',
'workspace.memberCount',
'workspace.users',
'workspaceUsers',
)
@ -105,7 +107,7 @@ export class WorkspaceService {
);
// and add user to space as owner
await this.spaceService.addUserToSpace(
await this.spaceMemberService.addUserToSpace(
user.id,
createdSpace.id,
SpaceRole.OWNER,
@ -114,7 +116,7 @@ export class WorkspaceService {
);
// add default group to space as writer
await this.spaceService.addGroupToSpace(
await this.spaceMemberService.addGroupToSpace(
group.id,
createdSpace.id,
SpaceRole.WRITER,

View File

@ -1,18 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddSpacesUsers1708941651476 implements MigrationInterface {
name = 'AddSpacesUsers1708941651476'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "space_users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" uuid NOT NULL, "spaceId" uuid NOT NULL, "role" character varying(100), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_5819a4f6b83e86596c57c19e39f" UNIQUE ("spaceId", "userId"), CONSTRAINT "PK_8d03fbe7f6bc26f9ac665250e1d" PRIMARY KEY ("id"))`);
await queryRunner.query(`ALTER TABLE "space_users" ADD CONSTRAINT "FK_e735cdb3781f344a2dff3083fd5" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "space_users" ADD CONSTRAINT "FK_dae4f7e55306bdcec6ac8f602c1" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "space_users" DROP CONSTRAINT "FK_dae4f7e55306bdcec6ac8f602c1"`);
await queryRunner.query(`ALTER TABLE "space_users" DROP CONSTRAINT "FK_e735cdb3781f344a2dff3083fd5"`);
await queryRunner.query(`DROP TABLE "space_users"`);
}
}

View File

@ -1,18 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class SpaceGroupsMembership1710892343941 implements MigrationInterface {
name = 'SpaceGroupsMembership1710892343941'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "space_groups" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "groupId" uuid NOT NULL, "spaceId" uuid NOT NULL, "role" character varying(100), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_68e59d7b983dfefc7d33febe4c3" UNIQUE ("spaceId", "groupId"), CONSTRAINT "PK_31f9b87a8dced378cb68f04836b" PRIMARY KEY ("id"))`);
await queryRunner.query(`ALTER TABLE "space_groups" ADD CONSTRAINT "FK_b3950d22b51148de9e14a1e5020" FOREIGN KEY ("groupId") REFERENCES "groups"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "space_groups" ADD CONSTRAINT "FK_80567cbf54af9e8e8ec469d247d" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "space_groups" DROP CONSTRAINT "FK_80567cbf54af9e8e8ec469d247d"`);
await queryRunner.query(`ALTER TABLE "space_groups" DROP CONSTRAINT "FK_b3950d22b51148de9e14a1e5020"`);
await queryRunner.query(`DROP TABLE "space_groups"`);
}
}

View File

@ -0,0 +1,22 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class PolymorphicSpaceMembers1711054895950 implements MigrationInterface {
name = 'PolymorphicSpaceMembers1711054895950'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "space_members" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" uuid, "groupId" uuid, "spaceId" uuid NOT NULL, "role" character varying(100) NOT NULL, "creatorId" uuid, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_07add45942b705c4b8c6c88013d" UNIQUE ("spaceId", "groupId"), CONSTRAINT "UQ_e91b442c5a1c7aa13c767c88363" UNIQUE ("spaceId", "userId"), CONSTRAINT "PK_5aaa6440d7f1e8b8c051df43d5e" PRIMARY KEY ("id"))`);
await queryRunner.query(`ALTER TABLE "space_members" ADD CONSTRAINT "FK_6b3b64db93d9a721ff7005eb6a3" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "space_members" ADD CONSTRAINT "FK_1677eab7e3f6602e13ca23418f5" FOREIGN KEY ("groupId") REFERENCES "groups"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "space_members" ADD CONSTRAINT "FK_25571cab1e221c0278499f4e801" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "space_members" ADD CONSTRAINT "FK_63ce441685d52339875a4a33b7e" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "space_members" DROP CONSTRAINT "FK_63ce441685d52339875a4a33b7e"`);
await queryRunner.query(`ALTER TABLE "space_members" DROP CONSTRAINT "FK_25571cab1e221c0278499f4e801"`);
await queryRunner.query(`ALTER TABLE "space_members" DROP CONSTRAINT "FK_1677eab7e3f6602e13ca23418f5"`);
await queryRunner.query(`ALTER TABLE "space_members" DROP CONSTRAINT "FK_6b3b64db93d9a721ff7005eb6a3"`);
await queryRunner.query(`DROP TABLE "space_members"`);
}
}

View File

@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class SpaceMemberEntityConstraint1711059108729 implements MigrationInterface {
name = 'SpaceMemberEntityConstraint1711059108729'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "space_members" ADD CONSTRAINT "CHK_allow_userId_or_groupId" CHECK (("userId" IS NOT NULL AND "groupId" IS NULL) OR ("userId" IS NULL AND "groupId" IS NOT NULL))`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "space_members" DROP CONSTRAINT "CHK_allow_userId_or_groupId"`);
}
}