mirror of
https://github.com/docmost/docmost.git
synced 2026-08-25 15:57:11 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d7302adc6 |
@@ -7,6 +7,7 @@ import {
|
|||||||
import { CreatePageDto, ContentFormat } from '../dto/create-page.dto';
|
import { CreatePageDto, ContentFormat } from '../dto/create-page.dto';
|
||||||
import { ContentOperation, UpdatePageDto } from '../dto/update-page.dto';
|
import { ContentOperation, UpdatePageDto } from '../dto/update-page.dto';
|
||||||
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
||||||
|
import { MAX_PAGE_TREE_DEPTH } from '@docmost/db/repos/page/constants';
|
||||||
import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo';
|
import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo';
|
||||||
import { InsertablePage, Page, User } from '@docmost/db/types/entity.types';
|
import { InsertablePage, Page, User } from '@docmost/db/types/entity.types';
|
||||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||||
@@ -111,6 +112,14 @@ export class PageService {
|
|||||||
throw new NotFoundException('Parent page not found');
|
throw new NotFoundException('Parent page not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ancestorIds = await this.pageRepo.getAncestorPageIds(
|
||||||
|
parentPage.id,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
if (ancestorIds.length >= MAX_PAGE_TREE_DEPTH) {
|
||||||
|
throw new BadRequestException('Page nesting is too deep');
|
||||||
|
}
|
||||||
|
|
||||||
parentPageId = parentPage.id;
|
parentPageId = parentPage.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -839,6 +848,18 @@ export class PageService {
|
|||||||
) {
|
) {
|
||||||
throw new NotFoundException('Parent page not found');
|
throw new NotFoundException('Parent page not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ancestorIds = await this.pageRepo.getAncestorPageIds(
|
||||||
|
parentPage.id,
|
||||||
|
);
|
||||||
|
if (ancestorIds.includes(movedPage.id)) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Cannot move a page under its own descendant',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (ancestorIds.length >= MAX_PAGE_TREE_DEPTH) {
|
||||||
|
throw new BadRequestException('Page nesting is too deep');
|
||||||
|
}
|
||||||
parentPageId = parentPage.id;
|
parentPageId = parentPage.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -868,6 +889,7 @@ export class PageService {
|
|||||||
'spaceId',
|
'spaceId',
|
||||||
'deletedAt',
|
'deletedAt',
|
||||||
])
|
])
|
||||||
|
.select(sql<number>`0`.as('depth'))
|
||||||
.where('id', '=', childPageId)
|
.where('id', '=', childPageId)
|
||||||
.where('deletedAt', 'is', null)
|
.where('deletedAt', 'is', null)
|
||||||
.unionAll((exp) =>
|
.unionAll((exp) =>
|
||||||
@@ -884,12 +906,24 @@ export class PageService {
|
|||||||
'p.spaceId',
|
'p.spaceId',
|
||||||
'p.deletedAt',
|
'p.deletedAt',
|
||||||
])
|
])
|
||||||
|
.select(sql<number>`pa.depth + 1`.as('depth'))
|
||||||
.innerJoin('page_ancestors as pa', 'pa.parentPageId', 'p.id')
|
.innerJoin('page_ancestors as pa', 'pa.parentPageId', 'p.id')
|
||||||
.where('p.deletedAt', 'is', null),
|
.where('p.deletedAt', 'is', null)
|
||||||
|
.where('pa.depth', '<', MAX_PAGE_TREE_DEPTH),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.selectFrom('page_ancestors')
|
.selectFrom('page_ancestors')
|
||||||
.selectAll('page_ancestors')
|
.select([
|
||||||
|
'id',
|
||||||
|
'slugId',
|
||||||
|
'title',
|
||||||
|
'icon',
|
||||||
|
'isBase',
|
||||||
|
'position',
|
||||||
|
'parentPageId',
|
||||||
|
'spaceId',
|
||||||
|
'deletedAt',
|
||||||
|
])
|
||||||
.select((eb) =>
|
.select((eb) =>
|
||||||
eb
|
eb
|
||||||
.exists(
|
.exists(
|
||||||
@@ -1009,17 +1043,18 @@ export class PageService {
|
|||||||
.withRecursive('page_descendants', (db) =>
|
.withRecursive('page_descendants', (db) =>
|
||||||
db
|
db
|
||||||
.selectFrom('pages')
|
.selectFrom('pages')
|
||||||
.select(['id'])
|
.select(['id', sql<number>`0`.as('depth')])
|
||||||
.where('id', '=', pageId)
|
.where('id', '=', pageId)
|
||||||
.unionAll((exp) =>
|
.unionAll((exp) =>
|
||||||
exp
|
exp
|
||||||
.selectFrom('pages as p')
|
.selectFrom('pages as p')
|
||||||
.select(['p.id'])
|
.select(['p.id', sql<number>`pd.depth + 1`.as('depth')])
|
||||||
.innerJoin('page_descendants as pd', 'pd.id', 'p.parentPageId'),
|
.innerJoin('page_descendants as pd', 'pd.id', 'p.parentPageId')
|
||||||
|
.where('pd.depth', '<', MAX_PAGE_TREE_DEPTH),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.selectFrom('page_descendants')
|
.selectFrom('page_descendants')
|
||||||
.selectAll()
|
.select(['id'])
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
const pageIds = descendants.map((d) => d.id);
|
const pageIds = descendants.map((d) => d.id);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||||||
import { Interval } from '@nestjs/schedule';
|
import { Interval } from '@nestjs/schedule';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { KyselyDB } from '@docmost/db/types/kysely.types';
|
import { KyselyDB } from '@docmost/db/types/kysely.types';
|
||||||
|
import { MAX_PAGE_TREE_DEPTH } from '@docmost/db/repos/page/constants';
|
||||||
|
import { sql } from 'kysely';
|
||||||
import { InjectQueue } from '@nestjs/bullmq';
|
import { InjectQueue } from '@nestjs/bullmq';
|
||||||
import { Queue } from 'bullmq';
|
import { Queue } from 'bullmq';
|
||||||
import { QueueJob, QueueName } from '../../../integrations/queue/constants';
|
import { QueueJob, QueueName } from '../../../integrations/queue/constants';
|
||||||
@@ -76,17 +78,18 @@ export class TrashCleanupService {
|
|||||||
.withRecursive('page_descendants', (db) =>
|
.withRecursive('page_descendants', (db) =>
|
||||||
db
|
db
|
||||||
.selectFrom('pages')
|
.selectFrom('pages')
|
||||||
.select(['id'])
|
.select(['id', sql<number>`0`.as('depth')])
|
||||||
.where('id', '=', pageId)
|
.where('id', '=', pageId)
|
||||||
.unionAll((exp) =>
|
.unionAll((exp) =>
|
||||||
exp
|
exp
|
||||||
.selectFrom('pages as p')
|
.selectFrom('pages as p')
|
||||||
.select(['p.id'])
|
.select(['p.id', sql<number>`pd.depth + 1`.as('depth')])
|
||||||
.innerJoin('page_descendants as pd', 'pd.id', 'p.parentPageId'),
|
.innerJoin('page_descendants as pd', 'pd.id', 'p.parentPageId')
|
||||||
|
.where('pd.depth', '<', MAX_PAGE_TREE_DEPTH),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.selectFrom('page_descendants')
|
.selectFrom('page_descendants')
|
||||||
.selectAll()
|
.select(['id'])
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
const pageIds = descendants.map((d) => d.id);
|
const pageIds = descendants.map((d) => d.id);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
import { Node } from '@tiptap/pm/model';
|
import { Node } from '@tiptap/pm/model';
|
||||||
import { ShareRepo } from '@docmost/db/repos/share/share.repo';
|
import { ShareRepo } from '@docmost/db/repos/share/share.repo';
|
||||||
import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo';
|
import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo';
|
||||||
|
import { MAX_PAGE_TREE_DEPTH } from '@docmost/db/repos/page/constants';
|
||||||
import { updateAttachmentAttr } from './share.util';
|
import { updateAttachmentAttr } from './share.util';
|
||||||
import { Page } from '@docmost/db/types/entity.types';
|
import { Page } from '@docmost/db/types/entity.types';
|
||||||
import { validate as isValidUUID } from 'uuid';
|
import { validate as isValidUUID } from 'uuid';
|
||||||
@@ -247,6 +248,7 @@ export class ShareService {
|
|||||||
.else(false)
|
.else(false)
|
||||||
.end()
|
.end()
|
||||||
.as('found'),
|
.as('found'),
|
||||||
|
sql<number>`0`.as('depth'),
|
||||||
])
|
])
|
||||||
.where(isValidUUID(childPageId) ? 'id' : 'slugId', '=', childPageId)
|
.where(isValidUUID(childPageId) ? 'id' : 'slugId', '=', childPageId)
|
||||||
.unionAll((exp) =>
|
.unionAll((exp) =>
|
||||||
@@ -266,14 +268,16 @@ export class ShareService {
|
|||||||
.else(false)
|
.else(false)
|
||||||
.end()
|
.end()
|
||||||
.as('found'),
|
.as('found'),
|
||||||
|
sql<number>`pa.depth + 1`.as('depth'),
|
||||||
])
|
])
|
||||||
.innerJoin('page_ancestors as pa', 'pa.parentPageId', 'p.id')
|
.innerJoin('page_ancestors as pa', 'pa.parentPageId', 'p.id')
|
||||||
// Continue recursing only when the target ancestor hasn't been found on that branch.
|
// Continue recursing only when the target ancestor hasn't been found on that branch.
|
||||||
.where('pa.found', '=', false),
|
.where('pa.found', '=', false)
|
||||||
|
.where('pa.depth', '<', MAX_PAGE_TREE_DEPTH),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.selectFrom('page_ancestors')
|
.selectFrom('page_ancestors')
|
||||||
.selectAll()
|
.select(['id', 'slugId', 'title', 'parentPageId', 'spaceId'])
|
||||||
.where('found', '=', true)
|
.where('found', '=', true)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export const MAX_PAGE_TREE_DEPTH = 100;
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
CacheKey,
|
CacheKey,
|
||||||
PERMISSION_CACHE_TTL_MS,
|
PERMISSION_CACHE_TTL_MS,
|
||||||
} from '../../../common/helpers/cache-keys';
|
} from '../../../common/helpers/cache-keys';
|
||||||
|
import { MAX_PAGE_TREE_DEPTH } from './constants';
|
||||||
|
|
||||||
export { PagePermissionMember } from './types/page-permission.types';
|
export { PagePermissionMember } from './types/page-permission.types';
|
||||||
|
|
||||||
@@ -350,7 +351,8 @@ export class PagePermissionRepo {
|
|||||||
'pages.id as ancestorId',
|
'pages.id as ancestorId',
|
||||||
'pages.parentPageId',
|
'pages.parentPageId',
|
||||||
sql<number>`ancestors.depth + 1`.as('depth'),
|
sql<number>`ancestors.depth + 1`.as('depth'),
|
||||||
]),
|
])
|
||||||
|
.where('ancestors.depth', '<', MAX_PAGE_TREE_DEPTH),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.selectFrom('ancestors')
|
.selectFrom('ancestors')
|
||||||
@@ -405,6 +407,7 @@ export class PagePermissionRepo {
|
|||||||
SELECT p.id, p.parent_page_id, a.depth + 1
|
SELECT p.id, p.parent_page_id, a.depth + 1
|
||||||
FROM pages p
|
FROM pages p
|
||||||
JOIN ancestors a ON a.parent_page_id = p.id
|
JOIN ancestors a ON a.parent_page_id = p.id
|
||||||
|
WHERE a.depth < ${MAX_PAGE_TREE_DEPTH}
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
bool_and(pp.id IS NOT NULL) AS "canAccess",
|
bool_and(pp.id IS NOT NULL) AS "canAccess",
|
||||||
@@ -471,7 +474,8 @@ export class PagePermissionRepo {
|
|||||||
'pages.id as ancestorId',
|
'pages.id as ancestorId',
|
||||||
'pages.parentPageId',
|
'pages.parentPageId',
|
||||||
sql<number>`ancestors.depth + 1`.as('depth'),
|
sql<number>`ancestors.depth + 1`.as('depth'),
|
||||||
]),
|
])
|
||||||
|
.where('ancestors.depth', '<', MAX_PAGE_TREE_DEPTH),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.selectFrom('pages')
|
.selectFrom('pages')
|
||||||
@@ -676,6 +680,7 @@ export class PagePermissionRepo {
|
|||||||
'pages.id as pageId',
|
'pages.id as pageId',
|
||||||
'pages.id as ancestorId',
|
'pages.id as ancestorId',
|
||||||
'pages.parentPageId',
|
'pages.parentPageId',
|
||||||
|
sql<number>`0`.as('depth'),
|
||||||
])
|
])
|
||||||
.where(sql<SqlBool>`pages.id = ANY(${pageIds}::uuid[])`)
|
.where(sql<SqlBool>`pages.id = ANY(${pageIds}::uuid[])`)
|
||||||
.unionAll((eb) =>
|
.unionAll((eb) =>
|
||||||
@@ -690,7 +695,9 @@ export class PagePermissionRepo {
|
|||||||
'allAncestors.pageId',
|
'allAncestors.pageId',
|
||||||
'pages.id as ancestorId',
|
'pages.id as ancestorId',
|
||||||
'pages.parentPageId',
|
'pages.parentPageId',
|
||||||
]),
|
sql<number>`all_ancestors.depth + 1`.as('depth'),
|
||||||
|
])
|
||||||
|
.where('allAncestors.depth', '<', MAX_PAGE_TREE_DEPTH),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.selectFrom('pages')
|
.selectFrom('pages')
|
||||||
@@ -760,7 +767,8 @@ export class PagePermissionRepo {
|
|||||||
'pages.id as ancestorId',
|
'pages.id as ancestorId',
|
||||||
'pages.parentPageId',
|
'pages.parentPageId',
|
||||||
sql<number>`all_ancestors.depth + 1`.as('depth'),
|
sql<number>`all_ancestors.depth + 1`.as('depth'),
|
||||||
]),
|
])
|
||||||
|
.where('allAncestors.depth', '<', MAX_PAGE_TREE_DEPTH),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.selectFrom('pages')
|
.selectFrom('pages')
|
||||||
@@ -865,13 +873,22 @@ export class PagePermissionRepo {
|
|||||||
.withRecursive('ancestors', (qb) =>
|
.withRecursive('ancestors', (qb) =>
|
||||||
qb
|
qb
|
||||||
.selectFrom('pages')
|
.selectFrom('pages')
|
||||||
.select(['pages.id as ancestorId', 'pages.parentPageId'])
|
.select([
|
||||||
|
'pages.id as ancestorId',
|
||||||
|
'pages.parentPageId',
|
||||||
|
sql<number>`0`.as('depth'),
|
||||||
|
])
|
||||||
.where('pages.id', '=', pageId)
|
.where('pages.id', '=', pageId)
|
||||||
.unionAll((eb) =>
|
.unionAll((eb) =>
|
||||||
eb
|
eb
|
||||||
.selectFrom('pages')
|
.selectFrom('pages')
|
||||||
.innerJoin('ancestors', 'ancestors.parentPageId', 'pages.id')
|
.innerJoin('ancestors', 'ancestors.parentPageId', 'pages.id')
|
||||||
.select(['pages.id as ancestorId', 'pages.parentPageId']),
|
.select([
|
||||||
|
'pages.id as ancestorId',
|
||||||
|
'pages.parentPageId',
|
||||||
|
sql<number>`ancestors.depth + 1`.as('depth'),
|
||||||
|
])
|
||||||
|
.where('ancestors.depth', '<', MAX_PAGE_TREE_DEPTH),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.selectFrom('ancestors')
|
.selectFrom('ancestors')
|
||||||
@@ -921,6 +938,7 @@ export class PagePermissionRepo {
|
|||||||
'child.id as childId',
|
'child.id as childId',
|
||||||
'child.id as ancestorId',
|
'child.id as ancestorId',
|
||||||
'child.parentPageId as ancestorParentId',
|
'child.parentPageId as ancestorParentId',
|
||||||
|
sql<number>`0`.as('depth'),
|
||||||
])
|
])
|
||||||
.where('child.parentPageId', 'in', parentIds)
|
.where('child.parentPageId', 'in', parentIds)
|
||||||
.where('child.deletedAt', 'is', null)
|
.where('child.deletedAt', 'is', null)
|
||||||
@@ -936,7 +954,9 @@ export class PagePermissionRepo {
|
|||||||
'childAncestors.childId',
|
'childAncestors.childId',
|
||||||
'pages.id as ancestorId',
|
'pages.id as ancestorId',
|
||||||
'pages.parentPageId as ancestorParentId',
|
'pages.parentPageId as ancestorParentId',
|
||||||
]),
|
sql<number>`child_ancestors.depth + 1`.as('depth'),
|
||||||
|
])
|
||||||
|
.where('childAncestors.depth', '<', MAX_PAGE_TREE_DEPTH),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.selectFrom('pages as child')
|
.selectFrom('pages as child')
|
||||||
@@ -988,7 +1008,11 @@ export class PagePermissionRepo {
|
|||||||
.withRecursive('descendants', (qb) =>
|
.withRecursive('descendants', (qb) =>
|
||||||
qb
|
qb
|
||||||
.selectFrom('pages')
|
.selectFrom('pages')
|
||||||
.select(['pages.id as descendantId', 'pages.parentPageId'])
|
.select([
|
||||||
|
'pages.id as descendantId',
|
||||||
|
'pages.parentPageId',
|
||||||
|
sql<number>`0`.as('depth'),
|
||||||
|
])
|
||||||
.where('pages.id', '=', rootPageId)
|
.where('pages.id', '=', rootPageId)
|
||||||
.unionAll((eb) =>
|
.unionAll((eb) =>
|
||||||
eb
|
eb
|
||||||
@@ -998,8 +1022,13 @@ export class PagePermissionRepo {
|
|||||||
'descendants.descendantId',
|
'descendants.descendantId',
|
||||||
'pages.parentPageId',
|
'pages.parentPageId',
|
||||||
)
|
)
|
||||||
.select(['pages.id as descendantId', 'pages.parentPageId'])
|
.select([
|
||||||
.where('pages.deletedAt', 'is', null),
|
'pages.id as descendantId',
|
||||||
|
'pages.parentPageId',
|
||||||
|
sql<number>`descendants.depth + 1`.as('depth'),
|
||||||
|
])
|
||||||
|
.where('pages.deletedAt', 'is', null)
|
||||||
|
.where('descendants.depth', '<', MAX_PAGE_TREE_DEPTH),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.withRecursive('descendantAncestors', (qb) =>
|
.withRecursive('descendantAncestors', (qb) =>
|
||||||
@@ -1010,6 +1039,7 @@ export class PagePermissionRepo {
|
|||||||
'descendants.descendantId',
|
'descendants.descendantId',
|
||||||
'pages.id as ancestorId',
|
'pages.id as ancestorId',
|
||||||
'pages.parentPageId as ancestorParentId',
|
'pages.parentPageId as ancestorParentId',
|
||||||
|
sql<number>`0`.as('depth'),
|
||||||
])
|
])
|
||||||
.unionAll((eb) =>
|
.unionAll((eb) =>
|
||||||
eb
|
eb
|
||||||
@@ -1023,7 +1053,9 @@ export class PagePermissionRepo {
|
|||||||
'descendantAncestors.descendantId',
|
'descendantAncestors.descendantId',
|
||||||
'pages.id as ancestorId',
|
'pages.id as ancestorId',
|
||||||
'pages.parentPageId as ancestorParentId',
|
'pages.parentPageId as ancestorParentId',
|
||||||
]),
|
sql<number>`descendant_ancestors.depth + 1`.as('depth'),
|
||||||
|
])
|
||||||
|
.where('descendantAncestors.depth', '<', MAX_PAGE_TREE_DEPTH),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.selectFrom('descendantAncestors')
|
.selectFrom('descendantAncestors')
|
||||||
@@ -1052,13 +1084,14 @@ export class PagePermissionRepo {
|
|||||||
|
|
||||||
const results = await sql<{ userId: string }>`
|
const results = await sql<{ userId: string }>`
|
||||||
WITH RECURSIVE ancestors AS (
|
WITH RECURSIVE ancestors AS (
|
||||||
SELECT id AS ancestor_id, parent_page_id
|
SELECT id AS ancestor_id, parent_page_id, 0 AS depth
|
||||||
FROM pages
|
FROM pages
|
||||||
WHERE id = ${pageId}::uuid
|
WHERE id = ${pageId}::uuid
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT p.id, p.parent_page_id
|
SELECT p.id, p.parent_page_id, a.depth + 1
|
||||||
FROM pages p
|
FROM pages p
|
||||||
JOIN ancestors a ON a.parent_page_id = p.id
|
JOIN ancestors a ON a.parent_page_id = p.id
|
||||||
|
WHERE a.depth < ${MAX_PAGE_TREE_DEPTH}
|
||||||
)
|
)
|
||||||
SELECT cu.user_id AS "userId"
|
SELECT cu.user_id AS "userId"
|
||||||
FROM unnest(${userIds}::uuid[]) AS cu(user_id)
|
FROM unnest(${userIds}::uuid[]) AS cu(user_id)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
|||||||
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
import { EventName } from '../../../common/events/event.contants';
|
import { EventName } from '../../../common/events/event.contants';
|
||||||
|
import { MAX_PAGE_TREE_DEPTH } from './constants';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PageRepo {
|
export class PageRepo {
|
||||||
@@ -203,19 +204,20 @@ export class PageRepo {
|
|||||||
.withRecursive('page_descendants', (db) =>
|
.withRecursive('page_descendants', (db) =>
|
||||||
db
|
db
|
||||||
.selectFrom('pages')
|
.selectFrom('pages')
|
||||||
.select(['id'])
|
.select(['id', sql<number>`0`.as('depth')])
|
||||||
.where('id', '=', pageId)
|
.where('id', '=', pageId)
|
||||||
.where('deletedAt', 'is', null)
|
.where('deletedAt', 'is', null)
|
||||||
.unionAll((exp) =>
|
.unionAll((exp) =>
|
||||||
exp
|
exp
|
||||||
.selectFrom('pages as p')
|
.selectFrom('pages as p')
|
||||||
.select(['p.id'])
|
.select(['p.id', sql<number>`pd.depth + 1`.as('depth')])
|
||||||
.innerJoin('page_descendants as pd', 'pd.id', 'p.parentPageId')
|
.innerJoin('page_descendants as pd', 'pd.id', 'p.parentPageId')
|
||||||
.where('p.deletedAt', 'is', null),
|
.where('p.deletedAt', 'is', null)
|
||||||
|
.where('pd.depth', '<', MAX_PAGE_TREE_DEPTH),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.selectFrom('page_descendants')
|
.selectFrom('page_descendants')
|
||||||
.selectAll()
|
.select(['id'])
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
const pageIds = descendants.map((d) => d.id);
|
const pageIds = descendants.map((d) => d.id);
|
||||||
@@ -272,17 +274,18 @@ export class PageRepo {
|
|||||||
.withRecursive('page_descendants', (db) =>
|
.withRecursive('page_descendants', (db) =>
|
||||||
db
|
db
|
||||||
.selectFrom('pages')
|
.selectFrom('pages')
|
||||||
.select(['id'])
|
.select(['id', sql<number>`0`.as('depth')])
|
||||||
.where('id', '=', pageId)
|
.where('id', '=', pageId)
|
||||||
.unionAll((exp) =>
|
.unionAll((exp) =>
|
||||||
exp
|
exp
|
||||||
.selectFrom('pages as p')
|
.selectFrom('pages as p')
|
||||||
.select(['p.id'])
|
.select(['p.id', sql<number>`pd.depth + 1`.as('depth')])
|
||||||
.innerJoin('page_descendants as pd', 'pd.id', 'p.parentPageId'),
|
.innerJoin('page_descendants as pd', 'pd.id', 'p.parentPageId')
|
||||||
|
.where('pd.depth', '<', MAX_PAGE_TREE_DEPTH),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.selectFrom('page_descendants')
|
.selectFrom('page_descendants')
|
||||||
.selectAll()
|
.select(['id'])
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
const pageIds = pages.map((p) => p.id);
|
const pageIds = pages.map((p) => p.id);
|
||||||
@@ -487,6 +490,35 @@ export class PageRepo {
|
|||||||
.as('hasChildren');
|
.as('hasChildren');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAncestorPageIds(
|
||||||
|
pageId: string,
|
||||||
|
trx?: KyselyTransaction,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const ancestors = await dbOrTx(this.db, trx)
|
||||||
|
.withRecursive('page_ancestors', (db) =>
|
||||||
|
db
|
||||||
|
.selectFrom('pages')
|
||||||
|
.select(['id', 'parentPageId', sql<number>`0`.as('depth')])
|
||||||
|
.where('id', '=', pageId)
|
||||||
|
.unionAll((exp) =>
|
||||||
|
exp
|
||||||
|
.selectFrom('pages as p')
|
||||||
|
.select([
|
||||||
|
'p.id',
|
||||||
|
'p.parentPageId',
|
||||||
|
sql<number>`pa.depth + 1`.as('depth'),
|
||||||
|
])
|
||||||
|
.innerJoin('page_ancestors as pa', 'pa.parentPageId', 'p.id')
|
||||||
|
.where('pa.depth', '<', MAX_PAGE_TREE_DEPTH),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.selectFrom('page_ancestors')
|
||||||
|
.select(['id'])
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
return ancestors.map((ancestor) => ancestor.id);
|
||||||
|
}
|
||||||
|
|
||||||
async getPageAndDescendants(
|
async getPageAndDescendants(
|
||||||
parentPageId: string,
|
parentPageId: string,
|
||||||
opts: { includeContent: boolean },
|
opts: { includeContent: boolean },
|
||||||
@@ -507,6 +539,7 @@ export class PageRepo {
|
|||||||
'createdAt',
|
'createdAt',
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
])
|
])
|
||||||
|
.select(sql<number>`0`.as('depth'))
|
||||||
.$if(opts?.includeContent, (qb) => qb.select('content'))
|
.$if(opts?.includeContent, (qb) => qb.select('content'))
|
||||||
.where('id', '=', parentPageId)
|
.where('id', '=', parentPageId)
|
||||||
.where('deletedAt', 'is', null)
|
.where('deletedAt', 'is', null)
|
||||||
@@ -525,13 +558,27 @@ export class PageRepo {
|
|||||||
'p.createdAt',
|
'p.createdAt',
|
||||||
'p.updatedAt',
|
'p.updatedAt',
|
||||||
])
|
])
|
||||||
|
.select(sql<number>`ph.depth + 1`.as('depth'))
|
||||||
.$if(opts?.includeContent, (qb) => qb.select('p.content'))
|
.$if(opts?.includeContent, (qb) => qb.select('p.content'))
|
||||||
.innerJoin('page_hierarchy as ph', 'p.parentPageId', 'ph.id')
|
.innerJoin('page_hierarchy as ph', 'p.parentPageId', 'ph.id')
|
||||||
.where('p.deletedAt', 'is', null),
|
.where('p.deletedAt', 'is', null)
|
||||||
|
.where('ph.depth', '<', MAX_PAGE_TREE_DEPTH),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.selectFrom('page_hierarchy')
|
.selectFrom('page_hierarchy')
|
||||||
.selectAll()
|
.select([
|
||||||
|
'id',
|
||||||
|
'slugId',
|
||||||
|
'title',
|
||||||
|
'icon',
|
||||||
|
'position',
|
||||||
|
'parentPageId',
|
||||||
|
'spaceId',
|
||||||
|
'workspaceId',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
])
|
||||||
|
.$if(opts?.includeContent, (qb) => qb.select('content'))
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,6 +610,7 @@ export class PageRepo {
|
|||||||
'pages.workspaceId',
|
'pages.workspaceId',
|
||||||
sql<boolean>`page_access.id IS NOT NULL`.as('isRestricted'),
|
sql<boolean>`page_access.id IS NOT NULL`.as('isRestricted'),
|
||||||
])
|
])
|
||||||
|
.select(sql<number>`0`.as('depth'))
|
||||||
.$if(opts?.includeContent, (qb) => qb.select('pages.content'))
|
.$if(opts?.includeContent, (qb) => qb.select('pages.content'))
|
||||||
.where('pages.id', '=', parentPageId)
|
.where('pages.id', '=', parentPageId)
|
||||||
.where('pages.deletedAt', 'is', null)
|
.where('pages.deletedAt', 'is', null)
|
||||||
@@ -582,10 +630,12 @@ export class PageRepo {
|
|||||||
'p.workspaceId',
|
'p.workspaceId',
|
||||||
sql<boolean>`page_access.id IS NOT NULL`.as('isRestricted'),
|
sql<boolean>`page_access.id IS NOT NULL`.as('isRestricted'),
|
||||||
])
|
])
|
||||||
|
.select(sql<number>`ph.depth + 1`.as('depth'))
|
||||||
.$if(opts?.includeContent, (qb) => qb.select('p.content'))
|
.$if(opts?.includeContent, (qb) => qb.select('p.content'))
|
||||||
.where('p.deletedAt', 'is', null)
|
.where('p.deletedAt', 'is', null)
|
||||||
// Only recurse into children of non-restricted pages
|
// Only recurse into children of non-restricted pages
|
||||||
.where('ph.isRestricted', '=', false),
|
.where('ph.isRestricted', '=', false)
|
||||||
|
.where('ph.depth', '<', MAX_PAGE_TREE_DEPTH),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.selectFrom('page_hierarchy')
|
.selectFrom('page_hierarchy')
|
||||||
|
|||||||
+1
-1
Submodule apps/server/src/ee updated: 6dfbcb9241...e13af0ce05
@@ -97,15 +97,6 @@ export function xwikiFormatter($: CheerioAPI, $root: Cheerio<any>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isBareLink($el: Cheerio<any>): boolean {
|
|
||||||
const href = $el.attr("href")?.trim();
|
|
||||||
const text = $el.text().trim();
|
|
||||||
|
|
||||||
if(!text || !href) return false
|
|
||||||
|
|
||||||
return text === href;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function defaultHtmlFormatter($: CheerioAPI, $root: Cheerio<any>) {
|
export function defaultHtmlFormatter($: CheerioAPI, $root: Cheerio<any>) {
|
||||||
normalizeTableColumnWidths($, $root);
|
normalizeTableColumnWidths($, $root);
|
||||||
|
|
||||||
@@ -113,9 +104,7 @@ export function defaultHtmlFormatter($: CheerioAPI, $root: Cheerio<any>) {
|
|||||||
const $el = $(el);
|
const $el = $(el);
|
||||||
const url = $el.attr('href')!;
|
const url = $el.attr('href')!;
|
||||||
const { provider } = getEmbedUrlAndProvider(url);
|
const { provider } = getEmbedUrlAndProvider(url);
|
||||||
if (provider === 'iframe' || !isBareLink($el)) {
|
if (provider === 'iframe') return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const embed = `<div data-type=\"embed\" data-src=\"${url}\" data-provider=\"${provider}\" data-align=\"center\" data-width=\"640\" data-height=\"480\"></div>`;
|
const embed = `<div data-type=\"embed\" data-src=\"${url}\" data-provider=\"${provider}\" data-align=\"center\" data-width=\"640\" data-height=\"480\"></div>`;
|
||||||
$el.replaceWith(embed);
|
$el.replaceWith(embed);
|
||||||
|
|||||||
Reference in New Issue
Block a user