mirror of
https://github.com/docmost/docmost.git
synced 2025-11-13 05:22:37 +10:00
feat: spaces - WIP
This commit is contained in:
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
|
||||
@ -16,4 +16,7 @@ export class CreatePageDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentPageId?: string;
|
||||
|
||||
@IsString()
|
||||
spaceId: string;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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',
|
||||
}
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ export class PageRepository extends Repository<Page> {
|
||||
'page.parentPageId',
|
||||
'page.creatorId',
|
||||
'page.lastUpdatedById',
|
||||
'page.spaceId',
|
||||
'page.workspaceId',
|
||||
'page.isLocked',
|
||||
'page.status',
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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)
|
||||
|
||||
12
apps/server/src/core/space/dto/create-space.dto.ts
Normal file
12
apps/server/src/core/space/dto/create-space.dto.ts
Normal 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;
|
||||
}
|
||||
6
apps/server/src/core/space/dto/delete-space.dto.ts
Normal file
6
apps/server/src/core/space/dto/delete-space.dto.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { IsString } from 'class-validator';
|
||||
|
||||
export class DeleteSpaceDto {
|
||||
@IsString()
|
||||
spaceId: string;
|
||||
}
|
||||
4
apps/server/src/core/space/dto/update-space.dto.ts
Normal file
4
apps/server/src/core/space/dto/update-space.dto.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateSpaceDto } from './create-space.dto';
|
||||
|
||||
export class UpdateSpaceDto extends PartialType(CreateSpaceDto) {}
|
||||
46
apps/server/src/core/space/entities/space-user.entity.ts
Normal file
46
apps/server/src/core/space/entities/space-user.entity.ts
Normal 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;
|
||||
}
|
||||
54
apps/server/src/core/space/entities/space.entity.ts
Normal file
54
apps/server/src/core/space/entities/space.entity.ts
Normal 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;
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
14
apps/server/src/core/space/repositories/space.repository.ts
Normal file
14
apps/server/src/core/space/repositories/space.repository.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
20
apps/server/src/core/space/space.controller.spec.ts
Normal file
20
apps/server/src/core/space/space.controller.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
22
apps/server/src/core/space/space.controller.ts
Normal file
22
apps/server/src/core/space/space.controller.ts
Normal 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) {}
|
||||
}
|
||||
17
apps/server/src/core/space/space.module.ts
Normal file
17
apps/server/src/core/space/space.module.ts
Normal 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 {}
|
||||
18
apps/server/src/core/space/space.service.spec.ts
Normal file
18
apps/server/src/core/space/space.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
69
apps/server/src/core/space/space.service.ts
Normal file
69
apps/server/src/core/space/space.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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]),
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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],
|
||||
|
||||
30
apps/server/src/database/migrations/1706807570313-Spaces.ts
Normal file
30
apps/server/src/database/migrations/1706807570313-Spaces.ts
Normal 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"`);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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"`);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user