refactor(core): Decouple data table events

This commit is contained in:
Charlie Kolb 2026-10-07 12:51:24 +02:00 committed by GitHub
parent 88a2531558
commit e9b13630e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 66 additions and 29 deletions

View File

@ -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<EventMap>().toMatchTypeOf<CliEventMap>();
expectTypeOf<CliEventMap>().toMatchTypeOf<EventMap>();

View File

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

View File

@ -1136,15 +1136,6 @@ export type RelayEventMap = {
};
// #endregion
// #region Data Tables
'data-table-deleted': {
dataTableId: string;
projectId: string;
};
// #endregion
// #region Folders
'folder-deleted': {

View File

@ -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<EventNames extends keyof RelayEventMap>(
protected setupListeners<EventNames extends keyof EventMap>(
map: {
[EventName in EventNames]?: (event: RelayEventMap[EventName]) => void | Promise<void>;
[EventName in EventNames]?: (event: EventMap[EventName]) => void | Promise<void>;
},
) {
for (const [eventName, handler] of Object.entries(map) as Array<
[EventNames, (event: RelayEventMap[EventNames]) => void | Promise<void>]
[EventNames, (event: EventMap[EventNames]) => void | Promise<void>]
>) {
this.eventService.on(eventName, async (event) => {
await handler(event);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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