Merge branch 'main' into feat/integrations

This commit is contained in:
Philipinho
2026-08-13 01:33:47 +01:00
93 changed files with 2945 additions and 985 deletions
+7 -7
View File
@@ -51,19 +51,19 @@
"@nestjs-labs/nestjs-ioredis": "11.0.4",
"@nestjs/bullmq": "11.0.4",
"@nestjs/cache-manager": "3.1.3",
"@nestjs/common": "11.1.27",
"@nestjs/common": "11.1.28",
"@nestjs/config": "4.0.4",
"@nestjs/core": "11.1.27",
"@nestjs/event-emitter": "3.1.0",
"@nestjs/jwt": "11.0.2",
"@nestjs/mapped-types": "2.1.1",
"@nestjs/passport": "11.0.5",
"@nestjs/platform-fastify": "11.1.27",
"@nestjs/platform-socket.io": "11.1.27",
"@nestjs/platform-fastify": "11.1.28",
"@nestjs/platform-socket.io": "11.1.28",
"@nestjs/schedule": "6.1.3",
"@nestjs/terminus": "11.1.1",
"@nestjs/throttler": "6.5.0",
"@nestjs/websockets": "11.1.27",
"@nestjs/websockets": "11.1.28",
"@node-saml/passport-saml": "5.1.0",
"@socket.io/redis-adapter": "8.3.0",
"ai": "6.0.134",
@@ -90,8 +90,8 @@
"ldapts": "8.1.7",
"mammoth": "1.12.0",
"mime-types": "3.0.2",
"msgpackr": "^1.11.9",
"nanoid": "5.1.7",
"msgpackr": "1.11.9",
"nanoid": "5.1.16",
"nestjs-cls": "6.2.0",
"nestjs-kysely": "3.1.2",
"nestjs-pino": "4.6.1",
@@ -102,7 +102,7 @@
"passport-google-oauth20": "2.0.0",
"passport-jwt": "4.0.1",
"pg-tsquery": "8.4.2",
"pgvector": "^0.2.1",
"pgvector": "0.2.1",
"pino-http": "11.0.0",
"pino-pretty": "13.1.3",
"postgres": "3.4.8",
@@ -1,4 +1,5 @@
import { StarterKit } from '@tiptap/starter-kit';
import { Document } from '@tiptap/extension-document';
import { TextAlign } from '@tiptap/extension-text-align';
import { Superscript } from '@tiptap/extension-superscript';
import SubScript from '@tiptap/extension-subscript';
@@ -45,10 +46,18 @@ import {
TransclusionSource,
TransclusionReference,
BaseEmbed,
Footnotes,
Footnote,
FootnoteReference,
IntegrationLink,
IntegrationMention,
} from '@docmost/editor-ext';
import { generateText, getSchema, JSONContent } from '@tiptap/core';
import {
extensions as coreExtensions,
generateText,
getSchema,
JSONContent,
} from '@tiptap/core';
import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html';
// @tiptap/html library works best for generating prosemirror json state but not HTML
// see: https://github.com/ueberdosis/tiptap/issues/5352
@@ -59,12 +68,17 @@ import * as Y from 'yjs';
import { Logger } from '@nestjs/common';
export const tiptapExtensions = [
coreExtensions.TextDirection.configure({ direction: 'auto' }),
StarterKit.configure({
document: false,
codeBlock: false,
link: false,
trailingNode: false,
heading: false,
}),
Document.extend({
content: 'block+ footnotes?',
}),
Heading,
UniqueID.configure({
types: ['heading', 'paragraph', 'transclusionSource'],
@@ -113,6 +127,9 @@ export const tiptapExtensions = [
TransclusionSource,
TransclusionReference,
BaseEmbed,
Footnotes,
Footnote,
FootnoteReference,
IntegrationLink,
IntegrationMention
] as any;
@@ -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,
@@ -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([
@@ -810,6 +810,10 @@ export class PageService {
throw new BadRequestException('Invalid move position');
}
if (dto.parentPageId && dto.parentPageId === dto.pageId) {
throw new BadRequestException('A page cannot be its own parent');
}
let parentPageId = null;
if (movedPage.parentPageId === dto.parentPageId) {
parentPageId = undefined;
@@ -1,5 +1,8 @@
import { Injectable } from '@nestjs/common';
import { InjectKysely } from 'nestjs-kysely';
import { ExpressionBuilder, sql } from 'kysely';
import { jsonObjectFrom } from 'kysely/helpers/postgres';
import { DB } from '@docmost/db/types/db';
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
import { dbOrTx } from '@docmost/db/utils';
import {
@@ -8,6 +11,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 +94,41 @@ export class AttachmentRepo {
.execute();
}
async findPageAttachments(pageId: string, pagination: PaginationOptions) {
let query = this.db
.selectFrom('attachments')
.select(this.baseFields)
.select((eb) => this.withCreator(eb))
.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: 'desc' }],
parseCursor: (cursor) => ({ id: cursor.id }),
});
}
withCreator(eb: ExpressionBuilder<DB, 'attachments'>) {
return jsonObjectFrom(
eb
.selectFrom('users')
.select(['users.id', 'users.name', 'users.avatarUrl'])
.whereRef('users.id', '=', 'attachments.creatorId'),
).as('creator');
}
async findByIds(
ids: string[],
opts?: {