From f60cff1b93805704c9d8d49c26126a5005b9df61 Mon Sep 17 00:00:00 2001 From: Andreas Fitzek Date: Mon, 20 Apr 2026 17:23:16 +0200 Subject: [PATCH 1/3] Add cluster check interface --- packages/@n8n/decorators/package.json | 1 + .../__tests__/cluster-check-metadata.test.ts | 179 +++++++++ .../cluster-check/cluster-check-metadata.ts | 162 +++++++++ .../src/cluster-check/cluster-check.ts | 342 ++++++++++++++++++ .../decorators/src/cluster-check/index.ts | 2 + packages/@n8n/decorators/src/index.ts | 1 + packages/@n8n/decorators/tsconfig.json | 1 + pnpm-lock.yaml | 28 +- 8 files changed, 701 insertions(+), 15 deletions(-) create mode 100644 packages/@n8n/decorators/src/cluster-check/__tests__/cluster-check-metadata.test.ts create mode 100644 packages/@n8n/decorators/src/cluster-check/cluster-check-metadata.ts create mode 100644 packages/@n8n/decorators/src/cluster-check/cluster-check.ts create mode 100644 packages/@n8n/decorators/src/cluster-check/index.ts diff --git a/packages/@n8n/decorators/package.json b/packages/@n8n/decorators/package.json index d48b3a092d216..6df403a9ee92c 100644 --- a/packages/@n8n/decorators/package.json +++ b/packages/@n8n/decorators/package.json @@ -30,6 +30,7 @@ "zod": "catalog:" }, "dependencies": { + "@n8n/api-types": "workspace:*", "@n8n/constants": "workspace:*", "@n8n/di": "workspace:*", "@n8n/permissions": "workspace:*", diff --git a/packages/@n8n/decorators/src/cluster-check/__tests__/cluster-check-metadata.test.ts b/packages/@n8n/decorators/src/cluster-check/__tests__/cluster-check-metadata.test.ts new file mode 100644 index 0000000000000..b8f451ae71eb7 --- /dev/null +++ b/packages/@n8n/decorators/src/cluster-check/__tests__/cluster-check-metadata.test.ts @@ -0,0 +1,179 @@ +import { Container, Service } from '@n8n/di'; + +import type { ClusterCheckContext, ClusterCheckResult, IClusterCheck } from '../cluster-check'; +import { ClusterCheck, ClusterCheckMetadata } from '../cluster-check-metadata'; + +describe('ClusterCheckMetadata', () => { + let metadata: ClusterCheckMetadata; + + beforeEach(() => { + metadata = new ClusterCheckMetadata(); + }); + + it('should register check classes', () => { + class TestCheck implements IClusterCheck { + checkDescription = { name: 'test.check' }; + async run(_context: ClusterCheckContext): Promise { + return {}; + } + } + + metadata.register({ class: TestCheck }); + + expect(metadata.getClasses()).toContain(TestCheck); + }); + + it('should return all registered entries', () => { + class FirstCheck implements IClusterCheck { + checkDescription = { name: 'first.check' }; + async run(_context: ClusterCheckContext): Promise { + return {}; + } + } + class SecondCheck implements IClusterCheck { + checkDescription = { name: 'second.check' }; + async run(_context: ClusterCheckContext): Promise { + return {}; + } + } + + metadata.register({ class: FirstCheck }); + metadata.register({ class: SecondCheck }); + + expect(metadata.getEntries()).toHaveLength(2); + }); + + it('should return registered classes in registration order', () => { + class FirstCheck implements IClusterCheck { + checkDescription = { name: 'first.check' }; + async run(_context: ClusterCheckContext): Promise { + return {}; + } + } + class SecondCheck implements IClusterCheck { + checkDescription = { name: 'second.check' }; + async run(_context: ClusterCheckContext): Promise { + return {}; + } + } + + metadata.register({ class: FirstCheck }); + metadata.register({ class: SecondCheck }); + + expect(metadata.getClasses()).toEqual([FirstCheck, SecondCheck]); + }); +}); + +describe('@ClusterCheck decorator', () => { + let metadata: ClusterCheckMetadata; + + beforeEach(() => { + vi.resetAllMocks(); + + metadata = new ClusterCheckMetadata(); + Container.set(ClusterCheckMetadata, metadata); + }); + + it('should register the decorated class in ClusterCheckMetadata', () => { + @ClusterCheck() + class TestCheck implements IClusterCheck { + checkDescription = { name: 'cluster.test' }; + async run(_context: ClusterCheckContext): Promise { + return {}; + } + } + + const registered = metadata.getClasses(); + + expect(registered).toContain(TestCheck); + expect(registered).toHaveLength(1); + }); + + it('should register multiple decorated classes', () => { + @ClusterCheck() + class VersionCheck implements IClusterCheck { + checkDescription = { name: 'cluster.versionMismatch' }; + async run(_context: ClusterCheckContext): Promise { + return {}; + } + } + + @ClusterCheck() + class LeaderCheck implements IClusterCheck { + checkDescription = { name: 'cluster.leaderMissing' }; + async run(_context: ClusterCheckContext): Promise { + return {}; + } + } + + const registered = metadata.getClasses(); + + expect(registered).toContain(VersionCheck); + expect(registered).toContain(LeaderCheck); + expect(registered).toHaveLength(2); + }); + + it('should apply @Service() so the class is resolvable via DI', () => { + @ClusterCheck() + class TestCheck implements IClusterCheck { + checkDescription = { name: 'cluster.test' }; + async run(_context: ClusterCheckContext): Promise { + return {}; + } + } + + expect(Container.has(TestCheck)).toBe(true); + + const instance = Container.get(TestCheck); + + expect(instance).toBeInstanceOf(TestCheck); + expect(instance.checkDescription).toEqual({ name: 'cluster.test' }); + }); + + it('should support checks with constructor-injected dependencies', () => { + @Service() + class Logger { + log(message: string) { + return message; + } + } + + @ClusterCheck() + class TestCheck implements IClusterCheck { + checkDescription = { name: 'cluster.test' }; + constructor(private readonly logger: Logger) {} + async run(_context: ClusterCheckContext): Promise { + this.logger.log('run'); + return {}; + } + } + + const instance = Container.get(TestCheck); + + expect(instance).toBeInstanceOf(TestCheck); + }); + + it('should expose different description names for different checks', () => { + @ClusterCheck() + class VersionCheck implements IClusterCheck { + checkDescription = { name: 'cluster.versionMismatch', displayName: 'Version Mismatch' }; + async run(_context: ClusterCheckContext): Promise { + return {}; + } + } + + @ClusterCheck() + class LeaderCheck implements IClusterCheck { + checkDescription = { name: 'cluster.leaderMissing', displayName: 'Leader Missing' }; + async run(_context: ClusterCheckContext): Promise { + return {}; + } + } + + const versionInstance = Container.get(VersionCheck); + const leaderInstance = Container.get(LeaderCheck); + + expect(versionInstance.checkDescription.name).toBe('cluster.versionMismatch'); + expect(leaderInstance.checkDescription.name).toBe('cluster.leaderMissing'); + }); +}); diff --git a/packages/@n8n/decorators/src/cluster-check/cluster-check-metadata.ts b/packages/@n8n/decorators/src/cluster-check/cluster-check-metadata.ts new file mode 100644 index 0000000000000..4a5b590168eb6 --- /dev/null +++ b/packages/@n8n/decorators/src/cluster-check/cluster-check-metadata.ts @@ -0,0 +1,162 @@ +import { Container, Service } from '@n8n/di'; + +import { ClusterCheckClass } from './cluster-check'; + +/** + * Registry entry for a cluster check. + * + * Lightweight wrapper around the check class constructor, modelled on + * `ContextEstablishmentHookEntry`. Kept as a wrapper (rather than storing the + * class directly) so the entry shape can grow — e.g. module source, feature + * flag, license flag — without breaking the registration API. + * + * @internal + */ +type ClusterCheckEntry = { + /** The check class constructor for DI container instantiation. */ + class: ClusterCheckClass; +}; + +/** + * Low-level metadata registry for cluster checks. + * + * `ClusterCheckMetadata` is a simple collection of every class decorated with + * `@ClusterCheck()`. It is populated automatically at module load time, before + * the Leader Service starts, so checks are discoverable without any manual + * registration code. + * + * **Architecture:** + * ``` + * @ClusterCheck() → ClusterCheckMetadata → Cluster Check Registry → Leader Service + * (registration) (collection) (instantiation) (execution) + * ``` + * + * **Responsibilities kept here:** + * - Storing registered check classes. + * - Exposing them for downstream consumers. + * + * **Responsibilities explicitly NOT here:** + * - Instantiating checks (DI container). + * - Validating name uniqueness (higher-level registry). + * - Executing checks (Leader Service). + * + * @see ClusterCheck decorator for automatic registration. + * @see IClusterCheck for the check contract. + */ +@Service() +export class ClusterCheckMetadata { + /** + * Internal collection of registered cluster check entries. + * + * Uses `Set` for efficient deduplication (though duplicate registration + * should not occur with correct decorator usage). + */ + private readonly clusterChecks: Set = new Set(); + + /** + * Registers a cluster check class in the metadata collection. + * + * Called automatically by the `@ClusterCheck()` decorator at module load + * time. Should not be called directly by application code. + * + * Name uniqueness and any other semantic validation is the responsibility + * of the higher-level Cluster Check Registry, not this service. + * + * @param entry - The check class entry to register. + * @internal Called by decorator only. + */ + register(entry: ClusterCheckEntry) { + this.clusterChecks.add(entry); + } + + /** + * Retrieves all registered entries as `[index, entry]` tuples. + * + * Primarily useful for debugging or low-level iteration. Prefer + * {@link getClasses} for most use cases. + * + * @returns Array of `[index, entry]` tuples from the internal `Set`. + * + * @example + * ```typescript + * for (const [index, entry] of metadata.getEntries()) { + * console.log(`Check #${index}:`, entry.class.name); + * } + * ``` + */ + getEntries() { + return [...this.clusterChecks.entries()]; + } + + /** + * Retrieves all registered check class constructors in registration order. + * + * This is the primary method consumed by the Cluster Check Registry to + * instantiate checks via the DI container. + * + * @returns Array of check class constructors ready for DI instantiation. + * + * @example + * ```typescript + * @Service() + * export class ClusterCheckRegistry { + * constructor(private readonly metadata: ClusterCheckMetadata) { + * this.checks = this.metadata.getClasses().map((cls) => Container.get(cls)); + * } + * } + * ``` + */ + getClasses() { + return [...this.clusterChecks.values()].map((entry) => entry.class); + } +} + +/** + * Class decorator that registers a cluster check for auto-discovery. + * + * Performs two distinct operations at class-definition time: + * 1. **Registers** the class in {@link ClusterCheckMetadata} so the higher-level + * registry can discover it without manual wiring. + * 2. **Enables DI** by applying `@Service()`, making the class constructor + * resolvable via `Container.get()` (including dependencies). + * + * The decorator takes no arguments — all check metadata lives on the check + * instance itself via {@link IClusterCheck.checkDescription}. + * + * **Requirements:** + * - Decorated class MUST implement {@link IClusterCheck}. + * - Decorated class MUST expose a `checkDescription` with a unique `name`. + * + * **Notes:** + * - Registration happens eagerly at module load, not lazily. + * - Duplicate decoration of the same class is safe (`Set` deduplicates). + * - Checks are registered as singletons via `@Service()`. + * + * @example + * ```typescript + * @ClusterCheck() + * export class VersionMismatchCheck implements IClusterCheck { + * checkDescription = { + * name: 'cluster.versionMismatch', + * displayName: 'Cluster Version Mismatch', + * }; + * + * async run({ currentState }: ClusterCheckContext): Promise { + * // ... check logic + * return {}; + * } + * } + * ``` + * + * @returns A class decorator that registers the check and enables DI for it. + */ +export const ClusterCheck = + () => + (target: T) => { + Container.get(ClusterCheckMetadata).register({ + class: target, + }); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return Service()(target); + }; diff --git a/packages/@n8n/decorators/src/cluster-check/cluster-check.ts b/packages/@n8n/decorators/src/cluster-check/cluster-check.ts new file mode 100644 index 0000000000000..01befcd8c8dee --- /dev/null +++ b/packages/@n8n/decorators/src/cluster-check/cluster-check.ts @@ -0,0 +1,342 @@ +import type { InstanceRegistration } from '@n8n/api-types'; +import type { Constructable } from '@n8n/di'; + +/** + * Structured difference between two cluster state snapshots. + * + * Computed once by the Cluster Check Registry from the current and previous + * `Map` snapshots, then passed to every + * registered check via {@link ClusterCheckContext}. Checks consume the diff + * read-only — they never mutate it and they never recompute it themselves. + * + * **Semantics:** + * - `added`: instances present in the current snapshot that were not in the previous snapshot. + * - `removed`: instances present in the previous snapshot that are no longer in the current snapshot. + * - `changed`: instances present in both snapshots whose registration payload differs + * (e.g. role flip, version bump, `lastSeen` refresh). Each entry carries both the + * `previous` and `current` registration so checks can reason about the transition. + * + * **Equality definition for `changed`** is deliberately left to the registry so checks + * do not depend on a specific equality strategy (deep-equal, selected-field compare, etc.). + * + * @example + * ```typescript + * async run({ diff }: ClusterCheckContext) { + * const leadershipFlipped = diff.changed.filter( + * ({ previous, current }) => + * previous.instanceRole !== current.instanceRole && + * current.instanceRole === 'leader', + * ); + * } + * ``` + */ +export type ClusterStateDiff = { + /** Instances present in the current snapshot that were not in the previous one. */ + added: readonly InstanceRegistration[]; + + /** Instances present in the previous snapshot that are no longer in the current one. */ + removed: readonly InstanceRegistration[]; + + /** + * Instances whose registration payload changed between snapshots. + * Each entry carries both the `previous` and `current` registration. + */ + changed: ReadonlyArray<{ previous: InstanceRegistration; current: InstanceRegistration }>; +}; + +/** + * Input context passed to a cluster check on every invocation. + * + * The context bundles the two state snapshots the check reasons over (current and + * previous) together with a pre-computed {@link ClusterStateDiff}. Snapshots are + * keyed by `instanceKey` to match the existing instance storage contract + * (`InstanceStorage.getLastKnownState()` / `saveLastKnownState()`). + * + * **Read-only by contract:** the maps and the diff are shared across every + * registered check in a single run. Checks must not mutate them. The types + * enforce this with `ReadonlyMap` and `readonly` arrays. + * + * @example + * ```typescript + * async run({ currentState, previousState, diff }: ClusterCheckContext) { + * const currentCount = currentState.size; + * const previousCount = previousState.size; + * const flapping = diff.added.length > 0 && diff.removed.length > 0; + * // ... + * } + * ``` + */ +export type ClusterCheckContext = { + /** Current cluster state keyed by `instanceKey`. */ + currentState: ReadonlyMap; + + /** + * Previous cluster state keyed by `instanceKey`. + * + * Sourced from `InstanceStorage.getLastKnownState()`. May be empty on the very + * first check run after a fresh leader start. + */ + previousState: ReadonlyMap; + + /** + * Structured diff between `previousState` and `currentState`. + * + * Pre-computed by the registry so every check reuses the same diff result. + */ + diff: ClusterStateDiff; +}; + +/** + * Structured warning emitted by a cluster check. + * + * Warnings are a neutral, transport-agnostic signal the check wants to raise + * about the cluster. A downstream handler in `packages/cli` decides how to + * surface them (logs, metrics, admin dashboard, etc.). + * + * @example + * ```typescript + * { + * code: 'cluster.versionMismatch', + * message: 'Detected 2 distinct n8n versions across 5 instances', + * severity: 'warning', + * context: { versions: ['1.42.0', '1.43.0'] }, + * } + * ``` + */ +export type ClusterCheckWarning = { + /** + * Stable machine-readable identifier for the warning. + * + * Should be namespaced, e.g. `'cluster.versionMismatch'`, `'cluster.leaderMissing'`. + * Consumers key off this field for dedup, i18n, and aggregation. + */ + code: string; + + /** Human-readable single-line summary. */ + message: string; + + /** + * Severity hint. Defaults to `'warning'` if omitted. + * + * Kept deliberately narrow — finer-grained categorisation belongs to the + * downstream translator, not to the decorator layer. + */ + severity?: 'info' | 'warning' | 'error'; + + /** + * Optional structured context for the warning (e.g. offending instance keys, + * observed values). Serialised verbatim by downstream handlers. + */ + context?: Record; +}; + +/** + * Structured audit event emitted by a cluster check. + * + * Audit events are intentionally loose at this layer: the `@n8n/decorators` + * package must not depend on the cli audit event enum. A downstream translator + * maps `eventName` to the concrete `EventMessageAudit` class at emit time. + * + * @example + * ```typescript + * { + * eventName: 'n8n.audit.cluster.version-mismatch-detected', + * payload: { detectedVersions: ['1.42.0', '1.43.0'], instanceCount: 5 }, + * } + * ``` + */ +export type ClusterCheckAuditEvent = { + /** + * Audit event name the downstream translator will map to a concrete + * audit event class (e.g. `EventMessageAudit`). + */ + eventName: string; + + /** Event payload forwarded to the audit bus. Shape depends on `eventName`. */ + payload: Record; +}; + +/** + * Structured push notification emitted by a cluster check. + * + * Kept decoupled from the concrete `PushMessage` discriminated union defined in + * `@n8n/api-types/push` so the decorator package stays free of transport-type + * churn. A downstream translator narrows `{ type, data }` into the concrete + * push message union before broadcasting. + * + * @example + * ```typescript + * { + * type: 'cluster-version-mismatch', + * data: { versions: ['1.42.0', '1.43.0'] }, + * } + * ``` + */ +export type ClusterCheckPushNotification = { + /** + * Push message type string the downstream translator maps to a concrete + * `PushMessage` variant. + */ + type: string; + + /** Push payload forwarded to connected clients. Shape depends on `type`. */ + data: Record; +}; + +/** + * Result returned by a cluster check after processing a {@link ClusterCheckContext}. + * + * All fields are optional. A check that finds nothing worth reporting returns + * an empty object (or populates only the channels it needs). The registry + * aggregates results from every registered check and forwards each channel + * (warnings, audit events, push notifications) to its downstream handler. + * + * **Why three separate channels?** Each has a distinct consumer: + * - `warnings` feeds admin-facing diagnostics and logs. + * - `auditEvents` feeds the audit log / event bus for compliance. + * - `pushNotifications` feeds the real-time push channel to frontends. + * + * @example + * ```typescript + * return { + * warnings: [{ code: 'cluster.leaderMissing', message: 'No leader in cluster', severity: 'error' }], + * auditEvents: [{ eventName: 'n8n.audit.cluster.leader-lost', payload: {} }], + * pushNotifications: [{ type: 'cluster-leader-lost', data: {} }], + * }; + * ``` + */ +export type ClusterCheckResult = { + /** Warnings raised by the check. Omit or leave empty when nothing to report. */ + warnings?: ClusterCheckWarning[]; + + /** Audit events the check wants emitted. Omit or leave empty when not applicable. */ + auditEvents?: ClusterCheckAuditEvent[]; + + /** Push notifications the check wants broadcast. Omit or leave empty when not applicable. */ + pushNotifications?: ClusterCheckPushNotification[]; +}; + +/** + * Self-describing metadata for a cluster check. + * + * Each check instance carries its own description rather than passing it into + * the `@ClusterCheck()` decorator. This keeps the decorator parameter-free and + * lets checks compute display names from runtime data if needed. + * + * **Naming convention:** use namespaced lowercase ids, e.g. + * `'cluster.versionMismatch'`, `'cluster.leaderMissing'`. Names must be unique + * across all registered checks — uniqueness is validated by the higher-level + * registry, not by {@link ClusterCheckMetadata}. + * + * @example + * ```typescript + * checkDescription = { + * name: 'cluster.versionMismatch', + * displayName: 'Cluster Version Mismatch', + * }; + * ``` + */ +export type CheckDescription = { + /** + * Unique machine-readable identifier for this check. + * + * **Naming convention:** namespaced lowercase, e.g. `'cluster.versionMismatch'`. + * Used as the lookup key by the higher-level registry and in logs / telemetry. + */ + name: string; + + /** + * Human-readable display name for the check. Shown in admin UI and logs. + * Falls back to `name` when omitted. + */ + displayName?: string; +}; + +/** + * Interface every cluster check must implement. + * + * Checks are stateless consumers of cluster state snapshots. The registry + * invokes `run()` on every reconciliation tick, passes a pre-built + * {@link ClusterCheckContext}, and forwards the returned + * {@link ClusterCheckResult} to the appropriate downstream handlers. + * + * **Lifecycle:** + * 1. Class is decorated with `@ClusterCheck()` — registered in + * {@link ClusterCheckMetadata} and made injectable via `@Service()`. + * 2. Leader Service resolves the class from the DI container on startup. + * 3. `run()` is invoked on every tick with a fresh context. + * + * **Implementation rules:** + * - `run()` must be pure with respect to `context` — never mutate the maps or diff. + * - `run()` must be fast; it runs on the hot path of every reconciliation tick. + * - Throw only for unrecoverable failures. The registry logs and isolates errors + * so one failing check does not stop the others from running. + * + * @example + * ```typescript + * @ClusterCheck() + * export class VersionMismatchCheck implements IClusterCheck { + * checkDescription = { + * name: 'cluster.versionMismatch', + * displayName: 'Cluster Version Mismatch', + * }; + * + * async run({ currentState }: ClusterCheckContext): Promise { + * const versions = new Set( + * [...currentState.values()].map((instance) => instance.version), + * ); + * if (versions.size <= 1) return {}; + * return { + * warnings: [ + * { + * code: 'cluster.versionMismatch', + * message: `Detected ${versions.size} distinct n8n versions in the cluster`, + * severity: 'warning', + * context: { versions: [...versions] }, + * }, + * ], + * }; + * } + * } + * ``` + */ +export interface IClusterCheck { + /** + * Self-describing metadata used by the registry for lookup and display. + * + * @see CheckDescription for naming conventions and uniqueness requirements. + */ + checkDescription: CheckDescription; + + /** + * Runs the check over a cluster state snapshot. + * + * **Contract:** + * - Must treat `context` as read-only. + * - Must not log or persist plaintext state beyond what the returned + * {@link ClusterCheckResult} declares. + * - Errors thrown here stop this check's contribution to the current tick + * but do not stop other checks. The registry is responsible for error isolation. + * + * @param context - Current/previous cluster state and pre-computed diff. + * @returns Warnings, audit events, and push notifications the check wants emitted. + */ + run(context: ClusterCheckContext): Promise; +} + +/** + * Type representing the constructor of a class that implements {@link IClusterCheck}. + * + * Used by {@link ClusterCheckMetadata} to store registered classes and by the + * DI container to instantiate them. Works with the `@ClusterCheck()` decorator. + * + * @example + * ```typescript + * import { Container } from '@n8n/di'; + * import type { ClusterCheckClass } from '@n8n/decorators'; + * + * const CheckClass: ClusterCheckClass = VersionMismatchCheck; + * const instance = Container.get(CheckClass); + * ``` + */ +export type ClusterCheckClass = Constructable; diff --git a/packages/@n8n/decorators/src/cluster-check/index.ts b/packages/@n8n/decorators/src/cluster-check/index.ts new file mode 100644 index 0000000000000..b76d8750c5e14 --- /dev/null +++ b/packages/@n8n/decorators/src/cluster-check/index.ts @@ -0,0 +1,2 @@ +export { ClusterCheckMetadata, ClusterCheck } from './cluster-check-metadata'; +export type * from './cluster-check'; diff --git a/packages/@n8n/decorators/src/index.ts b/packages/@n8n/decorators/src/index.ts index 782c058816f04..82142f5220ac7 100644 --- a/packages/@n8n/decorators/src/index.ts +++ b/packages/@n8n/decorators/src/index.ts @@ -5,6 +5,7 @@ export { Debounce } from './debounce'; export * from './execution-lifecycle'; export { Memoized } from './memoized'; export * from './auth-handler'; +export * from './cluster-check'; export * from './context-establishment'; export * from './credential-resolver'; export * from './module'; diff --git a/packages/@n8n/decorators/tsconfig.json b/packages/@n8n/decorators/tsconfig.json index c9682bff55639..62d8b00a5435d 100644 --- a/packages/@n8n/decorators/tsconfig.json +++ b/packages/@n8n/decorators/tsconfig.json @@ -11,6 +11,7 @@ "include": ["src/**/*.ts"], "references": [ { "path": "../../workflow/tsconfig.build.esm.json" }, + { "path": "../api-types/tsconfig.build.json" }, { "path": "../constants/tsconfig.build.json" }, { "path": "../di/tsconfig.build.json" }, { "path": "../permissions/tsconfig.build.json" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4e53af441a89..94c0ef54a8899 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -808,7 +808,7 @@ importers: version: link:../vitest-config '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))) + version: 4.1.1(vitest@4.1.1) vitest: specifier: 'catalog:' version: 4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(@vitest/browser-playwright@4.0.16)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)) @@ -979,7 +979,7 @@ importers: version: link:../vitest-config '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))) + version: 4.1.1(vitest@4.1.1) vitest: specifier: 'catalog:' version: 4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(@vitest/browser-playwright@4.0.16)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)) @@ -1273,6 +1273,9 @@ importers: packages/@n8n/decorators: dependencies: + '@n8n/api-types': + specifier: workspace:* + version: link:../api-types '@n8n/constants': specifier: workspace:* version: link:../constants @@ -2242,7 +2245,7 @@ importers: version: link:../vitest-config '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))) + version: 4.1.1(vitest@4.1.1) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -3084,7 +3087,7 @@ importers: version: 5.2.4(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))(vue@3.5.26(typescript@6.0.2)) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))) + version: 4.1.1(vitest@4.1.1) unplugin-icons: specifier: ^0.19.0 version: 0.19.0(@vue/compiler-sfc@3.5.26) @@ -3524,7 +3527,7 @@ importers: version: 4.0.16(bufferutil@4.0.9)(playwright@1.58.0)(utf-8-validate@5.0.10)(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))(vitest@4.1.1) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))) + version: 4.1.1(vitest@4.1.1) '@vue/tsconfig': specifier: catalog:frontend version: 0.7.0(typescript@6.0.2)(vue@3.5.26(typescript@6.0.2)) @@ -3885,7 +3888,7 @@ importers: version: 5.2.4(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))(vue@3.5.26(typescript@6.0.2)) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))) + version: 4.1.1(vitest@4.1.1) browserslist-to-esbuild: specifier: ^2.1.1 version: 2.1.1(browserslist@4.28.1) @@ -4365,7 +4368,7 @@ importers: version: 20.19.21 '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))) + version: 4.1.1(vitest@4.1.1) ts-morph: specifier: 'catalog:' version: 27.0.2 @@ -4485,7 +4488,7 @@ importers: version: link:../../@n8n/vitest-config '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))) + version: 4.1.1(vitest@4.1.1) tsx: specifier: 'catalog:' version: 4.19.3 @@ -25564,7 +25567,7 @@ snapshots: '@currents/commit-info': 1.0.1-beta.0 async-retry: 1.3.3 axios: 1.15.0(debug@4.4.3) - axios-retry: 4.5.0(axios@1.15.0(debug@4.4.3)) + axios-retry: 4.5.0(axios@1.15.0) c12: 1.11.2(magicast@0.3.5) chalk: 4.1.2 commander: 12.1.0 @@ -32028,7 +32031,7 @@ snapshots: tinyrainbow: 3.0.3 vitest: 4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.2)) - '@vitest/coverage-v8@4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)))': + '@vitest/coverage-v8@4.1.1(vitest@4.1.1)': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.1.1 @@ -32968,11 +32971,6 @@ snapshots: axe-core@4.7.2: {} - axios-retry@4.5.0(axios@1.15.0(debug@4.4.3)): - dependencies: - axios: 1.15.0(debug@4.4.3) - is-retry-allowed: 2.2.0 - axios-retry@4.5.0(axios@1.15.0): dependencies: axios: 1.15.0 From 29534cbeffc00f35ce2ea0a2179caec1db4d6d7f Mon Sep 17 00:00:00 2001 From: Andreas Fitzek Date: Mon, 20 Apr 2026 17:43:53 +0200 Subject: [PATCH 2/3] Fix lock file --- pnpm-lock.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94c0ef54a8899..eadc53a0d8bbc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -808,7 +808,7 @@ importers: version: link:../vitest-config '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.1(vitest@4.1.1) + version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))) vitest: specifier: 'catalog:' version: 4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(@vitest/browser-playwright@4.0.16)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)) @@ -979,7 +979,7 @@ importers: version: link:../vitest-config '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.1(vitest@4.1.1) + version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))) vitest: specifier: 'catalog:' version: 4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(@vitest/browser-playwright@4.0.16)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)) @@ -2245,7 +2245,7 @@ importers: version: link:../vitest-config '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.1(vitest@4.1.1) + version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -3087,7 +3087,7 @@ importers: version: 5.2.4(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))(vue@3.5.26(typescript@6.0.2)) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.1(vitest@4.1.1) + version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))) unplugin-icons: specifier: ^0.19.0 version: 0.19.0(@vue/compiler-sfc@3.5.26) @@ -3527,7 +3527,7 @@ importers: version: 4.0.16(bufferutil@4.0.9)(playwright@1.58.0)(utf-8-validate@5.0.10)(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))(vitest@4.1.1) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.1(vitest@4.1.1) + version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))) '@vue/tsconfig': specifier: catalog:frontend version: 0.7.0(typescript@6.0.2)(vue@3.5.26(typescript@6.0.2)) @@ -3888,7 +3888,7 @@ importers: version: 5.2.4(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))(vue@3.5.26(typescript@6.0.2)) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.1(vitest@4.1.1) + version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))) browserslist-to-esbuild: specifier: ^2.1.1 version: 2.1.1(browserslist@4.28.1) @@ -4368,7 +4368,7 @@ importers: version: 20.19.21 '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.1(vitest@4.1.1) + version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))) ts-morph: specifier: 'catalog:' version: 27.0.2 @@ -4488,7 +4488,7 @@ importers: version: link:../../@n8n/vitest-config '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.1(vitest@4.1.1) + version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))) tsx: specifier: 'catalog:' version: 4.19.3 @@ -32031,7 +32031,7 @@ snapshots: tinyrainbow: 3.0.3 vitest: 4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.2)) - '@vitest/coverage-v8@4.1.1(vitest@4.1.1)': + '@vitest/coverage-v8@4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.1.1 From 23c83c797537666c57cc879d20a4e609b1526ae2 Mon Sep 17 00:00:00 2001 From: Andreas Fitzek Date: Mon, 20 Apr 2026 21:15:56 +0200 Subject: [PATCH 3/3] Addressed review --- .../__tests__/cluster-check-metadata.test.ts | 20 ----------------- .../cluster-check/cluster-check-metadata.ts | 19 ---------------- .../context-establishment-hook-metadata.ts | 22 ------------------- 3 files changed, 61 deletions(-) diff --git a/packages/@n8n/decorators/src/cluster-check/__tests__/cluster-check-metadata.test.ts b/packages/@n8n/decorators/src/cluster-check/__tests__/cluster-check-metadata.test.ts index b8f451ae71eb7..5d971ce9096d8 100644 --- a/packages/@n8n/decorators/src/cluster-check/__tests__/cluster-check-metadata.test.ts +++ b/packages/@n8n/decorators/src/cluster-check/__tests__/cluster-check-metadata.test.ts @@ -23,26 +23,6 @@ describe('ClusterCheckMetadata', () => { expect(metadata.getClasses()).toContain(TestCheck); }); - it('should return all registered entries', () => { - class FirstCheck implements IClusterCheck { - checkDescription = { name: 'first.check' }; - async run(_context: ClusterCheckContext): Promise { - return {}; - } - } - class SecondCheck implements IClusterCheck { - checkDescription = { name: 'second.check' }; - async run(_context: ClusterCheckContext): Promise { - return {}; - } - } - - metadata.register({ class: FirstCheck }); - metadata.register({ class: SecondCheck }); - - expect(metadata.getEntries()).toHaveLength(2); - }); - it('should return registered classes in registration order', () => { class FirstCheck implements IClusterCheck { checkDescription = { name: 'first.check' }; diff --git a/packages/@n8n/decorators/src/cluster-check/cluster-check-metadata.ts b/packages/@n8n/decorators/src/cluster-check/cluster-check-metadata.ts index 4a5b590168eb6..584e039c944ea 100644 --- a/packages/@n8n/decorators/src/cluster-check/cluster-check-metadata.ts +++ b/packages/@n8n/decorators/src/cluster-check/cluster-check-metadata.ts @@ -69,25 +69,6 @@ export class ClusterCheckMetadata { this.clusterChecks.add(entry); } - /** - * Retrieves all registered entries as `[index, entry]` tuples. - * - * Primarily useful for debugging or low-level iteration. Prefer - * {@link getClasses} for most use cases. - * - * @returns Array of `[index, entry]` tuples from the internal `Set`. - * - * @example - * ```typescript - * for (const [index, entry] of metadata.getEntries()) { - * console.log(`Check #${index}:`, entry.class.name); - * } - * ``` - */ - getEntries() { - return [...this.clusterChecks.entries()]; - } - /** * Retrieves all registered check class constructors in registration order. * diff --git a/packages/@n8n/decorators/src/context-establishment/context-establishment-hook-metadata.ts b/packages/@n8n/decorators/src/context-establishment/context-establishment-hook-metadata.ts index e26dfb1e74922..c615748f8ca9f 100644 --- a/packages/@n8n/decorators/src/context-establishment/context-establishment-hook-metadata.ts +++ b/packages/@n8n/decorators/src/context-establishment/context-establishment-hook-metadata.ts @@ -59,28 +59,6 @@ export class ContextEstablishmentHookMetadata { this.contextEstablishmentHooks.add(hookEntry); } - /** - * Retrieves all registered hook entries. - * - * Returns an array of [index, entry] tuples compatible with Set.entries(). - * Primarily used for debugging or low-level iteration. - * - * **Prefer getClasses()** for most use cases as it returns just the classes. - * - * @returns Array of [index, entry] tuples from the internal Set - * - * @example - * ```typescript - * const entries = metadata.getEntries(); - * for (const [index, entry] of entries) { - * console.log(`Hook ${index}:`, entry.class.name); - * } - * ``` - */ - getEntries() { - return [...this.contextEstablishmentHooks.entries()]; - } - /** * Retrieves all registered hook classes. *