Skip to content

Commit fb3fe5f

Browse files
authored
feat: Add projects to credentials list response (#25384)
1 parent a33f33d commit fb3fe5f

7 files changed

Lines changed: 184 additions & 7 deletions

File tree

packages/cli/src/public-api/v1/handlers/credentials/__tests__/credentials.service.test.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,79 @@ import type { GenericValue, IDataObject, INodeProperties } from 'n8n-workflow';
99
import { CredentialsService } from '@/credentials/credentials.service';
1010
import type { IDependency } from '@/public-api/types';
1111

12-
import { toJsonSchema, updateCredential } from '../credentials.service';
12+
import { buildSharedForCredential, toJsonSchema, updateCredential } from '../credentials.service';
1313

1414
// Set up real Cipher with mocked InstanceSettings for encryption
1515
const cipher = new Cipher(mock<InstanceSettings>({ encryptionKey: 'test-encryption-key' }));
1616
Container.set(Cipher, cipher);
1717

1818
describe('CredentialsService', () => {
19+
describe('buildSharedForCredential', () => {
20+
it('returns one shared entry when credential is shared with one project', () => {
21+
const createdAt = new Date('2024-01-01T00:00:00.000Z');
22+
const updatedAt = new Date('2024-01-02T00:00:00.000Z');
23+
const credential = {
24+
shared: [
25+
{
26+
role: 'credential:owner',
27+
createdAt,
28+
updatedAt,
29+
project: { id: 'proj-1', name: 'My Project' },
30+
},
31+
],
32+
} as unknown as CredentialsEntity;
33+
expect(buildSharedForCredential(credential)).toEqual([
34+
{
35+
id: 'proj-1',
36+
name: 'My Project',
37+
role: 'credential:owner',
38+
createdAt,
39+
updatedAt,
40+
},
41+
]);
42+
});
43+
44+
it('returns multiple shared entries and skips shared entries without project', () => {
45+
const createdAt1 = new Date('2024-01-01T00:00:00.000Z');
46+
const updatedAt1 = new Date('2024-01-02T00:00:00.000Z');
47+
const createdAt2 = new Date('2024-02-01T00:00:00.000Z');
48+
const updatedAt2 = new Date('2024-02-02T00:00:00.000Z');
49+
const credential = {
50+
shared: [
51+
{
52+
role: 'credential:owner',
53+
createdAt: createdAt1,
54+
updatedAt: updatedAt1,
55+
project: { id: 'proj-1', name: 'Project One' },
56+
},
57+
{ role: 'credential:user', createdAt: createdAt2, updatedAt: updatedAt2, project: null },
58+
{
59+
role: 'credential:user',
60+
createdAt: createdAt2,
61+
updatedAt: updatedAt2,
62+
project: { id: 'proj-2', name: 'Project Two' },
63+
},
64+
],
65+
} as unknown as CredentialsEntity;
66+
expect(buildSharedForCredential(credential)).toEqual([
67+
{
68+
id: 'proj-1',
69+
name: 'Project One',
70+
role: 'credential:owner',
71+
createdAt: createdAt1,
72+
updatedAt: updatedAt1,
73+
},
74+
{
75+
id: 'proj-2',
76+
name: 'Project Two',
77+
role: 'credential:user',
78+
createdAt: createdAt2,
79+
updatedAt: updatedAt2,
80+
},
81+
]);
82+
});
83+
});
84+
1985
describe('toJsonSchema', () => {
2086
it('should create separate conditionals for different values of the same dependant field', () => {
2187
// This test simulates the JWT auth credential scenario where

packages/cli/src/public-api/v1/handlers/credentials/credentials.handler.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
validCredentialsPropertiesForUpdate,
1919
} from './credentials.middleware';
2020
import {
21+
buildSharedForCredential,
2122
CredentialsIsNotUpdatableError,
2223
getCredentials,
2324
getSharedCredentials,
@@ -45,17 +46,41 @@ export = {
4546
req: CredentialRequest.GetAll,
4647
res: express.Response,
4748
): Promise<
48-
express.Response<{ data: Partial<CredentialsEntity>[]; nextCursor: string | null }>
49+
express.Response<{
50+
data: Array<{
51+
id: string;
52+
name: string;
53+
type: string;
54+
createdAt: Date;
55+
updatedAt: Date;
56+
shared: ReturnType<typeof buildSharedForCredential>;
57+
}>;
58+
nextCursor: string | null;
59+
}>
4960
> => {
5061
const offset = Number(req.query.offset) || 0;
5162
const limit = Math.min(Number(req.query.limit) || 100, 250);
5263

53-
const [credentials, count] = await Container.get(CredentialsRepository).findManyAndCount({
64+
const repo = Container.get(CredentialsRepository);
65+
const [credentials, count] = await repo.findAndCount({
5466
take: limit,
5567
skip: offset,
68+
select: ['id', 'name', 'type', 'createdAt', 'updatedAt'],
69+
relations: ['shared', 'shared.project'],
70+
order: { createdAt: 'DESC' },
5671
});
5772

58-
const data = sanitizeCredentials(credentials);
73+
const data = credentials.map((credential: CredentialsEntity) => {
74+
const shared = buildSharedForCredential(credential);
75+
return {
76+
id: credential.id,
77+
name: credential.name,
78+
type: credential.type,
79+
createdAt: credential.createdAt,
80+
updatedAt: credential.updatedAt,
81+
shared,
82+
};
83+
});
5984

6085
return res.json({
6186
data,

packages/cli/src/public-api/v1/handlers/credentials/credentials.service.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,37 @@ import type { IDependency, IJsonSchema } from '../../../types';
2727

2828
export class CredentialsIsNotUpdatableError extends BaseError {}
2929

30+
/**
31+
* Shared entry for credential list: project id/name plus sharing role and timestamps.
32+
* Derived from credential.shared (SharedCredentials + Project), limited to these fields.
33+
*/
34+
export type CredentialListSharedItem = {
35+
id: string;
36+
name: string;
37+
role: string;
38+
createdAt: Date;
39+
updatedAt: Date;
40+
};
41+
42+
/**
43+
* Build the shared array for a credential list item from credential.shared.
44+
* Each entry has id, name from the project and role, createdAt, updatedAt from the shared relation.
45+
*/
46+
export function buildSharedForCredential(
47+
credential: CredentialsEntity,
48+
): CredentialListSharedItem[] {
49+
const shared = credential.shared;
50+
return shared
51+
.filter((sh) => typeof sh.project?.id === 'string')
52+
.map((sh) => ({
53+
id: sh.project.id,
54+
name: sh.project.name,
55+
role: sh.role,
56+
createdAt: sh.createdAt,
57+
updatedAt: sh.updatedAt,
58+
}));
59+
}
60+
3061
export async function getCredentials(credentialId: string): Promise<ICredentialsDb | null> {
3162
return await Container.get(CredentialsRepository).findOneBy({ id: credentialId });
3263
}

packages/cli/src/public-api/v1/handlers/credentials/spec/schemas/credentialList.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ properties:
33
data:
44
type: array
55
items:
6-
$ref: './create-credential-response.yml'
6+
$ref: './credentialListItem.yml'
77
nextCursor:
88
type: string
99
description: Paginate through credentials by setting the cursor parameter to a nextCursor attribute returned by a previous request. Default value fetches the first "page" of the collection.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
allOf:
2+
- $ref: './create-credential-response.yml'
3+
- type: object
4+
required:
5+
- shared
6+
properties:
7+
shared:
8+
type: array
9+
description: Shared entries (project id, name, role, createdAt, updatedAt) from the credential's shared relation
10+
items:
11+
$ref: './credentialSharedItem.yml'
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
type: object
2+
required:
3+
- id
4+
- name
5+
- role
6+
- createdAt
7+
- updatedAt
8+
properties:
9+
id:
10+
type: string
11+
description: Project ID
12+
name:
13+
type: string
14+
description: Project name
15+
role:
16+
type: string
17+
description: Role of the credential in this project (e.g. credential:owner)
18+
createdAt:
19+
type: string
20+
format: date-time
21+
description: When the credential was shared with this project
22+
updatedAt:
23+
type: string
24+
format: date-time
25+
description: When the sharing was last updated

packages/cli/test/integration/public-api/credentials.test.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,14 +190,33 @@ describe('GET /credentials', () => {
190190
expect(Array.isArray(response.body.data)).toBe(true);
191191
expect(response.body.data.length).toBe(2);
192192

193+
const allowedListItemKeys = ['createdAt', 'id', 'name', 'shared', 'type', 'updatedAt'];
193194
response.body.data.forEach((item: Record<string, unknown>) => {
195+
expect(Object.keys(item).sort()).toEqual(allowedListItemKeys);
194196
expect(item).toHaveProperty('id');
195197
expect(item).toHaveProperty('name');
196198
expect(item).toHaveProperty('type');
197199
expect(item).toHaveProperty('createdAt');
198200
expect(item).toHaveProperty('updatedAt');
199-
expect(item).not.toHaveProperty('data');
200-
expect(item).not.toHaveProperty('shared');
201+
expect(item).toHaveProperty('shared');
202+
expect(Array.isArray((item as { shared: unknown }).shared)).toBe(true);
203+
(
204+
item as {
205+
shared: {
206+
id: string;
207+
name: string;
208+
role: string;
209+
createdAt: string;
210+
updatedAt: string;
211+
}[];
212+
}
213+
).shared.forEach((entry) => {
214+
expect(entry).toHaveProperty('id');
215+
expect(entry).toHaveProperty('name');
216+
expect(entry).toHaveProperty('role');
217+
expect(entry).toHaveProperty('createdAt');
218+
expect(entry).toHaveProperty('updatedAt');
219+
});
201220
});
202221
expect(response.body.data).toContainEqual(
203222
expect.objectContaining({ id: saved1.id, name: saved1.name }),

0 commit comments

Comments
 (0)