feat: spaces - WIP

This commit is contained in:
Philipinho
2024-02-28 02:39:46 +00:00
parent 1d620eba49
commit 40251aef7d
32 changed files with 512 additions and 119 deletions

View File

@ -1,30 +0,0 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { TokenService } from '../services/token.service';
@Injectable()
export class JwtGuard implements CanActivate {
constructor(private tokenService: TokenService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token: string = await this.tokenService.extractTokenFromHeader(
request,
);
if (!token) {
throw new UnauthorizedException('Invalid jwt token');
}
try {
request['user'] = await this.tokenService.verifyJwt(token);
} catch (error) {
throw new UnauthorizedException('Could not verify jwt token');
}
return true;
}
}

View File

@ -8,6 +8,7 @@ import { AttachmentModule } from './attachment/attachment.module';
import { EnvironmentModule } from '../environment/environment.module';
import { CommentModule } from './comment/comment.module';
import { SearchModule } from './search/search.module';
import { SpaceModule } from './space/space.module';
@Module({
imports: [
@ -21,6 +22,7 @@ import { SearchModule } from './search/search.module';
AttachmentModule,
CommentModule,
SearchModule,
SpaceModule,
],
})
export class CoreModule {}

View File

@ -16,4 +16,7 @@ export class CreatePageDto {
@IsOptional()
@IsString()
parentPageId?: string;
@IsString()
spaceId: string;
}

View File

@ -10,6 +10,7 @@ import {
import { Workspace } from '../../workspace/entities/workspace.entity';
import { Page } from './page.entity';
import { User } from '../../user/entities/user.entity';
import { Space } from '../../space/entities/space.entity';
@Entity('page_history')
export class PageHistory {
@ -48,6 +49,13 @@ export class PageHistory {
@JoinColumn({ name: 'lastUpdatedById' })
lastUpdatedBy: User;
@Column()
spaceId: string;
@ManyToOne(() => Space, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'spaceId' })
space: Space;
@Column()
workspaceId: string;

View File

@ -10,6 +10,7 @@ import {
DeleteDateColumn,
} from 'typeorm';
import { Workspace } from '../../workspace/entities/workspace.entity';
import { Space } from '../../space/entities/space.entity';
@Entity('page_ordering')
@Unique(['entityId', 'entityType'])
@ -23,9 +24,12 @@ export class PageOrdering {
@Column({ type: 'varchar', length: 50, nullable: false })
entityType: string;
@Column('uuid', { array: true })
@Column('uuid', { array: true, default: '{}' })
childrenIds: string[];
@Column('uuid')
workspaceId: string;
@ManyToOne(() => Workspace, (workspace) => workspace.id, {
onDelete: 'CASCADE',
})
@ -33,7 +37,13 @@ export class PageOrdering {
workspace: Workspace;
@Column('uuid')
workspaceId: string;
spaceId: string;
@ManyToOne(() => Space, (space) => space.id, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'spaceId' })
space: Space;
@DeleteDateColumn({ nullable: true })
deletedAt: Date;

View File

@ -14,6 +14,7 @@ import { User } from '../../user/entities/user.entity';
import { Workspace } from '../../workspace/entities/workspace.entity';
import { Comment } from '../../comment/entities/comment.entity';
import { PageHistory } from './page-history.entity';
import { Space } from '../../space/entities/space.entity';
@Entity('pages')
@Index('pages_tsv_idx', ['tsv'])
@ -82,6 +83,13 @@ export class Page {
@JoinColumn({ name: 'deletedById' })
deletedBy: User;
@Column()
spaceId: string;
@ManyToOne(() => Space, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'spaceId' })
space: Space;
@Column()
workspaceId: string;

View File

@ -9,7 +9,7 @@ import {
import { PageService } from './services/page.service';
import { CreatePageDto } from './dto/create-page.dto';
import { UpdatePageDto } from './dto/update-page.dto';
import { JwtGuard } from '../auth/guards/JwtGuard';
import { JwtGuard } from '../auth/guards/jwt.guard';
import { WorkspaceService } from '../workspace/services/workspace.service';
import { MovePageDto } from './dto/move-page.dto';
import { PageDetailsDto } from './dto/page-details.dto';
@ -71,39 +71,27 @@ export class PageController {
@HttpCode(HttpStatus.OK)
@Post('recent')
async getRecentWorkspacePages(@JwtUser() jwtUser) {
const workspaceId = (
await this.workspaceService.getUserCurrentWorkspace(jwtUser.id)
).id;
return this.pageService.getRecentWorkspacePages(workspaceId);
async getRecentSpacePages(@Body() { spaceId }) {
console.log(spaceId);
return this.pageService.getRecentSpacePages(spaceId);
}
@HttpCode(HttpStatus.OK)
@Post()
async getWorkspacePages(@JwtUser() jwtUser) {
const workspaceId = (
await this.workspaceService.getUserCurrentWorkspace(jwtUser.id)
).id;
return this.pageService.getSidebarPagesByWorkspaceId(workspaceId);
async getSpacePages(spaceId: string) {
return this.pageService.getSidebarPagesBySpaceId(spaceId);
}
@HttpCode(HttpStatus.OK)
@Post('ordering')
async getWorkspacePageOrder(@JwtUser() jwtUser) {
const workspaceId = (
await this.workspaceService.getUserCurrentWorkspace(jwtUser.id)
).id;
return this.pageOrderService.getWorkspacePageOrder(workspaceId);
async getSpacePageOrder(spaceId: string) {
return this.pageOrderService.getSpacePageOrder(spaceId);
}
@HttpCode(HttpStatus.OK)
@Post('tree')
async workspacePageTree(@JwtUser() jwtUser) {
const workspaceId = (
await this.workspaceService.getUserCurrentWorkspace(jwtUser.id)
).id;
return this.pageOrderService.convertToTree(workspaceId);
async spacePageTree(@Body() { spaceId }) {
return this.pageOrderService.convertToTree(spaceId);
}
@HttpCode(HttpStatus.OK)

View File

@ -2,7 +2,8 @@ import { MovePageDto } from './dto/move-page.dto';
import { EntityManager } from 'typeorm';
export enum OrderingEntity {
workspace = 'WORKSPACE',
workspace = 'SPACE',
space = 'SPACE',
page = 'PAGE',
}

View File

@ -18,6 +18,7 @@ export class PageRepository extends Repository<Page> {
'page.parentPageId',
'page.creatorId',
'page.lastUpdatedById',
'page.spaceId',
'page.workspaceId',
'page.isLocked',
'page.status',

View File

@ -34,7 +34,7 @@ export class PageOrderingService {
const movedPage = await manager
.createQueryBuilder(Page, 'page')
.where('page.id = :movedPageId', { movedPageId })
.select(['page.id', 'page.workspaceId', 'page.parentPageId'])
.select(['page.id', 'page.spaceId', 'page.parentPageId'])
.getOne();
if (!movedPage) throw new BadRequestException('Moved page not found');
@ -43,15 +43,15 @@ export class PageOrderingService {
if (movedPage.parentPageId) {
await this.removeFromParent(movedPage.parentPageId, dto.id, manager);
}
const workspaceOrdering = await this.getEntityOrdering(
movedPage.workspaceId,
OrderingEntity.workspace,
const spaceOrdering = await this.getEntityOrdering(
movedPage.spaceId,
OrderingEntity.space,
manager,
);
orderPageList(workspaceOrdering.childrenIds, dto);
orderPageList(spaceOrdering.childrenIds, dto);
await manager.save(workspaceOrdering);
await manager.save(spaceOrdering);
} else {
const parentPageId = dto.parentId;
@ -65,7 +65,7 @@ export class PageOrderingService {
parentPageOrdering = await this.createPageOrdering(
parentPageId,
OrderingEntity.page,
movedPage.workspaceId,
movedPage.spaceId,
manager,
);
}
@ -78,8 +78,8 @@ export class PageOrderingService {
// If movedPage didn't have a parent initially (was at root level), update the root level
if (!movedPage.parentPageId) {
await this.removeFromWorkspacePageOrder(
movedPage.workspaceId,
await this.removeFromSpacePageOrder(
movedPage.spaceId,
dto.id,
manager,
);
@ -95,36 +95,32 @@ export class PageOrderingService {
});
}
async addPageToOrder(
workspaceId: string,
pageId: string,
parentPageId?: string,
) {
async addPageToOrder(spaceId: string, pageId: string, parentPageId?: string) {
await this.dataSource.transaction(async (manager: EntityManager) => {
if (parentPageId) {
await this.upsertOrdering(
parentPageId,
OrderingEntity.page,
pageId,
workspaceId,
spaceId,
manager,
);
} else {
await this.addToWorkspacePageOrder(workspaceId, pageId, manager);
await this.addToSpacePageOrder(spaceId, pageId, manager);
}
});
}
async addToWorkspacePageOrder(
workspaceId: string,
async addToSpacePageOrder(
spaceId: string,
pageId: string,
manager: EntityManager,
) {
await this.upsertOrdering(
workspaceId,
OrderingEntity.workspace,
spaceId,
OrderingEntity.space,
pageId,
workspaceId,
spaceId,
manager,
);
}
@ -142,14 +138,14 @@ export class PageOrderingService {
);
}
async removeFromWorkspacePageOrder(
workspaceId: string,
async removeFromSpacePageOrder(
spaceId: string,
pageId: string,
manager: EntityManager,
) {
await this.removeChildFromOrdering(
workspaceId,
OrderingEntity.workspace,
spaceId,
OrderingEntity.space,
pageId,
manager,
);
@ -179,11 +175,7 @@ export class PageOrderingService {
if (page.parentPageId) {
await this.removeFromParent(page.parentPageId, page.id, manager);
} else {
await this.removeFromWorkspacePageOrder(
page.workspaceId,
page.id,
manager,
);
await this.removeFromSpacePageOrder(page.spaceId, page.id, manager);
}
}
@ -191,7 +183,7 @@ export class PageOrderingService {
entityId: string,
entityType: string,
childId: string,
workspaceId: string,
spaceId: string,
manager: EntityManager,
) {
let ordering = await this.getEntityOrdering(entityId, entityType, manager);
@ -200,7 +192,7 @@ export class PageOrderingService {
ordering = await this.createPageOrdering(
entityId,
entityType,
workspaceId,
spaceId,
manager,
);
}
@ -229,35 +221,35 @@ export class PageOrderingService {
async createPageOrdering(
entityId: string,
entityType: string,
workspaceId: string,
spaceId: string,
manager: EntityManager,
): Promise<PageOrdering> {
await manager.query(
`INSERT INTO page_ordering ("entityId", "entityType", "workspaceId", "childrenIds")
`INSERT INTO page_ordering ("entityId", "entityType", "spaceId", "childrenIds")
VALUES ($1, $2, $3, '{}')
ON CONFLICT ("entityId", "entityType") DO NOTHING`,
[entityId, entityType, workspaceId],
[entityId, entityType, spaceId],
);
return await this.getEntityOrdering(entityId, entityType, manager);
}
async getWorkspacePageOrder(workspaceId: string): Promise<PageOrdering> {
async getSpacePageOrder(spaceId: string): Promise<PageOrdering> {
return await this.dataSource
.createQueryBuilder(PageOrdering, 'ordering')
.select(['ordering.id', 'ordering.childrenIds', 'ordering.workspaceId'])
.where('ordering.entityId = :workspaceId', { workspaceId })
.select(['ordering.id', 'ordering.childrenIds', 'ordering.spaceId'])
.where('ordering.entityId = :spaceId', { spaceId })
.andWhere('ordering.entityType = :entityType', {
entityType: OrderingEntity.workspace,
entityType: OrderingEntity.space,
})
.getOne();
}
async convertToTree(workspaceId: string): Promise<TreeNode[]> {
const workspaceOrder = await this.getWorkspacePageOrder(workspaceId);
async convertToTree(spaceId: string): Promise<TreeNode[]> {
const spaceOrder = await this.getSpacePageOrder(spaceId);
const pageOrder = workspaceOrder ? workspaceOrder.childrenIds : undefined;
const pages = await this.pageService.getSidebarPagesByWorkspaceId(workspaceId);
const pageOrder = spaceOrder ? spaceOrder.childrenIds : undefined;
const pages = await this.pageService.getSidebarPagesBySpaceId(spaceId);
const pageMap: { [id: string]: PageWithOrderingDto } = {};
pages.forEach((page) => {

View File

@ -67,7 +67,7 @@ export class PageService {
page.lastUpdatedById = userId;
if (createPageDto.parentPageId) {
// TODO: make sure parent page belongs to same workspace and user has permissions
// TODO: make sure parent page belongs to same space and user has permissions
const parentPage = await this.pageRepository.findOne({
where: { id: createPageDto.parentPageId },
select: ['id'],
@ -79,7 +79,7 @@ export class PageService {
const createdPage = await this.pageRepository.save(page);
await this.pageOrderingService.addPageToOrder(
workspaceId,
createPageDto.spaceId,
createPageDto.id,
createPageDto.parentPageId,
);
@ -174,12 +174,7 @@ export class PageService {
const restoredPage = await manager
.createQueryBuilder(Page, 'page')
.where('page.id = :pageId', { pageId })
.select([
'page.id',
'page.title',
'page.workspaceId',
'page.parentPageId',
])
.select(['page.id', 'page.title', 'page.spaceId', 'page.parentPageId'])
.getOne();
if (!restoredPage) {
@ -188,7 +183,7 @@ export class PageService {
// add page back to its hierarchy
await this.pageOrderingService.addPageToOrder(
restoredPage.workspaceId,
restoredPage.spaceId,
pageId,
restoredPage.parentPageId,
);
@ -222,8 +217,8 @@ export class PageService {
return await this.pageRepository.findById(pageId);
}
async getSidebarPagesByWorkspaceId(
workspaceId: string,
async getSidebarPagesBySpaceId(
spaceId: string,
limit = 200,
): Promise<PageWithOrderingDto[]> {
const pages = await this.pageRepository
@ -234,12 +229,13 @@ export class PageService {
'ordering.entityId = page.id AND ordering.entityType = :entityType',
{ entityType: OrderingEntity.page },
)
.where('page.workspaceId = :workspaceId', { workspaceId })
.where('page.spaceId = :spaceId', { spaceId })
.select([
'page.id',
'page.title',
'page.icon',
'page.parentPageId',
'page.spaceId',
'ordering.childrenIds',
'page.creatorId',
'page.createdAt',
@ -251,14 +247,14 @@ export class PageService {
return transformPageResult(pages);
}
async getRecentWorkspacePages(
workspaceId: string,
async getRecentSpacePages(
spaceId: string,
limit = 20,
offset = 0,
): Promise<Page[]> {
const pages = await this.pageRepository
.createQueryBuilder('page')
.where('page.workspaceId = :workspaceId', { workspaceId })
.where('page.spaceId = :spaceId', { spaceId })
.select(this.pageRepository.baseFields)
.orderBy('page.updatedAt', 'DESC')
.offset(offset)

View File

@ -0,0 +1,12 @@
import { IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
export class CreateSpaceDto {
@MinLength(4)
@MaxLength(64)
@IsString()
name: string;
@IsOptional()
@IsString()
description?: string;
}

View File

@ -0,0 +1,6 @@
import { IsString } from 'class-validator';
export class DeleteSpaceDto {
@IsString()
spaceId: string;
}

View File

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateSpaceDto } from './create-space.dto';
export class UpdateSpaceDto extends PartialType(CreateSpaceDto) {}

View File

@ -0,0 +1,46 @@
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.spaceUsers, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'userId' })
user: User;
@Column()
spaceId: string;
@ManyToOne(() => Space, (space) => space.spaceUsers, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'spaceId' })
space: Space;
@Column({ length: 100, nullable: true })
role: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,54 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { User } from '../../user/entities/user.entity';
import { Workspace } from '../../workspace/entities/workspace.entity';
import { SpaceUser } from './space-user.entity';
@Entity('spaces')
export class Space {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ length: 255, nullable: true })
name: string;
@Column({ type: 'text', nullable: true })
description: string;
@Column({ length: 255, nullable: true })
icon: string;
@Column({ length: 255, nullable: true, unique: true })
hostname: string;
@Column()
creatorId: string;
@ManyToOne(() => User, (user) => user.spaces)
@JoinColumn({ name: 'creatorId' })
creator: User;
@Column()
workspaceId: string;
@ManyToOne(() => Workspace, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'workspaceId' })
workspace: Workspace;
@OneToMany(() => SpaceUser, (workspaceUser) => workspaceUser.space)
spaceUsers: SpaceUser[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,10 @@
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,14 @@
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { Space } from '../entities/space.entity';
@Injectable()
export class SpaceRepository extends Repository<Space> {
constructor(private dataSource: DataSource) {
super(Space, dataSource.createEntityManager());
}
async findById(spaceId: string) {
return this.findOneBy({ id: spaceId });
}
}

View File

@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SpaceController } from './space.controller';
import { SpaceService } from './space.service';
describe('SpaceController', () => {
let controller: SpaceController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [SpaceController],
providers: [SpaceService],
}).compile();
controller = module.get<SpaceController>(SpaceController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,22 @@
import {
Controller,
HttpCode,
HttpStatus,
Post,
Req,
UseGuards,
} from '@nestjs/common';
import { FastifyRequest } from 'fastify';
import { JwtGuard } from '../auth/guards/jwt.guard';
import { SpaceService } from './space.service';
@UseGuards(JwtGuard)
@Controller('spaces')
export class SpaceController {
constructor(private readonly spaceService: SpaceService) {}
// get all spaces user is a member of
@HttpCode(HttpStatus.OK)
@Post('/')
async getUserSpaces(@Req() req: FastifyRequest) {}
}

View File

@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { SpaceService } from './space.service';
import { SpaceController } from './space.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Space } from './entities/space.entity';
import { AuthModule } from '../auth/auth.module';
import { SpaceUser } from './entities/space-user.entity';
import { SpaceRepository } from './repositories/space.repository';
import { SpaceUserRepository } from './repositories/space-user.repository';
@Module({
imports: [TypeOrmModule.forFeature([Space, SpaceUser]), AuthModule],
controllers: [SpaceController],
providers: [SpaceService, SpaceRepository, SpaceUserRepository],
exports: [SpaceService, SpaceRepository, SpaceUserRepository],
})
export class SpaceModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SpaceService } from './space.service';
describe('SpaceService', () => {
let service: SpaceService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [SpaceService],
}).compile();
service = module.get<SpaceService>(SpaceService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,69 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { CreateSpaceDto } from './dto/create-space.dto';
import { Space } from './entities/space.entity';
import { plainToInstance } from 'class-transformer';
import { SpaceRepository } from './repositories/space.repository';
import { SpaceUserRepository } from './repositories/space-user.repository';
import { SpaceUser } from './entities/space-user.entity';
@Injectable()
export class SpaceService {
constructor(
private spaceRepository: SpaceRepository,
private spaceUserRepository: SpaceUserRepository,
) {}
async create(userId: string, workspaceId, createSpaceDto?: CreateSpaceDto) {
let space: Space;
if (createSpaceDto) {
space = plainToInstance(Space, createSpaceDto);
} else {
space = new Space();
}
space.creatorId = userId;
space.workspaceId = workspaceId;
space.name = createSpaceDto?.name ?? 'untitled space';
space.description = createSpaceDto?.description ?? null;
space = await this.spaceRepository.save(space);
return space;
}
async addUserToSpace(
userId: string,
spaceId: string,
role: string,
): Promise<SpaceUser> {
const existingSpaceUser = await this.spaceUserRepository.findOne({
where: { 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;
return this.spaceUserRepository.save(spaceUser);
}
async getUserSpacesInWorkspace(userId: string, workspaceId: string) {
const spaces = await this.spaceUserRepository.find({
relations: ['space'],
where: {
userId: userId,
space: {
workspaceId: workspaceId,
},
},
});
return spaces.map((userSpace: SpaceUser) => userSpace.space);
}
}

View File

@ -12,6 +12,8 @@ import { Workspace } from '../../workspace/entities/workspace.entity';
import { WorkspaceUser } from '../../workspace/entities/workspace-user.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';
@Entity('users')
export class User {
@ -66,6 +68,12 @@ export class User {
@OneToMany(() => Comment, (comment) => comment.creator)
comments: Comment[];
@OneToMany(() => Space, (space) => space.creator)
spaces: Space[];
@OneToMany(() => SpaceUser, (spaceUser) => spaceUser.user)
spaceUsers: SpaceUser[];
toJSON() {
delete this.password;
return this;

View File

@ -10,7 +10,7 @@ import {
Body,
} from '@nestjs/common';
import { UserService } from './user.service';
import { JwtGuard } from '../auth/guards/JwtGuard';
import { JwtGuard } from '../auth/guards/jwt.guard';
import { FastifyRequest } from 'fastify';
import { User } from './entities/user.entity';
import { Workspace } from '../workspace/entities/workspace.entity';

View File

@ -1,4 +1,4 @@
import { forwardRef, Module } from '@nestjs/common';
import { forwardRef, Global, Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
@ -7,6 +7,7 @@ import { UserRepository } from './repositories/user.repository';
import { AuthModule } from '../auth/auth.module';
import { WorkspaceModule } from '../workspace/workspace.module';
@Global()
@Module({
imports: [
TypeOrmModule.forFeature([User]),

View File

@ -31,16 +31,24 @@ export class UserService {
user = await this.userRepository.save(user);
//TODO: only create workspace if it is not a signup to an existing workspace
await this.workspaceService.create(user.id);
await this.workspaceService.createOrJoinWorkspace(user.id);
return user;
}
async getUserInstance(userId: string) {
const user: User = await this.findById(userId);
if (!user) {
throw new NotFoundException('User not found');
}
const workspace: Workspace =
await this.workspaceService.getUserCurrentWorkspace(userId);
if (!workspace) {
throw new NotFoundException('Workspace not found');
}
return { user, workspace };
}

View File

@ -11,7 +11,7 @@ import {
} from '@nestjs/common';
import { WorkspaceService } from '../services/workspace.service';
import { FastifyRequest } from 'fastify';
import { JwtGuard } from '../../auth/guards/JwtGuard';
import { JwtGuard } from '../../auth/guards/jwt.guard';
import { UpdateWorkspaceDto } from '../dto/update-workspace.dto';
import { CreateWorkspaceDto } from '../dto/create-workspace.dto';
import { DeleteWorkspaceDto } from '../dto/delete-workspace.dto';
@ -24,6 +24,17 @@ import { AddWorkspaceUserDto } from '../dto/add-workspace-user.dto';
export class WorkspaceController {
constructor(private readonly workspaceService: WorkspaceService) {}
@HttpCode(HttpStatus.OK)
@Post('test')
async test(
@Req() req: FastifyRequest,
//@Body() createWorkspaceDto: CreateWorkspaceDto,
) {
//const jwtPayload = req['user'];
// const userId = jwtPayload.sub;
// return this.workspaceService.createOrJoinWorkspace();
}
@HttpCode(HttpStatus.OK)
@Post('create')
async createWorkspace(

View File

@ -14,12 +14,14 @@ import { generateHostname } from '../workspace.util';
import { UpdateWorkspaceDto } from '../dto/update-workspace.dto';
import { DeleteWorkspaceDto } from '../dto/delete-workspace.dto';
import { UpdateWorkspaceUserRoleDto } from '../dto/update-workspace-user-role.dto';
import { SpaceService } from '../../space/space.service';
@Injectable()
export class WorkspaceService {
constructor(
private workspaceRepository: WorkspaceRepository,
private workspaceUserRepository: WorkspaceUserRepository,
private spaceService: SpaceService,
) {}
async findById(workspaceId: string): Promise<Workspace> {
@ -30,6 +32,45 @@ export class WorkspaceService {
return this.workspaceRepository.save(workspace);
}
async createOrJoinWorkspace(userId) {
// context:
// only create workspace if it is not a signup to an existing workspace
// OS version is limited to one workspace.
// if there is no existing workspace, create a new workspace
// and make first user owner/admin
// check if workspace already exists in the db
// if not create default, if yes add the user to existing workspace if signup is open.
const workspaceCount = await this.workspaceRepository.count();
if (workspaceCount === 0) {
// create first workspace
// add user to workspace as admin
const newWorkspace = await this.create(userId);
await this.addUserToWorkspace(userId, newWorkspace.id, 'owner');
// maybe create default space and add user to it too.
const newSpace = await this.spaceService.create(userId, newWorkspace.id);
await this.spaceService.addUserToSpace(userId, newSpace.id, 'owner');
} else {
//TODO: accept role as param
// if no role is passed use default new member role found in workspace settings
// fetch the oldest workspace and add user to it
const firstWorkspace = await this.workspaceRepository.find({
order: {
createdAt: 'ASC',
},
take: 1,
});
await this.addUserToWorkspace(userId, firstWorkspace[0].id, 'member');
// get workspace
// if there is a default space, we should add new users to it.
}
}
async create(
userId: string,
createWorkspaceDto?: CreateWorkspaceDto,
@ -50,7 +91,6 @@ export class WorkspaceService {
}
workspace = await this.workspaceRepository.save(workspace);
await this.addUserToWorkspace(userId, workspace.id, 'owner');
return workspace;
}
@ -151,6 +191,10 @@ export class WorkspaceService {
relations: ['workspace'],
});
if (!userWorkspace) {
throw new NotFoundException('No workspace found for this user');
}
return userWorkspace.workspace;
}

View File

@ -8,11 +8,13 @@ import { WorkspaceUser } from './entities/workspace-user.entity';
import { WorkspaceInvitation } from './entities/workspace-invitation.entity';
import { WorkspaceUserRepository } from './repositories/workspace-user.repository';
import { AuthModule } from '../auth/auth.module';
import { SpaceModule } from '../space/space.module';
@Module({
imports: [
TypeOrmModule.forFeature([Workspace, WorkspaceUser, WorkspaceInvitation]),
AuthModule,
SpaceModule,
],
controllers: [WorkspaceController],
providers: [WorkspaceService, WorkspaceRepository, WorkspaceUserRepository],

View File

@ -0,0 +1,30 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class Spaces1706807570313 implements MigrationInterface {
name = 'Spaces1706807570313'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "spaces" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255), "description" text, "icon" character varying(255), "hostname" character varying(255), "creatorId" uuid NOT NULL, "workspaceId" uuid NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_4f0a029f6eefd773fde2143b261" UNIQUE ("hostname"), CONSTRAINT "PK_dbe542974aca57afcb60709d4c8" PRIMARY KEY ("id"))`);
await queryRunner.query(`ALTER TABLE "page_history" ADD "spaceId" uuid NOT NULL`);
await queryRunner.query(`ALTER TABLE "pages" ADD "spaceId" uuid NOT NULL`);
await queryRunner.query(`ALTER TABLE "page_ordering" ADD "spaceId" uuid NOT NULL`);
await queryRunner.query(`ALTER TABLE "spaces" ADD CONSTRAINT "FK_8469f60fb94d43a0280a83d0b35" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "spaces" ADD CONSTRAINT "FK_f8c6dec54d8a2fdd26ea036fc8d" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "page_history" ADD CONSTRAINT "FK_8caa9d435480f4390b9885c4bd0" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "pages" ADD CONSTRAINT "FK_c4aef9b23f1222bebc5897de72d" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "page_ordering" ADD CONSTRAINT "FK_17f9d5fd14d32a81d58ad747359" 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 "page_ordering" DROP CONSTRAINT "FK_17f9d5fd14d32a81d58ad747359"`);
await queryRunner.query(`ALTER TABLE "pages" DROP CONSTRAINT "FK_c4aef9b23f1222bebc5897de72d"`);
await queryRunner.query(`ALTER TABLE "page_history" DROP CONSTRAINT "FK_8caa9d435480f4390b9885c4bd0"`);
await queryRunner.query(`ALTER TABLE "spaces" DROP CONSTRAINT "FK_f8c6dec54d8a2fdd26ea036fc8d"`);
await queryRunner.query(`ALTER TABLE "spaces" DROP CONSTRAINT "FK_8469f60fb94d43a0280a83d0b35"`);
await queryRunner.query(`ALTER TABLE "page_ordering" DROP COLUMN "spaceId"`);
await queryRunner.query(`ALTER TABLE "pages" DROP COLUMN "spaceId"`);
await queryRunner.query(`ALTER TABLE "page_history" DROP COLUMN "spaceId"`);
await queryRunner.query(`DROP TABLE "spaces"`);
}
}

View File

@ -0,0 +1,18 @@
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"`);
}
}