From 1eaf5ce88bbe80377876dd66d033ea4ec92b8012 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:08:17 +0100 Subject: [PATCH] feat: page attachments endpoint --- .../core/attachment/attachment.controller.ts | 50 +++++++++++++++++-- .../src/core/attachment/dto/attachment.dto.ts | 15 +++++- .../repos/attachment/attachment.repo.ts | 28 +++++++++++ apps/server/src/ee | 2 +- 4 files changed, 90 insertions(+), 5 deletions(-) diff --git a/apps/server/src/core/attachment/attachment.controller.ts b/apps/server/src/core/attachment/attachment.controller.ts index 736058191..57d4124c3 100644 --- a/apps/server/src/core/attachment/attachment.controller.ts +++ b/apps/server/src/core/attachment/attachment.controller.ts @@ -53,8 +53,14 @@ import { EnvironmentService } from '../../integrations/environment/environment.s import { TokenService } from '../auth/services/token.service'; import { JwtAttachmentPayload, JwtType } from '../auth/dto/jwt-payload'; import * as path from 'path'; -import { AttachmentInfoDto, RemoveIconDto } from './dto/attachment.dto'; +import { + AttachmentInfoDto, + PageIdDto, + RemoveIconDto, +} from './dto/attachment.dto'; +import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { PageAccessService } from '../page/page-access/page-access.service'; +import { DomainService } from '../../integrations/environment/domain.service'; import { AuditEvent, AuditResource } from '../../common/events/audit-events'; import { AUDIT_SERVICE, @@ -75,6 +81,7 @@ export class AttachmentController { private readonly environmentService: EnvironmentService, private readonly tokenService: TokenService, private readonly pageAccessService: PageAccessService, + private readonly domainService: DomainService, @Inject(AUDIT_SERVICE) private readonly auditService: IAuditService, ) {} @@ -151,7 +158,10 @@ export class AttachmentController { }, }); - return res.send(fileResponse); + return res.send({ + ...fileResponse, + url: this.buildFileUrl(workspace, fileResponse), + }); } catch (err: any) { if (err?.statusCode === 413) { const errMessage = `File too large. Exceeds the ${this.environmentService.getFileUploadSizeLimit()} limit`; @@ -411,7 +421,37 @@ export class AttachmentController { await this.pageAccessService.validateCanView(page, user); - return attachment; + return { ...attachment, url: this.buildFileUrl(workspace, attachment) }; + } + + @UseGuards(JwtAuthGuard) + @HttpCode(HttpStatus.OK) + @Post('pages/attachments') + async getPageAttachments( + @Body() dto: PageIdDto, + @Body() pagination: PaginationOptions, + @AuthUser() user: User, + @AuthWorkspace() workspace: Workspace, + ) { + const page = await this.pageRepo.findById(dto.pageId); + if (!page || page.workspaceId !== workspace.id) { + throw new NotFoundException('Page not found'); + } + + await this.pageAccessService.validateCanView(page, user); + + const result = await this.attachmentRepo.findPageAttachments( + page.id, + pagination, + ); + + return { + ...result, + items: result.items.map((attachment) => ({ + ...attachment, + url: this.buildFileUrl(workspace, attachment), + })), + }; } @UseGuards(JwtAuthGuard) @@ -465,6 +505,10 @@ export class AttachmentController { } } + private buildFileUrl(workspace: Workspace, attachment: Attachment): string { + return `${this.domainService.getUrl(workspace.hostname)}/api/files/${attachment.id}/${encodeURIComponent(attachment.fileName)}`; + } + private async sendFileResponse( req: FastifyRequest, res: FastifyReply, diff --git a/apps/server/src/core/attachment/dto/attachment.dto.ts b/apps/server/src/core/attachment/dto/attachment.dto.ts index 850de6f9e..80d510944 100644 --- a/apps/server/src/core/attachment/dto/attachment.dto.ts +++ b/apps/server/src/core/attachment/dto/attachment.dto.ts @@ -1,4 +1,11 @@ -import { IsEnum, IsIn, IsNotEmpty, IsOptional, IsUUID } from 'class-validator'; +import { + IsEnum, + IsIn, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; import { AttachmentType } from '../attachment.constants'; export class AttachmentInfoDto { @@ -7,6 +14,12 @@ export class AttachmentInfoDto { attachmentId: string; } +export class PageIdDto { + @IsString() + @IsNotEmpty() + pageId: string; +} + export class RemoveIconDto { @IsEnum(AttachmentType) @IsIn([ diff --git a/apps/server/src/database/repos/attachment/attachment.repo.ts b/apps/server/src/database/repos/attachment/attachment.repo.ts index f7d717ea0..d5db1bbf6 100644 --- a/apps/server/src/database/repos/attachment/attachment.repo.ts +++ b/apps/server/src/database/repos/attachment/attachment.repo.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectKysely } from 'nestjs-kysely'; +import { sql } from 'kysely'; import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types'; import { dbOrTx } from '@docmost/db/utils'; import { @@ -8,6 +9,8 @@ import { UpdatableAttachment, } from '@docmost/db/types/entity.types'; import { AttachmentType } from '../../../core/attachment/attachment.constants'; +import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; +import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination'; @Injectable() export class AttachmentRepo { @@ -89,6 +92,31 @@ export class AttachmentRepo { .execute(); } + async findPageAttachments(pageId: string, pagination: PaginationOptions) { + let query = this.db + .selectFrom('attachments') + .select(this.baseFields) + .where('pageId', '=', pageId) + .where('type', '=', AttachmentType.File) + .where('deletedAt', 'is', null); + + if (pagination.query) { + query = query.where( + sql`f_unaccent(file_name)`, + 'ilike', + sql`f_unaccent(${'%' + pagination.query + '%'})`, + ); + } + + return executeWithCursorPagination(query, { + perPage: pagination.limit, + cursor: pagination.cursor, + beforeCursor: pagination.beforeCursor, + fields: [{ expression: 'id', direction: 'asc' }], + parseCursor: (cursor) => ({ id: cursor.id }), + }); + } + async findByIds( ids: string[], opts?: { diff --git a/apps/server/src/ee b/apps/server/src/ee index 41acc8af7..c0f9440de 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit 41acc8af7da27b6b6b3491357b22dabee9ce3695 +Subproject commit c0f9440de97b91475d924a677073b97ec1b58527