diff --git a/OWNERS b/OWNERS index d7826a240a1..36a909af3c7 100644 --- a/OWNERS +++ b/OWNERS @@ -118,7 +118,7 @@ packages/cli/src/modules/data-table/ @n8n-io/knowledge packages/cli/src/evaluation.ee/ @n8n-io/ai-trust 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/modules/workflow-builder/ @n8n-io/ai-assistant packages/cli/src/modules/mcp/ @n8n-io/context packages/modules/quick-connect/ @n8n-io/ai packages/cli/src/modules/chat-hub/ @n8n-io/ai-assistant diff --git a/packages/cli/package.json b/packages/cli/package.json index fdc0f7d7244..3d6122ae9a3 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -159,6 +159,7 @@ "@n8n/backend-common": "workspace:*", "@n8n/backend-module-insights": "workspace:*", "@n8n/backend-module-quick-connect": "workspace:*", + "@n8n/backend-module-workflow-builder": "workspace:*", "@n8n/backend-network": "workspace:*", "@n8n/backend-services": "workspace:*", "@n8n/blob-storage": "workspace:*", diff --git a/packages/cli/src/modules/__tests__/workflow-builder-package.test.ts b/packages/cli/src/modules/__tests__/workflow-builder-package.test.ts new file mode 100644 index 00000000000..eec502ef051 --- /dev/null +++ b/packages/cli/src/modules/__tests__/workflow-builder-package.test.ts @@ -0,0 +1,21 @@ +import { WorkflowBuilderSession } from '@n8n/backend-module-workflow-builder'; +import { ModuleMetadata } from '@n8n/decorators'; +import { Container } from '@n8n/di'; + +import { packagedModules } from '../modules.manifest'; + +describe('packaged Workflow Builder', () => { + it('loads the package entrypoint through the CLI manifest', async () => { + const metadata = Container.get(ModuleMetadata); + expect(metadata.get('workflow-builder')).toBeUndefined(); + + const load = packagedModules['workflow-builder']; + expect(load).toBeTypeOf('function'); + await load?.(); + + const entry = metadata.get('workflow-builder'); + expect(entry?.instanceTypes).toEqual(['main']); + const module = entry && Container.get(entry.class); + expect(await module?.entities?.()).toEqual([WorkflowBuilderSession]); + }); +}); diff --git a/packages/cli/src/modules/modules.manifest.ts b/packages/cli/src/modules/modules.manifest.ts index 6142d7b1867..1fd7fe6499c 100644 --- a/packages/cli/src/modules/modules.manifest.ts +++ b/packages/cli/src/modules/modules.manifest.ts @@ -7,4 +7,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'), + 'workflow-builder': async () => await import('@n8n/backend-module-workflow-builder/module'), }; diff --git a/packages/cli/src/services/__tests__/ai-workflow-builder.service.test.ts b/packages/cli/src/services/__tests__/ai-workflow-builder.service.test.ts index a2ee89c2e07..c343a79180c 100644 --- a/packages/cli/src/services/__tests__/ai-workflow-builder.service.test.ts +++ b/packages/cli/src/services/__tests__/ai-workflow-builder.service.test.ts @@ -1,6 +1,7 @@ import type { Mock, MockedClass } from 'vitest'; import { AiWorkflowBuilderService } from '@n8n/ai-workflow-builder'; import type { Logger } from '@n8n/backend-common'; +import { WorkflowBuilderSessionRepository } from '@n8n/backend-module-workflow-builder'; import type { HttpTransport, OutboundHttp, SsrfProtectionService } from '@n8n/backend-network'; import type { GlobalConfig, SsrfProtectionConfig } from '@n8n/config'; import { AiAssistantClient } from '@n8n_io/ai-assistant-sdk'; @@ -14,7 +15,6 @@ import { join } from 'node:path'; import type { License } from '@/license'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; -import type { WorkflowBuilderSessionRepository } from '@/modules/workflow-builder'; import type { Push } from '@/push'; import { WorkflowBuilderService } from '@/services/ai-workflow-builder.service'; import type { DynamicNodeParametersService } from '@/services/dynamic-node-parameters.service'; @@ -138,6 +138,14 @@ describe('WorkflowBuilderService', () => { }); describe('constructor', () => { + it('uses the public repository constructor for dependency injection', () => { + const dependencies: unknown[] = Reflect.getMetadata( + 'design:paramtypes', + WorkflowBuilderService, + ); + expect(dependencies).toContain(WorkflowBuilderSessionRepository); + }); + it('should initialize without creating the service immediately', () => { expect(MockedAiWorkflowBuilderService).not.toHaveBeenCalled(); }); diff --git a/packages/cli/src/services/ai-workflow-builder.service.ts b/packages/cli/src/services/ai-workflow-builder.service.ts index b2d7aa95bb3..c1c61dbea53 100644 --- a/packages/cli/src/services/ai-workflow-builder.service.ts +++ b/packages/cli/src/services/ai-workflow-builder.service.ts @@ -5,6 +5,7 @@ import { ChatPayload, } from '@n8n/ai-workflow-builder'; import { Logger } from '@n8n/backend-common'; +import { WorkflowBuilderSessionRepository } from '@n8n/backend-module-workflow-builder'; import { OutboundHttp, SsrfProtectionService } from '@n8n/backend-network'; import { GlobalConfig, SsrfProtectionConfig } from '@n8n/config'; import { BUILTIN_NODES_PACKAGES } from '@n8n/constants'; @@ -24,7 +25,6 @@ import * as path from 'path'; import { N8N_VERSION } from '@/constants'; import { License } from '@/license'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; -import { WorkflowBuilderSessionRepository } from '@/modules/workflow-builder'; import { Push } from '@/push'; import { DynamicNodeParametersService } from '@/services/dynamic-node-parameters.service'; import { UrlService } from '@n8n/backend-services'; diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 56e54f4a184..727e50edc5c 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -42,6 +42,7 @@ { "path": "../@n8n/backend-services/tsconfig.build.json" }, { "path": "../modules/insights/backend/tsconfig.build.json" }, { "path": "../modules/quick-connect/backend/tsconfig.build.json" }, + { "path": "../modules/workflow-builder/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/cli/vitest.config.base.ts b/packages/cli/vitest.config.base.ts index 4172ddcd9c0..e6b10bd1edb 100644 --- a/packages/cli/vitest.config.base.ts +++ b/packages/cli/vitest.config.base.ts @@ -30,6 +30,14 @@ const alias = { __dirname, '../modules/insights/backend/src/index.ts', ), + '@n8n/backend-module-workflow-builder/module': path.resolve( + __dirname, + '../modules/workflow-builder/backend/src/workflow-builder.module.ts', + ), + '@n8n/backend-module-workflow-builder': path.resolve( + __dirname, + '../modules/workflow-builder/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'), diff --git a/packages/modules/workflow-builder/backend/README.md b/packages/modules/workflow-builder/backend/README.md new file mode 100644 index 00000000000..b0dd0149a86 --- /dev/null +++ b/packages/modules/workflow-builder/backend/README.md @@ -0,0 +1,17 @@ +# Workflow Builder backend + +This package stores Workflow Builder sessions. It owns the session entity and repository. + +Import `WorkflowBuilderSession`, `IWorkflowBuilderSession`, and +`WorkflowBuilderSessionRepository` from `@n8n/backend-module-workflow-builder`. +The public API does not register the backend module. + +The CLI manifest loads `@n8n/backend-module-workflow-builder/module` on demand. +The module registers its entity before the database starts. Migrations stay in `@n8n/db`. + +Run `pnpm build > build.log 2>&1` before you use the package from the CLI. The build writes JavaScript and +declarations to `dist`. It excludes tests. + +Run `pnpm test --maxWorkers=1` for unit tests and SQLite persistence tests. +Run `pnpm test:postgres --maxWorkers=1` for persistence tests with the shared PostgreSQL harness. +The PostgreSQL harness needs Docker. diff --git a/packages/modules/workflow-builder/backend/oxlint.config.mts b/packages/modules/workflow-builder/backend/oxlint.config.mts new file mode 100644 index 00000000000..23a0f5690f6 --- /dev/null +++ b/packages/modules/workflow-builder/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/workflow-builder/backend/package.json b/packages/modules/workflow-builder/backend/package.json new file mode 100644 index 00000000000..eec31946f89 --- /dev/null +++ b/packages/modules/workflow-builder/backend/package.json @@ -0,0 +1,71 @@ +{ + "name": "@n8n/backend-module-workflow-builder", + "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/workflow-builder.module.d.ts", + "import": "./dist/workflow-builder.module.js", + "require": "./dist/workflow-builder.module.js" + } + }, + "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", + "test:postgres": "N8N_LOG_LEVEL=silent vitest run --config vitest.config.postgres.ts" + }, + "dependencies": { + "@n8n/ai-workflow-builder": "workspace:*", + "@n8n/db": "workspace:*", + "@n8n/decorators": "workspace:*", + "@n8n/di": "workspace:*", + "@n8n/typeorm": "workspace:*" + }, + "peerDependencies": { + "@langchain/core": "catalog:" + }, + "devDependencies": { + "@langchain/core": "catalog:", + "@n8n/backend-common": "workspace:*", + "@n8n/backend-test-utils": "workspace:*", + "@n8n/oxlint-config": "workspace:*", + "@n8n/typescript-config": "workspace:*", + "@n8n/vitest-config": "workspace:*", + "n8n-containers": "workspace:*", + "oxlint": "catalog:", + "oxlint-tsgolint": "catalog:", + "typescript": "catalog:typescript", + "vite": "catalog:", + "vitest": "catalog:", + "vitest-mock-extended": "catalog:" + }, + "license": "LicenseRef-n8n-sustainable-use", + "typesVersions": { + "*": { + "module": [ + "dist/workflow-builder.module.d.ts" + ] + } + } +} diff --git a/packages/modules/workflow-builder/backend/src/__tests__/workflow-builder-session.integration.test.ts b/packages/modules/workflow-builder/backend/src/__tests__/workflow-builder-session.integration.test.ts new file mode 100644 index 00000000000..63e59439797 --- /dev/null +++ b/packages/modules/workflow-builder/backend/src/__tests__/workflow-builder-session.integration.test.ts @@ -0,0 +1,113 @@ +import { AIMessage, HumanMessage } from '@langchain/core/messages'; +import { ModuleRegistry } from '@n8n/backend-common'; +import { createWorkflow, testDb, testModules } from '@n8n/backend-test-utils'; +import { DbConnection, User, UserRepository, WorkflowEntity, WorkflowRepository } from '@n8n/db'; +import { Container } from '@n8n/di'; +import { DataSource } from '@n8n/typeorm'; + +import { WorkflowBuilderSession, WorkflowBuilderSessionRepository } from '../index'; + +let repository: WorkflowBuilderSessionRepository; +let dataSource: DataSource; + +beforeAll(async () => { + await testModules.loadModules(['workflow-builder'], { + 'workflow-builder': async () => await import('../workflow-builder.module.js'), + }); + expect(Container.get(ModuleRegistry).entities).toContain(WorkflowBuilderSession); + expect(Container.get(DbConnection).connectionState.connected).toBe(false); + await testDb.init(); + dataSource = Container.get(DataSource); + repository = Container.get(WorkflowBuilderSessionRepository); +}); + +beforeEach(async () => { + await repository.delete({}); +}); + +afterAll(async () => { + await testDb.terminate(); +}); + +async function createSessionOwner() { + const user = await Container.get(UserRepository).save({ role: { slug: 'global:owner' } }); + const workflow = await createWorkflow(); + return { user, workflow, threadId: `workflow-${workflow.id}-user-${user.id}` }; +} + +describe('packaged session persistence', () => { + it('resolves relations to the shared database entities', () => { + const metadata = dataSource.getMetadata(WorkflowBuilderSession); + expect(metadata.tableNameWithoutPrefix).toBe('workflow_builder_session'); + expect(metadata.findRelationWithPropertyPath('workflow')?.inverseEntityMetadata.target).toBe( + WorkflowEntity, + ); + expect(metadata.findRelationWithPropertyPath('user')?.inverseEntityMetadata.target).toBe(User); + }); + + it('saves, restores, and updates one session for both thread formats', async () => { + const { user, workflow, threadId } = await createSessionOwner(); + const messages = [new HumanMessage('Build a workflow'), new AIMessage('Workflow ready')]; + await repository.saveSession(threadId, { + messages, + previousSummary: 'Workflow summary', + updatedAt: new Date(), + activeVersionCardId: 'version-card', + resumeAfterRestoreMessageId: 'resume-message', + }); + + expect(await repository.getSession(`${threadId}-code`)).toEqual({ + messages, + previousSummary: 'Workflow summary', + updatedAt: expect.any(Date), + activeVersionCardId: 'version-card', + resumeAfterRestoreMessageId: 'resume-message', + }); + const original = await repository.findOneByOrFail({ workflowId: workflow.id, userId: user.id }); + expect(original.id).toMatch(/^[0-9a-f-]{36}$/); + expect(original.createdAt).toBeInstanceOf(Date); + expect(original.updatedAt).toBeInstanceOf(Date); + expect(original.messages.map((message) => message.type)).toEqual(['human', 'ai']); + + await repository.saveSession(`${threadId}-code`, { + messages: [new HumanMessage('Update the workflow')], + updatedAt: new Date(), + }); + expect(await repository.count()).toBe(1); + expect(await repository.findOneByOrFail({ id: original.id })).toMatchObject({ + createdAt: original.createdAt, + previousSummary: null, + activeVersionCardId: null, + resumeAfterRestoreMessageId: null, + }); + expect(await repository.getSession(threadId)).toMatchObject({ + messages: [new HumanMessage('Update the workflow')], + previousSummary: undefined, + }); + + await repository.deleteSession(`${threadId}-code`); + expect(await repository.getSession(threadId)).toBeNull(); + }); + + it('generates an ID and enforces the workflow and user uniqueness rule', async () => { + const { user, workflow } = await createSessionOwner(); + const values = { workflowId: workflow.id, userId: user.id, messages: [] }; + const entity = await repository.save(repository.create(values)); + expect(entity.id).toMatch(/^[0-9a-f-]{36}$/); + await expect(repository.save(repository.create(values))).rejects.toThrow(/unique/i); + }); + + it.each(['workflow', 'user'] as const)( + 'removes sessions when the %s is removed', + async (owner) => { + const { user, workflow, threadId } = await createSessionOwner(); + await repository.saveSession(threadId, { messages: [], updatedAt: new Date() }); + if (owner === 'workflow') { + await Container.get(WorkflowRepository).delete(workflow.id); + } else { + await Container.get(UserRepository).delete(user.id); + } + expect(await repository.getSession(threadId)).toBeNull(); + }, + ); +}); diff --git a/packages/cli/src/modules/workflow-builder/__tests__/workflow-builder-session.repository.test.ts b/packages/modules/workflow-builder/backend/src/__tests__/workflow-builder-session.repository.test.ts similarity index 93% rename from packages/cli/src/modules/workflow-builder/__tests__/workflow-builder-session.repository.test.ts rename to packages/modules/workflow-builder/backend/src/__tests__/workflow-builder-session.repository.test.ts index 62cd5274a3c..2a17e53731c 100644 --- a/packages/cli/src/modules/workflow-builder/__tests__/workflow-builder-session.repository.test.ts +++ b/packages/modules/workflow-builder/backend/src/__tests__/workflow-builder-session.repository.test.ts @@ -1,7 +1,7 @@ import { AIMessage, HumanMessage } from '@langchain/core/messages'; -import { mockClear } from 'vitest-mock-extended'; - -import { mockEntityManager } from '@test/mocking'; +import { mockInstance } from '@n8n/backend-test-utils'; +import { DataSource, EntityManager, type EntityMetadata } from '@n8n/typeorm'; +import { mock, mockClear } from 'vitest-mock-extended'; import { WorkflowBuilderSession } from '../workflow-builder-session.entity'; @@ -25,7 +25,12 @@ vi.mock('@n8n/ai-workflow-builder', () => ({ import { WorkflowBuilderSessionRepository } from '../workflow-builder-session.repository'; describe('WorkflowBuilderSessionRepository', () => { - const entityManager = mockEntityManager(WorkflowBuilderSession); + const entityManager = mockInstance(EntityManager); + const dataSource = mockInstance(DataSource, { + manager: entityManager, + getMetadata: () => mock({ target: WorkflowBuilderSession }), + }); + Object.assign(entityManager, { connection: dataSource }); let repository: WorkflowBuilderSessionRepository; const mockExecute = vi.fn().mockResolvedValue(undefined); @@ -46,11 +51,7 @@ describe('WorkflowBuilderSessionRepository', () => { mockQueryBuilder.values.mockClear().mockReturnThis(); mockQueryBuilder.orUpdate.mockClear().mockReturnThis(); - // Create repository with mocked data source - const mockDataSource = { - manager: entityManager, - }; - repository = new WorkflowBuilderSessionRepository(mockDataSource as never); + repository = new WorkflowBuilderSessionRepository(dataSource); vi.spyOn(repository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as never); }); diff --git a/packages/modules/workflow-builder/backend/src/__tests__/workflow-builder.module.test.ts b/packages/modules/workflow-builder/backend/src/__tests__/workflow-builder.module.test.ts new file mode 100644 index 00000000000..dd258608678 --- /dev/null +++ b/packages/modules/workflow-builder/backend/src/__tests__/workflow-builder.module.test.ts @@ -0,0 +1,15 @@ +import { ModuleMetadata } from '@n8n/decorators'; +import { Container } from '@n8n/di'; + +import { WorkflowBuilderSession } from '../index'; +import { WorkflowBuilderModule } from '../workflow-builder.module'; + +describe('WorkflowBuilderModule', () => { + it('registers the main instance entrypoint with the public entity', async () => { + expect(Container.get(ModuleMetadata).get('workflow-builder')).toMatchObject({ + class: WorkflowBuilderModule, + instanceTypes: ['main'], + }); + expect(await new WorkflowBuilderModule().entities()).toEqual([WorkflowBuilderSession]); + }); +}); diff --git a/packages/modules/workflow-builder/backend/src/index.test.ts b/packages/modules/workflow-builder/backend/src/index.test.ts new file mode 100644 index 00000000000..f9fe4633204 --- /dev/null +++ b/packages/modules/workflow-builder/backend/src/index.test.ts @@ -0,0 +1,17 @@ +import { ModuleMetadata } from '@n8n/decorators'; +import { Container } from '@n8n/di'; + +describe('public API', () => { + it('does not register the module entrypoint', async () => { + const metadata = Container.get(ModuleMetadata); + expect(metadata.get('workflow-builder')).toBeUndefined(); + + const publicApi = await import('./index.js'); + + expect(metadata.get('workflow-builder')).toBeUndefined(); + expect(Object.keys(publicApi).sort()).toEqual([ + 'WorkflowBuilderSession', + 'WorkflowBuilderSessionRepository', + ]); + }); +}); diff --git a/packages/cli/src/modules/workflow-builder/index.ts b/packages/modules/workflow-builder/backend/src/index.ts similarity index 74% rename from packages/cli/src/modules/workflow-builder/index.ts rename to packages/modules/workflow-builder/backend/src/index.ts index 829484eed44..ea86c8b6088 100644 --- a/packages/cli/src/modules/workflow-builder/index.ts +++ b/packages/modules/workflow-builder/backend/src/index.ts @@ -3,4 +3,3 @@ export { type IWorkflowBuilderSession, } from './workflow-builder-session.entity'; export { WorkflowBuilderSessionRepository } from './workflow-builder-session.repository'; -export { WorkflowBuilderModule } from './workflow-builder.module'; diff --git a/packages/cli/src/modules/workflow-builder/workflow-builder-session.entity.ts b/packages/modules/workflow-builder/backend/src/workflow-builder-session.entity.ts similarity index 100% rename from packages/cli/src/modules/workflow-builder/workflow-builder-session.entity.ts rename to packages/modules/workflow-builder/backend/src/workflow-builder-session.entity.ts diff --git a/packages/cli/src/modules/workflow-builder/workflow-builder-session.repository.ts b/packages/modules/workflow-builder/backend/src/workflow-builder-session.repository.ts similarity index 100% rename from packages/cli/src/modules/workflow-builder/workflow-builder-session.repository.ts rename to packages/modules/workflow-builder/backend/src/workflow-builder-session.repository.ts diff --git a/packages/cli/src/modules/workflow-builder/workflow-builder.module.ts b/packages/modules/workflow-builder/backend/src/workflow-builder.module.ts similarity index 100% rename from packages/cli/src/modules/workflow-builder/workflow-builder.module.ts rename to packages/modules/workflow-builder/backend/src/workflow-builder.module.ts diff --git a/packages/modules/workflow-builder/backend/test/setup-postgres.ts b/packages/modules/workflow-builder/backend/test/setup-postgres.ts new file mode 100644 index 00000000000..588fd629282 --- /dev/null +++ b/packages/modules/workflow-builder/backend/test/setup-postgres.ts @@ -0,0 +1,6 @@ +import './setup-test-folder'; + +import { createTestDatabaseGlobalSetup } from 'n8n-containers/test-db-global-setup'; + +// Run migrations after the suite registers this package's entity. +export const { setup, teardown } = createTestDatabaseGlobalSetup(); diff --git a/packages/modules/workflow-builder/backend/test/setup-test-folder.ts b/packages/modules/workflow-builder/backend/test/setup-test-folder.ts new file mode 100644 index 00000000000..a1141bf6365 --- /dev/null +++ b/packages/modules/workflow-builder/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-workflow-builder-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/workflow-builder/backend/tsconfig.build.json b/packages/modules/workflow-builder/backend/tsconfig.build.json new file mode 100644 index 00000000000..ac376c250d1 --- /dev/null +++ b/packages/modules/workflow-builder/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/workflow-builder/backend/tsconfig.json b/packages/modules/workflow-builder/backend/tsconfig.json new file mode 100644 index 00000000000..0df0697ff59 --- /dev/null +++ b/packages/modules/workflow-builder/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/workflow-builder/backend/vitest.config.postgres.ts b/packages/modules/workflow-builder/backend/vitest.config.postgres.ts new file mode 100644 index 00000000000..5fa6081e036 --- /dev/null +++ b/packages/modules/workflow-builder/backend/vitest.config.postgres.ts @@ -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'], + }, +}; diff --git a/packages/modules/workflow-builder/backend/vitest.config.ts b/packages/modules/workflow-builder/backend/vitest.config.ts new file mode 100644 index 00000000000..c4fd00ae6d5 --- /dev/null +++ b/packages/modules/workflow-builder/backend/vitest.config.ts @@ -0,0 +1,19 @@ +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) => /\.entity\.ts$/.test(fileName) }), + ], + test: { + pool: 'forks', + setupFiles: ['./test/setup-test-folder.ts'], + exclude: [...configDefaults.exclude, '**/dist/**'], + hookTimeout: 30_000, + }, + }, +); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dcfb25f2e85..fae88f48778 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4835,6 +4835,9 @@ importers: '@n8n/backend-module-quick-connect': specifier: workspace:* version: link:../modules/quick-connect/backend + '@n8n/backend-module-workflow-builder': + specifier: workspace:* + version: link:../modules/workflow-builder/backend '@n8n/backend-network': specifier: workspace:* version: link:../@n8n/backend-network @@ -7719,6 +7722,64 @@ importers: specifier: ^2.2.8 version: 2.2.8(patch_hash=e2aee939ccac8a57fe449bfd92bedd8117841579526217bc39aca26c6b8c317f)(typescript@6.0.2) + packages/modules/workflow-builder/backend: + dependencies: + '@n8n/ai-workflow-builder': + specifier: workspace:* + version: link:../../../@n8n/ai-workflow-builder.ee + '@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/typeorm': + specifier: workspace:* + version: link:../../../@n8n/typeorm + devDependencies: + '@langchain/core': + specifier: 'catalog:' + version: 1.2.8(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.221.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))(openai@6.46.0(@aws-sdk/credential-provider-node@3.972.80)(@smithy/signature-v4@5.7.2)(ws@8.21.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@n8n/backend-common': + specifier: workspace:* + version: link:../../../@n8n/backend-common + '@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 + 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) + packages/node-dev: dependencies: '@n8n/di':