Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into pay-967-add-tooltip…
Browse files Browse the repository at this point in the history
…s-to-workflow-history-for-free-active-and
  • Loading branch information
netroy committed Aug 27, 2024
2 parents 9d98846 + be52176 commit 20086cf
Show file tree
Hide file tree
Showing 75 changed files with 258 additions and 190 deletions.
63 changes: 35 additions & 28 deletions packages/@n8n/config/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,24 @@ import { Container, Service } from 'typedi';
type Class = Function;
type Constructable<T = unknown> = new (rawValue: string) => T;
type PropertyKey = string | symbol;
type PropertyType = number | boolean | string | Class;
interface PropertyMetadata {
type: unknown;
type: PropertyType;
envName?: string;
}

const globalMetadata = new Map<Class, Map<PropertyKey, PropertyMetadata>>();

const readEnv = (envName: string) => {
if (envName in process.env) return process.env[envName];

// Read the value from a file, if "_FILE" environment variable is defined
const filePath = process.env[`${envName}_FILE`];
if (filePath) return readFileSync(filePath, 'utf8');

return undefined;
};

export const Config: ClassDecorator = (ConfigClass: Class) => {
const factory = function () {
const config = new (ConfigClass as new () => Record<PropertyKey, unknown>)();
Expand All @@ -26,38 +37,28 @@ export const Config: ClassDecorator = (ConfigClass: Class) => {
if (typeof type === 'function' && globalMetadata.has(type)) {
config[key] = Container.get(type);
} else if (envName) {
let value: unknown = process.env[envName];

// Read the value from a file, if "_FILE" environment variable is defined
const filePath = process.env[`${envName}_FILE`];
if (filePath) {
value = readFileSync(filePath, 'utf8');
}
const value = readEnv(envName);
if (value === undefined) continue;

if (type === Number) {
value = Number(value);
if (isNaN(value as number)) {
// TODO: add a warning
value = undefined;
const parsed = Number(value);
if (isNaN(parsed)) {
console.warn(`Invalid number value for ${envName}: ${value}`);
} else {
config[key] = parsed;
}
} else if (type === Boolean) {
if (value !== 'true' && value !== 'false') {
// TODO: add a warning
value = undefined;
if (['true', '1'].includes(value.toLowerCase())) {
config[key] = true;
} else if (['false', '0'].includes(value.toLowerCase())) {
config[key] = false;
} else {
value = value === 'true';
console.warn(`Invalid boolean value for ${envName}: ${value}`);
}
} else if (type === Object) {
// eslint-disable-next-line n8n-local-rules/no-plain-errors
throw new Error(
`Invalid decorator metadata on key "${key as string}" on ${ConfigClass.name}\n Please use explicit typing on all config fields`,
);
} else if (type !== String && type !== Object) {
value = new (type as Constructable)(value as string);
}

if (value !== undefined) {
} else if (type === String) {
config[key] = value;
} else {
config[key] = new (type as Constructable)(value);
}
}
}
Expand All @@ -70,7 +71,7 @@ export const Config: ClassDecorator = (ConfigClass: Class) => {
export const Nested: PropertyDecorator = (target: object, key: PropertyKey) => {
const ConfigClass = target.constructor;
const classMetadata = globalMetadata.get(ConfigClass) ?? new Map<PropertyKey, PropertyMetadata>();
const type = Reflect.getMetadata('design:type', target, key) as unknown;
const type = Reflect.getMetadata('design:type', target, key) as PropertyType;
classMetadata.set(key, { type });
globalMetadata.set(ConfigClass, classMetadata);
};
Expand All @@ -81,7 +82,13 @@ export const Env =
const ConfigClass = target.constructor;
const classMetadata =
globalMetadata.get(ConfigClass) ?? new Map<PropertyKey, PropertyMetadata>();
const type = Reflect.getMetadata('design:type', target, key) as unknown;
const type = Reflect.getMetadata('design:type', target, key) as PropertyType;
if (type === Object) {
// eslint-disable-next-line n8n-local-rules/no-plain-errors
throw new Error(
`Invalid decorator metadata on key "${key as string}" on ${ConfigClass.name}\n Please use explicit typing on all config fields`,
);
}
classMetadata.set(key, { type, envName });
globalMetadata.set(ConfigClass, classMetadata);
};
25 changes: 19 additions & 6 deletions packages/@n8n/config/test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ describe('GlobalConfig', () => {
process.env = originalEnv;
});

// deepCopy for diff to show plain objects
// eslint-disable-next-line n8n-local-rules/no-json-parse-json-stringify
const deepCopy = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));

const defaultConfig: GlobalConfig = {
path: '/',
host: 'localhost',
Expand Down Expand Up @@ -218,10 +222,6 @@ describe('GlobalConfig', () => {
process.env = {};
const config = Container.get(GlobalConfig);

// deepCopy for diff to show plain objects
// eslint-disable-next-line n8n-local-rules/no-json-parse-json-stringify
const deepCopy = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));

expect(deepCopy(config)).toEqual(defaultConfig);
expect(mockFs.readFileSync).not.toHaveBeenCalled();
});
Expand All @@ -233,9 +233,11 @@ describe('GlobalConfig', () => {
DB_TABLE_PREFIX: 'test_',
NODES_INCLUDE: '["n8n-nodes-base.hackerNews"]',
DB_LOGGING_MAX_EXECUTION_TIME: '0',
N8N_METRICS: 'TRUE',
N8N_TEMPLATES_ENABLED: '0',
};
const config = Container.get(GlobalConfig);
expect(config).toEqual({
expect(deepCopy(config)).toEqual({
...defaultConfig,
database: {
logging: defaultConfig.database.logging,
Expand All @@ -249,10 +251,21 @@ describe('GlobalConfig', () => {
tablePrefix: 'test_',
type: 'sqlite',
},
endpoints: {
...defaultConfig.endpoints,
metrics: {
...defaultConfig.endpoints.metrics,
enable: true,
},
},
nodes: {
...defaultConfig.nodes,
include: ['n8n-nodes-base.hackerNews'],
},
templates: {
...defaultConfig.templates,
enabled: false,
},
});
expect(mockFs.readFileSync).not.toHaveBeenCalled();
});
Expand All @@ -265,7 +278,7 @@ describe('GlobalConfig', () => {
mockFs.readFileSync.calledWith(passwordFile, 'utf8').mockReturnValueOnce('password-from-file');

const config = Container.get(GlobalConfig);
expect(config).toEqual({
expect(deepCopy(config)).toEqual({
...defaultConfig,
database: {
...defaultConfig.database,
Expand Down
21 changes: 21 additions & 0 deletions packages/@n8n/config/test/decorators.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Container } from 'typedi';
import { Config, Env } from '../src/decorators';

describe('decorators', () => {
beforeEach(() => {
Container.reset();
});

it('should throw when explicit typing is missing', () => {
expect(() => {
@Config
class InvalidConfig {
@Env('STRING_VALUE')
value = 'string';
}
Container.get(InvalidConfig);
}).toThrowError(
'Invalid decorator metadata on key "value" on InvalidConfig\n Please use explicit typing on all config fields',
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,11 @@ export class EmbeddingsAzureOpenAi implements INodeType {

async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> {
this.logger.verbose('Supply data for embeddings');
const credentials = (await this.getCredentials('azureOpenAiApi')) as {
const credentials = await this.getCredentials<{
apiKey: string;
resourceName: string;
apiVersion: string;
};
}>('azureOpenAiApi');
const modelName = this.getNodeParameter('model', itemIndex) as string;

const options = this.getNodeParameter('options', itemIndex, {}) as {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export class EmbeddingsCohere implements INodeType {
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> {
this.logger.verbose('Supply data for embeddings Cohere');
const modelName = this.getNodeParameter('modelName', itemIndex, 'embed-english-v2.0') as string;
const credentials = (await this.getCredentials('cohereApi')) as { apiKey: string };
const credentials = await this.getCredentials<{ apiKey: string }>('cohereApi');
const embeddings = new CohereEmbeddings({
apiKey: credentials.apiKey,
model: modelName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,11 @@ export class LmChatAzureOpenAi implements INodeType {
};

async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = (await this.getCredentials('azureOpenAiApi')) as {
const credentials = await this.getCredentials<{
apiKey: string;
resourceName: string;
apiVersion: string;
};
}>('azureOpenAiApi');

const modelName = this.getNodeParameter('model', itemIndex) as string;
const options = this.getNodeParameter('options', itemIndex, {}) as {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class MemoryPostgresChat implements INodeType {
};

async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = (await this.getCredentials('postgres')) as PostgresNodeCredentials;
const credentials = await this.getCredentials<PostgresNodeCredentials>('postgres');
const tableName = this.getNodeParameter('tableName', itemIndex, 'n8n_chat_histories') as string;
const sessionId = getSessionId(this, itemIndex);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,11 @@ export class MemoryZep implements INodeType {
};

async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = (await this.getCredentials('zepApi')) as {
const credentials = await this.getCredentials<{
apiKey?: string;
apiUrl?: string;
cloud?: boolean;
};
}>('zepApi');

const nodeVersion = this.getNode().typeVersion;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export async function validateAuth(context: IWebhookFunctions) {
// Basic authorization is needed to call webhook
let expectedAuth: ICredentialDataDecryptedObject | undefined;
try {
expectedAuth = await context.getCredentials('httpBasicAuth');
expectedAuth = await context.getCredentials<ICredentialDataDecryptedObject>('httpBasicAuth');
} catch {}

if (expectedAuth === undefined || !expectedAuth.user || !expectedAuth.password) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ export const VectorStoreZep = createVectorStoreNode({
embeddingDimensions?: number;
}) || {};

const credentials = (await context.getCredentials('zepApi')) as {
const credentials = await context.getCredentials<{
apiKey?: string;
apiUrl: string;
};
}>('zepApi');

const zepConfig: IZepConfig = {
apiUrl: credentials.apiUrl,
Expand All @@ -102,10 +102,10 @@ export const VectorStoreZep = createVectorStoreNode({
embeddingDimensions?: number;
}) || {};

const credentials = (await context.getCredentials('zepApi')) as {
const credentials = await context.getCredentials<{
apiKey?: string;
apiUrl: string;
};
}>('zepApi');

const zepConfig = {
apiUrl: credentials.apiUrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,10 @@ export class VectorStoreZepInsert implements INodeType {
embeddingDimensions?: number;
}) || {};

const credentials = (await this.getCredentials('zepApi')) as {
const credentials = await this.getCredentials<{
apiKey?: string;
apiUrl: string;
};
}>('zepApi');

const documentInput = (await this.getInputConnectionData(NodeConnectionType.AiDocument, 0)) as
| N8nJsonLoader
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,10 @@ export class VectorStoreZepLoad implements INodeType {
embeddingDimensions?: number;
}) || {};

const credentials = (await this.getCredentials('zepApi')) as {
const credentials = await this.getCredentials<{
apiKey?: string;
apiUrl: string;
};
}>('zepApi');
const embeddings = (await this.getInputConnectionData(
NodeConnectionType.AiEmbedding,
0,
Expand Down
6 changes: 4 additions & 2 deletions packages/cli/src/commands/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ import { NodeTypes } from '@/node-types';
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
import type { N8nInstanceType } from '@/Interfaces';
import { PostHogClient } from '@/posthog';
import { InternalHooks } from '@/internal-hooks';
import { License } from '@/license';
import { ExternalSecretsManager } from '@/external-secrets/external-secrets-manager.ee';
import { initExpressionEvaluator } from '@/expression-evaluator';
import { generateHostInstanceId } from '@db/utils/generators';
import { WorkflowHistoryManager } from '@/workflows/workflow-history/workflow-history-manager.ee';
import { ShutdownService } from '@/shutdown/shutdown.service';
import { TelemetryEventRelay } from '@/events/telemetry-event-relay';
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';

export abstract class BaseCommand extends Command {
protected logger = Container.get(Logger);
Expand Down Expand Up @@ -120,8 +120,10 @@ export abstract class BaseCommand extends Command {
await Container.get(CommunityPackagesService).checkForMissingPackages();
}

// TODO: remove this after the cyclic dependencies around the event-bus are resolved
Container.get(MessageEventBus);

await Container.get(PostHogClient).init();
await Container.get(InternalHooks).init();
await Container.get(TelemetryEventRelay).init();
}

Expand Down
24 changes: 0 additions & 24 deletions packages/cli/src/internal-hooks.ts

This file was deleted.

4 changes: 2 additions & 2 deletions packages/cli/test/integration/shared/utils/testCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import type { BaseCommand } from '@/commands/base-command';
import * as testDb from '../testDb';
import { TelemetryEventRelay } from '@/events/telemetry-event-relay';
import { mockInstance } from '@test/mocking';
import { InternalHooks } from '@/internal-hooks';
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';

mockInstance(InternalHooks);
mockInstance(MessageEventBus);

export const setupTestCommand = <T extends BaseCommand>(Command: Class<T>) => {
const config = mock<Config>();
Expand Down
Loading

0 comments on commit 20086cf

Please sign in to comment.