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:
Matsu 2026-04-27 17:21:31 +03:00 committed by GitHub
parent 553976d065
commit 370b281216
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
63 changed files with 322 additions and 192 deletions

View File

@ -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.

View File

@ -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;

View File

@ -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();
}); });
}); });

View File

@ -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');

View File

@ -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
*/ */

View File

@ -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;

View File

@ -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;
} }

View File

@ -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.

View File

@ -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,

View File

@ -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',
}, },
]; ];

View File

@ -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;

View File

@ -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,

View File

@ -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',
}); });

View File

@ -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}"`);

View File

@ -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) {

View File

@ -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,

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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,
]; ];

View File

@ -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';

View File

@ -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.

View File

@ -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()];
}; };

View File

@ -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);

View File

@ -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) {

View File

@ -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) {

View File

@ -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);

View File

@ -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();

View File

@ -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);
} }

View File

@ -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;

View File

@ -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 [];
} }

View File

@ -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

View File

@ -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 {

View File

@ -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;

View File

@ -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);
},
); );
} }

View 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);
}

View File

@ -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
// ---------------------------------- // ----------------------------------

View File

@ -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;
}); });

View File

@ -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'] }],

View File

@ -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);
}); });
} }
} }

View File

@ -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);

View File

@ -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(

View File

@ -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';

View File

@ -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,

View File

@ -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',

View File

@ -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

View 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;
}

View File

@ -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' },
}); });

View File

@ -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);
}, },

View File

@ -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,
); );
/** /**

View File

@ -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);
}; };
} }

View File

@ -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) {

View File

@ -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 {

View File

@ -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);

View File

@ -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;
}
}); });
} }

View File

@ -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();

View File

@ -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]);
}); });
}); });

View File

@ -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)));
}); });
}); });

View File

@ -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);

View File

@ -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"],

View File

@ -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
>; >;

View File

@ -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')) {

View File

@ -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(