mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
refactor(core): Let modules register favorite resource resolvers (#35909)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
da1d057268
commit
f6f293bec2
|
|
@ -59,6 +59,9 @@ export class AgentsModule implements ModuleInterface {
|
|||
agentKnowledgeFileStore: Container.get(AgentKnowledgeFileStore),
|
||||
});
|
||||
|
||||
const { registerFavoriteResolver } = await import('./register-favorite-resolver.js');
|
||||
registerFavoriteResolver();
|
||||
|
||||
const { AgentRuntimeCacheService } = await import('./agent-runtime-cache.service.js');
|
||||
Container.get(AgentRuntimeCacheService);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import { Container } from '@n8n/di';
|
||||
|
||||
import { FavoriteResourceResolverRegistry } from '@/modules/favorites/favorite-resource-resolver.registry';
|
||||
|
||||
import { AgentRepository } from './repositories/agent.repository';
|
||||
|
||||
export function registerFavoriteResolver() {
|
||||
const agentRepository = Container.get(AgentRepository);
|
||||
Container.get(FavoriteResourceResolverRegistry).register('agent', {
|
||||
globalReadScope: 'agent:read',
|
||||
findMeta: async (ids) =>
|
||||
new Map(
|
||||
(await agentRepository.findSummariesByIds(ids)).map(({ id, name, projectId }) => [
|
||||
id,
|
||||
{ name, projectId },
|
||||
]),
|
||||
),
|
||||
exists: async (id) => await agentRepository.existsBy({ id }),
|
||||
});
|
||||
}
|
||||
|
|
@ -29,6 +29,9 @@ export class DataTableModule implements ModuleInterface {
|
|||
},
|
||||
});
|
||||
|
||||
const { registerFavoriteResolver } = await import('./register-favorite-resolver.js');
|
||||
registerFavoriteResolver();
|
||||
|
||||
const { DataTableAggregateService } = await import('./data-table-aggregate.service.js');
|
||||
await Container.get(DataTableAggregateService).start();
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { Logger } from '@n8n/backend-common';
|
|||
import { GlobalConfig } from '@n8n/config';
|
||||
import { Project, withTransaction } from '@n8n/db';
|
||||
import { Service } from '@n8n/di';
|
||||
import { DataSource, EntityManager, Repository, SelectQueryBuilder } from '@n8n/typeorm';
|
||||
import { DataSource, EntityManager, In, Repository, SelectQueryBuilder } from '@n8n/typeorm';
|
||||
import {
|
||||
DATA_TABLE_SYSTEM_COLUMNS,
|
||||
DATA_TABLE_SYSTEM_TESTING_COLUMN,
|
||||
|
|
@ -191,6 +191,17 @@ export class DataTableRepository extends Repository<DataTable> {
|
|||
});
|
||||
}
|
||||
|
||||
async findSummariesByIds(
|
||||
ids: string[],
|
||||
): Promise<Array<Pick<DataTable, 'id' | 'name' | 'projectId'>>> {
|
||||
if (ids.length === 0) return [];
|
||||
|
||||
return await this.find({
|
||||
select: ['id', 'name', 'projectId'],
|
||||
where: { id: In(ids) },
|
||||
});
|
||||
}
|
||||
|
||||
async getManyAndCount(options: Partial<ListDataTableQueryDto>) {
|
||||
const query = this.getManyQuery(options);
|
||||
const [data, count] = await query.getManyAndCount();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import { Container } from '@n8n/di';
|
||||
|
||||
import { FavoriteResourceResolverRegistry } from '@/modules/favorites/favorite-resource-resolver.registry';
|
||||
|
||||
import { DataTableRepository } from './data-table.repository';
|
||||
|
||||
export function registerFavoriteResolver() {
|
||||
const dataTableRepository = Container.get(DataTableRepository);
|
||||
Container.get(FavoriteResourceResolverRegistry).register('dataTable', {
|
||||
globalReadScope: 'dataTable:read',
|
||||
findMeta: async (ids) =>
|
||||
new Map(
|
||||
(await dataTableRepository.findSummariesByIds(ids)).map(({ id, name, projectId }) => [
|
||||
id,
|
||||
{ name, projectId },
|
||||
]),
|
||||
),
|
||||
exists: async (id) => await dataTableRepository.existsBy({ id }),
|
||||
});
|
||||
}
|
||||
|
|
@ -11,11 +11,13 @@ import { mock } from 'vitest-mock-extended';
|
|||
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import type { DataTableRepository } from '@/modules/data-table/data-table.repository';
|
||||
import type { AgentRepository } from '@/modules/agents/repositories/agent.repository';
|
||||
|
||||
import type { UserFavorite } from '../database/entities/user-favorite.entity';
|
||||
import type { UserFavoriteRepository } from '../database/repositories/user-favorite.repository';
|
||||
import {
|
||||
FavoriteResourceResolverRegistry,
|
||||
type FavoriteResourceResolver,
|
||||
} from '../favorite-resource-resolver.registry';
|
||||
import { FavoritesService } from '../favorites.service';
|
||||
|
||||
const makeUserFavorite = (overrides: Partial<UserFavorite> = {}): UserFavorite =>
|
||||
|
|
@ -46,17 +48,27 @@ describe('FavoritesService', () => {
|
|||
const workflowRepository = mock<WorkflowRepository>();
|
||||
const projectRepository = mock<ProjectRepository>();
|
||||
const sharedWorkflowRepository = mock<SharedWorkflowRepository>();
|
||||
const dataTableRepository = mock<DataTableRepository>();
|
||||
const folderRepository = mock<FolderRepository>();
|
||||
const agentRepository = mock<AgentRepository>();
|
||||
const dataTableResolver = {
|
||||
globalReadScope: 'dataTable:read',
|
||||
findMeta: vi.fn<FavoriteResourceResolver['findMeta']>(),
|
||||
exists: vi.fn<FavoriteResourceResolver['exists']>(),
|
||||
} satisfies FavoriteResourceResolver;
|
||||
const agentResolver = {
|
||||
globalReadScope: 'agent:read',
|
||||
findMeta: vi.fn<FavoriteResourceResolver['findMeta']>(),
|
||||
exists: vi.fn<FavoriteResourceResolver['exists']>(),
|
||||
} satisfies FavoriteResourceResolver;
|
||||
const resolverRegistry = new FavoriteResourceResolverRegistry();
|
||||
resolverRegistry.register('dataTable', dataTableResolver);
|
||||
resolverRegistry.register('agent', agentResolver);
|
||||
const service = new FavoritesService(
|
||||
repo,
|
||||
workflowRepository,
|
||||
projectRepository,
|
||||
sharedWorkflowRepository,
|
||||
dataTableRepository,
|
||||
folderRepository,
|
||||
agentRepository,
|
||||
resolverRegistry,
|
||||
);
|
||||
|
||||
afterEach(() => vi.clearAllMocks());
|
||||
|
|
@ -143,12 +155,13 @@ describe('FavoritesService', () => {
|
|||
projectRepository.getAccessibleProjects.mockResolvedValue([
|
||||
{ id: 'proj1', name: 'Project 1' } as never,
|
||||
]);
|
||||
dataTableRepository.find.mockResolvedValue([
|
||||
{ id: 'dt1', name: 'My Table', projectId: 'proj1' } as never,
|
||||
]);
|
||||
dataTableResolver.findMeta.mockResolvedValue(
|
||||
new Map([['dt1', { name: 'My Table', projectId: 'proj1' }]]),
|
||||
);
|
||||
|
||||
const result = await service.getEnrichedFavorites(makeUser());
|
||||
|
||||
expect(dataTableResolver.findMeta).toHaveBeenCalledWith(['dt1']);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toMatchObject({
|
||||
resourceId: 'dt1',
|
||||
|
|
@ -163,15 +176,35 @@ describe('FavoritesService', () => {
|
|||
projectRepository.getAccessibleProjects.mockResolvedValue([
|
||||
{ id: 'proj1', name: 'Project 1' } as never,
|
||||
]);
|
||||
dataTableRepository.find.mockResolvedValue([
|
||||
{ id: 'dt1', name: 'My Table', projectId: 'proj-other' } as never,
|
||||
]);
|
||||
dataTableResolver.findMeta.mockResolvedValue(
|
||||
new Map([['dt1', { name: 'My Table', projectId: 'proj-other' }]]),
|
||||
);
|
||||
|
||||
const result = await service.getEnrichedFavorites(makeUser());
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should exclude favorites of a type that has no registered resolver', async () => {
|
||||
const favorite = makeUserFavorite({ resourceId: 'dt1', resourceType: 'dataTable' });
|
||||
repo.findByUser.mockResolvedValue([favorite]);
|
||||
projectRepository.getAccessibleProjects.mockResolvedValue([
|
||||
{ id: 'proj1', name: 'Project 1' } as never,
|
||||
]);
|
||||
const serviceWithoutResolvers = new FavoritesService(
|
||||
repo,
|
||||
workflowRepository,
|
||||
projectRepository,
|
||||
sharedWorkflowRepository,
|
||||
folderRepository,
|
||||
new FavoriteResourceResolverRegistry(),
|
||||
);
|
||||
|
||||
const result = await serviceWithoutResolvers.getEnrichedFavorites(makeUser());
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should enrich folder favorites with name and projectId for accessible folders', async () => {
|
||||
const favorite = makeUserFavorite({ resourceId: 'folder1', resourceType: 'folder' });
|
||||
repo.findByUser.mockResolvedValue([favorite]);
|
||||
|
|
@ -240,12 +273,13 @@ describe('FavoritesService', () => {
|
|||
projectRepository.getAccessibleProjects.mockResolvedValue([
|
||||
{ id: 'proj1', name: 'Project 1' } as never,
|
||||
]);
|
||||
agentRepository.find.mockResolvedValue([
|
||||
{ id: 'agent1', name: 'My Agent', projectId: 'proj1' } as never,
|
||||
]);
|
||||
agentResolver.findMeta.mockResolvedValue(
|
||||
new Map([['agent1', { name: 'My Agent', projectId: 'proj1' }]]),
|
||||
);
|
||||
|
||||
const result = await service.getEnrichedFavorites(makeUser());
|
||||
|
||||
expect(agentResolver.findMeta).toHaveBeenCalledWith(['agent1']);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toMatchObject({
|
||||
resourceId: 'agent1',
|
||||
|
|
@ -260,9 +294,9 @@ describe('FavoritesService', () => {
|
|||
projectRepository.getAccessibleProjects.mockResolvedValue([
|
||||
{ id: 'proj1', name: 'Project 1' } as never,
|
||||
]);
|
||||
agentRepository.find.mockResolvedValue([
|
||||
{ id: 'agent1', name: 'My Agent', projectId: 'proj-other' } as never,
|
||||
]);
|
||||
agentResolver.findMeta.mockResolvedValue(
|
||||
new Map([['agent1', { name: 'My Agent', projectId: 'proj-other' }]]),
|
||||
);
|
||||
|
||||
const result = await service.getEnrichedFavorites(makeUser());
|
||||
|
||||
|
|
@ -282,15 +316,15 @@ describe('FavoritesService', () => {
|
|||
]);
|
||||
workflowRepository.findByIds.mockResolvedValue([{ id: 'wf1', name: 'My Workflow' } as never]);
|
||||
sharedWorkflowRepository.find.mockResolvedValue([{ workflowId: 'wf1' } as never]);
|
||||
dataTableRepository.find.mockResolvedValue([
|
||||
{ id: 'dt1', name: 'My Table', projectId: 'proj1' } as never,
|
||||
]);
|
||||
dataTableResolver.findMeta.mockResolvedValue(
|
||||
new Map([['dt1', { name: 'My Table', projectId: 'proj1' }]]),
|
||||
);
|
||||
folderRepository.find.mockResolvedValue([
|
||||
{ id: 'folder1', name: 'My Folder', homeProject: { id: 'proj1' } } as never,
|
||||
]);
|
||||
agentRepository.find.mockResolvedValue([
|
||||
{ id: 'agent1', name: 'My Agent', projectId: 'proj1' } as never,
|
||||
]);
|
||||
agentResolver.findMeta.mockResolvedValue(
|
||||
new Map([['agent1', { name: 'My Agent', projectId: 'proj1' }]]),
|
||||
);
|
||||
|
||||
const result = await service.getEnrichedFavorites(makeUser());
|
||||
|
||||
|
|
@ -334,9 +368,9 @@ describe('FavoritesService', () => {
|
|||
const favorite = makeUserFavorite({ resourceId: 'dt1', resourceType: 'dataTable' });
|
||||
repo.findByUser.mockResolvedValue([favorite]);
|
||||
projectRepository.getAccessibleProjects.mockResolvedValue([]);
|
||||
dataTableRepository.find.mockResolvedValue([
|
||||
{ id: 'dt1', name: 'My Table', projectId: 'proj-other' } as never,
|
||||
]);
|
||||
dataTableResolver.findMeta.mockResolvedValue(
|
||||
new Map([['dt1', { name: 'My Table', projectId: 'proj-other' }]]),
|
||||
);
|
||||
|
||||
const result = await service.getEnrichedFavorites(makeAdmin());
|
||||
|
||||
|
|
@ -374,9 +408,9 @@ describe('FavoritesService', () => {
|
|||
const favorite = makeUserFavorite({ resourceId: 'agent1', resourceType: 'agent' });
|
||||
repo.findByUser.mockResolvedValue([favorite]);
|
||||
projectRepository.getAccessibleProjects.mockResolvedValue([]);
|
||||
agentRepository.find.mockResolvedValue([
|
||||
{ id: 'agent1', name: 'My Agent', projectId: 'proj-other' } as never,
|
||||
]);
|
||||
agentResolver.findMeta.mockResolvedValue(
|
||||
new Map([['agent1', { name: 'My Agent', projectId: 'proj-other' }]]),
|
||||
);
|
||||
|
||||
const result = await service.getEnrichedFavorites(makeAdmin());
|
||||
|
||||
|
|
@ -475,18 +509,14 @@ describe('FavoritesService', () => {
|
|||
it.each([
|
||||
{ resourceType: 'workflow' as const, repository: 'workflowRepository' },
|
||||
{ resourceType: 'project' as const, repository: 'projectRepository' },
|
||||
{ resourceType: 'dataTable' as const, repository: 'dataTableRepository' },
|
||||
{ resourceType: 'folder' as const, repository: 'folderRepository' },
|
||||
{ resourceType: 'agent' as const, repository: 'agentRepository' },
|
||||
])(
|
||||
'should check existence using the correct repository for $resourceType',
|
||||
async ({ resourceType, repository }) => {
|
||||
const repositories = {
|
||||
workflowRepository,
|
||||
projectRepository,
|
||||
dataTableRepository,
|
||||
folderRepository,
|
||||
agentRepository,
|
||||
};
|
||||
repo.findOne.mockResolvedValue(null);
|
||||
repositories[repository as keyof typeof repositories].existsBy.mockResolvedValue(false);
|
||||
|
|
@ -499,6 +529,65 @@ describe('FavoritesService', () => {
|
|||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ resourceType: 'dataTable' as const, resolver: dataTableResolver },
|
||||
{ resourceType: 'agent' as const, resolver: agentResolver },
|
||||
])(
|
||||
'should check existence using the registered resolver for $resourceType',
|
||||
async ({ resourceType, resolver }) => {
|
||||
repo.findOne.mockResolvedValue(null);
|
||||
resolver.exists.mockResolvedValue(false);
|
||||
|
||||
await expect(service.addFavorite('user1', 'res1', resourceType)).rejects.toThrow(
|
||||
NotFoundError,
|
||||
);
|
||||
expect(resolver.exists).toHaveBeenCalledWith('res1');
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ resourceType: 'dataTable' as const, resolver: dataTableResolver },
|
||||
{ resourceType: 'agent' as const, resolver: agentResolver },
|
||||
])(
|
||||
'should create the favorite when the resolver reports the $resourceType exists',
|
||||
async ({ resourceType, resolver }) => {
|
||||
const favorite = mock<UserFavorite>();
|
||||
repo.findOne.mockResolvedValue(null);
|
||||
resolver.exists.mockResolvedValue(true);
|
||||
repo.count.mockResolvedValue(0);
|
||||
repo.create.mockReturnValue(favorite);
|
||||
repo.save.mockResolvedValue(favorite);
|
||||
|
||||
const result = await service.addFavorite('user1', 'res1', resourceType);
|
||||
|
||||
expect(resolver.exists).toHaveBeenCalledWith('res1');
|
||||
expect(repo.create).toHaveBeenCalledWith({
|
||||
userId: 'user1',
|
||||
resourceId: 'res1',
|
||||
resourceType,
|
||||
});
|
||||
expect(repo.save).toHaveBeenCalledWith(favorite);
|
||||
expect(result).toBe(favorite);
|
||||
},
|
||||
);
|
||||
|
||||
it('should throw NotFoundError when the resource type has no registered resolver', async () => {
|
||||
const serviceWithoutResolvers = new FavoritesService(
|
||||
repo,
|
||||
workflowRepository,
|
||||
projectRepository,
|
||||
sharedWorkflowRepository,
|
||||
folderRepository,
|
||||
new FavoriteResourceResolverRegistry(),
|
||||
);
|
||||
repo.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(serviceWithoutResolvers.addFavorite('user1', 'res1', 'agent')).rejects.toThrow(
|
||||
NotFoundError,
|
||||
);
|
||||
expect(repo.save).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeFavorite', () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
import type { FavoriteResourceType } from '@n8n/api-types';
|
||||
import { Service } from '@n8n/di';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
|
||||
export type FavoriteResourceMeta = { name: string; projectId: string };
|
||||
|
||||
/** Favorite types resolved via a registered resolver; all others are handled by the favorites module itself. */
|
||||
export type ResolvedFavoriteResourceType = Extract<FavoriteResourceType, 'dataTable' | 'agent'>;
|
||||
|
||||
/**
|
||||
* Resolves favorited resources owned by another backend module. The owning
|
||||
* module registers its resolver in its `init()`, so favorites never depends
|
||||
* on module internals; if the module is inactive, no resolver exists and
|
||||
* favorites of that type are treated as missing resources.
|
||||
*/
|
||||
export interface FavoriteResourceResolver {
|
||||
/** Scope granting access to the resource regardless of project membership. */
|
||||
readonly globalReadScope: Scope;
|
||||
/** Per-id metadata; ids missing from the map are treated as deleted or inaccessible. */
|
||||
findMeta(ids: string[]): Promise<Map<string, FavoriteResourceMeta>>;
|
||||
exists(id: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
@Service()
|
||||
export class FavoriteResourceResolverRegistry {
|
||||
private readonly resolvers = new Map<ResolvedFavoriteResourceType, FavoriteResourceResolver>();
|
||||
|
||||
register(type: ResolvedFavoriteResourceType, resolver: FavoriteResourceResolver) {
|
||||
this.resolvers.set(type, resolver);
|
||||
}
|
||||
|
||||
get(type: ResolvedFavoriteResourceType): FavoriteResourceResolver | undefined {
|
||||
return this.resolvers.get(type);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,13 +13,13 @@ import { In } from '@n8n/typeorm';
|
|||
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import { DataTableRepository } from '@/modules/data-table/data-table.repository';
|
||||
|
||||
import { UserFavoriteRepository } from './database/repositories/user-favorite.repository';
|
||||
|
||||
import { AgentRepository } from '../agents/repositories/agent.repository';
|
||||
|
||||
type ResourceMeta = { name: string; projectId: string };
|
||||
import type {
|
||||
FavoriteResourceMeta as ResourceMeta,
|
||||
ResolvedFavoriteResourceType,
|
||||
} from './favorite-resource-resolver.registry';
|
||||
import { FavoriteResourceResolverRegistry } from './favorite-resource-resolver.registry';
|
||||
|
||||
type Favorite = { resourceId: string; resourceType: FavoriteResourceType };
|
||||
|
||||
|
|
@ -48,9 +48,8 @@ export class FavoritesService {
|
|||
private readonly workflowRepository: WorkflowRepository,
|
||||
private readonly projectRepository: ProjectRepository,
|
||||
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
|
||||
private readonly dataTableRepository: DataTableRepository,
|
||||
private readonly folderRepository: FolderRepository,
|
||||
private readonly agentRepository: AgentRepository,
|
||||
private readonly resourceResolvers: FavoriteResourceResolverRegistry,
|
||||
) {}
|
||||
|
||||
async getEnrichedFavorites(user: User) {
|
||||
|
|
@ -71,9 +70,9 @@ export class FavoritesService {
|
|||
const [workflowNames, projectNames, dataTableMeta, folderMeta, agentMeta] = await Promise.all([
|
||||
this.enrichWorkflowFavorites(user, ids.workflow, accessibleProjectIds),
|
||||
this.enrichProjectFavorites(user, ids.project, accessibleProjects),
|
||||
this.enrichDataTableFavorites(user, ids.dataTable, accessibleProjectIds),
|
||||
this.enrichResolvedFavorites(user, 'dataTable', ids.dataTable, accessibleProjectIds),
|
||||
this.enrichFolderFavorites(user, ids.folder, accessibleProjectIds),
|
||||
this.enrichAgentFavorites(user, ids.agent, accessibleProjectIds),
|
||||
this.enrichResolvedFavorites(user, 'agent', ids.agent, accessibleProjectIds),
|
||||
]);
|
||||
|
||||
return favorites.flatMap((fav) => {
|
||||
|
|
@ -148,20 +147,26 @@ export class FavoritesService {
|
|||
return result;
|
||||
}
|
||||
|
||||
private async enrichDataTableFavorites(
|
||||
/** Enriches favorites whose resource type is owned by another module, via its registered resolver. */
|
||||
private async enrichResolvedFavorites(
|
||||
user: User,
|
||||
dataTableIds: string[],
|
||||
resourceType: ResolvedFavoriteResourceType,
|
||||
resourceIds: string[],
|
||||
accessibleProjectIds: Set<string>,
|
||||
): Promise<Map<string, ResourceMeta>> {
|
||||
const result = new Map<string, ResourceMeta>();
|
||||
if (dataTableIds.length === 0) return result;
|
||||
if (resourceIds.length === 0) return result;
|
||||
|
||||
const hasGlobalAccess = hasGlobalScope(user, 'dataTable:read');
|
||||
const dataTables = await this.dataTableRepository.find({ where: { id: In(dataTableIds) } });
|
||||
// No resolver means the owning module is inactive: treat its favorites as missing resources
|
||||
const resolver = this.resourceResolvers.get(resourceType);
|
||||
if (!resolver) return result;
|
||||
|
||||
for (const dt of dataTables) {
|
||||
if (hasGlobalAccess || accessibleProjectIds.has(dt.projectId)) {
|
||||
result.set(dt.id, { name: dt.name, projectId: dt.projectId });
|
||||
const hasGlobalAccess = hasGlobalScope(user, resolver.globalReadScope);
|
||||
const meta = await resolver.findMeta(resourceIds);
|
||||
|
||||
for (const [id, resourceMeta] of meta) {
|
||||
if (hasGlobalAccess || accessibleProjectIds.has(resourceMeta.projectId)) {
|
||||
result.set(id, resourceMeta);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
|
@ -190,27 +195,6 @@ export class FavoritesService {
|
|||
return result;
|
||||
}
|
||||
|
||||
private async enrichAgentFavorites(
|
||||
user: User,
|
||||
agentIds: string[],
|
||||
accessibleProjectIds: Set<string>,
|
||||
): Promise<Map<string, ResourceMeta>> {
|
||||
const result = new Map<string, ResourceMeta>();
|
||||
if (agentIds.length === 0) return result;
|
||||
|
||||
const hasGlobalAccess = hasGlobalScope(user, 'agent:read');
|
||||
const agents = await this.agentRepository.find({
|
||||
where: { id: In(agentIds) },
|
||||
});
|
||||
|
||||
for (const agent of agents) {
|
||||
if (hasGlobalAccess || accessibleProjectIds.has(agent.projectId)) {
|
||||
result.set(agent.id, { name: agent.name, projectId: agent.projectId });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async addFavorite(userId: string, resourceId: string, resourceType: FavoriteResourceType) {
|
||||
const existing = await this.userFavoriteRepository.findOne({
|
||||
where: { userId, resourceId, resourceType },
|
||||
|
|
@ -242,14 +226,12 @@ export class FavoritesService {
|
|||
case 'project':
|
||||
exists = await this.projectRepository.existsBy({ id: resourceId });
|
||||
break;
|
||||
case 'dataTable':
|
||||
exists = await this.dataTableRepository.existsBy({ id: resourceId });
|
||||
break;
|
||||
case 'folder':
|
||||
exists = await this.folderRepository.existsBy({ id: resourceId });
|
||||
break;
|
||||
case 'dataTable':
|
||||
case 'agent':
|
||||
exists = await this.agentRepository.existsBy({ id: resourceId });
|
||||
exists = (await this.resourceResolvers.get(resourceType)?.exists(resourceId)) ?? false;
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user