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

View File

@ -7,9 +7,9 @@ import 'reflect-metadata';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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;

View File

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

View File

@ -67,8 +67,6 @@ export abstract class AbstractServer {
private fullyReady = false;
readonly uniqueInstanceId: string;
constructor() {
this.app = express();
this.app.disable('x-powered-by');

View File

@ -40,9 +40,9 @@ describe('ConcurrencyControlService', () => {
});
describe('constructor', () => {
it.each(['production', 'evaluation'])(
it.each<ConcurrencyQueueType>(['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<ConcurrencyQueueType>(['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<ConcurrencyQueueType>(['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<ExecutionMode>(['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<ExecutionMode>(['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<ExecutionMode>(['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<ExecutionMode>(['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<ExecutionMode>(['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<ExecutionMode>(['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<ConcurrencyQueueType>(['production', 'evaluation'])(
'should remove all executions from the %s queue',
async (type: ConcurrencyQueueType) => {
async (type) => {
/**
* Arrange
*/

View File

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

View File

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

View File

@ -460,10 +460,11 @@ export class CredentialsService {
credentials: CredentialsEntity[],
): Array<ICredentialsDecrypted<ICredentialDataDecryptedObject>> {
return credentials.map(
(
c: CredentialsEntity & ScopesField,
): ICredentialsDecrypted<ICredentialDataDecryptedObject> => {
const data = c.scopes.includes('credential:update') ? this.decrypt(c) : undefined;
(c: CredentialsEntity): ICredentialsDecrypted<ICredentialDataDecryptedObject> => {
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.

View File

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

View File

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

View File

@ -16,6 +16,8 @@ export type AiEventMap = {
'ai-documents-retrieved': AiEventPayload;
'ai-document-reranked': AiEventPayload;
'ai-document-embedded': AiEventPayload;
'ai-query-embedded': AiEventPayload;

View File

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

View File

@ -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<JsonObject>(req.query.filter, {
errorMessage: 'Failed to parse query string',
});

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<string>;
@ -15,7 +14,7 @@ export function interceptResponseWrites<T extends Response>(
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';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
import { invert } from '@/utils/inverter';
function isValid<T extends Record<number | string | symbol, unknown>>(
value: number | string | symbol,
constant: T,
@ -5,6 +7,9 @@ function isValid<T extends Record<number | string | symbol, unknown>>(
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<PeriodUnitNumber, PeriodUnit>,
);
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<TypeUnitNumber, TypeUnit>,
);
export const NumberToType = invert(TypeToNumber);
export function isValidTypeNumber(value: number) {
return isValid(value, NumberToType);

View File

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

View File

@ -39,7 +39,7 @@ export class LRUCache<T> {
// 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);
}

View File

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

View File

@ -38,9 +38,9 @@ const quickConnectOptionSchema = z.union([
export type QuickConnectOption = z.infer<typeof quickConnectOptionSchema>;
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 [];
}

View File

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

View File

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

View File

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

View File

@ -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<PushEvents> {
`/${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);
},
);
}

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

View File

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

View File

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

View File

@ -156,7 +156,7 @@ export class MultiMainSetup extends TypedEmitter<MultiMainEvents> {
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);
});
}
}

View File

@ -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<Role>) =>
(opts?.relations as string[] | undefined)?.includes('scopes') ? [] : correctRoles,
);
roleRepository.save.mockImplementation(async (entities) => entities as never);

View File

@ -42,7 +42,7 @@ type ListSearchMethod = (
type LoadOptionsMethod = (this: ILoadOptionsFunctions) => Promise<INodePropertyOptions[]>;
type ActionHandlerMethod = (
this: ILoadOptionsFunctions,
payload?: string,
payload?: string | IDataObject,
) => Promise<NodeParameterValueType>;
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`
// 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(

View File

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

View File

@ -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<RateLimiterLimits> = {
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({
type: 'requester:rpcresponse',

View File

@ -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<TaskRequester>).setRunnerUnavailable(
'python',
failureReason,
);
const error = new MissingRequirementsError(failureReason);
this.logger.warn(error.message);
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', () => {
const webhookManager = mock<Required<IWebhookManager>>();
const handler = createWebhookHandlerFor(webhookManager);
const handler = createWebhookHandlerFor(webhookManager) as (
req: WebhookRequest | WebhookOptionsRequest,
res: Response,
) => Promise<void>;
beforeEach(() => {
jest.resetAllMocks();
@ -29,6 +32,7 @@ describe('WebhookRequestHandler', () => {
it('should throw for unsupported methods', async () => {
const req = mock<WebhookRequest | WebhookOptionsRequest>({
path: '/',
method: 'CONNECT' as IHttpRequestMethods,
});
const res = mock<Response>();
@ -46,6 +50,7 @@ describe('WebhookRequestHandler', () => {
describe('preflight requests', () => {
it('should handle missing header for requested method', async () => {
const req = mock<WebhookRequest | WebhookOptionsRequest>({
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<WebhookRequest | WebhookOptionsRequest>({
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<WebhookRequest | WebhookOptionsRequest>({
path: '/',
method: 'OPTIONS',
headers: {
origin: randomOrigin,
@ -122,6 +129,7 @@ describe('WebhookRequestHandler', () => {
it('should handle custom origin', async () => {
const req = mock<WebhookRequest | WebhookOptionsRequest>({
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<WebhookRequest>({
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<WebhookRequest>({
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<WebhookRequest>({
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<WebhookRequest>({
path: '/',
method: 'GET',
params: { path: 'test' },
});
@ -251,6 +263,7 @@ describe('WebhookRequestHandler', () => {
"should handle '%s' method",
async (method) => {
const req = mock<WebhookRequest>({
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<WebhookRequest>({
path: '/',
method: 'GET',
params: { path: 'test' },
});
@ -296,6 +310,7 @@ describe('WebhookRequestHandler', () => {
jest.mocked(isWebhookHtmlSandboxingDisabled).mockReturnValueOnce(true);
const req = mock<WebhookRequest>({
path: '/',
method: 'GET',
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 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<IWebhookResponseCallbackData> {
): Promise<IWebhookResponseCallbackData | WebhookResponse> {
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);
},

View File

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

View File

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

View File

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

View File

@ -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<IWebhookResponseCallbackData>;
): Promise<IWebhookResponseCallbackData | WebhookResponse>;
}
export interface IWebhookResponseCallbackData {

View File

@ -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<TaskRequester>).getRunnerStatus(taskType),
};
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(
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;
}
});
}

View File

@ -144,9 +144,9 @@ describe('init()', () => {
describe('add()', () => {
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",
async (mode: WorkflowActivateMode) => {
async (mode) => {
await activeWorkflowManager.init();
const dbWorkflow = await createActiveWorkflow();

View File

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

View File

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

View File

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

View File

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

View File

@ -42,7 +42,7 @@ export type ExecutionLifecycleHookHandlers = {
(
this: ExecutionLifecycleHooks,
workflow: Workflow,
data?: IRunExecutionData,
data: IRunExecutionData,
) => Promise<void> | void
>;

View File

@ -7,7 +7,7 @@ interface MemoryMessage {
lc: number;
type: string;
id: string[];
kwargs: {
kwargs?: {
content: unknown;
additional_kwargs: Record<string, unknown>;
};
@ -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')) {

View File

@ -1240,7 +1240,7 @@ export interface IPollFunctions
__emit(
data: INodeExecutionData[][],
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
donePromise?: IDeferredPromise<IRun>,
donePromise?: IDeferredPromise<IRun | undefined>,
): void;
__emitError(error: Error, responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>): void;
getNodeParameter(