From e9b13630e12c525ad362885d74a1fd77d094ea78 Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Wed, 7 Oct 2026 12:51:24 +0200 Subject: [PATCH] refactor(core): Decouple data table events --- .../cli/src/events/__tests__/event-map.test.ts | 5 ++++- .../__tests__/telemetry-event-relay.test.ts | 14 ++++++++++++++ packages/cli/src/events/maps/relay.event-map.ts | 9 --------- packages/cli/src/events/relays/event-relay.ts | 10 ++++------ .../src/events/relays/telemetry.event-relay.ts | 12 ++++++++++++ .../data-table-size-validator.service.test.ts | 11 +++++++---- .../data-table-size.integration.test.ts | 4 ++-- .../modules/data-table/data-table-event-map.ts | 16 ++++++++++++++++ .../data-table-size-validator.service.ts | 11 +++++------ .../__tests__/favorites.event-relay.test.ts | 3 ++- 10 files changed, 66 insertions(+), 29 deletions(-) create mode 100644 packages/cli/src/modules/data-table/data-table-event-map.ts diff --git a/packages/cli/src/events/__tests__/event-map.test.ts b/packages/cli/src/events/__tests__/event-map.test.ts index dd504487846..65d8b13cf77 100644 --- a/packages/cli/src/events/__tests__/event-map.test.ts +++ b/packages/cli/src/events/__tests__/event-map.test.ts @@ -1,6 +1,8 @@ import { EventService, type EventMap } from '@n8n/backend-services'; import { expectTypeOf } from 'vitest'; +import type { DataTableEventMap } from '@/modules/data-table/data-table-event-map'; + import type { AiEventMap } from '../maps/ai.event-map'; import type { ExecutionDataEventMap } from '../maps/execution-data.event-map'; import type { InstanceAiEventMap } from '../maps/instance-ai.event-map'; @@ -21,7 +23,8 @@ describe('CLI event map', () => { McpPostSaveMetricsEventMap & WorkflowPublicationMetricsEventMap & PollTriggerMetricsEventMap & - SystemTaskMetricsEventMap; + SystemTaskMetricsEventMap & + DataTableEventMap; expectTypeOf().toMatchTypeOf(); expectTypeOf().toMatchTypeOf(); diff --git a/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts b/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts index cf6c3d7a718..f8ee0a8bb23 100644 --- a/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts +++ b/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts @@ -271,6 +271,20 @@ describe('TelemetryEventRelay', () => { }); }); + describe('data table events', () => { + it('should track when the storage limit is reached', () => { + eventService.emit('data-table-storage-limit-hit', { + totalBytes: 100, + maxBytes: 100, + }); + + expect(telemetry.track).toHaveBeenCalledWith('User hit data table storage limit', { + total_bytes: 100, + max_bytes: 100, + }); + }); + }); + describe('project events', () => { it('should track on `team-project-updated` event', () => { const event: RelayEventMap['team-project-updated'] = { diff --git a/packages/cli/src/events/maps/relay.event-map.ts b/packages/cli/src/events/maps/relay.event-map.ts index c5717c91632..22464c6bf6b 100644 --- a/packages/cli/src/events/maps/relay.event-map.ts +++ b/packages/cli/src/events/maps/relay.event-map.ts @@ -1136,15 +1136,6 @@ export type RelayEventMap = { }; // #endregion - // #region Data Tables - - 'data-table-deleted': { - dataTableId: string; - projectId: string; - }; - - // #endregion - // #region Folders 'folder-deleted': { diff --git a/packages/cli/src/events/relays/event-relay.ts b/packages/cli/src/events/relays/event-relay.ts index 2e28b853a37..732fde86de7 100644 --- a/packages/cli/src/events/relays/event-relay.ts +++ b/packages/cli/src/events/relays/event-relay.ts @@ -1,19 +1,17 @@ -import { EventService } from '@n8n/backend-services'; +import { EventService, type EventMap } from '@n8n/backend-services'; import { Service } from '@n8n/di'; -import type { RelayEventMap } from '@/events/maps/relay.event-map'; - @Service() export class EventRelay { constructor(readonly eventService: EventService) {} - protected setupListeners( + protected setupListeners( map: { - [EventName in EventNames]?: (event: RelayEventMap[EventName]) => void | Promise; + [EventName in EventNames]?: (event: EventMap[EventName]) => void | Promise; }, ) { for (const [eventName, handler] of Object.entries(map) as Array< - [EventNames, (event: RelayEventMap[EventNames]) => void | Promise] + [EventNames, (event: EventMap[EventNames]) => void | Promise] >) { this.eventService.on(eventName, async (event) => { await handler(event); diff --git a/packages/cli/src/events/relays/telemetry.event-relay.ts b/packages/cli/src/events/relays/telemetry.event-relay.ts index dfc43248e2f..d630a8545ec 100644 --- a/packages/cli/src/events/relays/telemetry.event-relay.ts +++ b/packages/cli/src/events/relays/telemetry.event-relay.ts @@ -60,6 +60,7 @@ import type { import { NodeTypes } from '@/node-types'; import { PostHogClient } from '@/posthog'; import { OwnershipService } from '@/services/ownership.service'; +import type { DataTableEventMap } from '@/modules/data-table/data-table-event-map'; import { EventRelay } from './event-relay'; import { Telemetry } from '../../telemetry'; @@ -216,6 +217,7 @@ export class TelemetryEventRelay extends EventRelay { await this.telemetry.init(); this.setupListeners({ + 'data-table-storage-limit-hit': (event) => this.dataTableStorageLimitHit(event), 'team-project-updated': (event) => this.teamProjectUpdated(event), 'team-project-deleted': (event) => this.teamProjectDeleted(event), 'team-project-created': (event) => this.teamProjectCreated(event), @@ -341,6 +343,16 @@ export class TelemetryEventRelay extends EventRelay { }); } + private dataTableStorageLimitHit({ + totalBytes, + maxBytes, + }: DataTableEventMap['data-table-storage-limit-hit']) { + this.telemetry.track('User hit data table storage limit', { + total_bytes: totalBytes, + max_bytes: maxBytes, + }); + } + // #region HITL private hitlResponseActioned({ diff --git a/packages/cli/src/modules/data-table/__tests__/data-table-size-validator.service.test.ts b/packages/cli/src/modules/data-table/__tests__/data-table-size-validator.service.test.ts index 4bc3b8263f8..0c8a7dabd44 100644 --- a/packages/cli/src/modules/data-table/__tests__/data-table-size-validator.service.test.ts +++ b/packages/cli/src/modules/data-table/__tests__/data-table-size-validator.service.test.ts @@ -1,9 +1,8 @@ import { mockInstance } from '@n8n/backend-test-utils'; +import { EventService } from '@n8n/backend-services'; import { GlobalConfig } from '@n8n/config'; import type { Mock } from 'vitest'; -import { Telemetry } from '@/telemetry'; - import { DataTableSizeValidator } from '../data-table-size-validator.service'; describe('DataTableSizeValidator', () => { @@ -16,9 +15,9 @@ describe('DataTableSizeValidator', () => { maxSize: 100 * 1024 * 1024, }, }); - const telemetry = mockInstance(Telemetry); + const eventService = mockInstance(EventService); beforeEach(() => { - validator = new DataTableSizeValidator(globalConfig, telemetry); + validator = new DataTableSizeValidator(globalConfig, eventService); fetchSizeFn = vi.fn(); vi.clearAllMocks(); }); @@ -46,6 +45,10 @@ describe('DataTableSizeValidator', () => { await expect( validator.validateSize(fetchSizeFn, new Date('2024-01-01T00:00:00Z')), ).rejects.toThrow('Data table size limit exceeded: 150MB used, limit is 100MB'); + expect(eventService.emit).toHaveBeenCalledWith('data-table-storage-limit-hit', { + totalBytes: 150 * 1024 * 1024, + maxBytes: 100 * 1024 * 1024, + }); }); it('should throw error when size equals limit', async () => { diff --git a/packages/cli/src/modules/data-table/__tests__/data-table-size.integration.test.ts b/packages/cli/src/modules/data-table/__tests__/data-table-size.integration.test.ts index c3778b9c108..a894ec49646 100644 --- a/packages/cli/src/modules/data-table/__tests__/data-table-size.integration.test.ts +++ b/packages/cli/src/modules/data-table/__tests__/data-table-size.integration.test.ts @@ -6,10 +6,10 @@ import { testModules, } from '@n8n/backend-test-utils'; import { GlobalConfig } from '@n8n/config'; +import { EventService } from '@n8n/backend-services'; import { GLOBAL_MEMBER_ROLE, GLOBAL_OWNER_ROLE, type Project, type User } from '@n8n/db'; import { Container } from '@n8n/di'; -import { Telemetry } from '@/telemetry'; import { createUser } from '@test-integration/db/users'; import { DataTableSizeValidator } from '../data-table-size-validator.service'; @@ -18,7 +18,7 @@ import { DataTableService } from '../data-table.service'; import { DataTableValidationError } from '../errors/data-table-validation.error'; beforeAll(async () => { - mockInstance(Telemetry); + mockInstance(EventService); await testModules.loadModules(['data-table']); await testDb.init(); }); diff --git a/packages/cli/src/modules/data-table/data-table-event-map.ts b/packages/cli/src/modules/data-table/data-table-event-map.ts new file mode 100644 index 00000000000..0b0109f044a --- /dev/null +++ b/packages/cli/src/modules/data-table/data-table-event-map.ts @@ -0,0 +1,16 @@ +import type {} from '@n8n/backend-services'; + +export type DataTableEventMap = { + 'data-table-deleted': { + dataTableId: string; + projectId: string; + }; + 'data-table-storage-limit-hit': { + totalBytes: number; + maxBytes: number; + }; +}; + +declare module '@n8n/backend-services' { + interface EventMap extends DataTableEventMap {} +} diff --git a/packages/cli/src/modules/data-table/data-table-size-validator.service.ts b/packages/cli/src/modules/data-table/data-table-size-validator.service.ts index 12eceeb7c6d..c6f592a3ca2 100644 --- a/packages/cli/src/modules/data-table/data-table-size-validator.service.ts +++ b/packages/cli/src/modules/data-table/data-table-size-validator.service.ts @@ -1,10 +1,9 @@ +import { EventService } from '@n8n/backend-services'; import { GlobalConfig } from '@n8n/config'; import { Service } from '@n8n/di'; import { toMb } from '@n8n/utils/number/bytes'; import { DataTableSizeStatus, DataTablesSizeData } from 'n8n-workflow'; -import { Telemetry } from '@/telemetry'; - import { DataTableValidationError } from './errors/data-table-validation.error'; @Service() @@ -15,7 +14,7 @@ export class DataTableSizeValidator { constructor( private readonly globalConfig: GlobalConfig, - private readonly telemetry: Telemetry, + private readonly eventService: EventService, ) {} private shouldRefresh(now: Date): boolean { @@ -59,9 +58,9 @@ export class DataTableSizeValidator { ): Promise { const size = await this.getCachedSizeData(fetchSizeFn, now); if (size.totalBytes >= this.globalConfig.dataTable.maxSize) { - this.telemetry.track('User hit data table storage limit', { - total_bytes: size.totalBytes, - max_bytes: this.globalConfig.dataTable.maxSize, + this.eventService.emit('data-table-storage-limit-hit', { + totalBytes: size.totalBytes, + maxBytes: this.globalConfig.dataTable.maxSize, }); throw new DataTableValidationError( diff --git a/packages/cli/src/modules/favorites/__tests__/favorites.event-relay.test.ts b/packages/cli/src/modules/favorites/__tests__/favorites.event-relay.test.ts index 3d2985a933a..9e2a5697b52 100644 --- a/packages/cli/src/modules/favorites/__tests__/favorites.event-relay.test.ts +++ b/packages/cli/src/modules/favorites/__tests__/favorites.event-relay.test.ts @@ -2,6 +2,7 @@ import { EventService } from '@n8n/backend-services'; import { mock } from 'vitest-mock-extended'; import type { RelayEventMap } from '@/events/maps/relay.event-map'; +import type { DataTableEventMap } from '@/modules/data-table/data-table-event-map'; import { FavoritesEventRelay } from '../favorites.event-relay'; import type { FavoritesService } from '../favorites.service'; @@ -33,7 +34,7 @@ describe('FavoritesEventRelay', () => { describe('data-table-deleted', () => { it('should delete favorites for the deleted data table', async () => { - const event: RelayEventMap['data-table-deleted'] = { + const event: DataTableEventMap['data-table-deleted'] = { dataTableId: 'dt1', projectId: 'proj1', };