mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
chore(core): Enable TypeScript strict mode in packages/cli (no-changelog) (#27876)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
553976d065
commit
370b281216
|
|
@ -447,6 +447,10 @@ export type AuthenticatedRequest<
|
||||||
tokenGrant?: TokenGrant;
|
tokenGrant?: TokenGrant;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function isAuthenticatedRequest(req: express.Request): req is AuthenticatedRequest {
|
||||||
|
return 'user' in req && req.user !== null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simplified to prevent excessively deep type instantiation error from
|
* Simplified to prevent excessively deep type instantiation error from
|
||||||
* `INodeExecutionData` in `IPinData` in a TypeORM entity field.
|
* `INodeExecutionData` in `IPinData` in a TypeORM entity field.
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ import 'reflect-metadata';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export type Constructable<T = unknown> = new (...args: any[]) => T;
|
export type Constructable<T = unknown> = new (...args: any[]) => T;
|
||||||
|
|
||||||
type AbstractConstructable<T = unknown> = abstract new (...args: unknown[]) => T;
|
export type AbstractConstructable<T = unknown> = abstract new (...args: unknown[]) => T;
|
||||||
|
|
||||||
type ServiceIdentifier<T = unknown> = Constructable<T> | AbstractConstructable<T>;
|
export type ServiceIdentifier<T = unknown> = Constructable<T> | AbstractConstructable<T>;
|
||||||
|
|
||||||
type Factory<T = unknown> = (...args: unknown[]) => T;
|
type Factory<T = unknown> = (...args: unknown[]) => T;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -176,17 +176,17 @@ describe('NodeTypes', () => {
|
||||||
expect(result.description.outputs).toEqual(['ai_tool']);
|
expect(result.description.outputs).toEqual(['ai_tool']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a declarative node-type with an `.execute` method', () => {
|
it('should return a declarative node-type with an `.execute` method', async () => {
|
||||||
const result = nodeTypes.getByNameAndVersion('n8n-nodes-base.declarativeNode');
|
const result = nodeTypes.getByNameAndVersion('n8n-nodes-base.declarativeNode');
|
||||||
expect(result).toBe(declarativeNode.type);
|
expect(result).toBe(declarativeNode.type);
|
||||||
expect(result.execute).toBeDefined();
|
expect(result.execute).toBeDefined();
|
||||||
|
|
||||||
const runNodeSpy = jest.spyOn(RoutingNode.prototype, 'runNode').mockResolvedValue([]);
|
const runNodeSpy = jest.spyOn(RoutingNode.prototype, 'runNode').mockResolvedValue([]);
|
||||||
result.execute!.call(mock());
|
await result.execute!.call(mock());
|
||||||
expect(runNodeSpy).toHaveBeenCalled();
|
expect(runNodeSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a declarative node-type as a tool with an `.execute` method', () => {
|
it('should return a declarative node-type as a tool with an `.execute` method', async () => {
|
||||||
const result = nodeTypes.getByNameAndVersion('n8n-nodes-base.declarativeNodeTool');
|
const result = nodeTypes.getByNameAndVersion('n8n-nodes-base.declarativeNodeTool');
|
||||||
expect(result).not.toEqual(declarativeNode.type);
|
expect(result).not.toEqual(declarativeNode.type);
|
||||||
expect(result.description.name).toEqual('n8n-nodes-base.declarativeNodeTool');
|
expect(result.description.name).toEqual('n8n-nodes-base.declarativeNodeTool');
|
||||||
|
|
@ -197,7 +197,7 @@ describe('NodeTypes', () => {
|
||||||
expect(result.execute).toBeDefined();
|
expect(result.execute).toBeDefined();
|
||||||
|
|
||||||
const runNodeSpy = jest.spyOn(RoutingNode.prototype, 'runNode').mockResolvedValue([]);
|
const runNodeSpy = jest.spyOn(RoutingNode.prototype, 'runNode').mockResolvedValue([]);
|
||||||
result.execute!.call(mock());
|
await result.execute!.call(mock());
|
||||||
expect(runNodeSpy).toHaveBeenCalled();
|
expect(runNodeSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -67,8 +67,6 @@ export abstract class AbstractServer {
|
||||||
|
|
||||||
private fullyReady = false;
|
private fullyReady = false;
|
||||||
|
|
||||||
readonly uniqueInstanceId: string;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.app = express();
|
this.app = express();
|
||||||
this.app.disable('x-powered-by');
|
this.app.disable('x-powered-by');
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,9 @@ describe('ConcurrencyControlService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('constructor', () => {
|
describe('constructor', () => {
|
||||||
it.each(['production', 'evaluation'])(
|
it.each<ConcurrencyQueueType>(['production', 'evaluation'])(
|
||||||
'should be enabled if %s cap is positive',
|
'should be enabled if %s cap is positive',
|
||||||
(type: ConcurrencyQueueType) => {
|
(type) => {
|
||||||
/**
|
/**
|
||||||
* Arrange
|
* Arrange
|
||||||
*/
|
*/
|
||||||
|
|
@ -72,9 +72,9 @@ describe('ConcurrencyControlService', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it.each(['production', 'evaluation'])(
|
it.each<ConcurrencyQueueType>(['production', 'evaluation'])(
|
||||||
'should throw if %s cap is 0',
|
'should throw if %s cap is 0',
|
||||||
(type: ConcurrencyQueueType) => {
|
(type) => {
|
||||||
/**
|
/**
|
||||||
* Arrange
|
* Arrange
|
||||||
*/
|
*/
|
||||||
|
|
@ -126,9 +126,9 @@ describe('ConcurrencyControlService', () => {
|
||||||
expect(service.isEnabled).toBe(false);
|
expect(service.isEnabled).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(['production', 'evaluation'])(
|
it.each<ConcurrencyQueueType>(['production', 'evaluation'])(
|
||||||
'should be disabled if %s cap is lower than -1',
|
'should be disabled if %s cap is lower than -1',
|
||||||
(type: ConcurrencyQueueType) => {
|
(type) => {
|
||||||
/**
|
/**
|
||||||
* Arrange
|
* Arrange
|
||||||
*/
|
*/
|
||||||
|
|
@ -186,9 +186,9 @@ describe('ConcurrencyControlService', () => {
|
||||||
|
|
||||||
describe('if enabled', () => {
|
describe('if enabled', () => {
|
||||||
describe('throttle', () => {
|
describe('throttle', () => {
|
||||||
it.each(['cli', 'error', 'integrated', 'internal', 'manual', 'retry'])(
|
it.each<ExecutionMode>(['cli', 'error', 'integrated', 'internal', 'manual', 'retry'])(
|
||||||
'should do nothing on %s mode',
|
'should do nothing on %s mode',
|
||||||
async (mode: ExecutionMode) => {
|
async (mode) => {
|
||||||
/**
|
/**
|
||||||
* Arrange
|
* Arrange
|
||||||
*/
|
*/
|
||||||
|
|
@ -215,9 +215,9 @@ describe('ConcurrencyControlService', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it.each(['webhook', 'trigger', 'chat'])(
|
it.each<ExecutionMode>(['webhook', 'trigger', 'chat'])(
|
||||||
'should enqueue on %s mode',
|
'should enqueue on %s mode',
|
||||||
async (mode: ExecutionMode) => {
|
async (mode) => {
|
||||||
/**
|
/**
|
||||||
* Arrange
|
* Arrange
|
||||||
*/
|
*/
|
||||||
|
|
@ -272,9 +272,9 @@ describe('ConcurrencyControlService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('release', () => {
|
describe('release', () => {
|
||||||
it.each(['cli', 'error', 'integrated', 'internal', 'manual', 'retry'])(
|
it.each<ExecutionMode>(['cli', 'error', 'integrated', 'internal', 'manual', 'retry'])(
|
||||||
'should do nothing on %s mode',
|
'should do nothing on %s mode',
|
||||||
async (mode: ExecutionMode) => {
|
async (mode) => {
|
||||||
/**
|
/**
|
||||||
* Arrange
|
* Arrange
|
||||||
*/
|
*/
|
||||||
|
|
@ -301,9 +301,9 @@ describe('ConcurrencyControlService', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it.each(['webhook', 'trigger', 'chat'])(
|
it.each<ExecutionMode>(['webhook', 'trigger', 'chat'])(
|
||||||
'should dequeue on %s mode',
|
'should dequeue on %s mode',
|
||||||
(mode: ExecutionMode) => {
|
(mode) => {
|
||||||
/**
|
/**
|
||||||
* Arrange
|
* Arrange
|
||||||
*/
|
*/
|
||||||
|
|
@ -358,9 +358,9 @@ describe('ConcurrencyControlService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('remove', () => {
|
describe('remove', () => {
|
||||||
it.each(['cli', 'error', 'integrated', 'internal', 'manual', 'retry'])(
|
it.each<ExecutionMode>(['cli', 'error', 'integrated', 'internal', 'manual', 'retry'])(
|
||||||
'should do nothing on %s mode',
|
'should do nothing on %s mode',
|
||||||
async (mode: ExecutionMode) => {
|
async (mode) => {
|
||||||
/**
|
/**
|
||||||
* Arrange
|
* Arrange
|
||||||
*/
|
*/
|
||||||
|
|
@ -387,9 +387,9 @@ describe('ConcurrencyControlService', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it.each(['webhook', 'trigger', 'chat'])(
|
it.each<ExecutionMode>(['webhook', 'trigger', 'chat'])(
|
||||||
'should remove an execution on %s mode',
|
'should remove an execution on %s mode',
|
||||||
(mode: ExecutionMode) => {
|
(mode) => {
|
||||||
/**
|
/**
|
||||||
* Arrange
|
* Arrange
|
||||||
*/
|
*/
|
||||||
|
|
@ -444,9 +444,9 @@ describe('ConcurrencyControlService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('removeAll', () => {
|
describe('removeAll', () => {
|
||||||
it.each(['production', 'evaluation'])(
|
it.each<ConcurrencyQueueType>(['production', 'evaluation'])(
|
||||||
'should remove all executions from the %s queue',
|
'should remove all executions from the %s queue',
|
||||||
async (type: ConcurrencyQueueType) => {
|
async (type) => {
|
||||||
/**
|
/**
|
||||||
* Arrange
|
* Arrange
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
|
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
|
||||||
import { GlobalConfig } from '@n8n/config';
|
import { GlobalConfig } from '@n8n/config';
|
||||||
import { type BooleanLicenseFeature } from '@n8n/constants';
|
import { type BooleanLicenseFeature } from '@n8n/constants';
|
||||||
import type { AuthenticatedRequest } from '@n8n/db';
|
|
||||||
import { ControllerRegistryMetadata } from '@n8n/decorators';
|
import { ControllerRegistryMetadata } from '@n8n/decorators';
|
||||||
import type {
|
import type {
|
||||||
AccessScope,
|
AccessScope,
|
||||||
|
|
@ -28,6 +27,7 @@ import { userHasScopes } from '@/permissions.ee/check-access';
|
||||||
import { send } from '@/response-helper';
|
import { send } from '@/response-helper';
|
||||||
import { CorsService } from './services/cors-service';
|
import { CorsService } from './services/cors-service';
|
||||||
import { inProduction } from '@n8n/backend-common';
|
import { inProduction } from '@n8n/backend-common';
|
||||||
|
import { isAuthenticatedRequest } from '@n8n/db';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ControllerRegistry {
|
export class ControllerRegistry {
|
||||||
|
|
@ -172,7 +172,7 @@ export class ControllerRegistry {
|
||||||
allowSkipPreviewAuth: route.allowSkipPreviewAuth ?? false,
|
allowSkipPreviewAuth: route.allowSkipPreviewAuth ?? false,
|
||||||
allowUnauthenticated: route.allowUnauthenticated ?? false,
|
allowUnauthenticated: route.allowUnauthenticated ?? false,
|
||||||
}),
|
}),
|
||||||
this.lastActiveAtService.middleware.bind(this.lastActiveAtService) as RequestHandler,
|
this.lastActiveAtService.middleware.bind(this.lastActiveAtService),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,11 +219,8 @@ export class ControllerRegistry {
|
||||||
}
|
}
|
||||||
|
|
||||||
private createScopedMiddleware(accessScope: AccessScope): RequestHandler {
|
private createScopedMiddleware(accessScope: AccessScope): RequestHandler {
|
||||||
return async (
|
return async (req, res, next) => {
|
||||||
req: AuthenticatedRequest<{ credentialId?: string; workflowId?: string; projectId?: string }>,
|
if (!isAuthenticatedRequest(req)) throw new UnauthenticatedError();
|
||||||
res,
|
|
||||||
next,
|
|
||||||
) => {
|
|
||||||
if (!req.user) throw new UnauthenticatedError();
|
if (!req.user) throw new UnauthenticatedError();
|
||||||
|
|
||||||
const { scope, globalOnly } = accessScope;
|
const { scope, globalOnly } = accessScope;
|
||||||
|
|
|
||||||
|
|
@ -326,7 +326,7 @@ describe('AiController', () => {
|
||||||
let abortSignalPassed: AbortSignal | undefined;
|
let abortSignalPassed: AbortSignal | undefined;
|
||||||
|
|
||||||
// Mock response.on to capture the close handler
|
// Mock response.on to capture the close handler
|
||||||
response.on.mockImplementation((event: string, handler: () => void) => {
|
response.on.mockImplementation((event: string | symbol, handler: () => void) => {
|
||||||
if (event === 'close') {
|
if (event === 'close') {
|
||||||
abortHandler = handler;
|
abortHandler = handler;
|
||||||
}
|
}
|
||||||
|
|
@ -406,7 +406,7 @@ describe('AiController', () => {
|
||||||
let abortHandler: (() => void) | undefined;
|
let abortHandler: (() => void) | undefined;
|
||||||
let abortSignalPassed: AbortSignal | undefined;
|
let abortSignalPassed: AbortSignal | undefined;
|
||||||
|
|
||||||
response.on.mockImplementation((event: string, handler: () => void) => {
|
response.on.mockImplementation((event: string | symbol, handler: () => void) => {
|
||||||
if (event === 'close') {
|
if (event === 'close') {
|
||||||
abortHandler = handler;
|
abortHandler = handler;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -460,10 +460,11 @@ export class CredentialsService {
|
||||||
credentials: CredentialsEntity[],
|
credentials: CredentialsEntity[],
|
||||||
): Array<ICredentialsDecrypted<ICredentialDataDecryptedObject>> {
|
): Array<ICredentialsDecrypted<ICredentialDataDecryptedObject>> {
|
||||||
return credentials.map(
|
return credentials.map(
|
||||||
(
|
(c: CredentialsEntity): ICredentialsDecrypted<ICredentialDataDecryptedObject> => {
|
||||||
c: CredentialsEntity & ScopesField,
|
const credWithScopes = c as CredentialsEntity & ScopesField;
|
||||||
): ICredentialsDecrypted<ICredentialDataDecryptedObject> => {
|
const data = credWithScopes.scopes.includes('credential:update')
|
||||||
const data = c.scopes.includes('credential:update') ? this.decrypt(c) : undefined;
|
? this.decrypt(c)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
// We never want to expose the oauthTokenData to the frontend, but it
|
// We never want to expose the oauthTokenData to the frontend, but it
|
||||||
// expects it to check if the credential is already connected.
|
// expects it to check if the credential is already connected.
|
||||||
|
|
|
||||||
|
|
@ -73,9 +73,9 @@ describe('DeprecationService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when executions.mode is not queue', () => {
|
describe('when executions.mode is not queue', () => {
|
||||||
test.each([['main'], ['worker'], ['webhook']])(
|
test.each<[InstanceType]>([['main'], ['worker'], ['webhook']])(
|
||||||
'should not warn for instanceType %s',
|
'should not warn for instanceType %s',
|
||||||
(instanceType: InstanceType) => {
|
(instanceType) => {
|
||||||
process.env[envVar] = 'false';
|
process.env[envVar] = 'false';
|
||||||
const service = new DeprecationService(
|
const service = new DeprecationService(
|
||||||
logger,
|
logger,
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ export class DeprecationService {
|
||||||
envVar: 'EXECUTIONS_PROCESS',
|
envVar: 'EXECUTIONS_PROCESS',
|
||||||
message:
|
message:
|
||||||
'n8n does not support `own` mode since May 2023. Please remove this environment variable to allow n8n to start. If you need the isolation and performance gains, please consider queue mode: https://docs.n8n.io/hosting/scaling/queue-mode/',
|
'n8n does not support `own` mode since May 2023. Please remove this environment variable to allow n8n to start. If you need the isolation and performance gains, please consider queue mode: https://docs.n8n.io/hosting/scaling/queue-mode/',
|
||||||
checkValue: (value: string) => value === 'own',
|
checkValue: (value: string | undefined): value is 'own' => value === 'own',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ export type AiEventMap = {
|
||||||
|
|
||||||
'ai-documents-retrieved': AiEventPayload;
|
'ai-documents-retrieved': AiEventPayload;
|
||||||
|
|
||||||
|
'ai-document-reranked': AiEventPayload;
|
||||||
|
|
||||||
'ai-document-embedded': AiEventPayload;
|
'ai-document-embedded': AiEventPayload;
|
||||||
|
|
||||||
'ai-query-embedded': AiEventPayload;
|
'ai-query-embedded': AiEventPayload;
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ class ModulesHooksRegistry {
|
||||||
case 'workflowExecuteAfter':
|
case 'workflowExecuteAfter':
|
||||||
hooks.addHandler(eventName, async function (runData, newStaticData) {
|
hooks.addHandler(eventName, async function (runData, newStaticData) {
|
||||||
const context = {
|
const context = {
|
||||||
type: 'workflowExecuteAfter',
|
type: 'workflowExecuteAfter' as const,
|
||||||
workflow: this.workflowData,
|
workflow: this.workflowData,
|
||||||
runData,
|
runData,
|
||||||
newStaticData,
|
newStaticData,
|
||||||
|
|
@ -73,7 +73,7 @@ class ModulesHooksRegistry {
|
||||||
case 'nodeExecuteBefore':
|
case 'nodeExecuteBefore':
|
||||||
hooks.addHandler(eventName, async function (nodeName, taskData) {
|
hooks.addHandler(eventName, async function (nodeName, taskData) {
|
||||||
const context = {
|
const context = {
|
||||||
type: 'nodeExecuteBefore',
|
type: 'nodeExecuteBefore' as const,
|
||||||
workflow: this.workflowData,
|
workflow: this.workflowData,
|
||||||
nodeName,
|
nodeName,
|
||||||
taskData,
|
taskData,
|
||||||
|
|
@ -87,7 +87,7 @@ class ModulesHooksRegistry {
|
||||||
case 'nodeExecuteAfter':
|
case 'nodeExecuteAfter':
|
||||||
hooks.addHandler(eventName, async function (nodeName, taskData, executionData) {
|
hooks.addHandler(eventName, async function (nodeName, taskData, executionData) {
|
||||||
const context = {
|
const context = {
|
||||||
type: 'nodeExecuteAfter',
|
type: 'nodeExecuteAfter' as const,
|
||||||
workflow: this.workflowData,
|
workflow: this.workflowData,
|
||||||
nodeName,
|
nodeName,
|
||||||
taskData,
|
taskData,
|
||||||
|
|
@ -102,7 +102,7 @@ class ModulesHooksRegistry {
|
||||||
case 'workflowExecuteBefore':
|
case 'workflowExecuteBefore':
|
||||||
hooks.addHandler(eventName, async function (workflowInstance, executionData) {
|
hooks.addHandler(eventName, async function (workflowInstance, executionData) {
|
||||||
const context = {
|
const context = {
|
||||||
type: 'workflowExecuteBefore',
|
type: 'workflowExecuteBefore' as const,
|
||||||
workflow: this.workflowData,
|
workflow: this.workflowData,
|
||||||
workflowInstance,
|
workflowInstance,
|
||||||
executionData,
|
executionData,
|
||||||
|
|
@ -116,7 +116,7 @@ class ModulesHooksRegistry {
|
||||||
case 'workflowExecuteResume':
|
case 'workflowExecuteResume':
|
||||||
hooks.addHandler(eventName, async function (workflowInstance, executionData) {
|
hooks.addHandler(eventName, async function (workflowInstance, executionData) {
|
||||||
const context = {
|
const context = {
|
||||||
type: 'workflowExecuteResume',
|
type: 'workflowExecuteResume' as const,
|
||||||
workflow: this.workflowData,
|
workflow: this.workflowData,
|
||||||
workflowInstance,
|
workflowInstance,
|
||||||
executionData,
|
executionData,
|
||||||
|
|
|
||||||
|
|
@ -27,13 +27,15 @@ export const parseRangeQuery = (
|
||||||
try {
|
try {
|
||||||
req.rangeQuery = {
|
req.rangeQuery = {
|
||||||
kind: 'range',
|
kind: 'range',
|
||||||
range: { limit: limit ? Math.min(parseInt(limit, 10), 100) : 20 },
|
range: {
|
||||||
|
limit: limit && typeof limit === 'string' ? Math.min(parseInt(limit, 10), 100) : 20,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (firstId) req.rangeQuery.range.firstId = firstId;
|
if (firstId && typeof firstId === 'string') req.rangeQuery.range.firstId = firstId;
|
||||||
if (lastId) req.rangeQuery.range.lastId = lastId;
|
if (lastId && typeof lastId === 'string') req.rangeQuery.range.lastId = lastId;
|
||||||
|
|
||||||
if (req.query.filter) {
|
if (typeof req.query.filter === 'string') {
|
||||||
const jsonFilter = jsonParse<JsonObject>(req.query.filter, {
|
const jsonFilter = jsonParse<JsonObject>(req.query.filter, {
|
||||||
errorMessage: 'Failed to parse query string',
|
errorMessage: 'Failed to parse query string',
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ export class ExternalHooks {
|
||||||
|
|
||||||
for (const hookFunction of hookFunctions) {
|
for (const hookFunction of hookFunctions) {
|
||||||
try {
|
try {
|
||||||
await hookFunction.apply(context, hookParameters);
|
await hookFunction.apply(context, hookParameters!);
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
this.logger.error(`There was a problem running hook "${hookName}"`);
|
this.logger.error(`There was a problem running hook "${hookName}"`);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { NextFunction, Response } from 'express';
|
import type { RequestHandler } from 'express';
|
||||||
|
|
||||||
import type { ListQuery } from '@/requests';
|
import { appendListQueryOptions } from '@/requests';
|
||||||
import * as ResponseHelper from '@/response-helper';
|
import * as ResponseHelper from '@/response-helper';
|
||||||
import { toError } from '@/utils';
|
import { toError } from '@/utils';
|
||||||
|
|
||||||
|
|
@ -8,14 +8,10 @@ import { CredentialsFilter } from './dtos/credentials.filter.dto';
|
||||||
import { UserFilter } from './dtos/user.filter.dto';
|
import { UserFilter } from './dtos/user.filter.dto';
|
||||||
import { WorkflowFilter } from './dtos/workflow.filter.dto';
|
import { WorkflowFilter } from './dtos/workflow.filter.dto';
|
||||||
|
|
||||||
export const filterListQueryMiddleware = async (
|
export const filterListQueryMiddleware: RequestHandler = async (req, res, next) => {
|
||||||
req: ListQuery.Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction,
|
|
||||||
) => {
|
|
||||||
const { filter: rawFilter } = req.query;
|
const { filter: rawFilter } = req.query;
|
||||||
|
|
||||||
if (!rawFilter) return next();
|
if (!rawFilter || typeof rawFilter !== 'string') return next();
|
||||||
|
|
||||||
let Filter;
|
let Filter;
|
||||||
|
|
||||||
|
|
@ -34,7 +30,7 @@ export const filterListQueryMiddleware = async (
|
||||||
|
|
||||||
if (Object.keys(filter).length === 0) return next();
|
if (Object.keys(filter).length === 0) return next();
|
||||||
|
|
||||||
req.listQueryOptions = { ...req.listQueryOptions, filter };
|
appendListQueryOptions(req, { filter });
|
||||||
|
|
||||||
next();
|
next();
|
||||||
} catch (maybeError) {
|
} catch (maybeError) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { type NextFunction, type Response } from 'express';
|
import type { RequestHandler, NextFunction, Response } from 'express';
|
||||||
|
|
||||||
import type { ListQuery } from '@/requests';
|
import type { ListQuery } from '@/requests';
|
||||||
|
|
||||||
|
|
@ -16,7 +16,7 @@ export type ListQueryMiddleware = (
|
||||||
/**
|
/**
|
||||||
* @deprecated Please create Zod validators in `@n8n/api-types` instead.
|
* @deprecated Please create Zod validators in `@n8n/api-types` instead.
|
||||||
*/
|
*/
|
||||||
export const listQueryMiddleware: ListQueryMiddleware[] = [
|
export const listQueryMiddleware: RequestHandler[] = [
|
||||||
filterListQueryMiddleware,
|
filterListQueryMiddleware,
|
||||||
selectListQueryMiddleware,
|
selectListQueryMiddleware,
|
||||||
paginationListQueryMiddleware,
|
paginationListQueryMiddleware,
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,30 @@
|
||||||
import type { RequestHandler } from 'express';
|
import type { RequestHandler } from 'express';
|
||||||
import { UnexpectedError } from 'n8n-workflow';
|
import { UnexpectedError } from 'n8n-workflow';
|
||||||
|
|
||||||
import type { ListQuery } from '@/requests';
|
import { appendListQueryOptions } from '@/requests';
|
||||||
import * as ResponseHelper from '@/response-helper';
|
import * as ResponseHelper from '@/response-helper';
|
||||||
import { toError } from '@/utils';
|
import { toError } from '@/utils';
|
||||||
|
|
||||||
import { Pagination } from './dtos/pagination.dto';
|
import { Pagination } from './dtos/pagination.dto';
|
||||||
|
|
||||||
export const paginationListQueryMiddleware: RequestHandler = (
|
export const paginationListQueryMiddleware: RequestHandler = (req, res, next) => {
|
||||||
req: ListQuery.Request,
|
const { take: rawTake } = req.query;
|
||||||
res,
|
let { skip: rawSkip = '0' } = req.query;
|
||||||
next,
|
|
||||||
) => {
|
|
||||||
const { take: rawTake, skip: rawSkip = '0' } = req.query;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!rawTake && req.query.skip) {
|
if (!rawTake && req.query.skip) {
|
||||||
throw new UnexpectedError('Please specify `take` when using `skip`');
|
throw new UnexpectedError('Please specify `take` when using `skip`');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!rawTake) return next();
|
if (!rawTake || typeof rawTake !== 'string') return next();
|
||||||
|
|
||||||
|
if (typeof rawSkip !== 'string') {
|
||||||
|
rawSkip = '0';
|
||||||
|
}
|
||||||
|
|
||||||
const { take, skip } = Pagination.fromString(rawTake, rawSkip);
|
const { take, skip } = Pagination.fromString(rawTake, rawSkip);
|
||||||
|
|
||||||
req.listQueryOptions = { ...req.listQueryOptions, skip, take };
|
appendListQueryOptions(req, { skip, take });
|
||||||
|
|
||||||
next();
|
next();
|
||||||
} catch (maybeError) {
|
} catch (maybeError) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { RequestHandler } from 'express';
|
import type { RequestHandler } from 'express';
|
||||||
|
|
||||||
import type { ListQuery } from '@/requests';
|
import { appendListQueryOptions } from '@/requests';
|
||||||
import * as ResponseHelper from '@/response-helper';
|
import * as ResponseHelper from '@/response-helper';
|
||||||
import { toError } from '@/utils';
|
import { toError } from '@/utils';
|
||||||
|
|
||||||
|
|
@ -8,10 +8,10 @@ import { CredentialsSelect } from './dtos/credentials.select.dto';
|
||||||
import { UserSelect } from './dtos/user.select.dto';
|
import { UserSelect } from './dtos/user.select.dto';
|
||||||
import { WorkflowSelect } from './dtos/workflow.select.dto';
|
import { WorkflowSelect } from './dtos/workflow.select.dto';
|
||||||
|
|
||||||
export const selectListQueryMiddleware: RequestHandler = (req: ListQuery.Request, res, next) => {
|
export const selectListQueryMiddleware: RequestHandler = (req, res, next) => {
|
||||||
const { select: rawSelect } = req.query;
|
const { select: rawSelect } = req.query;
|
||||||
|
|
||||||
if (!rawSelect) return next();
|
if (!rawSelect || typeof rawSelect !== 'string') return next();
|
||||||
|
|
||||||
let Select;
|
let Select;
|
||||||
|
|
||||||
|
|
@ -30,7 +30,7 @@ export const selectListQueryMiddleware: RequestHandler = (req: ListQuery.Request
|
||||||
|
|
||||||
if (Object.keys(select).length === 0) return next();
|
if (Object.keys(select).length === 0) return next();
|
||||||
|
|
||||||
req.listQueryOptions = { ...req.listQueryOptions, select };
|
appendListQueryOptions(req, { select });
|
||||||
|
|
||||||
next();
|
next();
|
||||||
} catch (maybeError) {
|
} catch (maybeError) {
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,16 @@ import { validateSync } from 'class-validator';
|
||||||
import type { RequestHandler } from 'express';
|
import type { RequestHandler } from 'express';
|
||||||
import { UnexpectedError } from 'n8n-workflow';
|
import { UnexpectedError } from 'n8n-workflow';
|
||||||
|
|
||||||
import type { ListQuery } from '@/requests';
|
import { appendListQueryOptions } from '@/requests';
|
||||||
import * as ResponseHelper from '@/response-helper';
|
import * as ResponseHelper from '@/response-helper';
|
||||||
import { toError } from '@/utils';
|
import { toError } from '@/utils';
|
||||||
|
|
||||||
import { WorkflowSorting } from './dtos/workflow.sort-by.dto';
|
import { WorkflowSorting } from './dtos/workflow.sort-by.dto';
|
||||||
|
|
||||||
export const sortByQueryMiddleware: RequestHandler = (req: ListQuery.Request, res, next) => {
|
export const sortByQueryMiddleware: RequestHandler = (req, res, next) => {
|
||||||
const { sortBy } = req.query;
|
const { sortBy } = req.query;
|
||||||
|
|
||||||
if (!sortBy) return next();
|
if (!sortBy || typeof sortBy !== 'string') return next();
|
||||||
|
|
||||||
let SortBy;
|
let SortBy;
|
||||||
|
|
||||||
|
|
@ -30,7 +30,7 @@ export const sortByQueryMiddleware: RequestHandler = (req: ListQuery.Request, re
|
||||||
throw new UnexpectedError(validationError.constraints?.workflowSortBy ?? '');
|
throw new UnexpectedError(validationError.constraints?.workflowSortBy ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
req.listQueryOptions = { ...req.listQueryOptions, sortBy };
|
appendListQueryOptions(req, { sortBy });
|
||||||
|
|
||||||
next();
|
next();
|
||||||
} catch (maybeError) {
|
} catch (maybeError) {
|
||||||
|
|
|
||||||
|
|
@ -424,7 +424,7 @@ export class ChatHubWorkflowService {
|
||||||
const nodeNames = new Set(nodes.map((node) => node.name));
|
const nodeNames = new Set(nodes.map((node) => node.name));
|
||||||
const distinctTools = tools.map((tool, i) => {
|
const distinctTools = tools.map((tool, i) => {
|
||||||
// Spread out the tool nodes so that they don't overlap on the canvas
|
// Spread out the tool nodes so that they don't overlap on the canvas
|
||||||
const position = [
|
const position: [number, number] = [
|
||||||
700 + Math.floor(i / 3) * 60 + (i % 3) * 120,
|
700 + Math.floor(i / 3) * 60 + (i % 3) * 120,
|
||||||
300 + Math.floor(i / 3) * 120 - (i % 3) * 30,
|
300 + Math.floor(i / 3) * 120 - (i % 3) * 30,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import type { ChatHubMessage } from './chat-hub-message.entity';
|
import type { ChatHubMessage } from './chat-hub-message.entity';
|
||||||
|
|
||||||
type Write = ServerResponse['write'];
|
|
||||||
type End = ServerResponse['end'];
|
type End = ServerResponse['end'];
|
||||||
|
|
||||||
export type ChunkTransformer = (chunk: string) => Promise<string>;
|
export type ChunkTransformer = (chunk: string) => Promise<string>;
|
||||||
|
|
@ -15,7 +14,7 @@ export function interceptResponseWrites<T extends Response>(
|
||||||
res: T,
|
res: T,
|
||||||
transform: ChunkTransformer,
|
transform: ChunkTransformer,
|
||||||
): T {
|
): T {
|
||||||
const originalWrite = res.write.bind(res) as Write;
|
const originalWrite = res.write.bind(res);
|
||||||
const originalEnd = res.end.bind(res) as End;
|
const originalEnd = res.end.bind(res) as End;
|
||||||
const defaultEncoding = 'utf8';
|
const defaultEncoding = 'utf8';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { Logger } from '@n8n/backend-common';
|
import { Logger } from '@n8n/backend-common';
|
||||||
import { AuthenticatedRequest } from '@n8n/db';
|
|
||||||
import { CredentialResolverError } from '@n8n/decorators';
|
import { CredentialResolverError } from '@n8n/decorators';
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
import { NextFunction, Response } from 'express';
|
import type { NextFunction, Response } from 'express';
|
||||||
import { Cipher } from 'n8n-core';
|
import { Cipher } from 'n8n-core';
|
||||||
import type {
|
import type {
|
||||||
ICredentialDataDecryptedObject,
|
ICredentialDataDecryptedObject,
|
||||||
|
|
@ -28,6 +27,7 @@ import { CredentialResolutionError } from '../errors/credential-resolution.error
|
||||||
import { CredentialResolverNotConfiguredError } from '../errors/credential-resolver-not-configured.error';
|
import { CredentialResolverNotConfiguredError } from '../errors/credential-resolver-not-configured.error';
|
||||||
import { CredentialResolverNotFoundError } from '../errors/credential-resolver-not-found.error';
|
import { CredentialResolverNotFoundError } from '../errors/credential-resolver-not-found.error';
|
||||||
import { MissingExecutionContextError } from '../errors/missing-execution-context.error';
|
import { MissingExecutionContextError } from '../errors/missing-execution-context.error';
|
||||||
|
import { AuthenticatedRequest } from '@n8n/db';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for resolving credentials dynamically via configured resolvers.
|
* Service for resolving credentials dynamically via configured resolvers.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
import type { RequestHandler } from 'express';
|
||||||
|
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
import { DynamicCredentialService } from './services/dynamic-credential.service';
|
import { DynamicCredentialService } from './services/dynamic-credential.service';
|
||||||
|
|
||||||
export const getDynamicCredentialMiddlewares = () => {
|
export const getDynamicCredentialMiddlewares = (): RequestHandler[] => {
|
||||||
return [Container.get(DynamicCredentialService).getDynamicCredentialsEndpointsMiddleware()];
|
return [Container.get(DynamicCredentialService).getDynamicCredentialsEndpointsMiddleware()];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ describe('ProviderLifecycle', () => {
|
||||||
const originalConnect = provider.connect.bind(provider);
|
const originalConnect = provider.connect.bind(provider);
|
||||||
jest.spyOn(provider, 'connect').mockImplementation(async function (this: DummyProvider) {
|
jest.spyOn(provider, 'connect').mockImplementation(async function (this: DummyProvider) {
|
||||||
stateBeforeConnect = this.state;
|
stateBeforeConnect = this.state;
|
||||||
return originalConnect();
|
return await originalConnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
await lifecycle.connect(provider);
|
await lifecycle.connect(provider);
|
||||||
|
|
|
||||||
|
|
@ -36,13 +36,14 @@ export class InsightsByPeriod extends BaseEntity {
|
||||||
private type_: number;
|
private type_: number;
|
||||||
|
|
||||||
get type() {
|
get type() {
|
||||||
if (!isValidTypeNumber(this.type_)) {
|
const typeValue = this.type_;
|
||||||
|
if (!isValidTypeNumber(typeValue)) {
|
||||||
throw new UnexpectedError(
|
throw new UnexpectedError(
|
||||||
`Type '${this.type_}' is not a valid type for 'InsightsByPeriod.type'`,
|
`Type '${typeValue}' is not a valid type for 'InsightsByPeriod.type'`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NumberToType[this.type_];
|
return NumberToType[typeValue];
|
||||||
}
|
}
|
||||||
|
|
||||||
set type(value: keyof typeof TypeToNumber) {
|
set type(value: keyof typeof TypeToNumber) {
|
||||||
|
|
@ -61,13 +62,14 @@ export class InsightsByPeriod extends BaseEntity {
|
||||||
private periodUnit_: number;
|
private periodUnit_: number;
|
||||||
|
|
||||||
get periodUnit() {
|
get periodUnit() {
|
||||||
if (!isValidPeriodNumber(this.periodUnit_)) {
|
const periodUnitValue = this.periodUnit_;
|
||||||
|
if (!isValidPeriodNumber(periodUnitValue)) {
|
||||||
throw new UnexpectedError(
|
throw new UnexpectedError(
|
||||||
`Period unit '${this.periodUnit_}' is not a valid unit for 'InsightsByPeriod.periodUnit'`,
|
`Period unit '${periodUnitValue}' is not a valid unit for 'InsightsByPeriod.periodUnit'`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NumberToPeriodUnit[this.periodUnit_];
|
return NumberToPeriodUnit[periodUnitValue];
|
||||||
}
|
}
|
||||||
|
|
||||||
set periodUnit(value: PeriodUnit) {
|
set periodUnit(value: PeriodUnit) {
|
||||||
|
|
|
||||||
|
|
@ -25,13 +25,14 @@ export class InsightsRaw extends BaseEntity {
|
||||||
private type_: number;
|
private type_: number;
|
||||||
|
|
||||||
get type() {
|
get type() {
|
||||||
if (!isValidTypeNumber(this.type_)) {
|
const typeValue = this.type_;
|
||||||
|
if (!isValidTypeNumber(typeValue)) {
|
||||||
throw new UnexpectedError(
|
throw new UnexpectedError(
|
||||||
`Type '${this.type_}' is not a valid type for 'InsightsByPeriod.type'`,
|
`Type '${typeValue}' is not a valid type for 'InsightsByPeriod.type'`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NumberToType[this.type_];
|
return NumberToType[typeValue];
|
||||||
}
|
}
|
||||||
|
|
||||||
set type(value: keyof typeof TypeToNumber) {
|
set type(value: keyof typeof TypeToNumber) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { invert } from '@/utils/inverter';
|
||||||
|
|
||||||
function isValid<T extends Record<number | string | symbol, unknown>>(
|
function isValid<T extends Record<number | string | symbol, unknown>>(
|
||||||
value: number | string | symbol,
|
value: number | string | symbol,
|
||||||
constant: T,
|
constant: T,
|
||||||
|
|
@ -5,6 +7,9 @@ function isValid<T extends Record<number | string | symbol, unknown>>(
|
||||||
return Object.keys(constant).includes(value.toString());
|
return Object.keys(constant).includes(value.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PeriodUnit = keyof typeof PeriodUnitToNumber;
|
||||||
|
|
||||||
|
export type PeriodUnitNumber = (typeof PeriodUnitToNumber)[PeriodUnit];
|
||||||
// Periods
|
// Periods
|
||||||
export const PeriodUnitToNumber = {
|
export const PeriodUnitToNumber = {
|
||||||
hour: 0,
|
hour: 0,
|
||||||
|
|
@ -12,16 +17,8 @@ export const PeriodUnitToNumber = {
|
||||||
week: 2,
|
week: 2,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type PeriodUnit = keyof typeof PeriodUnitToNumber;
|
export const NumberToPeriodUnit = invert(PeriodUnitToNumber);
|
||||||
|
|
||||||
export type PeriodUnitNumber = (typeof PeriodUnitToNumber)[PeriodUnit];
|
|
||||||
export const NumberToPeriodUnit = Object.entries(PeriodUnitToNumber).reduce(
|
|
||||||
(acc, [key, value]: [PeriodUnit, PeriodUnitNumber]) => {
|
|
||||||
acc[value] = key;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<PeriodUnitNumber, PeriodUnit>,
|
|
||||||
);
|
|
||||||
export function isValidPeriodNumber(value: number) {
|
export function isValidPeriodNumber(value: number) {
|
||||||
return isValid(value, NumberToPeriodUnit);
|
return isValid(value, NumberToPeriodUnit);
|
||||||
}
|
}
|
||||||
|
|
@ -37,13 +34,7 @@ export const TypeToNumber = {
|
||||||
export type TypeUnit = keyof typeof TypeToNumber;
|
export type TypeUnit = keyof typeof TypeToNumber;
|
||||||
|
|
||||||
export type TypeUnitNumber = (typeof TypeToNumber)[TypeUnit];
|
export type TypeUnitNumber = (typeof TypeToNumber)[TypeUnit];
|
||||||
export const NumberToType = Object.entries(TypeToNumber).reduce(
|
export const NumberToType = invert(TypeToNumber);
|
||||||
(acc, [key, value]: [TypeUnit, TypeUnitNumber]) => {
|
|
||||||
acc[value] = key;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<TypeUnitNumber, TypeUnit>,
|
|
||||||
);
|
|
||||||
|
|
||||||
export function isValidTypeNumber(value: number) {
|
export function isValidTypeNumber(value: number) {
|
||||||
return isValid(value, NumberToType);
|
return isValid(value, NumberToType);
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,10 @@ describe('getDateRangesCommonTableExpressionQuery', () => {
|
||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.each([
|
describe.each<[DatabaseConfig['type'], string]>([
|
||||||
['sqlite', 'SQLite'],
|
['sqlite', 'SQLite'],
|
||||||
['postgresdb', 'PostgreSQL'],
|
['postgresdb', 'PostgreSQL'],
|
||||||
])('%s', (dbType: DatabaseConfig['type']) => {
|
])('%s', (dbType) => {
|
||||||
describe('hour periodicity (1 day - startDate == endDate)', () => {
|
describe('hour periodicity (1 day - startDate == endDate)', () => {
|
||||||
test('last 24 hours (endDate is today)', () => {
|
test('last 24 hours (endDate is today)', () => {
|
||||||
const startDate = now.minus({ days: 1 }).startOf('day').toJSDate();
|
const startDate = now.minus({ days: 1 }).startOf('day').toJSDate();
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ export class LRUCache<T> {
|
||||||
|
|
||||||
// Evict oldest if at capacity
|
// Evict oldest if at capacity
|
||||||
if (this.map.size >= this.maxEntries) {
|
if (this.map.size >= this.maxEntries) {
|
||||||
const oldest = this.map.keys().next().value as string | undefined;
|
const oldest = this.map.keys().next().value;
|
||||||
if (oldest !== undefined) {
|
if (oldest !== undefined) {
|
||||||
this.map.delete(oldest);
|
this.map.delete(oldest);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -230,7 +230,12 @@ export class MessageEventBusDestinationWebhook
|
||||||
|
|
||||||
const parametersToKeyValue = async (
|
const parametersToKeyValue = async (
|
||||||
acc: Promise<{ [key: string]: any }>,
|
acc: Promise<{ [key: string]: any }>,
|
||||||
cur: { name: string; value: string; parameterType?: string; inputDataFieldName?: string },
|
cur: {
|
||||||
|
name: string;
|
||||||
|
value: string | number | boolean | null;
|
||||||
|
parameterType?: string;
|
||||||
|
inputDataFieldName?: string;
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
const accumulator = await acc;
|
const accumulator = await acc;
|
||||||
accumulator[cur.name] = cur.value;
|
accumulator[cur.name] = cur.value;
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,9 @@ const quickConnectOptionSchema = z.union([
|
||||||
export type QuickConnectOption = z.infer<typeof quickConnectOptionSchema>;
|
export type QuickConnectOption = z.infer<typeof quickConnectOptionSchema>;
|
||||||
|
|
||||||
const quickConnectOptionsSchema = z.string().pipe(
|
const quickConnectOptionsSchema = z.string().pipe(
|
||||||
z.preprocess((input: string) => {
|
z.preprocess((input: unknown) => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(input);
|
return JSON.parse(input as string);
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,10 +69,11 @@ describe('OauthService', () => {
|
||||||
axios.post = jest.fn();
|
axios.post = jest.fn();
|
||||||
|
|
||||||
// Setup cipher mock - encrypt returns the input as-is for testing, decrypt does the reverse
|
// Setup cipher mock - encrypt returns the input as-is for testing, decrypt does the reverse
|
||||||
cipher.encrypt.mockImplementation((data: string) => {
|
cipher.encrypt.mockImplementation((data: string | object) => {
|
||||||
// For testing, we'll use base64 encoding as a simple mock
|
// For testing, we'll use base64 encoding as a simple mock
|
||||||
// In production, this would be actual encryption
|
// In production, this would be actual encryption
|
||||||
return Buffer.from(data).toString('base64');
|
const str = typeof data === 'string' ? data : JSON.stringify(data);
|
||||||
|
return Buffer.from(str).toString('base64');
|
||||||
});
|
});
|
||||||
cipher.decrypt.mockImplementation((data: string) => {
|
cipher.decrypt.mockImplementation((data: string) => {
|
||||||
// For testing, decode the base64
|
// For testing, decode the base64
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,15 @@ import type { IDependency, IJsonSchema } from '../../../types';
|
||||||
|
|
||||||
export class CredentialsIsNotUpdatableError extends BaseError {}
|
export class CredentialsIsNotUpdatableError extends BaseError {}
|
||||||
|
|
||||||
|
function isNodePropertyOptions(options: unknown): options is INodePropertyOptions[] {
|
||||||
|
return (
|
||||||
|
Array.isArray(options) &&
|
||||||
|
options.every(
|
||||||
|
(item) => typeof item === 'object' && item !== null && 'value' in item && 'name' in item,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared entry for credential list: project id/name plus sharing role and timestamps.
|
* Shared entry for credential list: project id/name plus sharing role and timestamps.
|
||||||
* Derived from credential.shared (SharedCredentials + Project), limited to these fields.
|
* Derived from credential.shared (SharedCredentials + Project), limited to these fields.
|
||||||
|
|
@ -294,7 +303,9 @@ export function toJsonSchema(properties: INodeProperties[]): IDataObject {
|
||||||
.filter((property) => property.type === 'options')
|
.filter((property) => property.type === 'options')
|
||||||
.forEach((property) => {
|
.forEach((property) => {
|
||||||
Object.assign(optionsValues, {
|
Object.assign(optionsValues, {
|
||||||
[property.name]: property.options?.map((option: INodePropertyOptions) => option.value),
|
[property.name]: isNodePropertyOptions(property.options)
|
||||||
|
? property.options.map((option) => option.value)
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -317,7 +328,9 @@ export function toJsonSchema(properties: INodeProperties[]): IDataObject {
|
||||||
Object.assign(jsonSchema.properties, {
|
Object.assign(jsonSchema.properties, {
|
||||||
[property.name]: {
|
[property.name]: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
enum: property.options?.map((data: INodePropertyOptions) => data.value),
|
enum: isNodePropertyOptions(property.options)
|
||||||
|
? property.options.map((data) => data.value)
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import type { AuthenticatedRequest } from '@n8n/db';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
import type { ApiKeyScope, Scope } from '@n8n/permissions';
|
import type { ApiKeyScope, Scope } from '@n8n/permissions';
|
||||||
import type express from 'express';
|
import type express from 'express';
|
||||||
|
import type { NextFunction, Request, Response } from 'express';
|
||||||
|
|
||||||
import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error';
|
import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error';
|
||||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||||
|
|
@ -85,16 +86,15 @@ export const validCursor = (
|
||||||
return next();
|
return next();
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ScopeTaggedMiddleware = ((...args: unknown[]) => unknown) & {
|
export type ScopeTaggedMiddleware = Middleware & {
|
||||||
__apiKeyScope: ApiKeyScope;
|
__apiKeyScope: ApiKeyScope;
|
||||||
};
|
};
|
||||||
|
|
||||||
function tagMiddleware(
|
export type Middleware = (req: Request, res: Response, next: NextFunction) => unknown;
|
||||||
middleware: (...args: unknown[]) => unknown,
|
|
||||||
apiKeyScope: ApiKeyScope,
|
function tagMiddleware(middleware: Middleware, apiKeyScope: ApiKeyScope): ScopeTaggedMiddleware {
|
||||||
): ScopeTaggedMiddleware {
|
|
||||||
const tagged: ScopeTaggedMiddleware = Object.assign(
|
const tagged: ScopeTaggedMiddleware = Object.assign(
|
||||||
(req: unknown, res: unknown, next: unknown) => middleware(req, res, next),
|
(req: Request, res: Response, next: NextFunction) => middleware(req, res, next),
|
||||||
{ __apiKeyScope: apiKeyScope },
|
{ __apiKeyScope: apiKeyScope },
|
||||||
);
|
);
|
||||||
return tagged;
|
return tagged;
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,15 @@ import { TypedEmitter } from '@/typed-emitter';
|
||||||
import { validateOriginHeaders } from './origin-validator';
|
import { validateOriginHeaders } from './origin-validator';
|
||||||
import { PushConfig } from './push.config';
|
import { PushConfig } from './push.config';
|
||||||
import { SSEPush } from './sse.push';
|
import { SSEPush } from './sse.push';
|
||||||
import type { OnPushMessage, PushResponse, SSEPushRequest, WebSocketPushRequest } from './types';
|
import {
|
||||||
|
type OnPushMessage,
|
||||||
|
type PushResponse,
|
||||||
|
type SSEPushRequest,
|
||||||
|
type WebSocketPushRequest,
|
||||||
|
} from './types';
|
||||||
import { WebSocketPush } from './websocket.push';
|
import { WebSocketPush } from './websocket.push';
|
||||||
|
import { isPushResponse, isSSEPushRequest, isWebSocketPushRequest } from './push-helpers';
|
||||||
|
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
||||||
|
|
||||||
type PushEvents = {
|
type PushEvents = {
|
||||||
editorUiConnected: string;
|
editorUiConnected: string;
|
||||||
|
|
@ -95,8 +102,15 @@ export class Push extends TypedEmitter<PushEvents> {
|
||||||
`/${restEndpoint}/push`,
|
`/${restEndpoint}/push`,
|
||||||
|
|
||||||
this.authService.createAuthMiddleware({ allowSkipMFA: false }),
|
this.authService.createAuthMiddleware({ allowSkipMFA: false }),
|
||||||
(req: SSEPushRequest | WebSocketPushRequest, res: PushResponse) =>
|
(req, res) => {
|
||||||
this.handleRequest(req, res),
|
if (!isWebSocketPushRequest(req) && !isSSEPushRequest(req)) {
|
||||||
|
throw new BadRequestError('Request is not a PushRequest');
|
||||||
|
}
|
||||||
|
if (!isPushResponse(res)) {
|
||||||
|
throw new InternalServerError('Malformed response object');
|
||||||
|
}
|
||||||
|
return this.handleRequest(req, res);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
20
packages/cli/src/push/push-helpers.ts
Normal file
20
packages/cli/src/push/push-helpers.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import type { Request, Response } from 'express';
|
||||||
|
|
||||||
|
import type { PushRequest, PushResponse, SSEPushRequest, WebSocketPushRequest } from './types';
|
||||||
|
|
||||||
|
export function isPushRequest(req: Request): req is PushRequest {
|
||||||
|
return 'pushRef' in req.query && typeof req.query.pushRef === 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSSEPushRequest(req: Request): req is SSEPushRequest {
|
||||||
|
const hasWs = 'ws' in req;
|
||||||
|
return isPushRequest(req) && (!hasWs || req.ws === undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWebSocketPushRequest(req: Request): req is WebSocketPushRequest {
|
||||||
|
return isPushRequest(req) && 'ws' in req && req.ws !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPushResponse(res: Response): res is PushResponse {
|
||||||
|
return 'req' in res && isPushRequest(res.req);
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ import type {
|
||||||
ProjectRole,
|
ProjectRole,
|
||||||
Scope,
|
Scope,
|
||||||
} from '@n8n/permissions';
|
} from '@n8n/permissions';
|
||||||
|
import type { Request } from 'express';
|
||||||
import type {
|
import type {
|
||||||
ICredentialDataDecryptedObject,
|
ICredentialDataDecryptedObject,
|
||||||
INodeCredentialTestRequest,
|
INodeCredentialTestRequest,
|
||||||
|
|
@ -50,6 +51,11 @@ export namespace ListQuery {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function appendListQueryOptions(req: Request, options: ListQuery.Options) {
|
||||||
|
const listReq = req as ListQuery.Request;
|
||||||
|
listReq.listQueryOptions = { ...listReq.listQueryOptions, ...options };
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
// list query
|
// list query
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ describe('WorkerServer', () => {
|
||||||
|
|
||||||
jest.spyOn(http, 'createServer').mockReturnValue(server);
|
jest.spyOn(http, 'createServer').mockReturnValue(server);
|
||||||
|
|
||||||
server.on.mockImplementation((event: string, callback: (arg?: unknown) => void) => {
|
server.on.mockImplementation((event: string, callback: (...args: unknown[]) => void) => {
|
||||||
if (event === 'error') callback(addressInUseError());
|
if (event === 'error') callback(addressInUseError());
|
||||||
return server;
|
return server;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
import type { Tool } from '@langchain/core/tools';
|
import type { Tool } from '@langchain/core/tools';
|
||||||
import type {
|
import type {
|
||||||
ExecutionStatus,
|
ExecutionStatus,
|
||||||
|
IDataObject,
|
||||||
IExecuteData,
|
IExecuteData,
|
||||||
IExecuteFunctions,
|
IExecuteFunctions,
|
||||||
IExecuteResponsePromiseData,
|
IExecuteResponsePromiseData,
|
||||||
|
|
@ -20,6 +21,7 @@ import type {
|
||||||
IWorkflowExecutionDataProcess,
|
IWorkflowExecutionDataProcess,
|
||||||
StructuredChunk,
|
StructuredChunk,
|
||||||
CloseFunction,
|
CloseFunction,
|
||||||
|
GenericValue,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import {
|
import {
|
||||||
BINARY_ENCODING,
|
BINARY_ENCODING,
|
||||||
|
|
@ -167,7 +169,9 @@ export class JobProcessor {
|
||||||
|
|
||||||
if (pushRef) {
|
if (pushRef) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
additionalData.sendDataToUI = WorkflowExecuteAdditionalData.sendDataToUI.bind({ pushRef });
|
additionalData.sendDataToUI = WorkflowExecuteAdditionalData.sendDataToUI.bind({
|
||||||
|
pushRef,
|
||||||
|
}) as (type: string, data: IDataObject | IDataObject[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
lifecycleHooks.addHandler('sendResponse', async (response): Promise<void> => {
|
lifecycleHooks.addHandler('sendResponse', async (response): Promise<void> => {
|
||||||
|
|
@ -542,7 +546,10 @@ export class JobProcessor {
|
||||||
|
|
||||||
const result = await nodeType.execute.call(context as unknown as IExecuteFunctions);
|
const result = await nodeType.execute.call(context as unknown as IExecuteFunctions);
|
||||||
|
|
||||||
const response = result?.[0]?.flatMap((item: INodeExecutionData) => item.json);
|
let response: IDataObject | IDataObject[] | GenericValue | GenericValue[] = [];
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
response = result?.[0]?.flatMap((item: INodeExecutionData) => item.json);
|
||||||
|
}
|
||||||
|
|
||||||
context.addOutputData(NodeConnectionTypes.AiTool, 0, [
|
context.addOutputData(NodeConnectionTypes.AiTool, 0, [
|
||||||
[{ json: { response } as INodeExecutionData['json'] }],
|
[{ json: { response } as INodeExecutionData['json'] }],
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,7 @@ export class MultiMainSetup extends TypedEmitter<MultiMainEvents> {
|
||||||
const instance = Container.get(eventHandlerClass);
|
const instance = Container.get(eventHandlerClass);
|
||||||
this.on(eventName, async () => {
|
this.on(eventName, async () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
return instance[methodName].call(instance);
|
return await instance[methodName].call(instance);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
PERSONAL_SPACE_SHARING_SETTING,
|
PERSONAL_SPACE_SHARING_SETTING,
|
||||||
EXTERNAL_SECRETS_SYSTEM_ROLES_ENABLED_SETTING,
|
EXTERNAL_SECRETS_SYSTEM_ROLES_ENABLED_SETTING,
|
||||||
} from '@n8n/permissions';
|
} from '@n8n/permissions';
|
||||||
import type { EntityManager, Repository } from '@n8n/typeorm';
|
import type { EntityManager, FindManyOptions, Repository } from '@n8n/typeorm';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
|
|
||||||
const SHARING_SCOPES = PERSONAL_SPACE_SHARING_SETTING.scopes;
|
const SHARING_SCOPES = PERSONAL_SPACE_SHARING_SETTING.scopes;
|
||||||
|
|
@ -446,8 +446,8 @@ describe('AuthRolesService', () => {
|
||||||
scopeRepository.find.mockResolvedValue(allScopes);
|
scopeRepository.find.mockResolvedValue(allScopes);
|
||||||
// syncScopes calls roleRepository.find({ relations: ['scopes'], ... }); syncRoles calls it with select/where.
|
// syncScopes calls roleRepository.find({ relations: ['scopes'], ... }); syncRoles calls it with select/where.
|
||||||
// Return [] for the obsolete-scopes lookup so syncScopes does not save; return correctRoles for syncRoles.
|
// Return [] for the obsolete-scopes lookup so syncScopes does not save; return correctRoles for syncRoles.
|
||||||
roleRepository.find.mockImplementation(async (opts?: { relations?: string[] }) =>
|
roleRepository.find.mockImplementation(async (opts?: FindManyOptions<Role>) =>
|
||||||
opts?.relations?.includes('scopes') ? [] : correctRoles,
|
(opts?.relations as string[] | undefined)?.includes('scopes') ? [] : correctRoles,
|
||||||
);
|
);
|
||||||
roleRepository.save.mockImplementation(async (entities) => entities as never);
|
roleRepository.save.mockImplementation(async (entities) => entities as never);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ type ListSearchMethod = (
|
||||||
type LoadOptionsMethod = (this: ILoadOptionsFunctions) => Promise<INodePropertyOptions[]>;
|
type LoadOptionsMethod = (this: ILoadOptionsFunctions) => Promise<INodePropertyOptions[]>;
|
||||||
type ActionHandlerMethod = (
|
type ActionHandlerMethod = (
|
||||||
this: ILoadOptionsFunctions,
|
this: ILoadOptionsFunctions,
|
||||||
payload?: string,
|
payload?: string | IDataObject,
|
||||||
) => Promise<NodeParameterValueType>;
|
) => Promise<NodeParameterValueType>;
|
||||||
type ResourceMappingMethod = (this: ILoadOptionsFunctions) => Promise<ResourceMapperFields>;
|
type ResourceMappingMethod = (this: ILoadOptionsFunctions) => Promise<ResourceMapperFields>;
|
||||||
|
|
||||||
|
|
@ -137,7 +137,7 @@ export class DynamicNodeParametersService {
|
||||||
// Need to use untyped call since `this` usage is widespread and we don't have `strictBindCallApply`
|
// Need to use untyped call since `this` usage is widespread and we don't have `strictBindCallApply`
|
||||||
// enabled in `tsconfig.json`
|
// enabled in `tsconfig.json`
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
return method.call(thisArgs);
|
return await method.call(thisArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the available options via a loadOptions param */
|
/** Returns the available options via a loadOptions param */
|
||||||
|
|
@ -238,7 +238,7 @@ export class DynamicNodeParametersService {
|
||||||
const workflow = this.getWorkflow(nodeTypeAndVersion, currentNodeParameters, credentials);
|
const workflow = this.getWorkflow(nodeTypeAndVersion, currentNodeParameters, credentials);
|
||||||
const thisArgs = this.getThisArg(path, additionalData, workflow);
|
const thisArgs = this.getThisArg(path, additionalData, workflow);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
return method.call(thisArgs, filter, paginationToken);
|
return await method.call(thisArgs, filter, paginationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the available mapping fields for the ResourceMapper component */
|
/** Returns the available mapping fields for the ResourceMapper component */
|
||||||
|
|
@ -254,9 +254,7 @@ export class DynamicNodeParametersService {
|
||||||
const method = this.getMethod('resourceMapping', methodName, nodeType);
|
const method = this.getMethod('resourceMapping', methodName, nodeType);
|
||||||
const workflow = this.getWorkflow(nodeTypeAndVersion, currentNodeParameters, credentials);
|
const workflow = this.getWorkflow(nodeTypeAndVersion, currentNodeParameters, credentials);
|
||||||
const thisArgs = this.getThisArg(path, additionalData, workflow);
|
const thisArgs = this.getThisArg(path, additionalData, workflow);
|
||||||
return this.removeDuplicateResourceMappingFields(
|
return this.removeDuplicateResourceMappingFields(await method.call(thisArgs));
|
||||||
(await method.call(thisArgs)) as ResourceMapperFields,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the available workflow input mapping fields for the ResourceMapper component */
|
/** Returns the available workflow input mapping fields for the ResourceMapper component */
|
||||||
|
|
@ -269,9 +267,7 @@ export class DynamicNodeParametersService {
|
||||||
const nodeType = this.getNodeType(nodeTypeAndVersion);
|
const nodeType = this.getNodeType(nodeTypeAndVersion);
|
||||||
const method = this.getMethod('localResourceMapping', methodName, nodeType);
|
const method = this.getMethod('localResourceMapping', methodName, nodeType);
|
||||||
const thisArgs = this.getLocalLoadOptionsContext(path, additionalData);
|
const thisArgs = this.getLocalLoadOptionsContext(path, additionalData);
|
||||||
return this.removeDuplicateResourceMappingFields(
|
return this.removeDuplicateResourceMappingFields(await method.call(thisArgs));
|
||||||
(await method.call(thisArgs)) as ResourceMapperFields,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the result of the action handler */
|
/** Returns the result of the action handler */
|
||||||
|
|
@ -289,7 +285,7 @@ export class DynamicNodeParametersService {
|
||||||
const workflow = this.getWorkflow(nodeTypeAndVersion, currentNodeParameters, credentials);
|
const workflow = this.getWorkflow(nodeTypeAndVersion, currentNodeParameters, credentials);
|
||||||
const thisArgs = this.getThisArg(path, additionalData, workflow);
|
const thisArgs = this.getThisArg(path, additionalData, workflow);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
return method.call(thisArgs, payload);
|
return await method.call(thisArgs, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getMethod(
|
private getMethod(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { Logger } from '@n8n/backend-common';
|
import { Logger } from '@n8n/backend-common';
|
||||||
import type { AuthenticatedRequest } from '@n8n/db';
|
import { AuthenticatedRequest, UserRepository } from '@n8n/db';
|
||||||
import { UserRepository } from '@n8n/db';
|
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
import type { NextFunction, Response } from 'express';
|
import type { NextFunction, Response } from 'express';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { Time } from '@n8n/constants';
|
import { Time } from '@n8n/constants';
|
||||||
import type { AuthenticatedRequest } from '@n8n/db';
|
|
||||||
import type { RateLimiterLimits, UserKeyedRateLimiterConfig } from '@n8n/decorators';
|
import type { RateLimiterLimits, UserKeyedRateLimiterConfig } from '@n8n/decorators';
|
||||||
import { BodyKeyedRateLimiterConfig } from '@n8n/decorators';
|
import { BodyKeyedRateLimiterConfig } from '@n8n/decorators';
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
|
|
@ -8,6 +7,7 @@ import { rateLimit as expressRateLimit } from 'express-rate-limit';
|
||||||
import assert from 'node:assert';
|
import assert from 'node:assert';
|
||||||
import type { ZodTypeAny } from 'zod';
|
import type { ZodTypeAny } from 'zod';
|
||||||
import type { ZodClass } from '@n8n/api-types';
|
import type { ZodClass } from '@n8n/api-types';
|
||||||
|
import { AuthenticatedRequest } from '@n8n/db';
|
||||||
|
|
||||||
const defaultLimits: Required<RateLimiterLimits> = {
|
const defaultLimits: Required<RateLimiterLimits> = {
|
||||||
limit: 5,
|
limit: 5,
|
||||||
|
|
|
||||||
|
|
@ -442,7 +442,7 @@ export abstract class TaskRequester {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = (await func.call(funcs, ...params)) as unknown;
|
const data = await func.call(funcs, ...params);
|
||||||
|
|
||||||
this.sendMessage({
|
this.sendMessage({
|
||||||
type: 'requester:rpcresponse',
|
type: 'requester:rpcresponse',
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Logger } from '@n8n/backend-common';
|
||||||
import { TaskRunnersConfig } from '@n8n/config';
|
import { TaskRunnersConfig } from '@n8n/config';
|
||||||
import { OnShutdown } from '@n8n/decorators';
|
import { OnShutdown } from '@n8n/decorators';
|
||||||
import { Container, Service } from '@n8n/di';
|
import { Container, Service } from '@n8n/di';
|
||||||
|
import type { ServiceIdentifier } from '@n8n/di';
|
||||||
import { ErrorReporter } from 'n8n-core';
|
import { ErrorReporter } from 'n8n-core';
|
||||||
import { sleep } from 'n8n-workflow';
|
import { sleep } from 'n8n-workflow';
|
||||||
import * as a from 'node:assert/strict';
|
import * as a from 'node:assert/strict';
|
||||||
|
|
@ -135,7 +136,10 @@ export class TaskRunnerModule {
|
||||||
|
|
||||||
const failureReason = await PyTaskRunnerProcess.checkRequirements();
|
const failureReason = await PyTaskRunnerProcess.checkRequirements();
|
||||||
if (failureReason) {
|
if (failureReason) {
|
||||||
Container.get(TaskRequester).setRunnerUnavailable('python', failureReason);
|
Container.get(TaskRequester as ServiceIdentifier<TaskRequester>).setRunnerUnavailable(
|
||||||
|
'python',
|
||||||
|
failureReason,
|
||||||
|
);
|
||||||
const error = new MissingRequirementsError(failureReason);
|
const error = new MissingRequirementsError(failureReason);
|
||||||
this.logger.warn(error.message);
|
this.logger.warn(error.message);
|
||||||
return; // allow bootup, will fail at execution time
|
return; // allow bootup, will fail at execution time
|
||||||
|
|
|
||||||
7
packages/cli/src/utils/inverter.ts
Normal file
7
packages/cli/src/utils/inverter.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export function invert<T extends Record<PropertyKey, PropertyKey>>(obj: T) {
|
||||||
|
const result = {} as Record<T[keyof T], keyof T>;
|
||||||
|
for (const key in obj) {
|
||||||
|
result[obj[key]] = key;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,10 @@ jest.mock('n8n-core', () => ({
|
||||||
|
|
||||||
describe('WebhookRequestHandler', () => {
|
describe('WebhookRequestHandler', () => {
|
||||||
const webhookManager = mock<Required<IWebhookManager>>();
|
const webhookManager = mock<Required<IWebhookManager>>();
|
||||||
const handler = createWebhookHandlerFor(webhookManager);
|
const handler = createWebhookHandlerFor(webhookManager) as (
|
||||||
|
req: WebhookRequest | WebhookOptionsRequest,
|
||||||
|
res: Response,
|
||||||
|
) => Promise<void>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
|
|
@ -29,6 +32,7 @@ describe('WebhookRequestHandler', () => {
|
||||||
|
|
||||||
it('should throw for unsupported methods', async () => {
|
it('should throw for unsupported methods', async () => {
|
||||||
const req = mock<WebhookRequest | WebhookOptionsRequest>({
|
const req = mock<WebhookRequest | WebhookOptionsRequest>({
|
||||||
|
path: '/',
|
||||||
method: 'CONNECT' as IHttpRequestMethods,
|
method: 'CONNECT' as IHttpRequestMethods,
|
||||||
});
|
});
|
||||||
const res = mock<Response>();
|
const res = mock<Response>();
|
||||||
|
|
@ -46,6 +50,7 @@ describe('WebhookRequestHandler', () => {
|
||||||
describe('preflight requests', () => {
|
describe('preflight requests', () => {
|
||||||
it('should handle missing header for requested method', async () => {
|
it('should handle missing header for requested method', async () => {
|
||||||
const req = mock<WebhookRequest | WebhookOptionsRequest>({
|
const req = mock<WebhookRequest | WebhookOptionsRequest>({
|
||||||
|
path: '/',
|
||||||
method: 'OPTIONS',
|
method: 'OPTIONS',
|
||||||
headers: {
|
headers: {
|
||||||
origin: 'https://example.com',
|
origin: 'https://example.com',
|
||||||
|
|
@ -69,6 +74,7 @@ describe('WebhookRequestHandler', () => {
|
||||||
|
|
||||||
it('should handle default origin and max-age', async () => {
|
it('should handle default origin and max-age', async () => {
|
||||||
const req = mock<WebhookRequest | WebhookOptionsRequest>({
|
const req = mock<WebhookRequest | WebhookOptionsRequest>({
|
||||||
|
path: '/',
|
||||||
method: 'OPTIONS',
|
method: 'OPTIONS',
|
||||||
headers: {
|
headers: {
|
||||||
origin: 'https://example.com',
|
origin: 'https://example.com',
|
||||||
|
|
@ -95,6 +101,7 @@ describe('WebhookRequestHandler', () => {
|
||||||
it('should handle wildcard origin', async () => {
|
it('should handle wildcard origin', async () => {
|
||||||
const randomOrigin = randomString(10);
|
const randomOrigin = randomString(10);
|
||||||
const req = mock<WebhookRequest | WebhookOptionsRequest>({
|
const req = mock<WebhookRequest | WebhookOptionsRequest>({
|
||||||
|
path: '/',
|
||||||
method: 'OPTIONS',
|
method: 'OPTIONS',
|
||||||
headers: {
|
headers: {
|
||||||
origin: randomOrigin,
|
origin: randomOrigin,
|
||||||
|
|
@ -122,6 +129,7 @@ describe('WebhookRequestHandler', () => {
|
||||||
|
|
||||||
it('should handle custom origin', async () => {
|
it('should handle custom origin', async () => {
|
||||||
const req = mock<WebhookRequest | WebhookOptionsRequest>({
|
const req = mock<WebhookRequest | WebhookOptionsRequest>({
|
||||||
|
path: '/',
|
||||||
method: 'OPTIONS',
|
method: 'OPTIONS',
|
||||||
headers: {
|
headers: {
|
||||||
origin: 'https://example.com',
|
origin: 'https://example.com',
|
||||||
|
|
@ -151,6 +159,7 @@ describe('WebhookRequestHandler', () => {
|
||||||
describe('webhook requests', () => {
|
describe('webhook requests', () => {
|
||||||
it('should delegate the request to the webhook manager and send the response', async () => {
|
it('should delegate the request to the webhook manager and send the response', async () => {
|
||||||
const req = mock<WebhookRequest>({
|
const req = mock<WebhookRequest>({
|
||||||
|
path: '/',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params: { path: 'test' },
|
params: { path: 'test' },
|
||||||
});
|
});
|
||||||
|
|
@ -177,6 +186,7 @@ describe('WebhookRequestHandler', () => {
|
||||||
it('should send an error response if webhook execution throws', async () => {
|
it('should send an error response if webhook execution throws', async () => {
|
||||||
class TestError extends ResponseError {}
|
class TestError extends ResponseError {}
|
||||||
const req = mock<WebhookRequest>({
|
const req = mock<WebhookRequest>({
|
||||||
|
path: '/',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params: { path: 'test' },
|
params: { path: 'test' },
|
||||||
});
|
});
|
||||||
|
|
@ -201,6 +211,7 @@ describe('WebhookRequestHandler', () => {
|
||||||
|
|
||||||
it('should not throw when legacy response headers contain invalid names', async () => {
|
it('should not throw when legacy response headers contain invalid names', async () => {
|
||||||
const req = mock<WebhookRequest>({
|
const req = mock<WebhookRequest>({
|
||||||
|
path: '/',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params: { path: 'test' },
|
params: { path: 'test' },
|
||||||
});
|
});
|
||||||
|
|
@ -226,6 +237,7 @@ describe('WebhookRequestHandler', () => {
|
||||||
|
|
||||||
it('should not allow user to override CSP via response headers', async () => {
|
it('should not allow user to override CSP via response headers', async () => {
|
||||||
const req = mock<WebhookRequest>({
|
const req = mock<WebhookRequest>({
|
||||||
|
path: '/',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params: { path: 'test' },
|
params: { path: 'test' },
|
||||||
});
|
});
|
||||||
|
|
@ -251,6 +263,7 @@ describe('WebhookRequestHandler', () => {
|
||||||
"should handle '%s' method",
|
"should handle '%s' method",
|
||||||
async (method) => {
|
async (method) => {
|
||||||
const req = mock<WebhookRequest>({
|
const req = mock<WebhookRequest>({
|
||||||
|
path: '/',
|
||||||
method,
|
method,
|
||||||
params: { path: 'test' },
|
params: { path: 'test' },
|
||||||
});
|
});
|
||||||
|
|
@ -274,6 +287,7 @@ describe('WebhookRequestHandler', () => {
|
||||||
describe('CSP sandbox header', () => {
|
describe('CSP sandbox header', () => {
|
||||||
it('should set CSP sandbox header on all webhook responses', async () => {
|
it('should set CSP sandbox header on all webhook responses', async () => {
|
||||||
const req = mock<WebhookRequest>({
|
const req = mock<WebhookRequest>({
|
||||||
|
path: '/',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params: { path: 'test' },
|
params: { path: 'test' },
|
||||||
});
|
});
|
||||||
|
|
@ -296,6 +310,7 @@ describe('WebhookRequestHandler', () => {
|
||||||
jest.mocked(isWebhookHtmlSandboxingDisabled).mockReturnValueOnce(true);
|
jest.mocked(isWebhookHtmlSandboxingDisabled).mockReturnValueOnce(true);
|
||||||
|
|
||||||
const req = mock<WebhookRequest>({
|
const req = mock<WebhookRequest>({
|
||||||
|
path: '/',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params: { path: 'test' },
|
params: { path: 'test' },
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import { TestWebhookRegistrationsService } from '@/webhooks/test-webhook-registr
|
||||||
import * as WebhookHelpers from '@/webhooks/webhook-helpers';
|
import * as WebhookHelpers from '@/webhooks/webhook-helpers';
|
||||||
import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data';
|
import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data';
|
||||||
import type { WorkflowRequest } from '@/workflows/workflow.request';
|
import type { WorkflowRequest } from '@/workflows/workflow.request';
|
||||||
|
import { WebhookResponse } from './webhook-response';
|
||||||
|
|
||||||
import { authAllowlistedNodes } from './constants';
|
import { authAllowlistedNodes } from './constants';
|
||||||
import { matchesExpectedNodeType } from './node-type-matcher';
|
import { matchesExpectedNodeType } from './node-type-matcher';
|
||||||
|
|
@ -70,7 +71,7 @@ export class TestWebhooks implements IWebhookManager {
|
||||||
request: WebhookRequest,
|
request: WebhookRequest,
|
||||||
response: express.Response,
|
response: express.Response,
|
||||||
expectedNodeType?: ExpectedWebhookNodeType,
|
expectedNodeType?: ExpectedWebhookNodeType,
|
||||||
): Promise<IWebhookResponseCallbackData> {
|
): Promise<IWebhookResponseCallbackData | WebhookResponse> {
|
||||||
const httpMethod = request.method;
|
const httpMethod = request.method;
|
||||||
|
|
||||||
let path = removeTrailingSlash(request.params.path);
|
let path = removeTrailingSlash(request.params.path);
|
||||||
|
|
@ -158,7 +159,7 @@ export class TestWebhooks implements IWebhookManager {
|
||||||
undefined, // executionId
|
undefined, // executionId
|
||||||
request,
|
request,
|
||||||
response,
|
response,
|
||||||
(error: Error | null, data: IWebhookResponseCallbackData) => {
|
(error: Error | null, data: IWebhookResponseCallbackData | WebhookResponse) => {
|
||||||
if (error !== null) reject(error);
|
if (error !== null) reject(error);
|
||||||
else resolve(data);
|
else resolve(data);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import type {
|
||||||
IDataObject,
|
IDataObject,
|
||||||
IDeferredPromise,
|
IDeferredPromise,
|
||||||
IExecuteData,
|
IExecuteData,
|
||||||
|
IExecuteResponsePromiseData,
|
||||||
IN8nHttpFullResponse,
|
IN8nHttpFullResponse,
|
||||||
INode,
|
INode,
|
||||||
IPinData,
|
IPinData,
|
||||||
|
|
@ -754,7 +755,7 @@ export async function executeWebhook(
|
||||||
true,
|
true,
|
||||||
!didSendResponse && !shouldDeferOnReceivedResponse,
|
!didSendResponse && !shouldDeferOnReceivedResponse,
|
||||||
executionId,
|
executionId,
|
||||||
responsePromise,
|
responsePromise as IDeferredPromise<IExecuteResponsePromiseData> | undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -238,14 +238,16 @@ class WebhookRequestHandler {
|
||||||
export function createWebhookHandlerFor(
|
export function createWebhookHandlerFor(
|
||||||
webhookManager: IWebhookManager,
|
webhookManager: IWebhookManager,
|
||||||
expectedNodeType?: ExpectedWebhookNodeType,
|
expectedNodeType?: ExpectedWebhookNodeType,
|
||||||
) {
|
): express.RequestHandler {
|
||||||
const handler = new WebhookRequestHandler(webhookManager, expectedNodeType);
|
const handler = new WebhookRequestHandler(webhookManager, expectedNodeType);
|
||||||
|
|
||||||
return async (req: WebhookRequest | WebhookOptionsRequest, res: express.Response) => {
|
return async (req, res) => {
|
||||||
const { params } = req;
|
const webhookRequest = req as WebhookRequest | WebhookOptionsRequest;
|
||||||
|
|
||||||
|
const { params } = webhookRequest;
|
||||||
if (Array.isArray(params.path)) {
|
if (Array.isArray(params.path)) {
|
||||||
params.path = params.path.join('/');
|
params.path = params.path.join('/');
|
||||||
}
|
}
|
||||||
await handler.handleRequest(req, res);
|
await handler.handleRequest(webhookRequest, res);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -427,7 +427,7 @@ export class WebhookService {
|
||||||
webhookData,
|
webhookData,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (await webhookFn.call(context)) as boolean;
|
return await webhookFn.call(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -463,7 +463,7 @@ export class WebhookService {
|
||||||
try {
|
try {
|
||||||
return nodeType instanceof Node
|
return nodeType instanceof Node
|
||||||
? await nodeType.webhook(context)
|
? await nodeType.webhook(context)
|
||||||
: ((await nodeType.webhook.call(context)) as IWebhookResponseData);
|
: await nodeType.webhook.call(context);
|
||||||
} finally {
|
} finally {
|
||||||
const settledResults = await Promise.allSettled(closeFunctions.map(async (fn) => await fn()));
|
const settledResults = await Promise.allSettled(closeFunctions.map(async (fn) => await fn()));
|
||||||
for (const result of settledResults) {
|
for (const result of settledResults) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import type { Request, Response } from 'express';
|
||||||
import type { IDataObject, IHttpRequestMethods } from 'n8n-workflow';
|
import type { IDataObject, IHttpRequestMethods } from 'n8n-workflow';
|
||||||
|
|
||||||
import type { ExpectedWebhookNodeType } from './node-type-matcher';
|
import type { ExpectedWebhookNodeType } from './node-type-matcher';
|
||||||
|
import type { WebhookResponse } from './webhook-response';
|
||||||
|
|
||||||
export type WebhookOptionsRequest = Request & { method: 'OPTIONS' };
|
export type WebhookOptionsRequest = Request & { method: 'OPTIONS' };
|
||||||
|
|
||||||
|
|
@ -32,7 +33,7 @@ export interface IWebhookManager {
|
||||||
req: WebhookRequest,
|
req: WebhookRequest,
|
||||||
res: Response,
|
res: Response,
|
||||||
expectedNodeType?: ExpectedWebhookNodeType,
|
expectedNodeType?: ExpectedWebhookNodeType,
|
||||||
): Promise<IWebhookResponseCallbackData>;
|
): Promise<IWebhookResponseCallbackData | WebhookResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWebhookResponseCallbackData {
|
export interface IWebhookResponseCallbackData {
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,11 @@ import { Logger, ModuleRegistry } from '@n8n/backend-common';
|
||||||
import { GlobalConfig, SsrfProtectionConfig } from '@n8n/config';
|
import { GlobalConfig, SsrfProtectionConfig } from '@n8n/config';
|
||||||
import { ExecutionRepository, WorkflowRepository } from '@n8n/db';
|
import { ExecutionRepository, WorkflowRepository } from '@n8n/db';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
|
import type { ServiceIdentifier } from '@n8n/di';
|
||||||
import { ExternalSecretsProxy, WorkflowExecute } from 'n8n-core';
|
import { ExternalSecretsProxy, WorkflowExecute } from 'n8n-core';
|
||||||
import { UnexpectedError, Workflow, createRunExecutionData } from 'n8n-workflow';
|
import { UnexpectedError, Workflow, createRunExecutionData } from 'n8n-workflow';
|
||||||
import type {
|
import type {
|
||||||
|
AiEvent,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
IExecuteData,
|
IExecuteData,
|
||||||
IExecuteWorkflowInfo,
|
IExecuteWorkflowInfo,
|
||||||
|
|
@ -35,7 +37,7 @@ import type {
|
||||||
import { ActiveExecutions } from '@/active-executions';
|
import { ActiveExecutions } from '@/active-executions';
|
||||||
import { CredentialsHelper } from '@/credentials-helper';
|
import { CredentialsHelper } from '@/credentials-helper';
|
||||||
import { EventService } from '@/events/event.service';
|
import { EventService } from '@/events/event.service';
|
||||||
import type { AiEventMap, AiEventPayload } from '@/events/maps/ai.event-map';
|
import type { AiEventPayload } from '@/events/maps/ai.event-map';
|
||||||
import { getLifecycleHooksForSubExecutions } from '@/execution-lifecycle/execution-lifecycle-hooks';
|
import { getLifecycleHooksForSubExecutions } from '@/execution-lifecycle/execution-lifecycle-hooks';
|
||||||
import { FailedRunFactory } from '@/executions/failed-run-factory';
|
import { FailedRunFactory } from '@/executions/failed-run-factory';
|
||||||
import { isManualOrChatExecution } from '@/executions/execution.utils';
|
import { isManualOrChatExecution } from '@/executions/execution.utils';
|
||||||
|
|
@ -415,7 +417,7 @@ async function startExecution(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setExecutionStatus(status: ExecutionStatus) {
|
export function setExecutionStatus(this: { executionId?: string }, status: ExecutionStatus) {
|
||||||
const logger = Container.get(Logger);
|
const logger = Container.get(Logger);
|
||||||
if (this.executionId === undefined) {
|
if (this.executionId === undefined) {
|
||||||
logger.debug(`Setting execution status "${status}" failed because executionId is undefined`);
|
logger.debug(`Setting execution status "${status}" failed because executionId is undefined`);
|
||||||
|
|
@ -425,7 +427,11 @@ export function setExecutionStatus(status: ExecutionStatus) {
|
||||||
Container.get(ActiveExecutions).setStatus(this.executionId, status);
|
Container.get(ActiveExecutions).setStatus(this.executionId, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendDataToUI(type: PushType, data: IDataObject | IDataObject[]) {
|
export function sendDataToUI(
|
||||||
|
this: { pushRef?: string },
|
||||||
|
type: PushType,
|
||||||
|
data: IDataObject | IDataObject[],
|
||||||
|
) {
|
||||||
const { pushRef } = this;
|
const { pushRef } = this;
|
||||||
if (pushRef === undefined) {
|
if (pushRef === undefined) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -538,9 +544,11 @@ export async function getBase({
|
||||||
executeData,
|
executeData,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
logAiEvent: (eventName: keyof AiEventMap, payload: AiEventPayload) =>
|
logAiEvent: (eventName: AiEvent, payload: AiEventPayload) => {
|
||||||
eventService.emit(eventName, payload),
|
eventService.emit(eventName, payload);
|
||||||
getRunnerStatus: (taskType: string) => Container.get(TaskRequester).getRunnerStatus(taskType),
|
},
|
||||||
|
getRunnerStatus: (taskType: string) =>
|
||||||
|
Container.get(TaskRequester as ServiceIdentifier<TaskRequester>).getRunnerStatus(taskType),
|
||||||
};
|
};
|
||||||
|
|
||||||
const ssrfConfig = Container.get(SsrfProtectionConfig);
|
const ssrfConfig = Container.get(SsrfProtectionConfig);
|
||||||
|
|
|
||||||
|
|
@ -251,6 +251,12 @@ export class WorkflowService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isWorkflowWithSharing(
|
||||||
|
workflow: ListQueryDb.Workflow.Plain,
|
||||||
|
): workflow is ListQueryDb.Workflow.WithSharing {
|
||||||
|
return 'shared' in workflow;
|
||||||
|
}
|
||||||
|
|
||||||
private cleanupSharedField(
|
private cleanupSharedField(
|
||||||
workflows: ListQueryDb.Workflow.Plain[] | ListQueryDb.Workflow.WithSharing[],
|
workflows: ListQueryDb.Workflow.Plain[] | ListQueryDb.Workflow.WithSharing[],
|
||||||
): void {
|
): void {
|
||||||
|
|
@ -260,7 +266,9 @@ export class WorkflowService {
|
||||||
though. So to avoid leaking the information we just delete it.
|
though. So to avoid leaking the information we just delete it.
|
||||||
*/
|
*/
|
||||||
workflows.forEach((workflow) => {
|
workflows.forEach((workflow) => {
|
||||||
delete workflow.shared;
|
if (this.isWorkflowWithSharing(workflow)) {
|
||||||
|
delete workflow.shared;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -144,9 +144,9 @@ describe('init()', () => {
|
||||||
|
|
||||||
describe('add()', () => {
|
describe('add()', () => {
|
||||||
describe('in single-main mode', () => {
|
describe('in single-main mode', () => {
|
||||||
test.each(['activate', 'update'])(
|
test.each<WorkflowActivateMode>(['activate', 'update'])(
|
||||||
"should add webhooks, triggers and pollers for workflow in '%s' activation mode",
|
"should add webhooks, triggers and pollers for workflow in '%s' activation mode",
|
||||||
async (mode: WorkflowActivateMode) => {
|
async (mode) => {
|
||||||
await activeWorkflowManager.init();
|
await activeWorkflowManager.init();
|
||||||
|
|
||||||
const dbWorkflow = await createActiveWorkflow();
|
const dbWorkflow = await createActiveWorkflow();
|
||||||
|
|
|
||||||
|
|
@ -534,8 +534,9 @@ describe('SourceControlService', () => {
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
fsReadFile.mockImplementation(async (path: string) => {
|
fsReadFile.mockImplementation(async (path) => {
|
||||||
const pathWithoutCwd = isAbsolute(path) ? basename(path) : path;
|
const pathStr = String(path);
|
||||||
|
const pathWithoutCwd = isAbsolute(pathStr) ? basename(pathStr) : pathStr;
|
||||||
return JSON.stringify(gitFiles[pathWithoutCwd]);
|
return JSON.stringify(gitFiles[pathWithoutCwd]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,11 @@ import { gzipSync, deflateSync } from 'zlib';
|
||||||
import { rawBodyReader, bodyParser } from '@/middlewares/body-parser';
|
import { rawBodyReader, bodyParser } from '@/middlewares/body-parser';
|
||||||
|
|
||||||
describe('bodyParser', () => {
|
describe('bodyParser', () => {
|
||||||
const server = createServer((req: Request, res: Response) => {
|
const server = createServer((req, res) => {
|
||||||
void rawBodyReader(req, res, async () => {
|
const expressReq = req as unknown as Request;
|
||||||
void bodyParser(req, res, () => res.end(JSON.stringify(req.body)));
|
const expressRes = res as unknown as Response;
|
||||||
|
void rawBodyReader(expressReq, expressRes, async () => {
|
||||||
|
void bodyParser(expressReq, expressRes, () => res.end(JSON.stringify(expressReq.body)));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,13 @@ export class LicenseMocker {
|
||||||
|
|
||||||
mock(license: License) {
|
mock(license: License) {
|
||||||
license.isLicensed = this.isFeatureEnabled.bind(this);
|
license.isLicensed = this.isFeatureEnabled.bind(this);
|
||||||
license.getValue = this.getFeatureValue.bind(this);
|
license.getValue = this.getFeatureValue.bind(this) as typeof license.getValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
mockLicenseState(licenseState: LicenseState) {
|
mockLicenseState(licenseState: LicenseState) {
|
||||||
const licenseProvider: LicenseProvider = {
|
const licenseProvider: LicenseProvider = {
|
||||||
isLicensed: this.isFeatureEnabled.bind(this),
|
isLicensed: this.isFeatureEnabled.bind(this),
|
||||||
getValue: this.getFeatureValue.bind(this),
|
getValue: this.getFeatureValue.bind(this) as LicenseProvider['getValue'],
|
||||||
};
|
};
|
||||||
|
|
||||||
licenseState.setLicenseProvider(licenseProvider);
|
licenseState.setLicenseProvider(licenseProvider);
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,10 @@
|
||||||
"@test-integration/*": ["./test/integration/shared/*"]
|
"@test-integration/*": ["./test/integration/shared/*"]
|
||||||
},
|
},
|
||||||
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo",
|
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo",
|
||||||
|
"strict": true,
|
||||||
|
"strictFunctionTypes": false,
|
||||||
|
"strictPropertyInitialization": false,
|
||||||
// TODO: remove all options below this line
|
// TODO: remove all options below this line
|
||||||
"strict": false,
|
|
||||||
"useUnknownInCatchVariables": false
|
"useUnknownInCatchVariables": false
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "test/**/*.ts", "src/sso.ee/saml/saml-schema-metadata-2.0.xsd"],
|
"include": ["src/**/*.ts", "test/**/*.ts", "src/sso.ee/saml/saml-schema-metadata-2.0.xsd"],
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ export type ExecutionLifecycleHookHandlers = {
|
||||||
(
|
(
|
||||||
this: ExecutionLifecycleHooks,
|
this: ExecutionLifecycleHooks,
|
||||||
workflow: Workflow,
|
workflow: Workflow,
|
||||||
data?: IRunExecutionData,
|
data: IRunExecutionData,
|
||||||
) => Promise<void> | void
|
) => Promise<void> | void
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ interface MemoryMessage {
|
||||||
lc: number;
|
lc: number;
|
||||||
type: string;
|
type: string;
|
||||||
id: string[];
|
id: string[];
|
||||||
kwargs: {
|
kwargs?: {
|
||||||
content: unknown;
|
content: unknown;
|
||||||
additional_kwargs: Record<string, unknown>;
|
additional_kwargs: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
@ -26,6 +26,27 @@ const fallbackParser = (execData: IDataObject) => ({
|
||||||
parsed: false,
|
parsed: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function isMemoryMessage(message: unknown): message is MemoryMessage {
|
||||||
|
if (typeof message !== 'object' || message === null || Array.isArray(message)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!('lc' in message) || typeof message.lc !== 'number') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!('type' in message) || typeof message.type !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!('id' in message) ||
|
||||||
|
!Array.isArray(message.id) ||
|
||||||
|
!message.id.every((item): item is string => typeof item === 'string')
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const outputTypeParsers: {
|
const outputTypeParsers: {
|
||||||
[key in AllowedEndpointType]: (execData: IDataObject) => {
|
[key in AllowedEndpointType]: (execData: IDataObject) => {
|
||||||
type: 'json' | 'text' | 'markdown';
|
type: 'json' | 'text' | 'markdown';
|
||||||
|
|
@ -93,12 +114,9 @@ const outputTypeParsers: {
|
||||||
(execData?.response as IDataObject)?.chat_history;
|
(execData?.response as IDataObject)?.chat_history;
|
||||||
if (Array.isArray(chatHistory)) {
|
if (Array.isArray(chatHistory)) {
|
||||||
const responseText = chatHistory
|
const responseText = chatHistory
|
||||||
|
.filter(isMemoryMessage)
|
||||||
.map((content: MemoryMessage) => {
|
.map((content: MemoryMessage) => {
|
||||||
if (
|
if (content.type === 'constructor' && content.id.includes('messages') && content.kwargs) {
|
||||||
content.type === 'constructor' &&
|
|
||||||
content.id?.includes('messages') &&
|
|
||||||
content.kwargs
|
|
||||||
) {
|
|
||||||
interface MessageContent {
|
interface MessageContent {
|
||||||
type: string;
|
type: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
|
|
@ -126,7 +144,10 @@ const outputTypeParsers: {
|
||||||
})
|
})
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
if (Object.keys(content.kwargs.additional_kwargs).length) {
|
if (
|
||||||
|
content.kwargs.additional_kwargs &&
|
||||||
|
Object.keys(content.kwargs.additional_kwargs).length
|
||||||
|
) {
|
||||||
message += ` (${JSON.stringify(content.kwargs.additional_kwargs)})`;
|
message += ` (${JSON.stringify(content.kwargs.additional_kwargs)})`;
|
||||||
}
|
}
|
||||||
if (content.id.includes('HumanMessage')) {
|
if (content.id.includes('HumanMessage')) {
|
||||||
|
|
|
||||||
|
|
@ -1240,7 +1240,7 @@ export interface IPollFunctions
|
||||||
__emit(
|
__emit(
|
||||||
data: INodeExecutionData[][],
|
data: INodeExecutionData[][],
|
||||||
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
|
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
|
||||||
donePromise?: IDeferredPromise<IRun>,
|
donePromise?: IDeferredPromise<IRun | undefined>,
|
||||||
): void;
|
): void;
|
||||||
__emitError(error: Error, responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>): void;
|
__emitError(error: Error, responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>): void;
|
||||||
getNodeParameter(
|
getNodeParameter(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user