mirror of
https://github.com/docmost/docmost.git
synced 2025-11-13 04:32:41 +10:00
Refactoring
* replace TypeORM with Kysely query builder * refactor migrations * other changes and fixes
This commit is contained in:
@ -18,17 +18,11 @@
|
|||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
"test:e2e": "jest --config test/jest-e2e.json",
|
"test:e2e": "jest --config test/jest-e2e.json",
|
||||||
"typeorm": "typeorm-ts-node-commonjs -d src/database/typeorm.config.ts",
|
"migration:create": "tsx ./src/kysely/migrate.ts create",
|
||||||
"migration:generate": "cd ./src/database/migrations/ && pnpm run typeorm migration:generate",
|
|
||||||
"migration:create": "cd ./src/database/migrations/ && typeorm-ts-node-commonjs migration:create",
|
|
||||||
"migration:run": "pnpm run typeorm migration:run",
|
|
||||||
"migration:revert": "pnpm run typeorm migration:revert",
|
|
||||||
"migration:show": "pnpm run typeorm migration:show",
|
|
||||||
"migration:make": "tsx ./src/kysely/migrate.ts create",
|
|
||||||
"migration:up": "tsx ./src/kysely/migrate.ts up",
|
"migration:up": "tsx ./src/kysely/migrate.ts up",
|
||||||
"migration:down": "tsx ./src/kysely/migrate.ts down",
|
"migration:down": "tsx ./src/kysely/migrate.ts down",
|
||||||
"migration:latest": "tsx ./src/kysely/migrate.ts latest",
|
"migration:latest": "tsx ./src/kysely/migrate.ts latest",
|
||||||
"migration:reset": "tsx ./src/kysely/migrate.ts redo",
|
"migration:redo": "tsx ./src/kysely/migrate.ts redo",
|
||||||
"migration:codegen": "kysely-codegen --dialect=postgres --env-file=../../.env --out-file=./src/kysely/types/db.d.ts"
|
"migration:codegen": "kysely-codegen --dialect=postgres --env-file=../../.env --out-file=./src/kysely/types/db.d.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -46,7 +40,6 @@
|
|||||||
"@nestjs/platform-fastify": "^10.3.5",
|
"@nestjs/platform-fastify": "^10.3.5",
|
||||||
"@nestjs/platform-socket.io": "^10.3.5",
|
"@nestjs/platform-socket.io": "^10.3.5",
|
||||||
"@nestjs/serve-static": "^4.0.1",
|
"@nestjs/serve-static": "^4.0.1",
|
||||||
"@nestjs/typeorm": "^10.0.2",
|
|
||||||
"@nestjs/websockets": "^10.3.5",
|
"@nestjs/websockets": "^10.3.5",
|
||||||
"@types/pg": "^8.11.4",
|
"@types/pg": "^8.11.4",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
@ -60,14 +53,13 @@
|
|||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"nestjs-kysely": "^0.1.6",
|
"nestjs-kysely": "^0.1.6",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.",
|
||||||
"pg-tsquery": "^8.4.2",
|
"pg-tsquery": "^8.4.2",
|
||||||
"reflect-metadata": "^0.2.1",
|
"reflect-metadata": "^0.2.1",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"sanitize-filename-ts": "^1.0.2",
|
"sanitize-filename-ts": "^1.0.2",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"socket.io": "^4.7.5",
|
"socket.io": "^4.7.5",
|
||||||
"typeorm": "^0.3.20",
|
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"ws": "^8.16.0"
|
"ws": "^8.16.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -4,38 +4,21 @@ import { AppService } from './app.service';
|
|||||||
import { CoreModule } from './core/core.module';
|
import { CoreModule } from './core/core.module';
|
||||||
import { EnvironmentModule } from './integrations/environment/environment.module';
|
import { EnvironmentModule } from './integrations/environment/environment.module';
|
||||||
import { CollaborationModule } from './collaboration/collaboration.module';
|
import { CollaborationModule } from './collaboration/collaboration.module';
|
||||||
import { DatabaseModule } from './database/database.module';
|
|
||||||
import { WsModule } from './ws/ws.module';
|
import { WsModule } from './ws/ws.module';
|
||||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { KyselyModule } from 'nestjs-kysely';
|
import { KyselyDbModule } from './kysely/kysely-db.module';
|
||||||
import { EnvironmentService } from './integrations/environment/environment.service';
|
|
||||||
import { PostgresDialect } from 'kysely';
|
|
||||||
import { Pool } from 'pg';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
CoreModule,
|
CoreModule,
|
||||||
|
KyselyDbModule,
|
||||||
EnvironmentModule,
|
EnvironmentModule,
|
||||||
DatabaseModule,
|
|
||||||
CollaborationModule,
|
CollaborationModule,
|
||||||
WsModule,
|
WsModule,
|
||||||
ServeStaticModule.forRoot({
|
ServeStaticModule.forRoot({
|
||||||
rootPath: join(__dirname, '..', '..', '..', 'client/dist'),
|
rootPath: join(__dirname, '..', '..', '..', 'client/dist'),
|
||||||
}),
|
}),
|
||||||
KyselyModule.forRootAsync({
|
|
||||||
imports: [],
|
|
||||||
inject: [EnvironmentService],
|
|
||||||
useFactory: (envService: EnvironmentService) => {
|
|
||||||
return {
|
|
||||||
dialect: new PostgresDialect({
|
|
||||||
pool: new Pool({
|
|
||||||
connectionString: envService.getDatabaseURL(),
|
|
||||||
}) as any,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
import { Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||||
import { UserModule } from '../core/user/user.module';
|
|
||||||
import { AuthModule } from '../core/auth/auth.module';
|
import { AuthModule } from '../core/auth/auth.module';
|
||||||
import { AuthenticationExtension } from './extensions/authentication.extension';
|
import { AuthenticationExtension } from './extensions/authentication.extension';
|
||||||
import { PersistenceExtension } from './extensions/persistence.extension';
|
import { PersistenceExtension } from './extensions/persistence.extension';
|
||||||
@ -18,7 +17,7 @@ import { HistoryExtension } from './extensions/history.extension';
|
|||||||
PersistenceExtension,
|
PersistenceExtension,
|
||||||
HistoryExtension,
|
HistoryExtension,
|
||||||
],
|
],
|
||||||
imports: [UserModule, AuthModule, PageModule],
|
imports: [AuthModule, PageModule],
|
||||||
})
|
})
|
||||||
export class CollaborationModule implements OnModuleInit, OnModuleDestroy {
|
export class CollaborationModule implements OnModuleInit, OnModuleDestroy {
|
||||||
private collabWsAdapter: CollabWsAdapter;
|
private collabWsAdapter: CollabWsAdapter;
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { Extension, onAuthenticatePayload } from '@hocuspocus/server';
|
import { Extension, onAuthenticatePayload } from '@hocuspocus/server';
|
||||||
import { UserService } from '../../core/user/user.service';
|
|
||||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
import { TokenService } from '../../core/auth/services/token.service';
|
import { TokenService } from '../../core/auth/services/token.service';
|
||||||
|
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthenticationExtension implements Extension {
|
export class AuthenticationExtension implements Extension {
|
||||||
constructor(
|
constructor(
|
||||||
private tokenService: TokenService,
|
private tokenService: TokenService,
|
||||||
private userService: UserService,
|
private userRepo: UserRepo,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onAuthenticate(data: onAuthenticatePayload) {
|
async onAuthenticate(data: onAuthenticatePayload) {
|
||||||
@ -22,7 +22,9 @@ export class AuthenticationExtension implements Extension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userId = jwtPayload.sub;
|
const userId = jwtPayload.sub;
|
||||||
const user = await this.userService.findById(userId);
|
const workspaceId = jwtPayload.workspaceId;
|
||||||
|
|
||||||
|
const user = await this.userRepo.findById(userId, workspaceId);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException();
|
||||||
|
|||||||
@ -53,7 +53,8 @@ export class HistoryExtension implements Extension {
|
|||||||
|
|
||||||
async recordHistory(pageId: string) {
|
async recordHistory(pageId: string) {
|
||||||
try {
|
try {
|
||||||
const page = await this.pageService.findWithContent(pageId);
|
const includeContent = true;
|
||||||
|
const page = await this.pageService.findById(pageId, includeContent);
|
||||||
// Todo: compare if data is the same as the previous version
|
// Todo: compare if data is the same as the previous version
|
||||||
await this.pageHistoryService.saveHistory(page);
|
await this.pageHistoryService.saveHistory(page);
|
||||||
console.log(`New history created for: ${pageId}`);
|
console.log(`New history created for: ${pageId}`);
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export class PersistenceExtension implements Extension {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const page = await this.pageService.findWithAllFields(pageId);
|
const page = await this.pageService.findById(pageId, true, true);
|
||||||
|
|
||||||
if (!page) {
|
if (!page) {
|
||||||
console.log('page does not exist.');
|
console.log('page does not exist.');
|
||||||
|
|||||||
@ -14,10 +14,9 @@ import { FastifyReply, FastifyRequest } from 'fastify';
|
|||||||
import { AttachmentInterceptor } from './attachment.interceptor';
|
import { AttachmentInterceptor } from './attachment.interceptor';
|
||||||
import * as bytes from 'bytes';
|
import * as bytes from 'bytes';
|
||||||
import { AuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUser } from '../../decorators/auth-user.decorator';
|
||||||
import { User } from '../user/entities/user.entity';
|
|
||||||
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
|
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
|
||||||
import { Workspace } from '../workspace/entities/workspace.entity';
|
|
||||||
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
||||||
|
import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||||
|
|
||||||
@Controller('attachments')
|
@Controller('attachments')
|
||||||
export class AttachmentController {
|
export class AttachmentController {
|
||||||
@ -31,6 +30,7 @@ export class AttachmentController {
|
|||||||
@Req() req: FastifyRequest,
|
@Req() req: FastifyRequest,
|
||||||
@Res() res: FastifyReply,
|
@Res() res: FastifyReply,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
const maxFileSize = bytes('5MB');
|
const maxFileSize = bytes('5MB');
|
||||||
|
|
||||||
@ -42,6 +42,7 @@ export class AttachmentController {
|
|||||||
const fileResponse = await this.attachmentService.uploadAvatar(
|
const fileResponse = await this.attachmentService.uploadAvatar(
|
||||||
file,
|
file,
|
||||||
user.id,
|
user.id,
|
||||||
|
workspace.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.send(fileResponse);
|
return res.send(fileResponse);
|
||||||
|
|||||||
@ -2,20 +2,12 @@ import { Module } from '@nestjs/common';
|
|||||||
import { AttachmentService } from './attachment.service';
|
import { AttachmentService } from './attachment.service';
|
||||||
import { AttachmentController } from './attachment.controller';
|
import { AttachmentController } from './attachment.controller';
|
||||||
import { StorageModule } from '../../integrations/storage/storage.module';
|
import { StorageModule } from '../../integrations/storage/storage.module';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { Attachment } from './entities/attachment.entity';
|
|
||||||
import { AttachmentRepository } from './repositories/attachment.repository';
|
|
||||||
import { UserModule } from '../user/user.module';
|
import { UserModule } from '../user/user.module';
|
||||||
import { WorkspaceModule } from '../workspace/workspace.module';
|
import { WorkspaceModule } from '../workspace/workspace.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [StorageModule, UserModule, WorkspaceModule],
|
||||||
TypeOrmModule.forFeature([Attachment]),
|
|
||||||
StorageModule,
|
|
||||||
UserModule,
|
|
||||||
WorkspaceModule,
|
|
||||||
],
|
|
||||||
controllers: [AttachmentController],
|
controllers: [AttachmentController],
|
||||||
providers: [AttachmentService, AttachmentRepository],
|
providers: [AttachmentService],
|
||||||
})
|
})
|
||||||
export class AttachmentModule {}
|
export class AttachmentModule {}
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { StorageService } from '../../integrations/storage/storage.service';
|
import { StorageService } from '../../integrations/storage/storage.service';
|
||||||
import { MultipartFile } from '@fastify/multipart';
|
import { MultipartFile } from '@fastify/multipart';
|
||||||
import { AttachmentRepository } from './repositories/attachment.repository';
|
|
||||||
import { Attachment } from './entities/attachment.entity';
|
|
||||||
import { UserService } from '../user/user.service';
|
import { UserService } from '../user/user.service';
|
||||||
import { UpdateUserDto } from '../user/dto/update-user.dto';
|
import { UpdateUserDto } from '../user/dto/update-user.dto';
|
||||||
import {
|
import {
|
||||||
@ -15,14 +13,16 @@ import {
|
|||||||
import { v4 as uuid4 } from 'uuid';
|
import { v4 as uuid4 } from 'uuid';
|
||||||
import { WorkspaceService } from '../workspace/services/workspace.service';
|
import { WorkspaceService } from '../workspace/services/workspace.service';
|
||||||
import { UpdateWorkspaceDto } from '../workspace/dto/update-workspace.dto';
|
import { UpdateWorkspaceDto } from '../workspace/dto/update-workspace.dto';
|
||||||
|
import { AttachmentRepo } from '@docmost/db/repos/attachment/attachment.repo';
|
||||||
|
|
||||||
|
// TODO: make code better
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AttachmentService {
|
export class AttachmentService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly storageService: StorageService,
|
private readonly storageService: StorageService,
|
||||||
private readonly attachmentRepo: AttachmentRepository,
|
|
||||||
private readonly workspaceService: WorkspaceService,
|
private readonly workspaceService: WorkspaceService,
|
||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
|
private readonly attachmentRepo: AttachmentRepo,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async uploadToDrive(preparedFile: PreparedFile, filePath: string) {
|
async uploadToDrive(preparedFile: PreparedFile, filePath: string) {
|
||||||
@ -34,10 +34,10 @@ export class AttachmentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUserAvatar(userId: string, avatarUrl: string) {
|
async updateUserAvatar(avatarUrl: string, userId: string, workspaceId) {
|
||||||
const updateUserDto = new UpdateUserDto();
|
const updateUserDto = new UpdateUserDto();
|
||||||
updateUserDto.avatarUrl = avatarUrl;
|
updateUserDto.avatarUrl = avatarUrl;
|
||||||
await this.userService.update(userId, updateUserDto);
|
await this.userService.update(updateUserDto, userId, workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateWorkspaceLogo(workspaceId: string, logoUrl: string) {
|
async updateWorkspaceLogo(workspaceId: string, logoUrl: string) {
|
||||||
@ -46,7 +46,11 @@ export class AttachmentService {
|
|||||||
await this.workspaceService.update(workspaceId, updateWorkspaceDto);
|
await this.workspaceService.update(workspaceId, updateWorkspaceDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadAvatar(filePromise: Promise<MultipartFile>, userId: string) {
|
async uploadAvatar(
|
||||||
|
filePromise: Promise<MultipartFile>,
|
||||||
|
userId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const preparedFile: PreparedFile = await prepareFile(filePromise);
|
const preparedFile: PreparedFile = await prepareFile(filePromise);
|
||||||
const allowedImageTypes = ['.jpg', '.jpeg', '.png'];
|
const allowedImageTypes = ['.jpg', '.jpeg', '.png'];
|
||||||
@ -60,19 +64,19 @@ export class AttachmentService {
|
|||||||
|
|
||||||
await this.uploadToDrive(preparedFile, filePath);
|
await this.uploadToDrive(preparedFile, filePath);
|
||||||
|
|
||||||
const attachment = new Attachment();
|
// todo: in transaction
|
||||||
|
const attachment = await this.attachmentRepo.insertAttachment({
|
||||||
|
creatorId: userId,
|
||||||
|
type: AttachmentType.Avatar,
|
||||||
|
filePath: filePath,
|
||||||
|
fileName: preparedFile.fileName,
|
||||||
|
fileSize: preparedFile.fileSize,
|
||||||
|
mimeType: preparedFile.mimeType,
|
||||||
|
fileExt: preparedFile.fileExtension,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
attachment.creatorId = userId;
|
await this.updateUserAvatar(filePath, userId, workspaceId);
|
||||||
attachment.pageId = null;
|
|
||||||
attachment.workspaceId = null;
|
|
||||||
attachment.type = AttachmentType.Avatar;
|
|
||||||
attachment.filePath = filePath;
|
|
||||||
attachment.fileName = preparedFile.fileName;
|
|
||||||
attachment.fileSize = preparedFile.fileSize;
|
|
||||||
attachment.mimeType = preparedFile.mimeType;
|
|
||||||
attachment.fileExt = preparedFile.fileExtension;
|
|
||||||
|
|
||||||
await this.updateUserAvatar(userId, filePath);
|
|
||||||
|
|
||||||
return attachment;
|
return attachment;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -102,17 +106,17 @@ export class AttachmentService {
|
|||||||
|
|
||||||
await this.uploadToDrive(preparedFile, filePath);
|
await this.uploadToDrive(preparedFile, filePath);
|
||||||
|
|
||||||
const attachment = new Attachment();
|
// todo: in trx
|
||||||
|
const attachment = await this.attachmentRepo.insertAttachment({
|
||||||
attachment.creatorId = userId;
|
creatorId: userId,
|
||||||
attachment.pageId = null;
|
type: AttachmentType.WorkspaceLogo,
|
||||||
attachment.workspaceId = workspaceId;
|
filePath: filePath,
|
||||||
attachment.type = AttachmentType.WorkspaceLogo;
|
fileName: preparedFile.fileName,
|
||||||
attachment.filePath = filePath;
|
fileSize: preparedFile.fileSize,
|
||||||
attachment.fileName = preparedFile.fileName;
|
mimeType: preparedFile.mimeType,
|
||||||
attachment.fileSize = preparedFile.fileSize;
|
fileExt: preparedFile.fileExtension,
|
||||||
attachment.mimeType = preparedFile.mimeType;
|
workspaceId: workspaceId,
|
||||||
attachment.fileExt = preparedFile.fileExtension;
|
});
|
||||||
|
|
||||||
await this.updateWorkspaceLogo(workspaceId, filePath);
|
await this.updateWorkspaceLogo(workspaceId, filePath);
|
||||||
|
|
||||||
@ -143,17 +147,17 @@ export class AttachmentService {
|
|||||||
|
|
||||||
await this.uploadToDrive(preparedFile, filePath);
|
await this.uploadToDrive(preparedFile, filePath);
|
||||||
|
|
||||||
const attachment = new Attachment();
|
const attachment = await this.attachmentRepo.insertAttachment({
|
||||||
|
creatorId: userId,
|
||||||
attachment.creatorId = userId;
|
pageId: pageId,
|
||||||
attachment.pageId = pageId;
|
type: AttachmentType.File,
|
||||||
attachment.workspaceId = workspaceId;
|
filePath: filePath,
|
||||||
attachment.type = AttachmentType.WorkspaceLogo;
|
fileName: preparedFile.fileName,
|
||||||
attachment.filePath = filePath;
|
fileSize: preparedFile.fileSize,
|
||||||
attachment.fileName = preparedFile.fileName;
|
mimeType: preparedFile.mimeType,
|
||||||
attachment.fileSize = preparedFile.fileSize;
|
fileExt: preparedFile.fileExtension,
|
||||||
attachment.mimeType = preparedFile.mimeType;
|
workspaceId: workspaceId,
|
||||||
attachment.fileExt = preparedFile.fileExtension;
|
});
|
||||||
|
|
||||||
return attachment;
|
return attachment;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -1,65 +0,0 @@
|
|||||||
import {
|
|
||||||
Entity,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Column,
|
|
||||||
ManyToOne,
|
|
||||||
JoinColumn,
|
|
||||||
CreateDateColumn,
|
|
||||||
DeleteDateColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { User } from '../../user/entities/user.entity';
|
|
||||||
import { Page } from '../../page/entities/page.entity';
|
|
||||||
import { Workspace } from '../../workspace/entities/workspace.entity';
|
|
||||||
|
|
||||||
@Entity('attachments')
|
|
||||||
export class Attachment {
|
|
||||||
@PrimaryGeneratedColumn('uuid')
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ type: 'varchar', length: 255 })
|
|
||||||
fileName: string;
|
|
||||||
|
|
||||||
@Column({ type: 'varchar' })
|
|
||||||
filePath: string;
|
|
||||||
|
|
||||||
@Column({ type: 'bigint' })
|
|
||||||
fileSize: number;
|
|
||||||
|
|
||||||
@Column({ type: 'varchar', length: 55 })
|
|
||||||
fileExt: string;
|
|
||||||
|
|
||||||
@Column({ type: 'varchar', length: 255 })
|
|
||||||
mimeType: string;
|
|
||||||
|
|
||||||
@Column({ type: 'varchar', length: 55 })
|
|
||||||
type: string; // e.g. page / workspace / avatar
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
creatorId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => User)
|
|
||||||
@JoinColumn({ name: 'creatorId' })
|
|
||||||
creator: User;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
pageId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Page)
|
|
||||||
@JoinColumn({ name: 'pageId' })
|
|
||||||
page: Page;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
workspaceId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Workspace, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: 'workspaceId' })
|
|
||||||
workspace: Workspace;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@DeleteDateColumn({ nullable: true })
|
|
||||||
deletedAt: Date;
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
import { DataSource, Repository } from 'typeorm';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { Attachment } from '../entities/attachment.entity';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AttachmentRepository extends Repository<Attachment> {
|
|
||||||
constructor(private dataSource: DataSource) {
|
|
||||||
super(Attachment, dataSource.createEntityManager());
|
|
||||||
}
|
|
||||||
|
|
||||||
async findById(id: string) {
|
|
||||||
return this.findOneBy({ id: id });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import * as bcrypt from 'bcrypt';
|
|
||||||
|
|
||||||
export async function comparePasswordHash(
|
|
||||||
plainPassword: string,
|
|
||||||
passwordHash: string,
|
|
||||||
): Promise<boolean> {
|
|
||||||
return bcrypt.compare(plainPassword, passwordHash);
|
|
||||||
}
|
|
||||||
@ -1,11 +1,12 @@
|
|||||||
import { CanActivate, ForbiddenException, Injectable } from '@nestjs/common';
|
import { CanActivate, ForbiddenException, Injectable } from '@nestjs/common';
|
||||||
import { WorkspaceRepository } from '../../workspace/repositories/workspace.repository';
|
import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SetupGuard implements CanActivate {
|
export class SetupGuard implements CanActivate {
|
||||||
constructor(private workspaceRepository: WorkspaceRepository) {}
|
constructor(private workspaceRepo: WorkspaceRepo) {}
|
||||||
|
|
||||||
async canActivate(): Promise<boolean> {
|
async canActivate(): Promise<boolean> {
|
||||||
const workspaceCount = await this.workspaceRepository.count();
|
const workspaceCount = await this.workspaceRepo.count();
|
||||||
if (workspaceCount > 0) {
|
if (workspaceCount > 0) {
|
||||||
throw new ForbiddenException('Workspace setup already completed.');
|
throw new ForbiddenException('Workspace setup already completed.');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
import { LoginDto } from '../dto/login.dto';
|
import { LoginDto } from '../dto/login.dto';
|
||||||
import { User } from '../../user/entities/user.entity';
|
|
||||||
import { CreateUserDto } from '../dto/create-user.dto';
|
import { CreateUserDto } from '../dto/create-user.dto';
|
||||||
import { UserService } from '../../user/user.service';
|
import { UserService } from '../../user/user.service';
|
||||||
import { TokenService } from './token.service';
|
import { TokenService } from './token.service';
|
||||||
import { TokensDto } from '../dto/tokens.dto';
|
import { TokensDto } from '../dto/tokens.dto';
|
||||||
import { UserRepository } from '../../user/repositories/user.repository';
|
|
||||||
import { comparePasswordHash } from '../auth.utils';
|
|
||||||
import { SignupService } from './signup.service';
|
import { SignupService } from './signup.service';
|
||||||
import { CreateAdminUserDto } from '../dto/create-admin-user.dto';
|
import { CreateAdminUserDto } from '../dto/create-admin-user.dto';
|
||||||
|
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||||
|
import { comparePasswordHash } from '../../../helpers/utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@ -16,14 +15,11 @@ export class AuthService {
|
|||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
private signupService: SignupService,
|
private signupService: SignupService,
|
||||||
private tokenService: TokenService,
|
private tokenService: TokenService,
|
||||||
private userRepository: UserRepository,
|
private userRepo: UserRepo,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async login(loginDto: LoginDto, workspaceId: string) {
|
async login(loginDto: LoginDto, workspaceId: string) {
|
||||||
const user = await this.userRepository.findOneByEmail(
|
const user = await this.userRepo.findByEmail(loginDto.email, workspaceId);
|
||||||
loginDto.email,
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!user ||
|
!user ||
|
||||||
@ -33,17 +29,14 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
user.lastLoginAt = new Date();
|
user.lastLoginAt = new Date();
|
||||||
await this.userRepository.save(user);
|
await this.userRepo.updateLastLogin(user.id, workspaceId);
|
||||||
|
|
||||||
const tokens: TokensDto = await this.tokenService.generateTokens(user);
|
const tokens: TokensDto = await this.tokenService.generateTokens(user);
|
||||||
return { tokens };
|
return { tokens };
|
||||||
}
|
}
|
||||||
|
|
||||||
async register(createUserDto: CreateUserDto, workspaceId: string) {
|
async register(createUserDto: CreateUserDto, workspaceId: string) {
|
||||||
const user: User = await this.signupService.signup(
|
const user = await this.signupService.signup(createUserDto, workspaceId);
|
||||||
createUserDto,
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const tokens: TokensDto = await this.tokenService.generateTokens(user);
|
const tokens: TokensDto = await this.tokenService.generateTokens(user);
|
||||||
|
|
||||||
@ -51,8 +44,7 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setup(createAdminUserDto: CreateAdminUserDto) {
|
async setup(createAdminUserDto: CreateAdminUserDto) {
|
||||||
const user: User =
|
const user = await this.signupService.initialSetup(createAdminUserDto);
|
||||||
await this.signupService.initialSetup(createAdminUserDto);
|
|
||||||
|
|
||||||
const tokens: TokensDto = await this.tokenService.generateTokens(user);
|
const tokens: TokensDto = await this.tokenService.generateTokens(user);
|
||||||
|
|
||||||
|
|||||||
@ -1,140 +1,95 @@
|
|||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { CreateUserDto } from '../dto/create-user.dto';
|
import { CreateUserDto } from '../dto/create-user.dto';
|
||||||
import { DataSource, EntityManager } from 'typeorm';
|
|
||||||
import { User } from '../../user/entities/user.entity';
|
|
||||||
import { transactionWrapper } from '../../../helpers/db.helper';
|
|
||||||
import { UserRepository } from '../../user/repositories/user.repository';
|
|
||||||
import { WorkspaceRepository } from '../../workspace/repositories/workspace.repository';
|
|
||||||
import { WorkspaceService } from '../../workspace/services/workspace.service';
|
import { WorkspaceService } from '../../workspace/services/workspace.service';
|
||||||
import { CreateWorkspaceDto } from '../../workspace/dto/create-workspace.dto';
|
import { CreateWorkspaceDto } from '../../workspace/dto/create-workspace.dto';
|
||||||
import { Workspace } from '../../workspace/entities/workspace.entity';
|
|
||||||
import { SpaceService } from '../../space/services/space.service';
|
import { SpaceService } from '../../space/services/space.service';
|
||||||
import { CreateAdminUserDto } from '../dto/create-admin-user.dto';
|
import { CreateAdminUserDto } from '../dto/create-admin-user.dto';
|
||||||
import { GroupUserService } from '../../group/services/group-user.service';
|
import { GroupUserService } from '../../group/services/group-user.service';
|
||||||
|
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||||
|
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||||
|
import { executeTx } from '@docmost/db/utils';
|
||||||
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
|
import { User } from '@docmost/db/types/entity.types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SignupService {
|
export class SignupService {
|
||||||
constructor(
|
constructor(
|
||||||
private userRepository: UserRepository,
|
private userRepo: UserRepo,
|
||||||
private workspaceRepository: WorkspaceRepository,
|
|
||||||
private workspaceService: WorkspaceService,
|
private workspaceService: WorkspaceService,
|
||||||
private spaceService: SpaceService,
|
private spaceService: SpaceService,
|
||||||
private groupUserService: GroupUserService,
|
private groupUserService: GroupUserService,
|
||||||
private dataSource: DataSource,
|
@InjectKysely() private readonly db: KyselyDB,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
prepareUser(createUserDto: CreateUserDto): User {
|
|
||||||
const user = new User();
|
|
||||||
user.name = createUserDto.name || createUserDto.email.split('@')[0];
|
|
||||||
user.email = createUserDto.email.toLowerCase();
|
|
||||||
user.password = createUserDto.password;
|
|
||||||
user.locale = 'en';
|
|
||||||
user.lastLoginAt = new Date();
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
async createUser(
|
|
||||||
createUserDto: CreateUserDto,
|
|
||||||
manager?: EntityManager,
|
|
||||||
): Promise<User> {
|
|
||||||
return await transactionWrapper(
|
|
||||||
async (transactionManager: EntityManager) => {
|
|
||||||
let user = this.prepareUser(createUserDto);
|
|
||||||
user = await transactionManager.save(user);
|
|
||||||
return user;
|
|
||||||
},
|
|
||||||
this.dataSource,
|
|
||||||
manager,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async signup(
|
async signup(
|
||||||
createUserDto: CreateUserDto,
|
createUserDto: CreateUserDto,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
manager?: EntityManager,
|
trx?: KyselyTransaction,
|
||||||
): Promise<User> {
|
): Promise<User> {
|
||||||
const userCheck = await this.userRepository.findOneByEmail(
|
const userCheck = await this.userRepo.findByEmail(
|
||||||
createUserDto.email,
|
createUserDto.email,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (userCheck) {
|
if (userCheck) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
'You already have an account on this workspace',
|
'You already have an account on this workspace',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await transactionWrapper(
|
return await executeTx(
|
||||||
async (manager: EntityManager) => {
|
this.db,
|
||||||
|
async (trx) => {
|
||||||
// create user
|
// create user
|
||||||
const user = await this.createUser(createUserDto, manager);
|
const user = await this.userRepo.insertUser(createUserDto, trx);
|
||||||
|
|
||||||
// add user to workspace
|
// add user to workspace
|
||||||
await this.workspaceService.addUserToWorkspace(
|
await this.workspaceService.addUserToWorkspace(
|
||||||
user,
|
user.id,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
undefined,
|
undefined,
|
||||||
manager,
|
trx,
|
||||||
);
|
);
|
||||||
|
|
||||||
// add user to default group
|
// add user to default group
|
||||||
await this.groupUserService.addUserToDefaultGroup(
|
await this.groupUserService.addUserToDefaultGroup(
|
||||||
user.id,
|
user.id,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
manager,
|
trx,
|
||||||
);
|
);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
},
|
},
|
||||||
this.dataSource,
|
trx,
|
||||||
manager,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createWorkspace(
|
async createWorkspace(user, workspaceName, trx?: KyselyTransaction) {
|
||||||
user: User,
|
return await executeTx(
|
||||||
workspaceName,
|
this.db,
|
||||||
manager?: EntityManager,
|
async (trx) => {
|
||||||
): Promise<Workspace> {
|
|
||||||
return await transactionWrapper(
|
|
||||||
async (manager: EntityManager) => {
|
|
||||||
// for cloud
|
|
||||||
const workspaceData: CreateWorkspaceDto = {
|
const workspaceData: CreateWorkspaceDto = {
|
||||||
name: workspaceName,
|
name: workspaceName,
|
||||||
// hostname: '', // generate
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return await this.workspaceService.create(user, workspaceData, manager);
|
return await this.workspaceService.create(user, workspaceData, trx);
|
||||||
},
|
},
|
||||||
this.dataSource,
|
trx,
|
||||||
manager,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialSetup(
|
async initialSetup(
|
||||||
createAdminUserDto: CreateAdminUserDto,
|
createAdminUserDto: CreateAdminUserDto,
|
||||||
manager?: EntityManager,
|
trx?: KyselyTransaction,
|
||||||
): Promise<User> {
|
) {
|
||||||
return await transactionWrapper(
|
return await executeTx(
|
||||||
async (manager: EntityManager) => {
|
this.db,
|
||||||
|
async (trx) => {
|
||||||
// create user
|
// create user
|
||||||
const user = await this.createUser(createAdminUserDto, manager);
|
const user = await this.userRepo.insertUser(createAdminUserDto, trx);
|
||||||
await this.createWorkspace(
|
await this.createWorkspace(user, createAdminUserDto.workspaceName, trx);
|
||||||
user,
|
|
||||||
createAdminUserDto.workspaceName,
|
|
||||||
manager,
|
|
||||||
);
|
|
||||||
return user;
|
return user;
|
||||||
},
|
},
|
||||||
this.dataSource,
|
trx,
|
||||||
manager,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// create user -
|
|
||||||
// create workspace -
|
|
||||||
// create default group
|
|
||||||
// create space
|
|
||||||
// add group to space instead of user
|
|
||||||
|
|
||||||
// add new users to default group
|
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { EnvironmentService } from '../../../integrations/environment/environment.service';
|
import { EnvironmentService } from '../../../integrations/environment/environment.service';
|
||||||
import { User } from '../../user/entities/user.entity';
|
|
||||||
import { TokensDto } from '../dto/tokens.dto';
|
import { TokensDto } from '../dto/tokens.dto';
|
||||||
import { JwtPayload, JwtRefreshPayload, JwtType } from '../dto/jwt-payload';
|
import { JwtPayload, JwtRefreshPayload, JwtType } from '../dto/jwt-payload';
|
||||||
|
import { User } from '@docmost/db/types/entity.types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TokenService {
|
export class TokenService {
|
||||||
@ -32,7 +32,7 @@ export class TokenService {
|
|||||||
return this.jwtService.sign(payload, { expiresIn });
|
return this.jwtService.sign(payload, { expiresIn });
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateTokens(user: User): Promise<TokensDto> {
|
async generateTokens(user): Promise<TokensDto> {
|
||||||
return {
|
return {
|
||||||
accessToken: await this.generateAccessToken(user),
|
accessToken: await this.generateAccessToken(user),
|
||||||
refreshToken: await this.generateRefreshToken(user.id, user.workspaceId),
|
refreshToken: await this.generateRefreshToken(user.id, user.workspaceId),
|
||||||
|
|||||||
@ -7,18 +7,14 @@ import { PassportStrategy } from '@nestjs/passport';
|
|||||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
import { EnvironmentService } from '../../../integrations/environment/environment.service';
|
import { EnvironmentService } from '../../../integrations/environment/environment.service';
|
||||||
import { JwtPayload, JwtType } from '../dto/jwt-payload';
|
import { JwtPayload, JwtType } from '../dto/jwt-payload';
|
||||||
import { AuthService } from '../services/auth.service';
|
import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
|
||||||
import { UserRepository } from '../../user/repositories/user.repository';
|
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||||
import { UserService } from '../../user/user.service';
|
|
||||||
import { WorkspaceRepository } from '../../workspace/repositories/workspace.repository';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||||
constructor(
|
constructor(
|
||||||
private authService: AuthService,
|
private userRepo: UserRepo,
|
||||||
private userService: UserService,
|
private workspaceRepo: WorkspaceRepo,
|
||||||
private userRepository: UserRepository,
|
|
||||||
private workspaceRepository: WorkspaceRepository,
|
|
||||||
private readonly environmentService: EnvironmentService,
|
private readonly environmentService: EnvironmentService,
|
||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
@ -29,7 +25,11 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async validate(req, payload: JwtPayload) {
|
async validate(req: any, payload: JwtPayload) {
|
||||||
|
if (!payload.workspaceId || payload.type !== JwtType.ACCESS) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
|
||||||
// CLOUD ENV
|
// CLOUD ENV
|
||||||
if (this.environmentService.isCloud()) {
|
if (this.environmentService.isCloud()) {
|
||||||
if (req.raw.workspaceId && req.raw.workspaceId !== payload.workspaceId) {
|
if (req.raw.workspaceId && req.raw.workspaceId !== payload.workspaceId) {
|
||||||
@ -37,23 +37,12 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!payload.workspaceId || payload.type !== JwtType.ACCESS) {
|
const workspace = await this.workspaceRepo.findById(payload.workspaceId);
|
||||||
throw new UnauthorizedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspace = await this.workspaceRepository.findById(
|
|
||||||
payload.workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException();
|
||||||
}
|
}
|
||||||
const user = await this.userRepository.findOne({
|
const user = await this.userRepo.findById(payload.sub, payload.workspaceId);
|
||||||
where: {
|
|
||||||
id: payload.sub,
|
|
||||||
workspaceId: payload.workspaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException();
|
||||||
|
|||||||
@ -3,87 +3,62 @@ import {
|
|||||||
AbilityBuilder,
|
AbilityBuilder,
|
||||||
createMongoAbility,
|
createMongoAbility,
|
||||||
ExtractSubjectType,
|
ExtractSubjectType,
|
||||||
InferSubjects,
|
|
||||||
MongoAbility,
|
MongoAbility,
|
||||||
} from '@casl/ability';
|
} from '@casl/ability';
|
||||||
import { User } from '../../user/entities/user.entity';
|
|
||||||
import { Action } from '../ability.action';
|
import { Action } from '../ability.action';
|
||||||
import { Workspace } from '../../workspace/entities/workspace.entity';
|
|
||||||
import { WorkspaceInvitation } from '../../workspace/entities/workspace-invitation.entity';
|
|
||||||
import { UserRole } from '../../../helpers/types/permission';
|
import { UserRole } from '../../../helpers/types/permission';
|
||||||
import { Group } from '../../group/entities/group.entity';
|
import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||||
import { GroupUser } from '../../group/entities/group-user.entity';
|
|
||||||
import { Attachment } from '../../attachment/entities/attachment.entity';
|
|
||||||
import { Space } from '../../space/entities/space.entity';
|
|
||||||
import { Page } from '../../page/entities/page.entity';
|
|
||||||
import { Comment } from '../../comment/entities/comment.entity';
|
|
||||||
import { SpaceMember } from '../../space/entities/space-member.entity';
|
|
||||||
|
|
||||||
export type Subjects =
|
export type Subjects =
|
||||||
| InferSubjects<
|
| 'Workspace'
|
||||||
| typeof Workspace
|
| 'WorkspaceInvitation'
|
||||||
| typeof WorkspaceInvitation
|
| 'Space'
|
||||||
| typeof Space
|
| 'SpaceMember'
|
||||||
| typeof SpaceMember
|
| 'Group'
|
||||||
| typeof Group
|
| 'GroupUser'
|
||||||
| typeof GroupUser
|
| 'Attachment'
|
||||||
| typeof Attachment
|
| 'Comment'
|
||||||
| typeof Comment
|
| 'Page'
|
||||||
| typeof Page
|
| 'User'
|
||||||
| typeof User
|
| 'WorkspaceUser'
|
||||||
>
|
|
||||||
| 'workspaceUser'
|
|
||||||
| 'all';
|
| 'all';
|
||||||
export type AppAbility = MongoAbility<[Action, Subjects]>;
|
export type AppAbility = MongoAbility<[Action, Subjects]>;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class CaslAbilityFactory {
|
export default class CaslAbilityFactory {
|
||||||
createForWorkspace(user: User, workspace: Workspace) {
|
createForUser(user: User, workspace: Workspace) {
|
||||||
const { can, build } = new AbilityBuilder<AppAbility>(createMongoAbility);
|
const { can, build } = new AbilityBuilder<AppAbility>(createMongoAbility);
|
||||||
|
|
||||||
const userRole = user.role;
|
const userRole = user.role;
|
||||||
|
|
||||||
if (userRole === UserRole.OWNER || userRole === UserRole.ADMIN) {
|
if (userRole === UserRole.OWNER || userRole === UserRole.ADMIN) {
|
||||||
// Workspace Users
|
// Workspace Users
|
||||||
can<any>([Action.Manage], Workspace);
|
can([Action.Manage], 'Workspace');
|
||||||
can<any>([Action.Manage], 'workspaceUser');
|
can([Action.Manage], 'WorkspaceUser');
|
||||||
|
|
||||||
can<any>([Action.Manage], WorkspaceInvitation);
|
can([Action.Manage], 'WorkspaceInvitation');
|
||||||
|
|
||||||
// Groups
|
// Groups
|
||||||
can<any>([Action.Manage], Group);
|
can([Action.Manage], 'Group');
|
||||||
can<any>([Action.Manage], GroupUser);
|
can([Action.Manage], 'GroupUser');
|
||||||
|
|
||||||
// Attachments
|
// Attachments
|
||||||
can<any>([Action.Manage], Attachment);
|
can([Action.Manage], 'Attachment');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userRole === UserRole.MEMBER) {
|
if (userRole === UserRole.MEMBER) {
|
||||||
// can<any>([Action.Read], WorkspaceUser);
|
// can<any>([Action.Read], WorkspaceUser);
|
||||||
|
|
||||||
// Groups
|
// Groups
|
||||||
can<any>([Action.Read], Group);
|
can([Action.Read], 'Group');
|
||||||
can<any>([Action.Read], GroupUser);
|
can([Action.Read], 'GroupUser');
|
||||||
|
|
||||||
// Attachments
|
// Attachments
|
||||||
can<any>([Action.Read, Action.Create], Attachment);
|
can([Action.Read, Action.Create], 'Attachment');
|
||||||
}
|
}
|
||||||
|
|
||||||
return build({
|
return build({
|
||||||
detectSubjectType: (item) =>
|
detectSubjectType: (item) => item as ExtractSubjectType<Subjects>,
|
||||||
item.constructor as ExtractSubjectType<Subjects>,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
createForUser(user: User) {
|
|
||||||
const { can, build } = new AbilityBuilder<AppAbility>(createMongoAbility);
|
|
||||||
|
|
||||||
can<any>([Action.Manage], User, { id: user.id });
|
|
||||||
can<any>([Action.Read], User);
|
|
||||||
|
|
||||||
return build({
|
|
||||||
detectSubjectType: (item) =>
|
|
||||||
item.constructor as ExtractSubjectType<Subjects>,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export class PoliciesGuard implements CanActivate {
|
|||||||
const user = request.user.user;
|
const user = request.user.user;
|
||||||
const workspace = request.user.workspace;
|
const workspace = request.user.workspace;
|
||||||
|
|
||||||
const ability = this.caslAbilityFactory.createForWorkspace(user, workspace);
|
const ability = this.caslAbilityFactory.createForUser(user, workspace);
|
||||||
|
|
||||||
return policyHandlers.every((handler) =>
|
return policyHandlers.every((handler) =>
|
||||||
this.execPolicyHandler(handler, ability),
|
this.execPolicyHandler(handler, ability),
|
||||||
|
|||||||
@ -10,12 +10,11 @@ import { CommentService } from './comment.service';
|
|||||||
import { CreateCommentDto } from './dto/create-comment.dto';
|
import { CreateCommentDto } from './dto/create-comment.dto';
|
||||||
import { UpdateCommentDto } from './dto/update-comment.dto';
|
import { UpdateCommentDto } from './dto/update-comment.dto';
|
||||||
import { CommentsInput, SingleCommentInput } from './dto/comments.input';
|
import { CommentsInput, SingleCommentInput } from './dto/comments.input';
|
||||||
import { ResolveCommentDto } from './dto/resolve-comment.dto';
|
|
||||||
import { AuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUser } from '../../decorators/auth-user.decorator';
|
||||||
import { User } from '../user/entities/user.entity';
|
|
||||||
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
|
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
|
||||||
import { Workspace } from '../workspace/entities/workspace.entity';
|
|
||||||
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
||||||
|
import { PaginationOptions } from 'src/helpers/pagination/pagination-options';
|
||||||
|
import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('comments')
|
@Controller('comments')
|
||||||
@ -34,8 +33,14 @@ export class CommentController {
|
|||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post()
|
@Post()
|
||||||
findPageComments(@Body() input: CommentsInput) {
|
findPageComments(
|
||||||
return this.commentService.findByPageId(input.pageId);
|
@Body() input: CommentsInput,
|
||||||
|
@Body()
|
||||||
|
pagination: PaginationOptions,
|
||||||
|
//@AuthUser() user: User,
|
||||||
|
// @AuthWorkspace() workspace: Workspace,
|
||||||
|
) {
|
||||||
|
return this.commentService.findByPageId(input.pageId, pagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ -50,15 +55,6 @@ export class CommentController {
|
|||||||
return this.commentService.update(updateCommentDto.id, updateCommentDto);
|
return this.commentService.update(updateCommentDto.id, updateCommentDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@Post('resolve')
|
|
||||||
resolve(
|
|
||||||
@Body() resolveCommentDto: ResolveCommentDto,
|
|
||||||
@AuthUser() user: User,
|
|
||||||
) {
|
|
||||||
return this.commentService.resolveComment(user.id, resolveCommentDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('delete')
|
@Post('delete')
|
||||||
remove(@Body() input: SingleCommentInput) {
|
remove(@Body() input: SingleCommentInput) {
|
||||||
|
|||||||
@ -1,15 +1,12 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { CommentService } from './comment.service';
|
import { CommentService } from './comment.service';
|
||||||
import { CommentController } from './comment.controller';
|
import { CommentController } from './comment.controller';
|
||||||
import { CommentRepository } from './repositories/comment.repository';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { Comment } from './entities/comment.entity';
|
|
||||||
import { PageModule } from '../page/page.module';
|
import { PageModule } from '../page/page.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Comment]), PageModule],
|
imports: [PageModule],
|
||||||
controllers: [CommentController],
|
controllers: [CommentController],
|
||||||
providers: [CommentService, CommentRepository],
|
providers: [CommentService],
|
||||||
exports: [CommentService, CommentRepository],
|
exports: [CommentService],
|
||||||
})
|
})
|
||||||
export class CommentModule {}
|
export class CommentModule {}
|
||||||
|
|||||||
@ -1,24 +1,22 @@
|
|||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { CreateCommentDto } from './dto/create-comment.dto';
|
import { CreateCommentDto } from './dto/create-comment.dto';
|
||||||
import { UpdateCommentDto } from './dto/update-comment.dto';
|
import { UpdateCommentDto } from './dto/update-comment.dto';
|
||||||
import { plainToInstance } from 'class-transformer';
|
|
||||||
import { Comment } from './entities/comment.entity';
|
|
||||||
import { CommentRepository } from './repositories/comment.repository';
|
|
||||||
import { ResolveCommentDto } from './dto/resolve-comment.dto';
|
|
||||||
import { PageService } from '../page/services/page.service';
|
import { PageService } from '../page/services/page.service';
|
||||||
|
import { CommentRepo } from '@docmost/db/repos/comment/comment.repo';
|
||||||
|
import { Comment } from '@docmost/db/types/entity.types';
|
||||||
|
import { PaginationOptions } from 'src/helpers/pagination/pagination-options';
|
||||||
|
import { PaginatedResult } from 'src/helpers/pagination/paginated-result';
|
||||||
|
import { PaginationMetaDto } from 'src/helpers/pagination/pagination-meta-dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CommentService {
|
export class CommentService {
|
||||||
constructor(
|
constructor(
|
||||||
private commentRepository: CommentRepository,
|
private commentRepo: CommentRepo,
|
||||||
private pageService: PageService,
|
private pageService: PageService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findWithCreator(commentId: string) {
|
async findWithCreator(commentId: string) {
|
||||||
return await this.commentRepository.findOne({
|
// todo: find comment with creator object
|
||||||
where: { id: commentId },
|
|
||||||
relations: ['creator'],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
@ -26,25 +24,19 @@ export class CommentService {
|
|||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
createCommentDto: CreateCommentDto,
|
createCommentDto: CreateCommentDto,
|
||||||
) {
|
) {
|
||||||
const comment = plainToInstance(Comment, createCommentDto);
|
const commentContent = JSON.parse(createCommentDto.content);
|
||||||
comment.creatorId = userId;
|
|
||||||
comment.workspaceId = workspaceId;
|
|
||||||
comment.content = JSON.parse(createCommentDto.content);
|
|
||||||
|
|
||||||
if (createCommentDto.selection) {
|
const page = await this.pageService.findById(createCommentDto.pageId);
|
||||||
comment.selection = createCommentDto.selection.substring(0, 250);
|
// const spaceId = null; // todo, get from page
|
||||||
}
|
|
||||||
|
|
||||||
const page = await this.pageService.findWithBasic(createCommentDto.pageId);
|
|
||||||
if (!page) {
|
if (!page) {
|
||||||
throw new BadRequestException('Page not found');
|
throw new BadRequestException('Page not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (createCommentDto.parentCommentId) {
|
if (createCommentDto.parentCommentId) {
|
||||||
const parentComment = await this.commentRepository.findOne({
|
const parentComment = await this.commentRepo.findById(
|
||||||
where: { id: createCommentDto.parentCommentId },
|
createCommentDto.parentCommentId,
|
||||||
select: ['id', 'parentCommentId'],
|
);
|
||||||
});
|
|
||||||
|
|
||||||
if (!parentComment) {
|
if (!parentComment) {
|
||||||
throw new BadRequestException('Parent comment not found');
|
throw new BadRequestException('Parent comment not found');
|
||||||
@ -55,68 +47,51 @@ export class CommentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedComment = await this.commentRepository.save(comment);
|
const createdComment = await this.commentRepo.insertComment({
|
||||||
return this.findWithCreator(savedComment.id);
|
pageId: createCommentDto.pageId,
|
||||||
|
content: commentContent,
|
||||||
|
selection: createCommentDto?.selection.substring(0, 250),
|
||||||
|
type: 'inline', // for now
|
||||||
|
parentCommentId: createCommentDto?.parentCommentId,
|
||||||
|
creatorId: userId,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
});
|
||||||
|
// todo return created comment and creator relation
|
||||||
|
|
||||||
|
return createdComment;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByPageId(pageId: string, offset = 0, limit = 100) {
|
async findByPageId(
|
||||||
const comments = this.commentRepository.find({
|
pageId: string,
|
||||||
where: {
|
paginationOptions: PaginationOptions,
|
||||||
pageId: pageId,
|
): Promise<PaginatedResult<Comment>> {
|
||||||
},
|
const { comments, count } = await this.commentRepo.findPageComments(
|
||||||
order: {
|
pageId,
|
||||||
createdAt: 'asc',
|
paginationOptions,
|
||||||
},
|
);
|
||||||
take: limit,
|
|
||||||
skip: offset,
|
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
||||||
relations: ['creator'],
|
return new PaginatedResult(comments, paginationMeta);
|
||||||
});
|
|
||||||
return comments;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(
|
async update(
|
||||||
commentId: string,
|
commentId: string,
|
||||||
updateCommentDto: UpdateCommentDto,
|
updateCommentDto: UpdateCommentDto,
|
||||||
): Promise<Comment> {
|
): Promise<Comment> {
|
||||||
updateCommentDto.content = JSON.parse(updateCommentDto.content);
|
const commentContent = JSON.parse(updateCommentDto.content);
|
||||||
|
|
||||||
const result = await this.commentRepository.update(commentId, {
|
await this.commentRepo.updateComment(
|
||||||
...updateCommentDto,
|
|
||||||
editedAt: new Date(),
|
|
||||||
});
|
|
||||||
if (result.affected === 0) {
|
|
||||||
throw new BadRequestException(`Comment not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.findWithCreator(commentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async resolveComment(
|
|
||||||
userId: string,
|
|
||||||
resolveCommentDto: ResolveCommentDto,
|
|
||||||
): Promise<Comment> {
|
|
||||||
const resolvedAt = resolveCommentDto.resolved ? new Date() : null;
|
|
||||||
const resolvedById = resolveCommentDto.resolved ? userId : null;
|
|
||||||
|
|
||||||
const result = await this.commentRepository.update(
|
|
||||||
resolveCommentDto.commentId,
|
|
||||||
{
|
{
|
||||||
resolvedAt,
|
content: commentContent,
|
||||||
resolvedById,
|
editedAt: new Date(),
|
||||||
},
|
},
|
||||||
|
commentId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.affected === 0) {
|
return this.commentRepo.findById(commentId);
|
||||||
throw new BadRequestException(`Comment not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.findWithCreator(resolveCommentDto.commentId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(id: string): Promise<void> {
|
async remove(id: string): Promise<void> {
|
||||||
const result = await this.commentRepository.delete(id);
|
await this.commentRepo.deleteComment(id);
|
||||||
if (result.affected === 0) {
|
|
||||||
throw new BadRequestException(`Comment with ID ${id} not found.`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
import { IsBoolean, IsUUID } from 'class-validator';
|
|
||||||
|
|
||||||
export class ResolveCommentDto {
|
|
||||||
@IsUUID()
|
|
||||||
commentId: string;
|
|
||||||
|
|
||||||
@IsBoolean()
|
|
||||||
resolved: boolean;
|
|
||||||
}
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
import {
|
|
||||||
Entity,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Column,
|
|
||||||
ManyToOne,
|
|
||||||
JoinColumn,
|
|
||||||
CreateDateColumn,
|
|
||||||
OneToMany,
|
|
||||||
DeleteDateColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { User } from '../../user/entities/user.entity';
|
|
||||||
import { Page } from '../../page/entities/page.entity';
|
|
||||||
import { Workspace } from '../../workspace/entities/workspace.entity';
|
|
||||||
|
|
||||||
@Entity('comments')
|
|
||||||
export class Comment {
|
|
||||||
@PrimaryGeneratedColumn('uuid')
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ type: 'jsonb', nullable: true })
|
|
||||||
content: any;
|
|
||||||
|
|
||||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
|
||||||
selection: string;
|
|
||||||
|
|
||||||
@Column({ type: 'varchar', length: 55, nullable: true })
|
|
||||||
type: string;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
creatorId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => User, (user) => user.comments)
|
|
||||||
@JoinColumn({ name: 'creatorId' })
|
|
||||||
creator: User;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
pageId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Page, (page) => page.comments, { onDelete: 'CASCADE' })
|
|
||||||
@JoinColumn({ name: 'pageId' })
|
|
||||||
page: Page;
|
|
||||||
|
|
||||||
@Column({ type: 'uuid', nullable: true })
|
|
||||||
parentCommentId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Comment, (comment) => comment.replies, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: 'parentCommentId' })
|
|
||||||
parentComment: Comment;
|
|
||||||
|
|
||||||
@OneToMany(() => Comment, (comment) => comment.parentComment)
|
|
||||||
replies: Comment[];
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
resolvedById: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => User)
|
|
||||||
@JoinColumn({ name: 'resolvedById' })
|
|
||||||
resolvedBy: User;
|
|
||||||
|
|
||||||
@Column({ type: 'timestamp', nullable: true })
|
|
||||||
resolvedAt: Date;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
workspaceId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Workspace, (workspace) => workspace.comments, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: 'workspaceId' })
|
|
||||||
workspace: Workspace;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@Column({ type: 'timestamp', nullable: true })
|
|
||||||
editedAt: Date;
|
|
||||||
|
|
||||||
@DeleteDateColumn({ nullable: true })
|
|
||||||
deletedAt: Date;
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
import { DataSource, Repository } from 'typeorm';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { Comment } from '../entities/comment.entity';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class CommentRepository extends Repository<Comment> {
|
|
||||||
constructor(private dataSource: DataSource) {
|
|
||||||
super(Comment, dataSource.createEntityManager());
|
|
||||||
}
|
|
||||||
|
|
||||||
async findById(commentId: string) {
|
|
||||||
return this.findOneBy({ id: commentId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
import {
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
Entity,
|
|
||||||
JoinColumn,
|
|
||||||
ManyToOne,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Unique,
|
|
||||||
UpdateDateColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { User } from '../../user/entities/user.entity';
|
|
||||||
import { Group } from './group.entity';
|
|
||||||
|
|
||||||
@Entity('group_users')
|
|
||||||
@Unique(['groupId', 'userId'])
|
|
||||||
export class GroupUser {
|
|
||||||
@PrimaryGeneratedColumn('uuid')
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
userId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => User, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: 'userId' })
|
|
||||||
user: User;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
groupId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Group, (group) => group.groupUsers, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: 'groupId' })
|
|
||||||
group: Group;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
import {
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
Entity,
|
|
||||||
JoinColumn,
|
|
||||||
ManyToOne,
|
|
||||||
OneToMany,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { GroupUser } from './group-user.entity';
|
|
||||||
import { Workspace } from '../../workspace/entities/workspace.entity';
|
|
||||||
import { User } from '../../user/entities/user.entity';
|
|
||||||
import { Unique } from 'typeorm';
|
|
||||||
import { SpaceMember } from '../../space/entities/space-member.entity';
|
|
||||||
|
|
||||||
@Entity('groups')
|
|
||||||
@Unique(['name', 'workspaceId'])
|
|
||||||
export class Group {
|
|
||||||
@PrimaryGeneratedColumn('uuid')
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ length: 255 })
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@Column({ type: 'text', nullable: true })
|
|
||||||
description: string;
|
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: false })
|
|
||||||
isDefault: boolean;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
workspaceId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Workspace, (workspace) => workspace.groups, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: 'workspaceId' })
|
|
||||||
workspace: Workspace;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
creatorId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => User)
|
|
||||||
@JoinColumn({ name: 'creatorId' })
|
|
||||||
creator: User;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: Date;
|
|
||||||
|
|
||||||
@OneToMany(() => GroupUser, (groupUser) => groupUser.group)
|
|
||||||
groupUsers: GroupUser[];
|
|
||||||
|
|
||||||
@OneToMany(() => SpaceMember, (spaceMembership) => spaceMembership.group)
|
|
||||||
spaces: SpaceMember[];
|
|
||||||
|
|
||||||
memberCount?: number;
|
|
||||||
}
|
|
||||||
@ -10,8 +10,6 @@ import { GroupService } from './services/group.service';
|
|||||||
import { CreateGroupDto } from './dto/create-group.dto';
|
import { CreateGroupDto } from './dto/create-group.dto';
|
||||||
import { AuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUser } from '../../decorators/auth-user.decorator';
|
||||||
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
|
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
|
||||||
import { User } from '../user/entities/user.entity';
|
|
||||||
import { Workspace } from '../workspace/entities/workspace.entity';
|
|
||||||
import { GroupUserService } from './services/group-user.service';
|
import { GroupUserService } from './services/group-user.service';
|
||||||
import { GroupIdDto } from './dto/group-id.dto';
|
import { GroupIdDto } from './dto/group-id.dto';
|
||||||
import { PaginationOptions } from '../../helpers/pagination/pagination-options';
|
import { PaginationOptions } from '../../helpers/pagination/pagination-options';
|
||||||
@ -19,12 +17,11 @@ import { AddGroupUserDto } from './dto/add-group-user.dto';
|
|||||||
import { RemoveGroupUserDto } from './dto/remove-group-user.dto';
|
import { RemoveGroupUserDto } from './dto/remove-group-user.dto';
|
||||||
import { UpdateGroupDto } from './dto/update-group.dto';
|
import { UpdateGroupDto } from './dto/update-group.dto';
|
||||||
import { Action } from '../casl/ability.action';
|
import { Action } from '../casl/ability.action';
|
||||||
import { Group } from './entities/group.entity';
|
|
||||||
import { GroupUser } from './entities/group-user.entity';
|
|
||||||
import { PoliciesGuard } from '../casl/guards/policies.guard';
|
import { PoliciesGuard } from '../casl/guards/policies.guard';
|
||||||
import { CheckPolicies } from '../casl/decorators/policies.decorator';
|
import { CheckPolicies } from '../casl/decorators/policies.decorator';
|
||||||
import { AppAbility } from '../casl/abilities/casl-ability.factory';
|
import { AppAbility } from '../casl/abilities/casl-ability.factory';
|
||||||
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
||||||
|
import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('groups')
|
@Controller('groups')
|
||||||
@ -45,7 +42,7 @@ export class GroupController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(PoliciesGuard)
|
@UseGuards(PoliciesGuard)
|
||||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Group))
|
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, 'Group'))
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('/info')
|
@Post('/info')
|
||||||
getGroup(
|
getGroup(
|
||||||
@ -57,7 +54,7 @@ export class GroupController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(PoliciesGuard)
|
@UseGuards(PoliciesGuard)
|
||||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, Group))
|
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, 'Group'))
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('create')
|
@Post('create')
|
||||||
createGroup(
|
createGroup(
|
||||||
@ -69,7 +66,7 @@ export class GroupController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(PoliciesGuard)
|
@UseGuards(PoliciesGuard)
|
||||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, Group))
|
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, 'Group'))
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('update')
|
@Post('update')
|
||||||
updateGroup(
|
updateGroup(
|
||||||
@ -81,7 +78,7 @@ export class GroupController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(PoliciesGuard)
|
@UseGuards(PoliciesGuard)
|
||||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, GroupUser))
|
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, 'GroupUser'))
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('members')
|
@Post('members')
|
||||||
getGroupMembers(
|
getGroupMembers(
|
||||||
@ -97,7 +94,9 @@ export class GroupController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(PoliciesGuard)
|
@UseGuards(PoliciesGuard)
|
||||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, GroupUser))
|
@CheckPolicies((ability: AppAbility) =>
|
||||||
|
ability.can(Action.Manage, 'GroupUser'),
|
||||||
|
)
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('members/add')
|
@Post('members/add')
|
||||||
addGroupMember(
|
addGroupMember(
|
||||||
@ -113,7 +112,9 @@ export class GroupController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(PoliciesGuard)
|
@UseGuards(PoliciesGuard)
|
||||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, GroupUser))
|
@CheckPolicies((ability: AppAbility) =>
|
||||||
|
ability.can(Action.Manage, 'GroupUser'),
|
||||||
|
)
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('members/remove')
|
@Post('members/remove')
|
||||||
removeGroupMember(
|
removeGroupMember(
|
||||||
@ -129,7 +130,7 @@ export class GroupController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(PoliciesGuard)
|
@UseGuards(PoliciesGuard)
|
||||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, Group))
|
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, 'Group'))
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('delete')
|
@Post('delete')
|
||||||
deleteGroup(
|
deleteGroup(
|
||||||
|
|||||||
@ -1,22 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { GroupService } from './services/group.service';
|
import { GroupService } from './services/group.service';
|
||||||
import { GroupController } from './group.controller';
|
import { GroupController } from './group.controller';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { Group } from './entities/group.entity';
|
|
||||||
import { GroupUser } from './entities/group-user.entity';
|
|
||||||
import { GroupRepository } from './respositories/group.repository';
|
|
||||||
import { GroupUserRepository } from './respositories/group-user.repository';
|
|
||||||
import { GroupUserService } from './services/group-user.service';
|
import { GroupUserService } from './services/group-user.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Group, GroupUser])],
|
|
||||||
controllers: [GroupController],
|
controllers: [GroupController],
|
||||||
providers: [
|
providers: [GroupService, GroupUserService],
|
||||||
GroupService,
|
|
||||||
GroupUserService,
|
|
||||||
GroupRepository,
|
|
||||||
GroupUserRepository,
|
|
||||||
],
|
|
||||||
exports: [GroupService, GroupUserService],
|
exports: [GroupService, GroupUserService],
|
||||||
})
|
})
|
||||||
export class GroupModule {}
|
export class GroupModule {}
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { DataSource, Repository } from 'typeorm';
|
|
||||||
import { GroupUser } from '../entities/group-user.entity';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class GroupUserRepository extends Repository<GroupUser> {
|
|
||||||
constructor(private dataSource: DataSource) {
|
|
||||||
super(GroupUser, dataSource.createEntityManager());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { DataSource, Repository } from 'typeorm';
|
|
||||||
import { Group } from '../entities/group.entity';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class GroupRepository extends Repository<Group> {
|
|
||||||
constructor(private dataSource: DataSource) {
|
|
||||||
super(Group, dataSource.createEntityManager());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,48 +1,35 @@
|
|||||||
import {
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
BadRequestException,
|
|
||||||
Injectable,
|
|
||||||
NotFoundException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { DataSource, EntityManager } from 'typeorm';
|
|
||||||
import { GroupUserRepository } from '../respositories/group-user.repository';
|
|
||||||
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
|
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
|
||||||
import { transactionWrapper } from '../../../helpers/db.helper';
|
|
||||||
import { User } from '../../user/entities/user.entity';
|
|
||||||
import { GroupUser } from '../entities/group-user.entity';
|
|
||||||
import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto';
|
import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto';
|
||||||
import { PaginatedResult } from '../../../helpers/pagination/paginated-result';
|
import { PaginatedResult } from '../../../helpers/pagination/paginated-result';
|
||||||
import { Group } from '../entities/group.entity';
|
|
||||||
import { GroupService } from './group.service';
|
import { GroupService } from './group.service';
|
||||||
|
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||||
|
import { executeTx } from '@docmost/db/utils';
|
||||||
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
|
import { GroupRepo } from '@docmost/db/repos/group/group.repo';
|
||||||
|
import { GroupUserRepo } from '@docmost/db/repos/group/group-user.repo';
|
||||||
|
import { User } from '@docmost/db/types/entity.types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GroupUserService {
|
export class GroupUserService {
|
||||||
constructor(
|
constructor(
|
||||||
private groupUserRepository: GroupUserRepository,
|
private groupRepo: GroupRepo,
|
||||||
|
private groupUserRepo: GroupUserRepo,
|
||||||
private groupService: GroupService,
|
private groupService: GroupService,
|
||||||
private dataSource: DataSource,
|
@InjectKysely() private readonly db: KyselyDB,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getGroupUsers(
|
async getGroupUsers(
|
||||||
groupId,
|
groupId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
paginationOptions: PaginationOptions,
|
paginationOptions: PaginationOptions,
|
||||||
): Promise<PaginatedResult<User>> {
|
): Promise<PaginatedResult<User>> {
|
||||||
await this.groupService.findAndValidateGroup(groupId, workspaceId);
|
await this.groupService.findAndValidateGroup(groupId, workspaceId);
|
||||||
|
|
||||||
const [groupUsers, count] = await this.groupUserRepository.findAndCount({
|
const { users, count } = await this.groupUserRepo.getGroupUsersPaginated(
|
||||||
relations: ['user'],
|
groupId,
|
||||||
where: {
|
paginationOptions,
|
||||||
groupId: groupId,
|
);
|
||||||
group: {
|
|
||||||
workspaceId: workspaceId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
take: paginationOptions.limit,
|
|
||||||
skip: paginationOptions.skip,
|
|
||||||
});
|
|
||||||
|
|
||||||
const users = groupUsers.map((groupUser: GroupUser) => groupUser.user);
|
|
||||||
|
|
||||||
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
||||||
|
|
||||||
@ -52,23 +39,18 @@ export class GroupUserService {
|
|||||||
async addUserToDefaultGroup(
|
async addUserToDefaultGroup(
|
||||||
userId: string,
|
userId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
manager?: EntityManager,
|
trx?: KyselyTransaction,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return await transactionWrapper(
|
await executeTx(
|
||||||
async (manager) => {
|
this.db,
|
||||||
const defaultGroup = await this.groupService.getDefaultGroup(
|
async (trx) => {
|
||||||
|
const defaultGroup = await this.groupRepo.getDefaultGroup(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
manager,
|
trx,
|
||||||
);
|
|
||||||
await this.addUserToGroup(
|
|
||||||
userId,
|
|
||||||
defaultGroup.id,
|
|
||||||
workspaceId,
|
|
||||||
manager,
|
|
||||||
);
|
);
|
||||||
|
await this.addUserToGroup(userId, defaultGroup.id, workspaceId, trx);
|
||||||
},
|
},
|
||||||
this.dataSource,
|
trx,
|
||||||
manager,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,46 +58,33 @@ export class GroupUserService {
|
|||||||
userId: string,
|
userId: string,
|
||||||
groupId: string,
|
groupId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
manager?: EntityManager,
|
trx?: KyselyTransaction,
|
||||||
): Promise<GroupUser> {
|
): Promise<void> {
|
||||||
return await transactionWrapper(
|
await executeTx(
|
||||||
async (manager) => {
|
this.db,
|
||||||
const group = await manager.findOneBy(Group, {
|
async (trx) => {
|
||||||
id: groupId,
|
await this.groupService.findAndValidateGroup(groupId, workspaceId);
|
||||||
workspaceId: workspaceId,
|
const groupUserExists = await this.groupUserRepo.getGroupUserById(
|
||||||
});
|
userId,
|
||||||
|
groupId,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
|
||||||
if (!group) {
|
if (groupUserExists) {
|
||||||
throw new NotFoundException('Group not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const userExists = await manager.exists(User, {
|
|
||||||
where: { id: userId, workspaceId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userExists) {
|
|
||||||
throw new NotFoundException('User not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingGroupUser = await manager.findOneBy(GroupUser, {
|
|
||||||
userId: userId,
|
|
||||||
groupId: groupId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingGroupUser) {
|
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
'User is already a member of this group',
|
'User is already a member of this group',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupUser = new GroupUser();
|
await this.groupUserRepo.insertGroupUser(
|
||||||
groupUser.userId = userId;
|
{
|
||||||
groupUser.groupId = groupId;
|
userId,
|
||||||
|
groupId,
|
||||||
return manager.save(groupUser);
|
},
|
||||||
|
trx,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
this.dataSource,
|
trx,
|
||||||
manager,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,22 +104,15 @@ export class GroupUserService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupUser = await this.getGroupUser(userId, groupId);
|
const groupUser = await this.groupUserRepo.getGroupUserById(
|
||||||
|
userId,
|
||||||
|
groupId,
|
||||||
|
);
|
||||||
|
|
||||||
if (!groupUser) {
|
if (!groupUser) {
|
||||||
throw new BadRequestException('Group member not found');
|
throw new BadRequestException('Group member not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.groupUserRepository.delete({
|
await this.groupUserRepo.delete(userId, groupId);
|
||||||
userId,
|
|
||||||
groupId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getGroupUser(userId: string, groupId: string): Promise<GroupUser> {
|
|
||||||
return await this.groupUserRepository.findOneBy({
|
|
||||||
userId,
|
|
||||||
groupId,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,87 +4,64 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { CreateGroupDto, DefaultGroup } from '../dto/create-group.dto';
|
import { CreateGroupDto, DefaultGroup } from '../dto/create-group.dto';
|
||||||
import { GroupRepository } from '../respositories/group.repository';
|
|
||||||
import { Group } from '../entities/group.entity';
|
|
||||||
import { plainToInstance } from 'class-transformer';
|
|
||||||
import { User } from '../../user/entities/user.entity';
|
|
||||||
import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto';
|
import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto';
|
||||||
import { PaginatedResult } from '../../../helpers/pagination/paginated-result';
|
import { PaginatedResult } from '../../../helpers/pagination/paginated-result';
|
||||||
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
|
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
|
||||||
import { UpdateGroupDto } from '../dto/update-group.dto';
|
import { UpdateGroupDto } from '../dto/update-group.dto';
|
||||||
import { DataSource, EntityManager } from 'typeorm';
|
import { KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||||
import { transactionWrapper } from '../../../helpers/db.helper';
|
import { GroupRepo } from '@docmost/db/repos/group/group.repo';
|
||||||
|
import { Group, InsertableGroup, User } from '@docmost/db/types/entity.types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GroupService {
|
export class GroupService {
|
||||||
constructor(
|
constructor(private groupRepo: GroupRepo) {}
|
||||||
private groupRepository: GroupRepository,
|
|
||||||
private dataSource: DataSource,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async createGroup(
|
async createGroup(
|
||||||
authUser: User,
|
authUser: User,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
createGroupDto: CreateGroupDto,
|
createGroupDto: CreateGroupDto,
|
||||||
|
trx?: KyselyTransaction,
|
||||||
): Promise<Group> {
|
): Promise<Group> {
|
||||||
const group = plainToInstance(Group, createGroupDto);
|
const groupExists = await this.groupRepo.findByName(
|
||||||
group.creatorId = authUser.id;
|
|
||||||
group.workspaceId = workspaceId;
|
|
||||||
|
|
||||||
const groupExists = await this.findGroupByName(
|
|
||||||
createGroupDto.name,
|
createGroupDto.name,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
if (groupExists) {
|
if (groupExists) {
|
||||||
throw new BadRequestException('Group name already exists');
|
throw new BadRequestException('Group name already exists');
|
||||||
}
|
}
|
||||||
|
const insertableGroup: InsertableGroup = {
|
||||||
|
name: createGroupDto.name,
|
||||||
|
description: createGroupDto.description,
|
||||||
|
isDefault: false,
|
||||||
|
creatorId: authUser.id,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
};
|
||||||
|
|
||||||
return await this.groupRepository.save(group);
|
return await this.groupRepo.insertGroup(insertableGroup, trx);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createDefaultGroup(
|
async createDefaultGroup(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
userId?: string,
|
userId?: string,
|
||||||
manager?: EntityManager,
|
trx?: KyselyTransaction,
|
||||||
): Promise<Group> {
|
): Promise<Group> {
|
||||||
return await transactionWrapper(
|
const insertableGroup: InsertableGroup = {
|
||||||
async (manager: EntityManager) => {
|
name: DefaultGroup.EVERYONE,
|
||||||
const group = new Group();
|
isDefault: true,
|
||||||
group.name = DefaultGroup.EVERYONE;
|
creatorId: userId ?? null,
|
||||||
group.isDefault = true;
|
workspaceId: workspaceId,
|
||||||
group.creatorId = userId ?? null;
|
};
|
||||||
group.workspaceId = workspaceId;
|
return await this.groupRepo.insertGroup(insertableGroup, trx);
|
||||||
return await manager.save(group);
|
|
||||||
},
|
|
||||||
this.dataSource,
|
|
||||||
manager,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDefaultGroup(
|
|
||||||
workspaceId: string,
|
|
||||||
manager: EntityManager,
|
|
||||||
): Promise<Group> {
|
|
||||||
return await transactionWrapper(
|
|
||||||
async (manager: EntityManager) => {
|
|
||||||
return await manager.findOneBy(Group, {
|
|
||||||
isDefault: true,
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
this.dataSource,
|
|
||||||
manager,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateGroup(
|
async updateGroup(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
updateGroupDto: UpdateGroupDto,
|
updateGroupDto: UpdateGroupDto,
|
||||||
): Promise<Group> {
|
): Promise<Group> {
|
||||||
const group = await this.groupRepository.findOneBy({
|
const group = await this.groupRepo.findById(
|
||||||
id: updateGroupDto.groupId,
|
updateGroupDto.groupId,
|
||||||
workspaceId: workspaceId,
|
workspaceId,
|
||||||
});
|
);
|
||||||
|
|
||||||
if (!group) {
|
if (!group) {
|
||||||
throw new NotFoundException('Group not found');
|
throw new NotFoundException('Group not found');
|
||||||
@ -94,7 +71,7 @@ export class GroupService {
|
|||||||
throw new BadRequestException('You cannot update a default group');
|
throw new BadRequestException('You cannot update a default group');
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupExists = await this.findGroupByName(
|
const groupExists = await this.groupRepo.findByName(
|
||||||
updateGroupDto.name,
|
updateGroupDto.name,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
@ -110,20 +87,21 @@ export class GroupService {
|
|||||||
group.description = updateGroupDto.description;
|
group.description = updateGroupDto.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.groupRepository.save(group);
|
await this.groupRepo.update(
|
||||||
|
{
|
||||||
|
name: updateGroupDto.name,
|
||||||
|
description: updateGroupDto.description,
|
||||||
|
},
|
||||||
|
group.id,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getGroupInfo(groupId: string, workspaceId: string): Promise<Group> {
|
async getGroupInfo(groupId: string, workspaceId: string): Promise<Group> {
|
||||||
const group = await this.groupRepository
|
// todo: add member count
|
||||||
.createQueryBuilder('group')
|
const group = await this.groupRepo.findById(groupId, workspaceId);
|
||||||
.where('group.id = :groupId', { groupId })
|
|
||||||
.andWhere('group.workspaceId = :workspaceId', { workspaceId })
|
|
||||||
.loadRelationCountAndMap(
|
|
||||||
'group.memberCount',
|
|
||||||
'group.groupUsers',
|
|
||||||
'groupUsers',
|
|
||||||
)
|
|
||||||
.getOne();
|
|
||||||
|
|
||||||
if (!group) {
|
if (!group) {
|
||||||
throw new NotFoundException('Group not found');
|
throw new NotFoundException('Group not found');
|
||||||
@ -136,17 +114,10 @@ export class GroupService {
|
|||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
paginationOptions: PaginationOptions,
|
paginationOptions: PaginationOptions,
|
||||||
): Promise<PaginatedResult<Group>> {
|
): Promise<PaginatedResult<Group>> {
|
||||||
const [groups, count] = await this.groupRepository
|
const { groups, count } = await this.groupRepo.getGroupsPaginated(
|
||||||
.createQueryBuilder('group')
|
workspaceId,
|
||||||
.where('group.workspaceId = :workspaceId', { workspaceId })
|
paginationOptions,
|
||||||
.loadRelationCountAndMap(
|
);
|
||||||
'group.memberCount',
|
|
||||||
'group.groupUsers',
|
|
||||||
'groupUsers',
|
|
||||||
)
|
|
||||||
.take(paginationOptions.limit)
|
|
||||||
.skip(paginationOptions.skip)
|
|
||||||
.getManyAndCount();
|
|
||||||
|
|
||||||
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
||||||
|
|
||||||
@ -158,34 +129,18 @@ export class GroupService {
|
|||||||
if (group.isDefault) {
|
if (group.isDefault) {
|
||||||
throw new BadRequestException('You cannot delete a default group');
|
throw new BadRequestException('You cannot delete a default group');
|
||||||
}
|
}
|
||||||
await this.groupRepository.delete(groupId);
|
await this.groupRepo.delete(groupId, workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAndValidateGroup(
|
async findAndValidateGroup(
|
||||||
groupId: string,
|
groupId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
): Promise<Group> {
|
): Promise<Group> {
|
||||||
const group = await this.groupRepository.findOne({
|
const group = await this.groupRepo.findById(groupId, workspaceId);
|
||||||
where: {
|
|
||||||
id: groupId,
|
|
||||||
workspaceId: workspaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!group) {
|
if (!group) {
|
||||||
throw new NotFoundException('Group not found');
|
throw new NotFoundException('Group not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findGroupByName(
|
|
||||||
groupName: string,
|
|
||||||
workspaceId: string,
|
|
||||||
): Promise<Group> {
|
|
||||||
return this.groupRepository
|
|
||||||
.createQueryBuilder('group')
|
|
||||||
.where('LOWER(group.name) = LOWER(:groupName)', { groupName })
|
|
||||||
.andWhere('group.workspaceId = :workspaceId', { workspaceId })
|
|
||||||
.getOne();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { IsOptional, IsString, IsUUID } from 'class-validator';
|
|||||||
export class CreatePageDto {
|
export class CreatePageDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
id?: string;
|
pageId?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
@ -2,5 +2,5 @@ import { IsUUID } from 'class-validator';
|
|||||||
|
|
||||||
export class DeletePageDto {
|
export class DeletePageDto {
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
id: string;
|
pageId: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,5 +2,5 @@ import { IsUUID } from 'class-validator';
|
|||||||
|
|
||||||
export class HistoryDetailsDto {
|
export class HistoryDetailsDto {
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
id: string;
|
historyId: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { IsString, IsOptional, IsUUID } from 'class-validator';
|
|||||||
|
|
||||||
export class MovePageDto {
|
export class MovePageDto {
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
id: string;
|
pageId: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
@ -2,5 +2,5 @@ import { IsUUID } from 'class-validator';
|
|||||||
|
|
||||||
export class PageDetailsDto {
|
export class PageDetailsDto {
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
id: string;
|
pageId: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { Page } from '../entities/page.entity';
|
import { Page } from '@docmost/db/types/entity.types';
|
||||||
|
|
||||||
export class PageWithOrderingDto extends Page {
|
export type PageWithOrderingDto = Page & { childrenIds?: string[] };
|
||||||
childrenIds?: string[];
|
|
||||||
}
|
|
||||||
|
|||||||
@ -4,5 +4,5 @@ import { IsUUID } from 'class-validator';
|
|||||||
|
|
||||||
export class UpdatePageDto extends PartialType(CreatePageDto) {
|
export class UpdatePageDto extends PartialType(CreatePageDto) {
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
id: string;
|
pageId: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,71 +0,0 @@
|
|||||||
import {
|
|
||||||
Entity,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
ManyToOne,
|
|
||||||
JoinColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
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 {
|
|
||||||
@PrimaryGeneratedColumn('uuid')
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ type: 'uuid' })
|
|
||||||
pageId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Page, (page) => page.pageHistory, { onDelete: 'CASCADE' })
|
|
||||||
@JoinColumn({ name: 'pageId' })
|
|
||||||
page: Page;
|
|
||||||
|
|
||||||
@Column({ length: 500, nullable: true })
|
|
||||||
title: string;
|
|
||||||
|
|
||||||
@Column({ type: 'jsonb', nullable: true })
|
|
||||||
content: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
slug: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
icon: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
coverPhoto: string;
|
|
||||||
|
|
||||||
@Column({ type: 'int' })
|
|
||||||
version: number;
|
|
||||||
|
|
||||||
@Column({ type: 'uuid' })
|
|
||||||
lastUpdatedById: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => User)
|
|
||||||
@JoinColumn({ name: 'lastUpdatedById' })
|
|
||||||
lastUpdatedBy: User;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
spaceId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Space, { onDelete: 'CASCADE' })
|
|
||||||
@JoinColumn({ name: 'spaceId' })
|
|
||||||
space: Space;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
workspaceId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Workspace, { onDelete: 'CASCADE' })
|
|
||||||
@JoinColumn({ name: 'workspaceId' })
|
|
||||||
workspace: Workspace;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
import {
|
|
||||||
Entity,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Column,
|
|
||||||
ManyToOne,
|
|
||||||
JoinColumn,
|
|
||||||
Unique,
|
|
||||||
CreateDateColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
DeleteDateColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { Workspace } from '../../workspace/entities/workspace.entity';
|
|
||||||
import { Space } from '../../space/entities/space.entity';
|
|
||||||
|
|
||||||
@Entity('page_ordering')
|
|
||||||
@Unique(['entityId', 'entityType'])
|
|
||||||
export class PageOrdering {
|
|
||||||
@PrimaryGeneratedColumn('uuid')
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column('uuid')
|
|
||||||
entityId: string;
|
|
||||||
|
|
||||||
@Column({ type: 'varchar', length: 50, nullable: false })
|
|
||||||
entityType: string;
|
|
||||||
|
|
||||||
@Column('uuid', { array: true, default: '{}' })
|
|
||||||
childrenIds: string[];
|
|
||||||
|
|
||||||
@Column('uuid')
|
|
||||||
workspaceId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Workspace, (workspace) => workspace.id, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: 'workspaceId' })
|
|
||||||
workspace: Workspace;
|
|
||||||
|
|
||||||
@Column('uuid')
|
|
||||||
spaceId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Space, (space) => space.id, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: 'spaceId' })
|
|
||||||
space: Space;
|
|
||||||
|
|
||||||
@DeleteDateColumn({ nullable: true })
|
|
||||||
deletedAt: Date;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
@ -1,130 +0,0 @@
|
|||||||
import {
|
|
||||||
Entity,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
ManyToOne,
|
|
||||||
JoinColumn,
|
|
||||||
OneToMany,
|
|
||||||
DeleteDateColumn,
|
|
||||||
Index,
|
|
||||||
} from 'typeorm';
|
|
||||||
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(['tsv'])
|
|
||||||
export class Page {
|
|
||||||
@PrimaryGeneratedColumn('uuid')
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ length: 500, nullable: true })
|
|
||||||
title: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
icon: string;
|
|
||||||
|
|
||||||
@Column({ type: 'jsonb', nullable: true })
|
|
||||||
content: string;
|
|
||||||
|
|
||||||
@Column({ type: 'text', nullable: true })
|
|
||||||
html: string;
|
|
||||||
|
|
||||||
@Column({ type: 'text', nullable: true })
|
|
||||||
textContent: string;
|
|
||||||
|
|
||||||
@Column({
|
|
||||||
type: 'tsvector',
|
|
||||||
select: false,
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
tsv: string;
|
|
||||||
|
|
||||||
@Column({ type: 'bytea', nullable: true })
|
|
||||||
ydoc: any;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
slug: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
coverPhoto: string;
|
|
||||||
|
|
||||||
@Column({ length: 255, nullable: true })
|
|
||||||
editor: string;
|
|
||||||
|
|
||||||
@Column({ length: 255, nullable: true })
|
|
||||||
shareId: string;
|
|
||||||
|
|
||||||
@Column({ type: 'uuid', nullable: true })
|
|
||||||
parentPageId: string;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
creatorId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => User)
|
|
||||||
@JoinColumn({ name: 'creatorId' })
|
|
||||||
creator: User;
|
|
||||||
|
|
||||||
@Column({ type: 'uuid', nullable: true })
|
|
||||||
lastUpdatedById: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => User)
|
|
||||||
@JoinColumn({ name: 'lastUpdatedById' })
|
|
||||||
lastUpdatedBy: User;
|
|
||||||
|
|
||||||
@Column({ type: 'uuid', nullable: true })
|
|
||||||
deletedById: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => User)
|
|
||||||
@JoinColumn({ name: 'deletedById' })
|
|
||||||
deletedBy: User;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
spaceId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Space, { onDelete: 'CASCADE' })
|
|
||||||
@JoinColumn({ name: 'spaceId' })
|
|
||||||
space: Space;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
workspaceId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Workspace, { onDelete: 'CASCADE' })
|
|
||||||
@JoinColumn({ name: 'workspaceId' })
|
|
||||||
workspace: Workspace;
|
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: false })
|
|
||||||
isLocked: boolean;
|
|
||||||
|
|
||||||
@Column({ length: 255, nullable: true })
|
|
||||||
status: string;
|
|
||||||
|
|
||||||
@Column({ type: 'date', nullable: true })
|
|
||||||
publishedAt: Date;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: Date;
|
|
||||||
|
|
||||||
@DeleteDateColumn({ nullable: true })
|
|
||||||
deletedAt: Date;
|
|
||||||
|
|
||||||
@ManyToOne(() => Page, (page) => page.childPages)
|
|
||||||
@JoinColumn({ name: 'parentPageId' })
|
|
||||||
parentPage: Page;
|
|
||||||
|
|
||||||
@OneToMany(() => Page, (page) => page.parentPage, { onDelete: 'CASCADE' })
|
|
||||||
childPages: Page[];
|
|
||||||
|
|
||||||
@OneToMany(() => PageHistory, (pageHistory) => pageHistory.page)
|
|
||||||
pageHistory: PageHistory[];
|
|
||||||
|
|
||||||
@OneToMany(() => Comment, (comment) => comment.page)
|
|
||||||
comments: Comment[];
|
|
||||||
}
|
|
||||||
@ -17,10 +17,10 @@ import { PageHistoryService } from './services/page-history.service';
|
|||||||
import { HistoryDetailsDto } from './dto/history-details.dto';
|
import { HistoryDetailsDto } from './dto/history-details.dto';
|
||||||
import { PageHistoryDto } from './dto/page-history.dto';
|
import { PageHistoryDto } from './dto/page-history.dto';
|
||||||
import { AuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUser } from '../../decorators/auth-user.decorator';
|
||||||
import { User } from '../user/entities/user.entity';
|
|
||||||
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
|
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
|
||||||
import { Workspace } from '../workspace/entities/workspace.entity';
|
|
||||||
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
||||||
|
import { PaginationOptions } from 'src/helpers/pagination/pagination-options';
|
||||||
|
import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('pages')
|
@Controller('pages')
|
||||||
@ -34,7 +34,7 @@ export class PageController {
|
|||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('/info')
|
@Post('/info')
|
||||||
async getPage(@Body() input: PageDetailsDto) {
|
async getPage(@Body() input: PageDetailsDto) {
|
||||||
return this.pageService.findOne(input.id);
|
return this.pageService.findById(input.pageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.CREATED)
|
@HttpCode(HttpStatus.CREATED)
|
||||||
@ -50,19 +50,23 @@ export class PageController {
|
|||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('update')
|
@Post('update')
|
||||||
async update(@Body() updatePageDto: UpdatePageDto, @AuthUser() user: User) {
|
async update(@Body() updatePageDto: UpdatePageDto, @AuthUser() user: User) {
|
||||||
return this.pageService.update(updatePageDto.id, updatePageDto, user.id);
|
return this.pageService.update(
|
||||||
|
updatePageDto.pageId,
|
||||||
|
updatePageDto,
|
||||||
|
user.id,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('delete')
|
@Post('delete')
|
||||||
async delete(@Body() deletePageDto: DeletePageDto) {
|
async delete(@Body() deletePageDto: DeletePageDto) {
|
||||||
await this.pageService.delete(deletePageDto.id);
|
await this.pageService.forceDelete(deletePageDto.pageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('restore')
|
@Post('restore')
|
||||||
async restore(@Body() deletePageDto: DeletePageDto) {
|
async restore(@Body() deletePageDto: DeletePageDto) {
|
||||||
await this.pageService.restore(deletePageDto.id);
|
// await this.pageService.restore(deletePageDto.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ -73,9 +77,11 @@ export class PageController {
|
|||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('recent')
|
@Post('recent')
|
||||||
async getRecentSpacePages(@Body() { spaceId }) {
|
async getRecentSpacePages(
|
||||||
console.log(spaceId);
|
@Body() { spaceId },
|
||||||
return this.pageService.getRecentSpacePages(spaceId);
|
@Body() pagination: PaginationOptions,
|
||||||
|
) {
|
||||||
|
return this.pageService.getRecentSpacePages(spaceId, pagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ -96,15 +102,19 @@ export class PageController {
|
|||||||
return this.pageOrderService.convertToTree(spaceId);
|
return this.pageOrderService.convertToTree(spaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: scope to workspaces
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('/history')
|
@Post('/history')
|
||||||
async getPageHistory(@Body() dto: PageHistoryDto) {
|
async getPageHistory(
|
||||||
return this.pageHistoryService.findHistoryByPageId(dto.pageId);
|
@Body() dto: PageHistoryDto,
|
||||||
|
@Body() pagination: PaginationOptions,
|
||||||
|
) {
|
||||||
|
return this.pageHistoryService.findHistoryByPageId(dto.pageId, pagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('/history/details')
|
@Post('/history/details')
|
||||||
async get(@Body() dto: HistoryDetailsDto) {
|
async get(@Body() dto: HistoryDetailsDto) {
|
||||||
return this.pageHistoryService.findOne(dto.id);
|
return this.pageHistoryService.findById(dto.historyId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,34 +1,14 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { PageService } from './services/page.service';
|
import { PageService } from './services/page.service';
|
||||||
import { PageController } from './page.controller';
|
import { PageController } from './page.controller';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { Page } from './entities/page.entity';
|
|
||||||
import { PageRepository } from './repositories/page.repository';
|
|
||||||
import { WorkspaceModule } from '../workspace/workspace.module';
|
import { WorkspaceModule } from '../workspace/workspace.module';
|
||||||
import { PageOrderingService } from './services/page-ordering.service';
|
import { PageOrderingService } from './services/page-ordering.service';
|
||||||
import { PageOrdering } from './entities/page-ordering.entity';
|
|
||||||
import { PageHistoryService } from './services/page-history.service';
|
import { PageHistoryService } from './services/page-history.service';
|
||||||
import { PageHistory } from './entities/page-history.entity';
|
|
||||||
import { PageHistoryRepository } from './repositories/page-history.repository';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [WorkspaceModule],
|
||||||
TypeOrmModule.forFeature([Page, PageOrdering, PageHistory]),
|
|
||||||
WorkspaceModule,
|
|
||||||
],
|
|
||||||
controllers: [PageController],
|
controllers: [PageController],
|
||||||
providers: [
|
providers: [PageService, PageOrderingService, PageHistoryService],
|
||||||
PageService,
|
exports: [PageService, PageOrderingService, PageHistoryService],
|
||||||
PageOrderingService,
|
|
||||||
PageHistoryService,
|
|
||||||
PageRepository,
|
|
||||||
PageHistoryRepository,
|
|
||||||
],
|
|
||||||
exports: [
|
|
||||||
PageService,
|
|
||||||
PageOrderingService,
|
|
||||||
PageHistoryService,
|
|
||||||
PageRepository,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class PageModule {}
|
export class PageModule {}
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
|
import { KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||||
import { MovePageDto } from './dto/move-page.dto';
|
import { MovePageDto } from './dto/move-page.dto';
|
||||||
import { EntityManager } from 'typeorm';
|
import { PageOrdering } from '@docmost/db/types/entity.types';
|
||||||
|
|
||||||
export enum OrderingEntity {
|
export enum OrderingEntity {
|
||||||
workspace = 'SPACE',
|
WORKSPACE = 'WORKSPACE',
|
||||||
space = 'SPACE',
|
SPACE = 'SPACE',
|
||||||
page = 'PAGE',
|
PAGE = 'PAGE',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TreeNode = {
|
export type TreeNode = {
|
||||||
@ -15,7 +16,7 @@ export type TreeNode = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function orderPageList(arr: string[], payload: MovePageDto): void {
|
export function orderPageList(arr: string[], payload: MovePageDto): void {
|
||||||
const { id, after, before } = payload;
|
const { pageId: id, after, before } = payload;
|
||||||
|
|
||||||
// Removing the item we are moving from the array first.
|
// Removing the item we are moving from the array first.
|
||||||
const index = arr.indexOf(id);
|
const index = arr.indexOf(id);
|
||||||
@ -46,23 +47,27 @@ export function orderPageList(arr: string[], payload: MovePageDto): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove an item from an array and save the entity
|
* Remove an item from an array and update the entity
|
||||||
* @param entity - The entity instance (Page or Workspace)
|
* @param entity - The entity instance (Page or Space)
|
||||||
* @param arrayField - The name of the field which is an array
|
* @param arrayField - The name of the field which is an array
|
||||||
* @param itemToRemove - The item to remove from the array
|
* @param itemToRemove - The item to remove from the array
|
||||||
* @param manager - EntityManager instance
|
* @param manager - EntityManager instance
|
||||||
*/
|
*/
|
||||||
export async function removeFromArrayAndSave<T>(
|
export async function removeFromArrayAndSave(
|
||||||
entity: T,
|
entity: PageOrdering,
|
||||||
arrayField: string,
|
arrayField: string,
|
||||||
itemToRemove: any,
|
itemToRemove: any,
|
||||||
manager: EntityManager,
|
trx: KyselyTransaction,
|
||||||
) {
|
) {
|
||||||
const array = entity[arrayField];
|
const array = entity[arrayField];
|
||||||
const index = array.indexOf(itemToRemove);
|
const index = array.indexOf(itemToRemove);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
array.splice(index, 1);
|
array.splice(index, 1);
|
||||||
await manager.save(entity);
|
await trx
|
||||||
|
.updateTable('page_ordering')
|
||||||
|
.set(entity)
|
||||||
|
.where('id', '=', entity.id)
|
||||||
|
.execute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,11 +75,11 @@ export function transformPageResult(result: any[]): any[] {
|
|||||||
return result.map((row) => {
|
return result.map((row) => {
|
||||||
const processedRow = {};
|
const processedRow = {};
|
||||||
for (const key in row) {
|
for (const key in row) {
|
||||||
const newKey = key.split('_').slice(1).join('_');
|
//const newKey = key.split('_').slice(1).join('_');
|
||||||
if (newKey === 'childrenIds' && !row[key]) {
|
if (key === 'childrenIds' && !row[key]) {
|
||||||
processedRow[newKey] = [];
|
processedRow[key] = [];
|
||||||
} else {
|
} else {
|
||||||
processedRow[newKey] = row[key];
|
processedRow[key] = row[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return processedRow;
|
return processedRow;
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
import { DataSource, Repository } from 'typeorm';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { PageHistory } from '../entities/page-history.entity';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PageHistoryRepository extends Repository<PageHistory> {
|
|
||||||
constructor(private dataSource: DataSource) {
|
|
||||||
super(PageHistory, dataSource.createEntityManager());
|
|
||||||
}
|
|
||||||
|
|
||||||
async findById(pageId: string) {
|
|
||||||
return this.findOne({
|
|
||||||
where: {
|
|
||||||
id: pageId,
|
|
||||||
},
|
|
||||||
relations: ['lastUpdatedBy'],
|
|
||||||
select: {
|
|
||||||
lastUpdatedBy: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
avatarUrl: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
import { DataSource, Repository } from 'typeorm';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { Page } from '../entities/page.entity';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PageRepository extends Repository<Page> {
|
|
||||||
constructor(private dataSource: DataSource) {
|
|
||||||
super(Page, dataSource.createEntityManager());
|
|
||||||
}
|
|
||||||
|
|
||||||
public baseFields = [
|
|
||||||
'page.id',
|
|
||||||
'page.title',
|
|
||||||
'page.slug',
|
|
||||||
'page.icon',
|
|
||||||
'page.coverPhoto',
|
|
||||||
'page.shareId',
|
|
||||||
'page.parentPageId',
|
|
||||||
'page.creatorId',
|
|
||||||
'page.lastUpdatedById',
|
|
||||||
'page.spaceId',
|
|
||||||
'page.workspaceId',
|
|
||||||
'page.isLocked',
|
|
||||||
'page.status',
|
|
||||||
'page.publishedAt',
|
|
||||||
'page.createdAt',
|
|
||||||
'page.updatedAt',
|
|
||||||
'page.deletedAt',
|
|
||||||
];
|
|
||||||
|
|
||||||
private async baseFind(pageId: string, selectFields: string[]) {
|
|
||||||
return this.dataSource
|
|
||||||
.createQueryBuilder(Page, 'page')
|
|
||||||
.where('page.id = :id', { id: pageId })
|
|
||||||
.select(selectFields)
|
|
||||||
.getOne();
|
|
||||||
}
|
|
||||||
|
|
||||||
async findById(pageId: string) {
|
|
||||||
return this.baseFind(pageId, this.baseFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
async findWithYDoc(pageId: string) {
|
|
||||||
const extendedFields = [...this.baseFields, 'page.ydoc'];
|
|
||||||
return this.baseFind(pageId, extendedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
async findWithContent(pageId: string) {
|
|
||||||
const extendedFields = [...this.baseFields, 'page.content'];
|
|
||||||
return this.baseFind(pageId, extendedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
async findWithAllFields(pageId: string) {
|
|
||||||
const extendedFields = [...this.baseFields, 'page.content', 'page.ydoc'];
|
|
||||||
return this.baseFind(pageId, extendedFields);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,13 +1,15 @@
|
|||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { PageHistory } from '../entities/page-history.entity';
|
import { PageHistoryRepo } from '@docmost/db/repos/page/page-history.repo';
|
||||||
import { Page } from '../entities/page.entity';
|
import { Page, PageHistory } from '@docmost/db/types/entity.types';
|
||||||
import { PageHistoryRepository } from '../repositories/page-history.repository';
|
import { PaginationOptions } from 'src/helpers/pagination/pagination-options';
|
||||||
|
import { PaginatedResult } from 'src/helpers/pagination/paginated-result';
|
||||||
|
import { PaginationMetaDto } from 'src/helpers/pagination/pagination-meta-dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PageHistoryService {
|
export class PageHistoryService {
|
||||||
constructor(private pageHistoryRepo: PageHistoryRepository) {}
|
constructor(private pageHistoryRepo: PageHistoryRepo) {}
|
||||||
|
|
||||||
async findOne(historyId: string): Promise<PageHistory> {
|
async findById(historyId: string): Promise<PageHistory> {
|
||||||
const history = await this.pageHistoryRepo.findById(historyId);
|
const history = await this.pageHistoryRepo.findById(historyId);
|
||||||
if (!history) {
|
if (!history) {
|
||||||
throw new BadRequestException('History not found');
|
throw new BadRequestException('History not found');
|
||||||
@ -16,45 +18,31 @@ export class PageHistoryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveHistory(page: Page): Promise<void> {
|
async saveHistory(page: Page): Promise<void> {
|
||||||
const pageHistory = new PageHistory();
|
await this.pageHistoryRepo.insertPageHistory({
|
||||||
pageHistory.pageId = page.id;
|
pageId: page.id,
|
||||||
pageHistory.title = page.title;
|
title: page.title,
|
||||||
pageHistory.content = page.content;
|
content: page.content,
|
||||||
pageHistory.slug = page.slug;
|
slug: page.slug,
|
||||||
pageHistory.icon = page.icon;
|
icon: page.icon,
|
||||||
pageHistory.version = 1; // TODO: make incremental
|
version: 1, // TODO: make incremental
|
||||||
pageHistory.coverPhoto = page.coverPhoto;
|
coverPhoto: page.coverPhoto,
|
||||||
pageHistory.lastUpdatedById = page.lastUpdatedById ?? page.creatorId;
|
lastUpdatedById: page.lastUpdatedById ?? page.creatorId,
|
||||||
pageHistory.workspaceId = page.workspaceId;
|
spaceId: page.spaceId,
|
||||||
|
workspaceId: page.workspaceId,
|
||||||
await this.pageHistoryRepo.save(pageHistory);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findHistoryByPageId(pageId: string, limit = 50, offset = 0) {
|
async findHistoryByPageId(
|
||||||
const history = await this.pageHistoryRepo
|
pageId: string,
|
||||||
.createQueryBuilder('history')
|
paginationOptions: PaginationOptions,
|
||||||
.where('history.pageId = :pageId', { pageId })
|
) {
|
||||||
.leftJoinAndSelect('history.lastUpdatedBy', 'user')
|
const { pageHistory, count } =
|
||||||
.select([
|
await this.pageHistoryRepo.findPageHistoryByPageId(
|
||||||
'history.id',
|
pageId,
|
||||||
'history.pageId',
|
paginationOptions,
|
||||||
'history.title',
|
);
|
||||||
'history.slug',
|
|
||||||
'history.icon',
|
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
||||||
'history.coverPhoto',
|
return new PaginatedResult(pageHistory, paginationMeta);
|
||||||
'history.version',
|
|
||||||
'history.lastUpdatedById',
|
|
||||||
'history.workspaceId',
|
|
||||||
'history.createdAt',
|
|
||||||
'history.updatedAt',
|
|
||||||
'user.id',
|
|
||||||
'user.name',
|
|
||||||
'user.avatarUrl',
|
|
||||||
])
|
|
||||||
.orderBy('history.updatedAt', 'DESC')
|
|
||||||
.offset(offset)
|
|
||||||
.take(limit)
|
|
||||||
.getMany();
|
|
||||||
return history;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
BadRequestException,
|
|
||||||
forwardRef,
|
forwardRef,
|
||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { PageRepository } from '../repositories/page.repository';
|
|
||||||
import { Page } from '../entities/page.entity';
|
|
||||||
import { MovePageDto } from '../dto/move-page.dto';
|
import { MovePageDto } from '../dto/move-page.dto';
|
||||||
import {
|
import {
|
||||||
OrderingEntity,
|
OrderingEntity,
|
||||||
@ -13,141 +11,185 @@ import {
|
|||||||
removeFromArrayAndSave,
|
removeFromArrayAndSave,
|
||||||
TreeNode,
|
TreeNode,
|
||||||
} from '../page.util';
|
} from '../page.util';
|
||||||
import { DataSource, EntityManager } from 'typeorm';
|
|
||||||
import { PageService } from './page.service';
|
import { PageService } from './page.service';
|
||||||
import { PageOrdering } from '../entities/page-ordering.entity';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
|
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||||
|
import { executeTx } from '@docmost/db/utils';
|
||||||
|
import { Page, PageOrdering } from '@docmost/db/types/entity.types';
|
||||||
import { PageWithOrderingDto } from '../dto/page-with-ordering.dto';
|
import { PageWithOrderingDto } from '../dto/page-with-ordering.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PageOrderingService {
|
export class PageOrderingService {
|
||||||
constructor(
|
constructor(
|
||||||
private pageRepository: PageRepository,
|
|
||||||
private dataSource: DataSource,
|
|
||||||
@Inject(forwardRef(() => PageService))
|
@Inject(forwardRef(() => PageService))
|
||||||
private pageService: PageService,
|
private pageService: PageService,
|
||||||
|
@InjectKysely() private readonly db: KyselyDB,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async movePage(dto: MovePageDto): Promise<void> {
|
// TODO: scope to workspace and space
|
||||||
await this.dataSource.transaction(async (manager: EntityManager) => {
|
|
||||||
const movedPageId = dto.id;
|
|
||||||
|
|
||||||
const movedPage = await manager
|
async movePage(dto: MovePageDto, trx?: KyselyTransaction): Promise<void> {
|
||||||
.createQueryBuilder(Page, 'page')
|
await executeTx(
|
||||||
.where('page.id = :movedPageId', { movedPageId })
|
this.db,
|
||||||
.select(['page.id', 'page.spaceId', 'page.parentPageId'])
|
async (trx) => {
|
||||||
.getOne();
|
const movedPageId = dto.pageId;
|
||||||
|
|
||||||
if (!movedPage) throw new BadRequestException('Moved page not found');
|
const movedPage = await trx
|
||||||
|
.selectFrom('pages as page')
|
||||||
|
.select(['page.id', 'page.spaceId', 'page.parentPageId'])
|
||||||
|
.where('page.id', '=', movedPageId)
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
if (!dto.parentId) {
|
if (!movedPage) throw new NotFoundException('Moved page not found');
|
||||||
if (movedPage.parentPageId) {
|
|
||||||
await this.removeFromParent(movedPage.parentPageId, dto.id, manager);
|
|
||||||
}
|
|
||||||
const spaceOrdering = await this.getEntityOrdering(
|
|
||||||
movedPage.spaceId,
|
|
||||||
OrderingEntity.space,
|
|
||||||
manager,
|
|
||||||
);
|
|
||||||
|
|
||||||
orderPageList(spaceOrdering.childrenIds, dto);
|
// if no parentId, it means the page is a root page or now a root page
|
||||||
|
if (!dto.parentId) {
|
||||||
|
// if it had a parent before being moved, we detach it from the previous parent
|
||||||
|
if (movedPage.parentPageId) {
|
||||||
|
await this.removeFromParent(
|
||||||
|
movedPage.parentPageId,
|
||||||
|
dto.pageId,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const spaceOrdering = await this.getEntityOrdering(
|
||||||
|
movedPage.spaceId,
|
||||||
|
OrderingEntity.SPACE,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
|
||||||
await manager.save(spaceOrdering);
|
orderPageList(spaceOrdering.childrenIds, dto);
|
||||||
} else {
|
// it should save or update right?
|
||||||
const parentPageId = dto.parentId;
|
// await manager.save(spaceOrdering); //TODO: to update or create new record? pretty confusing
|
||||||
|
await trx
|
||||||
|
.updateTable('page_ordering')
|
||||||
|
.set(spaceOrdering)
|
||||||
|
.where('id', '=', spaceOrdering.id)
|
||||||
|
.execute();
|
||||||
|
} else {
|
||||||
|
const parentPageId = dto.parentId;
|
||||||
|
|
||||||
let parentPageOrdering = await this.getEntityOrdering(
|
let parentPageOrdering = await this.getEntityOrdering(
|
||||||
parentPageId,
|
|
||||||
OrderingEntity.page,
|
|
||||||
manager,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!parentPageOrdering) {
|
|
||||||
parentPageOrdering = await this.createPageOrdering(
|
|
||||||
parentPageId,
|
parentPageId,
|
||||||
OrderingEntity.page,
|
OrderingEntity.PAGE,
|
||||||
movedPage.spaceId,
|
trx,
|
||||||
manager,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!parentPageOrdering) {
|
||||||
|
parentPageOrdering = await this.createPageOrdering(
|
||||||
|
parentPageId,
|
||||||
|
OrderingEntity.PAGE,
|
||||||
|
movedPage.spaceId,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the parent was changed
|
||||||
|
if (
|
||||||
|
movedPage.parentPageId &&
|
||||||
|
movedPage.parentPageId !== parentPageId
|
||||||
|
) {
|
||||||
|
//if yes, remove moved page from old parent's children
|
||||||
|
await this.removeFromParent(
|
||||||
|
movedPage.parentPageId,
|
||||||
|
dto.pageId,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If movedPage didn't have a parent initially (was at root level), update the root level
|
||||||
|
if (!movedPage.parentPageId) {
|
||||||
|
await this.removeFromSpacePageOrder(
|
||||||
|
movedPage.spaceId,
|
||||||
|
dto.pageId,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify the children list of the new parentPage and save
|
||||||
|
orderPageList(parentPageOrdering.childrenIds, dto);
|
||||||
|
await trx
|
||||||
|
.updateTable('page_ordering')
|
||||||
|
.set(parentPageOrdering)
|
||||||
|
.where('id', '=', parentPageOrdering.id)
|
||||||
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the parent was changed
|
// update the parent Id of the moved page
|
||||||
if (movedPage.parentPageId && movedPage.parentPageId !== parentPageId) {
|
await trx
|
||||||
//if yes, remove moved page from old parent's children
|
.updateTable('pages')
|
||||||
await this.removeFromParent(movedPage.parentPageId, dto.id, manager);
|
.set({
|
||||||
}
|
parentPageId: movedPage.parentPageId || null,
|
||||||
|
})
|
||||||
// If movedPage didn't have a parent initially (was at root level), update the root level
|
.where('id', '=', movedPage.id)
|
||||||
if (!movedPage.parentPageId) {
|
.execute();
|
||||||
await this.removeFromSpacePageOrder(
|
},
|
||||||
movedPage.spaceId,
|
trx,
|
||||||
dto.id,
|
);
|
||||||
manager,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modify the children list of the new parentPage and save
|
|
||||||
orderPageList(parentPageOrdering.childrenIds, dto);
|
|
||||||
await manager.save(parentPageOrdering);
|
|
||||||
}
|
|
||||||
|
|
||||||
movedPage.parentPageId = dto.parentId || null;
|
|
||||||
await manager.save(movedPage);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async addPageToOrder(spaceId: string, pageId: string, parentPageId?: string) {
|
async addPageToOrder(
|
||||||
await this.dataSource.transaction(async (manager: EntityManager) => {
|
spaceId: string,
|
||||||
if (parentPageId) {
|
pageId: string,
|
||||||
await this.upsertOrdering(
|
parentPageId?: string,
|
||||||
parentPageId,
|
trx?: KyselyTransaction,
|
||||||
OrderingEntity.page,
|
) {
|
||||||
pageId,
|
await executeTx(
|
||||||
spaceId,
|
this.db,
|
||||||
manager,
|
async (trx: KyselyTransaction) => {
|
||||||
);
|
if (parentPageId) {
|
||||||
} else {
|
await this.upsertOrdering(
|
||||||
await this.addToSpacePageOrder(spaceId, pageId, manager);
|
parentPageId,
|
||||||
}
|
OrderingEntity.PAGE,
|
||||||
});
|
pageId,
|
||||||
|
spaceId,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await this.addToSpacePageOrder(spaceId, pageId, trx);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trx,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addToSpacePageOrder(
|
async addToSpacePageOrder(
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
pageId: string,
|
pageId: string,
|
||||||
manager: EntityManager,
|
trx: KyselyTransaction,
|
||||||
) {
|
) {
|
||||||
await this.upsertOrdering(
|
await this.upsertOrdering(
|
||||||
spaceId,
|
spaceId,
|
||||||
OrderingEntity.space,
|
OrderingEntity.SPACE,
|
||||||
pageId,
|
pageId,
|
||||||
spaceId,
|
spaceId,
|
||||||
manager,
|
trx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeFromParent(
|
async removeFromParent(
|
||||||
parentId: string,
|
parentId: string,
|
||||||
childId: string,
|
childId: string,
|
||||||
manager: EntityManager,
|
trx: KyselyTransaction,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.removeChildFromOrdering(
|
await this.removeChildFromOrdering(
|
||||||
parentId,
|
parentId,
|
||||||
OrderingEntity.page,
|
OrderingEntity.PAGE,
|
||||||
childId,
|
childId,
|
||||||
manager,
|
trx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeFromSpacePageOrder(
|
async removeFromSpacePageOrder(
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
pageId: string,
|
pageId: string,
|
||||||
manager: EntityManager,
|
trx: KyselyTransaction,
|
||||||
) {
|
) {
|
||||||
await this.removeChildFromOrdering(
|
await this.removeChildFromOrdering(
|
||||||
spaceId,
|
spaceId,
|
||||||
OrderingEntity.space,
|
OrderingEntity.SPACE,
|
||||||
pageId,
|
pageId,
|
||||||
manager,
|
trx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,27 +197,23 @@ export class PageOrderingService {
|
|||||||
entityId: string,
|
entityId: string,
|
||||||
entityType: string,
|
entityType: string,
|
||||||
childId: string,
|
childId: string,
|
||||||
manager: EntityManager,
|
trx: KyselyTransaction,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const ordering = await this.getEntityOrdering(
|
const ordering = await this.getEntityOrdering(entityId, entityType, trx);
|
||||||
entityId,
|
|
||||||
entityType,
|
|
||||||
manager,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (ordering && ordering.childrenIds.includes(childId)) {
|
if (ordering && ordering.childrenIds.includes(childId)) {
|
||||||
await removeFromArrayAndSave(ordering, 'childrenIds', childId, manager);
|
await removeFromArrayAndSave(ordering, 'childrenIds', childId, trx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async removePageFromHierarchy(
|
async removePageFromHierarchy(
|
||||||
page: Page,
|
page: Page,
|
||||||
manager: EntityManager,
|
trx: KyselyTransaction,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (page.parentPageId) {
|
if (page.parentPageId) {
|
||||||
await this.removeFromParent(page.parentPageId, page.id, manager);
|
await this.removeFromParent(page.parentPageId, page.id, trx);
|
||||||
} else {
|
} else {
|
||||||
await this.removeFromSpacePageOrder(page.spaceId, page.id, manager);
|
await this.removeFromSpacePageOrder(page.spaceId, page.id, trx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,65 +222,74 @@ export class PageOrderingService {
|
|||||||
entityType: string,
|
entityType: string,
|
||||||
childId: string,
|
childId: string,
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
manager: EntityManager,
|
trx: KyselyTransaction,
|
||||||
) {
|
) {
|
||||||
let ordering = await this.getEntityOrdering(entityId, entityType, manager);
|
let ordering = await this.getEntityOrdering(entityId, entityType, trx);
|
||||||
|
|
||||||
if (!ordering) {
|
if (!ordering) {
|
||||||
ordering = await this.createPageOrdering(
|
ordering = await this.createPageOrdering(
|
||||||
entityId,
|
entityId,
|
||||||
entityType,
|
entityType,
|
||||||
spaceId,
|
spaceId,
|
||||||
manager,
|
trx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ordering.childrenIds.includes(childId)) {
|
if (!ordering.childrenIds.includes(childId)) {
|
||||||
ordering.childrenIds.unshift(childId);
|
ordering.childrenIds.unshift(childId);
|
||||||
await manager.save(PageOrdering, ordering);
|
await trx
|
||||||
|
.updateTable('page_ordering')
|
||||||
|
.set(ordering)
|
||||||
|
.where('id', '=', ordering.id)
|
||||||
|
.execute();
|
||||||
|
//await manager.save(PageOrdering, ordering);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEntityOrdering(
|
async getEntityOrdering(
|
||||||
entityId: string,
|
entityId: string,
|
||||||
entityType: string,
|
entityType: string,
|
||||||
manager,
|
trx: KyselyTransaction,
|
||||||
): Promise<PageOrdering> {
|
): Promise<PageOrdering> {
|
||||||
return manager
|
return trx
|
||||||
.createQueryBuilder(PageOrdering, 'ordering')
|
.selectFrom('page_ordering')
|
||||||
.setLock('pessimistic_write')
|
.selectAll()
|
||||||
.where('ordering.entityId = :entityId', { entityId })
|
.where('entityId', '=', entityId)
|
||||||
.andWhere('ordering.entityType = :entityType', {
|
.where('entityType', '=', entityType)
|
||||||
entityType,
|
.forUpdate()
|
||||||
})
|
.executeTakeFirst();
|
||||||
.getOne();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createPageOrdering(
|
async createPageOrdering(
|
||||||
entityId: string,
|
entityId: string,
|
||||||
entityType: string,
|
entityType: string,
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
manager: EntityManager,
|
trx: KyselyTransaction,
|
||||||
): Promise<PageOrdering> {
|
): Promise<PageOrdering> {
|
||||||
await manager.query(
|
await trx
|
||||||
`INSERT INTO page_ordering ("entityId", "entityType", "spaceId", "childrenIds")
|
.insertInto('page_ordering')
|
||||||
VALUES ($1, $2, $3, '{}')
|
.values({
|
||||||
ON CONFLICT ("entityId", "entityType") DO NOTHING`,
|
entityId,
|
||||||
[entityId, entityType, spaceId],
|
entityType,
|
||||||
);
|
spaceId,
|
||||||
|
childrenIds: [],
|
||||||
|
})
|
||||||
|
.onConflict((oc) => oc.columns(['entityId', 'entityType']).doNothing())
|
||||||
|
.execute();
|
||||||
|
|
||||||
return await this.getEntityOrdering(entityId, entityType, manager);
|
// Todo: maybe use returning above
|
||||||
|
return await this.getEntityOrdering(entityId, entityType, trx);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSpacePageOrder(spaceId: string): Promise<PageOrdering> {
|
async getSpacePageOrder(
|
||||||
return await this.dataSource
|
spaceId: string,
|
||||||
.createQueryBuilder(PageOrdering, 'ordering')
|
): Promise<{ id: string; childrenIds: string[]; spaceId: string }> {
|
||||||
.select(['ordering.id', 'ordering.childrenIds', 'ordering.spaceId'])
|
return await this.db
|
||||||
.where('ordering.entityId = :spaceId', { spaceId })
|
.selectFrom('page_ordering')
|
||||||
.andWhere('ordering.entityType = :entityType', {
|
.select(['id', 'childrenIds', 'spaceId'])
|
||||||
entityType: OrderingEntity.space,
|
.where('entityId', '=', spaceId)
|
||||||
})
|
.where('entityType', '=', OrderingEntity.SPACE)
|
||||||
.getOne();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
async convertToTree(spaceId: string): Promise<TreeNode[]> {
|
async convertToTree(spaceId: string): Promise<TreeNode[]> {
|
||||||
|
|||||||
@ -1,59 +1,34 @@
|
|||||||
import {
|
import {
|
||||||
BadRequestException,
|
|
||||||
forwardRef,
|
forwardRef,
|
||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { PageRepository } from '../repositories/page.repository';
|
|
||||||
import { CreatePageDto } from '../dto/create-page.dto';
|
import { CreatePageDto } from '../dto/create-page.dto';
|
||||||
import { Page } from '../entities/page.entity';
|
|
||||||
import { UpdatePageDto } from '../dto/update-page.dto';
|
import { UpdatePageDto } from '../dto/update-page.dto';
|
||||||
import { plainToInstance } from 'class-transformer';
|
|
||||||
import { DataSource, EntityManager } from 'typeorm';
|
|
||||||
import { PageOrderingService } from './page-ordering.service';
|
import { PageOrderingService } from './page-ordering.service';
|
||||||
import { PageWithOrderingDto } from '../dto/page-with-ordering.dto';
|
import { PageWithOrderingDto } from '../dto/page-with-ordering.dto';
|
||||||
import { OrderingEntity, transformPageResult } from '../page.util';
|
import { transformPageResult } from '../page.util';
|
||||||
|
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
||||||
|
import { Page } from '@docmost/db/types/entity.types';
|
||||||
|
import { PaginationOptions } from 'src/helpers/pagination/pagination-options';
|
||||||
|
import { PaginationMetaDto } from 'src/helpers/pagination/pagination-meta-dto';
|
||||||
|
import { PaginatedResult } from 'src/helpers/pagination/paginated-result';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PageService {
|
export class PageService {
|
||||||
constructor(
|
constructor(
|
||||||
private pageRepository: PageRepository,
|
private pageRepo: PageRepo,
|
||||||
private dataSource: DataSource,
|
|
||||||
@Inject(forwardRef(() => PageOrderingService))
|
@Inject(forwardRef(() => PageOrderingService))
|
||||||
private pageOrderingService: PageOrderingService,
|
private pageOrderingService: PageOrderingService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findWithBasic(pageId: string) {
|
async findById(
|
||||||
return this.pageRepository.findOne({
|
pageId: string,
|
||||||
where: { id: pageId },
|
includeContent?: boolean,
|
||||||
select: ['id', 'title'],
|
includeYdoc?: boolean,
|
||||||
});
|
): Promise<Page> {
|
||||||
}
|
return this.pageRepo.findById(pageId, includeContent, includeYdoc);
|
||||||
|
|
||||||
async findById(pageId: string) {
|
|
||||||
return this.pageRepository.findById(pageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async findWithContent(pageId: string) {
|
|
||||||
return this.pageRepository.findWithContent(pageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async findWithYdoc(pageId: string) {
|
|
||||||
return this.pageRepository.findWithYDoc(pageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async findWithAllFields(pageId: string) {
|
|
||||||
return this.pageRepository.findWithAllFields(pageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async findOne(pageId: string): Promise<Page> {
|
|
||||||
const page = await this.findById(pageId);
|
|
||||||
if (!page) {
|
|
||||||
throw new BadRequestException('Page not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return page;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
@ -61,26 +36,26 @@ export class PageService {
|
|||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
createPageDto: CreatePageDto,
|
createPageDto: CreatePageDto,
|
||||||
): Promise<Page> {
|
): Promise<Page> {
|
||||||
const page = plainToInstance(Page, createPageDto);
|
// check if parent page exists
|
||||||
page.creatorId = userId;
|
|
||||||
page.workspaceId = workspaceId;
|
|
||||||
page.lastUpdatedById = userId;
|
|
||||||
|
|
||||||
if (createPageDto.parentPageId) {
|
if (createPageDto.parentPageId) {
|
||||||
// TODO: make sure parent page belongs to same space and user has permissions
|
// TODO: make sure parent page belongs to same space and user has permissions
|
||||||
const parentPage = await this.pageRepository.findOne({
|
const parentPage = await this.pageRepo.findById(
|
||||||
where: { id: createPageDto.parentPageId },
|
createPageDto.parentPageId,
|
||||||
select: ['id'],
|
);
|
||||||
});
|
if (!parentPage) throw new NotFoundException('Parent page not found');
|
||||||
|
|
||||||
if (!parentPage) throw new BadRequestException('Parent page not found');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdPage = await this.pageRepository.save(page);
|
//TODO: should be in a transaction
|
||||||
|
const createdPage = await this.pageRepo.insertPage({
|
||||||
|
...createPageDto,
|
||||||
|
creatorId: userId,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
lastUpdatedById: userId,
|
||||||
|
});
|
||||||
|
|
||||||
await this.pageOrderingService.addPageToOrder(
|
await this.pageOrderingService.addPageToOrder(
|
||||||
createPageDto.spaceId,
|
createPageDto.spaceId,
|
||||||
createPageDto.id,
|
createPageDto.pageId,
|
||||||
createPageDto.parentPageId,
|
createPageDto.parentPageId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -91,18 +66,16 @@ export class PageService {
|
|||||||
pageId: string,
|
pageId: string,
|
||||||
updatePageDto: UpdatePageDto,
|
updatePageDto: UpdatePageDto,
|
||||||
userId: string,
|
userId: string,
|
||||||
): Promise<Page> {
|
): Promise<void> {
|
||||||
const updateData = {
|
await this.pageRepo.updatePage(
|
||||||
...updatePageDto,
|
{
|
||||||
lastUpdatedById: userId,
|
...updatePageDto,
|
||||||
};
|
lastUpdatedById: userId,
|
||||||
|
},
|
||||||
|
pageId,
|
||||||
|
);
|
||||||
|
|
||||||
const result = await this.pageRepository.update(pageId, updateData);
|
//return await this.pageRepo.findById(pageId);
|
||||||
if (result.affected === 0) {
|
|
||||||
throw new BadRequestException(`Page not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.pageRepository.findById(pageId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateState(
|
async updateState(
|
||||||
@ -112,14 +85,19 @@ export class PageService {
|
|||||||
ydoc: any,
|
ydoc: any,
|
||||||
userId?: string, // TODO: fix this
|
userId?: string, // TODO: fix this
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.pageRepository.update(pageId, {
|
await this.pageRepo.updatePage(
|
||||||
content: content,
|
{
|
||||||
textContent: textContent,
|
content: content,
|
||||||
ydoc: ydoc,
|
textContent: textContent,
|
||||||
...(userId && { lastUpdatedById: userId }),
|
ydoc: ydoc,
|
||||||
});
|
...(userId && { lastUpdatedById: userId }),
|
||||||
|
},
|
||||||
|
pageId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// TODO: page deletion and restoration
|
||||||
async delete(pageId: string): Promise<void> {
|
async delete(pageId: string): Promise<void> {
|
||||||
await this.dataSource.transaction(async (manager: EntityManager) => {
|
await this.dataSource.transaction(async (manager: EntityManager) => {
|
||||||
const page = await manager
|
const page = await manager
|
||||||
@ -207,59 +185,30 @@ export class PageService {
|
|||||||
await manager.recover(Page, { id: child.id });
|
await manager.recover(Page, { id: child.id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
async forceDelete(pageId: string): Promise<void> {
|
async forceDelete(pageId: string): Promise<void> {
|
||||||
await this.pageRepository.delete(pageId);
|
await this.pageRepo.deletePage(pageId);
|
||||||
}
|
|
||||||
|
|
||||||
async lockOrUnlockPage(pageId: string, lock: boolean): Promise<Page> {
|
|
||||||
await this.pageRepository.update(pageId, { isLocked: lock });
|
|
||||||
return await this.pageRepository.findById(pageId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSidebarPagesBySpaceId(
|
async getSidebarPagesBySpaceId(
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
limit = 200,
|
limit = 200,
|
||||||
): Promise<PageWithOrderingDto[]> {
|
): Promise<PageWithOrderingDto[]> {
|
||||||
const pages = await this.pageRepository
|
const pages = await this.pageRepo.getSpaceSidebarPages(spaceId, limit);
|
||||||
.createQueryBuilder('page')
|
|
||||||
.leftJoin(
|
|
||||||
'page_ordering',
|
|
||||||
'ordering',
|
|
||||||
'ordering.entityId = page.id AND ordering.entityType = :entityType',
|
|
||||||
{ entityType: OrderingEntity.page },
|
|
||||||
)
|
|
||||||
.where('page.spaceId = :spaceId', { spaceId })
|
|
||||||
.select([
|
|
||||||
'page.id',
|
|
||||||
'page.title',
|
|
||||||
'page.icon',
|
|
||||||
'page.parentPageId',
|
|
||||||
'page.spaceId',
|
|
||||||
'ordering.childrenIds',
|
|
||||||
'page.creatorId',
|
|
||||||
'page.createdAt',
|
|
||||||
])
|
|
||||||
.orderBy('page.createdAt', 'DESC')
|
|
||||||
.take(limit)
|
|
||||||
.getRawMany<PageWithOrderingDto[]>();
|
|
||||||
|
|
||||||
return transformPageResult(pages);
|
return transformPageResult(pages);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRecentSpacePages(
|
async getRecentSpacePages(
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
limit = 20,
|
paginationOptions: PaginationOptions,
|
||||||
offset = 0,
|
): Promise<PaginatedResult<Page>> {
|
||||||
): Promise<Page[]> {
|
const { pages, count } = await this.pageRepo.getRecentPagesInSpace(
|
||||||
const pages = await this.pageRepository
|
spaceId,
|
||||||
.createQueryBuilder('page')
|
paginationOptions,
|
||||||
.where('page.spaceId = :spaceId', { spaceId })
|
);
|
||||||
.select(this.pageRepository.baseFields)
|
|
||||||
.orderBy('page.updatedAt', 'DESC')
|
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
||||||
.offset(offset)
|
|
||||||
.take(limit)
|
return new PaginatedResult(pages, paginationMeta);
|
||||||
.getMany();
|
|
||||||
return pages;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,8 @@ export class SearchResponseDto {
|
|||||||
icon: string;
|
icon: string;
|
||||||
parentPageId: string;
|
parentPageId: string;
|
||||||
creatorId: string;
|
creatorId: string;
|
||||||
rank: string;
|
rank: number;
|
||||||
highlight: string;
|
highlight: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,8 +10,8 @@ import {
|
|||||||
import { SearchService } from './search.service';
|
import { SearchService } from './search.service';
|
||||||
import { SearchDTO } from './dto/search.dto';
|
import { SearchDTO } from './dto/search.dto';
|
||||||
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
|
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
|
||||||
import { Workspace } from '../workspace/entities/workspace.entity';
|
|
||||||
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
||||||
|
import { Workspace } from '@docmost/db/types/entity.types';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('search')
|
@Controller('search')
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { SearchController } from './search.controller';
|
import { SearchController } from './search.controller';
|
||||||
import { SearchService } from './search.service';
|
import { SearchService } from './search.service';
|
||||||
import { PageModule } from '../page/page.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PageModule],
|
|
||||||
controllers: [SearchController],
|
controllers: [SearchController],
|
||||||
providers: [SearchService],
|
providers: [SearchService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { PageRepository } from '../page/repositories/page.repository';
|
|
||||||
import { SearchDTO } from './dto/search.dto';
|
import { SearchDTO } from './dto/search.dto';
|
||||||
import { SearchResponseDto } from './dto/search-response.dto';
|
import { SearchResponseDto } from './dto/search-response.dto';
|
||||||
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
|
import { KyselyDB } from '@docmost/db/types/kysely.types';
|
||||||
|
import { sql } from 'kysely';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const tsquery = require('pg-tsquery')();
|
const tsquery = require('pg-tsquery')();
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SearchService {
|
export class SearchService {
|
||||||
constructor(private pageRepository: PageRepository) {}
|
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||||
|
|
||||||
async searchPage(
|
async searchPage(
|
||||||
query: string,
|
query: string,
|
||||||
@ -19,46 +21,32 @@ export class SearchService {
|
|||||||
}
|
}
|
||||||
const searchQuery = tsquery(query.trim() + '*');
|
const searchQuery = tsquery(query.trim() + '*');
|
||||||
|
|
||||||
const selectColumns = [
|
const queryResults = await this.db
|
||||||
'page.id as id',
|
.selectFrom('pages')
|
||||||
'page.title as title',
|
.select([
|
||||||
'page.icon as icon',
|
'id',
|
||||||
'page.parentPageId as "parentPageId"',
|
'title',
|
||||||
'page.creatorId as "creatorId"',
|
'icon',
|
||||||
'page.createdAt as "createdAt"',
|
'parentPageId',
|
||||||
'page.updatedAt as "updatedAt"',
|
'creatorId',
|
||||||
];
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
const searchQueryBuilder = await this.pageRepository
|
sql<number>`ts_rank(tsv, to_ts_query(${searchQuery}))`.as('rank'),
|
||||||
.createQueryBuilder('page')
|
sql<string>`ts_headline('english', page.textContent, to_tsquery(${searchQuery}), 'MinWords=9, MaxWords=10, MaxFragments=10')`.as(
|
||||||
.select(selectColumns);
|
'highlight',
|
||||||
|
),
|
||||||
searchQueryBuilder.andWhere('page.workspaceId = :workspaceId', {
|
])
|
||||||
workspaceId,
|
.where('workspaceId', '=', workspaceId)
|
||||||
});
|
.where('tsv', '@@', sql<string>`to_tsquery(${searchQuery})`)
|
||||||
|
.$if(Boolean(searchParams.creatorId), (qb) =>
|
||||||
searchQueryBuilder
|
qb.where('creatorId', '=', searchParams.creatorId),
|
||||||
.addSelect('ts_rank(page.tsv, to_tsquery(:searchQuery))', 'rank')
|
|
||||||
.addSelect(
|
|
||||||
`ts_headline('english', page.textContent, to_tsquery(:searchQuery), 'MinWords=9, MaxWords=10, MaxFragments=10')`,
|
|
||||||
'highlight',
|
|
||||||
)
|
)
|
||||||
.andWhere('page.tsv @@ to_tsquery(:searchQuery)', { searchQuery })
|
.orderBy('rank', 'desc')
|
||||||
.orderBy('rank', 'DESC');
|
.limit(searchParams.limit | 20)
|
||||||
|
.offset(searchParams.offset || 0)
|
||||||
|
.execute();
|
||||||
|
|
||||||
if (searchParams?.creatorId) {
|
const searchResults = queryResults.map((result) => {
|
||||||
searchQueryBuilder.andWhere('page.creatorId = :creatorId', {
|
|
||||||
creatorId: searchParams.creatorId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
searchQueryBuilder
|
|
||||||
.take(searchParams.limit || 20)
|
|
||||||
.offset(searchParams.offset || 0);
|
|
||||||
|
|
||||||
const results = await searchQueryBuilder.getRawMany();
|
|
||||||
|
|
||||||
const searchResults = results.map((result) => {
|
|
||||||
if (result.highlight) {
|
if (result.highlight) {
|
||||||
result.highlight = result.highlight
|
result.highlight = result.highlight
|
||||||
.replace(/\r\n|\r|\n/g, ' ')
|
.replace(/\r\n|\r|\n/g, ' ')
|
||||||
|
|||||||
@ -1,69 +0,0 @@
|
|||||||
import {
|
|
||||||
Entity,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
ManyToOne,
|
|
||||||
JoinColumn,
|
|
||||||
Unique,
|
|
||||||
Check,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { User } from '../../user/entities/user.entity';
|
|
||||||
import { Space } from './space.entity';
|
|
||||||
import { Group } from '../../group/entities/group.entity';
|
|
||||||
|
|
||||||
@Entity('space_members')
|
|
||||||
// allow either userId or groupId
|
|
||||||
@Check(
|
|
||||||
'CHK_allow_userId_or_groupId',
|
|
||||||
`("userId" IS NOT NULL AND "groupId" IS NULL) OR ("userId" IS NULL AND "groupId" IS NOT NULL)`,
|
|
||||||
)
|
|
||||||
@Unique(['spaceId', 'userId'])
|
|
||||||
@Unique(['spaceId', 'groupId'])
|
|
||||||
export class SpaceMember {
|
|
||||||
@PrimaryGeneratedColumn('uuid')
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
userId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => User, (user) => user.spaces, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: 'userId' })
|
|
||||||
user: User;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
groupId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Group, (group) => group.spaces, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: 'groupId' })
|
|
||||||
group: Group;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
spaceId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Space, (space) => space.spaceMembers, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
space: Space;
|
|
||||||
|
|
||||||
@Column({ length: 100 })
|
|
||||||
role: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
creatorId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => User)
|
|
||||||
@JoinColumn({ name: 'creatorId' })
|
|
||||||
creator: User;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
import {
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
Entity,
|
|
||||||
JoinColumn,
|
|
||||||
ManyToOne,
|
|
||||||
OneToMany,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Unique,
|
|
||||||
UpdateDateColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { User } from '../../user/entities/user.entity';
|
|
||||||
import { Workspace } from '../../workspace/entities/workspace.entity';
|
|
||||||
import { Page } from '../../page/entities/page.entity';
|
|
||||||
import { SpaceVisibility, SpaceRole } from '../../../helpers/types/permission';
|
|
||||||
import { SpaceMember } from './space-member.entity';
|
|
||||||
|
|
||||||
@Entity('spaces')
|
|
||||||
@Unique(['slug', 'workspaceId'])
|
|
||||||
export class Space {
|
|
||||||
@PrimaryGeneratedColumn('uuid')
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ length: 255, nullable: true })
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@Column({ type: 'text', nullable: true })
|
|
||||||
description: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
slug: string;
|
|
||||||
|
|
||||||
@Column({ length: 255, nullable: true })
|
|
||||||
icon: string;
|
|
||||||
|
|
||||||
@Column({ length: 100, default: SpaceVisibility.OPEN })
|
|
||||||
visibility: string;
|
|
||||||
|
|
||||||
@Column({ length: 100, default: SpaceRole.WRITER })
|
|
||||||
defaultRole: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
creatorId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => User)
|
|
||||||
@JoinColumn({ name: 'creatorId' })
|
|
||||||
creator: User;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
workspaceId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Workspace, (workspace) => workspace.spaces, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: 'workspaceId' })
|
|
||||||
workspace: Workspace;
|
|
||||||
|
|
||||||
@OneToMany(() => SpaceMember, (spaceMember) => spaceMember.space)
|
|
||||||
spaceMembers: SpaceMember[];
|
|
||||||
|
|
||||||
@OneToMany(() => Page, (page) => page.space)
|
|
||||||
pages: Page[];
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { DataSource, Repository } from 'typeorm';
|
|
||||||
import { SpaceMember } from '../entities/space-member.entity';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class SpaceMemberRepository extends Repository<SpaceMember> {
|
|
||||||
constructor(private dataSource: DataSource) {
|
|
||||||
super(SpaceMember, dataSource.createEntityManager());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
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, workspaceId: string): Promise<Space> {
|
|
||||||
const queryBuilder = this.dataSource.createQueryBuilder(Space, 'space');
|
|
||||||
return await queryBuilder
|
|
||||||
.where('space.id = :id', { id: spaceId })
|
|
||||||
.andWhere('space.workspaceId = :workspaceId', { workspaceId })
|
|
||||||
.getOne();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,65 +1,62 @@
|
|||||||
import {
|
import { Injectable } from '@nestjs/common';
|
||||||
BadRequestException,
|
|
||||||
Injectable,
|
|
||||||
NotFoundException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { SpaceRepository } from '../repositories/space.repository';
|
|
||||||
import { transactionWrapper } from '../../../helpers/db.helper';
|
|
||||||
import { DataSource, EntityManager, IsNull, Not } from 'typeorm';
|
|
||||||
import { User } from '../../user/entities/user.entity';
|
|
||||||
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
|
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
|
||||||
import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto';
|
import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto';
|
||||||
import { PaginatedResult } from '../../../helpers/pagination/paginated-result';
|
import { PaginatedResult } from '../../../helpers/pagination/paginated-result';
|
||||||
import { Group } from '../../group/entities/group.entity';
|
import { KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||||
import { SpaceMemberRepository } from '../repositories/space-member.repository';
|
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||||
import { SpaceMember } from '../entities/space-member.entity';
|
import { SpaceMember } from '@docmost/db/types/entity.types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SpaceMemberService {
|
export class SpaceMemberService {
|
||||||
constructor(
|
constructor(private spaceMemberRepo: SpaceMemberRepo) {}
|
||||||
private spaceRepository: SpaceRepository,
|
|
||||||
private spaceMemberRepository: SpaceMemberRepository,
|
|
||||||
private dataSource: DataSource,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async addUserToSpace(
|
async addUserToSpace(
|
||||||
userId: string,
|
userId: string,
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
role: string,
|
role: string,
|
||||||
workspaceId,
|
workspaceId: string,
|
||||||
manager?: EntityManager,
|
trx?: KyselyTransaction,
|
||||||
): Promise<SpaceMember> {
|
): Promise<SpaceMember> {
|
||||||
return await transactionWrapper(
|
//if (existingSpaceUser) {
|
||||||
async (manager: EntityManager) => {
|
// throw new BadRequestException('User already added to this space');
|
||||||
const userExists = await manager.exists(User, {
|
// }
|
||||||
where: { id: userId, workspaceId },
|
return await this.spaceMemberRepo.insertSpaceMember(
|
||||||
});
|
{
|
||||||
if (!userExists) {
|
userId: userId,
|
||||||
throw new NotFoundException('User not found');
|
spaceId: spaceId,
|
||||||
}
|
role: role,
|
||||||
|
|
||||||
const existingSpaceUser = await manager.findOneBy(SpaceMember, {
|
|
||||||
userId: userId,
|
|
||||||
spaceId: spaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingSpaceUser) {
|
|
||||||
throw new BadRequestException('User already added to this space');
|
|
||||||
}
|
|
||||||
|
|
||||||
const spaceMember = new SpaceMember();
|
|
||||||
spaceMember.userId = userId;
|
|
||||||
spaceMember.spaceId = spaceId;
|
|
||||||
spaceMember.role = role;
|
|
||||||
await manager.save(spaceMember);
|
|
||||||
|
|
||||||
return spaceMember;
|
|
||||||
},
|
},
|
||||||
this.dataSource,
|
trx,
|
||||||
manager,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addGroupToSpace(
|
||||||
|
groupId: string,
|
||||||
|
spaceId: string,
|
||||||
|
role: string,
|
||||||
|
workspaceId: string,
|
||||||
|
trx?: KyselyTransaction,
|
||||||
|
): Promise<SpaceMember> {
|
||||||
|
//const existingSpaceUser = await manager.findOneBy(SpaceMember, {
|
||||||
|
// userId: userId,
|
||||||
|
// spaceId: spaceId,
|
||||||
|
// });
|
||||||
|
// validations?
|
||||||
|
return await this.spaceMemberRepo.insertSpaceMember(
|
||||||
|
{
|
||||||
|
groupId: groupId,
|
||||||
|
spaceId: spaceId,
|
||||||
|
role: role,
|
||||||
|
},
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* get spaces a user is a member of
|
||||||
|
* either by direct membership or via groups
|
||||||
|
*/
|
||||||
|
/*
|
||||||
async getUserSpaces(
|
async getUserSpaces(
|
||||||
userId: string,
|
userId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
@ -79,152 +76,31 @@ export class SpaceMemberService {
|
|||||||
.skip(paginationOptions.skip)
|
.skip(paginationOptions.skip)
|
||||||
.getManyAndCount();
|
.getManyAndCount();
|
||||||
|
|
||||||
/*
|
|
||||||
const getUserSpacesViaGroup = this.spaceRepository
|
|
||||||
.createQueryBuilder('space')
|
|
||||||
.leftJoin('space.spaceGroups', 'spaceGroup')
|
|
||||||
.leftJoin('spaceGroup.group', 'group')
|
|
||||||
.leftJoin('group.groupUsers', 'groupUser')
|
|
||||||
.where('groupUser.userId = :userId', { userId })
|
|
||||||
.andWhere('space.workspaceId = :workspaceId', { workspaceId })
|
|
||||||
.getManyAndCount();
|
|
||||||
|
|
||||||
console.log(await getUserSpacesViaGroup);
|
|
||||||
*/
|
|
||||||
|
|
||||||
const spaces = userSpaces.map((userSpace) => userSpace.space);
|
const spaces = userSpaces.map((userSpace) => userSpace.space);
|
||||||
|
|
||||||
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
||||||
return new PaginatedResult(spaces, paginationMeta);
|
return new PaginatedResult(spaces, paginationMeta);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* get members of a space.
|
||||||
|
* can be a group or user
|
||||||
|
*/
|
||||||
async getSpaceMembers(
|
async getSpaceMembers(
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
paginationOptions: PaginationOptions,
|
paginationOptions: PaginationOptions,
|
||||||
) {
|
) {
|
||||||
const [spaceMembers, count] = await this.spaceMemberRepository.findAndCount(
|
//todo: validate the space is inside the workspace
|
||||||
{
|
const { members, count } =
|
||||||
relations: ['user', 'group'],
|
await this.spaceMemberRepo.getSpaceMembersPaginated(
|
||||||
where: {
|
spaceId,
|
||||||
space: {
|
paginationOptions,
|
||||||
id: spaceId,
|
);
|
||||||
workspaceId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
order: {
|
|
||||||
createdAt: 'ASC',
|
|
||||||
},
|
|
||||||
take: paginationOptions.limit,
|
|
||||||
skip: paginationOptions.skip,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const members = await Promise.all(
|
|
||||||
spaceMembers.map(async (member) => {
|
|
||||||
let memberInfo = {};
|
|
||||||
|
|
||||||
if (member.user) {
|
|
||||||
memberInfo = {
|
|
||||||
id: member.user.id,
|
|
||||||
name: member.user.name,
|
|
||||||
email: member.user.email,
|
|
||||||
avatarUrl: member.user.avatarUrl,
|
|
||||||
type: 'user',
|
|
||||||
};
|
|
||||||
} else if (member.group) {
|
|
||||||
const memberCount = await this.dataSource.getRepository(Group).count({
|
|
||||||
where: {
|
|
||||||
id: member.groupId,
|
|
||||||
workspaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
memberInfo = {
|
|
||||||
id: member.group.id,
|
|
||||||
name: member.group.name,
|
|
||||||
isDefault: member.group.isDefault,
|
|
||||||
memberCount: memberCount,
|
|
||||||
type: 'group',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...memberInfo,
|
|
||||||
role: member.role,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
||||||
return new PaginatedResult(members, paginationMeta);
|
return new PaginatedResult(members, paginationMeta);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addGroupToSpace(
|
|
||||||
groupId: string,
|
|
||||||
spaceId: string,
|
|
||||||
role: string,
|
|
||||||
workspaceId,
|
|
||||||
manager?: EntityManager,
|
|
||||||
): Promise<SpaceMember> {
|
|
||||||
return await transactionWrapper(
|
|
||||||
async (manager: EntityManager) => {
|
|
||||||
const groupExists = await manager.exists(Group, {
|
|
||||||
where: { id: groupId, workspaceId },
|
|
||||||
});
|
|
||||||
if (!groupExists) {
|
|
||||||
throw new NotFoundException('Group not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingSpaceGroup = await manager.findOneBy(SpaceMember, {
|
|
||||||
groupId: groupId,
|
|
||||||
spaceId: spaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingSpaceGroup) {
|
|
||||||
throw new BadRequestException('Group already added to this space');
|
|
||||||
}
|
|
||||||
|
|
||||||
const spaceMember = new SpaceMember();
|
|
||||||
spaceMember.groupId = groupId;
|
|
||||||
spaceMember.spaceId = spaceId;
|
|
||||||
spaceMember.role = role;
|
|
||||||
await manager.save(spaceMember);
|
|
||||||
|
|
||||||
return spaceMember;
|
|
||||||
},
|
|
||||||
this.dataSource,
|
|
||||||
manager,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSpaceGroup(
|
|
||||||
spaceId: string,
|
|
||||||
workspaceId: string,
|
|
||||||
paginationOptions: PaginationOptions,
|
|
||||||
) {
|
|
||||||
const [spaceGroups, count] = await this.spaceMemberRepository.findAndCount({
|
|
||||||
relations: ['group'],
|
|
||||||
where: {
|
|
||||||
groupId: Not(IsNull()),
|
|
||||||
space: {
|
|
||||||
id: spaceId,
|
|
||||||
workspaceId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
take: paginationOptions.limit,
|
|
||||||
skip: paginationOptions.skip,
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: add group memberCount
|
|
||||||
const groups = spaceGroups.map((spaceGroup) => {
|
|
||||||
return {
|
|
||||||
...spaceGroup.group,
|
|
||||||
spaceRole: spaceGroup.role,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
|
||||||
return new PaginatedResult(groups, paginationMeta);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// 231 lines
|
||||||
|
|||||||
@ -1,59 +1,46 @@
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { CreateSpaceDto } from '../dto/create-space.dto';
|
import { CreateSpaceDto } from '../dto/create-space.dto';
|
||||||
import { Space } from '../entities/space.entity';
|
|
||||||
import { SpaceRepository } from '../repositories/space.repository';
|
|
||||||
import { transactionWrapper } from '../../../helpers/db.helper';
|
|
||||||
import { DataSource, EntityManager } from 'typeorm';
|
|
||||||
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
|
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
|
||||||
import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto';
|
import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto';
|
||||||
import { PaginatedResult } from '../../../helpers/pagination/paginated-result';
|
import { PaginatedResult } from '../../../helpers/pagination/paginated-result';
|
||||||
import { SpaceMemberRepository } from '../repositories/space-member.repository';
|
|
||||||
import slugify from 'slugify';
|
import slugify from 'slugify';
|
||||||
|
import { SpaceRepo } from '@docmost/db/repos/space/space.repo';
|
||||||
|
import { KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||||
|
import { getRandomInt } from '../../../helpers/utils';
|
||||||
|
import { Space } from '@docmost/db/types/entity.types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SpaceService {
|
export class SpaceService {
|
||||||
constructor(
|
constructor(private spaceRepo: SpaceRepo) {}
|
||||||
private spaceRepository: SpaceRepository,
|
|
||||||
private spaceMemberRepository: SpaceMemberRepository,
|
|
||||||
private dataSource: DataSource,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
userId: string,
|
userId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
createSpaceDto?: CreateSpaceDto,
|
createSpaceDto: CreateSpaceDto,
|
||||||
manager?: EntityManager,
|
trx?: KyselyTransaction,
|
||||||
): Promise<Space> {
|
): Promise<Space> {
|
||||||
return await transactionWrapper(
|
// until we allow slug in dto
|
||||||
async (manager: EntityManager) => {
|
let slug = slugify(createSpaceDto.name.toLowerCase());
|
||||||
const space = new Space();
|
const slugExists = await this.spaceRepo.slugExists(slug, workspaceId);
|
||||||
space.name = createSpaceDto.name ?? 'untitled space ';
|
if (slugExists) {
|
||||||
space.description = createSpaceDto.description ?? '';
|
slug = `${slug}-${getRandomInt()}`;
|
||||||
space.creatorId = userId;
|
}
|
||||||
space.workspaceId = workspaceId;
|
|
||||||
|
|
||||||
space.slug = slugify(space.name.toLowerCase()); // TODO: check for duplicate
|
return await this.spaceRepo.insertSpace(
|
||||||
|
{
|
||||||
await manager.save(space);
|
name: createSpaceDto.name ?? 'untitled space',
|
||||||
return space;
|
description: createSpaceDto.description ?? '',
|
||||||
|
creatorId: userId,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
slug: slug,
|
||||||
},
|
},
|
||||||
this.dataSource,
|
trx,
|
||||||
manager,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSpaceInfo(spaceId: string, workspaceId: string): Promise<Space> {
|
async getSpaceInfo(spaceId: string, workspaceId: string): Promise<Space> {
|
||||||
const space = await this.spaceRepository
|
// TODO: add memberCount
|
||||||
.createQueryBuilder('space')
|
const space = await this.spaceRepo.findById(spaceId, workspaceId);
|
||||||
.where('space.id = :spaceId', { spaceId })
|
|
||||||
.andWhere('space.workspaceId = :workspaceId', { workspaceId })
|
|
||||||
.loadRelationCountAndMap(
|
|
||||||
'space.memberCount',
|
|
||||||
'space.spaceMembers',
|
|
||||||
'spaceMembers',
|
|
||||||
) // TODO: add groups to memberCount
|
|
||||||
.getOne();
|
|
||||||
|
|
||||||
if (!space) {
|
if (!space) {
|
||||||
throw new NotFoundException('Space not found');
|
throw new NotFoundException('Space not found');
|
||||||
}
|
}
|
||||||
@ -65,17 +52,10 @@ export class SpaceService {
|
|||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
paginationOptions: PaginationOptions,
|
paginationOptions: PaginationOptions,
|
||||||
): Promise<PaginatedResult<Space>> {
|
): Promise<PaginatedResult<Space>> {
|
||||||
const [spaces, count] = await this.spaceRepository
|
const { spaces, count } = await this.spaceRepo.getSpacesInWorkspace(
|
||||||
.createQueryBuilder('space')
|
workspaceId,
|
||||||
.where('space.workspaceId = :workspaceId', { workspaceId })
|
paginationOptions,
|
||||||
.loadRelationCountAndMap(
|
);
|
||||||
'space.memberCount',
|
|
||||||
'space.spaceMembers',
|
|
||||||
'spaceMembers',
|
|
||||||
) // TODO: add groups to memberCount
|
|
||||||
.take(paginationOptions.limit)
|
|
||||||
.skip(paginationOptions.skip)
|
|
||||||
.getManyAndCount();
|
|
||||||
|
|
||||||
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
||||||
|
|
||||||
|
|||||||
@ -8,13 +8,12 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { SpaceService } from './services/space.service';
|
import { SpaceService } from './services/space.service';
|
||||||
import { AuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUser } from '../../decorators/auth-user.decorator';
|
||||||
import { User } from '../user/entities/user.entity';
|
|
||||||
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
|
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
|
||||||
import { Workspace } from '../workspace/entities/workspace.entity';
|
|
||||||
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
||||||
import { SpaceIdDto } from './dto/space-id.dto';
|
import { SpaceIdDto } from './dto/space-id.dto';
|
||||||
import { PaginationOptions } from '../../helpers/pagination/pagination-options';
|
import { PaginationOptions } from '../../helpers/pagination/pagination-options';
|
||||||
import { SpaceMemberService } from './services/space-member.service';
|
import { SpaceMemberService } from './services/space-member.service';
|
||||||
|
import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('spaces')
|
@Controller('spaces')
|
||||||
@ -37,6 +36,7 @@ export class SpaceController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get all spaces user is a member of
|
// get all spaces user is a member of
|
||||||
|
/*
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('user')
|
@Post('user')
|
||||||
async getUserSpaces(
|
async getUserSpaces(
|
||||||
@ -50,7 +50,7 @@ export class SpaceController {
|
|||||||
workspace.id,
|
workspace.id,
|
||||||
pagination,
|
pagination,
|
||||||
);
|
);
|
||||||
}
|
}*/
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('info')
|
@Post('info')
|
||||||
|
|||||||
@ -1,22 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { SpaceService } from './services/space.service';
|
import { SpaceService } from './services/space.service';
|
||||||
import { SpaceController } from './space.controller';
|
import { SpaceController } from './space.controller';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { Space } from './entities/space.entity';
|
|
||||||
import { SpaceRepository } from './repositories/space.repository';
|
|
||||||
import { SpaceMember } from './entities/space-member.entity';
|
|
||||||
import { SpaceMemberRepository } from './repositories/space-member.repository';
|
|
||||||
import { SpaceMemberService } from './services/space-member.service';
|
import { SpaceMemberService } from './services/space-member.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Space, SpaceMember])],
|
|
||||||
controllers: [SpaceController],
|
controllers: [SpaceController],
|
||||||
providers: [
|
providers: [SpaceService, SpaceMemberService],
|
||||||
SpaceService,
|
|
||||||
SpaceMemberService,
|
|
||||||
SpaceRepository,
|
|
||||||
SpaceMemberRepository,
|
|
||||||
],
|
|
||||||
exports: [SpaceService, SpaceMemberService],
|
exports: [SpaceService, SpaceMemberService],
|
||||||
})
|
})
|
||||||
export class SpaceModule {}
|
export class SpaceModule {}
|
||||||
|
|||||||
@ -1,94 +0,0 @@
|
|||||||
import {
|
|
||||||
BeforeInsert,
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
Entity,
|
|
||||||
ManyToOne,
|
|
||||||
OneToMany,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Unique,
|
|
||||||
UpdateDateColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
import * as bcrypt from 'bcrypt';
|
|
||||||
import { Workspace } from '../../workspace/entities/workspace.entity';
|
|
||||||
import { Page } from '../../page/entities/page.entity';
|
|
||||||
import { Comment } from '../../comment/entities/comment.entity';
|
|
||||||
import { Space } from '../../space/entities/space.entity';
|
|
||||||
import { SpaceMember } from '../../space/entities/space-member.entity';
|
|
||||||
|
|
||||||
@Entity('users')
|
|
||||||
@Unique(['email', 'workspaceId'])
|
|
||||||
export class User {
|
|
||||||
@PrimaryGeneratedColumn('uuid')
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ length: 255, nullable: true })
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@Column({ length: 255 })
|
|
||||||
email: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
emailVerifiedAt: Date;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
password: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
avatarUrl: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true, length: 100 })
|
|
||||||
role: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
workspaceId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Workspace, (workspace) => workspace.users, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
workspace: Workspace;
|
|
||||||
|
|
||||||
@Column({ length: 100, nullable: true })
|
|
||||||
locale: string;
|
|
||||||
|
|
||||||
@Column({ length: 300, nullable: true })
|
|
||||||
timezone: string;
|
|
||||||
|
|
||||||
@Column({ type: 'jsonb', nullable: true })
|
|
||||||
settings: any;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
lastLoginAt: Date;
|
|
||||||
|
|
||||||
@Column({ length: 100, nullable: true })
|
|
||||||
lastLoginIp: string;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: Date;
|
|
||||||
|
|
||||||
@OneToMany(() => Page, (page) => page.creator)
|
|
||||||
createdPages: Page[];
|
|
||||||
|
|
||||||
@OneToMany(() => Comment, (comment) => comment.creator)
|
|
||||||
comments: Comment[];
|
|
||||||
|
|
||||||
@OneToMany(() => Space, (space) => space.creator)
|
|
||||||
createdSpaces: Space[];
|
|
||||||
|
|
||||||
@OneToMany(() => SpaceMember, (spaceMembership) => spaceMembership.user)
|
|
||||||
spaces: SpaceMember[];
|
|
||||||
|
|
||||||
toJSON() {
|
|
||||||
delete this.password;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
async hashPassword() {
|
|
||||||
const saltRounds = 12;
|
|
||||||
this.password = await bcrypt.hash(this.password, saltRounds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
import { DataSource, Repository } from 'typeorm';
|
|
||||||
import { User } from '../entities/user.entity';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class UserRepository extends Repository<User> {
|
|
||||||
constructor(private dataSource: DataSource) {
|
|
||||||
super(User, dataSource.createEntityManager());
|
|
||||||
}
|
|
||||||
async findByEmail(email: string): Promise<User> {
|
|
||||||
const queryBuilder = this.dataSource.createQueryBuilder(User, 'user');
|
|
||||||
return await queryBuilder.where('user.email = :email', { email }).getOne();
|
|
||||||
}
|
|
||||||
|
|
||||||
async findById(userId: string): Promise<User> {
|
|
||||||
const queryBuilder = this.dataSource.createQueryBuilder(User, 'user');
|
|
||||||
return await queryBuilder.where('user.id = :id', { id: userId }).getOne();
|
|
||||||
}
|
|
||||||
|
|
||||||
async findOneByEmail(email: string, workspaceId: string): Promise<User> {
|
|
||||||
const queryBuilder = this.dataSource.createQueryBuilder(User, 'user');
|
|
||||||
return await queryBuilder
|
|
||||||
.where('user.email = :email', { email })
|
|
||||||
.andWhere('user.workspaceId = :workspaceId', { workspaceId })
|
|
||||||
.getOne();
|
|
||||||
}
|
|
||||||
|
|
||||||
async findOneByIdx(userId: string, workspaceId: string): Promise<User> {
|
|
||||||
const queryBuilder = this.dataSource.createQueryBuilder(User, 'user');
|
|
||||||
return await queryBuilder
|
|
||||||
.where('user.id = :id', { id: userId })
|
|
||||||
.andWhere('user.workspaceId = :workspaceId', { workspaceId })
|
|
||||||
.getOne();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -8,20 +8,28 @@ import {
|
|||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
import { User } from './entities/user.entity';
|
|
||||||
import { UpdateUserDto } from './dto/update-user.dto';
|
import { UpdateUserDto } from './dto/update-user.dto';
|
||||||
import { AuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUser } from '../../decorators/auth-user.decorator';
|
||||||
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
||||||
|
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||||
|
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
|
||||||
|
import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('users')
|
@Controller('users')
|
||||||
export class UserController {
|
export class UserController {
|
||||||
constructor(private readonly userService: UserService) {}
|
constructor(
|
||||||
|
private readonly userService: UserService,
|
||||||
|
private userRepo: UserRepo,
|
||||||
|
) {}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('me')
|
@Post('me')
|
||||||
async getUser(@AuthUser() authUser: User) {
|
async getUser(
|
||||||
const user: User = await this.userService.findById(authUser.id);
|
@AuthUser() authUser: User,
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
|
) {
|
||||||
|
const user = await this.userRepo.findById(authUser.id, workspace.id);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new UnauthorizedException('Invalid user');
|
throw new UnauthorizedException('Invalid user');
|
||||||
@ -35,7 +43,8 @@ export class UserController {
|
|||||||
async updateUser(
|
async updateUser(
|
||||||
@Body() updateUserDto: UpdateUserDto,
|
@Body() updateUserDto: UpdateUserDto,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
return this.userService.update(user.id, updateUserDto);
|
return this.userService.update(updateUserDto, user.id, workspace.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
import { UserController } from './user.controller';
|
import { UserController } from './user.controller';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||||
import { User } from './entities/user.entity';
|
|
||||||
import { UserRepository } from './repositories/user.repository';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([User])],
|
|
||||||
controllers: [UserController],
|
controllers: [UserController],
|
||||||
providers: [UserService, UserRepository],
|
providers: [UserService, UserRepo],
|
||||||
exports: [UserService, UserRepository],
|
exports: [UserService, UserRepo],
|
||||||
})
|
})
|
||||||
export class UserModule {}
|
export class UserModule {}
|
||||||
|
|||||||
@ -1,77 +0,0 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { UserService } from './user.service';
|
|
||||||
import { UserRepository } from './repositories/user.repository';
|
|
||||||
import { User } from './entities/user.entity';
|
|
||||||
import { BadRequestException } from '@nestjs/common';
|
|
||||||
import { CreateUserDto } from '../auth/dto/create-user.dto';
|
|
||||||
|
|
||||||
describe('UserService', () => {
|
|
||||||
let userService: UserService;
|
|
||||||
let userRepository: any;
|
|
||||||
|
|
||||||
const mockUserRepository = () => ({
|
|
||||||
findByEmail: jest.fn(),
|
|
||||||
save: jest.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [
|
|
||||||
UserService,
|
|
||||||
{
|
|
||||||
provide: UserRepository,
|
|
||||||
useFactory: mockUserRepository,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
userService = module.get<UserService>(UserService);
|
|
||||||
userRepository = module.get<UserRepository>(UserRepository);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(userService).toBeDefined();
|
|
||||||
expect(userRepository).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('create', () => {
|
|
||||||
const createUserDto: CreateUserDto = {
|
|
||||||
name: 'John Doe',
|
|
||||||
email: 'test@test.com',
|
|
||||||
password: 'password',
|
|
||||||
};
|
|
||||||
|
|
||||||
it('should throw an error if a user with this email already exists', async () => {
|
|
||||||
userRepository.findByEmail.mockResolvedValue(new User());
|
|
||||||
await expect(userService.create(createUserDto)).rejects.toThrow(
|
|
||||||
BadRequestException,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create the user if it does not already exist', async () => {
|
|
||||||
const savedUser = {
|
|
||||||
...createUserDto,
|
|
||||||
id: expect.any(String),
|
|
||||||
createdAt: expect.any(Date),
|
|
||||||
updatedAt: expect.any(Date),
|
|
||||||
lastLoginAt: expect.any(Date),
|
|
||||||
locale: 'en',
|
|
||||||
emailVerifiedAt: null,
|
|
||||||
avatar_url: null,
|
|
||||||
timezone: null,
|
|
||||||
settings: null,
|
|
||||||
lastLoginIp: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
//userRepository.findByEmail.mockResolvedValue(undefined);
|
|
||||||
userRepository.save.mockResolvedValue(savedUser);
|
|
||||||
|
|
||||||
const result = await userService.create(createUserDto);
|
|
||||||
expect(result).toMatchObject(savedUser);
|
|
||||||
|
|
||||||
expect(userRepository.save).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining(createUserDto),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -4,19 +4,23 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { UpdateUserDto } from './dto/update-user.dto';
|
import { UpdateUserDto } from './dto/update-user.dto';
|
||||||
import { User } from './entities/user.entity';
|
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||||
import { UserRepository } from './repositories/user.repository';
|
import { hashPassword } from '../../helpers/utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
constructor(private userRepository: UserRepository) {}
|
constructor(private userRepo: UserRepo) {}
|
||||||
|
|
||||||
async findById(userId: string) {
|
async findById(userId: string, workspaceId: string) {
|
||||||
return this.userRepository.findById(userId);
|
return this.userRepo.findById(userId, workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(userId: string, updateUserDto: UpdateUserDto) {
|
async update(
|
||||||
const user = await this.userRepository.findById(userId);
|
updateUserDto: UpdateUserDto,
|
||||||
|
userId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
) {
|
||||||
|
const user = await this.userRepo.findById(userId, workspaceId);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundException('User not found');
|
throw new NotFoundException('User not found');
|
||||||
}
|
}
|
||||||
@ -27,7 +31,7 @@ export class UserService {
|
|||||||
|
|
||||||
// todo need workspace scoping
|
// todo need workspace scoping
|
||||||
if (updateUserDto.email && user.email != updateUserDto.email) {
|
if (updateUserDto.email && user.email != updateUserDto.email) {
|
||||||
if (await this.userRepository.findByEmail(updateUserDto.email)) {
|
if (await this.userRepo.findByEmail(updateUserDto.email, workspaceId)) {
|
||||||
throw new BadRequestException('A user with this email already exists');
|
throw new BadRequestException('A user with this email already exists');
|
||||||
}
|
}
|
||||||
user.email = updateUserDto.email;
|
user.email = updateUserDto.email;
|
||||||
@ -37,6 +41,11 @@ export class UserService {
|
|||||||
user.avatarUrl = updateUserDto.avatarUrl;
|
user.avatarUrl = updateUserDto.avatarUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.userRepository.save(user);
|
if (updateUserDto.password) {
|
||||||
|
updateUserDto.password = await hashPassword(updateUserDto.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.userRepo.updateUser(updateUserDto, userId, workspaceId);
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,9 +11,7 @@ import { UpdateWorkspaceDto } from '../dto/update-workspace.dto';
|
|||||||
import { DeleteWorkspaceDto } from '../dto/delete-workspace.dto';
|
import { DeleteWorkspaceDto } from '../dto/delete-workspace.dto';
|
||||||
import { UpdateWorkspaceUserRoleDto } from '../dto/update-workspace-user-role.dto';
|
import { UpdateWorkspaceUserRoleDto } from '../dto/update-workspace-user-role.dto';
|
||||||
import { AuthUser } from '../../../decorators/auth-user.decorator';
|
import { AuthUser } from '../../../decorators/auth-user.decorator';
|
||||||
import { User } from '../../user/entities/user.entity';
|
|
||||||
import { AuthWorkspace } from '../../../decorators/auth-workspace.decorator';
|
import { AuthWorkspace } from '../../../decorators/auth-workspace.decorator';
|
||||||
import { Workspace } from '../entities/workspace.entity';
|
|
||||||
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
|
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
|
||||||
import { WorkspaceInvitationService } from '../services/workspace-invitation.service';
|
import { WorkspaceInvitationService } from '../services/workspace-invitation.service';
|
||||||
import { Public } from '../../../decorators/public.decorator';
|
import { Public } from '../../../decorators/public.decorator';
|
||||||
@ -23,12 +21,12 @@ import {
|
|||||||
RevokeInviteDto,
|
RevokeInviteDto,
|
||||||
} from '../dto/invitation.dto';
|
} from '../dto/invitation.dto';
|
||||||
import { Action } from '../../casl/ability.action';
|
import { Action } from '../../casl/ability.action';
|
||||||
import { WorkspaceInvitation } from '../entities/workspace-invitation.entity';
|
|
||||||
import { CheckPolicies } from '../../casl/decorators/policies.decorator';
|
import { CheckPolicies } from '../../casl/decorators/policies.decorator';
|
||||||
import { AppAbility } from '../../casl/abilities/casl-ability.factory';
|
import { AppAbility } from '../../casl/abilities/casl-ability.factory';
|
||||||
import { PoliciesGuard } from '../../casl/guards/policies.guard';
|
import { PoliciesGuard } from '../../casl/guards/policies.guard';
|
||||||
import { WorkspaceUserService } from '../services/workspace-user.service';
|
import { WorkspaceUserService } from '../services/workspace-user.service';
|
||||||
import { JwtAuthGuard } from '../../../guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../../guards/jwt-auth.guard';
|
||||||
|
import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('workspace')
|
@Controller('workspace')
|
||||||
@ -49,7 +47,9 @@ export class WorkspaceController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(PoliciesGuard)
|
@UseGuards(PoliciesGuard)
|
||||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, Workspace))
|
@CheckPolicies((ability: AppAbility) =>
|
||||||
|
ability.can(Action.Manage, 'Workspace'),
|
||||||
|
)
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('update')
|
@Post('update')
|
||||||
async updateWorkspace(
|
async updateWorkspace(
|
||||||
@ -60,7 +60,9 @@ export class WorkspaceController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(PoliciesGuard)
|
@UseGuards(PoliciesGuard)
|
||||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, Workspace))
|
@CheckPolicies((ability: AppAbility) =>
|
||||||
|
ability.can(Action.Manage, 'Workspace'),
|
||||||
|
)
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('delete')
|
@Post('delete')
|
||||||
async deleteWorkspace(@Body() deleteWorkspaceDto: DeleteWorkspaceDto) {
|
async deleteWorkspace(@Body() deleteWorkspaceDto: DeleteWorkspaceDto) {
|
||||||
@ -69,7 +71,7 @@ export class WorkspaceController {
|
|||||||
|
|
||||||
@UseGuards(PoliciesGuard)
|
@UseGuards(PoliciesGuard)
|
||||||
@CheckPolicies((ability: AppAbility) =>
|
@CheckPolicies((ability: AppAbility) =>
|
||||||
ability.can(Action.Read, 'workspaceUser'),
|
ability.can(Action.Read, 'WorkspaceUser'),
|
||||||
)
|
)
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('members')
|
@Post('members')
|
||||||
@ -96,7 +98,7 @@ export class WorkspaceController {
|
|||||||
|
|
||||||
@UseGuards(PoliciesGuard)
|
@UseGuards(PoliciesGuard)
|
||||||
@CheckPolicies((ability: AppAbility) =>
|
@CheckPolicies((ability: AppAbility) =>
|
||||||
ability.can(Action.Manage, 'workspaceUser'),
|
ability.can(Action.Manage, 'WorkspaceUser'),
|
||||||
)
|
)
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('members/role')
|
@Post('members/role')
|
||||||
@ -114,7 +116,7 @@ export class WorkspaceController {
|
|||||||
|
|
||||||
@UseGuards(PoliciesGuard)
|
@UseGuards(PoliciesGuard)
|
||||||
@CheckPolicies((ability: AppAbility) =>
|
@CheckPolicies((ability: AppAbility) =>
|
||||||
ability.can(Action.Manage, WorkspaceInvitation),
|
ability.can(Action.Manage, 'WorkspaceInvitation'),
|
||||||
)
|
)
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('invite')
|
@Post('invite')
|
||||||
@ -123,11 +125,11 @@ export class WorkspaceController {
|
|||||||
@AuthUser() authUser: User,
|
@AuthUser() authUser: User,
|
||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
return this.workspaceInvitationService.createInvitation(
|
/* return this.workspaceInvitationService.createInvitation(
|
||||||
authUser,
|
authUser,
|
||||||
workspace.id,
|
workspace.id,
|
||||||
inviteUserDto,
|
inviteUserDto,
|
||||||
);
|
);*/
|
||||||
}
|
}
|
||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@ -143,8 +145,8 @@ export class WorkspaceController {
|
|||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('invite/revoke')
|
@Post('invite/revoke')
|
||||||
async revokeInvite(@Body() revokeInviteDto: RevokeInviteDto) {
|
async revokeInvite(@Body() revokeInviteDto: RevokeInviteDto) {
|
||||||
return this.workspaceInvitationService.revokeInvitation(
|
// return this.workspaceInvitationService.revokeInvitation(
|
||||||
revokeInviteDto.invitationId,
|
// revokeInviteDto.invitationId,
|
||||||
);
|
// );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,48 +0,0 @@
|
|||||||
import {
|
|
||||||
Entity,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
ManyToOne,
|
|
||||||
JoinColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { Workspace } from './workspace.entity';
|
|
||||||
import { User } from '../../user/entities/user.entity';
|
|
||||||
|
|
||||||
@Entity('workspace_invitations')
|
|
||||||
export class WorkspaceInvitation {
|
|
||||||
@PrimaryGeneratedColumn('uuid')
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
workspaceId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Workspace, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: 'workspaceId' })
|
|
||||||
workspace: Workspace;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
invitedById: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => User)
|
|
||||||
@JoinColumn({ name: 'invitedById' })
|
|
||||||
invitedBy: User;
|
|
||||||
|
|
||||||
@Column({ length: 255 })
|
|
||||||
email: string;
|
|
||||||
|
|
||||||
@Column({ length: 100, nullable: true })
|
|
||||||
role: string;
|
|
||||||
|
|
||||||
@Column({ length: 100, nullable: true })
|
|
||||||
status: string;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
import {
|
|
||||||
Entity,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
OneToMany,
|
|
||||||
JoinColumn,
|
|
||||||
OneToOne,
|
|
||||||
DeleteDateColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { User } from '../../user/entities/user.entity';
|
|
||||||
import { Page } from '../../page/entities/page.entity';
|
|
||||||
import { WorkspaceInvitation } from './workspace-invitation.entity';
|
|
||||||
import { Comment } from '../../comment/entities/comment.entity';
|
|
||||||
import { Space } from '../../space/entities/space.entity';
|
|
||||||
import { Group } from '../../group/entities/group.entity';
|
|
||||||
import { UserRole } from '../../../helpers/types/permission';
|
|
||||||
|
|
||||||
@Entity('workspaces')
|
|
||||||
export class Workspace {
|
|
||||||
@PrimaryGeneratedColumn('uuid')
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ length: 255, nullable: true })
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@Column({ type: 'text', nullable: true })
|
|
||||||
description: string;
|
|
||||||
|
|
||||||
@Column({ length: 255, nullable: true })
|
|
||||||
logo: string;
|
|
||||||
|
|
||||||
@Column({ length: 255, nullable: true, unique: true })
|
|
||||||
hostname: string;
|
|
||||||
|
|
||||||
@Column({ length: 255, nullable: true })
|
|
||||||
customDomain: string;
|
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: true })
|
|
||||||
enableInvite: boolean;
|
|
||||||
|
|
||||||
@Column({ length: 255, unique: true, nullable: true })
|
|
||||||
inviteCode: string;
|
|
||||||
|
|
||||||
@Column({ type: 'jsonb', nullable: true })
|
|
||||||
settings: any;
|
|
||||||
|
|
||||||
@Column({ default: UserRole.MEMBER })
|
|
||||||
defaultRole: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true, type: 'uuid' })
|
|
||||||
creatorId: string;
|
|
||||||
|
|
||||||
@OneToOne(() => User)
|
|
||||||
@JoinColumn({ name: 'creatorId' })
|
|
||||||
creator: User;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
defaultSpaceId: string;
|
|
||||||
|
|
||||||
@OneToOne(() => Space, { onDelete: 'SET NULL' })
|
|
||||||
@JoinColumn({ name: 'defaultSpaceId' })
|
|
||||||
defaultSpace: Space;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: Date;
|
|
||||||
|
|
||||||
@DeleteDateColumn()
|
|
||||||
deletedAt: Date;
|
|
||||||
|
|
||||||
@OneToMany(() => User, (user) => user.workspace)
|
|
||||||
users: [];
|
|
||||||
|
|
||||||
@OneToMany(
|
|
||||||
() => WorkspaceInvitation,
|
|
||||||
(workspaceInvitation) => workspaceInvitation.workspace,
|
|
||||||
)
|
|
||||||
workspaceInvitations: WorkspaceInvitation[];
|
|
||||||
|
|
||||||
@OneToMany(() => Page, (page) => page.workspace)
|
|
||||||
pages: Page[];
|
|
||||||
|
|
||||||
@OneToMany(() => Comment, (comment) => comment.workspace)
|
|
||||||
comments: Comment[];
|
|
||||||
|
|
||||||
@OneToMany(() => Space, (space) => space.workspace)
|
|
||||||
spaces: [];
|
|
||||||
|
|
||||||
@OneToMany(() => Group, (group) => group.workspace)
|
|
||||||
groups: [];
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { DataSource, Repository } from 'typeorm';
|
|
||||||
import { WorkspaceInvitation } from '../entities/workspace-invitation.entity';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class WorkspaceInvitationRepository extends Repository<WorkspaceInvitation> {
|
|
||||||
constructor(private dataSource: DataSource) {
|
|
||||||
super(WorkspaceInvitation, dataSource.createEntityManager());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { DataSource, Repository } from 'typeorm';
|
|
||||||
import { Workspace } from '../entities/workspace.entity';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class WorkspaceRepository extends Repository<Workspace> {
|
|
||||||
constructor(private dataSource: DataSource) {
|
|
||||||
super(Workspace, dataSource.createEntityManager());
|
|
||||||
}
|
|
||||||
|
|
||||||
async findById(workspaceId: string): Promise<Workspace> {
|
|
||||||
// see: https://github.com/typeorm/typeorm/issues/9316
|
|
||||||
const queryBuilder = this.dataSource.createQueryBuilder(
|
|
||||||
Workspace,
|
|
||||||
'workspace',
|
|
||||||
);
|
|
||||||
return await queryBuilder
|
|
||||||
.where('workspace.id = :id', { id: workspaceId })
|
|
||||||
.getOne();
|
|
||||||
}
|
|
||||||
|
|
||||||
async findFirst(): Promise<Workspace> {
|
|
||||||
const createdWorkspace = await this.find({
|
|
||||||
order: {
|
|
||||||
createdAt: 'ASC',
|
|
||||||
},
|
|
||||||
take: 1,
|
|
||||||
});
|
|
||||||
return createdWorkspace[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,24 +1,17 @@
|
|||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { WorkspaceInvitationRepository } from '../repositories/workspace-invitation.repository';
|
|
||||||
import { WorkspaceInvitation } from '../entities/workspace-invitation.entity';
|
|
||||||
import { User } from '../../user/entities/user.entity';
|
|
||||||
import { WorkspaceService } from './workspace.service';
|
import { WorkspaceService } from './workspace.service';
|
||||||
import { UserService } from '../../user/user.service';
|
import { UserService } from '../../user/user.service';
|
||||||
import { InviteUserDto } from '../dto/invitation.dto';
|
|
||||||
import { WorkspaceUserService } from './workspace-user.service';
|
import { WorkspaceUserService } from './workspace-user.service';
|
||||||
import { UserRole } from '../../../helpers/types/permission';
|
|
||||||
import { UserRepository } from '../../user/repositories/user.repository';
|
|
||||||
|
|
||||||
|
// need reworking
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkspaceInvitationService {
|
export class WorkspaceInvitationService {
|
||||||
constructor(
|
constructor(
|
||||||
private workspaceInvitationRepository: WorkspaceInvitationRepository,
|
|
||||||
private workspaceService: WorkspaceService,
|
private workspaceService: WorkspaceService,
|
||||||
private workspaceUserService: WorkspaceUserService,
|
private workspaceUserService: WorkspaceUserService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
private userRepository: UserRepository,
|
|
||||||
) {}
|
) {}
|
||||||
|
/*
|
||||||
async findInvitedUserByEmail(
|
async findInvitedUserByEmail(
|
||||||
email,
|
email,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@ -108,4 +101,6 @@ export class WorkspaceInvitationService {
|
|||||||
|
|
||||||
await this.workspaceInvitationRepository.delete(invitationId);
|
await this.workspaceInvitationRepository.delete(invitationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,97 +3,67 @@ import { UpdateWorkspaceUserRoleDto } from '../dto/update-workspace-user-role.dt
|
|||||||
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
|
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
|
||||||
import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto';
|
import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto';
|
||||||
import { PaginatedResult } from '../../../helpers/pagination/paginated-result';
|
import { PaginatedResult } from '../../../helpers/pagination/paginated-result';
|
||||||
import { User } from '../../user/entities/user.entity';
|
|
||||||
import { WorkspaceRepository } from '../repositories/workspace.repository';
|
|
||||||
import { UserRepository } from '../../user/repositories/user.repository';
|
|
||||||
import { UserRole } from '../../../helpers/types/permission';
|
import { UserRole } from '../../../helpers/types/permission';
|
||||||
|
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||||
|
import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
|
||||||
|
import { User } from '@docmost/db/types/entity.types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkspaceUserService {
|
export class WorkspaceUserService {
|
||||||
constructor(
|
constructor(
|
||||||
private workspaceRepository: WorkspaceRepository,
|
private workspaceRepo: WorkspaceRepo,
|
||||||
private userRepository: UserRepository,
|
private userRepo: UserRepo,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getWorkspaceUsers(
|
async getWorkspaceUsers(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
paginationOptions: PaginationOptions,
|
paginationOptions: PaginationOptions,
|
||||||
): Promise<PaginatedResult<User>> {
|
): Promise<PaginatedResult<any>> {
|
||||||
const [workspaceUsers, count] = await this.userRepository.findAndCount({
|
const { users, count } = await this.userRepo.getUsersPaginated(
|
||||||
where: {
|
workspaceId,
|
||||||
workspaceId,
|
paginationOptions,
|
||||||
},
|
);
|
||||||
take: paginationOptions.limit,
|
|
||||||
skip: paginationOptions.skip,
|
|
||||||
});
|
|
||||||
|
|
||||||
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
||||||
return new PaginatedResult(workspaceUsers, paginationMeta);
|
return new PaginatedResult(users, paginationMeta);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateWorkspaceUserRole(
|
async updateWorkspaceUserRole(
|
||||||
authUser: User,
|
authUser: User,
|
||||||
workspaceUserRoleDto: UpdateWorkspaceUserRoleDto,
|
userRoleDto: UpdateWorkspaceUserRoleDto,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
) {
|
) {
|
||||||
const workspaceUser = await this.findAndValidateWorkspaceUser(
|
const user = await this.userRepo.findById(userRoleDto.userId, workspaceId);
|
||||||
workspaceUserRoleDto.userId,
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (workspaceUser.role === workspaceUserRoleDto.role) {
|
|
||||||
return workspaceUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspaceOwnerCount = await this.userRepository.count({
|
|
||||||
where: {
|
|
||||||
role: UserRole.OWNER,
|
|
||||||
workspaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (workspaceUser.role === UserRole.OWNER && workspaceOwnerCount === 1) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
'There must be at least one workspace owner',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
workspaceUser.role = workspaceUserRoleDto.role;
|
|
||||||
|
|
||||||
return this.userRepository.save(workspaceUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
async deactivateUser(): Promise<any> {
|
|
||||||
return 'todo';
|
|
||||||
}
|
|
||||||
|
|
||||||
async findWorkspaceUser(userId: string, workspaceId: string): Promise<User> {
|
|
||||||
return await this.userRepository.findOneBy({
|
|
||||||
id: userId,
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async findWorkspaceUserByEmail(
|
|
||||||
email: string,
|
|
||||||
workspaceId: string,
|
|
||||||
): Promise<User> {
|
|
||||||
return await this.userRepository.findOneBy({
|
|
||||||
email: email,
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async findAndValidateWorkspaceUser(
|
|
||||||
userId: string,
|
|
||||||
workspaceId: string,
|
|
||||||
): Promise<User> {
|
|
||||||
const user = await this.findWorkspaceUser(userId, workspaceId);
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new BadRequestException('Workspace member not found');
|
throw new BadRequestException('Workspace member not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return user;
|
if (user.role === userRoleDto.role) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceOwnerCount = await this.userRepo.roleCountByWorkspaceId(
|
||||||
|
UserRole.OWNER,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (user.role === UserRole.OWNER && workspaceOwnerCount === 1) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'There must be at least one workspace owner',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.userRepo.updateUser(
|
||||||
|
{
|
||||||
|
role: userRoleDto.role,
|
||||||
|
},
|
||||||
|
user.id,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deactivateUser(): Promise<any> {
|
||||||
|
return 'todo';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,93 +4,85 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { CreateWorkspaceDto } from '../dto/create-workspace.dto';
|
import { CreateWorkspaceDto } from '../dto/create-workspace.dto';
|
||||||
import { WorkspaceRepository } from '../repositories/workspace.repository';
|
|
||||||
import { Workspace } from '../entities/workspace.entity';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { UpdateWorkspaceDto } from '../dto/update-workspace.dto';
|
import { UpdateWorkspaceDto } from '../dto/update-workspace.dto';
|
||||||
import { DeleteWorkspaceDto } from '../dto/delete-workspace.dto';
|
|
||||||
import { SpaceService } from '../../space/services/space.service';
|
import { SpaceService } from '../../space/services/space.service';
|
||||||
import { DataSource, EntityManager } from 'typeorm';
|
|
||||||
import { transactionWrapper } from '../../../helpers/db.helper';
|
|
||||||
import { CreateSpaceDto } from '../../space/dto/create-space.dto';
|
import { CreateSpaceDto } from '../../space/dto/create-space.dto';
|
||||||
import { UserRepository } from '../../user/repositories/user.repository';
|
|
||||||
import { SpaceRole, UserRole } from '../../../helpers/types/permission';
|
import { SpaceRole, UserRole } from '../../../helpers/types/permission';
|
||||||
import { User } from '../../user/entities/user.entity';
|
|
||||||
import { EnvironmentService } from '../../../integrations/environment/environment.service';
|
|
||||||
import { GroupService } from '../../group/services/group.service';
|
import { GroupService } from '../../group/services/group.service';
|
||||||
import { GroupUserService } from '../../group/services/group-user.service';
|
import { GroupUserService } from '../../group/services/group-user.service';
|
||||||
import { SpaceMemberService } from '../../space/services/space-member.service';
|
import { SpaceMemberService } from '../../space/services/space-member.service';
|
||||||
|
import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
|
||||||
|
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||||
|
import { executeTx } from '@docmost/db/utils';
|
||||||
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
|
import { User } from '@docmost/db/types/entity.types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkspaceService {
|
export class WorkspaceService {
|
||||||
constructor(
|
constructor(
|
||||||
private workspaceRepository: WorkspaceRepository,
|
private workspaceRepo: WorkspaceRepo,
|
||||||
private userRepository: UserRepository,
|
|
||||||
private spaceService: SpaceService,
|
private spaceService: SpaceService,
|
||||||
private spaceMemberService: SpaceMemberService,
|
private spaceMemberService: SpaceMemberService,
|
||||||
private groupService: GroupService,
|
private groupService: GroupService,
|
||||||
private groupUserService: GroupUserService,
|
private groupUserService: GroupUserService,
|
||||||
private environmentService: EnvironmentService,
|
@InjectKysely() private readonly db: KyselyDB,
|
||||||
|
|
||||||
private dataSource: DataSource,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findById(workspaceId: string): Promise<Workspace> {
|
async findById(workspaceId: string) {
|
||||||
return this.workspaceRepository.findById(workspaceId);
|
return this.workspaceRepo.findById(workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getWorkspaceInfo(workspaceId: string): Promise<Workspace> {
|
async getWorkspaceInfo(workspaceId: string) {
|
||||||
const space = await this.workspaceRepository
|
// todo: add member count
|
||||||
.createQueryBuilder('workspace')
|
const workspace = this.workspaceRepo.findById(workspaceId);
|
||||||
.where('workspace.id = :workspaceId', { workspaceId })
|
if (!workspace) {
|
||||||
.loadRelationCountAndMap(
|
|
||||||
'workspace.memberCount',
|
|
||||||
'workspace.users',
|
|
||||||
'workspaceUsers',
|
|
||||||
)
|
|
||||||
.getOne();
|
|
||||||
|
|
||||||
if (!space) {
|
|
||||||
throw new NotFoundException('Workspace not found');
|
throw new NotFoundException('Workspace not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return space;
|
return workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
user: User,
|
user: User,
|
||||||
createWorkspaceDto: CreateWorkspaceDto,
|
createWorkspaceDto: CreateWorkspaceDto,
|
||||||
manager?: EntityManager,
|
trx?: KyselyTransaction,
|
||||||
): Promise<Workspace> {
|
) {
|
||||||
return await transactionWrapper(
|
return await executeTx(
|
||||||
async (manager) => {
|
this.db,
|
||||||
let workspace = new Workspace();
|
async (trx) => {
|
||||||
|
// create workspace
|
||||||
workspace.name = createWorkspaceDto.name;
|
const workspace = await this.workspaceRepo.insertWorkspace(
|
||||||
workspace.hostname = createWorkspaceDto?.hostname;
|
{
|
||||||
workspace.description = createWorkspaceDto.description;
|
name: createWorkspaceDto.name,
|
||||||
workspace.inviteCode = uuidv4();
|
hostname: createWorkspaceDto.hostname,
|
||||||
workspace.creatorId = user.id;
|
description: createWorkspaceDto.description,
|
||||||
workspace = await manager.save(workspace);
|
creatorId: user.id,
|
||||||
|
},
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
|
||||||
// create default group
|
// create default group
|
||||||
const group = await this.groupService.createDefaultGroup(
|
const group = await this.groupService.createDefaultGroup(
|
||||||
workspace.id,
|
workspace.id,
|
||||||
user.id,
|
user.id,
|
||||||
manager,
|
trx,
|
||||||
);
|
);
|
||||||
|
|
||||||
// attach user to workspace
|
// add user to workspace
|
||||||
user.workspaceId = workspace.id;
|
await trx
|
||||||
user.role = UserRole.OWNER;
|
.updateTable('users')
|
||||||
await manager.save(user);
|
.set({
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
role: UserRole.OWNER,
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
|
||||||
// add user to default group
|
// add user to default group
|
||||||
await this.groupUserService.addUserToGroup(
|
await this.groupUserService.addUserToGroup(
|
||||||
user.id,
|
user.id,
|
||||||
group.id,
|
group.id,
|
||||||
workspace.id,
|
workspace.id,
|
||||||
manager,
|
trx,
|
||||||
);
|
);
|
||||||
|
|
||||||
// create default space
|
// create default space
|
||||||
@ -98,12 +90,11 @@ export class WorkspaceService {
|
|||||||
name: 'General',
|
name: 'General',
|
||||||
};
|
};
|
||||||
|
|
||||||
// create default space
|
|
||||||
const createdSpace = await this.spaceService.create(
|
const createdSpace = await this.spaceService.create(
|
||||||
user.id,
|
user.id,
|
||||||
workspace.id,
|
workspace.id,
|
||||||
spaceInfo,
|
spaceInfo,
|
||||||
manager,
|
trx,
|
||||||
);
|
);
|
||||||
|
|
||||||
// and add user to space as owner
|
// and add user to space as owner
|
||||||
@ -112,7 +103,7 @@ export class WorkspaceService {
|
|||||||
createdSpace.id,
|
createdSpace.id,
|
||||||
SpaceRole.OWNER,
|
SpaceRole.OWNER,
|
||||||
workspace.id,
|
workspace.id,
|
||||||
manager,
|
trx,
|
||||||
);
|
);
|
||||||
|
|
||||||
// add default group to space as writer
|
// add default group to space as writer
|
||||||
@ -121,50 +112,58 @@ export class WorkspaceService {
|
|||||||
createdSpace.id,
|
createdSpace.id,
|
||||||
SpaceRole.WRITER,
|
SpaceRole.WRITER,
|
||||||
workspace.id,
|
workspace.id,
|
||||||
manager,
|
trx,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// update default spaceId
|
||||||
workspace.defaultSpaceId = createdSpace.id;
|
workspace.defaultSpaceId = createdSpace.id;
|
||||||
await manager.save(workspace);
|
await this.workspaceRepo.updateWorkspace(
|
||||||
|
{
|
||||||
|
defaultSpaceId: createdSpace.id,
|
||||||
|
},
|
||||||
|
workspace.id,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
return workspace;
|
return workspace;
|
||||||
},
|
},
|
||||||
this.dataSource,
|
trx,
|
||||||
manager,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addUserToWorkspace(
|
async addUserToWorkspace(
|
||||||
user: User,
|
userId: string,
|
||||||
workspaceId,
|
workspaceId: string,
|
||||||
assignedRole?: UserRole,
|
assignedRole?: UserRole,
|
||||||
manager?: EntityManager,
|
trx?: KyselyTransaction,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return await transactionWrapper(
|
return await executeTx(
|
||||||
async (manager: EntityManager) => {
|
this.db,
|
||||||
const workspace = await manager.findOneBy(Workspace, {
|
async (trx) => {
|
||||||
id: workspaceId,
|
const workspace = await trx
|
||||||
});
|
.selectFrom('workspaces')
|
||||||
|
.select(['id', 'defaultRole'])
|
||||||
|
.where('workspaces.id', '=', workspaceId)
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
throw new BadRequestException('Workspace does not exist');
|
throw new BadRequestException('Workspace not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
user.role = assignedRole ?? workspace.defaultRole;
|
await trx
|
||||||
user.workspaceId = workspace.id;
|
.updateTable('users')
|
||||||
await manager.save(user);
|
.set({
|
||||||
|
role: assignedRole ?? workspace.defaultRole,
|
||||||
// User is now added to the default space via the default group
|
workspaceId: workspace.id,
|
||||||
|
})
|
||||||
|
.where('id', '=', userId)
|
||||||
|
.execute();
|
||||||
},
|
},
|
||||||
this.dataSource,
|
trx,
|
||||||
manager,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(
|
async update(workspaceId: string, updateWorkspaceDto: UpdateWorkspaceDto) {
|
||||||
workspaceId: string,
|
const workspace = await this.workspaceRepo.findById(workspaceId);
|
||||||
updateWorkspaceDto: UpdateWorkspaceDto,
|
|
||||||
): Promise<Workspace> {
|
|
||||||
const workspace = await this.workspaceRepository.findById(workspaceId);
|
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
throw new NotFoundException('Workspace not found');
|
throw new NotFoundException('Workspace not found');
|
||||||
}
|
}
|
||||||
@ -177,16 +176,15 @@ export class WorkspaceService {
|
|||||||
workspace.logo = updateWorkspaceDto.logo;
|
workspace.logo = updateWorkspaceDto.logo;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.workspaceRepository.save(workspace);
|
await this.workspaceRepo.updateWorkspace(updateWorkspaceDto, workspaceId);
|
||||||
|
return workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(deleteWorkspaceDto: DeleteWorkspaceDto): Promise<void> {
|
async delete(workspaceId: string): Promise<void> {
|
||||||
const workspace = await this.workspaceRepository.findById(
|
const workspace = await this.workspaceRepo.findById(workspaceId);
|
||||||
deleteWorkspaceDto.workspaceId,
|
|
||||||
);
|
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
throw new NotFoundException('Workspace not found');
|
throw new NotFoundException('Workspace not found');
|
||||||
}
|
}
|
||||||
// delete
|
//delete
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,32 +1,20 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { WorkspaceService } from './services/workspace.service';
|
import { WorkspaceService } from './services/workspace.service';
|
||||||
import { WorkspaceController } from './controllers/workspace.controller';
|
import { WorkspaceController } from './controllers/workspace.controller';
|
||||||
import { WorkspaceRepository } from './repositories/workspace.repository';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { Workspace } from './entities/workspace.entity';
|
|
||||||
import { WorkspaceInvitation } from './entities/workspace-invitation.entity';
|
|
||||||
import { SpaceModule } from '../space/space.module';
|
import { SpaceModule } from '../space/space.module';
|
||||||
import { WorkspaceInvitationService } from './services/workspace-invitation.service';
|
import { WorkspaceInvitationService } from './services/workspace-invitation.service';
|
||||||
import { WorkspaceInvitationRepository } from './repositories/workspace-invitation.repository';
|
|
||||||
import { WorkspaceUserService } from './services/workspace-user.service';
|
import { WorkspaceUserService } from './services/workspace-user.service';
|
||||||
import { UserModule } from '../user/user.module';
|
import { UserModule } from '../user/user.module';
|
||||||
import { GroupModule } from '../group/group.module';
|
import { GroupModule } from '../group/group.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [SpaceModule, UserModule, GroupModule],
|
||||||
TypeOrmModule.forFeature([Workspace, WorkspaceInvitation]),
|
|
||||||
SpaceModule,
|
|
||||||
UserModule,
|
|
||||||
GroupModule,
|
|
||||||
],
|
|
||||||
controllers: [WorkspaceController],
|
controllers: [WorkspaceController],
|
||||||
providers: [
|
providers: [
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
WorkspaceUserService,
|
WorkspaceUserService,
|
||||||
WorkspaceInvitationService,
|
WorkspaceInvitationService,
|
||||||
WorkspaceRepository,
|
|
||||||
WorkspaceInvitationRepository,
|
|
||||||
],
|
],
|
||||||
exports: [WorkspaceService, WorkspaceRepository],
|
exports: [WorkspaceService],
|
||||||
})
|
})
|
||||||
export class WorkspaceModule {}
|
export class WorkspaceModule {}
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { AppDataSource } from './typeorm.config';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
TypeOrmModule.forRoot({
|
|
||||||
...AppDataSource.options,
|
|
||||||
entities: ['dist/src/**/*.entity.{ts,js}'],
|
|
||||||
migrations: ['dist/src/**/migrations/*.{ts,js}'],
|
|
||||||
autoLoadEntities: true,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class DatabaseModule {}
|
|
||||||
@ -1,271 +0,0 @@
|
|||||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
||||||
|
|
||||||
export class Init1711150216801 implements MigrationInterface {
|
|
||||||
name = 'Init1711150216801';
|
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(
|
|
||||||
`CREATE TABLE "comments" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "content" jsonb, "selection" character varying(255), "type" character varying(55), "creatorId" uuid NOT NULL, "pageId" uuid NOT NULL, "parentCommentId" uuid, "resolvedById" uuid, "resolvedAt" TIMESTAMP, "workspaceId" uuid NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "editedAt" TIMESTAMP, "deletedAt" TIMESTAMP, CONSTRAINT "PK_comments" PRIMARY KEY ("id"))`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`CREATE TABLE "group_users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" uuid NOT NULL, "groupId" uuid NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_group_users_groupId_userId" UNIQUE ("groupId", "userId"), CONSTRAINT "PK_group_users" PRIMARY KEY ("id"))`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`CREATE TABLE "groups" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "description" text, "isDefault" boolean NOT NULL DEFAULT false, "workspaceId" uuid NOT NULL, "creatorId" uuid, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_groups_name_workspaceId" UNIQUE ("name", "workspaceId"), CONSTRAINT "PK_groups" PRIMARY KEY ("id"))`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`CREATE TABLE "space_members" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" uuid, "groupId" uuid, "spaceId" uuid NOT NULL, "role" character varying(100) NOT NULL, "creatorId" uuid, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_space_members_spaceId_groupId" UNIQUE ("spaceId", "groupId"), CONSTRAINT "UQ_space_members_spaceId_userId" UNIQUE ("spaceId", "userId"), CONSTRAINT "CHK_allow_userId_or_groupId" CHECK (("userId" IS NOT NULL AND "groupId" IS NULL) OR ("userId" IS NULL AND "groupId" IS NOT NULL)), CONSTRAINT "PK_space_members" PRIMARY KEY ("id"))`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`CREATE TABLE "spaces" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255), "description" text, "slug" character varying, "icon" character varying(255), "visibility" character varying(100) NOT NULL DEFAULT 'open', "defaultRole" character varying(100) NOT NULL DEFAULT 'writer', "creatorId" uuid, "workspaceId" uuid NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_spaces_slug_workspaceId" UNIQUE ("slug", "workspaceId"), CONSTRAINT "PK_spaces" PRIMARY KEY ("id"))`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`CREATE TABLE "page_history" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "pageId" uuid NOT NULL, "title" character varying(500), "content" jsonb, "slug" character varying, "icon" character varying, "coverPhoto" character varying, "version" integer NOT NULL, "lastUpdatedById" uuid NOT NULL, "spaceId" uuid NOT NULL, "workspaceId" uuid NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_page_history" PRIMARY KEY ("id"))`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`CREATE TABLE "pages" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "title" character varying(500), "icon" character varying, "content" jsonb, "html" text, "textContent" text, "tsv" tsvector, "ydoc" bytea, "slug" character varying, "coverPhoto" character varying, "editor" character varying(255), "shareId" character varying(255), "parentPageId" uuid, "creatorId" uuid NOT NULL, "lastUpdatedById" uuid, "deletedById" uuid, "spaceId" uuid NOT NULL, "workspaceId" uuid NOT NULL, "isLocked" boolean NOT NULL DEFAULT false, "status" character varying(255), "publishedAt" date, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP, CONSTRAINT "PK_pages" PRIMARY KEY ("id"))`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(`CREATE INDEX "IDX_pages_tsv" ON "pages" ("id") `);
|
|
||||||
await queryRunner.query(
|
|
||||||
`CREATE TABLE "users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255), "email" character varying(255) NOT NULL, "emailVerifiedAt" TIMESTAMP, "password" character varying NOT NULL, "avatarUrl" character varying, "role" character varying(100), "workspaceId" uuid, "locale" character varying(100), "timezone" character varying(300), "settings" jsonb, "lastLoginAt" TIMESTAMP, "lastLoginIp" character varying(100), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_users_email_workspaceId" UNIQUE ("email", "workspaceId"), CONSTRAINT "PK_users" PRIMARY KEY ("id"))`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`CREATE TABLE "workspaces" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255), "description" text, "logo" character varying(255), "hostname" character varying(255), "customDomain" character varying(255), "enableInvite" boolean NOT NULL DEFAULT true, "inviteCode" character varying(255), "settings" jsonb, "defaultRole" character varying NOT NULL DEFAULT 'member', "creatorId" uuid, "defaultSpaceId" uuid, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP, CONSTRAINT "UQ_workspaces_hostname" UNIQUE ("hostname"), CONSTRAINT "UQ_workspaces_inviteCode" UNIQUE ("inviteCode"), CONSTRAINT "REL_workspaces_creatorId" UNIQUE ("creatorId"), CONSTRAINT "REL_workspaces_defaultSpaceId" UNIQUE ("defaultSpaceId"), CONSTRAINT "PK_workspaces" PRIMARY KEY ("id"))`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`CREATE TABLE "workspace_invitations" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "workspaceId" uuid NOT NULL, "invitedById" uuid NOT NULL, "email" character varying(255) NOT NULL, "role" character varying(100), "status" character varying(100), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_workspace_invitations" PRIMARY KEY ("id"))`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`CREATE TABLE "page_ordering" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "entityId" uuid NOT NULL, "entityType" character varying(50) NOT NULL, "childrenIds" uuid array NOT NULL DEFAULT '{}', "workspaceId" uuid NOT NULL, "spaceId" uuid NOT NULL, "deletedAt" TIMESTAMP, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_page_ordering_entityId_entityType" UNIQUE ("entityId", "entityType"), CONSTRAINT "PK_page_ordering" PRIMARY KEY ("id"))`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`CREATE TABLE "attachments" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "fileName" character varying(255) NOT NULL, "filePath" character varying NOT NULL, "fileSize" bigint NOT NULL, "fileExt" character varying(55) NOT NULL, "mimeType" character varying(255) NOT NULL, "type" character varying(55) NOT NULL, "creatorId" uuid NOT NULL, "pageId" uuid, "workspaceId" uuid, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP, CONSTRAINT "PK_attachments" PRIMARY KEY ("id"))`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "comments" ADD CONSTRAINT "FK_comments_users_creatorId" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "comments" ADD CONSTRAINT "FK_comments_pages_pageId" FOREIGN KEY ("pageId") REFERENCES "pages"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "comments" ADD CONSTRAINT "FK_comments_comments_parentCommentId" FOREIGN KEY ("parentCommentId") REFERENCES "comments"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "comments" ADD CONSTRAINT "FK_comments_users_resolvedById" FOREIGN KEY ("resolvedById") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "comments" ADD CONSTRAINT "FK_comments_workspaces_workspaceId" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "group_users" ADD CONSTRAINT "FK_group_users_users_userId" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "group_users" ADD CONSTRAINT "FK_group_users_groups_groupId" FOREIGN KEY ("groupId") REFERENCES "groups"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "groups" ADD CONSTRAINT "FK_groups_workspaces_workspaceId" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "groups" ADD CONSTRAINT "FK_groups_users_creatorId" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "space_members" ADD CONSTRAINT "FK_space_members_users_userId" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "space_members" ADD CONSTRAINT "FK_space_members_groups_groupId" FOREIGN KEY ("groupId") REFERENCES "groups"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "space_members" ADD CONSTRAINT "FK_space_members_spaces_spaceId" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "space_members" ADD CONSTRAINT "FK_space_members_users_creatorId" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "spaces" ADD CONSTRAINT "FK_spaces_users_creatorId" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "spaces" ADD CONSTRAINT "FK_spaces_workspaces_workspaceId" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "page_history" ADD CONSTRAINT "FK_page_history_pages_pageId" FOREIGN KEY ("pageId") REFERENCES "pages"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "page_history" ADD CONSTRAINT "FK_page_history_users_lastUpdatedById" FOREIGN KEY ("lastUpdatedById") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "page_history" ADD CONSTRAINT "FK_page_history_spaces_spaceId" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "page_history" ADD CONSTRAINT "FK_page_history_workspaces_workspaceId" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "pages" ADD CONSTRAINT "FK_pages_users_creatorId" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "pages" ADD CONSTRAINT "FK_pages_users_lastUpdatedById" FOREIGN KEY ("lastUpdatedById") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "pages" ADD CONSTRAINT "FK_pages_users_deletedById" FOREIGN KEY ("deletedById") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "pages" ADD CONSTRAINT "FK_pages_spaces_spaceId" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "pages" ADD CONSTRAINT "FK_pages_workspaces_workspaceId" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "pages" ADD CONSTRAINT "FK_pages_pages_parentPageId" FOREIGN KEY ("parentPageId") REFERENCES "pages"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "users" ADD CONSTRAINT "FK_users_workspaces_workspaceId" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "workspaces" ADD CONSTRAINT "FK_workspaces_users_creatorId" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "workspaces" ADD CONSTRAINT "FK_workspaces_spaces_defaultSpaceId" FOREIGN KEY ("defaultSpaceId") REFERENCES "spaces"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "workspace_invitations" ADD CONSTRAINT "FK_workspace_invitations_workspaces_workspaceId" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "workspace_invitations" ADD CONSTRAINT "FK_workspace_invitations_users_invitedById" FOREIGN KEY ("invitedById") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "page_ordering" ADD CONSTRAINT "FK_page_ordering_workspaces_workspaceId" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "page_ordering" ADD CONSTRAINT "FK_page_ordering_spaces_spaceId" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "attachments" ADD CONSTRAINT "FK_attachments_users_creatorId" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "attachments" ADD CONSTRAINT "FK_attachments_pages_pageId" FOREIGN KEY ("pageId") REFERENCES "pages"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "attachments" ADD CONSTRAINT "FK_attachments_workspaces_workspaceId" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "attachments" DROP CONSTRAINT "FK_attachments_workspaces_workspaceId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "attachments" DROP CONSTRAINT "FK_attachments_pages_pageId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "attachments" DROP CONSTRAINT "FK_attachments_users_creatorId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "page_ordering" DROP CONSTRAINT "FK_page_ordering_spaces_spaceId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "page_ordering" DROP CONSTRAINT "FK_page_ordering_workspaces_workspaceId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "workspace_invitations" DROP CONSTRAINT "FK_workspace_invitations_users_invitedById"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "workspace_invitations" DROP CONSTRAINT "FK_workspace_invitations_workspaces_workspaceId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "workspaces" DROP CONSTRAINT "FK_workspaces_spaces_defaultSpaceId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "workspaces" DROP CONSTRAINT "FK_workspaces_users_creatorId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "users" DROP CONSTRAINT "FK_users_workspaces_workspaceId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "pages" DROP CONSTRAINT "FK_pages_pages_parentPageId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "pages" DROP CONSTRAINT "FK_pages_workspaces_workspaceId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "pages" DROP CONSTRAINT "FK_pages_spaces_spaceId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "pages" DROP CONSTRAINT "FK_pages_users_deletedById"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "pages" DROP CONSTRAINT "FK_pages_users_lastUpdatedById"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "pages" DROP CONSTRAINT "FK_pages_users_creatorId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "page_history" DROP CONSTRAINT "FK_page_history_workspaces_workspaceId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "page_history" DROP CONSTRAINT "FK_page_history_spaces_spaceId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "page_history" DROP CONSTRAINT "FK_page_history_users_lastUpdatedById"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "page_history" DROP CONSTRAINT "FK_page_history_pages_pageId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "spaces" DROP CONSTRAINT "FK_spaces_workspaces_workspaceId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "spaces" DROP CONSTRAINT "FK_spaces_users_creatorId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "space_members" DROP CONSTRAINT "FK_space_members_users_creatorId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "space_members" DROP CONSTRAINT "FK_space_members_spaces_spaceId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "space_members" DROP CONSTRAINT "FK_space_members_groups_groupId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "space_members" DROP CONSTRAINT "FK_space_members_users_userId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "groups" DROP CONSTRAINT "FK_groups_users_creatorId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "groups" DROP CONSTRAINT "FK_groups_workspaces_workspaceId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "group_users" DROP CONSTRAINT "FK_group_users_groups_groupId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "group_users" DROP CONSTRAINT "FK_group_users_users_userId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "comments" DROP CONSTRAINT "FK_comments_workspaces_workspaceId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "comments" DROP CONSTRAINT "FK_comments_users_resolvedById"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "comments" DROP CONSTRAINT "FK_comments_comments_parentCommentId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "comments" DROP CONSTRAINT "FK_comments_pages_pageId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "comments" DROP CONSTRAINT "FK_comments_users_creatorId"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(`DROP TABLE "attachments"`);
|
|
||||||
await queryRunner.query(`DROP TABLE "page_ordering"`);
|
|
||||||
await queryRunner.query(`DROP TABLE "workspace_invitations"`);
|
|
||||||
await queryRunner.query(`DROP TABLE "workspaces"`);
|
|
||||||
await queryRunner.query(`DROP TABLE "users"`);
|
|
||||||
await queryRunner.query(`DROP TABLE "pages"`);
|
|
||||||
await queryRunner.query(`DROP INDEX IF EXISTS "public"."IDX_pages_id"`);
|
|
||||||
await queryRunner.query(`DROP TABLE "page_history"`);
|
|
||||||
await queryRunner.query(`DROP TABLE "spaces"`);
|
|
||||||
await queryRunner.query(`DROP TABLE "space_members"`);
|
|
||||||
await queryRunner.query(`DROP TABLE "groups"`);
|
|
||||||
await queryRunner.query(`DROP TABLE "group_users"`);
|
|
||||||
await queryRunner.query(`DROP TABLE "comments"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
||||||
|
|
||||||
export class AddTSVColumnIndex1711150345785 implements MigrationInterface {
|
|
||||||
name = 'AddTSVColumnIndex1711150345785';
|
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
// TypeORM entity does not support custom index type
|
|
||||||
// if we don't set the index on the entity,
|
|
||||||
// TypeORM will always generate the index here in new migrations
|
|
||||||
// dropping previous index to recreate using GIN
|
|
||||||
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_pages_tsv";`);
|
|
||||||
await queryRunner.query(
|
|
||||||
`CREATE INDEX "IDX_pages_tsv" ON pages USING GIN ("tsv");`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_pages_tsv";`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
||||||
|
|
||||||
export class AddTsvectorTrigger1711152548283 implements MigrationInterface {
|
|
||||||
name = 'AddTsvectorTrigger1711152548283';
|
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(`
|
|
||||||
CREATE FUNCTION pages_tsvector_trigger() RETURNS trigger AS $$
|
|
||||||
begin
|
|
||||||
new.tsv :=
|
|
||||||
setweight(to_tsvector('english', coalesce(new.title, '')), 'A') ||
|
|
||||||
setweight(to_tsvector('english', coalesce(new.\"textContent\", '')), 'B');
|
|
||||||
return new;
|
|
||||||
end;
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryRunner.query(`
|
|
||||||
CREATE TRIGGER pages_tsvector_update BEFORE INSERT OR UPDATE
|
|
||||||
ON pages FOR EACH ROW EXECUTE FUNCTION pages_tsvector_trigger();
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(`DROP TRIGGER pages_tsvector_update ON Pages`);
|
|
||||||
await queryRunner.query(`DROP FUNCTION pages_tsvector_trigger`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
import { DefaultNamingStrategy, Table } from 'typeorm';
|
|
||||||
|
|
||||||
export class NamingStrategy extends DefaultNamingStrategy {
|
|
||||||
primaryKeyName(tableOrName: Table | string, columnNames: string[]): string {
|
|
||||||
const tableName = this.normalizeTableName(tableOrName);
|
|
||||||
return `PK_${tableName}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
indexName(
|
|
||||||
tableOrName: Table | string,
|
|
||||||
columnNames: string[],
|
|
||||||
where?: string,
|
|
||||||
): string {
|
|
||||||
const tableName = this.normalizeTableName(tableOrName);
|
|
||||||
|
|
||||||
let name = `${tableName}_${columnNames.join('_')}`;
|
|
||||||
if (where) name += '_' + where;
|
|
||||||
|
|
||||||
return `IDX_${name}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
uniqueConstraintName(
|
|
||||||
tableOrName: Table | string,
|
|
||||||
columnNames: string[],
|
|
||||||
): string {
|
|
||||||
const tableName = this.normalizeTableName(tableOrName);
|
|
||||||
|
|
||||||
return `UQ_${tableName}_${columnNames.join('_')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreignKeyName(
|
|
||||||
tableOrName: Table | string,
|
|
||||||
columnNames: string[],
|
|
||||||
_referencedTablePath?: string,
|
|
||||||
_referencedColumnNames?: string[],
|
|
||||||
): string {
|
|
||||||
const tableName = this.normalizeTableName(tableOrName);
|
|
||||||
const targetTable = this.normalizeTableName(_referencedTablePath);
|
|
||||||
|
|
||||||
const name = `${tableName}_${targetTable}_${columnNames.join('_')}`;
|
|
||||||
return `FK_${name}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
relationConstraintName(
|
|
||||||
tableOrName: Table | string,
|
|
||||||
columnNames: string[],
|
|
||||||
where?: string,
|
|
||||||
): string {
|
|
||||||
const tableName = this.normalizeTableName(tableOrName);
|
|
||||||
|
|
||||||
let name = `${tableName}_${columnNames.join('_')}`;
|
|
||||||
if (where) name += '_' + where;
|
|
||||||
|
|
||||||
return `REL_${name}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
normalizeTableName(tableOrName: Table | string): string {
|
|
||||||
const tableName = this.getTableName(tableOrName);
|
|
||||||
return tableName.replace('.', '_');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import { DataSource } from 'typeorm';
|
|
||||||
import * as dotenv from 'dotenv';
|
|
||||||
import { NamingStrategy } from './naming-strategy';
|
|
||||||
import { envPath } from '../helpers/utils';
|
|
||||||
|
|
||||||
dotenv.config({ path: envPath });
|
|
||||||
|
|
||||||
export const AppDataSource: DataSource = new DataSource({
|
|
||||||
type: 'postgres',
|
|
||||||
url: process.env.DATABASE_URL,
|
|
||||||
entities: ['src/**/*.entity.{ts,js}'],
|
|
||||||
migrations: ['src/**/migrations/*.{ts,js}'],
|
|
||||||
subscribers: [],
|
|
||||||
synchronize: false,
|
|
||||||
namingStrategy: new NamingStrategy(),
|
|
||||||
logging: process.env.NODE_ENV === 'development',
|
|
||||||
});
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import { DataSource, EntityManager } from 'typeorm';
|
|
||||||
|
|
||||||
export async function transactionWrapper(
|
|
||||||
operation: (...args) => any,
|
|
||||||
datasource: DataSource,
|
|
||||||
entityManager: EntityManager,
|
|
||||||
): Promise<any> {
|
|
||||||
if (entityManager) {
|
|
||||||
return await operation(entityManager);
|
|
||||||
} else {
|
|
||||||
return await datasource.manager.transaction(
|
|
||||||
async (manager: EntityManager) => {
|
|
||||||
return await operation(manager);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,7 @@
|
|||||||
import { join } from 'path';
|
import * as path from 'path';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
|
||||||
|
export const envPath = path.resolve(process.cwd(), '..', '..', '.env');
|
||||||
|
|
||||||
export function generateHostname(name: string): string {
|
export function generateHostname(name: string): string {
|
||||||
let hostname = name.replace(/[^a-z0-9]/gi, '').toLowerCase();
|
let hostname = name.replace(/[^a-z0-9]/gi, '').toLowerCase();
|
||||||
@ -6,4 +9,18 @@ export function generateHostname(name: string): string {
|
|||||||
return hostname;
|
return hostname;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const envPath = join(__dirname, '..', '..', '..', '.env');
|
export async function hashPassword(password: string) {
|
||||||
|
const saltRounds = 12;
|
||||||
|
return bcrypt.hash(password, saltRounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function comparePasswordHash(
|
||||||
|
plainPassword: string,
|
||||||
|
passwordHash: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
return bcrypt.compare(plainPassword, passwordHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRandomInt(min = 4, max = 5) {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
|||||||
@ -37,7 +37,9 @@ export class LocalDriver implements StorageDriver {
|
|||||||
try {
|
try {
|
||||||
return await fs.pathExists(this._fullPath(filePath));
|
return await fs.pathExists(this._fullPath(filePath));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(`Failed to check file existence: ${(err as Error).message}`);
|
throw new Error(
|
||||||
|
`Failed to check file existence: ${(err as Error).message}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
70
apps/server/src/kysely/kysely-db.module.ts
Normal file
70
apps/server/src/kysely/kysely-db.module.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { KyselyModule } from 'nestjs-kysely';
|
||||||
|
import { EnvironmentService } from '../integrations/environment/environment.service';
|
||||||
|
import { LogEvent, PostgresDialect } from 'kysely';
|
||||||
|
import { Pool } from 'pg';
|
||||||
|
import { GroupRepo } from '@docmost/db/repos/group/group.repo';
|
||||||
|
import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
|
||||||
|
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||||
|
import { GroupUserRepo } from '@docmost/db/repos/group/group-user.repo';
|
||||||
|
import { SpaceRepo } from '@docmost/db/repos/space/space.repo';
|
||||||
|
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||||
|
import { PageRepo } from './repos/page/page.repo';
|
||||||
|
import { CommentRepo } from './repos/comment/comment.repo';
|
||||||
|
import { PageHistoryRepo } from './repos/page/page-history.repo';
|
||||||
|
import { PageOrderingRepo } from './repos/page/page-ordering.repo';
|
||||||
|
import { AttachmentRepo } from './repos/attachment/attachment.repo';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
KyselyModule.forRootAsync({
|
||||||
|
imports: [],
|
||||||
|
inject: [EnvironmentService],
|
||||||
|
useFactory: (environmentService: EnvironmentService) => ({
|
||||||
|
dialect: new PostgresDialect({
|
||||||
|
pool: new Pool({
|
||||||
|
connectionString: environmentService.getDatabaseURL(),
|
||||||
|
}) as any,
|
||||||
|
}),
|
||||||
|
log: (event: LogEvent) => {
|
||||||
|
if (environmentService.getEnv() !== 'development') return;
|
||||||
|
if (event.level === 'query') {
|
||||||
|
console.log(event.query.sql);
|
||||||
|
if (event.query.parameters.length > 0) {
|
||||||
|
console.log('parameters: ' + event.query.parameters);
|
||||||
|
}
|
||||||
|
console.log('time: ' + event.queryDurationMillis);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
WorkspaceRepo,
|
||||||
|
UserRepo,
|
||||||
|
GroupRepo,
|
||||||
|
GroupUserRepo,
|
||||||
|
SpaceRepo,
|
||||||
|
SpaceMemberRepo,
|
||||||
|
PageRepo,
|
||||||
|
PageHistoryRepo,
|
||||||
|
PageOrderingRepo,
|
||||||
|
CommentRepo,
|
||||||
|
AttachmentRepo,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
WorkspaceRepo,
|
||||||
|
UserRepo,
|
||||||
|
GroupRepo,
|
||||||
|
GroupUserRepo,
|
||||||
|
SpaceRepo,
|
||||||
|
SpaceMemberRepo,
|
||||||
|
PageRepo,
|
||||||
|
PageHistoryRepo,
|
||||||
|
PageOrderingRepo,
|
||||||
|
CommentRepo,
|
||||||
|
AttachmentRepo,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class KyselyDbModule {}
|
||||||
@ -12,7 +12,9 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
.addColumn('logo', 'varchar', (col) => col)
|
.addColumn('logo', 'varchar', (col) => col)
|
||||||
.addColumn('hostname', 'varchar', (col) => col)
|
.addColumn('hostname', 'varchar', (col) => col)
|
||||||
.addColumn('customDomain', 'varchar', (col) => col)
|
.addColumn('customDomain', 'varchar', (col) => col)
|
||||||
.addColumn('enableInvite', 'boolean', (col) => col.notNull())
|
.addColumn('enableInvite', 'boolean', (col) =>
|
||||||
|
col.defaultTo(true).notNull(),
|
||||||
|
)
|
||||||
.addColumn('inviteCode', 'varchar', (col) => col)
|
.addColumn('inviteCode', 'varchar', (col) => col)
|
||||||
.addColumn('settings', 'jsonb', (col) => col)
|
.addColumn('settings', 'jsonb', (col) => col)
|
||||||
.addColumn('defaultRole', 'varchar', (col) =>
|
.addColumn('defaultRole', 'varchar', (col) =>
|
||||||
@ -27,13 +29,9 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
col.notNull().defaultTo(sql`now()`),
|
col.notNull().defaultTo(sql`now()`),
|
||||||
)
|
)
|
||||||
.addColumn('deletedAt', 'timestamp', (col) => col)
|
.addColumn('deletedAt', 'timestamp', (col) => col)
|
||||||
.addUniqueConstraint('UQ_workspaces_hostname', ['hostname'])
|
.addUniqueConstraint('workspaces_hostname_unique', ['hostname'])
|
||||||
.addUniqueConstraint('UQ_workspaces_inviteCode', ['inviteCode'])
|
.addUniqueConstraint('workspaces_inviteCode_unique', ['inviteCode'])
|
||||||
.addUniqueConstraint('UQ_workspaces_inviteCode', ['inviteCode'])
|
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
// CONSTRAINT "REL_workspaces_creatorId" UNIQUE ("creatorId"),
|
|
||||||
// CONSTRAINT "REL_workspaces_defaultSpaceId" UNIQUE ("defaultSpaceId"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
|||||||
@ -12,10 +12,14 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
.addColumn('password', 'varchar', (col) => col.notNull())
|
.addColumn('password', 'varchar', (col) => col.notNull())
|
||||||
.addColumn('avatarUrl', 'varchar', (col) => col)
|
.addColumn('avatarUrl', 'varchar', (col) => col)
|
||||||
.addColumn('role', 'varchar', (col) => col)
|
.addColumn('role', 'varchar', (col) => col)
|
||||||
.addColumn('workspaceId', 'uuid', (col) => col)
|
.addColumn('status', 'varchar', (col) => col)
|
||||||
|
.addColumn('workspaceId', 'uuid', (col) =>
|
||||||
|
col.references('workspaces.id').onDelete('cascade'),
|
||||||
|
)
|
||||||
.addColumn('locale', 'varchar', (col) => col)
|
.addColumn('locale', 'varchar', (col) => col)
|
||||||
.addColumn('timezone', 'varchar', (col) => col)
|
.addColumn('timezone', 'varchar', (col) => col)
|
||||||
.addColumn('settings', 'jsonb', (col) => col)
|
.addColumn('settings', 'jsonb', (col) => col)
|
||||||
|
.addColumn('lastActiveAt', 'timestamp', (col) => col)
|
||||||
.addColumn('lastLoginAt', 'timestamp', (col) => col)
|
.addColumn('lastLoginAt', 'timestamp', (col) => col)
|
||||||
.addColumn('lastLoginIp', 'varchar', (col) => col)
|
.addColumn('lastLoginIp', 'varchar', (col) => col)
|
||||||
.addColumn('createdAt', 'timestamp', (col) =>
|
.addColumn('createdAt', 'timestamp', (col) =>
|
||||||
@ -24,26 +28,17 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
.addColumn('updatedAt', 'timestamp', (col) =>
|
.addColumn('updatedAt', 'timestamp', (col) =>
|
||||||
col.notNull().defaultTo(sql`now()`),
|
col.notNull().defaultTo(sql`now()`),
|
||||||
)
|
)
|
||||||
.addUniqueConstraint('UQ_users_email_workspaceId', ['email', 'workspaceId'])
|
.addUniqueConstraint('users_email_workspaceId_unique', [
|
||||||
.execute();
|
'email',
|
||||||
|
'workspaceId',
|
||||||
// foreign key relations
|
])
|
||||||
await db.schema
|
|
||||||
.alterTable('users')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_users_workspaces_workspaceId',
|
|
||||||
['workspaceId'],
|
|
||||||
'workspaces',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.onDelete('cascade')
|
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('users')
|
.alterTable('users')
|
||||||
.dropConstraint('FK_users_workspaces_workspaceId')
|
.dropConstraint('users_workspaceId_fkey')
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.schema.dropTable('users').execute();
|
await db.schema.dropTable('users').execute();
|
||||||
|
|||||||
@ -9,50 +9,33 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
.addColumn('name', 'varchar', (col) => col.notNull())
|
.addColumn('name', 'varchar', (col) => col.notNull())
|
||||||
.addColumn('description', 'text', (col) => col)
|
.addColumn('description', 'text', (col) => col)
|
||||||
.addColumn('isDefault', 'boolean', (col) => col.notNull())
|
.addColumn('isDefault', 'boolean', (col) => col.notNull())
|
||||||
.addColumn('workspaceId', 'uuid', (col) => col.notNull())
|
.addColumn('workspaceId', 'uuid', (col) =>
|
||||||
.addColumn('creatorId', 'uuid', (col) => col)
|
col.references('workspaces.id').onDelete('cascade').notNull(),
|
||||||
|
)
|
||||||
|
.addColumn('creatorId', 'uuid', (col) => col.references('users.id'))
|
||||||
.addColumn('createdAt', 'timestamp', (col) =>
|
.addColumn('createdAt', 'timestamp', (col) =>
|
||||||
col.notNull().defaultTo(sql`now()`),
|
col.notNull().defaultTo(sql`now()`),
|
||||||
)
|
)
|
||||||
.addColumn('updatedAt', 'timestamp', (col) =>
|
.addColumn('updatedAt', 'timestamp', (col) =>
|
||||||
col.notNull().defaultTo(sql`now()`),
|
col.notNull().defaultTo(sql`now()`),
|
||||||
)
|
)
|
||||||
.addUniqueConstraint('UQ_groups_name_workspaceId', ['name', 'workspaceId'])
|
.addUniqueConstraint('groups_name_workspaceId_unique', [
|
||||||
|
'name',
|
||||||
|
'workspaceId',
|
||||||
|
])
|
||||||
|
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
// foreign key relations
|
|
||||||
await db.schema
|
|
||||||
.alterTable('groups')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_groups_workspaces_workspaceId',
|
|
||||||
['workspaceId'],
|
|
||||||
'workspaces',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.onDelete('cascade')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('groups')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_groups_users_creatorId',
|
|
||||||
['creatorId'],
|
|
||||||
'users',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('groups')
|
.alterTable('groups')
|
||||||
.dropConstraint('FK_groups_workspaces_workspaceId')
|
.dropConstraint('groups_workspaceId_fkey')
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('groups')
|
.alterTable('groups')
|
||||||
.dropConstraint('FK_groups_users_creatorId')
|
.dropConstraint('groups_creatorId_fkey')
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.schema.dropTable('groups').execute();
|
await db.schema.dropTable('groups').execute();
|
||||||
|
|||||||
@ -6,50 +6,34 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
.addColumn('id', 'uuid', (col) =>
|
.addColumn('id', 'uuid', (col) =>
|
||||||
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||||
)
|
)
|
||||||
.addColumn('userId', 'uuid', (col) => col.notNull())
|
.addColumn('userId', 'uuid', (col) =>
|
||||||
.addColumn('groupId', 'uuid', (col) => col.notNull())
|
col.references('users.id').onDelete('cascade').notNull(),
|
||||||
|
)
|
||||||
|
.addColumn('groupId', 'uuid', (col) =>
|
||||||
|
col.references('groups.id').onDelete('cascade').notNull(),
|
||||||
|
)
|
||||||
.addColumn('createdAt', 'timestamp', (col) =>
|
.addColumn('createdAt', 'timestamp', (col) =>
|
||||||
col.notNull().defaultTo(sql`now()`),
|
col.notNull().defaultTo(sql`now()`),
|
||||||
)
|
)
|
||||||
.addColumn('updatedAt', 'timestamp', (col) =>
|
.addColumn('updatedAt', 'timestamp', (col) =>
|
||||||
col.notNull().defaultTo(sql`now()`),
|
col.notNull().defaultTo(sql`now()`),
|
||||||
)
|
)
|
||||||
.addUniqueConstraint('UQ_group_users_groupId_userId', ['groupId', 'userId'])
|
.addUniqueConstraint('group_users_groupId_userId_unique', [
|
||||||
.execute();
|
'groupId',
|
||||||
|
'userId',
|
||||||
// foreign key relations
|
])
|
||||||
await db.schema
|
|
||||||
.alterTable('group_users')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_group_users_users_userId',
|
|
||||||
['userId'],
|
|
||||||
'users',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.onDelete('cascade')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('group_users')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_group_users_groups_groupId',
|
|
||||||
['groupId'],
|
|
||||||
'groups',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.onDelete('cascade')
|
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('group_users')
|
.alterTable('group_users')
|
||||||
.dropConstraint('FK_group_users_users_userId')
|
.dropConstraint('group_users_userId_fkey')
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('group_users')
|
.alterTable('group_users')
|
||||||
.dropConstraint('FK_group_users_groups_groupId')
|
.dropConstraint('group_users_groupId_fkey')
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.schema.dropTable('group_users').execute();
|
await db.schema.dropTable('group_users').execute();
|
||||||
|
|||||||
@ -17,49 +17,32 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
.addColumn('defaultRole', 'varchar', (col) =>
|
.addColumn('defaultRole', 'varchar', (col) =>
|
||||||
col.defaultTo(SpaceRole.WRITER).notNull(),
|
col.defaultTo(SpaceRole.WRITER).notNull(),
|
||||||
)
|
)
|
||||||
.addColumn('creatorId', 'uuid', (col) => col)
|
.addColumn('creatorId', 'uuid', (col) => col.references('users.id'))
|
||||||
.addColumn('workspaceId', 'uuid', (col) => col.notNull())
|
.addColumn('workspaceId', 'uuid', (col) =>
|
||||||
|
col.references('workspaces.id').onDelete('cascade').notNull(),
|
||||||
|
)
|
||||||
.addColumn('createdAt', 'timestamp', (col) =>
|
.addColumn('createdAt', 'timestamp', (col) =>
|
||||||
col.notNull().defaultTo(sql`now()`),
|
col.notNull().defaultTo(sql`now()`),
|
||||||
)
|
)
|
||||||
.addColumn('updatedAt', 'timestamp', (col) =>
|
.addColumn('updatedAt', 'timestamp', (col) =>
|
||||||
col.notNull().defaultTo(sql`now()`),
|
col.notNull().defaultTo(sql`now()`),
|
||||||
)
|
)
|
||||||
.addUniqueConstraint('UQ_spaces_slug_workspaceId', ['slug', 'workspaceId'])
|
.addUniqueConstraint('spaces_slug_workspaceId_unique', [
|
||||||
.execute();
|
'slug',
|
||||||
|
'workspaceId',
|
||||||
// foreign key relations
|
])
|
||||||
await db.schema
|
|
||||||
.alterTable('spaces')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_spaces_users_creatorId',
|
|
||||||
['creatorId'],
|
|
||||||
'users',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('spaces')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_spaces_workspaces_workspaceId',
|
|
||||||
['workspaceId'],
|
|
||||||
'workspaces',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.onDelete('cascade')
|
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('spaces')
|
.alterTable('spaces')
|
||||||
.dropConstraint('FK_spaces_users_creatorId')
|
.dropConstraint('spaces_creatorId_fkey')
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('spaces')
|
.alterTable('spaces')
|
||||||
.dropConstraint('FK_spaces_workspaces_workspaceId')
|
.dropConstraint('spaces_workspaceId_fkey')
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.schema.dropTable('spaces').execute();
|
await db.schema.dropTable('spaces').execute();
|
||||||
|
|||||||
@ -6,95 +6,57 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
.addColumn('id', 'uuid', (col) =>
|
.addColumn('id', 'uuid', (col) =>
|
||||||
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||||
)
|
)
|
||||||
.addColumn('userId', 'uuid', (col) => col)
|
.addColumn('userId', 'uuid', (col) =>
|
||||||
.addColumn('groupId', 'uuid', (col) => col)
|
col.references('users.id').onDelete('cascade'),
|
||||||
.addColumn('spaceId', 'uuid', (col) => col.notNull())
|
)
|
||||||
|
.addColumn('groupId', 'uuid', (col) =>
|
||||||
|
col.references('groups.id').onDelete('cascade'),
|
||||||
|
)
|
||||||
|
.addColumn('spaceId', 'uuid', (col) =>
|
||||||
|
col.references('spaces.id').onDelete('cascade').notNull(),
|
||||||
|
)
|
||||||
.addColumn('role', 'varchar', (col) => col.notNull())
|
.addColumn('role', 'varchar', (col) => col.notNull())
|
||||||
.addColumn('creatorId', 'uuid', (col) => col)
|
.addColumn('creatorId', 'uuid', (col) => col.references('users.id'))
|
||||||
.addColumn('createdAt', 'timestamp', (col) =>
|
.addColumn('createdAt', 'timestamp', (col) =>
|
||||||
col.notNull().defaultTo(sql`now()`),
|
col.notNull().defaultTo(sql`now()`),
|
||||||
)
|
)
|
||||||
.addColumn('updatedAt', 'timestamp', (col) =>
|
.addColumn('updatedAt', 'timestamp', (col) =>
|
||||||
col.notNull().defaultTo(sql`now()`),
|
col.notNull().defaultTo(sql`now()`),
|
||||||
)
|
)
|
||||||
.addUniqueConstraint('UQ_space_members_spaceId_userId', [
|
.addUniqueConstraint('space_members_spaceId_userId_unique', [
|
||||||
'spaceId',
|
'spaceId',
|
||||||
'userId',
|
'userId',
|
||||||
])
|
])
|
||||||
.addUniqueConstraint('UQ_space_members_spaceId_groupId', [
|
.addUniqueConstraint('space_members_spaceId_groupId_unique', [
|
||||||
'spaceId',
|
'spaceId',
|
||||||
'groupId',
|
'groupId',
|
||||||
])
|
])
|
||||||
.addCheckConstraint(
|
.addCheckConstraint(
|
||||||
'CHK_allow_userId_or_groupId',
|
'allow_either_userId_or_groupId_check',
|
||||||
sql`(("userId" IS NOT NULL AND "groupId" IS NULL) OR ("userId" IS NULL AND "groupId" IS NOT NULL))`,
|
sql`(("userId" IS NOT NULL AND "groupId" IS NULL) OR ("userId" IS NULL AND "groupId" IS NOT NULL))`,
|
||||||
)
|
)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
// foreign key relations
|
|
||||||
await db.schema
|
|
||||||
.alterTable('space_members')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_space_members_users_userId',
|
|
||||||
['userId'],
|
|
||||||
'users',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.onDelete('cascade')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('space_members')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_space_members_groups_groupId',
|
|
||||||
['groupId'],
|
|
||||||
'groups',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.onDelete('cascade')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('space_members')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_space_members_spaces_spaceId',
|
|
||||||
['spaceId'],
|
|
||||||
'spaces',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.onDelete('cascade')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('space_members')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_space_members_users_creatorId',
|
|
||||||
['creatorId'],
|
|
||||||
'users',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('space_members')
|
.alterTable('space_members')
|
||||||
.dropConstraint('FK_space_members_users_userId')
|
.dropConstraint('space_members_userId_fkey')
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('space_members')
|
.alterTable('space_members')
|
||||||
.dropConstraint('FK_space_members_groups_groupId')
|
.dropConstraint('space_members_groupId_fkey')
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('space_members')
|
.alterTable('space_members')
|
||||||
.dropConstraint('FK_space_members_spaces_spaceId')
|
.dropConstraint('space_members_spaceId_fkey')
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('space_members')
|
.alterTable('space_members')
|
||||||
.dropConstraint('FK_space_members_users_creatorId')
|
.dropConstraint('space_members_creatorId_fkey')
|
||||||
.execute();
|
.execute();
|
||||||
await db.schema.dropTable('space_members').execute();
|
await db.schema.dropTable('space_members').execute();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('workspaces')
|
.alterTable('workspaces')
|
||||||
.addForeignKeyConstraint(
|
.addForeignKeyConstraint(
|
||||||
'FK_workspaces_users_creatorId',
|
'workspaces_creatorId_fkey',
|
||||||
['creatorId'],
|
['creatorId'],
|
||||||
'users',
|
'users',
|
||||||
['id'],
|
['id'],
|
||||||
@ -14,7 +14,7 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('workspaces')
|
.alterTable('workspaces')
|
||||||
.addForeignKeyConstraint(
|
.addForeignKeyConstraint(
|
||||||
'FK_workspaces_spaces_defaultSpaceId',
|
'workspaces_defaultSpaceId_fkey',
|
||||||
['defaultSpaceId'],
|
['defaultSpaceId'],
|
||||||
'spaces',
|
'spaces',
|
||||||
['id'],
|
['id'],
|
||||||
@ -26,11 +26,11 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('workspaces')
|
.alterTable('workspaces')
|
||||||
.dropConstraint('FK_workspaces_users_creatorId')
|
.dropConstraint('workspaces_creatorId_fkey')
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('workspaces')
|
.alterTable('workspaces')
|
||||||
.dropConstraint('FK_workspaces_spaces_defaultSpaceId')
|
.dropConstraint('workspaces_defaultSpaceId_fkey')
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,8 +6,10 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
.addColumn('id', 'uuid', (col) =>
|
.addColumn('id', 'uuid', (col) =>
|
||||||
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||||
)
|
)
|
||||||
.addColumn('workspaceId', 'uuid', (col) => col.notNull())
|
.addColumn('workspaceId', 'uuid', (col) =>
|
||||||
.addColumn('invitedById', 'uuid', (col) => col.notNull())
|
col.references('workspaces.id').onDelete('cascade').notNull(),
|
||||||
|
)
|
||||||
|
.addColumn('invitedById', 'uuid', (col) => col.references('users.id'))
|
||||||
.addColumn('email', 'varchar', (col) => col.notNull())
|
.addColumn('email', 'varchar', (col) => col.notNull())
|
||||||
.addColumn('role', 'varchar', (col) => col.notNull())
|
.addColumn('role', 'varchar', (col) => col.notNull())
|
||||||
.addColumn('status', 'varchar', (col) => col)
|
.addColumn('status', 'varchar', (col) => col)
|
||||||
@ -18,39 +20,17 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
col.notNull().defaultTo(sql`now()`),
|
col.notNull().defaultTo(sql`now()`),
|
||||||
)
|
)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
// foreign key relations
|
|
||||||
await db.schema
|
|
||||||
.alterTable('workspace_invitations')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_workspace_invitations_workspaces_workspaceId',
|
|
||||||
['workspaceId'],
|
|
||||||
'workspaces',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.onDelete('cascade')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('workspace_invitations')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_workspace_invitations_users_invitedById',
|
|
||||||
['invitedById'],
|
|
||||||
'users',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('workspace_invitations')
|
.alterTable('workspace_invitations')
|
||||||
.dropConstraint('FK_workspace_invitations_workspaces_workspaceId')
|
.dropConstraint('workspace_invitations_workspaceId_fkey')
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('workspace_invitations')
|
.alterTable('workspace_invitations')
|
||||||
.dropConstraint('FK_workspace_invitations_users_invitedById')
|
.dropConstraint('workspace_invitations_invitedById_fkey')
|
||||||
.execute();
|
.execute();
|
||||||
await db.schema.dropTable('workspace_invitations').execute();
|
await db.schema.dropTable('workspace_invitations').execute();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,12 +17,18 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
.addColumn('coverPhoto', 'varchar', (col) => col)
|
.addColumn('coverPhoto', 'varchar', (col) => col)
|
||||||
.addColumn('editor', 'varchar', (col) => col)
|
.addColumn('editor', 'varchar', (col) => col)
|
||||||
.addColumn('shareId', 'varchar', (col) => col)
|
.addColumn('shareId', 'varchar', (col) => col)
|
||||||
.addColumn('parentPageId', 'uuid', (col) => col)
|
.addColumn('parentPageId', 'uuid', (col) =>
|
||||||
.addColumn('creatorId', 'uuid', (col) => col.notNull())
|
col.references('pages.id').onDelete('cascade'),
|
||||||
.addColumn('lastUpdatedById', 'uuid', (col) => col)
|
)
|
||||||
.addColumn('deletedById', 'uuid', (col) => col)
|
.addColumn('creatorId', 'uuid', (col) => col.references('users.id'))
|
||||||
.addColumn('spaceId', 'uuid', (col) => col.notNull())
|
.addColumn('lastUpdatedById', 'uuid', (col) => col.references('users.id'))
|
||||||
.addColumn('workspaceId', 'uuid', (col) => col.notNull())
|
.addColumn('deletedById', 'uuid', (col) => col.references('users.id'))
|
||||||
|
.addColumn('spaceId', 'uuid', (col) =>
|
||||||
|
col.references('spaces.id').onDelete('cascade').notNull(),
|
||||||
|
)
|
||||||
|
.addColumn('workspaceId', 'uuid', (col) =>
|
||||||
|
col.references('workspaces.id').onDelete('cascade').notNull(),
|
||||||
|
)
|
||||||
.addColumn('isLocked', 'boolean', (col) => col.defaultTo(false).notNull())
|
.addColumn('isLocked', 'boolean', (col) => col.defaultTo(false).notNull())
|
||||||
.addColumn('status', 'varchar', (col) => col)
|
.addColumn('status', 'varchar', (col) => col)
|
||||||
.addColumn('publishedAt', 'date', (col) => col)
|
.addColumn('publishedAt', 'date', (col) => col)
|
||||||
@ -36,7 +42,7 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.schema
|
await db.schema
|
||||||
.createIndex('IDX_pages_tsv')
|
.createIndex('pages_tsv_idx')
|
||||||
.on('pages')
|
.on('pages')
|
||||||
.using('GIN')
|
.using('GIN')
|
||||||
.column('tsv')
|
.column('tsv')
|
||||||
@ -44,5 +50,35 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.alterTable('pages')
|
||||||
|
.dropConstraint('pages_creatorId_fkey')
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.alterTable('pages')
|
||||||
|
.dropConstraint('pages_lastUpdatedById_fkey')
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.alterTable('pages')
|
||||||
|
.dropConstraint('pages_deletedById_fkey')
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.alterTable('pages')
|
||||||
|
.dropConstraint('pages_spaceId_fkey')
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.alterTable('pages')
|
||||||
|
.dropConstraint('pages_workspaceId_fkey')
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.alterTable('pages')
|
||||||
|
.dropConstraint('pages_parentPageId_fkey')
|
||||||
|
.execute();
|
||||||
|
|
||||||
await db.schema.dropTable('pages').execute();
|
await db.schema.dropTable('pages').execute();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,94 +0,0 @@
|
|||||||
import { Kysely } from 'kysely';
|
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
|
||||||
await db.schema
|
|
||||||
.alterTable('pages')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_pages_users_creatorId',
|
|
||||||
['creatorId'],
|
|
||||||
'users',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('pages')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_pages_users_lastUpdatedById',
|
|
||||||
['lastUpdatedById'],
|
|
||||||
'users',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('pages')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_pages_users_deletedById',
|
|
||||||
['deletedById'],
|
|
||||||
'users',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('pages')
|
|
||||||
.addForeignKeyConstraint('FK_pages_spaces_spaceId', ['spaceId'], 'spaces', [
|
|
||||||
'id',
|
|
||||||
])
|
|
||||||
.onDelete('cascade')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('pages')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_pages_workspaces_workspaceId',
|
|
||||||
['workspaceId'],
|
|
||||||
'workspaces',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.onDelete('cascade')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('pages')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_pages_pages_parentPageId',
|
|
||||||
['parentPageId'],
|
|
||||||
'pages',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
|
||||||
await db.schema
|
|
||||||
.alterTable('pages')
|
|
||||||
.dropConstraint('FK_pages_users_creatorId')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('pages')
|
|
||||||
.dropConstraint('FK_pages_users_lastUpdatedById')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('pages')
|
|
||||||
.dropConstraint('FK_pages_users_deletedById')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('pages')
|
|
||||||
.dropConstraint('FK_pages_spaces_spaceId')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('pages')
|
|
||||||
.dropConstraint('FK_pages_workspaces_workspaceId')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('pages')
|
|
||||||
.dropConstraint('FK_pages_pages_parentPageId')
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
@ -6,16 +6,22 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
.addColumn('id', 'uuid', (col) =>
|
.addColumn('id', 'uuid', (col) =>
|
||||||
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||||
)
|
)
|
||||||
.addColumn('pageId', 'uuid', (col) => col.notNull())
|
.addColumn('pageId', 'uuid', (col) =>
|
||||||
|
col.references('pages.id').onDelete('cascade').notNull(),
|
||||||
|
)
|
||||||
.addColumn('title', 'varchar', (col) => col)
|
.addColumn('title', 'varchar', (col) => col)
|
||||||
.addColumn('content', 'jsonb', (col) => col)
|
.addColumn('content', 'jsonb', (col) => col)
|
||||||
.addColumn('slug', 'varchar', (col) => col)
|
.addColumn('slug', 'varchar', (col) => col)
|
||||||
.addColumn('icon', 'varchar', (col) => col)
|
.addColumn('icon', 'varchar', (col) => col)
|
||||||
.addColumn('coverPhoto', 'varchar', (col) => col)
|
.addColumn('coverPhoto', 'varchar', (col) => col)
|
||||||
.addColumn('version', 'int4', (col) => col.notNull())
|
.addColumn('version', 'int4', (col) => col.notNull())
|
||||||
.addColumn('lastUpdatedById', 'uuid', (col) => col.notNull())
|
.addColumn('lastUpdatedById', 'uuid', (col) => col.references('users.id'))
|
||||||
.addColumn('spaceId', 'uuid', (col) => col.notNull())
|
.addColumn('spaceId', 'uuid', (col) =>
|
||||||
.addColumn('workspaceId', 'uuid', (col) => col.notNull())
|
col.references('spaces.id').onDelete('cascade').notNull(),
|
||||||
|
)
|
||||||
|
.addColumn('workspaceId', 'uuid', (col) =>
|
||||||
|
col.references('workspaces.id').onDelete('cascade').notNull(),
|
||||||
|
)
|
||||||
.addColumn('createdAt', 'timestamp', (col) =>
|
.addColumn('createdAt', 'timestamp', (col) =>
|
||||||
col.notNull().defaultTo(sql`now()`),
|
col.notNull().defaultTo(sql`now()`),
|
||||||
)
|
)
|
||||||
@ -23,72 +29,27 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
col.notNull().defaultTo(sql`now()`),
|
col.notNull().defaultTo(sql`now()`),
|
||||||
)
|
)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
// foreign key relations
|
|
||||||
await db.schema
|
|
||||||
.alterTable('page_history')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_page_history_pages_pageId',
|
|
||||||
['pageId'],
|
|
||||||
'pages',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.onDelete('cascade')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('page_history')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_page_history_users_lastUpdatedById',
|
|
||||||
['lastUpdatedById'],
|
|
||||||
'users',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('page_history')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_page_history_spaces_spaceId',
|
|
||||||
['spaceId'],
|
|
||||||
'spaces',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.onDelete('cascade')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('page_history')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_page_history_workspaces_workspaceId',
|
|
||||||
['workspaceId'],
|
|
||||||
'workspaces',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.onDelete('cascade')
|
|
||||||
.onUpdate('no action')
|
|
||||||
.execute();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('page_history')
|
.alterTable('page_history')
|
||||||
.dropConstraint('FK_page_history_pages_pageId')
|
.dropConstraint('page_history_pageId_fkey')
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('page_history')
|
.alterTable('page_history')
|
||||||
.dropConstraint('FK_page_history_users_lastUpdatedById')
|
.dropConstraint('page_history_lastUpdatedById_fkey')
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('page_history')
|
.alterTable('page_history')
|
||||||
.dropConstraint('FK_page_history_spaces_spaceId')
|
.dropConstraint('page_history_spaceId_fkey')
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('page_history')
|
.alterTable('page_history')
|
||||||
.dropConstraint('FK_page_history_workspaces_workspaceId')
|
.dropConstraint('page_history_workspaceId_fkey')
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.schema.dropTable('page_history').execute();
|
await db.schema.dropTable('page_history').execute();
|
||||||
|
|||||||
@ -9,8 +9,12 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
.addColumn('entityId', 'uuid', (col) => col.notNull())
|
.addColumn('entityId', 'uuid', (col) => col.notNull())
|
||||||
.addColumn('entityType', 'varchar', (col) => col.notNull())
|
.addColumn('entityType', 'varchar', (col) => col.notNull())
|
||||||
.addColumn('childrenIds', sql`uuid[]`, (col) => col.notNull())
|
.addColumn('childrenIds', sql`uuid[]`, (col) => col.notNull())
|
||||||
.addColumn('spaceId', 'uuid', (col) => col.notNull())
|
.addColumn('spaceId', 'uuid', (col) =>
|
||||||
.addColumn('workspaceId', 'uuid', (col) => col.notNull())
|
col.references('spaces.id').onDelete('cascade').notNull(),
|
||||||
|
)
|
||||||
|
.addColumn('workspaceId', 'uuid', (col) =>
|
||||||
|
col.references('workspaces.id').onDelete('cascade').notNull(),
|
||||||
|
)
|
||||||
.addColumn('createdAt', 'timestamp', (col) =>
|
.addColumn('createdAt', 'timestamp', (col) =>
|
||||||
col.notNull().defaultTo(sql`now()`),
|
col.notNull().defaultTo(sql`now()`),
|
||||||
)
|
)
|
||||||
@ -18,45 +22,22 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
col.notNull().defaultTo(sql`now()`),
|
col.notNull().defaultTo(sql`now()`),
|
||||||
)
|
)
|
||||||
.addColumn('deletedAt', 'timestamp', (col) => col)
|
.addColumn('deletedAt', 'timestamp', (col) => col)
|
||||||
.addUniqueConstraint('UQ_page_ordering_entityId_entityType', [
|
.addUniqueConstraint('page_ordering_entityId_entityType_unique', [
|
||||||
'entityId',
|
'entityId',
|
||||||
'entityType',
|
'entityType',
|
||||||
])
|
])
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
// foreign key relations
|
|
||||||
await db.schema
|
|
||||||
.alterTable('page_ordering')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_page_ordering_spaces_spaceId',
|
|
||||||
['spaceId'],
|
|
||||||
'spaces',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.onDelete('cascade')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('page_ordering')
|
|
||||||
.addForeignKeyConstraint(
|
|
||||||
'FK_page_ordering_workspaces_workspaceId',
|
|
||||||
['workspaceId'],
|
|
||||||
'workspaces',
|
|
||||||
['id'],
|
|
||||||
)
|
|
||||||
.onDelete('cascade')
|
|
||||||
.execute();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('page_ordering')
|
.alterTable('page_ordering')
|
||||||
.dropConstraint('FK_page_ordering_spaces_spaceId')
|
.dropConstraint('page_ordering_spaceId_fkey')
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('page_ordering')
|
.alterTable('page_ordering')
|
||||||
.dropConstraint('FK_page_ordering_workspaces_workspaceId')
|
.dropConstraint('page_ordering_workspaceId_fkey')
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.schema.dropTable('page_ordering').execute();
|
await db.schema.dropTable('page_ordering').execute();
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user