diff --git a/package.json b/package.json index 130a7e2b211..791f41e15b0 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@types/jest": "^29.5.3", "@types/node": "*", "@types/supertest": "^6.0.3", + "@babel/preset-env": "^7.26.0", "babel-plugin-transform-import-meta": "^2.3.2", "bundlemon": "^3.1.0", "cross-env": "^7.0.3", diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolSerpApi/ToolSerpApi.node.test.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolSerpApi/ToolSerpApi.node.test.ts index 43e0acc1ee0..05dc372c4de 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolSerpApi/ToolSerpApi.node.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolSerpApi/ToolSerpApi.node.test.ts @@ -40,7 +40,7 @@ describe('ToolSerpApi', () => { const node = new ToolSerpApi(); const inputData: INodeExecutionData[] = [ { - json: { query: 'artificial intelligence news' }, + json: { input: 'artificial intelligence news' }, }, ]; @@ -69,17 +69,17 @@ describe('ToolSerpApi', () => { }, ], ]); - expect(SerpAPI.prototype.invoke).toHaveBeenCalledWith(inputData[0]); + expect(SerpAPI.prototype.invoke).toHaveBeenCalledWith(inputData[0].json); }); it('should handle multiple input items', async () => { const node = new ToolSerpApi(); const inputData: INodeExecutionData[] = [ { - json: { query: 'machine learning' }, + json: { input: 'machine learning' }, }, { - json: { query: 'deep learning' }, + json: { input: 'deep learning' }, }, ]; @@ -125,7 +125,7 @@ describe('ToolSerpApi', () => { const node = new ToolSerpApi(); const inputData: INodeExecutionData[] = [ { - json: { query: 'test query' }, + json: { input: 'test query' }, }, ]; @@ -144,5 +144,25 @@ describe('ToolSerpApi', () => { expect(mockExecute.getCredentials).toHaveBeenCalledWith('serpApi'); expect(mockExecute.getNodeParameter).toHaveBeenCalledWith('options', 0); }); + + it('should fail gracefully if input is missing', async () => { + const node = new ToolSerpApi(); + const inputData: INodeExecutionData[] = [ + { + json: {}, + }, + ]; + + const mockExecute = mock({ + getInputData: jest.fn(() => inputData), + getNode: jest.fn(() => mock({ name: 'test serpapi' })), + getCredentials: jest.fn().mockResolvedValue({ apiKey: 'test-api-key' }), + getNodeParameter: jest.fn().mockReturnValue({}), + }); + + await expect(node.execute.call(mockExecute)).rejects.toThrow( + 'Missing search query input at itemIndex 0', + ); + }); }); }); diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolSerpApi/ToolSerpApi.node.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolSerpApi/ToolSerpApi.node.ts index 6b21ca41389..152fe9eedc2 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolSerpApi/ToolSerpApi.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolSerpApi/ToolSerpApi.node.ts @@ -1,4 +1,6 @@ import { SerpAPI } from '@langchain/community/tools/serpapi'; +import { logWrapper } from '@utils/logWrapper'; +import { getConnectionHintNoticeField } from '@utils/sharedFields'; import { type IExecuteFunctions, NodeConnectionTypes, @@ -7,10 +9,9 @@ import { type ISupplyDataFunctions, type SupplyData, type INodeExecutionData, + NodeOperationError, } from 'n8n-workflow'; -import { logWrapper } from '@utils/logWrapper'; -import { getConnectionHintNoticeField } from '@utils/sharedFields'; async function getTool(ctx: ISupplyDataFunctions | IExecuteFunctions, itemIndex: number) { const credentials = await ctx.getCredentials('serpApi'); @@ -133,8 +134,17 @@ export class ToolSerpApi implements INodeType { const returnData: INodeExecutionData[] = []; for (let itemIndex = 0; itemIndex < inputData.length; itemIndex++) { const tool = await getTool(this, itemIndex); - const query = inputData[itemIndex]; - const result = await tool.invoke(query); + const item = inputData[itemIndex].json; + + if (typeof item.input !== 'string' || !item.input) { + throw new NodeOperationError( + this.getNode(), + `Missing search query input at itemIndex ${itemIndex}`, + ); + } + + const result = (await tool.invoke(item)) as string; + returnData.push({ json: { response: result, diff --git a/packages/cli/src/modules/chat-hub/chat-hub.controller.ts b/packages/cli/src/modules/chat-hub/chat-hub.controller.ts index 53ffe3a7643..0786e3152d2 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub.controller.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub.controller.ts @@ -134,16 +134,6 @@ export class ChatHubController { res: Response, @Body payload: ChatHubSendMessageRequest, ) { - this.logger.debug( - `Chat send request received: ${jsonStringify({ - ...payload, - attachments: payload.attachments?.map((a) => ({ - fileName: a.fileName, - data: `${a.data.length} characters (Base64)`, - })), - })}`, - ); - try { await this.chatService.sendHumanMessage(res, req.user, { ...payload, @@ -186,8 +176,6 @@ export class ChatHubController { @Param('messageId') editId: ChatMessageId, @Body payload: ChatHubEditMessageRequest, ) { - this.logger.debug(`Chat edit request received: ${jsonStringify(payload)}`); - try { await this.chatService.editMessage(res, req.user, { ...payload, @@ -232,8 +220,6 @@ export class ChatHubController { @Param('messageId') retryId: ChatMessageId, @Body payload: ChatHubRegenerateMessageRequest, ) { - this.logger.debug(`Chat retry request received: ${jsonStringify(payload)}`); - try { await this.chatService.regenerateAIMessage(res, req.user, { ...payload, @@ -277,8 +263,6 @@ export class ChatHubController { @Param('sessionId') sessionId: ChatSessionId, @Param('messageId') messageId: ChatMessageId, ) { - this.logger.debug(`Chat stop request received: ${jsonStringify({ sessionId, messageId })}`); - await this.chatService.stopGeneration(req.user, sessionId, messageId); res.status(204).send(); } diff --git a/packages/cli/src/webhooks/__tests__/test-webhook-registrations.service.test.ts b/packages/cli/src/webhooks/__tests__/test-webhook-registrations.service.test.ts index 75352e11fa3..586258605ce 100644 --- a/packages/cli/src/webhooks/__tests__/test-webhook-registrations.service.test.ts +++ b/packages/cli/src/webhooks/__tests__/test-webhook-registrations.service.test.ts @@ -107,14 +107,6 @@ describe('TestWebhookRegistrationsService', () => { }); }); - describe('deregisterAll()', () => { - test('should deregister all test webhook registrations', async () => { - await registrations.deregisterAll(); - - expect(cacheService.delete).toHaveBeenCalledWith(cacheKey); - }); - }); - describe('toKey()', () => { test('should convert a test webhook registration to a key', () => { const result = registrations.toKey(registration.webhook); diff --git a/packages/cli/src/webhooks/test-webhook-registrations.service.ts b/packages/cli/src/webhooks/test-webhook-registrations.service.ts index 13815f07dd2..4684e30978c 100644 --- a/packages/cli/src/webhooks/test-webhook-registrations.service.ts +++ b/packages/cli/src/webhooks/test-webhook-registrations.service.ts @@ -113,10 +113,6 @@ export class TestWebhookRegistrationsService { return val; } - async deregisterAll() { - await this.cacheService.delete(this.cacheKey); - } - toKey(webhook: Pick) { const { webhookId, httpMethod, path: webhookPath } = webhook; diff --git a/packages/cli/src/webhooks/test-webhooks.ts b/packages/cli/src/webhooks/test-webhooks.ts index d58808dfe1d..a8da8618bed 100644 --- a/packages/cli/src/webhooks/test-webhooks.ts +++ b/packages/cli/src/webhooks/test-webhooks.ts @@ -500,9 +500,10 @@ export class TestWebhooks implements IWebhookManager { if (staticData) workflow.staticData = staticData; await this.webhookService.deleteWebhook(workflow, webhook, 'internal', 'update'); - } - await this.registrations.deregisterAll(); + // Deregister only this webhook, not all webhooks from other running workflows + await this.registrations.deregister(webhook); + } } /** diff --git a/packages/core/src/execution-engine/node-execution-context/__tests__/node-execution-context.test.ts b/packages/core/src/execution-engine/node-execution-context/__tests__/node-execution-context.test.ts index 26ea8b9f198..43478ccbcad 100644 --- a/packages/core/src/execution-engine/node-execution-context/__tests__/node-execution-context.test.ts +++ b/packages/core/src/execution-engine/node-execution-context/__tests__/node-execution-context.test.ts @@ -462,4 +462,121 @@ describe('NodeExecutionContext', () => { ); }); }); + + describe('nodeFeatures', () => { + it('should return empty object when features are not defined', () => { + node.typeVersion = 2.4; + nodeType.description.features = undefined; + + const result = testContext['nodeFeatures']; + + expect(result).toEqual({}); + }); + + it('should return enabled features based on node version', () => { + node.typeVersion = 2.4; + nodeType.description.features = { + useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] }, + useFeatureB: { '@version': [{ _cnd: { lte: 2.1 } }] }, + useFeatureC: { '@version': [{ _cnd: { gte: 2.2 } }] }, + }; + + const result = testContext['nodeFeatures']; + + expect(result).toEqual({ + useFeatureA: true, + useFeatureB: false, + useFeatureC: true, + }); + }); + + it('should return correct features for version 2.1', () => { + node.typeVersion = 2.1; + nodeType.description.features = { + useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] }, + useFeatureB: { '@version': [{ _cnd: { lte: 2.1 } }] }, + useFeatureC: { '@version': [{ _cnd: { gte: 2.2 } }] }, + }; + + const result = testContext['nodeFeatures']; + + expect(result).toEqual({ + useFeatureA: false, + useFeatureB: true, + useFeatureC: false, + }); + }); + + it('should handle simple version number conditions', () => { + node.typeVersion = 2; + nodeType.description.features = { + useFeatureD: { '@version': [2] }, + useFeatureE: { '@version': [{ _cnd: { lt: 2.3 } }] }, + }; + + const result = testContext['nodeFeatures']; + + expect(result).toEqual({ + useFeatureD: true, + useFeatureE: true, + }); + }); + }); + + describe('isNodeFeatureEnabled', () => { + it('should return true when feature is enabled', () => { + node.typeVersion = 2.4; + nodeType.description.features = { + useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] }, + }; + + const result = testContext.isNodeFeatureEnabled('useFeatureA'); + + expect(result).toBe(true); + }); + + it('should return false when feature is disabled', () => { + node.typeVersion = 2.3; + nodeType.description.features = { + useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] }, + }; + + const result = testContext.isNodeFeatureEnabled('useFeatureA'); + + expect(result).toBe(false); + }); + + it('should return false when feature does not exist', () => { + node.typeVersion = 2.4; + nodeType.description.features = { + useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] }, + }; + + const result = testContext.isNodeFeatureEnabled('nonExistentFeature'); + + expect(result).toBe(false); + }); + + it('should return false when features are not defined', () => { + node.typeVersion = 2.4; + nodeType.description.features = undefined; + + const result = testContext.isNodeFeatureEnabled('useFeatureA'); + + expect(result).toBe(false); + }); + + it('should handle multiple features correctly', () => { + node.typeVersion = 2.4; + nodeType.description.features = { + useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] }, + useFeatureB: { '@version': [{ _cnd: { lte: 2.1 } }] }, + useFeatureC: { '@version': [{ _cnd: { gte: 2.2 } }] }, + }; + + expect(testContext.isNodeFeatureEnabled('useFeatureA')).toBe(true); + expect(testContext.isNodeFeatureEnabled('useFeatureB')).toBe(false); + expect(testContext.isNodeFeatureEnabled('useFeatureC')).toBe(true); + }); + }); }); diff --git a/packages/core/src/execution-engine/node-execution-context/node-execution-context.ts b/packages/core/src/execution-engine/node-execution-context/node-execution-context.ts index 0dc6f5db079..e37c0007fc1 100644 --- a/packages/core/src/execution-engine/node-execution-context/node-execution-context.ts +++ b/packages/core/src/execution-engine/node-execution-context/node-execution-context.ts @@ -17,6 +17,7 @@ import type { IRunExecutionData, IWorkflowExecuteAdditionalData, NodeConnectionType, + NodeFeatures, NodeInputConnections, NodeParameterValueType, NodeTypeAndVersion, @@ -168,6 +169,26 @@ export abstract class NodeExecutionContext implements Omit { diff --git a/packages/nodes-base/nodes/N8n/WorkflowDescription.ts b/packages/nodes-base/nodes/N8n/WorkflowDescription.ts index 7cfaf83c51a..89e217402ad 100644 --- a/packages/nodes-base/nodes/N8n/WorkflowDescription.ts +++ b/packages/nodes-base/nodes/N8n/WorkflowDescription.ts @@ -280,7 +280,7 @@ const getAllOperation: INodeProperties[] = [ }, options: [ { - displayName: 'Return Only Active Workflows', + displayName: 'Return Only Published Workflows', name: 'activeWorkflows', type: 'boolean', default: true, diff --git a/packages/testing/playwright/helpers/NodeParameterHelper.ts b/packages/testing/playwright/helpers/NodeParameterHelper.ts index 46f662ac3c3..db74efc30ad 100644 --- a/packages/testing/playwright/helpers/NodeParameterHelper.ts +++ b/packages/testing/playwright/helpers/NodeParameterHelper.ts @@ -58,6 +58,7 @@ export class NodeParameterHelper { break; case 'text': await this.ndv.setParameterInput(parameterName, value); + await this.ndv.waitForDebounce(); break; case 'switch': await this.ndv.setParameterSwitch(parameterName, value === 'true'); @@ -80,10 +81,11 @@ export class NodeParameterHelper { await this.setParameter('responseMode', config.responseMode, 'dropdown'); } - /** - * Simplified HTTP Request node parameter configuration - * @param config - Configuration object with parameter values - */ + async getWebhookPath(): Promise { + const input = this.ndv.getParameterInputField('path'); + return await input.inputValue(); + } + async httpRequest(config: { method?: string; url?: string; diff --git a/packages/testing/playwright/pages/NodeDetailsViewPage.ts b/packages/testing/playwright/pages/NodeDetailsViewPage.ts index 04edaffa1f7..843c137ad03 100644 --- a/packages/testing/playwright/pages/NodeDetailsViewPage.ts +++ b/packages/testing/playwright/pages/NodeDetailsViewPage.ts @@ -626,6 +626,12 @@ export class NodeDetailsViewPage extends BasePage { await input.fill(value); } + /** Waits for parameter input debounce (100ms) to flush. */ + async waitForDebounce(): Promise { + // eslint-disable-next-line playwright/no-wait-for-timeout + await this.page.waitForTimeout(150); + } + async clickGetBackToCanvas(): Promise { await this.clickBackToCanvasButton(); } diff --git a/packages/testing/playwright/services/api-helper.ts b/packages/testing/playwright/services/api-helper.ts index acfc574da57..f40b4d10c6d 100644 --- a/packages/testing/playwright/services/api-helper.ts +++ b/packages/testing/playwright/services/api-helper.ts @@ -15,6 +15,7 @@ import { RoleApiHelper } from './role-api-helper'; import { TagApiHelper } from './tag-api-helper'; import { UserApiHelper } from './user-api-helper'; import { VariablesApiHelper } from './variables-api-helper'; +import { WebhookApiHelper } from './webhook-api-helper'; import { WorkflowApiHelper } from './workflow-api-helper'; export interface LoginResponseData { @@ -39,6 +40,7 @@ const DB_TAGS = { export class ApiHelpers { request: APIRequestContext; workflows: WorkflowApiHelper; + webhooks: WebhookApiHelper; projects: ProjectApiHelper; credentials: CredentialApiHelper; variables: VariablesApiHelper; @@ -49,6 +51,7 @@ export class ApiHelpers { constructor(requestContext: APIRequestContext) { this.request = requestContext; this.workflows = new WorkflowApiHelper(this); + this.webhooks = new WebhookApiHelper(this); this.projects = new ProjectApiHelper(this); this.credentials = new CredentialApiHelper(this); this.variables = new VariablesApiHelper(this); diff --git a/packages/testing/playwright/services/webhook-api-helper.ts b/packages/testing/playwright/services/webhook-api-helper.ts new file mode 100644 index 00000000000..a7634e320a2 --- /dev/null +++ b/packages/testing/playwright/services/webhook-api-helper.ts @@ -0,0 +1,44 @@ +import type { APIResponse } from '@playwright/test'; + +import type { ApiHelpers } from './api-helper'; + +type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD'; + +interface TriggerOptions { + method?: HttpMethod; + headers?: Record; + data?: unknown; + /** Max retries for 404 (webhook not yet registered). Default: 3 */ + maxNotFoundRetries?: number; + /** Delay between 404 retries in ms. Default: 250 */ + notFoundRetryDelayMs?: number; +} + +/** Triggers webhooks with retry for 404s (async registration) and connection errors. */ +export class WebhookApiHelper { + constructor(private readonly api: ApiHelpers) {} + + async trigger(path: string, options?: TriggerOptions): Promise { + const maxNotFoundRetries = options?.maxNotFoundRetries ?? 3; + const notFoundRetryDelayMs = options?.notFoundRetryDelayMs ?? 250; + + let lastResponse: APIResponse | undefined; + + for (let attempt = 0; attempt <= maxNotFoundRetries; attempt++) { + lastResponse = await this.api.request.fetch(path, { + method: options?.method ?? 'GET', + headers: options?.headers, + data: options?.data, + maxRetries: 3, // Playwright retry for connection errors + }); + + if (lastResponse.status() !== 404 || attempt === maxNotFoundRetries) { + return lastResponse; + } + + await new Promise((resolve) => setTimeout(resolve, notFoundRetryDelayMs)); + } + + return lastResponse!; + } +} diff --git a/packages/testing/playwright/services/workflow-api-helper.ts b/packages/testing/playwright/services/workflow-api-helper.ts index 83d5ece6a5a..eb10ef850cf 100644 --- a/packages/testing/playwright/services/workflow-api-helper.ts +++ b/packages/testing/playwright/services/workflow-api-helper.ts @@ -19,6 +19,7 @@ type WorkflowImportResult = { createdWorkflow: IWorkflowBase; webhookPath?: string; webhookId?: string; + webhookMethod?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD'; }; export class WorkflowApiHelper { @@ -35,14 +36,7 @@ export class WorkflowApiHelper { return result.data ?? result; } - /** - * Creates a workflow in a project with optional folder placement (Uses Internal API not public API) - * @param project - Required project ID where the workflow will be created - * @param options - Optional configuration for workflow creation - * @param options.folder - Optional folder ID to place the workflow in - * @param options.name - Optional workflow name. If not provided, generates a unique name using nanoid - * @returns Object containing the name, ID, and versionId of the created workflow - */ + /** Creates a workflow in a project with optional folder placement. */ async createInProject( project: string, options?: { @@ -96,10 +90,7 @@ export class WorkflowApiHelper { } } - /** - * Make workflow unique by updating name, IDs, and webhook paths if present. - * This ensures no conflicts when importing workflows for testing. - */ + /** Makes workflow unique by updating name, IDs, and webhook paths. */ private makeWorkflowUnique( workflow: Partial, options?: { webhookPrefix?: string; idLength?: number }, @@ -121,6 +112,7 @@ export class WorkflowApiHelper { // Check if workflow has webhook nodes and process them let webhookId: string | undefined; let webhookPath: string | undefined; + let webhookMethod: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | undefined; if (workflow.nodes) { for (const node of workflow.nodes) { @@ -129,25 +121,24 @@ export class WorkflowApiHelper { webhookPath = `${webhookPrefix}-${webhookId}`; node.webhookId = webhookId; node.parameters.path = webhookPath; + // Extract HTTP method from webhook node, default to GET + webhookMethod = (node.parameters.httpMethod as typeof webhookMethod) ?? 'GET'; } } } - return { webhookId, webhookPath, workflow }; + return { webhookId, webhookPath, webhookMethod, workflow }; } - /** - * Create a workflow from an in-memory definition, making it unique for testing. - * Returns detailed information about what was created. - */ + /** Creates a workflow from definition, making it unique for testing. */ async createWorkflowFromDefinition( workflow: Partial, options?: { webhookPrefix?: string; idLength?: number; makeUnique?: boolean }, ): Promise { const { makeUnique = true, ...rest } = options ?? {}; - const { webhookPath, webhookId } = makeUnique + const { webhookPath, webhookId, webhookMethod } = makeUnique ? this.makeWorkflowUnique(workflow, rest) - : { webhookPath: undefined, webhookId: undefined }; + : { webhookPath: undefined, webhookId: undefined, webhookMethod: undefined }; const createdWorkflow = await this.createWorkflow(workflow); const workflowId: string = String(createdWorkflow.id); @@ -156,14 +147,11 @@ export class WorkflowApiHelper { createdWorkflow, webhookPath, webhookId, + webhookMethod, }; } - /** - * Import a workflow from file and make it unique for testing. - * The workflow will be created with its original active state from the JSON file. - * Returns detailed information about what was imported, including webhook info if present. - */ + /** Imports a workflow from file, making it unique for testing. */ async importWorkflowFromFile( fileName: string, options?: { webhookPrefix?: string; idLength?: number; makeUnique?: boolean }, @@ -179,7 +167,6 @@ export class WorkflowApiHelper { workflowDefinition: Partial, options?: { webhookPrefix?: string; idLength?: number; makeUnique?: boolean }, ): Promise { - // Store original active state const result = await this.createWorkflowFromDefinition(workflowDefinition, options); if (workflowDefinition.active) { @@ -188,7 +175,6 @@ export class WorkflowApiHelper { return result; } - // TODO: workflowId is being ignored async getExecutions(workflowId?: string, limit = 20): Promise { const params = new URLSearchParams(); if (workflowId) params.set('workflowId', workflowId); @@ -229,7 +215,9 @@ export class WorkflowApiHelper { if (executions.length > initialCount) { for (const execution of executions.slice(0, executions.length - initialCount)) { - if (execution.status === 'success' || execution.status === 'error') { + const isCompleted = execution.status === 'success' || execution.status === 'error'; + const isCorrectWorkflow = execution.workflowId === workflowId; + if (isCompleted && isCorrectWorkflow) { return execution; } } @@ -237,7 +225,8 @@ export class WorkflowApiHelper { for (const execution of executions) { const isCompleted = execution.status === 'success' || execution.status === 'error'; - if (isCompleted && execution.mode === 'webhook') { + const isCorrectWorkflow = execution.workflowId === workflowId; + if (isCompleted && isCorrectWorkflow && execution.mode === 'webhook') { const executionTime = new Date( execution.startedAt ?? execution.createdAt ?? Date.now(), ).getTime(); @@ -253,14 +242,7 @@ export class WorkflowApiHelper { throw new TestError(`Execution did not complete within ${timeoutMs}ms`); } - /** - * Wait for a workflow execution to reach a specific status - * @param workflowId - The workflow ID to check - * @param expectedStatus - The expected status (e.g., 'waiting', 'success', 'error') - * @param timeoutMs - Maximum time to wait in milliseconds - * @returns The execution once it reaches the expected status - * @throws TestError if execution doesn't reach the expected status within timeout - */ + /** Waits for a workflow execution to reach a specific status. */ async waitForWorkflowStatus( workflowId: string, expectedStatus: string, diff --git a/packages/testing/playwright/tests/e2e/api/webhook-external.spec.ts b/packages/testing/playwright/tests/e2e/api/webhook-external.spec.ts index bdc714fac69..6d7873a288d 100644 --- a/packages/testing/playwright/tests/e2e/api/webhook-external.spec.ts +++ b/packages/testing/playwright/tests/e2e/api/webhook-external.spec.ts @@ -10,7 +10,8 @@ test.describe('External Webhook Triggering', () => { const testPayload = { message: 'Hello from Playwright test' }; - const webhookResponse = await api.request.post(`/webhook/${webhookPath}`, { + const webhookResponse = await api.webhooks.trigger(`/webhook/${webhookPath}`, { + method: 'POST', data: testPayload, }); @@ -28,7 +29,7 @@ test.describe('External Webhook Triggering', () => { 'webhook-misconfiguration-test.json', ); - const webhookResponse = await api.request.get(`/webhook/${webhookPath}`); + const webhookResponse = await api.webhooks.trigger(`/webhook/${webhookPath}`); expect(webhookResponse.ok()).toBe(false); expect(await webhookResponse.text()).toContain('Unused Respond to Webhook node'); diff --git a/packages/testing/playwright/tests/e2e/api/webhook-isolation.spec.ts b/packages/testing/playwright/tests/e2e/api/webhook-isolation.spec.ts index ce2e0f37ba3..0a653340903 100644 --- a/packages/testing/playwright/tests/e2e/api/webhook-isolation.spec.ts +++ b/packages/testing/playwright/tests/e2e/api/webhook-isolation.spec.ts @@ -31,7 +31,7 @@ test.describe('Webhook Origin Isolation', () => { test(`Webhook responses should include the correct response headers for ${webhookPath}`, async ({ api, }) => { - const webhookResponse = await api.request.get(`/webhook/${webhookPath}`); + const webhookResponse = await api.webhooks.trigger(`/webhook/${webhookPath}`); expect(webhookResponse.ok()).toBe(true); const headers = webhookResponse.headers(); diff --git a/packages/testing/playwright/tests/e2e/api/webhook.spec.ts b/packages/testing/playwright/tests/e2e/nodes/webhook.spec.ts similarity index 76% rename from packages/testing/playwright/tests/e2e/api/webhook.spec.ts rename to packages/testing/playwright/tests/e2e/nodes/webhook.spec.ts index 27637132dee..d467c85aa35 100644 --- a/packages/testing/playwright/tests/e2e/api/webhook.spec.ts +++ b/packages/testing/playwright/tests/e2e/nodes/webhook.spec.ts @@ -7,46 +7,47 @@ import { EditFieldsNode } from '../../../pages/nodes/EditFieldsNode'; const cowBase64 = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k='; -// eslint-disable-next-line n8n-local-rules/no-skipped-tests -- Flaky in multi-main mode due to webhook registration timing issues -test.describe.skip('Webhook Trigger node', () => { - test.describe.configure({ mode: 'serial' }); +test.describe('Webhook Trigger node', () => { test.beforeEach(async ({ n8n }) => { await n8n.start.fromBlankCanvas(); }); - const HTTP_METHODS = ['GET', 'POST', 'DELETE', 'HEAD', 'PATCH', 'PUT']; - for (const httpMethod of HTTP_METHODS) { - test(`should listen for a ${httpMethod} request`, async ({ n8n }) => { - const webhookPath = nanoid(); - await n8n.canvas.addNode('Webhook'); - await n8n.ndv.setupHelper.webhook({ httpMethod, path: webhookPath }); + test('should listen for all HTTP methods (GET, POST, DELETE, HEAD, PATCH, PUT)', async ({ + n8n, + }) => { + await n8n.canvas.addNode('Webhook'); + const webhookPath = await n8n.ndv.setupHelper.getWebhookPath(); + + const methods = ['GET', 'POST', 'DELETE', 'HEAD', 'PATCH', 'PUT'] as const; + for (const method of methods) { + await n8n.ndv.setupHelper.webhook({ httpMethod: method }); await n8n.ndv.execute(); await expect(n8n.ndv.getWebhookTestEvent()).toBeVisible(); - const response = await n8n.api.request.fetch(`/webhook-test/${webhookPath}`, { - method: httpMethod, - }); - expect(response.ok()).toBe(true); - }); - } + + const response = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`, { method }); + expect(response.ok(), `${method} request should succeed`).toBe(true); + + // Wait for output to appear (confirms execution completed) + await expect(n8n.ndv.outputPanel.getDataContainer()).toBeVisible(); + } + }); test('should listen for a GET request and respond with Respond to Webhook node', async ({ n8n, }) => { - const webhookPath = nanoid(); - await n8n.canvas.addNode('Webhook'); await n8n.ndv.setupHelper.webhook({ httpMethod: 'GET', - path: webhookPath, responseMode: "Using 'Respond to Webhook' Node", }); + const webhookPath = await n8n.ndv.setupHelper.getWebhookPath(); await n8n.ndv.close(); await addEditFieldsNode(n8n); await n8n.canvas.addNode('Respond to Webhook', { closeNDV: true }); await n8n.canvas.clickExecuteWorkflowButton(); await expect(n8n.canvas.waitingForTriggerEvent()).toBeVisible(); - const response = await n8n.api.request.get(`/webhook-test/${webhookPath}`); + const response = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`); expect(response.ok()).toBe(true); const responseData = await response.json(); @@ -56,28 +57,27 @@ test.describe.skip('Webhook Trigger node', () => { test('should listen for a GET request and respond with custom status code 201', async ({ n8n, }) => { - const webhookPath = nanoid(); - await n8n.canvas.addNode('Webhook'); - await n8n.ndv.setupHelper.webhook({ httpMethod: 'GET', path: webhookPath }); + await n8n.ndv.setupHelper.webhook({ httpMethod: 'GET' }); + const webhookPath = await n8n.ndv.setupHelper.getWebhookPath(); await n8n.ndv.setOptionalParameter('Response Code', 'responseCode', '201'); + // Extra wait to ensure parameter is persisted before webhook registration + await n8n.ndv.waitForDebounce(); await n8n.ndv.execute(); await expect(n8n.ndv.getWebhookTestEvent()).toBeVisible(); - const response = await n8n.api.request.get(`/webhook-test/${webhookPath}`); + const response = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`); expect(response.status()).toBe(201); }); test('should listen for a GET request and respond with last node', async ({ n8n }) => { - const webhookPath = nanoid(); - await n8n.canvas.addNode('Webhook'); await n8n.ndv.setupHelper.webhook({ httpMethod: 'GET', - path: webhookPath, responseMode: 'When Last Node Finishes', }); + const webhookPath = await n8n.ndv.setupHelper.getWebhookPath(); await n8n.ndv.close(); await addEditFieldsNode(n8n); @@ -86,7 +86,7 @@ test.describe.skip('Webhook Trigger node', () => { await expect(n8n.canvas.waitingForTriggerEvent()).toBeVisible(); - const response = await n8n.api.request.get(`/webhook-test/${webhookPath}`); + const response = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`); expect(response.ok()).toBe(true); const responseData = await response.json(); @@ -96,14 +96,12 @@ test.describe.skip('Webhook Trigger node', () => { test('should listen for a GET request and respond with last node binary data', async ({ n8n, }) => { - const webhookPath = nanoid(); - await n8n.canvas.addNode('Webhook'); await n8n.ndv.setupHelper.webhook({ httpMethod: 'GET', - path: webhookPath, responseMode: 'When Last Node Finishes', }); + const webhookPath = await n8n.ndv.setupHelper.getWebhookPath(); await n8n.ndv.selectOptionInParameterDropdown('responseData', 'First Entry Binary'); await n8n.ndv.close(); @@ -120,7 +118,7 @@ test.describe.skip('Webhook Trigger node', () => { await expect(n8n.canvas.waitingForTriggerEvent()).toBeVisible(); - const response = await n8n.api.request.get(`/webhook-test/${webhookPath}`); + const response = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`); expect(response.ok()).toBe(true); const responseData = await response.json(); @@ -128,19 +126,17 @@ test.describe.skip('Webhook Trigger node', () => { }); test('should listen for a GET request and respond with an empty body', async ({ n8n }) => { - const webhookPath = nanoid(); - await n8n.canvas.addNode('Webhook'); await n8n.ndv.setupHelper.webhook({ httpMethod: 'GET', - path: webhookPath, responseMode: 'When Last Node Finishes', }); + const webhookPath = await n8n.ndv.setupHelper.getWebhookPath(); await n8n.ndv.selectOptionInParameterDropdown('responseData', 'No Response Body'); await n8n.ndv.execute(); await expect(n8n.ndv.getWebhookTestEvent()).toBeVisible(); - const response = await n8n.api.request.get(`/webhook-test/${webhookPath}`); + const response = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`); expect(response.ok()).toBe(true); const responseData = await response.text(); @@ -148,7 +144,6 @@ test.describe.skip('Webhook Trigger node', () => { }); test('should listen for a GET request with Basic Authentication', async ({ n8n }) => { - const webhookPath = nanoid(); const credentialName = `test-${nanoid()}`; const user = `test-${nanoid()}`; const password = `test-${nanoid()}`; @@ -164,21 +159,21 @@ test.describe.skip('Webhook Trigger node', () => { await n8n.canvas.addNode('Webhook'); await n8n.ndv.setupHelper.webhook({ httpMethod: 'GET', - path: webhookPath, authentication: 'Basic Auth', }); + const webhookPath = await n8n.ndv.setupHelper.getWebhookPath(); await n8n.ndv.execute(); await expect(n8n.ndv.getWebhookTestEvent()).toBeVisible(); - const failResponse = await n8n.api.request.get(`/webhook-test/${webhookPath}`, { + const failResponse = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`, { headers: { Authorization: 'Basic ' + Buffer.from('wrong:wrong').toString('base64'), }, }); expect(failResponse.status()).toBe(403); - const successResponse = await n8n.api.request.get(`/webhook-test/${webhookPath}`, { + const successResponse = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`, { headers: { Authorization: 'Basic ' + Buffer.from(`${user}:${password}`).toString('base64'), }, @@ -186,8 +181,7 @@ test.describe.skip('Webhook Trigger node', () => { expect(successResponse.ok()).toBe(true); }); - test('should listen for a GET request with Header Authentication', async ({ n8n, api }) => { - const webhookPath = nanoid(); + test('should listen for a GET request with Header Authentication', async ({ n8n }) => { const credentialName = `test-${nanoid()}`; const name = `test-${nanoid()}`; const value = `test-${nanoid()}`; @@ -203,14 +197,14 @@ test.describe.skip('Webhook Trigger node', () => { await n8n.canvas.addNode('Webhook'); await n8n.ndv.setupHelper.webhook({ httpMethod: 'GET', - path: webhookPath, authentication: 'Header Auth', }); + const webhookPath = await n8n.ndv.setupHelper.getWebhookPath(); await n8n.ndv.execute(); await expect(n8n.ndv.getWebhookTestEvent()).toBeVisible(); - const failResponse = await api.request.get(`/webhook-test/${webhookPath}`, { + const failResponse = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`, { headers: { test: 'wrong', }, @@ -218,7 +212,7 @@ test.describe.skip('Webhook Trigger node', () => { expect(failResponse.status()).toBe(403); - const successResponse = await api.request.get(`/webhook-test/${webhookPath}`, { + const successResponse = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`, { headers: { [name]: value, }, @@ -227,13 +221,10 @@ test.describe.skip('Webhook Trigger node', () => { }); test('CAT-1253-bug-cant-run-workflow-when-unconnected-nodes-have-errors', async ({ n8n }) => { - const webhookPath = nanoid(); - // Add Webhook node await n8n.canvas.addNode('Webhook'); await n8n.ndv.setupHelper.webhook({ httpMethod: 'GET', - path: webhookPath, }); await n8n.ndv.close(); diff --git a/packages/testing/playwright/tests/e2e/workflows/editor/execution/execution.spec.ts b/packages/testing/playwright/tests/e2e/workflows/editor/execution/execution.spec.ts index ec94936909e..69b7acd2687 100644 --- a/packages/testing/playwright/tests/e2e/workflows/editor/execution/execution.spec.ts +++ b/packages/testing/playwright/tests/e2e/workflows/editor/execution/execution.spec.ts @@ -123,8 +123,7 @@ test.describe('Execution', () => { await expect(n8n.canvas.clearExecutionDataButton()).toBeHidden(); }); - // eslint-disable-next-line n8n-local-rules/no-skipped-tests -- Flaky in multi-main mode due to webhook registration timing issues - test.skip('should test webhook workflow', async ({ n8n }) => { + test('should test webhook workflow', async ({ n8n }) => { await n8n.start.fromImportedWorkflow('Webhook_wait_set.json'); await expect(n8n.canvas.getExecuteWorkflowButton()).toBeVisible(); diff --git a/packages/testing/playwright/tests/e2e/workflows/editor/execution/logs.spec.ts b/packages/testing/playwright/tests/e2e/workflows/editor/execution/logs.spec.ts index 9a0ff4eff0a..887b5e4f7f2 100644 --- a/packages/testing/playwright/tests/e2e/workflows/editor/execution/logs.spec.ts +++ b/packages/testing/playwright/tests/e2e/workflows/editor/execution/logs.spec.ts @@ -231,10 +231,7 @@ test.describe('Logs', () => { await expect(n8n.executions.logsPanel.getLogEntries().nth(2)).toContainText('E2E Chat Model'); }); - // eslint-disable-next-line n8n-local-rules/no-skipped-tests -- Flaky in multi-main mode due to webhook registration timing issues - test.skip('should show logs for a workflow with a node that waits for webhook', async ({ - n8n, - }) => { + test('should show logs for a workflow with a node that waits for webhook', async ({ n8n }) => { await n8n.start.fromImportedWorkflow('Workflow_wait_for_webhook.json'); await n8n.canvas.deselectAll(); await n8n.canvas.logsPanel.open(); @@ -270,10 +267,7 @@ test.describe('Logs', () => { await expect(n8n.canvas.logsPanel.getLogEntries().nth(1)).toContainText('Success'); }); - // eslint-disable-next-line n8n-local-rules/no-skipped-tests -- Flaky in multi-main mode due to webhook registration timing issues - test.skip('should allow to cancel a workflow with a node that waits for webhook', async ({ - n8n, - }) => { + test('should allow to cancel a workflow with a node that waits for webhook', async ({ n8n }) => { await n8n.start.fromImportedWorkflow('Workflow_wait_for_webhook.json'); await n8n.canvas.deselectAll(); await n8n.canvas.logsPanel.open(); diff --git a/packages/testing/playwright/tests/e2e/workflows/editor/ndv/ndv-core.spec.ts b/packages/testing/playwright/tests/e2e/workflows/editor/ndv/ndv-core.spec.ts index 2c0e3a8f6b6..dad9c42cbc6 100644 --- a/packages/testing/playwright/tests/e2e/workflows/editor/ndv/ndv-core.spec.ts +++ b/packages/testing/playwright/tests/e2e/workflows/editor/ndv/ndv-core.spec.ts @@ -31,20 +31,6 @@ test.describe('NDV', () => { await expect(n8n.ndv.inputPanel.get()).toContainText('No input connected'); }); - test('should test webhook node', async ({ n8n }) => { - await n8n.canvas.addNode('Webhook', { closeNDV: false }); - - await n8n.ndv.execute(); - - const webhookUrl = await n8n.ndv.getWebhookUrl(); - await expect(n8n.ndv.getWebhookTriggerListening()).toBeVisible(); - const response = await n8n.ndv.makeWebhookRequest(webhookUrl as string); - expect(response.status()).toBe(200); - - await expect(n8n.ndv.outputPanel.get()).toBeVisible(); - await expect(n8n.ndv.outputPanel.getDataContainer()).toBeVisible(); - }); - test('should change input and go back to canvas', async ({ n8n }) => { await n8n.start.fromImportedWorkflow('NDV-test-select-input.json'); await n8n.canvas.clickZoomToFitButton(); diff --git a/packages/testing/playwright/tests/e2e/workflows/editor/subworkflows/wait.spec.ts b/packages/testing/playwright/tests/e2e/workflows/editor/subworkflows/wait.spec.ts index 37961abf67b..65293e15496 100644 --- a/packages/testing/playwright/tests/e2e/workflows/editor/subworkflows/wait.spec.ts +++ b/packages/testing/playwright/tests/e2e/workflows/editor/subworkflows/wait.spec.ts @@ -24,7 +24,7 @@ test.describe('Parent that does not wait for sub-workflow', () => { const { webhookPath, workflowId } = await api.workflows.importWorkflowFromDefinition(workflowDefinition); - const response = await api.request.get(`/webhook/${webhookPath}`); + const response = await api.webhooks.trigger(`/webhook/${webhookPath}`); expect(response.ok()).toBe(true); const execution = await api.workflows.waitForExecution(workflowId, 5000); expect(execution.status).toBe('success'); @@ -57,7 +57,7 @@ test.describe('Parent that does not wait for sub-workflow', () => { const { webhookPath, workflowId } = await api.workflows.importWorkflowFromDefinition(workflowDefinition); - const response = await api.request.get(`/webhook/${webhookPath}`); + const response = await api.webhooks.trigger(`/webhook/${webhookPath}`); expect(response.ok()).toBe(true); // First, wait for the child to finish. @@ -105,7 +105,7 @@ test.describe('CAT-1801: Parent receives correct data from child with wait node' await api.workflows.activate(parentWorkflowId, versionId!); // Trigger parent workflow via webhook - const webhookResponse = await api.request.get(`/webhook/${webhookPath}`); + const webhookResponse = await api.webhooks.trigger(`/webhook/${webhookPath}`); expect(webhookResponse.ok()).toBe(true); // Wait for child execution to appear and enter waiting state @@ -120,7 +120,9 @@ test.describe('CAT-1801: Parent receives correct data from child with wait node' ); // Trigger the wait webhook to resume child using child execution ID - const waitWebhookResponse = await api.request.get(`/webhook-waiting/${childExecution!.id}`); + const waitWebhookResponse = await api.webhooks.trigger( + `/webhook-waiting/${childExecution!.id}`, + ); expect(waitWebhookResponse.ok()).toBe(true); // Wait for parent to complete @@ -169,7 +171,7 @@ test.describe('CAT-1929: Parent should not resume until child with multiple wait await api.workflows.activate(parentWorkflowId, versionId!); // Trigger parent workflow via webhook - const webhookResponse = await api.request.get(`/webhook/${webhookPath}`); + const webhookResponse = await api.webhooks.trigger(`/webhook/${webhookPath}`); expect(webhookResponse.ok()).toBe(true); // Wait for child execution to appear and enter waiting state (first wait node) @@ -179,7 +181,7 @@ test.describe('CAT-1929: Parent should not resume until child with multiple wait await api.workflows.waitForWorkflowStatus(parentWorkflowId, 'waiting'); // Resume first wait node - const firstWaitResponse = await api.request.get(`/webhook-waiting/${childExecution.id}`); + const firstWaitResponse = await api.webhooks.trigger(`/webhook-waiting/${childExecution.id}`); expect(firstWaitResponse.ok()).toBe(true); // Wait for child to reach the second wait node @@ -193,7 +195,7 @@ test.describe('CAT-1929: Parent should not resume until child with multiple wait expect(parentExecAfterFirstWait.status).toBe('waiting'); // Resume second wait node - const secondWaitResponse = await api.request.get(`/webhook-waiting/${childExecution.id}`); + const secondWaitResponse = await api.webhooks.trigger(`/webhook-waiting/${childExecution.id}`); expect(secondWaitResponse.ok()).toBe(true); // Now parent should complete diff --git a/packages/workflow/src/interfaces.ts b/packages/workflow/src/interfaces.ts index 01109c11117..ed9924c52da 100644 --- a/packages/workflow/src/interfaces.ts +++ b/packages/workflow/src/interfaces.ts @@ -951,7 +951,7 @@ export interface FunctionsBase { getMode?: () => WorkflowExecuteMode; getActivationMode?: () => WorkflowActivateMode; getChatTrigger: () => INode | null; - + isNodeFeatureEnabled(featureName: string): boolean; getExecutionContext: () => IExecutionContext | undefined; /** @deprecated */ @@ -1549,12 +1549,17 @@ export type DisplayCondition = | { _cnd: { regex: string } } | { _cnd: { exists: true } }; +export type NodeFeatures = Record; +export type FeatureCondition = { '@version': Array }; +export type NodeFeaturesDefinition = Record; + export interface IDisplayOptions { hide?: { [key: string]: Array | undefined; }; show?: { '@version'?: Array; + '@feature'?: Array; '@tool'?: boolean[]; [key: string]: Array | undefined; }; @@ -2241,6 +2246,7 @@ export interface INodeTypeDescription extends INodeTypeBaseDescription { communityNodePackageVersion?: string; waitingNodeTooltip?: string; __loadOptionsMethods?: string[]; // only for validation during build + features?: NodeFeaturesDefinition; } export type TriggerPanelDefinition = { diff --git a/packages/workflow/src/node-helpers.ts b/packages/workflow/src/node-helpers.ts index 733d9bc7591..4cf23bf049c 100644 --- a/packages/workflow/src/node-helpers.ts +++ b/packages/workflow/src/node-helpers.ts @@ -35,6 +35,9 @@ import type { DisplayCondition, NodeConnectionType, ICredentialDataDecryptedObject, + FeatureCondition, + NodeFeaturesDefinition, + NodeFeatures, } from './interfaces'; import { validateFilterParameter } from './node-parameters/filter-parameter'; import type { IRunExecutionData } from './run-execution-data/run-execution-data'; @@ -252,6 +255,37 @@ export function isSubNodeType( : false; } +/** + * Evaluates a feature condition against a node version. + * @param featureDef The feature condition definition + * @param nodeVersion The node version to evaluate against + * @returns true if the feature is enabled, false otherwise + */ +function evaluateFeature(featureDef: FeatureCondition, nodeVersion: number): boolean { + return checkConditions(featureDef['@version'], [nodeVersion]); +} + +/** + * Evaluates all feature definitions for a node type and returns the computed features. + * @param featuresDef The feature definitions from the node type description + * @param nodeVersion The node version to evaluate against + * @returns A record of feature names to their enabled state + */ +export function getNodeFeatures( + featuresDef: NodeFeaturesDefinition | undefined, + nodeVersion: number, +): NodeFeatures { + if (!featuresDef) { + return {}; + } + + const features: NodeFeatures = {}; + for (const [featureName, condition] of Object.entries(featuresDef)) { + features[featureName] = evaluateFeature(condition, nodeVersion); + } + return features; +} + const getPropertyValues = ( nodeValues: INodeParameters | ICredentialDataDecryptedObject, propertyName: string, @@ -267,6 +301,15 @@ const getPropertyValues = ( value = node?.typeVersion || 0; } else if (propertyName === '@tool') { value = nodeTypeDescription?.name.endsWith('Tool') ?? false; + } else if (propertyName === '@feature') { + if (!nodeTypeDescription?.features || !node?.typeVersion) { + return []; + } + + const features = getNodeFeatures(nodeTypeDescription.features, node.typeVersion); + return Object.entries(features) + .filter(([_, enabled]) => enabled) + .map(([name]) => name); } else { // Get the value from current level value = get(nodeValues, propertyName); @@ -296,6 +339,12 @@ const checkConditions = ( ) { const [key, targetValue] = Object.entries(condition._cnd)[0]; + // Special case: empty array handling + if (actualValues.length === 0) { + if (key === 'not') return true; // Value is not present, so 'not' is true + return false; // For all other keys, empty array means condition is not met + } + return actualValues.every((propertyValue) => { if (key === 'eq') { return isEqual(propertyValue, targetValue); @@ -381,7 +430,7 @@ export function displayParameter( return true; } - if (values.length === 0 || !checkConditions(show[propertyName]!, values)) { + if (!checkConditions(show[propertyName]!, values)) { return false; } } diff --git a/packages/workflow/src/schemas.ts b/packages/workflow/src/schemas.ts index e89f2c9f939..f73147b2099 100644 --- a/packages/workflow/src/schemas.ts +++ b/packages/workflow/src/schemas.ts @@ -302,6 +302,7 @@ export const IDisplayOptionsSchema: z.ZodType = z.object({ show: z .object({ '@version': z.array(z.union([z.number(), DisplayConditionSchema])).optional(), + '@feature': z.array(z.union([z.string(), DisplayConditionSchema])).optional(), '@tool': z.array(z.boolean()).optional(), }) .catchall( diff --git a/packages/workflow/test/node-helpers.test.ts b/packages/workflow/test/node-helpers.test.ts index b24e5e3b730..c8d858b6f0e 100644 --- a/packages/workflow/test/node-helpers.test.ts +++ b/packages/workflow/test/node-helpers.test.ts @@ -4866,6 +4866,205 @@ describe('NodeHelpers', () => { }, false, ], + [ + 'Should return true if @feature is enabled (simple string array)', + { + ...defaultTestInput, + node: { + ...testNode, + typeVersion: 2.4, + }, + nodeTypeDescription: { + ...testNodeType, + features: { + useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] }, + }, + }, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + show: { + '@feature': ['useFeatureA'], + }, + }, + }, + }, + true, + ], + [ + 'Should return false if @feature is not enabled (simple string array)', + { + ...defaultTestInput, + node: { + ...testNode, + typeVersion: 2.3, + }, + nodeTypeDescription: { + ...testNodeType, + features: { + useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] }, + }, + }, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + show: { + '@feature': ['useFeatureA'], + }, + }, + }, + }, + false, + ], + [ + 'Should return true if @feature condition eq is met', + { + ...defaultTestInput, + node: { + ...testNode, + typeVersion: 2.4, + }, + nodeTypeDescription: { + ...testNodeType, + features: { + useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] }, + }, + }, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + show: { + '@feature': [{ _cnd: { eq: 'useFeatureA' } }], + }, + }, + }, + }, + true, + ], + [ + 'Should return false if @feature condition eq is not met', + { + ...defaultTestInput, + node: { + ...testNode, + typeVersion: 2.3, + }, + nodeTypeDescription: { + ...testNodeType, + features: { + useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] }, + }, + }, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + show: { + '@feature': [{ _cnd: { eq: 'useFeatureA' } }], + }, + }, + }, + }, + false, + ], + [ + 'Should return true if @feature condition not is met (feature disabled)', + { + ...defaultTestInput, + node: { + ...testNode, + typeVersion: 2.3, + }, + nodeTypeDescription: { + ...testNodeType, + features: { + useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] }, + }, + }, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + show: { + '@feature': [{ _cnd: { not: 'useFeatureA' } }], + }, + }, + }, + }, + true, + ], + [ + 'Should return false if @feature condition not is not met (feature enabled)', + { + ...defaultTestInput, + node: { + ...testNode, + typeVersion: 2.4, + }, + nodeTypeDescription: { + ...testNodeType, + features: { + useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] }, + }, + }, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + show: { + '@feature': [{ _cnd: { not: 'useFeatureA' } }], + }, + }, + }, + }, + false, + ], + [ + 'Should return true if @feature with multiple conditions (OR logic)', + { + ...defaultTestInput, + node: { + ...testNode, + typeVersion: 2.4, + }, + nodeTypeDescription: { + ...testNodeType, + features: { + useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] }, + useFeatureB: { '@version': [{ _cnd: { lte: 2.1 } }] }, + }, + }, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + show: { + '@feature': [{ _cnd: { eq: 'useFeatureA' } }, { _cnd: { eq: 'useFeatureB' } }], + }, + }, + }, + }, + true, + ], + [ + 'Should return false if @feature is checked but node has no features', + { + ...defaultTestInput, + node: { + ...testNode, + typeVersion: 2.4, + }, + nodeTypeDescription: { + ...testNodeType, + features: undefined, + }, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + show: { + '@feature': ['useFeatureA'], + }, + }, + }, + }, + false, + ], ]; for (const [description, input, expected] of tests) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d6d64cb4717..cdf7516cfe7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -279,6 +279,9 @@ importers: .: devDependencies: + '@babel/preset-env': + specifier: ^7.26.0 + version: 7.28.5(@babel/core@7.28.4) '@biomejs/biome': specifier: ^1.9.0 version: 1.9.0 @@ -555,7 +558,7 @@ importers: version: 4.0.7 axios: specifier: 1.12.0 - version: 1.12.0(debug@4.4.3) + version: 1.12.0(debug@4.4.1) dotenv: specifier: 17.2.3 version: 17.2.3 @@ -580,7 +583,7 @@ importers: dependencies: axios: specifier: 1.12.0 - version: 1.12.0(debug@4.4.3) + version: 1.12.0(debug@4.4.1) devDependencies: '@n8n/typescript-config': specifier: workspace:* @@ -1340,7 +1343,7 @@ importers: version: link:../eslint-plugin-community-nodes axios: specifier: 1.12.0 - version: 1.12.0(debug@4.4.3) + version: 1.12.0(debug@4.4.1) eslint: specifier: 'catalog:' version: 9.29.0(jiti@2.6.1) @@ -1554,7 +1557,7 @@ importers: version: 1.11.0 axios: specifier: 1.12.0 - version: 1.12.0(debug@4.4.3) + version: 1.12.0(debug@4.4.1) bcryptjs: specifier: 2.4.3 version: 2.4.3 @@ -1909,7 +1912,7 @@ importers: version: 9.42.1 axios: specifier: 1.12.0 - version: 1.12.0(debug@4.4.3) + version: 1.12.0(debug@4.4.1) callsites: specifier: 'catalog:' version: 3.1.0 @@ -2376,7 +2379,7 @@ importers: version: link:../../../@n8n/utils axios: specifier: 1.12.0 - version: 1.12.0(debug@4.4.3) + version: 1.12.0(debug@4.4.1) flatted: specifier: 'catalog:' version: 3.2.7 @@ -2633,7 +2636,7 @@ importers: version: 1.1.4 axios: specifier: 1.12.0 - version: 1.12.0(debug@4.4.3) + version: 1.12.0(debug@4.4.1) bowser: specifier: 2.11.0 version: 2.11.0 @@ -3963,10 +3966,6 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.27.5': - resolution: {integrity: sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==} - engines: {node: '>=6.9.0'} - '@babel/compat-data@7.28.5': resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} engines: {node: '>=6.9.0'} @@ -3983,10 +3982,6 @@ packages: resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} - '@babel/helper-annotate-as-pure@7.25.9': - resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} - engines: {node: '>=6.9.0'} - '@babel/helper-annotate-as-pure@7.27.3': resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} engines: {node: '>=6.9.0'} @@ -4001,12 +3996,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-create-regexp-features-plugin@7.26.3': - resolution: {integrity: sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@babel/helper-create-regexp-features-plugin@7.28.5': resolution: {integrity: sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==} engines: {node: '>=6.9.0'} @@ -4578,10 +4567,6 @@ packages: resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.4': - resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} - engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.5': resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} engines: {node: '>=6.9.0'} @@ -15900,10 +15885,6 @@ packages: reftools@1.1.9: resolution: {integrity: sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==} - regenerate-unicode-properties@10.2.0: - resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} - engines: {node: '>=4'} - regenerate-unicode-properties@10.2.2: resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==} engines: {node: '>=4'} @@ -15930,10 +15911,6 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} - regexpu-core@6.2.0: - resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==} - engines: {node: '>=4'} - regexpu-core@6.4.0: resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} engines: {node: '>=4'} @@ -17545,10 +17522,6 @@ packages: resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} engines: {node: '>=4'} - unicode-match-property-value-ecmascript@2.2.0: - resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==} - engines: {node: '>=4'} - unicode-match-property-value-ecmascript@2.2.1: resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==} engines: {node: '>=4'} @@ -17941,8 +17914,8 @@ packages: vue-component-type-helpers@2.2.12: resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==} - vue-component-type-helpers@3.1.7: - resolution: {integrity: sha512-SxZOijzRU0LiTYARVZNYf//21bZRLJ26P2prcA6a+FfDQxJtan4tEnsryIY7aiLh2LVoet0ci2G+6RrArQ+7tA==} + vue-component-type-helpers@3.1.8: + resolution: {integrity: sha512-oaowlmEM6BaYY+8o+9D9cuzxpWQWHqHTMKakMxXu0E+UCIOMTljyIPO15jcnaCwJtZu/zWDotK7mOIHvWD9mcw==} vue-demi@0.14.10: resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} @@ -20273,8 +20246,6 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.27.5': {} - '@babel/compat-data@7.28.5': {} '@babel/core@7.28.4': @@ -20286,7 +20257,7 @@ snapshots: '@babel/helpers': 7.28.4 '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 @@ -20313,17 +20284,13 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 - '@babel/helper-annotate-as-pure@7.25.9': - dependencies: - '@babel/types': 7.28.5 - '@babel/helper-annotate-as-pure@7.27.3': dependencies: '@babel/types': 7.28.5 '@babel/helper-compilation-targets@7.27.2': dependencies: - '@babel/compat-data': 7.27.5 + '@babel/compat-data': 7.28.5 '@babel/helper-validator-option': 7.27.1 browserslist: 4.27.0 lru-cache: 5.1.1 @@ -20342,13 +20309,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-create-regexp-features-plugin@7.26.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-annotate-as-pure': 7.25.9 - regexpu-core: 6.2.0 - semver: 7.7.3 - '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 @@ -20403,7 +20363,7 @@ snapshots: '@babel/core': 7.28.4 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-wrap-function': 7.28.3 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -20412,13 +20372,13 @@ snapshots: '@babel/core': 7.28.4 '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -20434,7 +20394,7 @@ snapshots: '@babel/helper-wrap-function@7.28.3': dependencies: '@babel/template': 7.27.2 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -20483,7 +20443,7 @@ snapshots: dependencies: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -20594,7 +20554,7 @@ snapshots: '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 - '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.28.4) + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.4)': @@ -20607,7 +20567,7 @@ snapshots: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.4) - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -20654,7 +20614,7 @@ snapshots: '@babel/helper-globals': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -20725,7 +20685,7 @@ snapshots: '@babel/core': 7.28.4 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -20811,7 +20771,7 @@ snapshots: '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.28.4) '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.4) - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -21031,18 +20991,6 @@ snapshots: '@babel/parser': 7.28.5 '@babel/types': 7.28.5 - '@babel/traverse@7.28.4': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.5 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.5 - '@babel/template': 7.27.2 - '@babel/types': 7.28.5 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - '@babel/traverse@7.28.5': dependencies: '@babel/code-frame': 7.27.1 @@ -23628,7 +23576,7 @@ snapshots: '@rudderstack/rudder-sdk-node@2.1.4(tslib@2.8.1)': dependencies: - axios: 1.12.0(debug@4.4.3) + axios: 1.12.0(debug@4.4.1) axios-retry: 4.5.0(axios@1.12.0) component-type: 2.0.0 join-component: 1.1.0 @@ -24930,7 +24878,7 @@ snapshots: storybook: 9.1.7(@testing-library/dom@10.4.0)(bufferutil@4.0.9)(prettier@3.6.2)(utf-8-validate@5.0.10)(vite@7.0.0(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.89.2)(terser@5.16.1)(tsx@4.19.3)) type-fest: 2.19.0 vue: 3.5.13(typescript@5.9.2) - vue-component-type-helpers: 3.1.7 + vue-component-type-helpers: 3.1.8 '@stylistic/eslint-plugin@5.0.0(eslint@9.29.0(jiti@2.6.1))': dependencies: @@ -26719,7 +26667,7 @@ snapshots: axios-retry@4.5.0(axios@1.12.0): dependencies: - axios: 1.12.0(debug@4.4.3) + axios: 1.12.0(debug@4.4.1) is-retry-allowed: 2.2.0 axios@1.12.0(debug@4.3.6): @@ -26738,14 +26686,6 @@ snapshots: transitivePeerDependencies: - debug - axios@1.12.0(debug@4.4.3): - dependencies: - follow-redirects: 1.15.11(debug@4.4.1) - form-data: 4.0.4 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - b4a@1.6.7: {} babel-jest@29.6.2(@babel/core@7.28.4): @@ -27049,10 +26989,10 @@ snapshots: browserslist@4.25.0: dependencies: - caniuse-lite: 1.0.30001724 - electron-to-chromium: 1.5.173 - node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.25.0) + caniuse-lite: 1.0.30001752 + electron-to-chromium: 1.5.244 + node-releases: 2.0.27 + update-browserslist-db: 1.1.4(browserslist@4.25.0) browserslist@4.27.0: dependencies: @@ -27132,7 +27072,7 @@ snapshots: bundlemon@3.1.0(typescript@5.9.2): dependencies: - axios: 1.12.0(debug@4.4.3) + axios: 1.12.0(debug@4.4.1) axios-retry: 4.5.0(axios@1.12.0) brotli-size: 4.0.0 bundlemon-utils: 2.0.1 @@ -30235,7 +30175,7 @@ snapshots: '@types/debug': 4.1.12 '@types/node': 20.19.21 '@types/tough-cookie': 4.0.5 - axios: 1.12.0(debug@4.4.3) + axios: 1.12.0(debug@4.4.1) camelcase: 6.3.0 debug: 4.4.3 dotenv: 16.6.1 @@ -30323,7 +30263,7 @@ snapshots: infisical-node@1.3.0: dependencies: - axios: 1.12.0(debug@4.4.3) + axios: 1.12.0(debug@4.4.1) dotenv: 16.6.1 tweetnacl: 1.0.3 tweetnacl-util: 0.15.1 @@ -33903,7 +33843,7 @@ snapshots: posthog-node@3.2.1: dependencies: - axios: 1.12.0(debug@4.4.3) + axios: 1.12.0(debug@4.4.1) rusha: 0.8.14 transitivePeerDependencies: - debug @@ -34470,10 +34410,6 @@ snapshots: reftools@1.1.9: {} - regenerate-unicode-properties@10.2.0: - dependencies: - regenerate: 1.4.2 - regenerate-unicode-properties@10.2.2: dependencies: regenerate: 1.4.2 @@ -34509,15 +34445,6 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 - regexpu-core@6.2.0: - dependencies: - regenerate: 1.4.2 - regenerate-unicode-properties: 10.2.0 - regjsgen: 0.8.0 - regjsparser: 0.12.0 - unicode-match-property-ecmascript: 2.0.0 - unicode-match-property-value-ecmascript: 2.2.0 - regexpu-core@6.4.0: dependencies: regenerate: 1.4.2 @@ -34628,7 +34555,7 @@ snapshots: retry-axios@2.6.0(axios@1.12.0(debug@4.4.3)): dependencies: - axios: 1.12.0(debug@4.4.3) + axios: 1.12.0(debug@4.4.1) retry-request@7.0.2(encoding@0.1.13): dependencies: @@ -35234,7 +35161,7 @@ snapshots: asn1.js: 5.4.1 asn1.js-rfc2560: 5.0.1(asn1.js@5.4.1) asn1.js-rfc5280: 3.0.0 - axios: 1.12.0(debug@4.4.3) + axios: 1.12.0(debug@4.4.1) big-integer: 1.6.52 bignumber.js: 9.1.2 binascii: 0.0.2 @@ -36565,8 +36492,6 @@ snapshots: unicode-canonical-property-names-ecmascript: 2.0.1 unicode-property-aliases-ecmascript: 2.1.0 - unicode-match-property-value-ecmascript@2.2.0: {} - unicode-match-property-value-ecmascript@2.2.1: {} unicode-property-aliases-ecmascript@2.1.0: {} @@ -36652,7 +36577,7 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - update-browserslist-db@1.1.3(browserslist@4.25.0): + update-browserslist-db@1.1.4(browserslist@4.25.0): dependencies: browserslist: 4.25.0 escalade: 3.2.0 @@ -36962,7 +36887,7 @@ snapshots: vue-component-type-helpers@2.2.12: {} - vue-component-type-helpers@3.1.7: {} + vue-component-type-helpers@3.1.8: {} vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)): dependencies: