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