build(core): Add the @n8n/backend-services package (no-changelog) (#39034)

Co-authored-by: Claude Fable 5.1 <[email protected]>
Co-authored-by: Lorent Lempereur <[email protected]>
This commit is contained in:
Charlie Kolb
2026-09-18 13:30:13 +00:00
committed by GitHub
co-authored by Claude Fable 5.1 Lorent Lempereur
parent b68164528e
commit 21008ce758
15 changed files with 329 additions and 1 deletions
+1
View File
@@ -27,6 +27,7 @@ packages/workflow/stryker.config.mjs @n8n-io/qa-dx
packages/@n8n/config/ @n8n-io/catalysts
packages/@n8n/backend-common/ @n8n-io/catalysts
packages/@n8n/backend-network/ @n8n-io/catalysts
packages/@n8n/backend-services/ @n8n-io/catalysts
packages/@n8n/backend-test-utils/ @n8n-io/catalysts
packages/@n8n/di/ @n8n-io/catalysts
packages/@n8n/errors/ @n8n-io/catalysts
+1
View File
@@ -60,6 +60,7 @@ component_management:
- packages/extensions/**
- packages/@n8n/api-types/**
- packages/@n8n/backend-common/**
- packages/@n8n/backend-services/**
- packages/@n8n/backend-test-utils/**
- packages/@n8n/cli/**
- packages/@n8n/client-oauth2/**
+65
View File
@@ -0,0 +1,65 @@
# @n8n/backend-services
Backend services and HTTP error classes that many backend modules share.
## Why this package exists
Backend modules live in `packages/cli/src/modules/<name>` today. Most of them
import a small set of services from the rest of `cli`. A module cannot move
into its own workspace package while those imports point at `@/...` paths in
`cli`.
This package is the home for that shared set. It sits above `@n8n/db` and
below `n8n` (`cli`) in the dependency graph:
```mermaid
flowchart TD
cli["n8n (cli) and backend modules"] --> bs["@n8n/backend-services"]
bs --> db["@n8n/db"]
bs --> bc["@n8n/backend-common"]
db --> bc
```
`@n8n/backend-common` holds the foundation that `@n8n/db` also needs (logger,
license state, module registry, locks). `@n8n/backend-services` holds business
and infrastructure services that need the persistence layer or that only
`cli` and the modules consume.
## What lives here
Nothing yet. The next PRs move the response errors, `UrlService`, `CacheService`,
`RedisClientService`, `ProtectedResourceRegistry`, `EventService`, `RoleService`,
the finder services and the scope checks here, one area at a time.
## Compatibility with cli
The package config mirrors `packages/cli`, so a file moved from `cli` compiles,
lints and tests here without edits:
| Concern | Setup | Mirrors |
| --- | --- | --- |
| TypeScript | `common.go` + `backend.go`, `lib` es2023, `strictFunctionTypes`, `strictPropertyInitialization` and `useUnknownInCatchVariables` off | `packages/cli/tsconfig.json` |
| Lint | `oxlint --type-aware`, then the ESLint guardrails pass. `eslint.config.mjs` is the policy twin that code-health and the guardrails read | `packages/cli/oxlint.config.mts`, `eslint.guardrails.config.mjs` |
| Tests | Vitest with the decorators config, one fork per file, the same `N8N_USER_FOLDER` and `N8N_ENCRYPTION_KEY` setup | `packages/cli/vitest.config.base.ts`, `test/setup-test-folder.ts` |
Two things do not carry over on purpose:
- There is no `@/` path alias. `cli` tests load this package from `dist/`
and other consumers inline it from `src/`. `tsc` does not rewrite an alias
in `dist/`, so it resolves in neither. Use relative imports inside the
package.
- `cli` turns a set of rules down to `warn` for its whole tree. This package
keeps the shared backend layer as is. Fix such findings when you move a file.
## Rules for adding code
- The code must not import from `n8n` (`cli`). If it needs a `cli` seam,
define a narrow DI port here and bind it in `cli` at bootstrap.
- At least two backend modules must use the code. A service that one module
uses belongs to that module.
- Keep the file layout of `cli` (`errors/`, `services/`). A move with `git mv`
keeps the history readable.
- Add each new export to `src/index.ts`. Consumers import from the package
root only.
- Keep `oxlint.config.mts` and `eslint.config.mjs` in step. A ratchet
allowlist for a moved TypeORM leak goes into both.
@@ -0,0 +1,39 @@
import { defineConfig, globalIgnores } from 'eslint/config';
import { backendConfig } from '@n8n/eslint-config/backend';
// Policy twin of oxlint.config.mts for the tools that read ESLint configs (guardrails, code-health).
export default defineConfig(
globalIgnores(['coverage/**', 'vitest.config.ts']),
backendConfig,
{
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 in @n8n/db.',
},
{
rule: 'no-unsealed-workflow-entity-write',
message: 'Route the write through a token-gated `WorkflowRepository` method.',
},
],
},
],
'n8n-local-rules/no-type-unsafe-event-emitter': 'error',
},
},
{
files: ['./test/**/*.ts', './src/**/__tests__/**/*.ts'],
rules: {
'n8n-local-rules/misplaced-n8n-typeorm-import': 'off',
'n8n-local-rules/no-type-unsafe-event-emitter': 'off',
// `vi.importActual<typeof import('x')>('x')` needs inline import types.
'@typescript-eslint/consistent-type-imports': ['error', { disallowTypeAnnotations: false }],
},
},
);
@@ -0,0 +1,43 @@
import { defineConfig, globalIgnores } from 'eslint/config';
import { functionalGuardrailsConfig } from '@n8n/eslint-config/functional-guardrails';
import { localRulesPlugin } from '@n8n/eslint-config/plugin';
import packageConfig from './eslint.config.mjs';
const directivePluginConfigs = packageConfig.flatMap(({ plugins }) => {
const externalPlugins = Object.fromEntries(
Object.entries(plugins ?? {}).filter(([name]) => name !== 'n8n-local-rules'),
);
return Object.keys(externalPlugins).length === 0 ? [] : [{ plugins: externalPlugins }];
});
const dependencyRuleConfigs = packageConfig.flatMap(({ files, ignores, settings, rules }) => {
const rule = rules?.['import-x/no-extraneous-dependencies'];
if (!rule) return [];
return [
{
...(files ? { files } : {}),
...(ignores ? { ignores } : {}),
...(settings ? { settings } : {}),
rules: { 'import-x/no-extraneous-dependencies': rule },
},
];
});
export default defineConfig(
globalIgnores([
'dist/**',
'coverage/**',
'eslint.config.mjs',
'eslint.guardrails.config.mjs',
'oxlint.config.mts',
'vitest.config.ts',
]),
{
plugins: { 'n8n-local-rules': localRulesPlugin },
},
functionalGuardrailsConfig,
...directivePluginConfigs,
...dependencyRuleConfigs,
);
@@ -0,0 +1,40 @@
import { backendConfig } from '@n8n/oxlint-config/backend';
import { defineConfig } from 'oxlint';
// Same rule set as packages/cli/oxlint.config.mts, so a file moved from cli lints the same way.
export default defineConfig({
extends: [backendConfig],
ignorePatterns: ['coverage/**', 'eslint.guardrails.config.mjs'],
rules: {
'n8n-local-rules/misplaced-n8n-typeorm-import': 'error',
// The narrow ESLint guardrail config owns no-unsealed-workflow-entity-write.
'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 in @n8n/db.',
},
{
rule: 'no-unsealed-workflow-entity-write',
message: 'Route the write through a token-gated `WorkflowRepository` method.',
},
],
},
],
'n8n-local-rules/no-type-unsafe-event-emitter': 'error',
},
overrides: [
{
files: ['./test/**/*.ts', './src/**/__tests__/**/*.ts'],
// An override that names a jsPlugin rule must re-declare the plugin.
jsPlugins: ['@n8n/eslint-config/plugin'],
rules: {
'n8n-local-rules/misplaced-n8n-typeorm-import': 'off',
'n8n-local-rules/no-type-unsafe-event-emitter': 'off',
},
},
],
});
@@ -0,0 +1,40 @@
{
"name": "@n8n/backend-services",
"version": "1.0.0",
"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": "pnpm run lint:base && pnpm run lint:guardrails",
"lint:base": "oxlint --type-aware . --quiet",
"lint:fix": "oxlint --type-aware . --fix && pnpm run lint:guardrails",
"lint:guardrails": "eslint --config eslint.guardrails.config.mjs . --quiet",
"watch": "tsc -p tsconfig.build.json --watch",
"test": "vitest run --passWithNoTests",
"test:unit": "vitest run --passWithNoTests",
"test:dev": "vitest --silent=false"
},
"main": "dist/index.js",
"module": "src/index.ts",
"types": "dist/index.d.ts",
"files": [
"dist/**/*"
],
"devDependencies": {
"@n8n/eslint-config": "workspace:*",
"@n8n/oxlint-config": "workspace:*",
"@n8n/typescript-config": "workspace:*",
"@n8n/vitest-config": "workspace:*",
"@vitest/coverage-v8": "catalog:",
"eslint": "catalog:",
"oxlint": "catalog:",
"oxlint-tsgolint": "catalog:",
"typescript": "catalog:typescript",
"vitest": "catalog:"
},
"license": "LicenseRef-n8n-sustainable-use"
}
@@ -0,0 +1 @@
export {};
@@ -0,0 +1,23 @@
import { mkdirSync, mkdtempSync, writeFileSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
// Same instance setup as packages/cli/test/setup-test-folder.ts, so a moved test finds it.
process.env.N8N_ENCRYPTION_KEY = 'test_key';
const baseDir = join(tmpdir(), 'n8n-tests/');
mkdirSync(baseDir, { recursive: true });
const testDir = mkdtempSync(baseDir);
mkdirSync(join(testDir, '.n8n'));
process.env.N8N_USER_FOLDER = testDir;
process.env.N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS = 'true';
writeFileSync(
join(testDir, '.n8n/config'),
JSON.stringify({ encryptionKey: 'test_key', instanceId: '123' }),
{
encoding: 'utf-8',
mode: 0o600,
},
);
@@ -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": ["test/**", "src/**/__tests__/**"]
}
@@ -0,0 +1,19 @@
{
"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"],
// Same relaxations as packages/cli/tsconfig.json, so a file moved from cli compiles unchanged.
"strict": true,
"strictFunctionTypes": false,
"strictPropertyInitialization": false,
// TODO: remove all options below this line
"useUnknownInCatchVariables": false
},
"include": ["src/**/*.ts", "test/**/*.ts"]
}
@@ -0,0 +1,12 @@
import { createVitestConfigWithDecorators } from '@n8n/vitest-config/node-decorators';
import { configDefaults, mergeConfig } from 'vitest/config';
export default mergeConfig(createVitestConfigWithDecorators(), {
test: {
// One fork per file: tests register mocks in the shared `@n8n/di` Container.
pool: 'forks',
setupFiles: ['./test/setup-test-folder.ts'],
// Vitest's default exclude does not cover dist; stale compiled tests there fail as CJS.
exclude: [...configDefaults.exclude, '**/dist/**'],
},
});
+33
View File
@@ -1732,6 +1732,39 @@ importers:
specifier: 'catalog:'
version: 3.1.0([email protected])([email protected])
packages/@n8n/backend-services:
devDependencies:
'@n8n/eslint-config':
specifier: workspace:*
version: link:../eslint-config
'@n8n/oxlint-config':
specifier: workspace:*
version: link:../oxlint-config
'@n8n/typescript-config':
specifier: workspace:*
version: link:../typescript-config
'@n8n/vitest-config':
specifier: workspace:*
version: link:../vitest-config
'@vitest/coverage-v8':
specifier: 'catalog:'
version: 4.1.9(@vitest/[email protected])([email protected])
eslint:
specifier: 'catalog:'
version: 9.29.0([email protected])([email protected])
oxlint:
specifier: 'catalog:'
version: 1.78.0([email protected])
oxlint-tsgolint:
specifier: 'catalog:'
version: 0.21.1
typescript:
specifier: catalog:typescript
version: 7.0.2
vitest:
specifier: 'catalog:'
version: 4.1.9(@opentelemetry/[email protected])(@types/[email protected])(@vitest/[email protected])(@vitest/[email protected])([email protected]([email protected])([email protected])([email protected]))([email protected](@emnapi/[email protected])(@emnapi/[email protected])(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))
packages/@n8n/backend-test-utils:
dependencies:
'@n8n/backend-common':
@@ -41,7 +41,7 @@ This is only a template - your module may not need all of these files, or it may
Backend modules currently live at `packages/cli/src/modules`, so imports can be:
- from inside the module dir
- from common packages like `@n8n/db`, `@n8n/backend-common`, `@n8n/backend-test-utils`, etc.
- 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`
+1
View File
@@ -26,6 +26,7 @@
"packages/@n8n/ai-workflow-builder.ee",
"packages/@n8n/backend-common",
"packages/@n8n/backend-network",
"packages/@n8n/backend-services",
"packages/@n8n/backend-test-utils",
"packages/@n8n/benchmark",
"packages/@n8n/blob-storage",