Skip to content

Commit

Permalink
fix(core): Use cached value in retrieval of personal project owner (#…
Browse files Browse the repository at this point in the history
…11533)

Co-authored-by: Danny Martini <danny@n8n.io>
  • Loading branch information
ivov and despairblue authored Nov 7, 2024
1 parent 5e6ec3b commit 04029d8
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 21 deletions.
49 changes: 38 additions & 11 deletions packages/cli/src/services/__tests__/ownership.service.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { mock } from 'jest-mock-extended';
import { v4 as uuid } from 'uuid';

import { Project } from '@/databases/entities/project';
import { ProjectRelation } from '@/databases/entities/project-relation';
Expand All @@ -13,12 +14,15 @@ import { OwnershipService } from '@/services/ownership.service';
import { mockCredential, mockProject } from '@test/mock-objects';
import { mockInstance } from '@test/mocking';

import { CacheService } from '../cache/cache.service';

describe('OwnershipService', () => {
const userRepository = mockInstance(UserRepository);
const sharedWorkflowRepository = mockInstance(SharedWorkflowRepository);
const projectRelationRepository = mockInstance(ProjectRelationRepository);
const cacheService = mockInstance(CacheService);
const ownershipService = new OwnershipService(
mock(),
cacheService,
userRepository,
mock(),
projectRelationRepository,
Expand Down Expand Up @@ -52,22 +56,22 @@ describe('OwnershipService', () => {
});
});

describe('getProjectOwnerCached()', () => {
describe('getPersonalProjectOwnerCached()', () => {
test('should retrieve a project owner', async () => {
const mockProject = new Project();
const mockOwner = new User();

const projectRelation = Object.assign(new ProjectRelation(), {
role: 'project:personalOwner',
project: mockProject,
user: mockOwner,
});
// ARRANGE
const project = new Project();
const owner = new User();
const projectRelation = new ProjectRelation();
projectRelation.role = 'project:personalOwner';
(projectRelation.project = project), (projectRelation.user = owner);

projectRelationRepository.getPersonalProjectOwners.mockResolvedValueOnce([projectRelation]);

// ACT
const returnedOwner = await ownershipService.getPersonalProjectOwnerCached('some-project-id');

expect(returnedOwner).toBe(mockOwner);
// ASSERT
expect(returnedOwner).toBe(owner);
});

test('should not throw if no project owner found, should return null instead', async () => {
Expand All @@ -77,6 +81,29 @@ describe('OwnershipService', () => {

expect(owner).toBeNull();
});

test('should not use the repository if the owner was found in the cache', async () => {
// ARRANGE
const project = new Project();
project.id = uuid();
const owner = new User();
owner.id = uuid();
const projectRelation = new ProjectRelation();
projectRelation.role = 'project:personalOwner';
(projectRelation.project = project), (projectRelation.user = owner);

cacheService.getHashValue.mockResolvedValueOnce(owner);
userRepository.create.mockReturnValueOnce(owner);

// ACT
const foundOwner = await ownershipService.getPersonalProjectOwnerCached(project.id);

// ASSERT
expect(cacheService.getHashValue).toHaveBeenCalledTimes(1);
expect(cacheService.getHashValue).toHaveBeenCalledWith('project-owner', project.id);
expect(projectRelationRepository.getPersonalProjectOwners).not.toHaveBeenCalled();
expect(foundOwner).toEqual(owner);
});
});

describe('getProjectOwnerCached()', () => {
Expand Down
8 changes: 2 additions & 6 deletions packages/cli/src/services/ownership.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,9 @@ export class OwnershipService {
* Personal project ownership is **immutable**.
*/
async getPersonalProjectOwnerCached(projectId: string): Promise<User | null> {
const cachedValue = await this.cacheService.getHashValue<User | null>(
'project-owner',
projectId,
);
const cachedValue = await this.cacheService.getHashValue<User>('project-owner', projectId);

if (cachedValue) this.userRepository.create(cachedValue);
if (cachedValue === null) return null;
if (cachedValue) return this.userRepository.create(cachedValue);

const ownerRel = await this.projectRelationRepository.getPersonalProjectOwners([projectId]);
const owner = ownerRel[0]?.user ?? null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { GlobalConfig } from '@n8n/config';
import { type Workflow, type INode, type WorkflowSettings } from 'n8n-workflow';
import { strict as assert } from 'node:assert';
import { Service } from 'typedi';

import type { Project } from '@/databases/entities/project';
Expand Down Expand Up @@ -68,11 +67,9 @@ export class SubworkflowPolicyChecker {

const owner = await this.ownershipService.getPersonalProjectOwnerCached(subworkflowProject.id);

assert(owner !== null); // only `null` if not personal

return {
hasReadAccess,
ownerName: owner.firstName + ' ' + owner.lastName,
ownerName: owner ? owner.firstName + ' ' + owner.lastName : 'No owner (team project)',
};
}

Expand Down

0 comments on commit 04029d8

Please sign in to comment.