refactor(favorites): move favorite filtering into the repository query

This commit is contained in:
Philipinho
2026-09-05 14:17:38 +01:00
parent ac7935eff9
commit b11a94cd45
4 changed files with 82 additions and 34 deletions
@@ -48,9 +48,9 @@ export class FavoriteController {
await this.favoriteService.addFavorite(user.id, workspace.id, {
type: dto.type,
pageId: dto.pageId,
pageId: dto.type === 'page' ? dto.pageId : undefined,
spaceId: dto.type === 'space' ? resolved.spaceId : undefined,
templateId: dto.templateId,
templateId: dto.type === 'template' ? dto.templateId : undefined,
});
}
@@ -6,14 +6,12 @@ import {
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { InsertableFavorite } from '@docmost/db/types/entity.types';
import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo';
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
@Injectable()
export class FavoriteService {
constructor(
private readonly favoriteRepo: FavoriteRepo,
private readonly pagePermissionRepo: PagePermissionRepo,
private readonly spaceMemberRepo: SpaceMemberRepo,
) {}
async getFavoriteIds(
@@ -43,12 +41,6 @@ export class FavoriteService {
result.items = result.items.filter((id) => accessibleSet.has(id));
}
if (type === FavoriteType.SPACE) {
const userSpaceIds = await this.spaceMemberRepo.getUserSpaceIds(userId);
const spaceSet = new Set(userSpaceIds);
result.items = result.items.filter((id) => spaceSet.has(id));
}
return result;
}
@@ -111,9 +103,6 @@ export class FavoriteService {
return result;
}
const userSpaceIds = await this.spaceMemberRepo.getUserSpaceIds(userId);
const spaceSet = new Set(userSpaceIds);
const pageFavorites = result.items.filter(
(f) => f.type === FavoriteType.PAGE && f.pageId,
);
@@ -129,19 +118,11 @@ export class FavoriteService {
accessiblePageSet = new Set(accessibleIds);
}
result.items = result.items.filter((f) => {
if (f.type === FavoriteType.PAGE) {
return f.pageId && accessiblePageSet?.has(f.pageId);
}
if (f.type === FavoriteType.SPACE) {
return f.spaceId && spaceSet.has(f.spaceId);
}
if (f.type === FavoriteType.TEMPLATE) {
const templateSpaceId = (f as any).template?.spaceId;
return !templateSpaceId || spaceSet.has(templateSpaceId);
}
return true;
});
result.items = result.items.filter(
(f) =>
f.type !== FavoriteType.PAGE ||
(f.pageId && accessiblePageSet?.has(f.pageId)),
);
return result;
}
@@ -8,6 +8,7 @@ import { jsonObjectFrom } from 'kysely/helpers/postgres';
import { ExpressionBuilder, SelectQueryBuilder, sql } from 'kysely';
import { DB } from '@docmost/db/types/db';
import { dbOrTx } from '@docmost/db/utils';
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
export const FavoriteType = {
PAGE: 'page',
@@ -19,7 +20,10 @@ export type FavoriteType = (typeof FavoriteType)[keyof typeof FavoriteType];
@Injectable()
export class FavoriteRepo {
constructor(@InjectKysely() private readonly db: KyselyDB) {}
constructor(
@InjectKysely() private readonly db: KyselyDB,
private readonly spaceMemberRepo: SpaceMemberRepo,
) {}
async insert(favorite: InsertableFavorite): Promise<Favorite | undefined> {
try {
@@ -82,6 +86,8 @@ export class FavoriteRepo {
.where('favorites.workspaceId', '=', workspaceId)
.where('favorites.type', '=', type);
query = this.applyMembershipFilter(query, userId);
if (spaceId) {
query = this.applySpaceFilter(query, type, spaceId);
}
@@ -113,6 +119,8 @@ export class FavoriteRepo {
.where('favorites.userId', '=', userId)
.where('favorites.workspaceId', '=', workspaceId);
query = this.applyMembershipFilter(query, userId);
if (type) {
query = query.where('favorites.type', '=', type);
}
@@ -155,7 +163,7 @@ export class FavoriteRepo {
): Promise<void> {
if (userIds.length === 0) return;
const { trx } = opts;
const { trx } = opts ?? {};
const db = dbOrTx(this.db, trx);
const usersWithAccess = db
@@ -174,7 +182,24 @@ export class FavoriteRepo {
await db
.deleteFrom('favorites')
.where('userId', 'in', userIds)
.where('spaceId', '=', spaceId)
.where((eb) =>
eb.or([
eb('spaceId', '=', spaceId),
eb(
'pageId',
'in',
eb.selectFrom('pages').select('id').where('spaceId', '=', spaceId),
),
eb(
'templateId',
'in',
eb
.selectFrom('templates')
.select('id')
.where('spaceId', '=', spaceId),
),
]),
)
.where('userId', 'not in', usersWithAccess)
.execute();
}
@@ -194,6 +219,46 @@ export class FavoriteRepo {
.execute();
}
private applyMembershipFilter<Q extends SelectQueryBuilder<any, any, any>>(
query: Q,
userId: string,
): Q {
const spaceIds = this.spaceMemberRepo.getUserSpaceIdsQuery(userId);
return query.where((eb: any) =>
eb.or([
eb.and([
eb('favorites.type', '=', FavoriteType.SPACE),
eb('favorites.spaceId', 'in', spaceIds),
]),
eb.and([
eb('favorites.type', '=', FavoriteType.PAGE),
eb.exists(
eb
.selectFrom('pages')
.select(sql`1`.as('one'))
.whereRef('pages.id', '=', 'favorites.pageId')
.where('pages.spaceId', 'in', spaceIds),
),
]),
eb.and([
eb('favorites.type', '=', FavoriteType.TEMPLATE),
eb.exists(
eb
.selectFrom('templates')
.select(sql`1`.as('one'))
.whereRef('templates.id', '=', 'favorites.templateId')
.where((e: any) =>
e.or([
e('templates.spaceId', 'is', null),
e('templates.spaceId', 'in', spaceIds),
]),
),
),
]),
]),
) as Q;
}
private applySpaceFilter<Q extends SelectQueryBuilder<any, any, any>>(
query: Q,
type: FavoriteType | undefined,
@@ -239,7 +304,8 @@ export class FavoriteRepo {
'pages.isBase',
'pages.spaceId',
])
.whereRef('pages.id', '=', 'favorites.pageId'),
.whereRef('pages.id', '=', 'favorites.pageId')
.where(sql.ref('favorites.type'), '=', FavoriteType.PAGE),
).as('page');
}
@@ -269,8 +335,8 @@ export class FavoriteRepo {
.select(['spaces.id', 'spaces.name', 'spaces.slug', 'spaces.logo'])
.where(({ or, ref }) =>
or([
sql<boolean>`${ref('spaces.id')} = ${ref('favorites.spaceId')}`,
sql<boolean>`${ref('spaces.id')} = (SELECT pages.space_id FROM pages WHERE pages.id = ${ref('favorites.pageId')})`,
sql<boolean>`${ref('favorites.type')} = ${FavoriteType.SPACE} and ${ref('spaces.id')} = ${ref('favorites.spaceId')}`,
sql<boolean>`${ref('favorites.type')} = ${FavoriteType.PAGE} and ${ref('spaces.id')} = (SELECT pages.space_id FROM pages WHERE pages.id = ${ref('favorites.pageId')})`,
]),
),
).as('space');
@@ -287,7 +353,8 @@ export class FavoriteRepo {
'templates.icon',
'templates.spaceId',
])
.whereRef('templates.id', '=', 'favorites.templateId'),
.whereRef('templates.id', '=', 'favorites.templateId')
.where(sql.ref('favorites.type'), '=', FavoriteType.TEMPLATE),
).as('template');
}
}