From 370b281216903fedfb469ff5f70493a0efcb24f6 Mon Sep 17 00:00:00 2001 From: Matsu Date: Mon, 27 Apr 2026 17:21:31 +0300 Subject: [PATCH] chore(core): Enable TypeScript strict mode in packages/cli (no-changelog) (#27876) Co-authored-by: Claude Sonnet 4.6 --- packages/@n8n/db/src/entities/types-db.ts | 4 ++ packages/@n8n/di/src/di.ts | 4 +- packages/cli/src/__tests__/node-types.test.ts | 8 ++-- packages/cli/src/abstract-server.ts | 2 - .../concurrency-control.service.test.ts | 40 +++++++++---------- packages/cli/src/controller.registry.ts | 11 ++--- .../__tests__/ai.controller.test.ts | 4 +- .../src/credentials/credentials.service.ts | 9 +++-- .../__tests__/deprecation.service.test.ts | 4 +- .../src/deprecation/deprecation.service.ts | 2 +- packages/cli/src/events/maps/ai.event-map.ts | 2 + .../execution-lifecycle-hooks.ts | 10 ++--- .../parse-range-query.middleware.ts | 10 +++-- packages/cli/src/external-hooks.ts | 2 +- .../cli/src/middlewares/list-query/filter.ts | 14 +++---- .../cli/src/middlewares/list-query/index.ts | 4 +- .../src/middlewares/list-query/pagination.ts | 19 ++++----- .../cli/src/middlewares/list-query/select.ts | 8 ++-- .../cli/src/middlewares/list-query/sort-by.ts | 8 ++-- .../chat-hub/chat-hub-workflow.service.ts | 2 +- .../src/modules/chat-hub/stream-capturer.ts | 3 +- .../services/dynamic-credential.service.ts | 4 +- .../modules/dynamic-credentials.ee/utils.ts | 4 +- .../provider-lifecycle.service.test.ts | 2 +- .../database/entities/insights-by-period.ts | 14 ++++--- .../database/entities/insights-raw.ts | 7 ++-- .../database/entities/insights-shared.ts | 23 ++++------- .../insights-by-period-query.helper.test.ts | 4 +- .../modules/instance-ai/web-research/cache.ts | 2 +- ...essage-event-bus-destination-webhook.ee.ts | 7 +++- .../quick-connect/quick-connect.config.ts | 4 +- .../src/oauth/__tests__/oauth.service.test.ts | 5 ++- .../credentials/credentials.service.ts | 17 +++++++- .../shared/middlewares/global.middleware.ts | 12 +++--- packages/cli/src/push/index.ts | 20 ++++++++-- packages/cli/src/push/push-helpers.ts | 20 ++++++++++ packages/cli/src/requests.ts | 6 +++ .../scaling/__tests__/worker-server.test.ts | 2 +- packages/cli/src/scaling/job-processor.ts | 11 ++++- .../cli/src/scaling/multi-main-setup.ee.ts | 2 +- .../__tests__/auth.roles.service.test.ts | 6 +-- .../dynamic-node-parameters.service.ts | 16 +++----- .../src/services/last-active-at.service.ts | 3 +- .../cli/src/services/rate-limit.service.ts | 2 +- .../task-managers/task-requester.ts | 2 +- .../src/task-runners/task-runner-module.ts | 6 ++- packages/cli/src/utils/inverter.ts | 7 ++++ .../__tests__/webhook-request-handler.test.ts | 17 +++++++- packages/cli/src/webhooks/test-webhooks.ts | 5 ++- packages/cli/src/webhooks/webhook-helpers.ts | 3 +- .../src/webhooks/webhook-request-handler.ts | 10 +++-- packages/cli/src/webhooks/webhook.service.ts | 4 +- packages/cli/src/webhooks/webhook.types.ts | 3 +- .../src/workflow-execute-additional-data.ts | 20 +++++++--- .../cli/src/workflows/workflow.service.ts | 10 ++++- .../active-workflow-manager.test.ts | 4 +- .../source-control.service.test.ts | 5 ++- .../middlewares/body-parser.test.ts | 8 ++-- .../cli/test/integration/shared/license.ts | 4 +- packages/cli/tsconfig.json | 4 +- .../execution-lifecycle-hooks.ts | 2 +- .../editor-ui/src/app/utils/aiUtils.ts | 35 ++++++++++++---- packages/workflow/src/interfaces.ts | 2 +- 63 files changed, 322 insertions(+), 192 deletions(-) create mode 100644 packages/cli/src/push/push-helpers.ts create mode 100644 packages/cli/src/utils/inverter.ts diff --git a/packages/@n8n/db/src/entities/types-db.ts b/packages/@n8n/db/src/entities/types-db.ts index bceee195edf..be85989495a 100644 --- a/packages/@n8n/db/src/entities/types-db.ts +++ b/packages/@n8n/db/src/entities/types-db.ts @@ -447,6 +447,10 @@ export type AuthenticatedRequest< 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 * `INodeExecutionData` in `IPinData` in a TypeORM entity field. diff --git a/packages/@n8n/di/src/di.ts b/packages/@n8n/di/src/di.ts index a6acc60ea58..5e8627ef598 100644 --- a/packages/@n8n/di/src/di.ts +++ b/packages/@n8n/di/src/di.ts @@ -7,9 +7,9 @@ import 'reflect-metadata'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Constructable = new (...args: any[]) => T; -type AbstractConstructable = abstract new (...args: unknown[]) => T; +export type AbstractConstructable = abstract new (...args: unknown[]) => T; -type ServiceIdentifier = Constructable | AbstractConstructable; +export type ServiceIdentifier = Constructable | AbstractConstructable; type Factory = (...args: unknown[]) => T; diff --git a/packages/cli/src/__tests__/node-types.test.ts b/packages/cli/src/__tests__/node-types.test.ts index bc4f0f52f1b..04be3858efa 100644 --- a/packages/cli/src/__tests__/node-types.test.ts +++ b/packages/cli/src/__tests__/node-types.test.ts @@ -176,17 +176,17 @@ describe('NodeTypes', () => { 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'); expect(result).toBe(declarativeNode.type); expect(result.execute).toBeDefined(); const runNodeSpy = jest.spyOn(RoutingNode.prototype, 'runNode').mockResolvedValue([]); - result.execute!.call(mock()); + await result.execute!.call(mock()); 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'); expect(result).not.toEqual(declarativeNode.type); expect(result.description.name).toEqual('n8n-nodes-base.declarativeNodeTool'); @@ -197,7 +197,7 @@ describe('NodeTypes', () => { expect(result.execute).toBeDefined(); const runNodeSpy = jest.spyOn(RoutingNode.prototype, 'runNode').mockResolvedValue([]); - result.execute!.call(mock()); + await result.execute!.call(mock()); expect(runNodeSpy).toHaveBeenCalled(); }); }); diff --git a/packages/cli/src/abstract-server.ts b/packages/cli/src/abstract-server.ts index 577a248ee43..987ddafb82e 100644 --- a/packages/cli/src/abstract-server.ts +++ b/packages/cli/src/abstract-server.ts @@ -67,8 +67,6 @@ export abstract class AbstractServer { private fullyReady = false; - readonly uniqueInstanceId: string; - constructor() { this.app = express(); this.app.disable('x-powered-by'); diff --git a/packages/cli/src/concurrency/__tests__/concurrency-control.service.test.ts b/packages/cli/src/concurrency/__tests__/concurrency-control.service.test.ts index bbdef1237fb..54e960df5ab 100644 --- a/packages/cli/src/concurrency/__tests__/concurrency-control.service.test.ts +++ b/packages/cli/src/concurrency/__tests__/concurrency-control.service.test.ts @@ -40,9 +40,9 @@ describe('ConcurrencyControlService', () => { }); describe('constructor', () => { - it.each(['production', 'evaluation'])( + it.each(['production', 'evaluation'])( 'should be enabled if %s cap is positive', - (type: ConcurrencyQueueType) => { + (type) => { /** * Arrange */ @@ -72,9 +72,9 @@ describe('ConcurrencyControlService', () => { }, ); - it.each(['production', 'evaluation'])( + it.each(['production', 'evaluation'])( 'should throw if %s cap is 0', - (type: ConcurrencyQueueType) => { + (type) => { /** * Arrange */ @@ -126,9 +126,9 @@ describe('ConcurrencyControlService', () => { expect(service.isEnabled).toBe(false); }); - it.each(['production', 'evaluation'])( + it.each(['production', 'evaluation'])( 'should be disabled if %s cap is lower than -1', - (type: ConcurrencyQueueType) => { + (type) => { /** * Arrange */ @@ -186,9 +186,9 @@ describe('ConcurrencyControlService', () => { describe('if enabled', () => { describe('throttle', () => { - it.each(['cli', 'error', 'integrated', 'internal', 'manual', 'retry'])( + it.each(['cli', 'error', 'integrated', 'internal', 'manual', 'retry'])( 'should do nothing on %s mode', - async (mode: ExecutionMode) => { + async (mode) => { /** * Arrange */ @@ -215,9 +215,9 @@ describe('ConcurrencyControlService', () => { }, ); - it.each(['webhook', 'trigger', 'chat'])( + it.each(['webhook', 'trigger', 'chat'])( 'should enqueue on %s mode', - async (mode: ExecutionMode) => { + async (mode) => { /** * Arrange */ @@ -272,9 +272,9 @@ describe('ConcurrencyControlService', () => { }); describe('release', () => { - it.each(['cli', 'error', 'integrated', 'internal', 'manual', 'retry'])( + it.each(['cli', 'error', 'integrated', 'internal', 'manual', 'retry'])( 'should do nothing on %s mode', - async (mode: ExecutionMode) => { + async (mode) => { /** * Arrange */ @@ -301,9 +301,9 @@ describe('ConcurrencyControlService', () => { }, ); - it.each(['webhook', 'trigger', 'chat'])( + it.each(['webhook', 'trigger', 'chat'])( 'should dequeue on %s mode', - (mode: ExecutionMode) => { + (mode) => { /** * Arrange */ @@ -358,9 +358,9 @@ describe('ConcurrencyControlService', () => { }); describe('remove', () => { - it.each(['cli', 'error', 'integrated', 'internal', 'manual', 'retry'])( + it.each(['cli', 'error', 'integrated', 'internal', 'manual', 'retry'])( 'should do nothing on %s mode', - async (mode: ExecutionMode) => { + async (mode) => { /** * Arrange */ @@ -387,9 +387,9 @@ describe('ConcurrencyControlService', () => { }, ); - it.each(['webhook', 'trigger', 'chat'])( + it.each(['webhook', 'trigger', 'chat'])( 'should remove an execution on %s mode', - (mode: ExecutionMode) => { + (mode) => { /** * Arrange */ @@ -444,9 +444,9 @@ describe('ConcurrencyControlService', () => { }); describe('removeAll', () => { - it.each(['production', 'evaluation'])( + it.each(['production', 'evaluation'])( 'should remove all executions from the %s queue', - async (type: ConcurrencyQueueType) => { + async (type) => { /** * Arrange */ diff --git a/packages/cli/src/controller.registry.ts b/packages/cli/src/controller.registry.ts index 15352fd2452..0559a5292dd 100644 --- a/packages/cli/src/controller.registry.ts +++ b/packages/cli/src/controller.registry.ts @@ -1,7 +1,6 @@ import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import { GlobalConfig } from '@n8n/config'; import { type BooleanLicenseFeature } from '@n8n/constants'; -import type { AuthenticatedRequest } from '@n8n/db'; import { ControllerRegistryMetadata } from '@n8n/decorators'; import type { AccessScope, @@ -28,6 +27,7 @@ import { userHasScopes } from '@/permissions.ee/check-access'; import { send } from '@/response-helper'; import { CorsService } from './services/cors-service'; import { inProduction } from '@n8n/backend-common'; +import { isAuthenticatedRequest } from '@n8n/db'; @Service() export class ControllerRegistry { @@ -172,7 +172,7 @@ export class ControllerRegistry { allowSkipPreviewAuth: route.allowSkipPreviewAuth ?? 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 { - return async ( - req: AuthenticatedRequest<{ credentialId?: string; workflowId?: string; projectId?: string }>, - res, - next, - ) => { + return async (req, res, next) => { + if (!isAuthenticatedRequest(req)) throw new UnauthenticatedError(); if (!req.user) throw new UnauthenticatedError(); const { scope, globalOnly } = accessScope; diff --git a/packages/cli/src/controllers/__tests__/ai.controller.test.ts b/packages/cli/src/controllers/__tests__/ai.controller.test.ts index 155cf570b48..658286ef8a1 100644 --- a/packages/cli/src/controllers/__tests__/ai.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/ai.controller.test.ts @@ -326,7 +326,7 @@ describe('AiController', () => { let abortSignalPassed: AbortSignal | undefined; // 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') { abortHandler = handler; } @@ -406,7 +406,7 @@ describe('AiController', () => { let abortHandler: (() => void) | undefined; let abortSignalPassed: AbortSignal | undefined; - response.on.mockImplementation((event: string, handler: () => void) => { + response.on.mockImplementation((event: string | symbol, handler: () => void) => { if (event === 'close') { abortHandler = handler; } diff --git a/packages/cli/src/credentials/credentials.service.ts b/packages/cli/src/credentials/credentials.service.ts index d3cc69152f3..9690639b951 100644 --- a/packages/cli/src/credentials/credentials.service.ts +++ b/packages/cli/src/credentials/credentials.service.ts @@ -460,10 +460,11 @@ export class CredentialsService { credentials: CredentialsEntity[], ): Array> { return credentials.map( - ( - c: CredentialsEntity & ScopesField, - ): ICredentialsDecrypted => { - const data = c.scopes.includes('credential:update') ? this.decrypt(c) : undefined; + (c: CredentialsEntity): ICredentialsDecrypted => { + const credWithScopes = c as CredentialsEntity & ScopesField; + const data = credWithScopes.scopes.includes('credential:update') + ? this.decrypt(c) + : undefined; // We never want to expose the oauthTokenData to the frontend, but it // expects it to check if the credential is already connected. diff --git a/packages/cli/src/deprecation/__tests__/deprecation.service.test.ts b/packages/cli/src/deprecation/__tests__/deprecation.service.test.ts index 8f2e0fa83d1..8526c19aef7 100644 --- a/packages/cli/src/deprecation/__tests__/deprecation.service.test.ts +++ b/packages/cli/src/deprecation/__tests__/deprecation.service.test.ts @@ -73,9 +73,9 @@ describe('DeprecationService', () => { }); describe('when executions.mode is not queue', () => { - test.each([['main'], ['worker'], ['webhook']])( + test.each<[InstanceType]>([['main'], ['worker'], ['webhook']])( 'should not warn for instanceType %s', - (instanceType: InstanceType) => { + (instanceType) => { process.env[envVar] = 'false'; const service = new DeprecationService( logger, diff --git a/packages/cli/src/deprecation/deprecation.service.ts b/packages/cli/src/deprecation/deprecation.service.ts index 77d1682b035..42f571b6ff4 100644 --- a/packages/cli/src/deprecation/deprecation.service.ts +++ b/packages/cli/src/deprecation/deprecation.service.ts @@ -71,7 +71,7 @@ export class DeprecationService { envVar: 'EXECUTIONS_PROCESS', 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/', - checkValue: (value: string) => value === 'own', + checkValue: (value: string | undefined): value is 'own' => value === 'own', }, ]; diff --git a/packages/cli/src/events/maps/ai.event-map.ts b/packages/cli/src/events/maps/ai.event-map.ts index 85170a0cd3e..a1a45ca80e3 100644 --- a/packages/cli/src/events/maps/ai.event-map.ts +++ b/packages/cli/src/events/maps/ai.event-map.ts @@ -16,6 +16,8 @@ export type AiEventMap = { 'ai-documents-retrieved': AiEventPayload; + 'ai-document-reranked': AiEventPayload; + 'ai-document-embedded': AiEventPayload; 'ai-query-embedded': AiEventPayload; diff --git a/packages/cli/src/execution-lifecycle/execution-lifecycle-hooks.ts b/packages/cli/src/execution-lifecycle/execution-lifecycle-hooks.ts index b4507b3105c..d50b2e2958a 100644 --- a/packages/cli/src/execution-lifecycle/execution-lifecycle-hooks.ts +++ b/packages/cli/src/execution-lifecycle/execution-lifecycle-hooks.ts @@ -58,7 +58,7 @@ class ModulesHooksRegistry { case 'workflowExecuteAfter': hooks.addHandler(eventName, async function (runData, newStaticData) { const context = { - type: 'workflowExecuteAfter', + type: 'workflowExecuteAfter' as const, workflow: this.workflowData, runData, newStaticData, @@ -73,7 +73,7 @@ class ModulesHooksRegistry { case 'nodeExecuteBefore': hooks.addHandler(eventName, async function (nodeName, taskData) { const context = { - type: 'nodeExecuteBefore', + type: 'nodeExecuteBefore' as const, workflow: this.workflowData, nodeName, taskData, @@ -87,7 +87,7 @@ class ModulesHooksRegistry { case 'nodeExecuteAfter': hooks.addHandler(eventName, async function (nodeName, taskData, executionData) { const context = { - type: 'nodeExecuteAfter', + type: 'nodeExecuteAfter' as const, workflow: this.workflowData, nodeName, taskData, @@ -102,7 +102,7 @@ class ModulesHooksRegistry { case 'workflowExecuteBefore': hooks.addHandler(eventName, async function (workflowInstance, executionData) { const context = { - type: 'workflowExecuteBefore', + type: 'workflowExecuteBefore' as const, workflow: this.workflowData, workflowInstance, executionData, @@ -116,7 +116,7 @@ class ModulesHooksRegistry { case 'workflowExecuteResume': hooks.addHandler(eventName, async function (workflowInstance, executionData) { const context = { - type: 'workflowExecuteResume', + type: 'workflowExecuteResume' as const, workflow: this.workflowData, workflowInstance, executionData, diff --git a/packages/cli/src/executions/parse-range-query.middleware.ts b/packages/cli/src/executions/parse-range-query.middleware.ts index 762e4a679a1..a36b597a34f 100644 --- a/packages/cli/src/executions/parse-range-query.middleware.ts +++ b/packages/cli/src/executions/parse-range-query.middleware.ts @@ -27,13 +27,15 @@ export const parseRangeQuery = ( try { req.rangeQuery = { 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 (lastId) req.rangeQuery.range.lastId = lastId; + if (firstId && typeof firstId === 'string') req.rangeQuery.range.firstId = firstId; + if (lastId && typeof lastId === 'string') req.rangeQuery.range.lastId = lastId; - if (req.query.filter) { + if (typeof req.query.filter === 'string') { const jsonFilter = jsonParse(req.query.filter, { errorMessage: 'Failed to parse query string', }); diff --git a/packages/cli/src/external-hooks.ts b/packages/cli/src/external-hooks.ts index acc03b4b6ad..d1a07de121f 100644 --- a/packages/cli/src/external-hooks.ts +++ b/packages/cli/src/external-hooks.ts @@ -163,7 +163,7 @@ export class ExternalHooks { for (const hookFunction of hookFunctions) { try { - await hookFunction.apply(context, hookParameters); + await hookFunction.apply(context, hookParameters!); } catch (cause) { this.logger.error(`There was a problem running hook "${hookName}"`); diff --git a/packages/cli/src/middlewares/list-query/filter.ts b/packages/cli/src/middlewares/list-query/filter.ts index 4f4cabc1bc1..6b14f423bb5 100644 --- a/packages/cli/src/middlewares/list-query/filter.ts +++ b/packages/cli/src/middlewares/list-query/filter.ts @@ -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 { toError } from '@/utils'; @@ -8,14 +8,10 @@ import { CredentialsFilter } from './dtos/credentials.filter.dto'; import { UserFilter } from './dtos/user.filter.dto'; import { WorkflowFilter } from './dtos/workflow.filter.dto'; -export const filterListQueryMiddleware = async ( - req: ListQuery.Request, - res: Response, - next: NextFunction, -) => { +export const filterListQueryMiddleware: RequestHandler = async (req, res, next) => { const { filter: rawFilter } = req.query; - if (!rawFilter) return next(); + if (!rawFilter || typeof rawFilter !== 'string') return next(); let Filter; @@ -34,7 +30,7 @@ export const filterListQueryMiddleware = async ( if (Object.keys(filter).length === 0) return next(); - req.listQueryOptions = { ...req.listQueryOptions, filter }; + appendListQueryOptions(req, { filter }); next(); } catch (maybeError) { diff --git a/packages/cli/src/middlewares/list-query/index.ts b/packages/cli/src/middlewares/list-query/index.ts index 81f6215cf9f..b0c3dc27d81 100644 --- a/packages/cli/src/middlewares/list-query/index.ts +++ b/packages/cli/src/middlewares/list-query/index.ts @@ -1,4 +1,4 @@ -import { type NextFunction, type Response } from 'express'; +import type { RequestHandler, NextFunction, Response } from 'express'; import type { ListQuery } from '@/requests'; @@ -16,7 +16,7 @@ export type ListQueryMiddleware = ( /** * @deprecated Please create Zod validators in `@n8n/api-types` instead. */ -export const listQueryMiddleware: ListQueryMiddleware[] = [ +export const listQueryMiddleware: RequestHandler[] = [ filterListQueryMiddleware, selectListQueryMiddleware, paginationListQueryMiddleware, diff --git a/packages/cli/src/middlewares/list-query/pagination.ts b/packages/cli/src/middlewares/list-query/pagination.ts index da24db6d5c0..c3978a865ef 100644 --- a/packages/cli/src/middlewares/list-query/pagination.ts +++ b/packages/cli/src/middlewares/list-query/pagination.ts @@ -1,29 +1,30 @@ import type { RequestHandler } from 'express'; import { UnexpectedError } from 'n8n-workflow'; -import type { ListQuery } from '@/requests'; +import { appendListQueryOptions } from '@/requests'; import * as ResponseHelper from '@/response-helper'; import { toError } from '@/utils'; import { Pagination } from './dtos/pagination.dto'; -export const paginationListQueryMiddleware: RequestHandler = ( - req: ListQuery.Request, - res, - next, -) => { - const { take: rawTake, skip: rawSkip = '0' } = req.query; +export const paginationListQueryMiddleware: RequestHandler = (req, res, next) => { + const { take: rawTake } = req.query; + let { skip: rawSkip = '0' } = req.query; try { if (!rawTake && req.query.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); - req.listQueryOptions = { ...req.listQueryOptions, skip, take }; + appendListQueryOptions(req, { skip, take }); next(); } catch (maybeError) { diff --git a/packages/cli/src/middlewares/list-query/select.ts b/packages/cli/src/middlewares/list-query/select.ts index afb7728c230..521c144c709 100644 --- a/packages/cli/src/middlewares/list-query/select.ts +++ b/packages/cli/src/middlewares/list-query/select.ts @@ -1,6 +1,6 @@ import type { RequestHandler } from 'express'; -import type { ListQuery } from '@/requests'; +import { appendListQueryOptions } from '@/requests'; import * as ResponseHelper from '@/response-helper'; import { toError } from '@/utils'; @@ -8,10 +8,10 @@ import { CredentialsSelect } from './dtos/credentials.select.dto'; import { UserSelect } from './dtos/user.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; - if (!rawSelect) return next(); + if (!rawSelect || typeof rawSelect !== 'string') return next(); let Select; @@ -30,7 +30,7 @@ export const selectListQueryMiddleware: RequestHandler = (req: ListQuery.Request if (Object.keys(select).length === 0) return next(); - req.listQueryOptions = { ...req.listQueryOptions, select }; + appendListQueryOptions(req, { select }); next(); } catch (maybeError) { diff --git a/packages/cli/src/middlewares/list-query/sort-by.ts b/packages/cli/src/middlewares/list-query/sort-by.ts index a62d1d42076..c052dfbe563 100644 --- a/packages/cli/src/middlewares/list-query/sort-by.ts +++ b/packages/cli/src/middlewares/list-query/sort-by.ts @@ -3,16 +3,16 @@ import { validateSync } from 'class-validator'; import type { RequestHandler } from 'express'; import { UnexpectedError } from 'n8n-workflow'; -import type { ListQuery } from '@/requests'; +import { appendListQueryOptions } from '@/requests'; import * as ResponseHelper from '@/response-helper'; import { toError } from '@/utils'; 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; - if (!sortBy) return next(); + if (!sortBy || typeof sortBy !== 'string') return next(); let SortBy; @@ -30,7 +30,7 @@ export const sortByQueryMiddleware: RequestHandler = (req: ListQuery.Request, re throw new UnexpectedError(validationError.constraints?.workflowSortBy ?? ''); } - req.listQueryOptions = { ...req.listQueryOptions, sortBy }; + appendListQueryOptions(req, { sortBy }); next(); } catch (maybeError) { diff --git a/packages/cli/src/modules/chat-hub/chat-hub-workflow.service.ts b/packages/cli/src/modules/chat-hub/chat-hub-workflow.service.ts index 72906e84715..ac2e30f745a 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub-workflow.service.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub-workflow.service.ts @@ -424,7 +424,7 @@ export class ChatHubWorkflowService { const nodeNames = new Set(nodes.map((node) => node.name)); const distinctTools = tools.map((tool, i) => { // 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, 300 + Math.floor(i / 3) * 120 - (i % 3) * 30, ]; diff --git a/packages/cli/src/modules/chat-hub/stream-capturer.ts b/packages/cli/src/modules/chat-hub/stream-capturer.ts index 9fe412d4876..93700375fe7 100644 --- a/packages/cli/src/modules/chat-hub/stream-capturer.ts +++ b/packages/cli/src/modules/chat-hub/stream-capturer.ts @@ -6,7 +6,6 @@ import { v4 as uuidv4 } from 'uuid'; import type { ChatHubMessage } from './chat-hub-message.entity'; -type Write = ServerResponse['write']; type End = ServerResponse['end']; export type ChunkTransformer = (chunk: string) => Promise; @@ -15,7 +14,7 @@ export function interceptResponseWrites( res: T, transform: ChunkTransformer, ): T { - const originalWrite = res.write.bind(res) as Write; + const originalWrite = res.write.bind(res); const originalEnd = res.end.bind(res) as End; const defaultEncoding = 'utf8'; diff --git a/packages/cli/src/modules/dynamic-credentials.ee/services/dynamic-credential.service.ts b/packages/cli/src/modules/dynamic-credentials.ee/services/dynamic-credential.service.ts index c512515114e..5c95db46581 100644 --- a/packages/cli/src/modules/dynamic-credentials.ee/services/dynamic-credential.service.ts +++ b/packages/cli/src/modules/dynamic-credentials.ee/services/dynamic-credential.service.ts @@ -1,8 +1,7 @@ import { Logger } from '@n8n/backend-common'; -import { AuthenticatedRequest } from '@n8n/db'; import { CredentialResolverError } from '@n8n/decorators'; import { Service } from '@n8n/di'; -import { NextFunction, Response } from 'express'; +import type { NextFunction, Response } from 'express'; import { Cipher } from 'n8n-core'; import type { ICredentialDataDecryptedObject, @@ -28,6 +27,7 @@ import { CredentialResolutionError } from '../errors/credential-resolution.error import { CredentialResolverNotConfiguredError } from '../errors/credential-resolver-not-configured.error'; import { CredentialResolverNotFoundError } from '../errors/credential-resolver-not-found.error'; import { MissingExecutionContextError } from '../errors/missing-execution-context.error'; +import { AuthenticatedRequest } from '@n8n/db'; /** * Service for resolving credentials dynamically via configured resolvers. diff --git a/packages/cli/src/modules/dynamic-credentials.ee/utils.ts b/packages/cli/src/modules/dynamic-credentials.ee/utils.ts index d4b52e32015..5d3a8123fc9 100644 --- a/packages/cli/src/modules/dynamic-credentials.ee/utils.ts +++ b/packages/cli/src/modules/dynamic-credentials.ee/utils.ts @@ -1,6 +1,8 @@ +import type { RequestHandler } from 'express'; + import { Container } from '@n8n/di'; import { DynamicCredentialService } from './services/dynamic-credential.service'; -export const getDynamicCredentialMiddlewares = () => { +export const getDynamicCredentialMiddlewares = (): RequestHandler[] => { return [Container.get(DynamicCredentialService).getDynamicCredentialsEndpointsMiddleware()]; }; diff --git a/packages/cli/src/modules/external-secrets.ee/__tests__/provider-lifecycle.service.test.ts b/packages/cli/src/modules/external-secrets.ee/__tests__/provider-lifecycle.service.test.ts index d867c8220b4..0fb90b88c2a 100644 --- a/packages/cli/src/modules/external-secrets.ee/__tests__/provider-lifecycle.service.test.ts +++ b/packages/cli/src/modules/external-secrets.ee/__tests__/provider-lifecycle.service.test.ts @@ -92,7 +92,7 @@ describe('ProviderLifecycle', () => { const originalConnect = provider.connect.bind(provider); jest.spyOn(provider, 'connect').mockImplementation(async function (this: DummyProvider) { stateBeforeConnect = this.state; - return originalConnect(); + return await originalConnect(); }); await lifecycle.connect(provider); diff --git a/packages/cli/src/modules/insights/database/entities/insights-by-period.ts b/packages/cli/src/modules/insights/database/entities/insights-by-period.ts index bf9c870c777..2da21e81259 100644 --- a/packages/cli/src/modules/insights/database/entities/insights-by-period.ts +++ b/packages/cli/src/modules/insights/database/entities/insights-by-period.ts @@ -36,13 +36,14 @@ export class InsightsByPeriod extends BaseEntity { private type_: number; get type() { - if (!isValidTypeNumber(this.type_)) { + const typeValue = this.type_; + if (!isValidTypeNumber(typeValue)) { 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) { @@ -61,13 +62,14 @@ export class InsightsByPeriod extends BaseEntity { private periodUnit_: number; get periodUnit() { - if (!isValidPeriodNumber(this.periodUnit_)) { + const periodUnitValue = this.periodUnit_; + if (!isValidPeriodNumber(periodUnitValue)) { 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) { diff --git a/packages/cli/src/modules/insights/database/entities/insights-raw.ts b/packages/cli/src/modules/insights/database/entities/insights-raw.ts index 34746394a30..42a997c8818 100644 --- a/packages/cli/src/modules/insights/database/entities/insights-raw.ts +++ b/packages/cli/src/modules/insights/database/entities/insights-raw.ts @@ -25,13 +25,14 @@ export class InsightsRaw extends BaseEntity { private type_: number; get type() { - if (!isValidTypeNumber(this.type_)) { + const typeValue = this.type_; + if (!isValidTypeNumber(typeValue)) { 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) { diff --git a/packages/cli/src/modules/insights/database/entities/insights-shared.ts b/packages/cli/src/modules/insights/database/entities/insights-shared.ts index 3e12622e850..bbd855ed453 100644 --- a/packages/cli/src/modules/insights/database/entities/insights-shared.ts +++ b/packages/cli/src/modules/insights/database/entities/insights-shared.ts @@ -1,3 +1,5 @@ +import { invert } from '@/utils/inverter'; + function isValid>( value: number | string | symbol, constant: T, @@ -5,6 +7,9 @@ function isValid>( return Object.keys(constant).includes(value.toString()); } +export type PeriodUnit = keyof typeof PeriodUnitToNumber; + +export type PeriodUnitNumber = (typeof PeriodUnitToNumber)[PeriodUnit]; // Periods export const PeriodUnitToNumber = { hour: 0, @@ -12,16 +17,8 @@ export const PeriodUnitToNumber = { week: 2, } 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, -); export function isValidPeriodNumber(value: number) { return isValid(value, NumberToPeriodUnit); } @@ -37,13 +34,7 @@ export const TypeToNumber = { export type TypeUnit = keyof typeof TypeToNumber; export type TypeUnitNumber = (typeof TypeToNumber)[TypeUnit]; -export const NumberToType = Object.entries(TypeToNumber).reduce( - (acc, [key, value]: [TypeUnit, TypeUnitNumber]) => { - acc[value] = key; - return acc; - }, - {} as Record, -); +export const NumberToType = invert(TypeToNumber); export function isValidTypeNumber(value: number) { return isValid(value, NumberToType); diff --git a/packages/cli/src/modules/insights/database/repositories/__tests__/insights-by-period-query.helper.test.ts b/packages/cli/src/modules/insights/database/repositories/__tests__/insights-by-period-query.helper.test.ts index 15e4f9e0a04..720724efa34 100644 --- a/packages/cli/src/modules/insights/database/repositories/__tests__/insights-by-period-query.helper.test.ts +++ b/packages/cli/src/modules/insights/database/repositories/__tests__/insights-by-period-query.helper.test.ts @@ -18,10 +18,10 @@ describe('getDateRangesCommonTableExpressionQuery', () => { jest.useRealTimers(); }); - describe.each([ + describe.each<[DatabaseConfig['type'], string]>([ ['sqlite', 'SQLite'], ['postgresdb', 'PostgreSQL'], - ])('%s', (dbType: DatabaseConfig['type']) => { + ])('%s', (dbType) => { describe('hour periodicity (1 day - startDate == endDate)', () => { test('last 24 hours (endDate is today)', () => { const startDate = now.minus({ days: 1 }).startOf('day').toJSDate(); diff --git a/packages/cli/src/modules/instance-ai/web-research/cache.ts b/packages/cli/src/modules/instance-ai/web-research/cache.ts index 3f83e629240..9727fd2ff80 100644 --- a/packages/cli/src/modules/instance-ai/web-research/cache.ts +++ b/packages/cli/src/modules/instance-ai/web-research/cache.ts @@ -39,7 +39,7 @@ export class LRUCache { // Evict oldest if at capacity 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) { this.map.delete(oldest); } diff --git a/packages/cli/src/modules/log-streaming.ee/destinations/message-event-bus-destination-webhook.ee.ts b/packages/cli/src/modules/log-streaming.ee/destinations/message-event-bus-destination-webhook.ee.ts index 1801ae10c57..d242d9a7f8f 100644 --- a/packages/cli/src/modules/log-streaming.ee/destinations/message-event-bus-destination-webhook.ee.ts +++ b/packages/cli/src/modules/log-streaming.ee/destinations/message-event-bus-destination-webhook.ee.ts @@ -230,7 +230,12 @@ export class MessageEventBusDestinationWebhook const parametersToKeyValue = async ( 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; accumulator[cur.name] = cur.value; diff --git a/packages/cli/src/modules/quick-connect/quick-connect.config.ts b/packages/cli/src/modules/quick-connect/quick-connect.config.ts index 32685eb8df6..ceef543b8b7 100644 --- a/packages/cli/src/modules/quick-connect/quick-connect.config.ts +++ b/packages/cli/src/modules/quick-connect/quick-connect.config.ts @@ -38,9 +38,9 @@ const quickConnectOptionSchema = z.union([ export type QuickConnectOption = z.infer; const quickConnectOptionsSchema = z.string().pipe( - z.preprocess((input: string) => { + z.preprocess((input: unknown) => { try { - return JSON.parse(input); + return JSON.parse(input as string); } catch { return []; } diff --git a/packages/cli/src/oauth/__tests__/oauth.service.test.ts b/packages/cli/src/oauth/__tests__/oauth.service.test.ts index 9d1c4ea37d3..b3ac118135e 100644 --- a/packages/cli/src/oauth/__tests__/oauth.service.test.ts +++ b/packages/cli/src/oauth/__tests__/oauth.service.test.ts @@ -69,10 +69,11 @@ describe('OauthService', () => { axios.post = jest.fn(); // 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 // 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) => { // For testing, decode the base64 diff --git a/packages/cli/src/public-api/v1/handlers/credentials/credentials.service.ts b/packages/cli/src/public-api/v1/handlers/credentials/credentials.service.ts index b6d0642eda8..5884685aae3 100644 --- a/packages/cli/src/public-api/v1/handlers/credentials/credentials.service.ts +++ b/packages/cli/src/public-api/v1/handlers/credentials/credentials.service.ts @@ -27,6 +27,15 @@ import type { IDependency, IJsonSchema } from '../../../types'; 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. * 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') .forEach((property) => { 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, { [property.name]: { type: 'string', - enum: property.options?.map((data: INodePropertyOptions) => data.value), + enum: isNodePropertyOptions(property.options) + ? property.options.map((data) => data.value) + : undefined, }, }); } else { diff --git a/packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts b/packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts index 5627a27b356..95d1c4d80c0 100644 --- a/packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts +++ b/packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts @@ -4,6 +4,7 @@ import type { AuthenticatedRequest } from '@n8n/db'; import { Container } from '@n8n/di'; import type { ApiKeyScope, Scope } from '@n8n/permissions'; import type express from 'express'; +import type { NextFunction, Request, Response } from 'express'; import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; @@ -85,16 +86,15 @@ export const validCursor = ( return next(); }; -export type ScopeTaggedMiddleware = ((...args: unknown[]) => unknown) & { +export type ScopeTaggedMiddleware = Middleware & { __apiKeyScope: ApiKeyScope; }; -function tagMiddleware( - middleware: (...args: unknown[]) => unknown, - apiKeyScope: ApiKeyScope, -): ScopeTaggedMiddleware { +export type Middleware = (req: Request, res: Response, next: NextFunction) => unknown; + +function tagMiddleware(middleware: Middleware, apiKeyScope: ApiKeyScope): ScopeTaggedMiddleware { 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 }, ); return tagged; diff --git a/packages/cli/src/push/index.ts b/packages/cli/src/push/index.ts index 4a8f2377f82..4b62af46bae 100644 --- a/packages/cli/src/push/index.ts +++ b/packages/cli/src/push/index.ts @@ -19,8 +19,15 @@ import { TypedEmitter } from '@/typed-emitter'; import { validateOriginHeaders } from './origin-validator'; import { PushConfig } from './push.config'; 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 { isPushResponse, isSSEPushRequest, isWebSocketPushRequest } from './push-helpers'; +import { InternalServerError } from '@/errors/response-errors/internal-server.error'; type PushEvents = { editorUiConnected: string; @@ -95,8 +102,15 @@ export class Push extends TypedEmitter { `/${restEndpoint}/push`, this.authService.createAuthMiddleware({ allowSkipMFA: false }), - (req: SSEPushRequest | WebSocketPushRequest, res: PushResponse) => - this.handleRequest(req, res), + (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); + }, ); } diff --git a/packages/cli/src/push/push-helpers.ts b/packages/cli/src/push/push-helpers.ts new file mode 100644 index 00000000000..c966f7f5b90 --- /dev/null +++ b/packages/cli/src/push/push-helpers.ts @@ -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); +} diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index 52635c5114f..60ded4ca962 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -15,6 +15,7 @@ import type { ProjectRole, Scope, } from '@n8n/permissions'; +import type { Request } from 'express'; import type { ICredentialDataDecryptedObject, 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 // ---------------------------------- diff --git a/packages/cli/src/scaling/__tests__/worker-server.test.ts b/packages/cli/src/scaling/__tests__/worker-server.test.ts index 165e87a359f..281002d953d 100644 --- a/packages/cli/src/scaling/__tests__/worker-server.test.ts +++ b/packages/cli/src/scaling/__tests__/worker-server.test.ts @@ -92,7 +92,7 @@ describe('WorkerServer', () => { 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()); return server; }); diff --git a/packages/cli/src/scaling/job-processor.ts b/packages/cli/src/scaling/job-processor.ts index 32c9bc5463c..98cdc455cef 100644 --- a/packages/cli/src/scaling/job-processor.ts +++ b/packages/cli/src/scaling/job-processor.ts @@ -12,6 +12,7 @@ import { import type { Tool } from '@langchain/core/tools'; import type { ExecutionStatus, + IDataObject, IExecuteData, IExecuteFunctions, IExecuteResponsePromiseData, @@ -20,6 +21,7 @@ import type { IWorkflowExecutionDataProcess, StructuredChunk, CloseFunction, + GenericValue, } from 'n8n-workflow'; import { BINARY_ENCODING, @@ -167,7 +169,9 @@ export class JobProcessor { if (pushRef) { // 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 => { @@ -542,7 +546,10 @@ export class JobProcessor { 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, [ [{ json: { response } as INodeExecutionData['json'] }], diff --git a/packages/cli/src/scaling/multi-main-setup.ee.ts b/packages/cli/src/scaling/multi-main-setup.ee.ts index a1290c87f17..f312aba881a 100644 --- a/packages/cli/src/scaling/multi-main-setup.ee.ts +++ b/packages/cli/src/scaling/multi-main-setup.ee.ts @@ -156,7 +156,7 @@ export class MultiMainSetup extends TypedEmitter { const instance = Container.get(eventHandlerClass); this.on(eventName, async () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return instance[methodName].call(instance); + return await instance[methodName].call(instance); }); } } diff --git a/packages/cli/src/services/__tests__/auth.roles.service.test.ts b/packages/cli/src/services/__tests__/auth.roles.service.test.ts index a13c4cc5923..d5fd85a0424 100644 --- a/packages/cli/src/services/__tests__/auth.roles.service.test.ts +++ b/packages/cli/src/services/__tests__/auth.roles.service.test.ts @@ -13,7 +13,7 @@ import { PERSONAL_SPACE_SHARING_SETTING, EXTERNAL_SECRETS_SYSTEM_ROLES_ENABLED_SETTING, } from '@n8n/permissions'; -import type { EntityManager, Repository } from '@n8n/typeorm'; +import type { EntityManager, FindManyOptions, Repository } from '@n8n/typeorm'; import { mock } from 'jest-mock-extended'; const SHARING_SCOPES = PERSONAL_SPACE_SHARING_SETTING.scopes; @@ -446,8 +446,8 @@ describe('AuthRolesService', () => { scopeRepository.find.mockResolvedValue(allScopes); // 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. - roleRepository.find.mockImplementation(async (opts?: { relations?: string[] }) => - opts?.relations?.includes('scopes') ? [] : correctRoles, + roleRepository.find.mockImplementation(async (opts?: FindManyOptions) => + (opts?.relations as string[] | undefined)?.includes('scopes') ? [] : correctRoles, ); roleRepository.save.mockImplementation(async (entities) => entities as never); diff --git a/packages/cli/src/services/dynamic-node-parameters.service.ts b/packages/cli/src/services/dynamic-node-parameters.service.ts index 3eb81f2c8d6..4c94edeaeb3 100644 --- a/packages/cli/src/services/dynamic-node-parameters.service.ts +++ b/packages/cli/src/services/dynamic-node-parameters.service.ts @@ -42,7 +42,7 @@ type ListSearchMethod = ( type LoadOptionsMethod = (this: ILoadOptionsFunctions) => Promise; type ActionHandlerMethod = ( this: ILoadOptionsFunctions, - payload?: string, + payload?: string | IDataObject, ) => Promise; type ResourceMappingMethod = (this: ILoadOptionsFunctions) => Promise; @@ -137,7 +137,7 @@ export class DynamicNodeParametersService { // Need to use untyped call since `this` usage is widespread and we don't have `strictBindCallApply` // enabled in `tsconfig.json` // 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 */ @@ -238,7 +238,7 @@ export class DynamicNodeParametersService { const workflow = this.getWorkflow(nodeTypeAndVersion, currentNodeParameters, credentials); const thisArgs = this.getThisArg(path, additionalData, workflow); // 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 */ @@ -254,9 +254,7 @@ export class DynamicNodeParametersService { const method = this.getMethod('resourceMapping', methodName, nodeType); const workflow = this.getWorkflow(nodeTypeAndVersion, currentNodeParameters, credentials); const thisArgs = this.getThisArg(path, additionalData, workflow); - return this.removeDuplicateResourceMappingFields( - (await method.call(thisArgs)) as ResourceMapperFields, - ); + return this.removeDuplicateResourceMappingFields(await method.call(thisArgs)); } /** Returns the available workflow input mapping fields for the ResourceMapper component */ @@ -269,9 +267,7 @@ export class DynamicNodeParametersService { const nodeType = this.getNodeType(nodeTypeAndVersion); const method = this.getMethod('localResourceMapping', methodName, nodeType); const thisArgs = this.getLocalLoadOptionsContext(path, additionalData); - return this.removeDuplicateResourceMappingFields( - (await method.call(thisArgs)) as ResourceMapperFields, - ); + return this.removeDuplicateResourceMappingFields(await method.call(thisArgs)); } /** Returns the result of the action handler */ @@ -289,7 +285,7 @@ export class DynamicNodeParametersService { const workflow = this.getWorkflow(nodeTypeAndVersion, currentNodeParameters, credentials); const thisArgs = this.getThisArg(path, additionalData, workflow); // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return method.call(thisArgs, payload); + return await method.call(thisArgs, payload); } private getMethod( diff --git a/packages/cli/src/services/last-active-at.service.ts b/packages/cli/src/services/last-active-at.service.ts index 4e6df98a9b6..33f52b1103e 100644 --- a/packages/cli/src/services/last-active-at.service.ts +++ b/packages/cli/src/services/last-active-at.service.ts @@ -1,6 +1,5 @@ import { Logger } from '@n8n/backend-common'; -import type { AuthenticatedRequest } from '@n8n/db'; -import { UserRepository } from '@n8n/db'; +import { AuthenticatedRequest, UserRepository } from '@n8n/db'; import { Service } from '@n8n/di'; import type { NextFunction, Response } from 'express'; import { DateTime } from 'luxon'; diff --git a/packages/cli/src/services/rate-limit.service.ts b/packages/cli/src/services/rate-limit.service.ts index 12bf296f015..bd5315ce47c 100644 --- a/packages/cli/src/services/rate-limit.service.ts +++ b/packages/cli/src/services/rate-limit.service.ts @@ -1,5 +1,4 @@ import { Time } from '@n8n/constants'; -import type { AuthenticatedRequest } from '@n8n/db'; import type { RateLimiterLimits, UserKeyedRateLimiterConfig } from '@n8n/decorators'; import { BodyKeyedRateLimiterConfig } from '@n8n/decorators'; import { Service } from '@n8n/di'; @@ -8,6 +7,7 @@ import { rateLimit as expressRateLimit } from 'express-rate-limit'; import assert from 'node:assert'; import type { ZodTypeAny } from 'zod'; import type { ZodClass } from '@n8n/api-types'; +import { AuthenticatedRequest } from '@n8n/db'; const defaultLimits: Required = { limit: 5, diff --git a/packages/cli/src/task-runners/task-managers/task-requester.ts b/packages/cli/src/task-runners/task-managers/task-requester.ts index e2288d9286e..c11dd440a64 100644 --- a/packages/cli/src/task-runners/task-managers/task-requester.ts +++ b/packages/cli/src/task-runners/task-managers/task-requester.ts @@ -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({ type: 'requester:rpcresponse', diff --git a/packages/cli/src/task-runners/task-runner-module.ts b/packages/cli/src/task-runners/task-runner-module.ts index 3a2ed1b43e5..e8cdfb21515 100644 --- a/packages/cli/src/task-runners/task-runner-module.ts +++ b/packages/cli/src/task-runners/task-runner-module.ts @@ -2,6 +2,7 @@ import { Logger } from '@n8n/backend-common'; import { TaskRunnersConfig } from '@n8n/config'; import { OnShutdown } from '@n8n/decorators'; import { Container, Service } from '@n8n/di'; +import type { ServiceIdentifier } from '@n8n/di'; import { ErrorReporter } from 'n8n-core'; import { sleep } from 'n8n-workflow'; import * as a from 'node:assert/strict'; @@ -135,7 +136,10 @@ export class TaskRunnerModule { const failureReason = await PyTaskRunnerProcess.checkRequirements(); if (failureReason) { - Container.get(TaskRequester).setRunnerUnavailable('python', failureReason); + Container.get(TaskRequester as ServiceIdentifier).setRunnerUnavailable( + 'python', + failureReason, + ); const error = new MissingRequirementsError(failureReason); this.logger.warn(error.message); return; // allow bootup, will fail at execution time diff --git a/packages/cli/src/utils/inverter.ts b/packages/cli/src/utils/inverter.ts new file mode 100644 index 00000000000..e4ecdde089d --- /dev/null +++ b/packages/cli/src/utils/inverter.ts @@ -0,0 +1,7 @@ +export function invert>(obj: T) { + const result = {} as Record; + for (const key in obj) { + result[obj[key]] = key; + } + return result; +} diff --git a/packages/cli/src/webhooks/__tests__/webhook-request-handler.test.ts b/packages/cli/src/webhooks/__tests__/webhook-request-handler.test.ts index 8d2a14503bb..b600d7b9485 100644 --- a/packages/cli/src/webhooks/__tests__/webhook-request-handler.test.ts +++ b/packages/cli/src/webhooks/__tests__/webhook-request-handler.test.ts @@ -21,7 +21,10 @@ jest.mock('n8n-core', () => ({ describe('WebhookRequestHandler', () => { const webhookManager = mock>(); - const handler = createWebhookHandlerFor(webhookManager); + const handler = createWebhookHandlerFor(webhookManager) as ( + req: WebhookRequest | WebhookOptionsRequest, + res: Response, + ) => Promise; beforeEach(() => { jest.resetAllMocks(); @@ -29,6 +32,7 @@ describe('WebhookRequestHandler', () => { it('should throw for unsupported methods', async () => { const req = mock({ + path: '/', method: 'CONNECT' as IHttpRequestMethods, }); const res = mock(); @@ -46,6 +50,7 @@ describe('WebhookRequestHandler', () => { describe('preflight requests', () => { it('should handle missing header for requested method', async () => { const req = mock({ + path: '/', method: 'OPTIONS', headers: { origin: 'https://example.com', @@ -69,6 +74,7 @@ describe('WebhookRequestHandler', () => { it('should handle default origin and max-age', async () => { const req = mock({ + path: '/', method: 'OPTIONS', headers: { origin: 'https://example.com', @@ -95,6 +101,7 @@ describe('WebhookRequestHandler', () => { it('should handle wildcard origin', async () => { const randomOrigin = randomString(10); const req = mock({ + path: '/', method: 'OPTIONS', headers: { origin: randomOrigin, @@ -122,6 +129,7 @@ describe('WebhookRequestHandler', () => { it('should handle custom origin', async () => { const req = mock({ + path: '/', method: 'OPTIONS', headers: { origin: 'https://example.com', @@ -151,6 +159,7 @@ describe('WebhookRequestHandler', () => { describe('webhook requests', () => { it('should delegate the request to the webhook manager and send the response', async () => { const req = mock({ + path: '/', method: 'GET', params: { path: 'test' }, }); @@ -177,6 +186,7 @@ describe('WebhookRequestHandler', () => { it('should send an error response if webhook execution throws', async () => { class TestError extends ResponseError {} const req = mock({ + path: '/', method: 'GET', params: { path: 'test' }, }); @@ -201,6 +211,7 @@ describe('WebhookRequestHandler', () => { it('should not throw when legacy response headers contain invalid names', async () => { const req = mock({ + path: '/', method: 'GET', params: { path: 'test' }, }); @@ -226,6 +237,7 @@ describe('WebhookRequestHandler', () => { it('should not allow user to override CSP via response headers', async () => { const req = mock({ + path: '/', method: 'GET', params: { path: 'test' }, }); @@ -251,6 +263,7 @@ describe('WebhookRequestHandler', () => { "should handle '%s' method", async (method) => { const req = mock({ + path: '/', method, params: { path: 'test' }, }); @@ -274,6 +287,7 @@ describe('WebhookRequestHandler', () => { describe('CSP sandbox header', () => { it('should set CSP sandbox header on all webhook responses', async () => { const req = mock({ + path: '/', method: 'GET', params: { path: 'test' }, }); @@ -296,6 +310,7 @@ describe('WebhookRequestHandler', () => { jest.mocked(isWebhookHtmlSandboxingDisabled).mockReturnValueOnce(true); const req = mock({ + path: '/', method: 'GET', params: { path: 'test' }, }); diff --git a/packages/cli/src/webhooks/test-webhooks.ts b/packages/cli/src/webhooks/test-webhooks.ts index 43752921f36..0bdcf80534d 100644 --- a/packages/cli/src/webhooks/test-webhooks.ts +++ b/packages/cli/src/webhooks/test-webhooks.ts @@ -26,6 +26,7 @@ import { TestWebhookRegistrationsService } from '@/webhooks/test-webhook-registr import * as WebhookHelpers from '@/webhooks/webhook-helpers'; import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data'; import type { WorkflowRequest } from '@/workflows/workflow.request'; +import { WebhookResponse } from './webhook-response'; import { authAllowlistedNodes } from './constants'; import { matchesExpectedNodeType } from './node-type-matcher'; @@ -70,7 +71,7 @@ export class TestWebhooks implements IWebhookManager { request: WebhookRequest, response: express.Response, expectedNodeType?: ExpectedWebhookNodeType, - ): Promise { + ): Promise { const httpMethod = request.method; let path = removeTrailingSlash(request.params.path); @@ -158,7 +159,7 @@ export class TestWebhooks implements IWebhookManager { undefined, // executionId request, response, - (error: Error | null, data: IWebhookResponseCallbackData) => { + (error: Error | null, data: IWebhookResponseCallbackData | WebhookResponse) => { if (error !== null) reject(error); else resolve(data); }, diff --git a/packages/cli/src/webhooks/webhook-helpers.ts b/packages/cli/src/webhooks/webhook-helpers.ts index d533c38f1b8..6948c2c1ebe 100644 --- a/packages/cli/src/webhooks/webhook-helpers.ts +++ b/packages/cli/src/webhooks/webhook-helpers.ts @@ -16,6 +16,7 @@ import type { IDataObject, IDeferredPromise, IExecuteData, + IExecuteResponsePromiseData, IN8nHttpFullResponse, INode, IPinData, @@ -754,7 +755,7 @@ export async function executeWebhook( true, !didSendResponse && !shouldDeferOnReceivedResponse, executionId, - responsePromise, + responsePromise as IDeferredPromise | undefined, ); /** diff --git a/packages/cli/src/webhooks/webhook-request-handler.ts b/packages/cli/src/webhooks/webhook-request-handler.ts index f813aeaa266..c477877b411 100644 --- a/packages/cli/src/webhooks/webhook-request-handler.ts +++ b/packages/cli/src/webhooks/webhook-request-handler.ts @@ -238,14 +238,16 @@ class WebhookRequestHandler { export function createWebhookHandlerFor( webhookManager: IWebhookManager, expectedNodeType?: ExpectedWebhookNodeType, -) { +): express.RequestHandler { const handler = new WebhookRequestHandler(webhookManager, expectedNodeType); - return async (req: WebhookRequest | WebhookOptionsRequest, res: express.Response) => { - const { params } = req; + return async (req, res) => { + const webhookRequest = req as WebhookRequest | WebhookOptionsRequest; + + const { params } = webhookRequest; if (Array.isArray(params.path)) { params.path = params.path.join('/'); } - await handler.handleRequest(req, res); + await handler.handleRequest(webhookRequest, res); }; } diff --git a/packages/cli/src/webhooks/webhook.service.ts b/packages/cli/src/webhooks/webhook.service.ts index b9a2bf0dd1d..2a50a2a3232 100644 --- a/packages/cli/src/webhooks/webhook.service.ts +++ b/packages/cli/src/webhooks/webhook.service.ts @@ -427,7 +427,7 @@ export class WebhookService { webhookData, ); - return (await webhookFn.call(context)) as boolean; + return await webhookFn.call(context); } /** @@ -463,7 +463,7 @@ export class WebhookService { try { return nodeType instanceof Node ? await nodeType.webhook(context) - : ((await nodeType.webhook.call(context)) as IWebhookResponseData); + : await nodeType.webhook.call(context); } finally { const settledResults = await Promise.allSettled(closeFunctions.map(async (fn) => await fn())); for (const result of settledResults) { diff --git a/packages/cli/src/webhooks/webhook.types.ts b/packages/cli/src/webhooks/webhook.types.ts index 53bc6db4103..7e218222b00 100644 --- a/packages/cli/src/webhooks/webhook.types.ts +++ b/packages/cli/src/webhooks/webhook.types.ts @@ -2,6 +2,7 @@ import type { Request, Response } from 'express'; import type { IDataObject, IHttpRequestMethods } from 'n8n-workflow'; import type { ExpectedWebhookNodeType } from './node-type-matcher'; +import type { WebhookResponse } from './webhook-response'; export type WebhookOptionsRequest = Request & { method: 'OPTIONS' }; @@ -32,7 +33,7 @@ export interface IWebhookManager { req: WebhookRequest, res: Response, expectedNodeType?: ExpectedWebhookNodeType, - ): Promise; + ): Promise; } export interface IWebhookResponseCallbackData { diff --git a/packages/cli/src/workflow-execute-additional-data.ts b/packages/cli/src/workflow-execute-additional-data.ts index 0b167bd568f..6626e391af0 100644 --- a/packages/cli/src/workflow-execute-additional-data.ts +++ b/packages/cli/src/workflow-execute-additional-data.ts @@ -7,9 +7,11 @@ import { Logger, ModuleRegistry } from '@n8n/backend-common'; import { GlobalConfig, SsrfProtectionConfig } from '@n8n/config'; import { ExecutionRepository, WorkflowRepository } from '@n8n/db'; import { Container } from '@n8n/di'; +import type { ServiceIdentifier } from '@n8n/di'; import { ExternalSecretsProxy, WorkflowExecute } from 'n8n-core'; import { UnexpectedError, Workflow, createRunExecutionData } from 'n8n-workflow'; import type { + AiEvent, IDataObject, IExecuteData, IExecuteWorkflowInfo, @@ -35,7 +37,7 @@ import type { import { ActiveExecutions } from '@/active-executions'; import { CredentialsHelper } from '@/credentials-helper'; 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 { FailedRunFactory } from '@/executions/failed-run-factory'; 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); if (this.executionId === 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); } -export function sendDataToUI(type: PushType, data: IDataObject | IDataObject[]) { +export function sendDataToUI( + this: { pushRef?: string }, + type: PushType, + data: IDataObject | IDataObject[], +) { const { pushRef } = this; if (pushRef === undefined) { return; @@ -538,9 +544,11 @@ export async function getBase({ executeData, ); }, - logAiEvent: (eventName: keyof AiEventMap, payload: AiEventPayload) => - eventService.emit(eventName, payload), - getRunnerStatus: (taskType: string) => Container.get(TaskRequester).getRunnerStatus(taskType), + logAiEvent: (eventName: AiEvent, payload: AiEventPayload) => { + eventService.emit(eventName, payload); + }, + getRunnerStatus: (taskType: string) => + Container.get(TaskRequester as ServiceIdentifier).getRunnerStatus(taskType), }; const ssrfConfig = Container.get(SsrfProtectionConfig); diff --git a/packages/cli/src/workflows/workflow.service.ts b/packages/cli/src/workflows/workflow.service.ts index b4872dc2c5b..e52560eac3d 100644 --- a/packages/cli/src/workflows/workflow.service.ts +++ b/packages/cli/src/workflows/workflow.service.ts @@ -251,6 +251,12 @@ export class WorkflowService { ); } + private isWorkflowWithSharing( + workflow: ListQueryDb.Workflow.Plain, + ): workflow is ListQueryDb.Workflow.WithSharing { + return 'shared' in workflow; + } + private cleanupSharedField( workflows: ListQueryDb.Workflow.Plain[] | ListQueryDb.Workflow.WithSharing[], ): void { @@ -260,7 +266,9 @@ export class WorkflowService { though. So to avoid leaking the information we just delete it. */ workflows.forEach((workflow) => { - delete workflow.shared; + if (this.isWorkflowWithSharing(workflow)) { + delete workflow.shared; + } }); } diff --git a/packages/cli/test/integration/active-workflow-manager.test.ts b/packages/cli/test/integration/active-workflow-manager.test.ts index 83600874b83..50722768c51 100644 --- a/packages/cli/test/integration/active-workflow-manager.test.ts +++ b/packages/cli/test/integration/active-workflow-manager.test.ts @@ -144,9 +144,9 @@ describe('init()', () => { describe('add()', () => { describe('in single-main mode', () => { - test.each(['activate', 'update'])( + test.each(['activate', 'update'])( "should add webhooks, triggers and pollers for workflow in '%s' activation mode", - async (mode: WorkflowActivateMode) => { + async (mode) => { await activeWorkflowManager.init(); const dbWorkflow = await createActiveWorkflow(); diff --git a/packages/cli/test/integration/environments/source-control.service.test.ts b/packages/cli/test/integration/environments/source-control.service.test.ts index a0b0c6d3e50..3bc8dc350a1 100644 --- a/packages/cli/test/integration/environments/source-control.service.test.ts +++ b/packages/cli/test/integration/environments/source-control.service.test.ts @@ -534,8 +534,9 @@ describe('SourceControlService', () => { return []; }); - fsReadFile.mockImplementation(async (path: string) => { - const pathWithoutCwd = isAbsolute(path) ? basename(path) : path; + fsReadFile.mockImplementation(async (path) => { + const pathStr = String(path); + const pathWithoutCwd = isAbsolute(pathStr) ? basename(pathStr) : pathStr; return JSON.stringify(gitFiles[pathWithoutCwd]); }); }); diff --git a/packages/cli/test/integration/middlewares/body-parser.test.ts b/packages/cli/test/integration/middlewares/body-parser.test.ts index 1956fe1b94f..1777dcb9200 100644 --- a/packages/cli/test/integration/middlewares/body-parser.test.ts +++ b/packages/cli/test/integration/middlewares/body-parser.test.ts @@ -6,9 +6,11 @@ import { gzipSync, deflateSync } from 'zlib'; import { rawBodyReader, bodyParser } from '@/middlewares/body-parser'; describe('bodyParser', () => { - const server = createServer((req: Request, res: Response) => { - void rawBodyReader(req, res, async () => { - void bodyParser(req, res, () => res.end(JSON.stringify(req.body))); + const server = createServer((req, res) => { + const expressReq = req as unknown as Request; + const expressRes = res as unknown as Response; + void rawBodyReader(expressReq, expressRes, async () => { + void bodyParser(expressReq, expressRes, () => res.end(JSON.stringify(expressReq.body))); }); }); diff --git a/packages/cli/test/integration/shared/license.ts b/packages/cli/test/integration/shared/license.ts index 4b3ec6c9237..af20b9848ac 100644 --- a/packages/cli/test/integration/shared/license.ts +++ b/packages/cli/test/integration/shared/license.ts @@ -19,13 +19,13 @@ export class LicenseMocker { mock(license: License) { 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) { const licenseProvider: LicenseProvider = { isLicensed: this.isFeatureEnabled.bind(this), - getValue: this.getFeatureValue.bind(this), + getValue: this.getFeatureValue.bind(this) as LicenseProvider['getValue'], }; licenseState.setLicenseProvider(licenseProvider); diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 9d8e54171be..ca7cd7ed16f 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -12,8 +12,10 @@ "@test-integration/*": ["./test/integration/shared/*"] }, "tsBuildInfoFile": "dist/typecheck.tsbuildinfo", + "strict": true, + "strictFunctionTypes": false, + "strictPropertyInitialization": false, // TODO: remove all options below this line - "strict": false, "useUnknownInCatchVariables": false }, "include": ["src/**/*.ts", "test/**/*.ts", "src/sso.ee/saml/saml-schema-metadata-2.0.xsd"], diff --git a/packages/core/src/execution-engine/execution-lifecycle-hooks.ts b/packages/core/src/execution-engine/execution-lifecycle-hooks.ts index 6de74ad743c..8ec3f739395 100644 --- a/packages/core/src/execution-engine/execution-lifecycle-hooks.ts +++ b/packages/core/src/execution-engine/execution-lifecycle-hooks.ts @@ -42,7 +42,7 @@ export type ExecutionLifecycleHookHandlers = { ( this: ExecutionLifecycleHooks, workflow: Workflow, - data?: IRunExecutionData, + data: IRunExecutionData, ) => Promise | void >; diff --git a/packages/frontend/editor-ui/src/app/utils/aiUtils.ts b/packages/frontend/editor-ui/src/app/utils/aiUtils.ts index 286e011b6f7..cc565755d31 100644 --- a/packages/frontend/editor-ui/src/app/utils/aiUtils.ts +++ b/packages/frontend/editor-ui/src/app/utils/aiUtils.ts @@ -7,7 +7,7 @@ interface MemoryMessage { lc: number; type: string; id: string[]; - kwargs: { + kwargs?: { content: unknown; additional_kwargs: Record; }; @@ -26,6 +26,27 @@ const fallbackParser = (execData: IDataObject) => ({ 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: { [key in AllowedEndpointType]: (execData: IDataObject) => { type: 'json' | 'text' | 'markdown'; @@ -93,12 +114,9 @@ const outputTypeParsers: { (execData?.response as IDataObject)?.chat_history; if (Array.isArray(chatHistory)) { const responseText = chatHistory + .filter(isMemoryMessage) .map((content: MemoryMessage) => { - if ( - content.type === 'constructor' && - content.id?.includes('messages') && - content.kwargs - ) { + if (content.type === 'constructor' && content.id.includes('messages') && content.kwargs) { interface MessageContent { type: string; text?: string; @@ -126,7 +144,10 @@ const outputTypeParsers: { }) .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)})`; } if (content.id.includes('HumanMessage')) { diff --git a/packages/workflow/src/interfaces.ts b/packages/workflow/src/interfaces.ts index 770229dcf3f..fb1c0394daa 100644 --- a/packages/workflow/src/interfaces.ts +++ b/packages/workflow/src/interfaces.ts @@ -1240,7 +1240,7 @@ export interface IPollFunctions __emit( data: INodeExecutionData[][], responsePromise?: IDeferredPromise, - donePromise?: IDeferredPromise, + donePromise?: IDeferredPromise, ): void; __emitError(error: Error, responsePromise?: IDeferredPromise): void; getNodeParameter(