feat: add pagination support for workspace config (#11859)

fix AI-78
This commit is contained in:
darkskygit
2025-04-23 11:25:41 +00:00
parent 5397fba897
commit ddb739fa13
13 changed files with 460 additions and 113 deletions

View File

@@ -9,7 +9,6 @@ import {
Resolver,
} from '@nestjs/graphql';
import type { Request } from 'express';
import { SafeIntResolver } from 'graphql-scalars';
import GraphQLUpload, {
type FileUpload,
} from 'graphql-upload/GraphQLUpload.mjs';
@@ -19,16 +18,22 @@ import {
CopilotEmbeddingUnavailable,
CopilotFailedToAddWorkspaceFileEmbedding,
Mutex,
paginate,
PaginationInput,
TooManyRequest,
UserFriendlyError,
} from '../../../base';
import { CurrentUser } from '../../../core/auth';
import { AccessController } from '../../../core/permission';
import { WorkspaceType } from '../../../core/workspaces';
import { CopilotWorkspaceFile, Models } from '../../../models';
import { COPILOT_LOCKER } from '../resolver';
import { MAX_EMBEDDABLE_SIZE } from '../types';
import { CopilotWorkspaceService } from './service';
import {
CopilotWorkspaceFileType,
PaginatedCopilotWorkspaceFileType,
PaginatedIgnoredDocsType,
} from './types';
@ObjectType('CopilotWorkspaceConfig')
export class CopilotWorkspaceConfigType {
@@ -36,27 +41,6 @@ export class CopilotWorkspaceConfigType {
workspaceId!: string;
}
@ObjectType('CopilotWorkspaceFile')
export class CopilotWorkspaceFileType implements CopilotWorkspaceFile {
@Field(() => String)
workspaceId!: string;
@Field(() => String)
fileId!: string;
@Field(() => String)
fileName!: string;
@Field(() => String)
mimeType!: string;
@Field(() => SafeIntResolver)
size!: number;
@Field(() => Date)
createdAt!: Date;
}
/**
* Workspace embedding config resolver
* Public apis rate limit: 10 req/m
@@ -86,18 +70,24 @@ export class CopilotWorkspaceEmbeddingResolver {
export class CopilotWorkspaceEmbeddingConfigResolver {
constructor(
private readonly ac: AccessController,
private readonly models: Models,
private readonly mutex: Mutex,
private readonly copilotWorkspace: CopilotWorkspaceService
) {}
@ResolveField(() => [String], {
@ResolveField(() => PaginatedIgnoredDocsType, {
complexity: 2,
})
async ignoredDocs(
@Parent() config: CopilotWorkspaceConfigType
): Promise<string[]> {
return this.models.copilotWorkspace.listIgnoredDocs(config.workspaceId);
@Parent() config: CopilotWorkspaceConfigType,
@Args('pagination', PaginationInput.decode) pagination: PaginationInput
): Promise<PaginatedIgnoredDocsType> {
const [ignoredDocs, totalCount] =
await this.copilotWorkspace.listIgnoredDocs(
config.workspaceId,
pagination
);
return paginate(ignoredDocs, 'createdAt', pagination, totalCount);
}
@Mutation(() => Number, {
@@ -118,20 +108,26 @@ export class CopilotWorkspaceEmbeddingConfigResolver {
.user(user.id)
.workspace(workspaceId)
.assert('Workspace.Settings.Update');
return await this.models.copilotWorkspace.updateIgnoredDocs(
return await this.copilotWorkspace.updateIgnoredDocs(
workspaceId,
add,
remove
);
}
@ResolveField(() => [CopilotWorkspaceFileType], {
@ResolveField(() => PaginatedCopilotWorkspaceFileType, {
complexity: 2,
})
async files(
@Parent() config: CopilotWorkspaceConfigType
): Promise<CopilotWorkspaceFileType[]> {
return this.models.copilotWorkspace.listWorkspaceFiles(config.workspaceId);
@Parent() config: CopilotWorkspaceConfigType,
@Args('pagination', PaginationInput.decode) pagination: PaginationInput
): Promise<PaginatedCopilotWorkspaceFileType> {
const [files, totalCount] = await this.copilotWorkspace.listWorkspaceFiles(
config.workspaceId,
pagination
);
return paginate(files, 'createdAt', pagination, totalCount);
}
@Mutation(() => CopilotWorkspaceFileType, {
@@ -210,9 +206,6 @@ export class CopilotWorkspaceEmbeddingConfigResolver {
.workspace(workspaceId)
.assert('Workspace.Settings.Update');
return await this.models.copilotWorkspace.removeWorkspaceFile(
workspaceId,
fileId
);
return await this.copilotWorkspace.removeWorkspaceFile(workspaceId, fileId);
}
}

View File

@@ -2,31 +2,11 @@ import { createHash } from 'node:crypto';
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
import { FileUpload, JobQueue } from '../../../base';
import { FileUpload, JobQueue, PaginationInput } from '../../../base';
import { Models } from '../../../models';
import { CopilotStorage } from '../storage';
import { readStream } from '../utils';
declare global {
interface Events {
'workspace.file.embedding.finished': {
jobId: string;
};
'workspace.file.embedding.failed': {
jobId: string;
};
}
interface Jobs {
'copilot.workspace.embedding.files': {
userId: string;
workspaceId: string;
blobId: string;
fileId: string;
fileName: string;
};
}
}
@Injectable()
export class CopilotWorkspaceService implements OnApplicationBootstrap {
private supportEmbedding = false;
@@ -49,6 +29,30 @@ export class CopilotWorkspaceService implements OnApplicationBootstrap {
return this.supportEmbedding;
}
async updateIgnoredDocs(
workspaceId: string,
add?: string[],
remove?: string[]
) {
return await this.models.copilotWorkspace.updateIgnoredDocs(
workspaceId,
add,
remove
);
}
async listIgnoredDocs(
workspaceId: string,
pagination?: {
includeRead?: boolean;
} & PaginationInput
) {
return await Promise.all([
this.models.copilotWorkspace.listIgnoredDocs(workspaceId, pagination),
this.models.copilotWorkspace.countIgnoredDocs(workspaceId),
]);
}
async addWorkspaceFile(
userId: string,
workspaceId: string,
@@ -70,6 +74,18 @@ export class CopilotWorkspaceService implements OnApplicationBootstrap {
return await this.models.copilotWorkspace.getFile(workspaceId, fileId);
}
async listWorkspaceFiles(
workspaceId: string,
pagination?: {
includeRead?: boolean;
} & PaginationInput
) {
return await Promise.all([
this.models.copilotWorkspace.listWorkspaceFiles(workspaceId, pagination),
this.models.copilotWorkspace.countIgnoredDocs(workspaceId),
]);
}
async addWorkspaceFileEmbeddingQueue(
file: Jobs['copilot.workspace.embedding.files']
) {
@@ -84,4 +100,11 @@ export class CopilotWorkspaceService implements OnApplicationBootstrap {
fileName,
});
}
async removeWorkspaceFile(workspaceId: string, fileId: string) {
return await this.models.copilotWorkspace.removeWorkspaceFile(
workspaceId,
fileId
);
}
}

View File

@@ -0,0 +1,65 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { SafeIntResolver } from 'graphql-scalars';
import { Paginated } from '../../../base';
import { CopilotWorkspaceFile } from '../../../models';
declare global {
interface Events {
'workspace.file.embedding.finished': {
jobId: string;
};
'workspace.file.embedding.failed': {
jobId: string;
};
}
interface Jobs {
'copilot.workspace.embedding.files': {
userId: string;
workspaceId: string;
blobId: string;
fileId: string;
fileName: string;
};
}
}
@ObjectType('CopilotWorkspaceIgnoredDoc')
export class CopilotWorkspaceIgnoredDocType {
@Field(() => String)
docId!: string;
@Field(() => Date)
createdAt!: Date;
}
@ObjectType()
export class PaginatedIgnoredDocsType extends Paginated(
CopilotWorkspaceIgnoredDocType
) {}
@ObjectType('CopilotWorkspaceFile')
export class CopilotWorkspaceFileType implements CopilotWorkspaceFile {
@Field(() => String)
workspaceId!: string;
@Field(() => String)
fileId!: string;
@Field(() => String)
fileName!: string;
@Field(() => String)
mimeType!: string;
@Field(() => SafeIntResolver)
size!: number;
@Field(() => Date)
createdAt!: Date;
}
@ObjectType()
export class PaginatedCopilotWorkspaceFileType extends Paginated(
CopilotWorkspaceFileType
) {}