mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
refactor(core): Extract Insights backend module package (#40261)
This commit is contained in:
parent
fb11cc482e
commit
62fd028089
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
|
|
|||
2
.github/test-metrics/e2e-impact-map.json
vendored
2
.github/test-metrics/e2e-impact-map.json
vendored
File diff suppressed because one or more lines are too long
8
.github/workflows/ci-pull-requests.yml
vendored
8
.github/workflows/ci-pull-requests.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
7
.github/workflows/test-db-reusable.yml
vendored
7
.github/workflows/test-db-reusable.yml
vendored
|
|
@ -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
6
OWNERS
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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:*",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
0
packages/extensions/.gitkeep
Normal file
0
packages/extensions/.gitkeep
Normal file
24
packages/extensions/insights/.gitignore
vendored
24
packages/extensions/insights/.gitignore
vendored
|
|
@ -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?
|
||||
|
|
@ -1 +0,0 @@
|
|||
See LICENSE.md in the root of this repository for more information.
|
||||
|
|
@ -1 +0,0 @@
|
|||
# @n8n/n8n-extension-insights
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import { defineBackendExtension } from '@n8n/extension-sdk/backend';
|
||||
|
||||
export default defineBackendExtension({
|
||||
setup(n8n) {
|
||||
console.log(n8n);
|
||||
},
|
||||
});
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
<script lang="ts" setup></script>
|
||||
<template>
|
||||
<div>
|
||||
<h1>INSIGHTS DASHBOARD</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -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));
|
||||
},
|
||||
});
|
||||
1
packages/extensions/insights/src/shims.d.ts
vendored
1
packages/extensions/insights/src/shims.d.ts
vendored
|
|
@ -1 +0,0 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import { baseConfig } from '@n8n/stylelint-config/base';
|
||||
|
||||
export default baseConfig;
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"extends": "@n8n/typescript-config/tsconfig.common.json",
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "bundler",
|
||||
"module": "esnext"
|
||||
},
|
||||
"include": ["src/backend/**/*.ts", "src/index.ts"]
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/frontend/**/*.ts", "src/frontend/**/*.vue"]
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.backend.json" },
|
||||
{ "path": "./tsconfig.frontend.json" },
|
||||
{ "path": "./tsconfig.vite.json" }
|
||||
]
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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,
|
||||
});
|
||||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
36
packages/modules/insights/backend/oxlint.config.mts
Normal file
36
packages/modules/insights/backend/oxlint.config.mts
Normal 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',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
97
packages/modules/insights/backend/package.json
Normal file
97
packages/modules/insights/backend/package.json
Normal 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"
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
@ -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);
|
||||
|
|
@ -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 () => {
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
@ -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()) {
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
@ -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';
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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();
|
||||
|
|
@ -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' });
|
||||
|
|
@ -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', () => {
|
||||
14
packages/modules/insights/backend/src/index.test.ts
Normal file
14
packages/modules/insights/backend/src/index.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
18
packages/modules/insights/backend/src/index.ts
Normal file
18
packages/modules/insights/backend/src/index.ts
Normal 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';
|
||||
|
|
@ -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,
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
@ -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> = {
|
||||
|
|
@ -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 = {
|
||||
|
|
@ -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> {
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
@ -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> {
|
||||
|
|
@ -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];
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
@ -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 {
|
||||
6
packages/modules/insights/backend/src/testing.ts
Normal file
6
packages/modules/insights/backend/src/testing.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export {
|
||||
createCompactedInsightsEvent,
|
||||
createMetadata,
|
||||
createRawInsightsEvent,
|
||||
createRawInsightsEvents,
|
||||
} from './testing/db-utils';
|
||||
|
|
@ -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({
|
||||
9
packages/modules/insights/backend/test/setup-postgres.ts
Normal file
9
packages/modules/insights/backend/test/setup-postgres.ts
Normal 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);
|
||||
},
|
||||
});
|
||||
19
packages/modules/insights/backend/test/setup-test-folder.ts
Normal file
19
packages/modules/insights/backend/test/setup-test-folder.ts
Normal 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,
|
||||
});
|
||||
10
packages/modules/insights/backend/tsconfig.build.json
Normal file
10
packages/modules/insights/backend/tsconfig.build.json
Normal 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"]
|
||||
}
|
||||
17
packages/modules/insights/backend/tsconfig.json
Normal file
17
packages/modules/insights/backend/tsconfig.json
Normal 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"]
|
||||
}
|
||||
12
packages/modules/insights/backend/vitest.config.postgres.ts
Normal file
12
packages/modules/insights/backend/vitest.config.postgres.ts
Normal 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'],
|
||||
},
|
||||
};
|
||||
24
packages/modules/insights/backend/vitest.config.ts
Normal file
24
packages/modules/insights/backend/vitest.config.ts
Normal 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/**'],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
@ -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();
|
||||
157
pnpm-lock.yaml
157
pnpm-lock.yaml
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue
Block a user