mirror of
https://github.com/docmost/docmost.git
synced 2025-11-12 17:42:36 +10:00
feat: delete space and edit space slug (#307)
* feat: make space slug editable * feat: delete space * client
This commit is contained in:
3
apps/server/src/common/events/event.contants.ts
Normal file
3
apps/server/src/common/events/event.contants.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export enum EventName {
|
||||
COLLAB_PAGE_UPDATED = 'collab.page.updated',
|
||||
}
|
||||
@ -4,10 +4,11 @@ import { AttachmentController } from './attachment.controller';
|
||||
import { StorageModule } from '../../integrations/storage/storage.module';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { WorkspaceModule } from '../workspace/workspace.module';
|
||||
import { AttachmentProcessor } from './processors/attachment.processor';
|
||||
|
||||
@Module({
|
||||
imports: [StorageModule, UserModule, WorkspaceModule],
|
||||
controllers: [AttachmentController],
|
||||
providers: [AttachmentService],
|
||||
providers: [AttachmentService, AttachmentProcessor],
|
||||
})
|
||||
export class AttachmentModule {}
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
import { Logger, OnModuleDestroy } from '@nestjs/common';
|
||||
import { OnWorkerEvent, Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Job } from 'bullmq';
|
||||
import { AttachmentService } from '../services/attachment.service';
|
||||
import { QueueJob, QueueName } from 'src/integrations/queue/constants';
|
||||
import { Space } from '@docmost/db/types/entity.types';
|
||||
|
||||
@Processor(QueueName.ATTACHEMENT_QUEUE)
|
||||
export class AttachmentProcessor extends WorkerHost implements OnModuleDestroy {
|
||||
private readonly logger = new Logger(AttachmentProcessor.name);
|
||||
constructor(private readonly attachmentService: AttachmentService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async process(job: Job<Space, void>): Promise<void> {
|
||||
try {
|
||||
if (job.name === QueueJob.DELETE_SPACE_ATTACHMENTS) {
|
||||
await this.attachmentService.handleDeleteSpaceAttachments(job.data.id);
|
||||
}
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@OnWorkerEvent('active')
|
||||
onActive(job: Job) {
|
||||
this.logger.debug(`Processing ${job.name} job`);
|
||||
}
|
||||
|
||||
@OnWorkerEvent('failed')
|
||||
onError(job: Job) {
|
||||
this.logger.error(
|
||||
`Error processing ${job.name} job. Reason: ${job.failedReason}`,
|
||||
);
|
||||
}
|
||||
|
||||
@OnWorkerEvent('completed')
|
||||
onCompleted(job: Job) {
|
||||
this.logger.debug(`Completed ${job.name} job`);
|
||||
}
|
||||
|
||||
async onModuleDestroy(): Promise<void> {
|
||||
if (this.worker) {
|
||||
await this.worker.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -256,4 +256,37 @@ export class AttachmentService {
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
async handleDeleteSpaceAttachments(spaceId: string) {
|
||||
try {
|
||||
const attachments = await this.attachmentRepo.findBySpaceId(spaceId);
|
||||
if (!attachments || attachments.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const failedDeletions = [];
|
||||
|
||||
await Promise.all(
|
||||
attachments.map(async (attachment) => {
|
||||
try {
|
||||
await this.storageService.delete(attachment.filePath);
|
||||
await this.attachmentRepo.deleteAttachmentById(attachment.id);
|
||||
} catch (err) {
|
||||
failedDeletions.push(attachment.id);
|
||||
this.logger.log(
|
||||
`DeleteSpaceAttachments: failed to delete attachment ${attachment.id}:`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if(failedDeletions.length === attachments.length){
|
||||
throw new Error(`Failed to delete any attachments for spaceId: ${spaceId}`);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,9 @@ import { executeTx } from '@docmost/db/utils';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { SpaceMemberService } from './space-member.service';
|
||||
import { SpaceRole } from '../../../common/helpers/types/permission';
|
||||
import { QueueJob, QueueName } from 'src/integrations/queue/constants';
|
||||
import { Queue } from 'bullmq';
|
||||
import { InjectQueue } from '@nestjs/bullmq';
|
||||
|
||||
@Injectable()
|
||||
export class SpaceService {
|
||||
@ -21,6 +24,7 @@ export class SpaceService {
|
||||
private spaceRepo: SpaceRepo,
|
||||
private spaceMemberService: SpaceMemberService,
|
||||
@InjectKysely() private readonly db: KyselyDB,
|
||||
@InjectQueue(QueueName.ATTACHEMENT_QUEUE) private attachmentQueue: Queue,
|
||||
) {}
|
||||
|
||||
async createSpace(
|
||||
@ -88,10 +92,24 @@ export class SpaceService {
|
||||
updateSpaceDto: UpdateSpaceDto,
|
||||
workspaceId: string,
|
||||
): Promise<Space> {
|
||||
if (updateSpaceDto?.slug) {
|
||||
const slugExists = await this.spaceRepo.slugExists(
|
||||
updateSpaceDto.slug,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (slugExists) {
|
||||
throw new BadRequestException(
|
||||
'Space slug exists. Please use a unique space slug',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return await this.spaceRepo.updateSpace(
|
||||
{
|
||||
name: updateSpaceDto.name,
|
||||
description: updateSpaceDto.description,
|
||||
slug: updateSpaceDto.slug,
|
||||
},
|
||||
updateSpaceDto.spaceId,
|
||||
workspaceId,
|
||||
@ -120,4 +138,14 @@ export class SpaceService {
|
||||
|
||||
return spaces;
|
||||
}
|
||||
|
||||
async deleteSpace(spaceId: string, workspaceId: string): Promise<void> {
|
||||
const space = await this.spaceRepo.findById(spaceId, workspaceId);
|
||||
if (!space) {
|
||||
throw new NotFoundException('Space not found');
|
||||
}
|
||||
|
||||
await this.spaceRepo.deleteSpace(spaceId, workspaceId);
|
||||
await this.attachmentQueue.add(QueueJob.DELETE_SPACE_ATTACHMENTS, space);
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,7 +95,7 @@ export class SpaceController {
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('create')
|
||||
createGroup(
|
||||
createSpace(
|
||||
@Body() createSpaceDto: CreateSpaceDto,
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@ -111,7 +111,7 @@ export class SpaceController {
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('update')
|
||||
async updateGroup(
|
||||
async updateSpace(
|
||||
@Body() updateSpaceDto: UpdateSpaceDto,
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@ -126,6 +126,23 @@ export class SpaceController {
|
||||
return this.spaceService.updateSpace(updateSpaceDto, workspace.id);
|
||||
}
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('delete')
|
||||
async deleteSpace(
|
||||
@Body() spaceIdDto: SpaceIdDto,
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const ability = await this.spaceAbility.createForUser(
|
||||
user,
|
||||
spaceIdDto.spaceId,
|
||||
);
|
||||
if (ability.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Settings)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
return this.spaceService.deleteSpace(spaceIdDto.spaceId, workspace.id);
|
||||
}
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('members')
|
||||
async getSpaceMembers(
|
||||
|
||||
@ -40,6 +40,21 @@ export class AttachmentRepo {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async findBySpaceId(
|
||||
spaceId: string,
|
||||
opts?: {
|
||||
trx?: KyselyTransaction;
|
||||
},
|
||||
): Promise<Attachment[]> {
|
||||
const db = dbOrTx(this.db, opts?.trx);
|
||||
|
||||
return db
|
||||
.selectFrom('attachments')
|
||||
.selectAll()
|
||||
.where('spaceId', '=', spaceId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async updateAttachment(
|
||||
updatableAttachment: UpdatableAttachment,
|
||||
attachmentId: string,
|
||||
@ -52,7 +67,7 @@ export class AttachmentRepo {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async deleteAttachment(attachmentId: string): Promise<void> {
|
||||
async deleteAttachmentById(attachmentId: string): Promise<void> {
|
||||
await this.db
|
||||
.deleteFrom('attachments')
|
||||
.where('id', '=', attachmentId)
|
||||
|
||||
@ -64,7 +64,7 @@ export class SpaceMemberRepo {
|
||||
} else if (opts.groupId) {
|
||||
query = query.where('groupId', '=', opts.groupId);
|
||||
} else {
|
||||
throw new BadRequestException('Please provider a userId or groupId');
|
||||
throw new BadRequestException('Please provide a userId or groupId');
|
||||
}
|
||||
return query.executeTakeFirst();
|
||||
}
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
export enum QueueName {
|
||||
EMAIL_QUEUE = '{email-queue}',
|
||||
ATTACHEMENT_QUEUE = '{attachment-queue}',
|
||||
}
|
||||
|
||||
export enum QueueJob {
|
||||
SEND_EMAIL = 'send-email',
|
||||
DELETE_SPACE_ATTACHMENTS = 'delete-space-attachments',
|
||||
DELETE_PAGE_ATTACHMENTS = 'delete-page-attachments',
|
||||
}
|
||||
|
||||
@ -31,6 +31,9 @@ import { QueueName } from './constants';
|
||||
BullModule.registerQueue({
|
||||
name: QueueName.EMAIL_QUEUE,
|
||||
}),
|
||||
BullModule.registerQueue({
|
||||
name: QueueName.ATTACHEMENT_QUEUE,
|
||||
}),
|
||||
],
|
||||
exports: [BullModule],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user