chore(sync): sync updates from docmost/docmost
This commit is contained in:
commit
b48cb476b4
23 changed files with 327 additions and 76 deletions
|
|
@ -1,7 +1,7 @@
|
|||
FROM node:26-slim AS base
|
||||
LABEL org.opencontainers.image.source="https://github.com/docmost/docmost"
|
||||
|
||||
RUN npm install -g pnpm@11.25.0
|
||||
RUN npm install -g pnpm@11.28.2
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
|
|
|
|||
|
|
@ -471,7 +471,9 @@ export const collabExtensions: CollabExtensions = (provider, user) => [
|
|||
CollaborationCaret.configure({
|
||||
provider,
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
avatarUrl: user.avatarUrl,
|
||||
color: randomElement(userColors),
|
||||
},
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@
|
|||
"nestjs-cls": "6.2.0",
|
||||
"nestjs-kysely": "3.1.2",
|
||||
"nestjs-pino": "4.6.1",
|
||||
"nodemailer": "9.1.1",
|
||||
"nodemailer": "10.0.10",
|
||||
"openid-client": "6.8.2",
|
||||
"otpauth": "9.5.0",
|
||||
"p-limit": "7.3.0",
|
||||
|
|
@ -120,7 +120,7 @@
|
|||
"tlds": "1.261.0",
|
||||
"tmp-promise": "3.0.3",
|
||||
"typesense": "3.0.5",
|
||||
"undici": "7.29.0",
|
||||
"undici": "7.29.1",
|
||||
"ws": "8.21.3",
|
||||
"yauzl": "3.4.0",
|
||||
"zod": "4.3.6"
|
||||
|
|
@ -136,7 +136,6 @@
|
|||
"@types/jest": "^30.0.0",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/nodemailer": "^7.0.11",
|
||||
"@types/passport-google-oauth20": "^2.0.17",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/supertest": "^6.0.3",
|
||||
|
|
|
|||
|
|
@ -146,8 +146,14 @@ export class CollaborationGateway {
|
|||
eventName: TName,
|
||||
documentName: string,
|
||||
payload: Parameters<CollabEventHandlers[TName]>[1],
|
||||
onlyIfOpen = false,
|
||||
) {
|
||||
return this.redisSync?.handleEvent(eventName, documentName, payload);
|
||||
return this.redisSync?.handleEvent(
|
||||
eventName,
|
||||
documentName,
|
||||
payload,
|
||||
onlyIfOpen,
|
||||
);
|
||||
}
|
||||
|
||||
openDirectConnection(documentName: string, context?: any) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,17 @@ export class CollaborationHandler {
|
|||
|
||||
getHandlers(hocuspocus: Hocuspocus) {
|
||||
return {
|
||||
getConnectedUserIds: async (documentName: string) => {
|
||||
const document = hocuspocus.documents.get(documentName);
|
||||
if (!document) return [];
|
||||
|
||||
const userIds = new Set<string>();
|
||||
for (const state of document.awareness.getStates().values()) {
|
||||
const userId = state?.user?.id;
|
||||
if (typeof userId === 'string') userIds.add(userId);
|
||||
}
|
||||
return [...userIds];
|
||||
},
|
||||
alterState: async (documentName: string, payload: { pageId: string }) => {
|
||||
// dummy
|
||||
// this.logger.log('Processing', documentName, payload);
|
||||
|
|
|
|||
|
|
@ -247,3 +247,7 @@ export function jsonToMarkdown(tiptapJson: any): string {
|
|||
const html = jsonToHtml(tiptapJson);
|
||||
return htmlToMarkdown(html);
|
||||
}
|
||||
|
||||
export function isRenderableObject(value: unknown): boolean {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
afterUnloadDocumentPayload,
|
||||
beforeHandleAwarenessPayload,
|
||||
Extension,
|
||||
onChangePayload,
|
||||
onLoadDocumentPayload,
|
||||
|
|
@ -8,7 +9,12 @@ import {
|
|||
import * as Y from 'yjs';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { TiptapTransformer } from '@hocuspocus/transformer';
|
||||
import { getPageId, jsonToText, tiptapExtensions } from '../collaboration.util';
|
||||
import {
|
||||
getPageId,
|
||||
isRenderableObject,
|
||||
jsonToText,
|
||||
tiptapExtensions,
|
||||
} from '../collaboration.util';
|
||||
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { KyselyDB } from '@docmost/db/types/kysely.types';
|
||||
|
|
@ -132,6 +138,8 @@ export class PersistenceExtension implements Extension {
|
|||
return;
|
||||
}
|
||||
|
||||
await this.collabHistory.addContributors(pageId, editingUserIds);
|
||||
|
||||
let contributorIds = undefined;
|
||||
try {
|
||||
const existingContributors = page.contributorIds || [];
|
||||
|
|
@ -162,6 +170,10 @@ export class PersistenceExtension implements Extension {
|
|||
});
|
||||
} catch (err) {
|
||||
this.logger.error(`Failed to update page ${pageId}`, err);
|
||||
page = null;
|
||||
editingUserIds.forEach((userId) =>
|
||||
this.trackContributor(documentName, userId),
|
||||
);
|
||||
}
|
||||
|
||||
if (page) {
|
||||
|
|
@ -184,8 +196,6 @@ export class PersistenceExtension implements Extension {
|
|||
}
|
||||
|
||||
if (page) {
|
||||
await this.collabHistory.addContributors(pageId, editingUserIds);
|
||||
|
||||
const mentions = extractMentions(tiptapJson);
|
||||
|
||||
const userMentions = extractUserMentions(mentions);
|
||||
|
|
@ -217,12 +227,45 @@ export class PersistenceExtension implements Extension {
|
|||
}
|
||||
}
|
||||
|
||||
// Drop malformed awareness before it is broadcast
|
||||
async beforeHandleAwareness({
|
||||
states,
|
||||
context,
|
||||
}: beforeHandleAwarenessPayload) {
|
||||
const user = context?.user;
|
||||
|
||||
for (const [clientId, state] of states) {
|
||||
if (!isRenderableObject(state)) {
|
||||
states.delete(clientId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ('user' in state && !isRenderableObject(state.user)) {
|
||||
delete state.user;
|
||||
}
|
||||
|
||||
if (state.user && user) {
|
||||
state.user.id = user.id;
|
||||
state.user.avatarUrl = user.avatarUrl ?? null;
|
||||
}
|
||||
|
||||
if (
|
||||
'cursor' in state &&
|
||||
state.cursor !== null &&
|
||||
!isRenderableObject(state.cursor)
|
||||
) {
|
||||
delete state.cursor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async onChange(data: onChangePayload) {
|
||||
const documentName = data.documentName;
|
||||
const userId = data.context?.user?.id;
|
||||
|
||||
if (!userId) return;
|
||||
this.trackContributor(data.documentName, userId);
|
||||
}
|
||||
|
||||
private trackContributor(documentName: string, userId: string) {
|
||||
if (!this.contributors.has(documentName)) {
|
||||
this.contributors.set(documentName, new Set());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -235,13 +235,11 @@ export class RedisSyncExtension<TCE extends CustomEvents> implements Extension {
|
|||
};
|
||||
|
||||
async maintainLock(documentName: string) {
|
||||
clearInterval(this.locks[documentName]);
|
||||
this.locks[documentName] = setInterval(() => {
|
||||
this.pub.set(
|
||||
this.getKey(documentName),
|
||||
this.serverId,
|
||||
'PX',
|
||||
this.lockTTL,
|
||||
);
|
||||
this.pub
|
||||
.set(this.getKey(documentName), this.serverId, 'PX', this.lockTTL)
|
||||
.catch(() => {});
|
||||
}, this.lockTTL / 2);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,14 @@ async function bootstrap() {
|
|||
|
||||
const logger = new Logger('CollabServer');
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
logger.error(`UnhandledRejection, reason: ${reason}`, promise);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
logger.error('UncaughtException:', error);
|
||||
});
|
||||
|
||||
const port = process.env.COLLAB_PORT || 3001;
|
||||
const host = process.env.HOST || '0.0.0.0';
|
||||
await app.listen(port, host, () => {
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@ import { CommentNotificationService } from './services/comment.notification';
|
|||
import { PageNotificationService } from './services/page.notification';
|
||||
import { VerificationNotificationService } from './services/verification.notification';
|
||||
import { PageUpdateEmailRateLimiter } from './services/page-update-email-rate-limiter';
|
||||
import { CollaborationModule } from '../../collaboration/collaboration.module';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
imports: [CollaborationModule],
|
||||
controllers: [NotificationController],
|
||||
providers: [
|
||||
NotificationService,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { PageUpdateDigestEmail } from '@docmost/transactional/emails/page-update
|
|||
import { PermissionGrantedEmail } from '@docmost/transactional/emails/permission-granted-email';
|
||||
import { getPageTitle } from '../../../common/helpers';
|
||||
import { QueueJob, QueueName } from '../../../integrations/queue/constants';
|
||||
import { CollaborationGateway } from '../../../collaboration/collaboration.gateway';
|
||||
|
||||
const PAGE_UPDATE_COOLDOWN_HOURS = 7;
|
||||
const DIGEST_DELAY_MS = 12 * 60 * 60 * 1000; // 12 hours
|
||||
|
|
@ -37,6 +38,7 @@ export class PageNotificationService {
|
|||
private readonly pagePermissionRepo: PagePermissionRepo,
|
||||
private readonly watcherRepo: WatcherRepo,
|
||||
private readonly rateLimiter: PageUpdateEmailRateLimiter,
|
||||
private readonly collaborationGateway: CollaborationGateway,
|
||||
@InjectQueue(QueueName.NOTIFICATION_QUEUE) private notificationQueue: Queue,
|
||||
) {}
|
||||
|
||||
|
|
@ -186,8 +188,22 @@ export class PageNotificationService {
|
|||
|
||||
if (watcherIds.length === 0) return;
|
||||
|
||||
const actorSet = new Set(actorIds);
|
||||
const candidateIds = watcherIds.filter((id) => !actorSet.has(id));
|
||||
let connectedUserIds: string[] = [];
|
||||
try {
|
||||
connectedUserIds =
|
||||
(await this.collaborationGateway.handleYjsEvent(
|
||||
'getConnectedUserIds',
|
||||
`page.${pageId}`,
|
||||
undefined,
|
||||
true,
|
||||
)) ?? [];
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
`Failed to get connected users for page ${pageId}: ${err?.['message']}`,
|
||||
);
|
||||
}
|
||||
const excludedIds = new Set([...actorIds, ...connectedUserIds]);
|
||||
const candidateIds = watcherIds.filter((id) => !excludedIds.has(id));
|
||||
if (candidateIds.length === 0) return;
|
||||
|
||||
const eligibleUsers = await this.getEligiblePageUpdateUsers(candidateIds);
|
||||
|
|
@ -370,13 +386,14 @@ export class PageNotificationService {
|
|||
const pages = spaceFilteredPages.filter((p) => accessiblePageIds.has(p.id));
|
||||
if (pages.length === 0) return;
|
||||
|
||||
const actors = actorIds.length > 0
|
||||
? await this.db
|
||||
.selectFrom('users')
|
||||
.select(['id', 'name'])
|
||||
.where('id', 'in', actorIds)
|
||||
.execute()
|
||||
: [];
|
||||
const actors =
|
||||
actorIds.length > 0
|
||||
? await this.db
|
||||
.selectFrom('users')
|
||||
.select(['id', 'name'])
|
||||
.where('id', 'in', actorIds)
|
||||
.execute()
|
||||
: [];
|
||||
|
||||
const actorMap = new Map(actors.map((a) => [a.id, a.name]));
|
||||
const pageActors = new Map<string, Set<string>>();
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ import { markdownToHtml } from '@docmost/editor-ext';
|
|||
import { WatcherService } from '../../watcher/watcher.service';
|
||||
import { sql } from 'kysely';
|
||||
import { TransclusionService } from '../transclusion/transclusion.service';
|
||||
import { LabelRepo } from '@docmost/db/repos/label/label.repo';
|
||||
|
||||
@Injectable()
|
||||
export class PageService {
|
||||
|
|
@ -74,6 +75,7 @@ export class PageService {
|
|||
private collaborationGateway: CollaborationGateway,
|
||||
private readonly watcherService: WatcherService,
|
||||
private readonly transclusionService: TransclusionService,
|
||||
private readonly labelRepo: LabelRepo,
|
||||
) {}
|
||||
|
||||
async findById(
|
||||
|
|
@ -715,7 +717,13 @@ export class PageService {
|
|||
}),
|
||||
);
|
||||
|
||||
await this.db.insertInto('pages').values(insertablePages).execute();
|
||||
await executeTx(this.db, async (trx) => {
|
||||
await trx.insertInto('pages').values(insertablePages).execute();
|
||||
await this.labelRepo.copyLabelsToPages(
|
||||
new Map([...pageMap].map(([oldId, entry]) => [oldId, entry.newPageId])),
|
||||
trx,
|
||||
);
|
||||
});
|
||||
|
||||
// Extract transclusions from every duplicated page and persist them in
|
||||
// one statement. Duplication bypasses Yjs onStoreDocument; brand-new
|
||||
|
|
|
|||
|
|
@ -6,11 +6,15 @@ import {
|
|||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
|
||||
export const SEARCH_QUERY_MAX_LENGTH = 200;
|
||||
|
||||
export class SearchDTO {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(SEARCH_QUERY_MAX_LENGTH)
|
||||
query?: string;
|
||||
|
||||
@IsOptional()
|
||||
|
|
@ -61,6 +65,7 @@ export class SearchPublicSpaceDTO extends SearchDTO {
|
|||
|
||||
export class SearchSuggestionDTO {
|
||||
@IsString()
|
||||
@MaxLength(SEARCH_QUERY_MAX_LENGTH)
|
||||
query: string;
|
||||
|
||||
@IsOptional()
|
||||
|
|
|
|||
|
|
@ -55,11 +55,6 @@ export class SearchService {
|
|||
: sql<number>`ts_rank(tsv, to_tsquery('english', f_unaccent(${searchQuery})))`.as(
|
||||
'rank',
|
||||
);
|
||||
const highlightColumn = browseByFilters || titleOnly
|
||||
? sql<string>`''`.as('highlight')
|
||||
: sql<string>`ts_headline('english', text_content, to_tsquery('english', f_unaccent(${searchQuery})),'MinWords=9, MaxWords=10, MaxFragments=3')`.as(
|
||||
'highlight',
|
||||
);
|
||||
|
||||
let queryResults = this.db
|
||||
.selectFrom('pages')
|
||||
|
|
@ -73,7 +68,6 @@ export class SearchService {
|
|||
'createdAt',
|
||||
'updatedAt',
|
||||
rankColumn,
|
||||
highlightColumn,
|
||||
])
|
||||
.$if(!browseByFilters && !titleOnly, (qb) =>
|
||||
qb.where(
|
||||
|
|
@ -189,10 +183,32 @@ export class SearchService {
|
|||
results = results.filter((r: any) => accessibleSet.has(r.id));
|
||||
}
|
||||
|
||||
if (!browseByFilters && !titleOnly && results.length > 0) {
|
||||
const highlights = await this.db
|
||||
.selectFrom('pages')
|
||||
.select([
|
||||
'id',
|
||||
sql<string>`ts_headline('english', substring(text_content, 1, 100000), to_tsquery('english', f_unaccent(${searchQuery})),'MinWords=9, MaxWords=10, MaxFragments=3')`.as(
|
||||
'highlight',
|
||||
),
|
||||
])
|
||||
.where(
|
||||
'id',
|
||||
'in',
|
||||
results.map((r: any) => r.id),
|
||||
)
|
||||
.execute();
|
||||
const highlightById = new Map(highlights.map((h) => [h.id, h.highlight]));
|
||||
for (const result of results) {
|
||||
result.highlight = highlightById.get(result.id) ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
const searchResults = results.map((result: SearchResponseDto) => {
|
||||
result.wholeWord = true
|
||||
if (!result.highlight) {
|
||||
result.highlight = '';
|
||||
result.matchedText = [];
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,6 +176,32 @@ export class LabelRepo {
|
|||
.execute();
|
||||
}
|
||||
|
||||
async copyLabelsToPages(
|
||||
pageIdMap: Map<string, string>,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
if (pageIdMap.size === 0) return;
|
||||
const db = dbOrTx(this.db, trx);
|
||||
|
||||
const sourceLabels = await db
|
||||
.selectFrom('pageLabels')
|
||||
.select(['pageId', 'labelId'])
|
||||
.where('pageId', 'in', [...pageIdMap.keys()])
|
||||
.execute();
|
||||
if (sourceLabels.length === 0) return;
|
||||
|
||||
await db
|
||||
.insertInto('pageLabels')
|
||||
.values(
|
||||
sourceLabels.map((row) => ({
|
||||
pageId: pageIdMap.get(row.pageId),
|
||||
labelId: row.labelId,
|
||||
})),
|
||||
)
|
||||
.onConflict((oc) => oc.doNothing())
|
||||
.execute();
|
||||
}
|
||||
|
||||
async removeLabelFromPage(
|
||||
pageId: string,
|
||||
labelId: string,
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 5e7120dcc86a344f5af09ccda0a29d5784659938
|
||||
Subproject commit 8190e2182cbbcc5e291217a13147fcd246ede3fe
|
||||
54
apps/server/src/integrations/export/html-to-markdown.spec.ts
Normal file
54
apps/server/src/integrations/export/html-to-markdown.spec.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { htmlToMarkdown } from '@docmost/editor-ext';
|
||||
|
||||
const colgroup =
|
||||
'<colgroup><col style="min-width: 25px;" /><col style="min-width: 25px;" /></colgroup>';
|
||||
|
||||
describe('htmlToMarkdown', () => {
|
||||
it('uses the table header row as the markdown header', () => {
|
||||
const markdown = htmlToMarkdown(
|
||||
`<table>${colgroup}<tbody>` +
|
||||
'<tr><th><p>Name</p></th><th><p>Role</p></th></tr>' +
|
||||
'<tr><td><p>Ada</p></td><td><p>Engineer</p></td></tr>' +
|
||||
'</tbody></table>',
|
||||
);
|
||||
|
||||
expect(markdown.trim().split('\n')).toEqual([
|
||||
'| Name | Role |',
|
||||
'| --- | --- |',
|
||||
'| Ada | Engineer |',
|
||||
]);
|
||||
});
|
||||
|
||||
it('adds an empty header when the table has no header row', () => {
|
||||
const markdown = htmlToMarkdown(
|
||||
`<table>${colgroup}<tbody>` +
|
||||
'<tr><td><p>Ada</p></td><td><p>Engineer</p></td></tr>' +
|
||||
'<tr><td><p>Alan</p></td><td><p>Mathematician</p></td></tr>' +
|
||||
'</tbody></table>',
|
||||
);
|
||||
|
||||
expect(markdown.trim().split('\n')).toEqual([
|
||||
'| | |',
|
||||
'| --- | --- |',
|
||||
'| Ada | Engineer |',
|
||||
'| Alan | Mathematician |',
|
||||
]);
|
||||
});
|
||||
|
||||
it('marks details blocks so their content is parsed as markdown', () => {
|
||||
const markdown = htmlToMarkdown(
|
||||
'<details><summary data-type="detailsSummary">Title</summary>' +
|
||||
'<div data-type="detailsContent"><p>Some <strong>bold</strong></p></div>' +
|
||||
'</details>',
|
||||
);
|
||||
|
||||
expect(markdown.trim().split('\n')).toEqual([
|
||||
'<details markdown="1">',
|
||||
'<summary>Title</summary>',
|
||||
'',
|
||||
'Some **bold**',
|
||||
'',
|
||||
'</details>',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { Reflector } from '@nestjs/core';
|
||||
import { ThrottlerStorageService } from '@nestjs/throttler';
|
||||
import { JwtType } from '../../core/auth/dto/jwt-payload';
|
||||
import { UserThrottlerGuard } from './user-throttler.guard';
|
||||
|
||||
const guard = new UserThrottlerGuard(
|
||||
[],
|
||||
new ThrottlerStorageService(),
|
||||
new Reflector(),
|
||||
);
|
||||
|
||||
function getTracker(req: Record<string, any>): Promise<string> {
|
||||
return guard['getTracker'](req);
|
||||
}
|
||||
|
||||
describe('UserThrottlerGuard.getTracker', () => {
|
||||
it('tracks a signed-in request by its user id', async () => {
|
||||
const req = {
|
||||
ip: '203.0.113.7',
|
||||
user: {
|
||||
user: { id: 'user_1' },
|
||||
workspace: { id: 'ws_1' },
|
||||
authType: JwtType.ACCESS,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(getTracker(req)).resolves.toBe('user:user_1');
|
||||
});
|
||||
|
||||
it('falls back to the client ip when nobody is signed in', async () => {
|
||||
const req = { ip: '203.0.113.7', user: null };
|
||||
|
||||
await expect(getTracker(req)).resolves.toBe('203.0.113.7');
|
||||
});
|
||||
|
||||
it('falls back to the socket address when the ip is empty', async () => {
|
||||
const req = {
|
||||
ip: '',
|
||||
user: null,
|
||||
socket: { remoteAddress: '198.51.100.4' },
|
||||
};
|
||||
|
||||
await expect(getTracker(req)).resolves.toBe('198.51.100.4');
|
||||
});
|
||||
|
||||
it('uses a constant tracker for an unidentifiable client', async () => {
|
||||
const req = { ip: '', user: null, socket: {} };
|
||||
|
||||
await expect(getTracker(req)).resolves.toBe('unknown');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,13 +1,19 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { ThrottlerGuard } from '@nestjs/throttler';
|
||||
|
||||
type AuthedRequest = { user?: { id?: string } };
|
||||
type AuthedRequest = {
|
||||
user?: { user?: { id?: string } } | null;
|
||||
socket?: { remoteAddress?: string };
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class UserThrottlerGuard extends ThrottlerGuard {
|
||||
protected async getTracker(req: AuthedRequest): Promise<string> {
|
||||
const userId = req.user?.id;
|
||||
const userId = req.user?.user?.id;
|
||||
if (userId) return `user:${userId}`;
|
||||
return super.getTracker(req as Parameters<ThrottlerGuard['getTracker']>[0]);
|
||||
const ip = await super.getTracker(
|
||||
req as Parameters<ThrottlerGuard['getTracker']>[0],
|
||||
);
|
||||
return ip || req.socket?.remoteAddress || 'unknown';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,5 +95,5 @@
|
|||
"packages/*"
|
||||
]
|
||||
},
|
||||
"packageManager": "pnpm@11.25.0"
|
||||
"packageManager": "pnpm@11.28.2"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,11 @@ export function htmlToMarkdown(html: string): string {
|
|||
footnoteRef,
|
||||
footnotesList,
|
||||
]);
|
||||
return turndownService.turndown(html).replaceAll('<br>', ' ');
|
||||
const htmlWithoutColgroups = html.replace(
|
||||
/<colgroup\b[^>]*>[\s\S]*?<\/colgroup>/gi,
|
||||
'',
|
||||
);
|
||||
return turndownService.turndown(htmlWithoutColgroups).replaceAll('<br>', ' ');
|
||||
}
|
||||
|
||||
function listParagraph(turndownService: _TurndownService) {
|
||||
|
|
@ -146,7 +150,7 @@ function preserveDetail(turndownService: _TurndownService) {
|
|||
)
|
||||
.join('');
|
||||
|
||||
return `\n<details>\n${detailSummary}\n\n${detailsContent}\n\n</details>\n`;
|
||||
return `\n<details markdown="1">\n${detailSummary}\n\n${detailsContent}\n\n</details>\n`;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ overrides:
|
|||
ws: 8.21.3
|
||||
dompurify: 3.4.13
|
||||
mermaid: 11.16.1
|
||||
undici: 7.29.0
|
||||
undici: 7.29.1
|
||||
tmp: 0.2.7
|
||||
nanoid@^3: 3.3.18
|
||||
lodash-es: 4.18.1
|
||||
|
|
@ -21,7 +21,7 @@ overrides:
|
|||
yaml@>=2.0.0 <2.8.3: 2.8.3
|
||||
brace-expansion@^5: 5.0.9
|
||||
axios: 1.18.1
|
||||
ip-address: 10.3.1
|
||||
ip-address: 10.5.1
|
||||
fast-uri: 3.1.7
|
||||
form-data@>=4.0.0 <4.0.6: 4.0.6
|
||||
nanoid@>=4.0.0 <5.1.16: 5.1.16
|
||||
|
|
@ -29,6 +29,7 @@ overrides:
|
|||
'@opentelemetry/core@>=2.0.0 <2.8.0': 2.9.0
|
||||
js-yaml@>=3.0.0 <3.15.2: 3.15.2
|
||||
js-yaml@>=4.0.0 <4.3.2: 4.3.2
|
||||
smol-toml@<1.7.1: 1.7.1
|
||||
|
||||
patchedDependencies:
|
||||
scimmy@1.3.5: 775d80f86830b2c5dd1a250c9802c10f8fc3da3c7898373de5aa0c23993d1673
|
||||
|
|
@ -694,8 +695,8 @@ importers:
|
|||
specifier: 4.6.1
|
||||
version: 4.6.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2)
|
||||
nodemailer:
|
||||
specifier: 9.1.1
|
||||
version: 9.1.1
|
||||
specifier: 10.0.10
|
||||
version: 10.0.10
|
||||
openid-client:
|
||||
specifier: 6.8.2
|
||||
version: 6.8.2
|
||||
|
|
@ -763,8 +764,8 @@ importers:
|
|||
specifier: 3.0.5
|
||||
version: 3.0.5(@babel/runtime@7.29.2)(debug@4.4.3(supports-color@7.2.0))(supports-color@10.2.2)
|
||||
undici:
|
||||
specifier: 7.29.0
|
||||
version: 7.29.0
|
||||
specifier: 7.29.1
|
||||
version: 7.29.1
|
||||
ws:
|
||||
specifier: 8.21.3
|
||||
version: 8.21.3
|
||||
|
|
@ -805,9 +806,6 @@ importers:
|
|||
'@types/node':
|
||||
specifier: ^25.5.0
|
||||
version: 25.5.0
|
||||
'@types/nodemailer':
|
||||
specifier: ^7.0.11
|
||||
version: 7.0.11
|
||||
'@types/passport-google-oauth20':
|
||||
specifier: ^2.0.17
|
||||
version: 2.0.17
|
||||
|
|
@ -4633,9 +4631,6 @@ packages:
|
|||
'@types/node@25.5.0':
|
||||
resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
|
||||
|
||||
'@types/nodemailer@7.0.11':
|
||||
resolution: {integrity: sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==}
|
||||
|
||||
'@types/oauth@0.9.6':
|
||||
resolution: {integrity: sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==}
|
||||
|
||||
|
|
@ -6906,8 +6901,8 @@ packages:
|
|||
resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==}
|
||||
engines: {node: '>=12.22.0'}
|
||||
|
||||
ip-address@10.3.1:
|
||||
resolution: {integrity: sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==}
|
||||
ip-address@10.5.1:
|
||||
resolution: {integrity: sha512-EXujUp9jyOI/chPgtqk6uy7fDq8AeCB/WlfEuPg9LN0fN9lzKAKfuDYi60SMhHwgUiEhZvVYsbGZN+RUU1INiA==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
|
|
@ -7984,9 +7979,9 @@ packages:
|
|||
resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
nodemailer@9.1.1:
|
||||
resolution: {integrity: sha512-izw9mVKFix6YSnC9eLgV6g1opl9DUlRio9ZNcq+Wu9Ujn2UwF+8Nl0B8nz22kEC+CTZCvinkxwJ0DeFbb6NwcQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
nodemailer@10.0.10:
|
||||
resolution: {integrity: sha512-He9XskOFms62SyAKkLu8CcGcYHAo+BSHSsORI8s4PJH8qZgZY4L4lDfvyN0twvmbpyG+eGrxpyBlskj9d9rJ8A==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
normalize-path@3.0.0:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
|
|
@ -9058,8 +9053,8 @@ packages:
|
|||
resolution: {integrity: sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==}
|
||||
deprecated: Unsupported
|
||||
|
||||
smol-toml@1.6.1:
|
||||
resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==}
|
||||
smol-toml@1.7.1:
|
||||
resolution: {integrity: sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
socket.io-adapter@2.5.4:
|
||||
|
|
@ -9570,8 +9565,8 @@ packages:
|
|||
undici-types@7.18.2:
|
||||
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
||||
|
||||
undici@7.29.0:
|
||||
resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==}
|
||||
undici@7.29.1:
|
||||
resolution: {integrity: sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
unicode-canonical-property-names-ecmascript@2.0.0:
|
||||
|
|
@ -10163,7 +10158,7 @@ snapshots:
|
|||
'@ai-sdk/provider': 3.0.16
|
||||
'@standard-schema/spec': 1.1.0
|
||||
eventsource-parser: 3.1.1
|
||||
undici: 7.29.0
|
||||
undici: 7.29.1
|
||||
zod: 4.3.6
|
||||
|
||||
'@ai-sdk/provider@3.0.16':
|
||||
|
|
@ -14362,7 +14357,7 @@ snapshots:
|
|||
'@turbopuffer/turbopuffer@2.8.0':
|
||||
dependencies:
|
||||
pako: 2.2.0
|
||||
undici: 7.29.0
|
||||
undici: 7.29.1
|
||||
|
||||
'@tybys/wasm-util@0.10.2':
|
||||
dependencies:
|
||||
|
|
@ -14640,10 +14635,6 @@ snapshots:
|
|||
dependencies:
|
||||
undici-types: 7.18.2
|
||||
|
||||
'@types/nodemailer@7.0.11':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
|
||||
'@types/oauth@0.9.6':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
|
|
@ -15617,7 +15608,7 @@ snapshots:
|
|||
parse5: 7.3.0
|
||||
parse5-htmlparser2-tree-adapter: 7.1.0
|
||||
parse5-parser-stream: 7.1.2
|
||||
undici: 7.29.0
|
||||
undici: 7.29.1
|
||||
whatwg-mimetype: 4.0.0
|
||||
|
||||
chevrotain-allstar@0.3.1(chevrotain@11.0.3):
|
||||
|
|
@ -16684,7 +16675,7 @@ snapshots:
|
|||
express-rate-limit@8.2.2(express@5.2.1(supports-color@10.2.2)):
|
||||
dependencies:
|
||||
express: 5.2.1(supports-color@10.2.2)
|
||||
ip-address: 10.3.1
|
||||
ip-address: 10.5.1
|
||||
|
||||
express@5.2.1(supports-color@10.2.2):
|
||||
dependencies:
|
||||
|
|
@ -17269,7 +17260,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
ip-address@10.3.1: {}
|
||||
ip-address@10.5.1: {}
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
|
||||
|
|
@ -18418,7 +18409,7 @@ snapshots:
|
|||
|
||||
node-releases@2.0.54: {}
|
||||
|
||||
nodemailer@9.1.1: {}
|
||||
nodemailer@10.0.10: {}
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
|
||||
|
|
@ -18537,7 +18528,7 @@ snapshots:
|
|||
safe-buffer: 5.2.1
|
||||
semver: 7.8.4
|
||||
signal-exit: 3.0.7
|
||||
smol-toml: 1.6.1
|
||||
smol-toml: 1.7.1
|
||||
string-width: 4.2.3
|
||||
string_decoder: 1.3.0
|
||||
strip-ansi: 6.0.1
|
||||
|
|
@ -19776,7 +19767,7 @@ snapshots:
|
|||
|
||||
sliced@1.0.1: {}
|
||||
|
||||
smol-toml@1.6.1: {}
|
||||
smol-toml@1.7.1: {}
|
||||
|
||||
socket.io-adapter@2.5.4(supports-color@10.2.2):
|
||||
dependencies:
|
||||
|
|
@ -20340,7 +20331,7 @@ snapshots:
|
|||
|
||||
undici-types@7.18.2: {}
|
||||
|
||||
undici@7.29.0: {}
|
||||
undici@7.29.1: {}
|
||||
|
||||
unicode-canonical-property-names-ecmascript@2.0.0: {}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ overrides:
|
|||
ws: 8.21.3
|
||||
dompurify: 3.4.13
|
||||
mermaid: 11.16.1
|
||||
undici: 7.29.0
|
||||
undici: 7.29.1
|
||||
tmp: 0.2.7
|
||||
nanoid@^3: 3.3.18
|
||||
lodash-es: 4.18.1
|
||||
|
|
@ -20,7 +20,7 @@ overrides:
|
|||
yaml@>=2.0.0 <2.8.3: 2.8.3
|
||||
brace-expansion@^5: 5.0.9
|
||||
axios: 1.18.1
|
||||
ip-address: 10.3.1
|
||||
ip-address: 10.5.1
|
||||
fast-uri: 3.1.7
|
||||
form-data@>=4.0.0 <4.0.6: 4.0.6
|
||||
nanoid@>=4.0.0 <5.1.16: 5.1.16
|
||||
|
|
@ -28,6 +28,7 @@ overrides:
|
|||
'@opentelemetry/core@>=2.0.0 <2.8.0': 2.9.0
|
||||
js-yaml@>=3.0.0 <3.15.2: 3.15.2
|
||||
js-yaml@>=4.0.0 <4.3.2: 4.3.2
|
||||
smol-toml@<1.7.1: 1.7.1
|
||||
shamefullyHoist: true
|
||||
minimumReleaseAge: 4320
|
||||
allowBuilds:
|
||||
|
|
|
|||
Loading…
Reference in a new issue