diff --git a/OWNERS b/OWNERS index a61385b5b07..a7dbbdfc339 100644 --- a/OWNERS +++ b/OWNERS @@ -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 diff --git a/packages/cli/package.json b/packages/cli/package.json index 418be45e894..cbcb4ed4e52 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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:*", diff --git a/packages/cli/src/modules/__tests__/modules.manifest.test.ts b/packages/cli/src/modules/__tests__/modules.manifest.test.ts new file mode 100644 index 00000000000..23cdc2711ff --- /dev/null +++ b/packages/cli/src/modules/__tests__/modules.manifest.test.ts @@ -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([]); + }); +}); diff --git a/packages/cli/src/modules/modules.manifest.ts b/packages/cli/src/modules/modules.manifest.ts index dbe3ca24424..6142d7b1867 100644 --- a/packages/cli/src/modules/modules.manifest.ts +++ b/packages/cli/src/modules/modules.manifest.ts @@ -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'), }; diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 01dc23c7600..56e54f4a184 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -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" }, diff --git a/packages/modules/quick-connect/backend/README.md b/packages/modules/quick-connect/backend/README.md new file mode 100644 index 00000000000..bb613c16dd8 --- /dev/null +++ b/packages/modules/quick-connect/backend/README.md @@ -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. diff --git a/packages/modules/quick-connect/backend/oxlint.config.mts b/packages/modules/quick-connect/backend/oxlint.config.mts new file mode 100644 index 00000000000..23a0f5690f6 --- /dev/null +++ b/packages/modules/quick-connect/backend/oxlint.config.mts @@ -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/**'], +}); diff --git a/packages/modules/quick-connect/backend/package.json b/packages/modules/quick-connect/backend/package.json new file mode 100644 index 00000000000..10b8eee3d1f --- /dev/null +++ b/packages/modules/quick-connect/backend/package.json @@ -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" +} diff --git a/packages/cli/src/modules/quick-connect/__tests__/quick-connect.config.test.ts b/packages/modules/quick-connect/backend/src/__tests__/quick-connect.config.test.ts similarity index 100% rename from packages/cli/src/modules/quick-connect/__tests__/quick-connect.config.test.ts rename to packages/modules/quick-connect/backend/src/__tests__/quick-connect.config.test.ts diff --git a/packages/cli/src/modules/quick-connect/__tests__/quick-connect.controller.test.ts b/packages/modules/quick-connect/backend/src/__tests__/quick-connect.controller.test.ts similarity index 100% rename from packages/cli/src/modules/quick-connect/__tests__/quick-connect.controller.test.ts rename to packages/modules/quick-connect/backend/src/__tests__/quick-connect.controller.test.ts diff --git a/packages/cli/src/modules/quick-connect/__tests__/quick-connect.module.test.ts b/packages/modules/quick-connect/backend/src/__tests__/quick-connect.module.test.ts similarity index 100% rename from packages/cli/src/modules/quick-connect/__tests__/quick-connect.module.test.ts rename to packages/modules/quick-connect/backend/src/__tests__/quick-connect.module.test.ts diff --git a/packages/cli/src/modules/quick-connect/__tests__/quick-connect.service.test.ts b/packages/modules/quick-connect/backend/src/__tests__/quick-connect.service.test.ts similarity index 100% rename from packages/cli/src/modules/quick-connect/__tests__/quick-connect.service.test.ts rename to packages/modules/quick-connect/backend/src/__tests__/quick-connect.service.test.ts diff --git a/packages/cli/src/modules/quick-connect/handlers/__tests__/firecrawl.handler.test.ts b/packages/modules/quick-connect/backend/src/handlers/__tests__/firecrawl.handler.test.ts similarity index 100% rename from packages/cli/src/modules/quick-connect/handlers/__tests__/firecrawl.handler.test.ts rename to packages/modules/quick-connect/backend/src/handlers/__tests__/firecrawl.handler.test.ts diff --git a/packages/cli/src/modules/quick-connect/handlers/firecrawl.handler.ts b/packages/modules/quick-connect/backend/src/handlers/firecrawl.handler.ts similarity index 100% rename from packages/cli/src/modules/quick-connect/handlers/firecrawl.handler.ts rename to packages/modules/quick-connect/backend/src/handlers/firecrawl.handler.ts diff --git a/packages/cli/src/modules/quick-connect/handlers/handler.interface.ts b/packages/modules/quick-connect/backend/src/handlers/handler.interface.ts similarity index 100% rename from packages/cli/src/modules/quick-connect/handlers/handler.interface.ts rename to packages/modules/quick-connect/backend/src/handlers/handler.interface.ts diff --git a/packages/modules/quick-connect/backend/src/index.test.ts b/packages/modules/quick-connect/backend/src/index.test.ts new file mode 100644 index 00000000000..a220efd5721 --- /dev/null +++ b/packages/modules/quick-connect/backend/src/index.test.ts @@ -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: '/' }); + } + }); +}); diff --git a/packages/modules/quick-connect/backend/src/index.ts b/packages/modules/quick-connect/backend/src/index.ts new file mode 100644 index 00000000000..cb0ff5c3b54 --- /dev/null +++ b/packages/modules/quick-connect/backend/src/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/cli/src/modules/quick-connect/quick-connect.config.ts b/packages/modules/quick-connect/backend/src/quick-connect.config.ts similarity index 100% rename from packages/cli/src/modules/quick-connect/quick-connect.config.ts rename to packages/modules/quick-connect/backend/src/quick-connect.config.ts diff --git a/packages/cli/src/modules/quick-connect/quick-connect.controller.ts b/packages/modules/quick-connect/backend/src/quick-connect.controller.ts similarity index 100% rename from packages/cli/src/modules/quick-connect/quick-connect.controller.ts rename to packages/modules/quick-connect/backend/src/quick-connect.controller.ts diff --git a/packages/cli/src/modules/quick-connect/quick-connect.errors.ts b/packages/modules/quick-connect/backend/src/quick-connect.errors.ts similarity index 100% rename from packages/cli/src/modules/quick-connect/quick-connect.errors.ts rename to packages/modules/quick-connect/backend/src/quick-connect.errors.ts diff --git a/packages/cli/src/modules/quick-connect/quick-connect.module.ts b/packages/modules/quick-connect/backend/src/quick-connect.module.ts similarity index 100% rename from packages/cli/src/modules/quick-connect/quick-connect.module.ts rename to packages/modules/quick-connect/backend/src/quick-connect.module.ts diff --git a/packages/cli/src/modules/quick-connect/quick-connect.service.ts b/packages/modules/quick-connect/backend/src/quick-connect.service.ts similarity index 100% rename from packages/cli/src/modules/quick-connect/quick-connect.service.ts rename to packages/modules/quick-connect/backend/src/quick-connect.service.ts diff --git a/packages/modules/quick-connect/backend/test/setup-test-folder.ts b/packages/modules/quick-connect/backend/test/setup-test-folder.ts new file mode 100644 index 00000000000..81fa8e51244 --- /dev/null +++ b/packages/modules/quick-connect/backend/test/setup-test-folder.ts @@ -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, +}); diff --git a/packages/modules/quick-connect/backend/tsconfig.build.json b/packages/modules/quick-connect/backend/tsconfig.build.json new file mode 100644 index 00000000000..ac376c250d1 --- /dev/null +++ b/packages/modules/quick-connect/backend/tsconfig.build.json @@ -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"] +} diff --git a/packages/modules/quick-connect/backend/tsconfig.json b/packages/modules/quick-connect/backend/tsconfig.json new file mode 100644 index 00000000000..b0b3a100a5d --- /dev/null +++ b/packages/modules/quick-connect/backend/tsconfig.json @@ -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"] +} diff --git a/packages/modules/quick-connect/backend/vitest.config.ts b/packages/modules/quick-connect/backend/vitest.config.ts new file mode 100644 index 00000000000..64500379e9b --- /dev/null +++ b/packages/modules/quick-connect/backend/vitest.config.ts @@ -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/**'], + }, + }, +); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e9f30f956c..52ce8fe1da0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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':