mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-12 02:19:43 +00:00
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:
co-authored by
Claude Fable 5.1
Lorent Lempereur
parent
b68164528e
commit
21008ce758
@@ -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
|
||||
|
||||
@@ -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/**
|
||||
|
||||
@@ -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/**'],
|
||||
},
|
||||
});
|
||||
Generated
+33
@@ -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`
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user