Skip to content

Commit 659ca26

Browse files
fix(core): Change VariablesService to DI and use caching (n8n-io#6827)
* support redis cluster * cleanup, fix config schema * set default prefix to bull * initial commit * improve logging * improve types and refactor * list support and refactor * fix redis service and tests * add comment * add redis and cache prefix * use injection * lint fix * clean schema comments * improve naming, tests, cluster client * merge master * cache returns unknown instead of T * update cache service, tests and doc * remove console.log * VariablesService as DI, add caching, fix tests * do not cache null or undefined values * import fix * more DI and remove collections * fix merge * lint fix * rename to ~Cached * fix test for CI * fix ActiveWorkflowRunner test
1 parent 41d8a18 commit 659ca26

File tree

11 files changed

+99
-51
lines changed

11 files changed

+99
-51
lines changed

packages/cli/src/WorkflowHelpers.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { isWorkflowIdValid } from './utils';
3737
import { UserService } from './user/user.service';
3838
import type { SharedWorkflow } from '@db/entities/SharedWorkflow';
3939
import type { RoleNames } from '@db/entities/Role';
40+
import { VariablesService } from './environments/variables/variables.service';
4041

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

@@ -571,8 +572,9 @@ export function validateWorkflowCredentialUsage(
571572
}
572573

573574
export async function getVariables(): Promise<IDataObject> {
575+
const variables = await Container.get(VariablesService).getAllCached();
574576
return Object.freeze(
575-
(await Db.collections.Variables.find()).reduce((prev, curr) => {
577+
variables.reduce((prev, curr) => {
576578
prev[curr.key] = curr.value;
577579
return prev;
578580
}, {} as IDataObject),

packages/cli/src/environments/sourceControl/sourceControlExport.service.ee.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
2626
import { In } from 'typeorm';
2727
import type { SourceControlledFile } from './types/sourceControlledFile';
28+
import { VariablesService } from '../variables/variables.service';
2829

2930
@Service()
3031
export class SourceControlExportService {
@@ -34,7 +35,7 @@ export class SourceControlExportService {
3435

3536
private credentialExportFolder: string;
3637

37-
constructor() {
38+
constructor(private readonly variablesService: VariablesService) {
3839
const userFolder = UserSettings.getUserN8nFolderPath();
3940
this.gitFolder = path.join(userFolder, SOURCE_CONTROL_GIT_FOLDER);
4041
this.workflowExportFolder = path.join(this.gitFolder, SOURCE_CONTROL_WORKFLOW_EXPORT_FOLDER);
@@ -136,7 +137,7 @@ export class SourceControlExportService {
136137
async exportVariablesToWorkFolder(): Promise<ExportResult> {
137138
try {
138139
sourceControlFoldersExistCheck([this.gitFolder]);
139-
const variables = await Db.collections.Variables.find();
140+
const variables = await this.variablesService.getAllCached();
140141
// do not export empty variables
141142
if (variables.length === 0) {
142143
return {

packages/cli/src/environments/sourceControl/sourceControlImport.service.ee.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Container, Service } from 'typedi';
1+
import { Service } from 'typedi';
22
import path from 'path';
33
import {
44
SOURCE_CONTROL_CREDENTIAL_EXPORT_FOLDER,
@@ -25,6 +25,7 @@ import { isUniqueConstraintError } from '@/ResponseHelper';
2525
import type { SourceControlWorkflowVersionId } from './types/sourceControlWorkflowVersionId';
2626
import { getCredentialExportPath, getWorkflowExportPath } from './sourceControlHelper.ee';
2727
import type { SourceControlledFile } from './types/sourceControlledFile';
28+
import { VariablesService } from '../variables/variables.service';
2829

2930
@Service()
3031
export class SourceControlImportService {
@@ -34,7 +35,10 @@ export class SourceControlImportService {
3435

3536
private credentialExportFolder: string;
3637

37-
constructor() {
38+
constructor(
39+
private readonly variablesService: VariablesService,
40+
private readonly activeWorkflowRunner: ActiveWorkflowRunner,
41+
) {
3842
const userFolder = UserSettings.getUserN8nFolderPath();
3943
this.gitFolder = path.join(userFolder, SOURCE_CONTROL_GIT_FOLDER);
4044
this.workflowExportFolder = path.join(this.gitFolder, SOURCE_CONTROL_WORKFLOW_EXPORT_FOLDER);
@@ -240,10 +244,7 @@ export class SourceControlImportService {
240244
}
241245

242246
public async getLocalVariablesFromDb(): Promise<Variables[]> {
243-
const localVariables = await Db.collections.Variables.find({
244-
select: ['id', 'key', 'type', 'value'],
245-
});
246-
return localVariables;
247+
return this.variablesService.getAllCached();
247248
}
248249

249250
public async getRemoteTagsAndMappingsFromFile(): Promise<{
@@ -280,7 +281,7 @@ export class SourceControlImportService {
280281

281282
public async importWorkflowFromWorkFolder(candidates: SourceControlledFile[], userId: string) {
282283
const ownerWorkflowRole = await this.getOwnerWorkflowRole();
283-
const workflowRunner = Container.get(ActiveWorkflowRunner);
284+
const workflowRunner = this.activeWorkflowRunner;
284285
const candidateIds = candidates.map((c) => c.id);
285286
const existingWorkflows = await Db.collections.Workflow.find({
286287
where: {
@@ -581,6 +582,8 @@ export class SourceControlImportService {
581582
}
582583
}
583584

585+
await this.variablesService.updateCache();
586+
584587
return result;
585588
}
586589
}

packages/cli/src/environments/variables/variables.controller.ee.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
VariablesValidationError,
1010
} from './variables.service.ee';
1111
import { isVariablesEnabled } from './enviromentHelpers';
12+
import Container from 'typedi';
1213

1314
// eslint-disable-next-line @typescript-eslint/naming-convention
1415
export const EEVariablesController = express.Router();
@@ -37,7 +38,7 @@ EEVariablesController.post(
3738
const variable = req.body;
3839
delete variable.id;
3940
try {
40-
return await EEVariablesService.create(variable);
41+
return await Container.get(EEVariablesService).create(variable);
4142
} catch (error) {
4243
if (error instanceof VariablesLicenseError) {
4344
throw new ResponseHelper.BadRequestError(error.message);
@@ -63,7 +64,7 @@ EEVariablesController.patch(
6364
const variable = req.body;
6465
delete variable.id;
6566
try {
66-
return await EEVariablesService.update(id, variable);
67+
return await Container.get(EEVariablesService).update(id, variable);
6768
} catch (error) {
6869
if (error instanceof VariablesLicenseError) {
6970
throw new ResponseHelper.BadRequestError(error.message);

packages/cli/src/environments/variables/variables.controller.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as ResponseHelper from '@/ResponseHelper';
66
import type { VariablesRequest } from '@/requests';
77
import { VariablesService } from './variables.service';
88
import { EEVariablesController } from './variables.controller.ee';
9+
import Container from 'typedi';
910

1011
export const variablesController = express.Router();
1112

@@ -28,7 +29,7 @@ variablesController.use(EEVariablesController);
2829
variablesController.get(
2930
'/',
3031
ResponseHelper.send(async () => {
31-
return VariablesService.getAll();
32+
return Container.get(VariablesService).getAllCached();
3233
}),
3334
);
3435

@@ -43,7 +44,7 @@ variablesController.get(
4344
'/:id(\\w+)',
4445
ResponseHelper.send(async (req: VariablesRequest.Get) => {
4546
const id = req.params.id;
46-
const variable = await VariablesService.get(id);
47+
const variable = await Container.get(VariablesService).getCached(id);
4748
if (variable === null) {
4849
throw new ResponseHelper.NotFoundError(`Variable with id ${req.params.id} not found`);
4950
}
@@ -69,7 +70,7 @@ variablesController.delete(
6970
});
7071
throw new ResponseHelper.AuthError('Unauthorized');
7172
}
72-
await VariablesService.delete(id);
73+
await Container.get(VariablesService).delete(id);
7374

7475
return true;
7576
}),
Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { Container } from 'typedi';
1+
import { Container, Service } from 'typedi';
22
import type { Variables } from '@db/entities/Variables';
3-
import { collections } from '@/Db';
43
import { InternalHooks } from '@/InternalHooks';
54
import { generateNanoId } from '@db/utils/generators';
65
import { canCreateNewVariable } from './enviromentHelpers';
@@ -9,12 +8,9 @@ import { VariablesService } from './variables.service';
98
export class VariablesLicenseError extends Error {}
109
export class VariablesValidationError extends Error {}
1110

11+
@Service()
1212
export class EEVariablesService extends VariablesService {
13-
static async getCount(): Promise<number> {
14-
return collections.Variables.count();
15-
}
16-
17-
static validateVariable(variable: Omit<Variables, 'id'>): void {
13+
validateVariable(variable: Omit<Variables, 'id'>): void {
1814
if (variable.key.length > 50) {
1915
throw new VariablesValidationError('key cannot be longer than 50 characters');
2016
}
@@ -26,23 +22,25 @@ export class EEVariablesService extends VariablesService {
2622
}
2723
}
2824

29-
static async create(variable: Omit<Variables, 'id'>): Promise<Variables> {
25+
async create(variable: Omit<Variables, 'id'>): Promise<Variables> {
3026
if (!canCreateNewVariable(await this.getCount())) {
3127
throw new VariablesLicenseError('Variables limit reached');
3228
}
3329
this.validateVariable(variable);
3430

3531
void Container.get(InternalHooks).onVariableCreated({ variable_type: variable.type });
36-
return collections.Variables.save({
32+
const saveResult = await this.variablesRepository.save({
3733
...variable,
3834
id: generateNanoId(),
3935
});
36+
await this.updateCache();
37+
return saveResult;
4038
}
4139

42-
static async update(id: string, variable: Omit<Variables, 'id'>): Promise<Variables> {
40+
async update(id: string, variable: Omit<Variables, 'id'>): Promise<Variables> {
4341
this.validateVariable(variable);
44-
await collections.Variables.update(id, variable);
45-
46-
return (await this.get(id))!;
42+
await this.variablesRepository.update(id, variable);
43+
await this.updateCache();
44+
return (await this.getCached(id))!;
4745
}
4846
}
Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,53 @@
11
import type { Variables } from '@db/entities/Variables';
2-
import { collections } from '@/Db';
2+
import { CacheService } from '@/services/cache.service';
3+
import Container, { Service } from 'typedi';
4+
import { VariablesRepository } from '@/databases/repositories';
5+
import type { DeepPartial } from 'typeorm';
36

7+
@Service()
48
export class VariablesService {
5-
static async getAll(): Promise<Variables[]> {
6-
return collections.Variables.find();
9+
constructor(
10+
protected cacheService: CacheService,
11+
protected variablesRepository: VariablesRepository,
12+
) {}
13+
14+
async getAllCached(): Promise<Variables[]> {
15+
const variables = await this.cacheService.get('variables', {
16+
async refreshFunction() {
17+
// TODO: log refresh cache metric
18+
return Container.get(VariablesService).findAll();
19+
},
20+
});
21+
return (variables as Array<DeepPartial<Variables>>).map((v) =>
22+
this.variablesRepository.create(v),
23+
);
24+
}
25+
26+
async getCount(): Promise<number> {
27+
return (await this.getAllCached()).length;
28+
}
29+
30+
async getCached(id: string): Promise<Variables | null> {
31+
const variables = await this.getAllCached();
32+
const foundVariable = variables.find((variable) => variable.id === id);
33+
if (!foundVariable) {
34+
return null;
35+
}
36+
return this.variablesRepository.create(foundVariable as DeepPartial<Variables>);
737
}
838

9-
static async getCount(): Promise<number> {
10-
return collections.Variables.count();
39+
async delete(id: string): Promise<void> {
40+
await this.variablesRepository.delete(id);
41+
await this.updateCache();
1142
}
1243

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

17-
static async delete(id: string): Promise<void> {
18-
await collections.Variables.delete(id);
50+
async findAll(): Promise<Variables[]> {
51+
return this.variablesRepository.find();
1952
}
2053
}

packages/cli/test/integration/shared/testDb.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import type {
3434
} from './types';
3535
import type { ExecutionData } from '@db/entities/ExecutionData';
3636
import { generateNanoId } from '@db/utils/generators';
37+
import { VariablesService } from '@/environments/variables/variables.service';
3738

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

@@ -514,11 +515,13 @@ export async function getWorkflowSharing(workflow: WorkflowEntity) {
514515
// ----------------------------------
515516

516517
export async function createVariable(key: string, value: string) {
517-
return Db.collections.Variables.save({
518+
const result = await Db.collections.Variables.save({
518519
id: generateNanoId(),
519520
key,
520521
value,
521522
});
523+
await Container.get(VariablesService).updateCache();
524+
return result;
522525
}
523526

524527
export async function getVariableByKey(key: string) {

packages/cli/test/integration/variables.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ describe('POST /variables', () => {
9898
});
9999
const toCreate = generatePayload();
100100

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

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

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

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

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

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

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

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

320-
test('should not delete a single credential for a member', async () => {
320+
test('should not delete a single variable for a member', async () => {
321321
const [var1, var2, var3] = await Promise.all([
322322
testDb.createVariable('test1', 'value1'),
323323
testDb.createVariable('test2', 'value2'),

0 commit comments

Comments
 (0)