mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-28 09:12:12 +08:00
fix(API): Skip projectRelations eager-load in GET /credentials (#37138)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -146,6 +146,19 @@ describe('CredentialsRepository', () => {
|
||||
expect(callArg!.order).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should honor a caller-provided relations array', async () => {
|
||||
entityManager.findAndCount.mockResolvedValueOnce([[], 0]);
|
||||
|
||||
await credentialsRepository.findManyAndCount({
|
||||
take: 10,
|
||||
skip: 0,
|
||||
relations: ['shared', 'shared.project'],
|
||||
});
|
||||
|
||||
const callArg = entityManager.findAndCount.mock.calls[0]?.[1];
|
||||
expect(callArg?.relations).toEqual(['shared', 'shared.project']);
|
||||
});
|
||||
|
||||
it('should apply credentialIds filter when provided', async () => {
|
||||
entityManager.findAndCount.mockResolvedValueOnce([[], 0]);
|
||||
|
||||
|
||||
@@ -18,9 +18,21 @@ import { parseListQuerySortBy } from '../utils/list-query-sort';
|
||||
|
||||
const SORTABLE_COLUMNS = new Set(['id', 'name', 'createdAt', 'updatedAt']);
|
||||
|
||||
export type CredentialSharingRelation =
|
||||
| 'shared'
|
||||
| 'shared.project'
|
||||
| 'shared.project.projectRelations';
|
||||
|
||||
const DEFAULT_CREDENTIAL_RELATIONS: CredentialSharingRelation[] = [
|
||||
'shared',
|
||||
'shared.project',
|
||||
'shared.project.projectRelations',
|
||||
];
|
||||
|
||||
type CredentialsListQueryOptions = ListQuery.Options & {
|
||||
includeData?: boolean;
|
||||
user?: User;
|
||||
relations?: CredentialSharingRelation[];
|
||||
};
|
||||
|
||||
@Service()
|
||||
@@ -149,7 +161,7 @@ export class CredentialsRepository extends BaseRepository<CredentialsEntity> {
|
||||
|
||||
type Select = Array<keyof CredentialsEntity>;
|
||||
|
||||
const defaultRelations = ['shared', 'shared.project', 'shared.project.projectRelations'];
|
||||
const relations = listQueryOptions?.relations ?? DEFAULT_CREDENTIAL_RELATIONS;
|
||||
const defaultSelect: Select = [
|
||||
'id',
|
||||
'name',
|
||||
@@ -165,7 +177,7 @@ export class CredentialsRepository extends BaseRepository<CredentialsEntity> {
|
||||
if (!listQueryOptions) {
|
||||
return {
|
||||
select: defaultSelect,
|
||||
relations: defaultRelations,
|
||||
relations,
|
||||
} as FindManyOptions<CredentialsEntity>;
|
||||
}
|
||||
|
||||
@@ -197,7 +209,7 @@ export class CredentialsRepository extends BaseRepository<CredentialsEntity> {
|
||||
|
||||
if (!findManyOptions.select) {
|
||||
findManyOptions.select = defaultSelect;
|
||||
findManyOptions.relations = defaultRelations;
|
||||
findManyOptions.relations = relations;
|
||||
}
|
||||
|
||||
if (sortBy) {
|
||||
@@ -525,7 +537,6 @@ export class CredentialsRepository extends BaseRepository<CredentialsEntity> {
|
||||
|
||||
// Apply relations
|
||||
if (!options.select) {
|
||||
// Only add relations if using default select
|
||||
qb.leftJoinAndSelect('credential.shared', 'shared')
|
||||
.leftJoinAndSelect('shared.project', 'project')
|
||||
.leftJoinAndSelect('project.projectRelations', 'projectRelations');
|
||||
|
||||
@@ -10,7 +10,7 @@ export { AuthIdentityRepository } from './auth-identity.repository';
|
||||
export { AuthProviderSyncHistoryRepository } from './auth-provider-sync-history.repository';
|
||||
export { BaseRepository } from './base-repository';
|
||||
export { BinaryDataRepository } from './binary-data.repository';
|
||||
export { CredentialsRepository } from './credentials.repository';
|
||||
export { CredentialsRepository, type CredentialSharingRelation } from './credentials.repository';
|
||||
export { CredentialDependencyRepository } from './credential-dependency.repository';
|
||||
export {
|
||||
DeploymentKeyRepository,
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
ICredentialsDb,
|
||||
ScopesField,
|
||||
OperationContext,
|
||||
CredentialSharingRelation,
|
||||
} from '@n8n/db';
|
||||
import { Service } from '@n8n/di';
|
||||
import { hasGlobalScope, PROJECT_OWNER_ROLE_SLUG, type Scope } from '@n8n/permissions';
|
||||
@@ -108,7 +109,9 @@ type PrepareUpdateDataOptions = {
|
||||
};
|
||||
|
||||
type GetManyOptions = {
|
||||
listQueryOptions?: ListQuery.Options;
|
||||
listQueryOptions?: ListQuery.Options & {
|
||||
relations?: CredentialSharingRelation[];
|
||||
};
|
||||
includeScopes?: boolean;
|
||||
includeData?: boolean;
|
||||
onlySharedWithMe?: boolean;
|
||||
|
||||
@@ -131,6 +131,8 @@ const credentialsHandlers: CredentialsHandlers = {
|
||||
take: limit,
|
||||
skip: offset,
|
||||
sortBy: 'createdAt:desc',
|
||||
// skip eager-loading shared.project.projectRelations to avoid query fan-out
|
||||
relations: ['shared', 'shared.project'],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -374,6 +374,25 @@ describe('GET /credentials', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('should return correct shared info for a credential in a team project with multiple members', async () => {
|
||||
const teamProject = await createTeamProject('multi-member-project', owner);
|
||||
await linkUserToProject(member, teamProject, 'project:editor');
|
||||
const saved = await saveCredential(dbCredential(), { project: teamProject });
|
||||
|
||||
const response = await authOwnerAgent.get('/credentials');
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
const item = response.body.data.find((c: { id: string }) => c.id === saved.id);
|
||||
expect(item).toBeDefined();
|
||||
expect(item.shared).toEqual([
|
||||
expect.objectContaining({
|
||||
id: teamProject.id,
|
||||
name: teamProject.name,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test('should return empty list when no credentials exist', async () => {
|
||||
const response = await authOwnerAgent.get('/credentials');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user