mirror of
https://github.com/docmost/docmost.git
synced 2025-11-17 09:31:10 +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],
|
||||
|
||||
Reference in New Issue
Block a user