fix: use subquery (#1833)

- enhance file tasks list endpoint
This commit is contained in:
Philip Okugbe
2026-01-13 15:58:26 +00:00
committed by GitHub
parent 13f529e064
commit 47097969a0
7 changed files with 57 additions and 60 deletions
@@ -1,5 +1,7 @@
import api from "@/lib/api-client"; import api from "@/lib/api-client";
import { IFileTask } from "@/features/file-task/types/file-task.types.ts"; import { IFileTask } from "@/features/file-task/types/file-task.types.ts";
import { IPagination, QueryParams } from "@/lib/types.ts";
import { IApiKey } from "@/ee/api-key";
export async function getFileTaskById(fileTaskId: string): Promise<IFileTask> { export async function getFileTaskById(fileTaskId: string): Promise<IFileTask> {
const req = await api.post<IFileTask>("/file-tasks/info", { const req = await api.post<IFileTask>("/file-tasks/info", {
@@ -8,7 +10,10 @@ export async function getFileTaskById(fileTaskId: string): Promise<IFileTask> {
return req.data; return req.data;
} }
export async function getFileTasks(): Promise<IFileTask[]> { export async function getFileTasks(
const req = await api.post<IFileTask[]>("/file-tasks"); params?: QueryParams,
): Promise<IPagination<IFileTask>> {
const req = await api.post("/file-tasks", { ...params });
return req.data; return req.data;
} }
+7 -10
View File
@@ -74,16 +74,13 @@ export class SearchService {
queryResults = queryResults.where('spaceId', '=', searchParams.spaceId); queryResults = queryResults.where('spaceId', '=', searchParams.spaceId);
} else if (opts.userId && !searchParams.spaceId) { } else if (opts.userId && !searchParams.spaceId) {
// only search spaces the user is a member of // only search spaces the user is a member of
const userSpaceIds = await this.spaceMemberRepo.getUserSpaceIds( queryResults = queryResults
opts.userId, .where(
); 'spaceId',
if (userSpaceIds.length > 0) { 'in',
queryResults = queryResults this.spaceMemberRepo.getUserSpaceIdsQuery(opts.userId),
.where('spaceId', 'in', userSpaceIds) )
.where('workspaceId', '=', opts.workspaceId); .where('workspaceId', '=', opts.workspaceId);
} else {
return [];
}
} else if (searchParams.shareId && !searchParams.spaceId && !opts.userId) { } else if (searchParams.shareId && !searchParams.spaceId && !opts.userId) {
// search in shares // search in shares
const shareId = searchParams.shareId; const shareId = searchParams.shareId;
@@ -293,24 +293,18 @@ export class PageRepo {
} }
async getRecentPages(userId: string, pagination: PaginationOptions) { async getRecentPages(userId: string, pagination: PaginationOptions) {
const userSpaceIds = await this.spaceMemberRepo.getUserSpaceIds(userId);
const query = this.db const query = this.db
.selectFrom('pages') .selectFrom('pages')
.select(this.baseFields) .select(this.baseFields)
.select((eb) => this.withSpace(eb)) .select((eb) => this.withSpace(eb))
.where('spaceId', 'in', userSpaceIds) .where('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(userId))
.where('deletedAt', 'is', null) .where('deletedAt', 'is', null)
.orderBy('updatedAt', 'desc'); .orderBy('updatedAt', 'desc');
const hasEmptyIds = userSpaceIds.length === 0; return executeWithPagination(query, {
const result = executeWithPagination(query, {
page: pagination.page, page: pagination.page,
perPage: pagination.limit, perPage: pagination.limit,
hasEmptyIds,
}); });
return result;
} }
async getDeletedPagesInSpace(spaceId: string, pagination: PaginationOptions) { async getDeletedPagesInSpace(spaceId: string, pagination: PaginationOptions) {
@@ -137,25 +137,19 @@ export class ShareRepo {
} }
async getShares(userId: string, pagination: PaginationOptions) { async getShares(userId: string, pagination: PaginationOptions) {
const userSpaceIds = await this.spaceMemberRepo.getUserSpaceIds(userId);
const query = this.db const query = this.db
.selectFrom('shares') .selectFrom('shares')
.select(this.baseFields) .select(this.baseFields)
.select((eb) => this.withPage(eb)) .select((eb) => this.withPage(eb))
.select((eb) => this.withSpace(eb, userId)) .select((eb) => this.withSpace(eb, userId))
.select((eb) => this.withCreator(eb)) .select((eb) => this.withCreator(eb))
.where('spaceId', 'in', userSpaceIds) .where('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(userId))
.orderBy('updatedAt', 'desc'); .orderBy('updatedAt', 'desc');
const hasEmptyIds = userSpaceIds.length === 0; return executeWithPagination(query, {
const result = executeWithPagination(query, {
page: pagination.page, page: pagination.page,
perPage: pagination.limit, perPage: pagination.limit,
hasEmptyIds,
}); });
return result;
} }
withPage(eb: ExpressionBuilder<DB, 'shares'>) { withPage(eb: ExpressionBuilder<DB, 'shares'>) {
@@ -209,34 +209,33 @@ export class SpaceMemberRepo {
return roles; return roles;
} }
async getUserSpaceIds(userId: string): Promise<string[]> { getUserSpaceIdsQuery(userId: string) {
const membership = await this.db return this.db
.selectFrom('spaceMembers') .selectFrom('spaceMembers')
.innerJoin('spaces', 'spaces.id', 'spaceMembers.spaceId') .innerJoin('spaces', 'spaces.id', 'spaceMembers.spaceId')
.select(['spaces.id']) .select('spaces.id')
.where('userId', '=', userId) .where('userId', '=', userId)
.union( .union(
this.db this.db
.selectFrom('spaceMembers') .selectFrom('spaceMembers')
.innerJoin('groupUsers', 'groupUsers.groupId', 'spaceMembers.groupId') .innerJoin('groupUsers', 'groupUsers.groupId', 'spaceMembers.groupId')
.innerJoin('spaces', 'spaces.id', 'spaceMembers.spaceId') .innerJoin('spaces', 'spaces.id', 'spaceMembers.spaceId')
.select(['spaces.id']) .select('spaces.id')
.where('groupUsers.userId', '=', userId), .where('groupUsers.userId', '=', userId),
) );
.execute(); }
async getUserSpaceIds(userId: string): Promise<string[]> {
const membership = await this.getUserSpaceIdsQuery(userId).execute();
return membership.map((space) => space.id); return membership.map((space) => space.id);
} }
async getUserSpaces(userId: string, pagination: PaginationOptions) { async getUserSpaces(userId: string, pagination: PaginationOptions) {
const userSpaceIds = await this.getUserSpaceIds(userId);
let query = this.db let query = this.db
.selectFrom('spaces') .selectFrom('spaces')
.selectAll() .selectAll()
.select((eb) => [this.spaceRepo.withMemberCount(eb)]) .select((eb) => [this.spaceRepo.withMemberCount(eb)])
//.where('workspaceId', '=', workspaceId) .where('id', 'in', this.getUserSpaceIdsQuery(userId))
.where('id', 'in', userSpaceIds)
.orderBy('createdAt', 'asc'); .orderBy('createdAt', 'asc');
if (pagination.query) { if (pagination.query) {
@@ -253,14 +252,9 @@ export class SpaceMemberRepo {
); );
} }
const hasEmptyIds = userSpaceIds.length === 0; return executeWithPagination(query, {
const result = executeWithPagination(query, {
page: pagination.page, page: pagination.page,
perPage: pagination.limit, perPage: pagination.limit,
hasEmptyIds,
}); });
return result;
} }
} }
@@ -10,46 +10,59 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import SpaceAbilityFactory from '../../core/casl/abilities/space-ability.factory'; import SpaceAbilityFactory from '../../core/casl/abilities/space-ability.factory';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { User } from '@docmost/db/types/entity.types'; import { User, Workspace } from '@docmost/db/types/entity.types';
import { import {
SpaceCaslAction, SpaceCaslAction,
SpaceCaslSubject, SpaceCaslSubject,
} from '../../core/casl/interfaces/space-ability.type'; } from '../../core/casl/interfaces/space-ability.type';
import {
WorkspaceCaslAction,
WorkspaceCaslSubject,
} from '../../core/casl/interfaces/workspace-ability.type';
import WorkspaceAbilityFactory from '../../core/casl/abilities/workspace-ability.factory';
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
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 { AuthUser } from '../../common/decorators/auth-user.decorator'; import { AuthUser } from '../../common/decorators/auth-user.decorator';
import { FileTaskIdDto } from './dto/file-task-dto'; import { FileTaskIdDto } from './dto/file-task-dto';
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo'; import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { executeWithPagination } from '@docmost/db/pagination/pagination';
@Controller('file-tasks') @Controller('file-tasks')
export class FileTaskController { export class FileTaskController {
constructor( constructor(
private readonly spaceMemberRepo: SpaceMemberRepo,
private readonly spaceAbility: SpaceAbilityFactory, private readonly spaceAbility: SpaceAbilityFactory,
private readonly workspaceAbility: WorkspaceAbilityFactory,
private readonly spaceMemberRepo: SpaceMemberRepo,
@InjectKysely() private readonly db: KyselyDB, @InjectKysely() private readonly db: KyselyDB,
) {} ) {}
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Post() @Post()
async getFileTasks(@AuthUser() user: User) { async getFileTasks(
const userSpaceIds = await this.spaceMemberRepo.getUserSpaceIds(user.id); @Body() pagination: PaginationOptions,
@AuthUser() user: User,
if (!userSpaceIds || userSpaceIds.length === 0) { @AuthWorkspace() workspace: Workspace,
return []; ) {
const ability = this.workspaceAbility.createForUser(user, workspace);
if (
ability.cannot(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Settings)
) {
throw new ForbiddenException();
} }
const fileTasks = await this.db const query = this.db
.selectFrom('fileTasks') .selectFrom('fileTasks')
.selectAll() .selectAll()
.where('spaceId', 'in', userSpaceIds) .where('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(user.id))
.execute(); .orderBy('createdAt', 'desc');
if (!fileTasks) { return executeWithPagination(query, {
throw new NotFoundException('File task not found'); page: pagination.page,
} perPage: pagination.limit,
});
return fileTasks;
} }
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)