refactor(core): Extract Insights backend module package (#40261)

This commit is contained in:
Charlie Kolb 2026-10-06 13:05:38 +00:00 committed by GitHub
parent fb11cc482e
commit 62fd028089
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
102 changed files with 657 additions and 556 deletions

View File

@ -989,14 +989,6 @@
"hash": "26b2999a3856"
}
],
"packages/extensions/insights/oxlint.config.mts": [
{
"rule": "lint-config-layering",
"line": 9,
"message": "packages/extensions/insights turns 'typescript/no-deprecated' down to 'off' for the whole package.",
"hash": "f936bed8fa8d"
}
],
"packages/frontend/editor-ui/eslint.config.mjs": [
{
"rule": "lint-config-layering",

View File

@ -58,6 +58,15 @@ describe('matchGlob', () => {
);
});
it('backend module pattern matches packaged module files', () => {
assert.ok(
matchGlob(
'packages/modules/insights/backend/src/insights.service.ts',
'packages/modules/*/backend/**',
),
);
});
it('* matches single-level only', () => {
assert.ok(matchGlob('README.md', '*.md'));
assert.ok(!matchGlob('docs/README.md', '*.md'));

File diff suppressed because one or more lines are too long

View File

@ -178,6 +178,7 @@ jobs:
packages/core/src/execution-engine/eval-mock-helpers.ts
.github/workflows/test-evals-discovery.yml
db:
packages/modules/*/backend/**
packages/cli/src/databases/**
packages/cli/src/modules/*/database/**
packages/cli/src/modules/**/*.entity.ts
@ -185,8 +186,7 @@ jobs:
packages/cli/test/integration/**
packages/cli/test/migration/**
packages/cli/test/shared/db/**
packages/cli/test/setup-testcontainers.js
packages/cli/test/teardown-testcontainers.js
packages/cli/test/setup-testcontainers.ts
packages/@n8n/db/**
packages/@n8n/backend-test-utils/**
packages/@n8n/scheduler/src/**
@ -195,6 +195,7 @@ jobs:
packages/cli/vitest.*
packages/quality/environments/containers/services/postgres.ts
packages/quality/environments/containers/services/redis.ts
packages/quality/environments/containers/test-db-global-setup.ts
packages/quality/environments/containers/postgres-versions.json
packages/quality/environments/containers/test-containers.ts
.github/workflows/test-db-reusable.yml
@ -209,8 +210,7 @@ jobs:
packages/@n8n/backend-test-utils/**
packages/cli/test/migration/**
packages/cli/test/shared/db/**
packages/cli/test/setup-testcontainers.js
packages/cli/test/teardown-testcontainers.js
packages/cli/test/setup-testcontainers.ts
packages/cli/vitest.config.base.ts
packages/cli/vitest.config.migration.ts
packages/cli/vitest.config.migration.testcontainers.ts

View File

@ -55,6 +55,13 @@ jobs:
working-directory: packages/cli
run: ${{ matrix.test-cmd }}
# A packaged backend module opts in by defining `test:postgres`.
- name: Run packaged backend module PostgreSQL tests
if: matrix.TEST_IMAGE_POSTGRES != ''
env:
COVERAGE_ENABLED: 'false'
run: pnpm --filter './packages/modules/*/backend' --if-present run test:postgres --maxWorkers=100%
# Regenerates the schema docs for this leg's database (sqlite temp file /
# postgres testcontainer) and fails if they differ from what's committed.
# tbls runs as a Docker image in CI.

6
OWNERS
View File

@ -109,7 +109,7 @@ packages/cli/src/environments.ee/ @n8n-io/ligo
packages/cli/src/public-api/ @n8n-io/ligo
packages/cli/src/modules/source-control.ee/ @n8n-io/ligo
packages/cli/src/modules/external-secrets.ee/ @n8n-io/ligo
packages/cli/src/modules/insights/ @n8n-io/ligo
packages/modules/insights/ @n8n-io/ligo
packages/cli/src/collaboration/ @n8n-io/catalysts
packages/cli/src/binary-data/ @n8n-io/catalysts
@ -251,10 +251,6 @@ packages/@n8n/permissions/ @n8n-io/iam
packages/@n8n/client-oauth2/ @n8n-io/iam
packages/@n8n/inbound-auth/ @n8n-io/iam
# LiGo
packages/extensions/insights/ @n8n-io/ligo
# Governance & Observability
packages/modules/type-availability-policies/ @n8n-io/governance-and-observability

View File

@ -28,11 +28,11 @@ the next `format:check` in CI fails on a module nobody touched by hand.
## Backend
**A placeholder. Nothing loads it.** The backend runtime discovers modules
under `packages/cli/src/modules/<name>`, which is where every real backend
module lives; `packages/modules/<name>/backend` reserves the path for the day
the two halves sit together and gets a README saying so. To create a backend
module that runs, use `pnpm setup-backend-module`.
Backend modules can run from a built workspace package at
`packages/modules/<name>/backend`. Register the package with a lazy import in
`packages/cli/src/modules/modules.manifest.ts`. The scaffolder does not create
this package shape yet. Use `@n8n/backend-module-insights` as the current
example.
## No build step

View File

@ -344,7 +344,6 @@ export default defineConfig({
'./src/modules/dynamic-credentials.ee/services/credential-resolver.service.ts',
'./src/modules/external-secrets.ee/secrets-providers-connections.service.ee.ts',
'./src/modules/favorites/favorites.service.ts',
'./src/modules/insights/insights-collection.service.ts',
'./src/modules/instance-ai/instance-ai.adapter.service.ts',
'./src/modules/instance-ai/mcp/instance-ai-mcp-registry.service.ts',
'./src/modules/instance-ai/storage/typeorm-agent-checkpoint-store.ts',

View File

@ -158,6 +158,7 @@
"@n8n/ai-workflow-builder": "workspace:*",
"@n8n/api-types": "workspace:*",
"@n8n/backend-common": "workspace:*",
"@n8n/backend-module-insights": "workspace:*",
"@n8n/backend-network": "workspace:*",
"@n8n/backend-services": "workspace:*",
"@n8n/blob-storage": "workspace:*",

View File

@ -11,8 +11,9 @@ import { Tracing, type InstanceSettings } from 'n8n-core';
import { mock } from 'vitest-mock-extended';
import type { License } from '@/license';
import { InsightsConfig } from '@/modules/insights/insights.config';
import type { InsightsService } from '@/modules/insights/insights.service';
import type { InsightsService } from '@n8n/backend-module-insights';
import { InsightsConfig } from '@n8n/backend-module-insights/config';
import { packagedModules } from '@/modules/modules.manifest';
import { DurableJobProvisioner } from '@/scheduling/durable-job-provisioner';
import { buildMaterializerTransaction } from '@/scheduling/durable-scheduler';
import { SystemTaskHandler } from '@/scheduling/system-tasks/system-task-handler';
@ -38,7 +39,7 @@ describe('instance reporting across mains', () => {
let schedulers: Array<Scheduler & SchedulerPasses> = [];
beforeAll(async () => {
await testModules.loadModules(['insights', 'instance-reporting']);
await testModules.loadModules(['insights', 'instance-reporting'], packagedModules);
await testDb.init();
reports = Container.get(InstanceMonitoringReportRepository);
jobs = Container.get(ScheduledJobRepository);

View File

@ -3,7 +3,7 @@ import type { Settings, SettingsRepository } from '@n8n/db';
import type { Mocked } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { InsightsConfig } from '@/modules/insights/insights.config';
import { InsightsConfig } from '@n8n/backend-module-insights/config';
import { InstanceReportingSettingsService } from '../instance-reporting-settings.service';
import { CENTRAL_INSTANCE_MONITORING_SETTINGS_KEY } from '../instance-reporting.constants';

View File

@ -15,9 +15,9 @@ import { InstanceSettings } from 'n8n-core';
import { mock } from 'vitest-mock-extended';
import type { License } from '@/license';
import { createCompactedInsightsEvent } from '@/modules/insights/database/entities/__tests__/db-utils';
import { InsightsConfig } from '@/modules/insights/insights.config';
import { InsightsService } from '@/modules/insights/insights.service';
import { createCompactedInsightsEvent } from '@n8n/backend-module-insights/testing';
import { InsightsConfig, InsightsService } from '@n8n/backend-module-insights';
import { packagedModules } from '@/modules/modules.manifest';
import type {
InstanceMonitoringReport,
@ -71,7 +71,7 @@ describe('instance reporting retries', () => {
let repository: InstanceMonitoringReportRepository;
beforeAll(async () => {
await testModules.loadModules(['insights', 'instance-reporting']);
await testModules.loadModules(['insights', 'instance-reporting'], packagedModules);
await testDb.init();
repository = Container.get(InstanceMonitoringReportRepository);

View File

@ -13,8 +13,8 @@ import type { Mocked } from 'vitest';
import { mock } from 'vitest-mock-extended';
import type { License } from '@/license';
import { InsightsConfig } from '@/modules/insights/insights.config';
import type { InsightsService } from '@/modules/insights/insights.service';
import type { InsightsService } from '@n8n/backend-module-insights';
import { InsightsConfig } from '@n8n/backend-module-insights/config';
import type { InstanceMonitoringReport } from '../database/entities/instance-monitoring-report';
import type { InstanceMonitoringReportRepository } from '../database/repositories/instance-monitoring-report.repository';

View File

@ -4,7 +4,7 @@ import { Service } from '@n8n/di';
import { randomInt } from 'node:crypto';
import { jsonParse } from 'n8n-workflow';
import { InsightsConfig } from '@/modules/insights/insights.config';
import { InsightsConfig } from '@n8n/backend-module-insights/config';
import { CENTRAL_INSTANCE_MONITORING_SETTINGS_KEY } from './instance-reporting.constants';
/** `HH:mm`, 24-hour. ISO 8601's local-time form; this instance reads it as UTC. */

View File

@ -11,8 +11,7 @@ import { OperationalError } from 'n8n-workflow';
import { N8N_VERSION } from '@/constants';
import { License } from '@/license';
import { InsightsConfig } from '@/modules/insights/insights.config';
import { InsightsService } from '@/modules/insights/insights.service';
import { InsightsConfig, InsightsService } from '@n8n/backend-module-insights';
import type {
InstanceMonitoringReport,

View File

@ -4,4 +4,6 @@ import type { PackagedModules } from '@n8n/backend-common';
* Backend modules that load from workspace packages instead of this directory.
* Keep each import in a thunk so an ineligible module is not imported.
*/
export const packagedModules: PackagedModules = {};
export const packagedModules: PackagedModules = {
insights: async () => await import('@n8n/backend-module-insights/module'),
};

View File

@ -15,7 +15,7 @@ import { DateTime } from 'luxon';
import { UserError } from 'n8n-workflow';
import { BadRequestError, ForbiddenError } from '@n8n/errors';
import { InsightsService } from '@/modules/insights/insights.service';
import { InsightsService } from '@n8n/backend-module-insights';
@PublicApiController('/insights')
export class InsightsPublicController {

View File

@ -27,6 +27,16 @@ function findEntityFiles(dir: string): string[] {
.filter((file) => readFileSync(file, 'utf8').includes('@Entity('));
}
function findPackagedModuleEntityFiles(): string[] {
const modulesDir = path.join(REPO_ROOT, 'packages/modules');
return readdirSync(modulesDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.flatMap((entry) => {
const backendSourceDir = path.join(modulesDir, entry.name, 'backend/src');
return existsSync(backendSourceDir) ? findEntityFiles(backendSourceDir) : [];
});
}
/**
* Guard: every entity that belongs to a Project must be explicitly
* accounted for in the ownership-transfer manifest, so that adding a new
@ -43,8 +53,11 @@ describe('ownership-transfer manifest', () => {
beforeAll(async () => {
// Register module entity decorators via the filesystem so that a newly
// added module entity is picked up without editing this test.
const moduleEntityFiles = findEntityFiles(path.resolve(__dirname, '../../../modules'));
// added CLI or packaged module entity is picked up without editing this test.
const moduleEntityFiles = [
...findEntityFiles(path.resolve(__dirname, '../../../modules')),
...findPackagedModuleEntityFiles(),
];
moduleEntityFileCount = moduleEntityFiles.length;
await Promise.all(moduleEntityFiles.map(async (file) => await import(file)));

View File

@ -76,7 +76,7 @@
},
{
"name": "InsightsMetadata",
"path": "packages/cli/src/modules/insights/database/entities/insights-metadata.ts",
"path": "packages/modules/insights/backend/src/insights-metadata.entity.ts",
"reason": "Denormalized analytics snapshot; historical insights keep the original project reference by design."
},
{

View File

@ -14,19 +14,21 @@ import { DataSource } from '@n8n/typeorm';
import { sleep } from '@n8n/utils/sleep';
import { DateTime } from 'luxon';
import { InsightsConfig } from '../../../insights.config';
import { createMember } from '@test-integration/db/users';
import {
type InsightsAccessFilter,
InsightsByPeriod,
InsightsByPeriodRepository,
InsightsConfig,
InsightsRawRepository,
TypeToNumber,
} from '@n8n/backend-module-insights';
import {
createCompactedInsightsEvent,
createMetadata,
createRawInsightsEvent,
} from '../../entities/__tests__/db-utils';
import { InsightsByPeriod } from '../../entities/insights-by-period';
import { TypeToNumber } from '../../entities/insights-shared';
import type { InsightsAccessFilter } from '../insights-by-period.repository';
import { InsightsByPeriodRepository } from '../insights-by-period.repository';
import { InsightsRawRepository } from '../insights-raw.repository';
} from '@n8n/backend-module-insights/testing';
import { createMember } from '@test-integration/db/users';
import { packagedModules } from '@/modules/modules.manifest';
const isPostgres = Container.get(GlobalConfig).database.type === 'postgresdb';
@ -62,7 +64,7 @@ function pauseFirstTransactionAfterRead(manager: EntityManager) {
describe('InsightsByPeriodRepository', () => {
beforeAll(async () => {
await testModules.loadModules(['insights']);
await testModules.loadModules(['insights'], packagedModules);
await testDb.init();
});

View File

@ -15,9 +15,12 @@ import { Container } from '@n8n/di';
import { InstanceSettings } from 'n8n-core';
import { createRunExecutionData } from 'n8n-workflow';
import { InsightsByPeriodRepository } from '@/modules/insights/database/repositories/insights-by-period.repository';
import { InsightsCollectionService } from '@/modules/insights/insights-collection.service';
import { InsightsCompactionService } from '@/modules/insights/insights-compaction.service';
import {
InsightsByPeriodRepository,
InsightsCollectionService,
InsightsCompactionService,
} from '@n8n/backend-module-insights';
import { packagedModules } from '@/modules/modules.manifest';
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
import { WorkflowRunner } from '@/workflow-runner';
@ -32,7 +35,7 @@ describe('Insights vs Workflow Statistics Integration', () => {
process.env.N8N_INSIGHTS_FLUSH_INTERVAL_SECONDS = '1'; // Flush every 1 second
process.env.N8N_INSIGHTS_COMPACTION_BATCH_SIZE = '100'; // Process up to 100 items per batch
await testModules.loadModules(['insights']);
await testModules.loadModules(['insights'], packagedModules);
await testDb.init();
// Load required node types from dist folder

View File

@ -10,8 +10,8 @@ import type { Project } from '@n8n/db';
import { GLOBAL_ADMIN_ROLE, GLOBAL_MEMBER_ROLE, GLOBAL_OWNER_ROLE } from '@n8n/db';
import { DateTime } from 'luxon';
import { createCompactedInsightsEvent } from '@/modules/insights/database/entities/__tests__/db-utils';
import type { InsightsByPeriod } from '@/modules/insights/database/entities/insights-by-period';
import { createCompactedInsightsEvent } from '@n8n/backend-module-insights/testing';
import type { InsightsByPeriod } from '@n8n/backend-module-insights';
import { Telemetry } from '@/telemetry';
import { createCustomRoleWithScopeSlugs } from '../shared/db/roles';

View File

@ -20,17 +20,19 @@ import { mock } from 'vitest-mock-extended';
import { ForbiddenError } from '@n8n/errors';
import type { ProjectScopeService, WorkflowSharingService } from '@n8n/backend-services';
import {
type InsightsByPeriod,
type InsightsByPeriodRepository,
InsightsCollectionService,
InsightsService,
} from '@n8n/backend-module-insights';
import { createCompactedInsightsEvent } from '@n8n/backend-module-insights/testing';
import { createMember } from '@test-integration/db/users';
import { createCompactedInsightsEvent } from '../database/entities/__tests__/db-utils';
import type { InsightsByPeriod } from '../database/entities/insights-by-period';
import type { InsightsByPeriodRepository } from '../database/repositories/insights-by-period.repository';
import { InsightsCollectionService } from '../insights-collection.service';
import { InsightsService } from '../insights.service';
import { packagedModules } from '@/modules/modules.manifest';
describe('InsightsService (Integration)', () => {
beforeAll(async () => {
await testModules.loadModules(['insights']);
await testModules.loadModules(['insights'], packagedModules);
await testDb.init();
});

View File

@ -9,7 +9,7 @@ import { type Project, type User } from '@n8n/db';
import { DateTime } from 'luxon';
import { AUTH_COOKIE_NAME } from '@/constants';
import { createCompactedInsightsEvent } from '@/modules/insights/database/entities/__tests__/db-utils';
import { createCompactedInsightsEvent } from '@n8n/backend-module-insights/testing';
import { createCustomRoleWithScopeSlugs } from '../shared/db/roles';
import { addApiKey, createOwnerWithApiKey, createUser } from '../shared/db/users';

View File

@ -17,6 +17,7 @@ import { License } from '@/license';
import { rawBodyReader, bodyParser } from '@/middlewares';
import { PostHogClient } from '@/posthog';
import { Push } from '@/push';
import { packagedModules } from '@/modules/modules.manifest';
import { ApiKeyAuthStrategy } from '@/services/api-key-auth.strategy';
import { AuthStrategyRegistry } from '@/services/auth-strategy.registry';
import { SessionCookieAuthStrategy } from '@/services/session-cookie-auth.strategy';
@ -148,7 +149,7 @@ export const setupTestServer = ({
// eslint-disable-next-line complexity
beforeAll(async () => {
if (modules) await testModules.loadModules(modules);
if (modules) await testModules.loadModules(modules, packagedModules);
await testDb.init();
Container.get(GlobalConfig).userManagement.jwtSecret = 'My JWT secret';
@ -396,7 +397,7 @@ export const setupTestServer = ({
break;
case 'insights':
await import('@/modules/insights/insights.module.js');
await import('@n8n/backend-module-insights/module');
break;
case 'data-table':

View File

@ -1,74 +1,9 @@
/**
* Vitest global setup for integration/migration tests using testcontainers.
* Starts postgres and redis containers via n8n-containers and exposes their
* connection details through `process.env`, which the forked test workers inherit.
*
* Note: Ryuk handles container cleanup on process exit (crashes/timeouts);
* `teardown` is the secondary cleanup path.
*/
import { createServiceStack } from 'n8n-containers';
import { randomBytes } from 'node:crypto';
import { createTestDatabaseGlobalSetup } from 'n8n-containers/test-db-global-setup';
let stack: Awaited<ReturnType<typeof createServiceStack>> | undefined;
export async function setup() {
const suffix = randomBytes(4).toString('hex');
stack = await createServiceStack({
services: ['postgres', 'redis'],
projectName: `n8n-integration-test-${suffix}`,
});
const pgResult = stack.serviceResults.postgres;
if (!pgResult) {
throw new Error('Failed to start postgres container');
}
const { container } = pgResult;
const meta = pgResult.meta as { database: string; username: string; password: string };
process.env.DB_TYPE = 'postgresdb';
process.env.DB_POSTGRESDB_HOST = container.getHost();
process.env.DB_POSTGRESDB_PORT = String(container.getMappedPort(5432));
process.env.DB_POSTGRESDB_DATABASE = meta.database;
process.env.DB_POSTGRESDB_USER = meta.username;
process.env.DB_POSTGRESDB_PASSWORD = meta.password;
process.env.DB_POSTGRESDB_SCHEMA = 'alt_schema';
process.env.DB_TABLE_PREFIX = 'test_';
process.env.DB_POSTGRESDB_POOL_SIZE = '1'; // Detect connection pooling deadlocks
console.log(
`\n✓ Postgres ready at ${process.env.DB_POSTGRESDB_HOST}:${process.env.DB_POSTGRESDB_PORT}\n`,
);
const redisResult = stack.serviceResults.redis;
if (!redisResult) {
throw new Error('Failed to start redis container');
}
process.env.N8N_TEST_REDIS_HOST = redisResult.container.getHost();
process.env.N8N_TEST_REDIS_PORT = String(redisResult.container.getMappedPort(6379));
console.log(
`✓ Redis ready at ${process.env.N8N_TEST_REDIS_HOST}:${process.env.N8N_TEST_REDIS_PORT}\n`,
);
// Build a template DB once, then each test file's testDb.init() clones it via
// CREATE DATABASE ... TEMPLATE instead of replaying the full migration history.
// Set N8N_TEST_DISABLE_TEMPLATE_DB=1 to opt out (e.g. when bisecting migration bugs).
if (process.env.N8N_TEST_DISABLE_TEMPLATE_DB !== '1') {
const templateName = `n8n_test_template_${suffix}`;
const tplStart = Date.now();
// Dynamic import so the module's DI lookups resolve GlobalConfig after the DB_* env vars are set.
export const { setup, teardown } = createTestDatabaseGlobalSetup({
initializeTemplateDb: async (templateName) => {
// Resolve config after the global setup has populated the DB environment.
const { testDb } = await import('@n8n/backend-test-utils');
await testDb.initTemplateDb(templateName);
process.env.N8N_TEST_TEMPLATE_DB = templateName;
console.log(
`✓ Template DB ${templateName} ready (${Date.now() - tplStart}ms) — workers will clone instead of migrate\n`,
);
}
}
export async function teardown() {
if (stack) {
await stack.stop();
console.log('\n✓ Testcontainers stack stopped\n');
}
}
},
});

View File

@ -40,6 +40,7 @@
{ "path": "../@n8n/inbound-auth/tsconfig.build.json" },
{ "path": "../@n8n/backend-common/tsconfig.build.json" },
{ "path": "../@n8n/backend-services/tsconfig.build.json" },
{ "path": "../modules/insights/backend/tsconfig.build.json" },
{ "path": "../@n8n/backend-test-utils/tsconfig.build.json" },
{ "path": "../@n8n/blob-storage/tsconfig.build.json" },
{ "path": "../@n8n/di/tsconfig.build.json" },

View File

@ -14,6 +14,22 @@ import { workspaceDistExternals } from './vitest.workspace-externals';
const alias = {
'@test-integration': path.resolve(__dirname, 'test/integration/shared'),
'@test': path.resolve(__dirname, 'test/shared'),
'@n8n/backend-module-insights/testing': path.resolve(
__dirname,
'../modules/insights/backend/src/testing.ts',
),
'@n8n/backend-module-insights/module': path.resolve(
__dirname,
'../modules/insights/backend/src/insights.module.ts',
),
'@n8n/backend-module-insights/config': path.resolve(
__dirname,
'../modules/insights/backend/src/insights.config.ts',
),
'@n8n/backend-module-insights': path.resolve(
__dirname,
'../modules/insights/backend/src/index.ts',
),
'@n8n/mcp-apps/server': path.resolve(__dirname, '../@n8n/mcp-apps/src/server/index.ts'),
'@n8n/telemetry': path.resolve(__dirname, '../@n8n/telemetry/src'),
'@': path.resolve(__dirname, 'src'),

View File

View File

@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -1 +0,0 @@
See LICENSE.md in the root of this repository for more information.

View File

@ -1 +0,0 @@
# @n8n/n8n-extension-insights

View File

@ -1,23 +0,0 @@
{
"name": "@n8n/n8n-extension-insights",
"version": "1.0.0",
"author": "n8n",
"category": "Internal",
"minSDKVersion": "0.0.0",
"permissions": {
"frontend": [],
"backend": []
},
"events": ["workflow:started"],
"setup": {
"backend": "./dist/backend/index.js",
"frontend": "./dist/frontend/index.js"
},
"extends": {
"views": {
"workflows": {
"header": "InsightsDashboard"
}
}
}
}

View File

@ -1,11 +0,0 @@
import { baseConfig } from '@n8n/oxlint-config/base';
import { defineConfig } from 'oxlint';
export default defineConfig({
extends: [baseConfig],
options: { typeAware: true },
ignorePatterns: ['src/shims.d.ts', '**/*.vue'],
rules: {
'typescript/no-deprecated': 'off',
},
});

View File

@ -1,71 +0,0 @@
{
"name": "@n8n/n8n-extension-insights",
"version": "0.23.0",
"type": "module",
"files": [
"dist",
"n8n.manifest.json",
"LICENSE",
"README.md"
],
"main": "./n8n.manifest.json",
"module": "./n8n.manifest.json",
"exports": {
".": {
"import": "./n8n.manifest.json",
"require": "./n8n.manifest.json"
},
"./backend": {
"types": "./dist/backend/index.d.mts",
"import": "./dist/backend/index.mjs",
"require": "./dist/backend/index.cjs"
},
"./frontend": {
"types": "./dist/frontend/index.d.mts",
"import": "./dist/frontend/index.mjs",
"require": "./dist/frontend/index.umd.cjs"
},
"./*": "./*"
},
"scripts": {
"clean": "rimraf dist .turbo",
"cleanup": "rimraf dist",
"dev": "tsdown --watch",
"lint": "oxlint src --quiet",
"lint:fix": "oxlint src --fix",
"lint:styles": "stylelint \"src/**/*.{scss,sass,vue}\" --cache",
"lint:styles:fix": "stylelint \"src/**/*.{scss,sass,vue}\" --fix --cache",
"typecheck": "vue-tsc --noEmit",
"build:backend": "tsdown",
"build:frontend": "vite build",
"build": "pnpm cleanup && pnpm run \"/^build:(backend|frontend)$/\"",
"build:unchecked": "pnpm cleanup && pnpm run build:backend && pnpm run build:frontend",
"preview": "vite preview"
},
"dependencies": {
"@n8n/extension-sdk": "workspace:*",
"vue": "catalog:frontend",
"vue-router": "catalog:frontend"
},
"devDependencies": {
"@n8n/oxlint-config": "workspace:*",
"@n8n/stylelint-config": "workspace:*",
"@n8n/typescript-config": "workspace:*",
"@vitejs/plugin-vue": "catalog:frontend",
"@vue/tsconfig": "catalog:frontend",
"oxlint": "catalog:",
"oxlint-tsgolint": "catalog:",
"stylelint": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:",
"rimraf": "catalog:",
"vite": "catalog:",
"vitest": "catalog:",
"unplugin-dts": "catalog:",
"vue-tsc": "catalog:frontend"
},
"peerDependencies": {
"@vue/language-core": "catalog:frontend"
},
"license": "LicenseRef-n8n-sustainable-use"
}

View File

@ -1,7 +0,0 @@
import { defineBackendExtension } from '@n8n/extension-sdk/backend';
export default defineBackendExtension({
setup(n8n) {
console.log(n8n);
},
});

View File

@ -1,6 +0,0 @@
<script lang="ts" setup></script>
<template>
<div>
<h1>INSIGHTS DASHBOARD</h1>
</div>
</template>

View File

@ -1,9 +0,0 @@
import { markRaw } from 'vue';
import { defineFrontendExtension } from '@n8n/extension-sdk/frontend';
import InsightsDashboard from './InsightsDashboard.vue';
export default defineFrontendExtension({
setup(n8n) {
n8n.registerComponent('InsightsDashboard', markRaw(InsightsDashboard));
},
});

View File

@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@ -1,3 +0,0 @@
import { baseConfig } from '@n8n/stylelint-config/base';
export default baseConfig;

View File

@ -1,8 +0,0 @@
{
"extends": "@n8n/typescript-config/tsconfig.common.json",
"compilerOptions": {
"moduleResolution": "bundler",
"module": "esnext"
},
"include": ["src/backend/**/*.ts", "src/index.ts"]
}

View File

@ -1,7 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo"
},
"include": ["src/frontend/**/*.ts", "src/frontend/**/*.vue"]
}

View File

@ -1,8 +0,0 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.backend.json" },
{ "path": "./tsconfig.frontend.json" },
{ "path": "./tsconfig.vite.json" }
]
}

View File

@ -1,19 +0,0 @@
{
"compilerOptions": {
"composite": true,
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"tsBuildInfoFile": "dist/tsconfig.vite.tsbuildinfo"
},
"include": ["vite.config.ts", "tsdown.config.ts"]
}

View File

@ -1,16 +0,0 @@
import { defineConfig } from 'tsdown';
export default defineConfig({
entry: [
'src/backend/**/*.ts',
'!src/backend/**/*.test.ts',
'!src/backend/**/*.d.ts',
'!src/backend/__tests__**/*',
],
outDir: 'dist/backend',
format: ['cjs', 'esm'],
dts: true,
sourcemap: true,
tsconfig: 'tsconfig.backend.json',
hash: false,
});

View File

@ -1,42 +0,0 @@
import { resolve } from 'path';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import dts from 'unplugin-dts/vite';
import { configDefaults as vitestConfig } from 'vitest/config';
const cwd = process.cwd();
export default defineConfig({
plugins: [
vue(),
dts({
tsconfigPath: resolve(cwd, 'tsconfig.frontend.json'),
processor: 'vue',
}),
],
build: {
emptyOutDir: false,
outDir: resolve(cwd, 'dist', 'frontend'),
lib: {
entry: resolve(cwd, 'src', 'frontend', 'index.ts'),
name: 'n8nFrontEndSdk',
fileName: 'index',
},
rollupOptions: {
external: ['vue'],
output: {
preserveModules: false,
globals: {
vue: 'Vue',
},
},
},
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['src/__tests__/setup.ts'],
include: ['src/**/*.spec.ts'],
exclude: vitestConfig.exclude,
},
});

View File

@ -4,10 +4,10 @@ Feature modules carved out of `packages/frontend/editor-ui`. Each one is a works
`<name>/frontend`: source-only (`main: "src/index.ts"`, no `dist`), consumed by the editor-ui shell
through Vite aliases.
`<name>/backend` is a reserved path rather than a workspace package — the backend runtime discovers
modules under `packages/cli/src/modules/<name>`. The extra nesting level is what lets both halves of
a module sit together later. Add packaged backend modules to
`packages/cli/src/modules/modules.manifest.ts`.
Backend modules can be built workspace packages at `<name>/backend`. Add each packaged backend
module to `packages/cli/src/modules/modules.manifest.ts`. The manifest uses lazy imports so a
disabled module does not load its package. Backend modules that have not moved to a package still
load from `packages/cli/src/modules/<name>`.
`packages/modules` itself stays tracked even when it holds nothing: turbo rejects a `--filter`
whose directory does not exist.

View File

@ -0,0 +1,36 @@
import { backendConfig } from '@n8n/oxlint-config/backend';
import { defineConfig } from 'oxlint';
export default defineConfig({
extends: [backendConfig],
options: { typeAware: true },
ignorePatterns: ['coverage/**', 'dist/**'],
rules: {
'n8n-local-rules/misplaced-n8n-typeorm-import': 'error',
'n8n-local-rules/no-guardrail-disable': [
'error',
{
guarded: [
{
rule: 'misplaced-n8n-typeorm-import',
message:
'Keep TypeORM in the persistence layer. Put the query behind a use-case repository method.',
},
],
},
],
},
overrides: [
{
files: [
'./src/**/*.entity.ts',
'./src/**/*.repository.ts',
'./src/**/__tests__/**/*.ts',
'./src/**/*.test.ts',
],
rules: {
'n8n-local-rules/misplaced-n8n-typeorm-import': 'off',
},
},
],
});

View File

@ -0,0 +1,97 @@
{
"name": "@n8n/backend-module-insights",
"version": "0.1.0",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.js"
},
"./module": {
"types": "./dist/insights.module.d.ts",
"import": "./dist/insights.module.js",
"require": "./dist/insights.module.js"
},
"./config": {
"types": "./dist/insights.config.d.ts",
"import": "./dist/insights.config.js",
"require": "./dist/insights.config.js"
},
"./testing": {
"types": "./dist/testing.d.ts",
"import": "./dist/testing.js",
"require": "./dist/testing.js"
}
},
"typesVersions": {
"*": {
"config": [
"dist/insights.config.d.ts"
],
"module": [
"dist/insights.module.d.ts"
],
"testing": [
"dist/testing.d.ts"
]
}
},
"files": [
"dist/**/*"
],
"scripts": {
"clean": "rimraf dist .turbo",
"dev": "pnpm watch",
"typecheck": "tsc --noEmit",
"build": "tsc -p tsconfig.build.json",
"build:unchecked": "tsc -p tsconfig.build.json --noCheck",
"format": "biome format --write .",
"format:check": "biome ci .",
"lint": "oxlint --type-aware . --quiet",
"lint:fix": "oxlint --type-aware . --fix",
"watch": "tsc -p tsconfig.build.json --watch",
"test": "vitest run",
"test:postgres": "N8N_LOG_LEVEL=silent vitest run --config vitest.config.postgres.ts",
"test:unit": "vitest run",
"test:dev": "vitest --silent=false"
},
"dependencies": {
"@n8n/api-types": "workspace:*",
"@n8n/backend-common": "workspace:*",
"@n8n/backend-services": "workspace:*",
"@n8n/config": "workspace:*",
"@n8n/constants": "workspace:*",
"@n8n/db": "workspace:*",
"@n8n/decorators": "workspace:*",
"@n8n/di": "workspace:*",
"@n8n/errors": "workspace:*",
"@n8n/typeorm": "workspace:*",
"@n8n/utils": "workspace:*",
"luxon": "catalog:",
"n8n-core": "workspace:*",
"n8n-workflow": "workspace:*"
},
"peerDependencies": {
"zod": "catalog:"
},
"devDependencies": {
"@n8n/backend-test-utils": "workspace:*",
"@n8n/oxlint-config": "workspace:*",
"@n8n/typescript-config": "workspace:*",
"@n8n/vitest-config": "workspace:*",
"@types/luxon": "catalog:",
"@vitest/coverage-v8": "catalog:",
"n8n-containers": "workspace:*",
"oxlint": "catalog:",
"oxlint-tsgolint": "catalog:",
"typescript": "catalog:typescript",
"vite": "catalog:",
"vitest": "catalog:",
"vitest-mock-extended": "catalog:",
"zod": "catalog:"
},
"license": "LicenseRef-n8n-sustainable-use"
}

View File

@ -2,17 +2,16 @@ import { createTeamProject, createWorkflow, testDb, testModules } from '@n8n/bac
import { Container } from '@n8n/di';
import { DateTime } from 'luxon';
import { InsightsRawRepository } from '../database/repositories/insights-raw.repository';
import { InsightsRawRepository } from '../insights-raw.repository';
import {
createRawInsightsEvent,
createRawInsightsEvents,
} from '../database/entities/__tests__/db-utils';
import { InsightsByPeriodRepository } from '../database/repositories/insights-by-period.repository';
import { createRawInsightsEvent, createRawInsightsEvents } from '../testing/db-utils';
import { InsightsByPeriodRepository } from '../insights-by-period.repository';
import { InsightsCompactionService } from '../insights-compaction.service';
beforeAll(async () => {
await testModules.loadModules(['insights']);
await testModules.loadModules(['insights'], {
insights: async () => await import('../insights.module.js'),
});
await testDb.init();
});

View File

@ -16,7 +16,9 @@ describe('ChangeValueTypesForInsights - insights_by_period table', () => {
let dataSource: DataSource;
beforeAll(async () => {
await testModules.loadModules(['insights']);
await testModules.loadModules(['insights'], {
insights: async () => await import('../insights.module.js'),
});
// Initialize DB connection without running migrations
const dbConnection = Container.get(DbConnection);

View File

@ -1,6 +1,6 @@
import { testDb } from '@n8n/backend-test-utils';
import { InsightsByPeriod } from '../insights-by-period';
import { InsightsByPeriod } from '../insights-by-period.entity';
import type { PeriodUnit, TypeUnit } from '../insights-shared';
beforeAll(async () => {

View File

@ -14,14 +14,16 @@ import { type ExecutionStatus, type IRun, type WorkflowExecuteMode } from 'n8n-w
import assert from 'node:assert';
import { mock } from 'vitest-mock-extended';
import type { TypeUnit } from '../database/entities/insights-shared';
import { InsightsMetadataRepository } from '../database/repositories/insights-metadata.repository';
import { InsightsRawRepository } from '../database/repositories/insights-raw.repository';
import type { TypeUnit } from '../insights-shared';
import { InsightsMetadataRepository } from '../insights-metadata.repository';
import { InsightsRawRepository } from '../insights-raw.repository';
import { InsightsCollectionService } from '../insights-collection.service';
import { InsightsConfig } from '../insights.config';
beforeAll(async () => {
await testModules.loadModules(['insights']);
await testModules.loadModules(['insights'], {
insights: async () => await import('../insights.module.js'),
});
await testDb.init();
});

View File

@ -5,9 +5,9 @@ import { DateTime } from 'luxon';
import type { IRun } from 'n8n-workflow';
import { mock } from 'vitest-mock-extended';
import type { InsightsMetadata } from '../database/entities/insights-metadata';
import type { InsightsMetadataRepository } from '../database/repositories/insights-metadata.repository';
import type { InsightsRawRepository } from '../database/repositories/insights-raw.repository';
import type { InsightsMetadata } from '../insights-metadata.entity';
import type { InsightsMetadataRepository } from '../insights-metadata.repository';
import type { InsightsRawRepository } from '../insights-raw.repository';
import { InsightsCollectionService } from '../insights-collection.service';
import type { InsightsConfig } from '../insights.config';

View File

@ -5,16 +5,16 @@ import { Container } from '@n8n/di';
import { DateTime } from 'luxon';
import { mock } from 'vitest-mock-extended';
import { InsightsRawRepository } from '../database/repositories/insights-raw.repository';
import { InsightsRawRepository } from '../insights-raw.repository';
import {
createMetadata,
createRawInsightsEvent,
createCompactedInsightsEvent,
createRawInsightsEvents,
} from '../database/entities/__tests__/db-utils';
import type { PeriodUnit } from '../database/entities/insights-shared';
import { InsightsByPeriodRepository } from '../database/repositories/insights-by-period.repository';
} from '../testing/db-utils';
import type { PeriodUnit } from '../insights-shared';
import { InsightsByPeriodRepository } from '../insights-by-period.repository';
import { InsightsCompactionService } from '../insights-compaction.service';
import { InsightsConfig } from '../insights.config';
@ -29,7 +29,9 @@ type CompactionConfig = Pick<
let defaultCompactionConfig: CompactionConfig;
beforeAll(async () => {
await testModules.loadModules(['insights']);
await testModules.loadModules(['insights'], {
insights: async () => await import('../insights.module.js'),
});
await testDb.init();
const config = Container.get(InsightsConfig);
@ -714,7 +716,7 @@ describe('compaction', () => {
const hourInsights = (await insightsByPeriodRepository.find()).filter(
(insight) => insight.periodUnit !== 'day',
);
expect(hourInsights).toBeEmptyArray();
expect(hourInsights).toEqual([]);
const allCompacted = await insightsByPeriodRepository.find({ order: { periodStart: 1 } });
expect(allCompacted).toHaveLength(batches.length);
for (const [index, compacted] of allCompacted.entries()) {
@ -801,7 +803,7 @@ describe('compaction', () => {
const hourAndDayInsights = (await insightsByPeriodRepository.find()).filter(
(insight) => insight.periodUnit !== 'week',
);
expect(hourAndDayInsights).toBeEmptyArray();
expect(hourAndDayInsights).toEqual([]);
const allCompacted = await insightsByPeriodRepository.find({ order: { periodStart: 1 } });
expect(allCompacted).toHaveLength(batches.length);
for (const [index, compacted] of allCompacted.entries()) {

View File

@ -9,16 +9,15 @@ import { Container } from '@n8n/di';
import { DateTime } from 'luxon';
import { mock } from 'vitest-mock-extended';
import {
createCompactedInsightsEvent,
createMetadata,
} from '../database/entities/__tests__/db-utils';
import { InsightsByPeriodRepository } from '../database/repositories/insights-by-period.repository';
import { createCompactedInsightsEvent, createMetadata } from '../testing/db-utils';
import { InsightsByPeriodRepository } from '../insights-by-period.repository';
import { InsightsPruningService } from '../insights-pruning.service';
import { InsightsConfig } from '../insights.config';
beforeAll(async () => {
await testModules.loadModules(['insights']);
await testModules.loadModules(['insights'], {
insights: async () => await import('../insights.module.js'),
});
await testDb.init();
});

View File

@ -1,7 +1,7 @@
import { mockLogger } from '@n8n/backend-test-utils';
import { mock } from 'vitest-mock-extended';
import type { InsightsByPeriodRepository } from '../database/repositories/insights-by-period.repository';
import type { InsightsByPeriodRepository } from '../insights-by-period.repository';
import { InsightsPruningService } from '../insights-pruning.service';
import { InsightsConfig } from '../insights.config';
import { INSIGHTS_MAX_AGE_DAYS_CAP, INSIGHTS_MAX_AGE_DAYS_DEFAULT } from '../insights.constants';

View File

@ -16,7 +16,9 @@ describe('ChangeValueTypesForInsights - insights_raw table', () => {
let dataSource: DataSource;
beforeAll(async () => {
await testModules.loadModules(['insights']);
await testModules.loadModules(['insights'], {
insights: async () => await import('../insights.module.js'),
});
// Initialize DB connection without running migrations
const dbConnection = Container.get(DbConnection);

View File

@ -2,15 +2,17 @@ import { createTeamProject, createWorkflow, testDb, testModules } from '@n8n/bac
import { Container } from '@n8n/di';
import { DateTime } from 'luxon';
import { createMetadata, createRawInsightsEvent } from './db-utils';
import { InsightsRawRepository } from '../../repositories/insights-raw.repository';
import { InsightsRaw } from '../insights-raw';
import { createMetadata, createRawInsightsEvent } from '../testing/db-utils';
import { InsightsRawRepository } from '../insights-raw.repository';
import { InsightsRaw } from '../insights-raw.entity';
import type { TypeUnit } from '../insights-shared';
let insightsRawRepository: InsightsRawRepository;
beforeAll(async () => {
await testModules.loadModules(['insights']);
await testModules.loadModules(['insights'], {
insights: async () => await import('../insights.module.js'),
});
await testDb.init();
insightsRawRepository = Container.get(InsightsRawRepository);
});

View File

@ -8,8 +8,8 @@ import { mock } from 'vitest-mock-extended';
import { BadRequestError, ForbiddenError } from '@n8n/errors';
import { WorkflowSharingService } from '@n8n/backend-services';
import { TypeToNumber, type TypeUnitNumber } from '../database/entities/insights-shared';
import { InsightsByPeriodRepository } from '../database/repositories/insights-by-period.repository';
import { TypeToNumber, type TypeUnitNumber } from '../insights-shared';
import { InsightsByPeriodRepository } from '../insights-by-period.repository';
import { InsightsController } from '../insights.controller';
function expectDatesClose(actual: Date, expected: Date, maxDriftMs?: number) {

View File

@ -6,7 +6,7 @@ import { InstanceSettings } from 'n8n-core';
import type { MockProxy } from 'vitest-mock-extended';
import { mock } from 'vitest-mock-extended';
import { InsightsByPeriodRepository } from '../database/repositories/insights-by-period.repository';
import { InsightsByPeriodRepository } from '../insights-by-period.repository';
import { InsightsCollectionService } from '../insights-collection.service';
import { InsightsCompactionTask } from '../insights-compaction.task';
import { InsightsPruningTask } from '../insights-pruning.task';
@ -67,8 +67,7 @@ describe('InsightsModule', () => {
describe('Dynamic conditional import of InsightsCollectionService', () => {
it('should not import InsightsCollectionService for worker instances', async () => {
// ARRANGE
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(mockInstanceSettings as any).instanceType = 'worker';
Object.assign(mockInstanceSettings, { instanceType: 'worker' });
// ACT - Call init AND settings which should NOT load InsightsCollectionService for workers
await insightsModule.init();
@ -82,8 +81,7 @@ describe('InsightsModule', () => {
'should import InsightsCollectionService for non-worker instances',
async (instanceType) => {
// ARRANGE
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(mockInstanceSettings as any).instanceType = instanceType;
Object.assign(mockInstanceSettings, { instanceType });
// ACT - Call init which should load InsightsCollectionService for non-workers
await insightsModule.init();

View File

@ -7,8 +7,8 @@ import { mock } from 'vitest-mock-extended';
import type { ProjectScopeService, WorkflowSharingService } from '@n8n/backend-services';
import { TypeToNumber, type TypeUnitNumber } from '../database/entities/insights-shared';
import type { InsightsByPeriodRepository } from '../database/repositories/insights-by-period.repository';
import { TypeToNumber, type TypeUnitNumber } from '../insights-shared';
import type { InsightsByPeriodRepository } from '../insights-by-period.repository';
import { InsightsService } from '../insights.service';
const user = mock<User>({ id: 'user-1' });

View File

@ -2,7 +2,7 @@ import type { LicenseState } from '@n8n/backend-common';
import type { Mocked } from 'vitest';
import { mock } from 'vitest-mock-extended';
import type { InsightsByPeriodRepository } from '../database/repositories/insights-by-period.repository';
import type { InsightsByPeriodRepository } from '../insights-by-period.repository';
import { InsightsSettings } from '../insights.settings';
describe('InsightsSettings', () => {

View File

@ -0,0 +1,14 @@
import { ModuleMetadata } from '@n8n/decorators';
import { Container } from '@n8n/di';
describe('public API', () => {
it('does not register the module entrypoint', async () => {
const moduleMetadata = Container.get(ModuleMetadata);
expect(moduleMetadata.get('insights')).toBeUndefined();
await import('./index.js');
expect(moduleMetadata.get('insights')).toBeUndefined();
});
});

View File

@ -0,0 +1,18 @@
export { InsightsConfig } from './insights.config';
export { InsightsService } from './insights.service';
export { InsightsCollectionService } from './insights-collection.service';
export { InsightsCompactionService } from './insights-compaction.service';
export { InsightsPruningService } from './insights-pruning.service';
export { InsightsByPeriod } from './insights-by-period.entity';
export { InsightsMetadata } from './insights-metadata.entity';
export { InsightsRaw } from './insights-raw.entity';
export type {
ByTimeInsightType,
PeriodUnit,
TypeUnit,
} from './insights-shared';
export { TypeToNumber } from './insights-shared';
export type { InsightsAccessFilter } from './insights-by-period.repository';
export { InsightsByPeriodRepository } from './insights-by-period.repository';
export { InsightsMetadataRepository } from './insights-metadata.repository';
export { InsightsRawRepository } from './insights-raw.repository';

View File

@ -9,7 +9,7 @@ import {
} from '@n8n/typeorm';
import { UnexpectedError } from 'n8n-workflow';
import { InsightsMetadata } from './insights-metadata';
import { InsightsMetadata } from './insights-metadata.entity';
import type { PeriodUnit } from './insights-shared';
import {
isValidPeriodNumber,

View File

@ -16,9 +16,9 @@ import { UnexpectedError } from 'n8n-workflow';
import { z } from 'zod';
import { getDateRangesCommonTableExpressionQuery } from './insights-by-period-query.helper';
import { InsightsByPeriod } from '../entities/insights-by-period';
import type { PeriodUnit, TypeUnitNumber, ByTimeInsightType } from '../entities/insights-shared';
import { PeriodUnitToNumber, TypeToNumber } from '../entities/insights-shared';
import { InsightsByPeriod } from './insights-by-period.entity';
import type { PeriodUnit, TypeUnitNumber, ByTimeInsightType } from './insights-shared';
import { PeriodUnitToNumber, TypeToNumber } from './insights-shared';
const dbType = Container.get(GlobalConfig).database.type;
const displayTypeName = {
@ -339,7 +339,7 @@ export class InsightsByPeriodRepository extends Repository<InsightsByPeriod> {
const result = await this.dbLockService.withLock(DbLock.INSIGHTS_COMPACTION, async (trx) => {
await trx.query(getBatchAndStoreInTemporaryTable);
await trx.query<Array<{ type: any; value: number }>>(upsertEvents);
await trx.query<Array<{ type: unknown; value: number }>>(upsertEvents);
const rowsInBatch = await trx.query<[{ rowsInBatch: number | string }]>(countBatch);

View File

@ -6,10 +6,10 @@ import { Service } from '@n8n/di';
import { DateTime } from 'luxon';
import { IRun, type ExecutionStatus, type WorkflowExecuteMode } from 'n8n-workflow';
import { InsightsMetadata } from './database/entities/insights-metadata';
import { InsightsRaw } from './database/entities/insights-raw';
import { InsightsMetadataRepository } from './database/repositories/insights-metadata.repository';
import { InsightsRawRepository } from './database/repositories/insights-raw.repository';
import { InsightsMetadata } from './insights-metadata.entity';
import { InsightsRaw } from './insights-raw.entity';
import { InsightsMetadataRepository } from './insights-metadata.repository';
import { InsightsRawRepository } from './insights-raw.repository';
import { InsightsConfig } from './insights.config';
const shouldSkipStatus: Record<ExecutionStatus, boolean> = {

View File

@ -3,8 +3,8 @@ import { Time } from '@n8n/constants';
import { Service } from '@n8n/di';
import { sleep } from '@n8n/utils/sleep';
import { InsightsByPeriodRepository } from './database/repositories/insights-by-period.repository';
import { InsightsRawRepository } from './database/repositories/insights-raw.repository';
import { InsightsByPeriodRepository } from './insights-by-period.repository';
import { InsightsRawRepository } from './insights-raw.repository';
import { InsightsConfig } from './insights.config';
type CompactionRunState = {

View File

@ -1,7 +1,7 @@
import { Service } from '@n8n/di';
import { DataSource, In, Repository } from '@n8n/typeorm';
import { InsightsMetadata } from '../entities/insights-metadata';
import { InsightsMetadata } from './insights-metadata.entity';
@Service()
export class InsightsMetadataRepository extends Repository<InsightsMetadata> {

View File

@ -1,7 +1,7 @@
import { Logger } from '@n8n/backend-common';
import { Service } from '@n8n/di';
import { InsightsByPeriodRepository } from './database/repositories/insights-by-period.repository';
import { InsightsByPeriodRepository } from './insights-by-period.repository';
import { InsightsConfig } from './insights.config';
import { INSIGHTS_MAX_AGE_DAYS_CAP, INSIGHTS_MAX_AGE_DAYS_DEFAULT } from './insights.constants';

View File

@ -1,7 +1,7 @@
import { Service } from '@n8n/di';
import { DataSource, Repository } from '@n8n/typeorm';
import { InsightsRaw } from '../entities/insights-raw';
import { InsightsRaw } from './insights-raw.entity';
@Service()
export class InsightsRawRepository extends Repository<InsightsRaw> {

View File

@ -22,9 +22,9 @@ export class InsightsModule implements ModuleInterface {
}
async entities() {
const { InsightsByPeriod } = await import('./database/entities/insights-by-period.js');
const { InsightsMetadata } = await import('./database/entities/insights-metadata.js');
const { InsightsRaw } = await import('./database/entities/insights-raw.js');
const { InsightsByPeriod } = await import('./insights-by-period.entity.js');
const { InsightsMetadata } = await import('./insights-metadata.entity.js');
const { InsightsRaw } = await import('./insights-raw.entity.js');
return [InsightsByPeriod, InsightsMetadata, InsightsRaw];
}

View File

@ -12,10 +12,10 @@ import { InstanceSettings } from 'n8n-core';
import { UserError } from 'n8n-workflow';
import { ForbiddenError } from '@n8n/errors';
import type { PeriodUnit, TypeUnit, ByTimeInsightType } from './database/entities/insights-shared';
import { NumberToType } from './database/entities/insights-shared';
import type { InsightsAccessFilter } from './database/repositories/insights-by-period.repository';
import { InsightsByPeriodRepository } from './database/repositories/insights-by-period.repository';
import type { PeriodUnit, TypeUnit, ByTimeInsightType } from './insights-shared';
import { NumberToType } from './insights-shared';
import type { InsightsAccessFilter } from './insights-by-period.repository';
import { InsightsByPeriodRepository } from './insights-by-period.repository';
const BY_TIME_INSIGHT_TYPES: ByTimeInsightType[] = [
'time_saved_min',

View File

@ -2,7 +2,7 @@ import { LicenseState } from '@n8n/backend-common';
import { Service } from '@n8n/di';
import { INSIGHTS_DATE_RANGE_KEYS, keyRangeToDays } from './insights.constants';
import { InsightsByPeriodRepository } from './database/repositories/insights-by-period.repository';
import { InsightsByPeriodRepository } from './insights-by-period.repository';
@Service()
export class InsightsSettings {

View File

@ -0,0 +1,6 @@
export {
createCompactedInsightsEvent,
createMetadata,
createRawInsightsEvent,
createRawInsightsEvents,
} from './testing/db-utils';

View File

@ -4,12 +4,12 @@ import { Container } from '@n8n/di';
import type { DateTime } from 'luxon';
import type { IWorkflowBase } from 'n8n-workflow';
import { InsightsByPeriodRepository } from '../../repositories/insights-by-period.repository';
import { InsightsMetadataRepository } from '../../repositories/insights-metadata.repository';
import { InsightsRawRepository } from '../../repositories/insights-raw.repository';
import { InsightsByPeriod } from '../insights-by-period';
import { InsightsMetadata } from '../insights-metadata';
import { InsightsRaw } from '../insights-raw';
import { InsightsByPeriod } from '../insights-by-period.entity';
import { InsightsMetadata } from '../insights-metadata.entity';
import { InsightsRaw } from '../insights-raw.entity';
import { InsightsByPeriodRepository } from '../insights-by-period.repository';
import { InsightsMetadataRepository } from '../insights-metadata.repository';
import { InsightsRawRepository } from '../insights-raw.repository';
async function getWorkflowSharing(workflow: IWorkflowBase) {
return await Container.get(SharedWorkflowRepository).find({

View File

@ -0,0 +1,9 @@
import { createTestDatabaseGlobalSetup } from 'n8n-containers/test-db-global-setup';
export const { setup, teardown } = createTestDatabaseGlobalSetup({
initializeTemplateDb: async (templateName) => {
// Resolve config after the global setup has populated the DB environment.
const { testDb } = await import('@n8n/backend-test-utils');
await testDb.initTemplateDb(templateName);
},
});

View File

@ -0,0 +1,19 @@
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
process.env.N8N_ENCRYPTION_KEY = 'test_key';
const baseDir = join(tmpdir(), 'n8n-insights-tests');
mkdirSync(baseDir, { recursive: true });
const testDir = mkdtempSync(join(baseDir, 'run-'));
const settingsDir = join(testDir, '.n8n');
mkdirSync(settingsDir);
process.env.N8N_USER_FOLDER = testDir;
process.env.N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS = 'true';
writeFileSync(join(settingsDir, 'config'), JSON.stringify({ encryptionKey: 'test_key' }), {
encoding: 'utf-8',
mode: 0o600,
});

View File

@ -0,0 +1,10 @@
{
"extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.go.json"],
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "dist"
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/__tests__/**", "src/**/*.test.ts"]
}

View File

@ -0,0 +1,17 @@
{
"extends": [
"@n8n/typescript-config/tsconfig.common.go.json",
"@n8n/typescript-config/tsconfig.backend.go.json"
],
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": ["es2023", "dom"],
"types": ["node", "vitest/globals"],
"strict": true,
"strictFunctionTypes": false,
"strictPropertyInitialization": false,
"useUnknownInCatchVariables": false
},
"include": ["src/**/*.ts", "test/**/*.ts", "vitest.config*.ts"]
}

View File

@ -0,0 +1,12 @@
import baseConfig from './vitest.config';
export default {
...baseConfig,
test: {
...baseConfig.test,
include: ['src/**/*.integration.test.ts'],
testTimeout: 30_000,
hookTimeout: 30_000,
globalSetup: ['./test/setup-postgres.ts'],
},
};

View File

@ -0,0 +1,24 @@
import { createVitestConfigWithDecorators } from '@n8n/vitest-config/node-decorators';
import { tscDecoratorTransform } from '@n8n/vitest-config/tsc-decorator-transform';
import path from 'node:path';
import { mergeConfig } from 'vite';
import { configDefaults } from 'vitest/config';
const entitiesDirectory = path.join('database', 'entities') + path.sep;
export default mergeConfig(
createVitestConfigWithDecorators({}, { pinCjs: ['@n8n/backend-common', '@n8n/decorators'] }),
{
plugins: [
tscDecoratorTransform({
filePredicate: (fileName) =>
fileName.includes(entitiesDirectory) || /\.config\.ts$/.test(fileName),
}),
],
test: {
pool: 'forks',
setupFiles: ['./test/setup-test-folder.ts'],
exclude: [...configDefaults.exclude, '**/dist/**'],
},
},
);

View File

@ -0,0 +1,85 @@
/**
* Vitest global setup for integration/migration tests using testcontainers.
* Starts postgres and redis containers via n8n-containers and exposes their
* connection details through `process.env`, which the forked test workers inherit.
*
* Note: Ryuk handles container cleanup on process exit (crashes/timeouts);
* `teardown` is the secondary cleanup path.
*/
import { randomBytes } from 'node:crypto';
import { createServiceStack } from './service-stack';
type TestDatabaseGlobalSetupOptions = {
initializeTemplateDb?: (templateName: string) => Promise<void>;
};
export function createTestDatabaseGlobalSetup({
initializeTemplateDb,
}: TestDatabaseGlobalSetupOptions = {}) {
let stack: Awaited<ReturnType<typeof createServiceStack>> | undefined;
async function setup() {
const suffix = randomBytes(4).toString('hex');
stack = await createServiceStack({
services: ['postgres', 'redis'],
projectName: `n8n-integration-test-${suffix}`,
});
const pgResult = stack.serviceResults.postgres;
if (!pgResult) {
throw new Error('Failed to start postgres container');
}
const { container } = pgResult;
const meta = pgResult.meta as { database: string; username: string; password: string };
process.env.DB_TYPE = 'postgresdb';
process.env.DB_POSTGRESDB_HOST = container.getHost();
process.env.DB_POSTGRESDB_PORT = String(container.getMappedPort(5432));
process.env.DB_POSTGRESDB_DATABASE = meta.database;
process.env.DB_POSTGRESDB_USER = meta.username;
process.env.DB_POSTGRESDB_PASSWORD = meta.password;
process.env.DB_POSTGRESDB_SCHEMA = 'alt_schema';
process.env.DB_TABLE_PREFIX = 'test_';
process.env.DB_POSTGRESDB_POOL_SIZE = '1'; // Detect connection pooling deadlocks
console.log(
`\n✓ Postgres ready at ${process.env.DB_POSTGRESDB_HOST}:${process.env.DB_POSTGRESDB_PORT}\n`,
);
const redisResult = stack.serviceResults.redis;
if (!redisResult) {
throw new Error('Failed to start redis container');
}
process.env.N8N_TEST_REDIS_HOST = redisResult.container.getHost();
process.env.N8N_TEST_REDIS_PORT = String(redisResult.container.getMappedPort(6379));
console.log(
`✓ Redis ready at ${process.env.N8N_TEST_REDIS_HOST}:${process.env.N8N_TEST_REDIS_PORT}\n`,
);
// Build a template DB once, then each test file's testDb.init() clones it via
// CREATE DATABASE ... TEMPLATE instead of replaying the full migration history.
// Set N8N_TEST_DISABLE_TEMPLATE_DB=1 to opt out (e.g. when bisecting migration bugs).
if (initializeTemplateDb && process.env.N8N_TEST_DISABLE_TEMPLATE_DB !== '1') {
const templateName = `n8n_test_template_${suffix}`;
const tplStart = Date.now();
await initializeTemplateDb(templateName);
process.env.N8N_TEST_TEMPLATE_DB = templateName;
console.log(
`✓ Template DB ${templateName} ready (${Date.now() - tplStart}ms) — workers will clone instead of migrate\n`,
);
}
}
async function teardown() {
if (stack) {
await stack.stop();
console.log('\n✓ Testcontainers stack stopped\n');
}
}
return { setup, teardown };
}
export const { setup, teardown } = createTestDatabaseGlobalSetup();

View File

@ -4786,6 +4786,9 @@ importers:
'@n8n/backend-common':
specifier: workspace:*
version: link:../@n8n/backend-common
'@n8n/backend-module-insights':
specifier: workspace:*
version: link:../modules/insights/backend
'@n8n/backend-network':
specifier: workspace:*
version: link:../@n8n/backend-network
@ -5542,67 +5545,6 @@ importers:
specifier: 3.25.76
version: 3.25.76
packages/extensions/insights:
dependencies:
'@n8n/extension-sdk':
specifier: workspace:*
version: link:../../@n8n/extension-sdk
'@vue/language-core':
specifier: catalog:frontend
version: 3.3.11
vue:
specifier: catalog:frontend
version: 3.5.26(typescript@6.0.2)
vue-router:
specifier: catalog:frontend
version: 4.5.0(vue@3.5.26(typescript@6.0.2))
devDependencies:
'@n8n/oxlint-config':
specifier: workspace:*
version: link:../../@n8n/oxlint-config
'@n8n/stylelint-config':
specifier: workspace:*
version: link:../../@n8n/stylelint-config
'@n8n/typescript-config':
specifier: workspace:*
version: link:../../@n8n/typescript-config
'@vitejs/plugin-vue':
specifier: catalog:frontend
version: 6.0.8(vite@8.0.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@20.19.41)(esbuild@0.28.1)(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))
'@vue/tsconfig':
specifier: catalog:frontend
version: 0.7.0(typescript@6.0.2)(vue@3.5.26(typescript@6.0.2))
oxlint:
specifier: 'catalog:'
version: 1.78.0(oxlint-tsgolint@0.21.1)
oxlint-tsgolint:
specifier: 'catalog:'
version: 0.21.1
rimraf:
specifier: 'catalog:'
version: 6.0.1
stylelint:
specifier: 'catalog:'
version: 16.23.0(supports-color@8.1.1)(typescript@6.0.2)
tsdown:
specifier: 'catalog:'
version: 0.16.5(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(oxc-resolver@11.21.2)(typescript@6.0.2)(vue-tsc@2.2.8(patch_hash=e2aee939ccac8a57fe449bfd92bedd8117841579526217bc39aca26c6b8c317f)(typescript@6.0.2))
typescript:
specifier: 'catalog:'
version: 6.0.2
unplugin-dts:
specifier: 'catalog:'
version: 1.1.0(@microsoft/api-extractor@7.52.1(@types/node@20.19.41))(@vue/language-core@3.3.11)(esbuild@0.28.1)(rolldown@1.1.5)(rollup@4.52.4)(supports-color@8.1.1)(typescript@6.0.2)(vite@8.0.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@20.19.41)(esbuild@0.28.1)(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))
vite:
specifier: 'catalog:'
version: 8.0.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@20.19.41)(esbuild@0.28.1)(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: 5.0.1(@opentelemetry/api@1.9.1)(@types/node@20.19.41)(@vitest/browser-playwright@5.0.1)(@vitest/coverage-istanbul@5.0.1)(@vitest/coverage-v8@5.0.1)(@vitest/ui@5.0.1)(jsdom@23.0.1(bufferutil@4.0.9)(supports-color@8.1.1)(utf-8-validate@5.0.10))(vite@8.0.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@20.19.41)(esbuild@0.28.1)(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-tsc:
specifier: ^2.2.8
version: 2.2.8(patch_hash=e2aee939ccac8a57fe449bfd92bedd8117841579526217bc39aca26c6b8c317f)(typescript@6.0.2)
packages/frontend/@n8n/chat:
dependencies:
'@n8n/design-system':
@ -7043,6 +6985,94 @@ importers:
specifier: 3.25.76
version: 3.25.76
packages/modules/insights/backend:
dependencies:
'@n8n/api-types':
specifier: workspace:*
version: link:../../../@n8n/api-types
'@n8n/backend-common':
specifier: workspace:*
version: link:../../../@n8n/backend-common
'@n8n/backend-services':
specifier: workspace:*
version: link:../../../@n8n/backend-services
'@n8n/config':
specifier: workspace:*
version: link:../../../@n8n/config
'@n8n/constants':
specifier: workspace:*
version: link:../../../@n8n/constants
'@n8n/db':
specifier: workspace:*
version: link:../../../@n8n/db
'@n8n/decorators':
specifier: workspace:*
version: link:../../../@n8n/decorators
'@n8n/di':
specifier: workspace:*
version: link:../../../@n8n/di
'@n8n/errors':
specifier: workspace:*
version: link:../../../@n8n/errors
'@n8n/typeorm':
specifier: workspace:*
version: link:../../../@n8n/typeorm
'@n8n/utils':
specifier: workspace:*
version: link:../../../@n8n/utils
luxon:
specifier: 'catalog:'
version: 3.7.2
n8n-core:
specifier: workspace:*
version: link:../../../core
n8n-workflow:
specifier: workspace:*
version: link:../../../workflow
devDependencies:
'@n8n/backend-test-utils':
specifier: workspace:*
version: link:../../../@n8n/backend-test-utils
'@n8n/oxlint-config':
specifier: workspace:*
version: link:../../../@n8n/oxlint-config
'@n8n/typescript-config':
specifier: workspace:*
version: link:../../../@n8n/typescript-config
'@n8n/vitest-config':
specifier: workspace:*
version: link:../../../@n8n/vitest-config
'@types/luxon':
specifier: 'catalog:'
version: 3.2.0
'@vitest/coverage-v8':
specifier: 'catalog:'
version: 5.0.1(@vitest/browser@5.0.1)(vitest@5.0.1)
n8n-containers:
specifier: workspace:*
version: link:../../../quality/environments/containers
oxlint:
specifier: 'catalog:'
version: 1.78.0(oxlint-tsgolint@0.21.1)
oxlint-tsgolint:
specifier: 'catalog:'
version: 0.21.1
typescript:
specifier: catalog:typescript
version: 7.0.2
vite:
specifier: 'catalog:'
version: 8.0.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@20.19.41)(esbuild@0.28.1)(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: 5.0.1(@opentelemetry/api@1.9.1)(@types/node@20.19.41)(@vitest/browser-playwright@5.0.1)(@vitest/coverage-istanbul@5.0.1)(@vitest/coverage-v8@5.0.1)(@vitest/ui@5.0.1)(jsdom@23.0.1(bufferutil@4.0.9)(supports-color@8.1.1)(utf-8-validate@5.0.10))(vite@8.0.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@20.19.41)(esbuild@0.28.1)(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-mock-extended:
specifier: 'catalog:'
version: 3.1.0(typescript@7.0.2)(vitest@5.0.1)
zod:
specifier: 3.25.76
version: 3.25.76
packages/modules/insights/frontend:
dependencies:
'@internationalized/date':
@ -33879,11 +33909,6 @@ snapshots:
typescript: '@typescript/typescript6@6.0.2'
vue: 3.5.26(@typescript/typescript6@6.0.2)
'@vue/tsconfig@0.7.0(typescript@6.0.2)(vue@3.5.26(typescript@6.0.2))':
optionalDependencies:
typescript: 6.0.2
vue: 3.5.26(typescript@6.0.2)
'@vueuse/components@10.11.0(vue@3.5.26(typescript@6.0.2))':
dependencies:
'@vueuse/core': 10.11.0(vue@3.5.26(typescript@6.0.2))

View File

@ -18,7 +18,9 @@ To set up a backend module, run this command at monorepo root:
pnpm setup-backend-module
```
Your new module will be located at `packages/cli/src/modules/my-feature`. Rename `my-feature` in the dirname and in the filenames to your actual feature name. Use kebab-case.
The setup command creates a module at `packages/cli/src/modules/my-feature`. Rename `my-feature` in the directory name and file names. Use kebab-case.
A decoupled module can instead use a built workspace package at `packages/modules/my-feature/backend`. The package name uses the `@n8n/backend-module-<name>` pattern. Add a lazy package import to `packages/cli/src/modules/modules.manifest.ts`. Add the package as a runtime dependency of `n8n`.
A module’s file structure is as follows:
@ -38,12 +40,14 @@ A module’s file structure is as follows:
This is only a template - your module may not need all of these files, or it may need more than these and also subdirs to keep things organized. Infixes are currently not required (except for the `.module.ts` entrypoint), but infixes are strongly recommended as they make the contents searchable and discoverable.
Backend modules currently live at `packages/cli/src/modules`, so imports can be:
Modules inside CLI can import:
- from inside the module dir
- from common packages like `@n8n/db`, `@n8n/backend-common`, `@n8n/backend-services`, `@n8n/backend-test-utils`, etc.
- from `cli`
- from third-party libs available in, or added to, `cli`
- from CLI
- from third-party libraries available in, or added to, CLI
A packaged backend module must not import CLI. Declare each third-party and workspace dependency in the module package. Export APIs for CLI consumers from the package root. Use a separate `./testing` export for shared test helpers.
Modules are managed via env vars:
@ -334,9 +338,9 @@ Occasionally, a module may need to define a module-specific CLI command. To do s
## Tests
Place unit and integration tests for a backend module at `packages/cli/src/modules/{featureName}/__tests__`. Use the `.test.ts` infix.
Place tests with the module that owns them. A packaged module keeps its tests under `packages/modules/{featureName}/backend/src`. Keep tests in CLI only when they verify CLI wiring or use CLI-owned fixtures.
Currently, testing utilities live partly at `cli` and partly at `@n8n/backend-test-utils`. In future, all testing utilities will be moved to common packages, to make modules more decoupled from `cli`.
Use `@n8n/backend-test-utils` for shared database and module helpers. Pass packaged module import thunks to `testModules.loadModules`. Use `tscDecoratorTransform` from `@n8n/vitest-config/tsc-decorator-transform` when a package owns TypeORM entities.
## Future work
@ -357,10 +361,10 @@ Currently, testing utilities live partly at `cli` and partly at `@n8n/backend-te
## FAQs
- **What is a good example of a backend module?** Our first backend module is the `insights` module at `packages/@n8n/modules/insights`.
- **What is a good example of a backend module?** Use the Insights backend package at `packages/modules/insights/backend`.
- **My feature is already a separate _package_ at `packages/@n8n/{feature}`. How does this work with modules?** If your feature is already fully decoupled from `cli`, or if you know in advance that your feature will have zero dependencies on `cli`, then you already stand to gain most of the benefits of modularity. In this case, you can add a thin module to `cli` containing an entrypoint to your feature imported from your package, so that your feature is loaded only when needed.
- **Does all new functionality need to be added as a module?** If your feature relies heavily on internals, e.g. workflow archival, then a module may not be a good fit. Consider a module first, but use your best judgment. Reach out if unsure.
- **Are backend modules meant for use by external contributors?** No, they are meant for features developed by the core team.
- **How do I hot reload a module?** Modules are part of `cli` so you can use the usual `watch` command.
- **How do I hot reload a module?** Run the package watch command together with the CLI watch command.
- **How do modules interoperate with each other?** This is not supported at this time. Reach out if you need this.
- **I have a use case that is not covered by modules. What should I do?** Modules live in `cli` so any imports from `cli` remain available, i.e. aim for decoupling but do not consider it a blocker for progress. Reach out if you think the module system needs expanding.
- **I have a use case that is not covered by modules. What should I do?** A module can stay in CLI until its dependencies have package-level interfaces. Reach out if the module system needs a new interface.

Some files were not shown because too many files have changed in this diff Show More