Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): Change VariablesService to DI and use caching #6827

Merged
merged 36 commits into from
Aug 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
765ebd2
support redis cluster
flipswitchingmonkey Jul 19, 2023
492e8a6
cleanup, fix config schema
flipswitchingmonkey Jul 20, 2023
4d6b64a
set default prefix to bull
flipswitchingmonkey Jul 20, 2023
76e1e6b
Merge branch 'master' into pay-639-make-redis-client-into-a-service-f…
flipswitchingmonkey Jul 20, 2023
8b924b4
initial commit
flipswitchingmonkey Jul 20, 2023
9686c8d
improve logging
flipswitchingmonkey Jul 20, 2023
ffc70dd
improve types and refactor
flipswitchingmonkey Jul 21, 2023
15bb8ca
list support and refactor
flipswitchingmonkey Jul 21, 2023
ec08e9a
Merge branch 'master' into pay-639-make-redis-client-into-a-service-f…
flipswitchingmonkey Jul 25, 2023
0e584eb
fix redis service and tests
flipswitchingmonkey Jul 25, 2023
1fa934d
add comment
flipswitchingmonkey Jul 25, 2023
16e2b8a
add redis and cache prefix
flipswitchingmonkey Jul 26, 2023
364e0ec
use injection
flipswitchingmonkey Jul 26, 2023
6bff179
lint fix
flipswitchingmonkey Jul 26, 2023
5bfabdd
Merge branch 'master' into pay-639-make-redis-client-into-a-service-f…
flipswitchingmonkey Jul 26, 2023
331d433
clean schema comments
flipswitchingmonkey Jul 26, 2023
f7f83fd
improve naming, tests, cluster client
flipswitchingmonkey Jul 31, 2023
d56317c
Merge branch 'master' into pay-639-make-redis-client-into-a-service-f…
flipswitchingmonkey Jul 31, 2023
edec7c6
merge master
flipswitchingmonkey Jul 31, 2023
0ced012
cache returns unknown instead of T
flipswitchingmonkey Jul 31, 2023
1a5b761
update cache service, tests and doc
flipswitchingmonkey Aug 1, 2023
86f7eaa
Merge branch 'master' into pay-639-make-redis-client-into-a-service-f…
flipswitchingmonkey Aug 1, 2023
ef7a266
remove console.log
flipswitchingmonkey Aug 1, 2023
5b85ec0
VariablesService as DI, add caching, fix tests
flipswitchingmonkey Aug 1, 2023
69dbe17
do not cache null or undefined values
flipswitchingmonkey Aug 1, 2023
abbb6f4
import fix
flipswitchingmonkey Aug 2, 2023
d66162c
more DI and remove collections
flipswitchingmonkey Aug 2, 2023
acbbfdb
Merge branch 'master' into pay-639-make-redis-client-into-a-service-f…
flipswitchingmonkey Aug 2, 2023
659bbe7
fix merge
flipswitchingmonkey Aug 2, 2023
2298c3c
Merge branch 'master' into pay-639-make-redis-client-into-a-service-f…
flipswitchingmonkey Aug 2, 2023
056de7d
lint fix
flipswitchingmonkey Aug 2, 2023
dc30d03
Merge branch 'pay-639-make-redis-client-into-a-service-for-injection'…
flipswitchingmonkey Aug 2, 2023
3212d8a
Merge branch 'master' into pay-644-variables-loading-might-be-slow
flipswitchingmonkey Aug 2, 2023
8d3b2cb
rename to ~Cached
flipswitchingmonkey Aug 2, 2023
b15630e
fix test for CI
flipswitchingmonkey Aug 2, 2023
ae8fa2b
fix ActiveWorkflowRunner test
flipswitchingmonkey Aug 2, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/cli/src/WorkflowHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { isWorkflowIdValid } from './utils';
import { UserService } from './user/user.service';
import type { SharedWorkflow } from '@db/entities/SharedWorkflow';
import type { RoleNames } from '@db/entities/Role';
import { VariablesService } from './environments/variables/variables.service';

const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');

Expand Down Expand Up @@ -571,8 +572,9 @@ export function validateWorkflowCredentialUsage(
}

export async function getVariables(): Promise<IDataObject> {
const variables = await Container.get(VariablesService).getAllCached();
return Object.freeze(
(await Db.collections.Variables.find()).reduce((prev, curr) => {
variables.reduce((prev, curr) => {
prev[curr.key] = curr.value;
return prev;
}, {} as IDataObject),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { In } from 'typeorm';
import type { SourceControlledFile } from './types/sourceControlledFile';
import { VariablesService } from '../variables/variables.service';

@Service()
export class SourceControlExportService {
Expand All @@ -34,7 +35,7 @@ export class SourceControlExportService {

private credentialExportFolder: string;

constructor() {
constructor(private readonly variablesService: VariablesService) {
const userFolder = UserSettings.getUserN8nFolderPath();
this.gitFolder = path.join(userFolder, SOURCE_CONTROL_GIT_FOLDER);
this.workflowExportFolder = path.join(this.gitFolder, SOURCE_CONTROL_WORKFLOW_EXPORT_FOLDER);
Expand Down Expand Up @@ -136,7 +137,7 @@ export class SourceControlExportService {
async exportVariablesToWorkFolder(): Promise<ExportResult> {
try {
sourceControlFoldersExistCheck([this.gitFolder]);
const variables = await Db.collections.Variables.find();
const variables = await this.variablesService.getAllCached();
// do not export empty variables
if (variables.length === 0) {
return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Container, Service } from 'typedi';
import { Service } from 'typedi';
import path from 'path';
import {
SOURCE_CONTROL_CREDENTIAL_EXPORT_FOLDER,
Expand All @@ -25,6 +25,7 @@ import { isUniqueConstraintError } from '@/ResponseHelper';
import type { SourceControlWorkflowVersionId } from './types/sourceControlWorkflowVersionId';
import { getCredentialExportPath, getWorkflowExportPath } from './sourceControlHelper.ee';
import type { SourceControlledFile } from './types/sourceControlledFile';
import { VariablesService } from '../variables/variables.service';

@Service()
export class SourceControlImportService {
Expand All @@ -34,7 +35,10 @@ export class SourceControlImportService {

private credentialExportFolder: string;

constructor() {
constructor(
private readonly variablesService: VariablesService,
private readonly activeWorkflowRunner: ActiveWorkflowRunner,
) {
const userFolder = UserSettings.getUserN8nFolderPath();
this.gitFolder = path.join(userFolder, SOURCE_CONTROL_GIT_FOLDER);
this.workflowExportFolder = path.join(this.gitFolder, SOURCE_CONTROL_WORKFLOW_EXPORT_FOLDER);
Expand Down Expand Up @@ -240,10 +244,7 @@ export class SourceControlImportService {
}

public async getLocalVariablesFromDb(): Promise<Variables[]> {
const localVariables = await Db.collections.Variables.find({
select: ['id', 'key', 'type', 'value'],
});
return localVariables;
return this.variablesService.getAllCached();
}

public async getRemoteTagsAndMappingsFromFile(): Promise<{
Expand Down Expand Up @@ -280,7 +281,7 @@ export class SourceControlImportService {

public async importWorkflowFromWorkFolder(candidates: SourceControlledFile[], userId: string) {
const ownerWorkflowRole = await this.getOwnerWorkflowRole();
const workflowRunner = Container.get(ActiveWorkflowRunner);
const workflowRunner = this.activeWorkflowRunner;
const candidateIds = candidates.map((c) => c.id);
const existingWorkflows = await Db.collections.Workflow.find({
where: {
Expand Down Expand Up @@ -581,6 +582,8 @@ export class SourceControlImportService {
}
}

await this.variablesService.updateCache();

return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
VariablesValidationError,
} from './variables.service.ee';
import { isVariablesEnabled } from './enviromentHelpers';
import Container from 'typedi';

// eslint-disable-next-line @typescript-eslint/naming-convention
export const EEVariablesController = express.Router();
Expand Down Expand Up @@ -37,7 +38,7 @@ EEVariablesController.post(
const variable = req.body;
delete variable.id;
try {
return await EEVariablesService.create(variable);
return await Container.get(EEVariablesService).create(variable);
ivov marked this conversation as resolved.
Show resolved Hide resolved
} catch (error) {
if (error instanceof VariablesLicenseError) {
throw new ResponseHelper.BadRequestError(error.message);
Expand All @@ -63,7 +64,7 @@ EEVariablesController.patch(
const variable = req.body;
delete variable.id;
try {
return await EEVariablesService.update(id, variable);
return await Container.get(EEVariablesService).update(id, variable);
} catch (error) {
if (error instanceof VariablesLicenseError) {
throw new ResponseHelper.BadRequestError(error.message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as ResponseHelper from '@/ResponseHelper';
import type { VariablesRequest } from '@/requests';
import { VariablesService } from './variables.service';
import { EEVariablesController } from './variables.controller.ee';
import Container from 'typedi';

export const variablesController = express.Router();

Expand All @@ -28,7 +29,7 @@ variablesController.use(EEVariablesController);
variablesController.get(
'/',
ResponseHelper.send(async () => {
return VariablesService.getAll();
return Container.get(VariablesService).getAllCached();
}),
);

Expand All @@ -43,7 +44,7 @@ variablesController.get(
'/:id(\\w+)',
ResponseHelper.send(async (req: VariablesRequest.Get) => {
const id = req.params.id;
const variable = await VariablesService.get(id);
const variable = await Container.get(VariablesService).getCached(id);
if (variable === null) {
throw new ResponseHelper.NotFoundError(`Variable with id ${req.params.id} not found`);
}
Expand All @@ -69,7 +70,7 @@ variablesController.delete(
});
throw new ResponseHelper.AuthError('Unauthorized');
}
await VariablesService.delete(id);
await Container.get(VariablesService).delete(id);

return true;
}),
Expand Down
24 changes: 11 additions & 13 deletions packages/cli/src/environments/variables/variables.service.ee.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Container } from 'typedi';
import { Container, Service } from 'typedi';
import type { Variables } from '@db/entities/Variables';
import { collections } from '@/Db';
import { InternalHooks } from '@/InternalHooks';
import { generateNanoId } from '@db/utils/generators';
import { canCreateNewVariable } from './enviromentHelpers';
Expand All @@ -9,12 +8,9 @@ import { VariablesService } from './variables.service';
export class VariablesLicenseError extends Error {}
export class VariablesValidationError extends Error {}

@Service()
export class EEVariablesService extends VariablesService {
static async getCount(): Promise<number> {
return collections.Variables.count();
}

static validateVariable(variable: Omit<Variables, 'id'>): void {
validateVariable(variable: Omit<Variables, 'id'>): void {
if (variable.key.length > 50) {
throw new VariablesValidationError('key cannot be longer than 50 characters');
}
Expand All @@ -26,23 +22,25 @@ export class EEVariablesService extends VariablesService {
}
}

static async create(variable: Omit<Variables, 'id'>): Promise<Variables> {
async create(variable: Omit<Variables, 'id'>): Promise<Variables> {
if (!canCreateNewVariable(await this.getCount())) {
flipswitchingmonkey marked this conversation as resolved.
Show resolved Hide resolved
throw new VariablesLicenseError('Variables limit reached');
}
this.validateVariable(variable);

void Container.get(InternalHooks).onVariableCreated({ variable_type: variable.type });
return collections.Variables.save({
const saveResult = await this.variablesRepository.save({
...variable,
id: generateNanoId(),
});
await this.updateCache();
return saveResult;
ivov marked this conversation as resolved.
Show resolved Hide resolved
}

static async update(id: string, variable: Omit<Variables, 'id'>): Promise<Variables> {
async update(id: string, variable: Omit<Variables, 'id'>): Promise<Variables> {
this.validateVariable(variable);
flipswitchingmonkey marked this conversation as resolved.
Show resolved Hide resolved
await collections.Variables.update(id, variable);

return (await this.get(id))!;
await this.variablesRepository.update(id, variable);
await this.updateCache();
return (await this.getCached(id))!;
ivov marked this conversation as resolved.
Show resolved Hide resolved
}
}
51 changes: 42 additions & 9 deletions packages/cli/src/environments/variables/variables.service.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,53 @@
import type { Variables } from '@db/entities/Variables';
import { collections } from '@/Db';
import { CacheService } from '@/services/cache.service';
import Container, { Service } from 'typedi';
import { VariablesRepository } from '@/databases/repositories';
import type { DeepPartial } from 'typeorm';

@Service()
export class VariablesService {
static async getAll(): Promise<Variables[]> {
return collections.Variables.find();
constructor(
protected cacheService: CacheService,
protected variablesRepository: VariablesRepository,
) {}

async getAllCached(): Promise<Variables[]> {
const variables = await this.cacheService.get('variables', {
async refreshFunction() {
// TODO: log refresh cache metric
return Container.get(VariablesService).findAll();
},
});
return (variables as Array<DeepPartial<Variables>>).map((v) =>
this.variablesRepository.create(v),
);
}

async getCount(): Promise<number> {
return (await this.getAllCached()).length;
}

async getCached(id: string): Promise<Variables | null> {
const variables = await this.getAllCached();
const foundVariable = variables.find((variable) => variable.id === id);
if (!foundVariable) {
return null;
}
return this.variablesRepository.create(foundVariable as DeepPartial<Variables>);
}

static async getCount(): Promise<number> {
return collections.Variables.count();
async delete(id: string): Promise<void> {
await this.variablesRepository.delete(id);
await this.updateCache();
ivov marked this conversation as resolved.
Show resolved Hide resolved
}

static async get(id: string): Promise<Variables | null> {
return collections.Variables.findOne({ where: { id } });
async updateCache(): Promise<void> {
// TODO: log update cache metric
const variables = await this.findAll();
await this.cacheService.set('variables', variables);
}

static async delete(id: string): Promise<void> {
await collections.Variables.delete(id);
async findAll(): Promise<Variables[]> {
return this.variablesRepository.find();
}
}
5 changes: 4 additions & 1 deletion packages/cli/test/integration/shared/testDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import type {
} from './types';
import type { ExecutionData } from '@db/entities/ExecutionData';
import { generateNanoId } from '@db/utils/generators';
import { VariablesService } from '@/environments/variables/variables.service';

export type TestDBType = 'postgres' | 'mysql';

Expand Down Expand Up @@ -514,11 +515,13 @@ export async function getWorkflowSharing(workflow: WorkflowEntity) {
// ----------------------------------

export async function createVariable(key: string, value: string) {
return Db.collections.Variables.save({
const result = await Db.collections.Variables.save({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd expect VariablesService to offer createVariable() to take care of creating the var and updating the cache.

id: generateNanoId(),
key,
value,
});
await Container.get(VariablesService).updateCache();
return result;
}

export async function getVariableByKey(key: string) {
Expand Down
20 changes: 10 additions & 10 deletions packages/cli/test/integration/variables.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ describe('POST /variables', () => {
});
const toCreate = generatePayload();

test('should create a new credential and return it for an owner', async () => {
test('should create a new variable and return it for an owner', async () => {
const response = await authOwnerAgent.post('/variables').send(toCreate);
expect(response.statusCode).toBe(200);
expect(response.body.data.key).toBe(toCreate.key);
Expand All @@ -118,7 +118,7 @@ describe('POST /variables', () => {
expect(byKey!.value).toBe(toCreate.value);
});

test('should not create a new credential and return it for a member', async () => {
test('should not create a new variable and return it for a member', async () => {
const response = await authMemberAgent.post('/variables').send(toCreate);
expect(response.statusCode).toBe(401);
expect(response.body.data?.key).not.toBe(toCreate.key);
Expand All @@ -128,7 +128,7 @@ describe('POST /variables', () => {
expect(byKey).toBeNull();
});

test("POST /variables should not create a new credential and return it if the instance doesn't have a license", async () => {
test("POST /variables should not create a new variable and return it if the instance doesn't have a license", async () => {
licenseLike.isVariablesEnabled.mockReturnValue(false);
const response = await authOwnerAgent.post('/variables').send(toCreate);
expect(response.statusCode).toBe(400);
Expand All @@ -139,7 +139,7 @@ describe('POST /variables', () => {
expect(byKey).toBeNull();
});

test('should fail to create a new credential and if one with the same key exists', async () => {
test('should fail to create a new variable and if one with the same key exists', async () => {
await testDb.createVariable(toCreate.key, toCreate.value);
const response = await authOwnerAgent.post('/variables').send(toCreate);
expect(response.statusCode).toBe(500);
Expand Down Expand Up @@ -224,7 +224,7 @@ describe('PATCH /variables/:id', () => {
value: 'createvalue1',
};

test('should modify existing credential if use is an owner', async () => {
test('should modify existing variable if use is an owner', async () => {
const variable = await testDb.createVariable('test1', 'value1');
const response = await authOwnerAgent.patch(`/variables/${variable.id}`).send(toModify);
expect(response.statusCode).toBe(200);
Expand All @@ -245,7 +245,7 @@ describe('PATCH /variables/:id', () => {
expect(byKey!.value).toBe(toModify.value);
});

test('should modify existing credential if use is an owner', async () => {
test('should modify existing variable if use is an owner', async () => {
const variable = await testDb.createVariable('test1', 'value1');
const response = await authOwnerAgent.patch(`/variables/${variable.id}`).send(toModify);
expect(response.statusCode).toBe(200);
Expand All @@ -266,7 +266,7 @@ describe('PATCH /variables/:id', () => {
expect(byKey!.value).toBe(toModify.value);
});

test('should not modify existing credential if use is a member', async () => {
test('should not modify existing variable if use is a member', async () => {
const variable = await testDb.createVariable('test1', 'value1');
const response = await authMemberAgent.patch(`/variables/${variable.id}`).send(toModify);
expect(response.statusCode).toBe(401);
Expand All @@ -279,7 +279,7 @@ describe('PATCH /variables/:id', () => {
expect(byId!.value).not.toBe(toModify.value);
});

test('should not modify existing credential if one with the same key exists', async () => {
test('should not modify existing variable if one with the same key exists', async () => {
const [var1, var2] = await Promise.all([
testDb.createVariable('test1', 'value1'),
testDb.createVariable(toModify.key, toModify.value),
Expand All @@ -300,7 +300,7 @@ describe('PATCH /variables/:id', () => {
// DELETE /variables/:id - change a variable
// ----------------------------------------
describe('DELETE /variables/:id', () => {
test('should delete a single credential for an owner', async () => {
test('should delete a single variable for an owner', async () => {
const [var1, var2, var3] = await Promise.all([
testDb.createVariable('test1', 'value1'),
testDb.createVariable('test2', 'value2'),
Expand All @@ -317,7 +317,7 @@ describe('DELETE /variables/:id', () => {
expect(getResponse.body.data.length).toBe(2);
});

test('should not delete a single credential for a member', async () => {
test('should not delete a single variable for a member', async () => {
const [var1, var2, var3] = await Promise.all([
testDb.createVariable('test1', 'value1'),
testDb.createVariable('test2', 'value2'),
Expand Down
Loading
Loading