refactor(core): Extract Quick Connect into a backend package (#41076)

This commit is contained in:
Charlie Kolb 2026-10-14 12:04:16 +00:00 committed by GitHub
parent 898701b05b
commit 76c43094cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 313 additions and 1 deletions

2
OWNERS
View File

@ -120,7 +120,7 @@ packages/cli/src/chat/ @n8n-io/ai
packages/cli/src/tool-generation/ @n8n-io/ai
packages/cli/src/modules/workflow-builder/ @n8n-io/ai-assistant
packages/cli/src/modules/mcp/ @n8n-io/context
packages/cli/src/modules/quick-connect/ @n8n-io/ai
packages/modules/quick-connect/ @n8n-io/ai
packages/cli/src/modules/chat-hub/ @n8n-io/ai-assistant
packages/cli/src/modules/instance-ai/ @n8n-io/ai-assistant

View File

@ -160,6 +160,7 @@
"@n8n/api-types": "workspace:*",
"@n8n/backend-common": "workspace:*",
"@n8n/backend-module-insights": "workspace:*",
"@n8n/backend-module-quick-connect": "workspace:*",
"@n8n/backend-network": "workspace:*",
"@n8n/backend-services": "workspace:*",
"@n8n/blob-storage": "workspace:*",

View File

@ -0,0 +1,23 @@
import { ControllerRegistryMetadata, ModuleMetadata } from '@n8n/decorators';
import { Container } from '@n8n/di';
describe('packaged module manifest', () => {
it('loads the Quick Connect package only when requested', async () => {
const metadata = Container.get(ModuleMetadata);
const controllers = Container.get(ControllerRegistryMetadata);
expect(metadata.get('quick-connect')).toBeUndefined();
const { packagedModules } = await import('../modules.manifest.js');
expect(metadata.get('quick-connect')).toBeUndefined();
expect([...controllers.controllerClasses]).toEqual([]);
const loadQuickConnect = packagedModules['quick-connect'];
expect(loadQuickConnect).toBeTypeOf('function');
const entrypoint = await loadQuickConnect?.();
const packagedEntrypoint = await import('@n8n/backend-module-quick-connect/module');
expect(entrypoint).toBe(packagedEntrypoint);
expect(metadata.get('quick-connect')).toEqual({ class: packagedEntrypoint.QuickConnectModule });
expect([...controllers.controllerClasses]).toEqual([]);
});
});

View File

@ -6,4 +6,5 @@ import type { PackagedModules } from '@n8n/backend-common';
*/
export const packagedModules: PackagedModules = {
insights: async () => await import('@n8n/backend-module-insights/module'),
'quick-connect': async () => await import('@n8n/backend-module-quick-connect/module'),
};

View File

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

View File

@ -0,0 +1,25 @@
# @n8n/backend-module-quick-connect
Backend feature module. The n8n runtime loads the module through the lazy import in
`packages/cli/src/modules/modules.manifest.ts`.
```bash
pnpm turbo typecheck --filter=@n8n/backend-module-quick-connect
pnpm turbo lint --filter=@n8n/backend-module-quick-connect
pnpm turbo test --filter=@n8n/backend-module-quick-connect
```
Go through Turbo. It builds this package's workspace dependencies first.
Read `scripts/backend-module/backend-module-guide.md` for the module lifecycle, REST controller,
service, repository, entity, configuration, and testing patterns.
## Import rules
- Do not import from `packages/cli` or from the `n8n` package.
- Declare each workspace and third-party import in this package.
- Export APIs for other packages from `src/index.ts`.
- Keep the module entrypoint in the `./module` export. Do not export it from `src/index.ts`.
Importing the entrypoint runs the `@BackendModule` decorator.
- Use dynamic imports inside the module entrypoint. This keeps disabled modules unloaded.
- Keep TypeORM imports in entity and repository files.

View File

@ -0,0 +1,8 @@
import { backendConfig } from '@n8n/oxlint-config/backend';
import { defineConfig } from 'oxlint';
export default defineConfig({
extends: [backendConfig],
options: { typeAware: true },
ignorePatterns: ['coverage/**', 'dist/**'],
});

View File

@ -0,0 +1,74 @@
{
"name": "@n8n/backend-module-quick-connect",
"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/quick-connect.module.d.ts",
"import": "./dist/quick-connect.module.js",
"require": "./dist/quick-connect.module.js"
}
},
"typesVersions": {
"*": {
"module": [
"dist/quick-connect.module.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:unit": "vitest run",
"test:dev": "vitest --silent=false"
},
"dependencies": {
"@n8n/api-types": "workspace:*",
"@n8n/backend-common": "workspace:*",
"@n8n/backend-network": "workspace:*",
"@n8n/config": "workspace:*",
"@n8n/constants": "workspace:*",
"@n8n/db": "workspace:*",
"@n8n/decorators": "workspace:*",
"@n8n/di": "workspace:*",
"@n8n/errors": "workspace:*",
"n8n-workflow": "workspace:*"
},
"peerDependencies": {
"zod": "catalog:"
},
"devDependencies": {
"@n8n/oxlint-config": "workspace:*",
"@n8n/typescript-config": "workspace:*",
"@n8n/vitest-config": "workspace:*",
"@vitest/coverage-v8": "catalog:",
"nock": "catalog:",
"oxlint": "catalog:",
"oxlint-tsgolint": "catalog:",
"typescript": "catalog:typescript",
"vite": "catalog:",
"vitest": "catalog:",
"vitest-mock-extended": "catalog:",
"zod": "catalog:"
},
"license": "LicenseRef-n8n-sustainable-use"
}

View File

@ -0,0 +1,41 @@
import { ControllerRegistryMetadata, ModuleMetadata } from '@n8n/decorators';
import { Container } from '@n8n/di';
describe('package entrypoints', () => {
it('loads registration only through the module entrypoint', async () => {
const moduleMetadata = Container.get(ModuleMetadata);
const controllerMetadata = Container.get(ControllerRegistryMetadata);
expect(moduleMetadata.get('quick-connect')).toBeUndefined();
expect([...controllerMetadata.controllerClasses]).toEqual([]);
const publicApi = await import('./index.js');
expect(Object.keys(publicApi)).toEqual([]);
expect(moduleMetadata.get('quick-connect')).toBeUndefined();
expect([...controllerMetadata.controllerClasses]).toEqual([]);
const { QuickConnectModule } = await import('./quick-connect.module.js');
expect(moduleMetadata.get('quick-connect')).toEqual({ class: QuickConnectModule });
expect([...controllerMetadata.controllerClasses]).toEqual([]);
const { QuickConnectService } = await import('./quick-connect.service.js');
const registerHandlers = vi
.spyOn(QuickConnectService.prototype, 'registerHandlers')
.mockImplementation(async () => {
expect([...controllerMetadata.controllerClasses]).toEqual([]);
});
await new QuickConnectModule().init();
expect(registerHandlers).toHaveBeenCalledOnce();
const { QuickConnectController } = await import('./quick-connect.controller.js');
expect([...controllerMetadata.controllerClasses]).toEqual([QuickConnectController]);
for (const controller of controllerMetadata.controllerClasses) {
const metadata = controllerMetadata.getControllerMetadata(controller);
expect(metadata.basePath).toBe('/quick-connect');
expect(metadata.routes.get('getCredentialData')).toMatchObject({ method: 'post', path: '/' });
}
});
});

View File

@ -0,0 +1 @@
export {};

View File

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

View File

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

View File

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

View File

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

View File

@ -4823,6 +4823,9 @@ importers:
'@n8n/backend-module-insights':
specifier: workspace:*
version: link:../modules/insights/backend
'@n8n/backend-module-quick-connect':
specifier: workspace:*
version: link:../modules/quick-connect/backend
'@n8n/backend-network':
specifier: workspace:*
version: link:../@n8n/backend-network
@ -7549,6 +7552,76 @@ importers:
specifier: ^2.2.8
version: 2.2.8(patch_hash=e2aee939ccac8a57fe449bfd92bedd8117841579526217bc39aca26c6b8c317f)(typescript@6.0.2)
packages/modules/quick-connect/backend:
dependencies:
'@n8n/api-types':
specifier: workspace:*
version: link:../../../@n8n/api-types
'@n8n/backend-common':
specifier: workspace:*
version: link:../../../@n8n/backend-common
'@n8n/backend-network':
specifier: workspace:*
version: link:../../../@n8n/backend-network
'@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-workflow:
specifier: workspace:*
version: link:../../../workflow
devDependencies:
'@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
'@vitest/coverage-v8':
specifier: 'catalog:'
version: 5.0.1(@vitest/browser@5.0.1)(vitest@5.0.1)
nock:
specifier: 'catalog:'
version: 14.0.14
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/type-availability-policies/frontend:
dependencies:
'@n8n/api-types':