mirror of
https://github.com/docmost/docmost.git
synced 2026-08-23 19:12:13 +10:00
feat: page attachments endpoint
This commit is contained in:
@@ -53,8 +53,14 @@ import { EnvironmentService } from '../../integrations/environment/environment.s
|
|||||||
import { TokenService } from '../auth/services/token.service';
|
import { TokenService } from '../auth/services/token.service';
|
||||||
import { JwtAttachmentPayload, JwtType } from '../auth/dto/jwt-payload';
|
import { JwtAttachmentPayload, JwtType } from '../auth/dto/jwt-payload';
|
||||||
import * as path from 'path';
|
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 { PageAccessService } from '../page/page-access/page-access.service';
|
||||||
|
import { DomainService } from '../../integrations/environment/domain.service';
|
||||||
import { AuditEvent, AuditResource } from '../../common/events/audit-events';
|
import { AuditEvent, AuditResource } from '../../common/events/audit-events';
|
||||||
import {
|
import {
|
||||||
AUDIT_SERVICE,
|
AUDIT_SERVICE,
|
||||||
@@ -75,6 +81,7 @@ export class AttachmentController {
|
|||||||
private readonly environmentService: EnvironmentService,
|
private readonly environmentService: EnvironmentService,
|
||||||
private readonly tokenService: TokenService,
|
private readonly tokenService: TokenService,
|
||||||
private readonly pageAccessService: PageAccessService,
|
private readonly pageAccessService: PageAccessService,
|
||||||
|
private readonly domainService: DomainService,
|
||||||
@Inject(AUDIT_SERVICE) private readonly auditService: IAuditService,
|
@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) {
|
} catch (err: any) {
|
||||||
if (err?.statusCode === 413) {
|
if (err?.statusCode === 413) {
|
||||||
const errMessage = `File too large. Exceeds the ${this.environmentService.getFileUploadSizeLimit()} limit`;
|
const errMessage = `File too large. Exceeds the ${this.environmentService.getFileUploadSizeLimit()} limit`;
|
||||||
@@ -411,7 +421,37 @@ export class AttachmentController {
|
|||||||
|
|
||||||
await this.pageAccessService.validateCanView(page, user);
|
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)
|
@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(
|
private async sendFileResponse(
|
||||||
req: FastifyRequest,
|
req: FastifyRequest,
|
||||||
res: FastifyReply,
|
res: FastifyReply,
|
||||||
|
|||||||
@@ -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';
|
import { AttachmentType } from '../attachment.constants';
|
||||||
|
|
||||||
export class AttachmentInfoDto {
|
export class AttachmentInfoDto {
|
||||||
@@ -7,6 +14,12 @@ export class AttachmentInfoDto {
|
|||||||
attachmentId: string;
|
attachmentId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class PageIdDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
pageId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class RemoveIconDto {
|
export class RemoveIconDto {
|
||||||
@IsEnum(AttachmentType)
|
@IsEnum(AttachmentType)
|
||||||
@IsIn([
|
@IsIn([
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
|
import { sql } from 'kysely';
|
||||||
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||||
import { dbOrTx } from '@docmost/db/utils';
|
import { dbOrTx } from '@docmost/db/utils';
|
||||||
import {
|
import {
|
||||||
@@ -8,6 +9,8 @@ import {
|
|||||||
UpdatableAttachment,
|
UpdatableAttachment,
|
||||||
} from '@docmost/db/types/entity.types';
|
} from '@docmost/db/types/entity.types';
|
||||||
import { AttachmentType } from '../../../core/attachment/attachment.constants';
|
import { AttachmentType } from '../../../core/attachment/attachment.constants';
|
||||||
|
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||||
|
import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AttachmentRepo {
|
export class AttachmentRepo {
|
||||||
@@ -89,6 +92,31 @@ export class AttachmentRepo {
|
|||||||
.execute();
|
.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(
|
async findByIds(
|
||||||
ids: string[],
|
ids: string[],
|
||||||
opts?: {
|
opts?: {
|
||||||
|
|||||||
+1
-1
Submodule apps/server/src/ee updated: 41acc8af7d...c0f9440de9
Reference in New Issue
Block a user