mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-11 22:50:06 +00:00
refactor(core): Enforce TypeORM persistence boundary with Code Health (#40384)
This commit is contained in:
@@ -14,7 +14,8 @@ Flag a diff that **adds** entries to any of these, and ask for the fix instead:
|
||||
| `.code-health-baseline.json` | `@n8n/code-health` violations |
|
||||
| `.boundaries-baseline.json` | `turbo boundaries` issue count |
|
||||
| `packages/quality/testing/playwright/.janitor-baseline.json` | Playwright janitor findings |
|
||||
| `packages/cli/eslint.config.mjs` | the `misplaced-n8n-typeorm-import` and public-API allowlists, each captioned "NEVER add to this list" |
|
||||
| `.code-health-baseline.json` | the `typeorm-persistence-boundary` baseline, which only shrinks as business logic moves queries into repositories |
|
||||
| `packages/cli/eslint.config.mjs` | the public-API allowlists captioned "NEVER add to this list" |
|
||||
| `.code-health-baseline.json` (`lint-config-layering`) | package-wide rule downgrades left in package ESLint configs |
|
||||
|
||||
Removals are the healthy direction and need no comment.
|
||||
|
||||
@@ -1404,6 +1404,650 @@
|
||||
"message": "Override \"langsmith\" duplicates a catalog entry for langsmith — set the override to \"catalog:\" or remove it",
|
||||
"hash": "9ac3dbae3d22"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/binary-data/database.manager.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 2,
|
||||
"message": "packages/cli imports the TypeORM re-export `In` from @n8n/db.",
|
||||
"hash": "a4aeaccef8b6"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/credentials/credential-connection-status-provider.interface.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 1,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "4d287b55d52a"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/credentials/credential-connection-status-proxy.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 2,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "f425e2b10e7f"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/credentials/credential-dependency.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 8,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "abdb9e128ab8"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/credentials/credentials.controller.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 32,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "510cfb224f30"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/credentials/credentials.service.ee.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 12,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "f759a907bfa6"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/credentials/credentials.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 32,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "7f1b3146a6a5"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/executions/execution-persistence.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 7,
|
||||
"message": "packages/cli imports the TypeORM re-export `EntityManager` from @n8n/db.",
|
||||
"hash": "9c7a73941bb9"
|
||||
},
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 10,
|
||||
"message": "packages/cli imports the TypeORM re-export `FindManyOptions` from @n8n/db.",
|
||||
"hash": "2beb2d36dbee"
|
||||
},
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 11,
|
||||
"message": "packages/cli imports the TypeORM re-export `FindOptionsWhere` from @n8n/db.",
|
||||
"hash": "ef8617dcfa5a"
|
||||
},
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 18,
|
||||
"message": "packages/cli imports the TypeORM re-export `In` from @n8n/db.",
|
||||
"hash": "be7b5858aa7c"
|
||||
},
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 18,
|
||||
"message": "packages/cli imports the TypeORM re-export `Not` from @n8n/db.",
|
||||
"hash": "5faea85ebf23"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/permissions.ee/check-access.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 2,
|
||||
"message": "packages/cli imports the TypeORM re-export `EntityManager` from @n8n/db.",
|
||||
"hash": "ffa998a4f0e7"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/scheduling/durable-job-provisioner.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 5,
|
||||
"message": "packages/cli imports the TypeORM re-export `EntityManager` from @n8n/db.",
|
||||
"hash": "7341e70aa4d7"
|
||||
},
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 10,
|
||||
"message": "packages/cli imports the TypeORM re-export `DataSource` from @n8n/db.",
|
||||
"hash": "64f5ed0abb25"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/scheduling/durable-scheduler.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 4,
|
||||
"message": "packages/cli imports the TypeORM re-export `DataSource` from @n8n/db.",
|
||||
"hash": "aac4ac3f74d8"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/services/export.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 4,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "079882e3843e"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/services/folder.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 10,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "b71b84bbde01"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/services/hooks.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 15,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "112edc87061f"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/services/import.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 17,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "5f09782b0c0a"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/services/project.service.ee.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 33,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "5f5086c28a45"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/services/public-api-key.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 15,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "848bd2e47604"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/services/user.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 15,
|
||||
"message": "packages/cli imports the TypeORM re-export `Not` from @n8n/db.",
|
||||
"hash": "afb93b8300ff"
|
||||
},
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 16,
|
||||
"message": "packages/cli imports the TypeORM re-export `In` from @n8n/db.",
|
||||
"hash": "2fd9c6af03c1"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/workflows/workflow-creation.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 5,
|
||||
"message": "packages/cli imports the TypeORM re-export `EntityManager` from @n8n/db.",
|
||||
"hash": "db8f5c7d285b"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/workflows/workflow-finder.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 11,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "81b365efba5a"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/workflows/workflow-validation.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 4,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "fce2fb07a423"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/workflows/workflow.service.ee.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 22,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "1e39cc245dd5"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/workflows/workflow.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 23,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "ba3b76242d76"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/workflows/workflows.controller.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 39,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "63742f55819c"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/commands/import/credentials.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 19,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "a5cc02a1d765"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/evaluation.ee/test-runner/test-runner.service.ee.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 17,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "d34dbe0bf9e7"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/events/relays/telemetry.event-relay.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 8,
|
||||
"message": "packages/cli imports the TypeORM re-export `In` from @n8n/db.",
|
||||
"hash": "d8a744f2e2b8"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/executions/execution-data/db-store.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 1,
|
||||
"message": "packages/cli imports the TypeORM re-export `In` from @n8n/db.",
|
||||
"hash": "c19615f89980"
|
||||
},
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 2,
|
||||
"message": "packages/cli imports the TypeORM re-export `EntityManager` from @n8n/db.",
|
||||
"hash": "47398e9fd892"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/instance-settings-loader/loaders/log-streaming.instance-settings-loader.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 3,
|
||||
"message": "packages/cli imports the TypeORM re-export `EntityManager` from @n8n/db.",
|
||||
"hash": "b6baaab2dc92"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/agents/agent-publish.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 15,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "9857b27c5bb1"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/agents/agent-task.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 8,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "1799d75327d9"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/agents/agents.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 18,
|
||||
"message": "packages/cli imports the TypeORM re-export `In` from @n8n/db.",
|
||||
"hash": "e893390cf6b7"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/agents/instance-ai-builder-delegate.adapter.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 13,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "a2f27310e801"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/breaking-changes/breaking-changes.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 12,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "680ac8daac26"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/chat-hub/chat-hub-agent.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 10,
|
||||
"message": "packages/cli imports the TypeORM re-export `EntityManager` from @n8n/db.",
|
||||
"hash": "0aef6c89993c"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/chat-hub/chat-hub-credentials.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 8,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "6125ed3d7f9b"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/chat-hub/chat-hub-title.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 11,
|
||||
"message": "packages/cli imports the TypeORM re-export `EntityManager` from @n8n/db.",
|
||||
"hash": "57eab6d29eed"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/chat-hub/chat-hub-tool.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 7,
|
||||
"message": "packages/cli imports the TypeORM re-export `EntityManager` from @n8n/db.",
|
||||
"hash": "7069ba20f7cd"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/chat-hub/chat-hub-workflow.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 25,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "7305b701b234"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/chat-hub/chat-hub.attachment.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 3,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "07380799ff46"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/chat-hub/chat-hub.models.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 10,
|
||||
"message": "packages/cli imports the TypeORM re-export `In` from @n8n/db.",
|
||||
"hash": "830f87d4b9eb"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/chat-hub/chat-hub.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 17,
|
||||
"message": "packages/cli imports the TypeORM re-export `EntityManager` from @n8n/db.",
|
||||
"hash": "55126ad50e59"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/chat-hub/chat-hub.settings.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 11,
|
||||
"message": "packages/cli imports the TypeORM re-export `EntityManager` from @n8n/db.",
|
||||
"hash": "ee4b7ca996c4"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/data-table/data-table-ddl.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 3,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "9d6b2c45c02a"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/data-table/data-table.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 18,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "b1aa220b40ab"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/external-secrets.ee/secrets-providers-connections.service.ee.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 20,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "45167590d2e2"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/favorites/favorites.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 12,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "03cdaa860f2a"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/instance-ai/instance-ai.adapter.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 117,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "afbc7f6b5ec7"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/instance-ai/suspended-thread-persistence.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 3,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "5f8f9db2966f"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/log-streaming.ee/log-streaming-destination.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 5,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "f31366dabfeb"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/mcp/mcp-api-key.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 3,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "c2cbaae08659"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/mcp/mcp.settings.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 8,
|
||||
"message": "packages/cli imports the TypeORM re-export `EntityManager` from @n8n/db.",
|
||||
"hash": "6ff9d5453466"
|
||||
},
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 11,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "4c5e31384fc0"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/provisioning.ee/provisioning.service.ee.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 23,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "fd2fa9036a75"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/provisioning.ee/role-mapping-rule.service.ee.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 16,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "d060301c09ad"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/provisioning.ee/role-resolver.service.ee.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 4,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "15464f0f7a2a"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/source-control.ee/source-control-context.factory.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 5,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "cbc873aadc22"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/source-control.ee/source-control-export.service.ee.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 15,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "8e15c3127f0d"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/source-control.ee/source-control-import.service.ee.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 4,
|
||||
"message": "packages/cli imports the TypeORM re-export `FindOptionsWhere` from @n8n/db.",
|
||||
"hash": "2ca6a59e5a14"
|
||||
},
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 29,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "e472973ab769"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/source-control.ee/source-control-scoped.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 12,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "12e1f73d1d57"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/source-control.ee/source-control-status.service.ee.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 7,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "627dc538505e"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/workflow-index/workflow-dependency-query.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 17,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "27681e00a358"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/scheduling/poll-trigger-node/poll-trigger-job-registrar.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 4,
|
||||
"message": "packages/cli imports the TypeORM re-export `EntityManager` from @n8n/db.",
|
||||
"hash": "faaf4ec6e197"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/scheduling/schedule-trigger-node/schedule-trigger-job-registrar.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 4,
|
||||
"message": "packages/cli imports the TypeORM re-export `EntityManager` from @n8n/db.",
|
||||
"hash": "795cfafd58f5"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/workflows/workflow-history/workflow-history.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 12,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "4405da700deb"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/agents/integrations/n8n-memory.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 45,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "3f27e7921159"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/data-table/utils/sql-utils.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 10,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "9780aefcf874"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/dynamic-credentials.ee/services/credential-connection-status.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 4,
|
||||
"message": "packages/cli imports the TypeORM re-export `In` from @n8n/db.",
|
||||
"hash": "00c000824ff4"
|
||||
},
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 13,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "15940453447b"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/dynamic-credentials.ee/services/credential-resolver-workflow.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 1,
|
||||
"message": "packages/cli imports the TypeORM re-export `In` from @n8n/db.",
|
||||
"hash": "e867f51e2b7c"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/dynamic-credentials.ee/services/credential-resolver.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 10,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "9b6ffe4b90ce"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/instance-ai/storage/typeorm-agent-checkpoint-store.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 3,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "5cac6e5f7c07"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/instance-ai/storage/typeorm-agent-memory.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 30,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "eb011718641f"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/instance-ai/storage/typeorm-observation-log-store.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 20,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "e45963a37c9c"
|
||||
}
|
||||
],
|
||||
"packages/cli/src/modules/token-exchange/services/trusted-key.service.ts": [
|
||||
{
|
||||
"rule": "typeorm-persistence-boundary",
|
||||
"line": 6,
|
||||
"message": "Business logic imports TypeORM directly.",
|
||||
"hash": "b44bfdcf5ae3"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,12 +215,16 @@ const children = getChildNodes(workflow.connections, 'NodeName', 'main', 1);
|
||||
|
||||
### Persistence layer & the TypeORM boundary
|
||||
|
||||
TypeORM (`@n8n/typeorm`) must stay in the **persistence layer** — the `@n8n/db`
|
||||
package or a backend module's own `database/` folder (entity/repository files).
|
||||
TypeORM (`@n8n/typeorm`) must stay in the **persistence layer**. The shared
|
||||
`@n8n/db` package and backend persistence adapters can import it. A module can
|
||||
colocate or group its entities and repositories according to its domain.
|
||||
Business logic — services, controllers, handlers, commands, factories — must not
|
||||
import from `@n8n/typeorm` (including `@n8n/typeorm/...` subpaths). In
|
||||
`packages/cli` this is enforced by the `misplaced-n8n-typeorm-import` lint rule;
|
||||
a new import (or an inline `eslint-disable` of the rule) fails CI.
|
||||
import from `@n8n/typeorm` (including `@n8n/typeorm/...` subpaths). The
|
||||
`typeorm-persistence-boundary` Code Health rule recognizes entity and repository
|
||||
declarations. It scans backend packages that depend on `@n8n/db` or
|
||||
`@n8n/typeorm`. Tests, migrations, and explicit helper-only adapters are
|
||||
exceptions. Existing business-logic imports stay in the shrink-only Code Health
|
||||
baseline. A new business-logic import fails CI.
|
||||
|
||||
- **Pattern:** when a query needs operators (`In`, `IsNull`, `LessThan`,
|
||||
`FindOptionsWhere`, …), put it behind a **use-case-named repository method**
|
||||
|
||||
@@ -7,16 +7,10 @@ export default defineConfig(
|
||||
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.',
|
||||
|
||||
@@ -7,16 +7,10 @@ export default defineConfig({
|
||||
options: { typeAware: true },
|
||||
ignorePatterns: ['coverage/**'],
|
||||
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.',
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
PROJECT_EDITOR_ROLE_SLUG,
|
||||
} from '@n8n/permissions';
|
||||
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
import { In } from '@n8n/typeorm';
|
||||
import type { EntityManager } from '@n8n/typeorm';
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DatabaseConfig } from '@n8n/config';
|
||||
import { Service } from '@n8n/di';
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
import { DataSource, QueryFailedError } from '@n8n/typeorm';
|
||||
import type { EntityManager } from '@n8n/typeorm';
|
||||
import { OperationalError } from 'n8n-workflow';
|
||||
|
||||
@@ -6,7 +6,6 @@ import { NoUselessCatchThrowRule } from './no-useless-catch-throw.js';
|
||||
import { NoSkippedTestsRule } from './no-skipped-tests.js';
|
||||
import { NoInterpolationInRegularStringRule } from './no-interpolation-in-regular-string.js';
|
||||
import { NoDynamicImportTemplateRule } from './no-dynamic-import-template.js';
|
||||
import { MisplacedN8nTypeormImportRule } from './misplaced-n8n-typeorm-import.js';
|
||||
import { NoGuardrailDisableRule } from './no-guardrail-disable.js';
|
||||
import { NoTypeUnsafeEventEmitterRule } from './no-type-unsafe-event-emitter.js';
|
||||
import { NoUntypedConfigClassFieldRule } from './no-untyped-config-class-field.js';
|
||||
@@ -44,7 +43,6 @@ export const rules = {
|
||||
'no-skipped-tests': NoSkippedTestsRule,
|
||||
'no-interpolation-in-regular-string': NoInterpolationInRegularStringRule,
|
||||
'no-dynamic-import-template': NoDynamicImportTemplateRule,
|
||||
'misplaced-n8n-typeorm-import': MisplacedN8nTypeormImportRule,
|
||||
'no-guardrail-disable': NoGuardrailDisableRule,
|
||||
'no-type-unsafe-event-emitter': NoTypeUnsafeEventEmitterRule,
|
||||
'no-untyped-config-class-field': NoUntypedConfigClassFieldRule,
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
import { MisplacedN8nTypeormImportRule } from './misplaced-n8n-typeorm-import.js';
|
||||
|
||||
const ruleTester = new RuleTester();
|
||||
|
||||
ruleTester.run('misplaced-n8n-typeorm-import', MisplacedN8nTypeormImportRule, {
|
||||
valid: [
|
||||
// Persistence layer (path contains `@n8n/db`) may import TypeORM freely.
|
||||
{
|
||||
code: "import { In } from '@n8n/typeorm';",
|
||||
filename: '/repo/packages/@n8n/db/src/repositories/foo.repository.ts',
|
||||
},
|
||||
{
|
||||
code: "import { In } from '@n8n/db';",
|
||||
filename: '/repo/packages/@n8n/db/src/index.ts',
|
||||
},
|
||||
// Sanctioned `@n8n/db` exports are not TypeORM re-exports.
|
||||
{
|
||||
code: "import { TransactionRunner, WorkflowRepository, type User } from '@n8n/db';",
|
||||
filename: '/repo/packages/cli/src/services/foo.service.ts',
|
||||
},
|
||||
// Unrelated imports are ignored.
|
||||
{
|
||||
code: "import { something } from 'other-package';",
|
||||
filename: '/repo/packages/cli/src/services/foo.service.ts',
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
// Direct `@n8n/typeorm` import in business logic.
|
||||
{
|
||||
code: "import { In } from '@n8n/typeorm';",
|
||||
filename: '/repo/packages/cli/src/services/foo.service.ts',
|
||||
errors: [{ messageId: 'moveImport' }],
|
||||
},
|
||||
// Subpath import.
|
||||
{
|
||||
code: "import { Foo } from '@n8n/typeorm/browser';",
|
||||
filename: '/repo/packages/cli/src/services/foo.service.ts',
|
||||
errors: [{ messageId: 'moveImport' }],
|
||||
},
|
||||
// Relabeled operator import from `@n8n/db` — one error per guarded symbol.
|
||||
{
|
||||
code: "import { In, Not, WorkflowRepository } from '@n8n/db';",
|
||||
filename: '/repo/packages/cli/src/services/foo.service.ts',
|
||||
errors: [
|
||||
{ messageId: 'noTypeormViaDb', data: { name: 'In' } },
|
||||
{ messageId: 'noTypeormViaDb', data: { name: 'Not' } },
|
||||
],
|
||||
},
|
||||
// Type-only relabel is still the anti-pattern.
|
||||
{
|
||||
code: "import type { FindOptionsWhere, EntityManager } from '@n8n/db';",
|
||||
filename: '/repo/packages/cli/src/services/foo.service.ts',
|
||||
errors: [
|
||||
{ messageId: 'noTypeormViaDb', data: { name: 'FindOptionsWhere' } },
|
||||
{ messageId: 'noTypeormViaDb', data: { name: 'EntityManager' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
import { ESLintUtils } from '@typescript-eslint/utils';
|
||||
|
||||
/**
|
||||
* TypeORM operators and driver types that `@n8n/db` re-exports from `@n8n/typeorm`.
|
||||
* Importing one of these from `@n8n/db` in business logic relabels the dependency
|
||||
* without decoupling it, so it's flagged just like a direct `@n8n/typeorm` import.
|
||||
* Keep in sync with the `@n8n/typeorm` re-export block in `@n8n/db/src/index.ts`.
|
||||
*/
|
||||
const GUARDED_DB_REEXPORTS = new Set([
|
||||
'In',
|
||||
'Like',
|
||||
'MoreThanOrEqual',
|
||||
'Not',
|
||||
'DataSource',
|
||||
'FindManyOptions',
|
||||
'FindOptionsWhere',
|
||||
'EntityManager',
|
||||
]);
|
||||
|
||||
export const MisplacedN8nTypeormImportRule = ESLintUtils.RuleCreator.withoutDocs({
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Ensure `@n8n/typeorm` is imported only from within the `@n8n/db` package.',
|
||||
},
|
||||
messages: {
|
||||
moveImport:
|
||||
'Import `@n8n/typeorm` only in the persistence layer (`@n8n/db` or a module’s `database/` folder). In business logic, add a use-case repository method instead — do not relabel the import to `@n8n/db`.',
|
||||
noTypeormViaDb:
|
||||
'`{{name}}` is a TypeORM operator/driver type re-exported by `@n8n/db`; importing it here relabels the dependency without decoupling. Add a use-case repository method instead of using TypeORM in business logic.',
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
defaultOptions: [],
|
||||
create(context) {
|
||||
if (context.filename.includes('@n8n/db')) return {};
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const source = node.source.value;
|
||||
if (typeof source !== 'string') return;
|
||||
|
||||
if (source === '@n8n/typeorm' || source.startsWith('@n8n/typeorm/')) {
|
||||
context.report({ node, messageId: 'moveImport' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (source === '@n8n/db') {
|
||||
for (const specifier of node.specifiers) {
|
||||
if (
|
||||
specifier.type === 'ImportSpecifier' &&
|
||||
specifier.imported.type === 'Identifier' &&
|
||||
GUARDED_DB_REEXPORTS.has(specifier.imported.name)
|
||||
) {
|
||||
context.report({
|
||||
node: specifier,
|
||||
messageId: 'noTypeormViaDb',
|
||||
data: { name: specifier.imported.name },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
import { NoGuardrailDisableRule } from './no-guardrail-disable.js';
|
||||
import { NoUnsealedCredentialsEntityWriteRule } from './no-unsealed-credentials-entity-write.js';
|
||||
import { NoUnsealedWorkflowEntityWriteRule } from './no-unsealed-workflow-entity-write.js';
|
||||
import { MisplacedN8nTypeormImportRule } from './misplaced-n8n-typeorm-import.js';
|
||||
|
||||
// Register the guarded rules so ESLint can resolve the disable directives under test (an
|
||||
// unknown rule name in a directive would emit an extra "rule not found" error). The `-audit`
|
||||
@@ -11,7 +11,7 @@ const ruleTester = new RuleTester({
|
||||
plugins: {
|
||||
'n8n-local-rules': {
|
||||
rules: {
|
||||
'misplaced-n8n-typeorm-import': MisplacedN8nTypeormImportRule,
|
||||
'no-unsealed-credentials-entity-write': NoUnsealedCredentialsEntityWriteRule,
|
||||
'no-unsealed-workflow-entity-write': NoUnsealedWorkflowEntityWriteRule,
|
||||
'no-unsealed-workflow-entity-write-audit': NoUnsealedWorkflowEntityWriteRule,
|
||||
'no-guardrail-disable': NoGuardrailDisableRule,
|
||||
@@ -23,15 +23,18 @@ const ruleTester = new RuleTester({
|
||||
const options: [{ guarded: Array<{ rule: string; message: string }> }] = [
|
||||
{
|
||||
guarded: [
|
||||
{ rule: 'misplaced-n8n-typeorm-import', message: 'Add the file to the allowlist.' },
|
||||
{ rule: 'no-unsealed-credentials-entity-write', message: 'Use `updateCredentials`.' },
|
||||
{ rule: 'no-unsealed-workflow-entity-write', message: 'Use `updateContent`.' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const typeormHit = {
|
||||
const credentialsHit = {
|
||||
messageId: 'noDisable' as const,
|
||||
data: { rule: 'misplaced-n8n-typeorm-import', message: 'Add the file to the allowlist.' },
|
||||
data: {
|
||||
rule: 'no-unsealed-credentials-entity-write',
|
||||
message: 'Use `updateCredentials`.',
|
||||
},
|
||||
};
|
||||
const sealHit = {
|
||||
messageId: 'noDisable' as const,
|
||||
@@ -39,7 +42,7 @@ const sealHit = {
|
||||
};
|
||||
const blanket = {
|
||||
messageId: 'noBlanketDisable' as const,
|
||||
data: { rules: 'misplaced-n8n-typeorm-import`, `no-unsealed-workflow-entity-write' },
|
||||
data: { rules: 'no-unsealed-credentials-entity-write`, `no-unsealed-workflow-entity-write' },
|
||||
};
|
||||
|
||||
ruleTester.run('no-guardrail-disable', NoGuardrailDisableRule, {
|
||||
@@ -48,8 +51,8 @@ ruleTester.run('no-guardrail-disable', NoGuardrailDisableRule, {
|
||||
{ code: '// eslint-disable-next-line no-console\nconsole.log("x");', options },
|
||||
{ code: '/* eslint-disable no-console, no-debugger */', options },
|
||||
// A plain comment mentioning the rule name is not a disable directive.
|
||||
{ code: '// see misplaced-n8n-typeorm-import for context', options },
|
||||
{ code: '// eslint-disable-next-line-ish misplaced-n8n-typeorm-import', options },
|
||||
{ code: '// see no-unsealed-credentials-entity-write for context', options },
|
||||
{ code: '// eslint-disable-next-line-ish no-unsealed-credentials-entity-write', options },
|
||||
// A guarded name inside the justification or inside another rule ID is not a match.
|
||||
{
|
||||
code: '// eslint-disable-next-line no-console -- mirrors no-unsealed-workflow-entity-write\nconsole.log("x");',
|
||||
@@ -61,7 +64,7 @@ ruleTester.run('no-guardrail-disable', NoGuardrailDisableRule, {
|
||||
},
|
||||
// With no guarded rules configured, nothing is reported.
|
||||
{
|
||||
code: '// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import\nexport const x = 1;',
|
||||
code: '// eslint-disable-next-line n8n-local-rules/no-unsealed-credentials-entity-write\nexport const x = 1;',
|
||||
},
|
||||
{ code: '// eslint-disable-next-line\nexport const x = 1;' },
|
||||
// Ceiling: a blanket directive on the first line of the file leaves no position outside its
|
||||
@@ -71,9 +74,9 @@ ruleTester.run('no-guardrail-disable', NoGuardrailDisableRule, {
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: '// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import\nexport const x = 1;',
|
||||
code: '// eslint-disable-next-line n8n-local-rules/no-unsealed-credentials-entity-write\nexport const x = 1;',
|
||||
options,
|
||||
errors: [typeormHit],
|
||||
errors: [credentialsHit],
|
||||
},
|
||||
{
|
||||
code: '// oxlint-disable-next-line n8n-local-rules/no-unsealed-workflow-entity-write\nexport const x = 1;',
|
||||
@@ -98,9 +101,9 @@ ruleTester.run('no-guardrail-disable', NoGuardrailDisableRule, {
|
||||
errors: [sealHit],
|
||||
},
|
||||
{
|
||||
code: '/* eslint-disable no-console,\n n8n-local-rules/misplaced-n8n-typeorm-import, n8n-local-rules/no-unsealed-workflow-entity-write */',
|
||||
code: '/* eslint-disable no-console,\n n8n-local-rules/no-unsealed-credentials-entity-write, n8n-local-rules/no-unsealed-workflow-entity-write */',
|
||||
options,
|
||||
errors: [typeormHit, sealHit],
|
||||
errors: [credentialsHit, sealHit],
|
||||
},
|
||||
// A directive with no rule list, or one that names this rule, disables the guarded rules too.
|
||||
{ code: '// eslint-disable-next-line\nexport const x = 1;', options, errors: [blanket] },
|
||||
@@ -120,7 +123,7 @@ ruleTester.run('no-guardrail-disable', NoGuardrailDisableRule, {
|
||||
errors: [blanket],
|
||||
},
|
||||
{
|
||||
code: 'export const y = 1;\n/* eslint-disable n8n-local-rules/no-guardrail-disable, n8n-local-rules/misplaced-n8n-typeorm-import */',
|
||||
code: 'export const y = 1;\n/* eslint-disable n8n-local-rules/no-guardrail-disable, n8n-local-rules/no-unsealed-credentials-entity-write */',
|
||||
options,
|
||||
errors: [blanket],
|
||||
},
|
||||
|
||||
+12
-24
@@ -7,37 +7,25 @@ for repo-wide conventions.
|
||||
|
||||
TypeORM belongs in the **persistence layer**, not in business logic.
|
||||
|
||||
**Allowed to import `@n8n/typeorm`** — entity and repository files, including the
|
||||
ones co-located inside `src/modules/**`:
|
||||
|
||||
- `src/databases/**`
|
||||
- a module's `database/entities/**` and `database/repositories/**`
|
||||
- files named `*.entity.ts` or `*.repository.ts` (some modules keep these at the
|
||||
module root)
|
||||
|
||||
These are exempted in `eslint.config.mjs` **by location**, so a genuine `@Entity`
|
||||
or repository class is never flagged — including the few entity files that lack
|
||||
the `.entity.ts` suffix (they live in a `database/entities/` folder).
|
||||
**Allowed to import `@n8n/typeorm`** — files that declare an `@Entity` class or a
|
||||
repository class that extends TypeORM `Repository` or n8n `BaseRepository`.
|
||||
Their folder and filename do not affect the rule. Tests and migration tooling use
|
||||
Code Health exceptions. Helper-only persistence adapters need a narrow,
|
||||
auditable entry in the `typeorm-persistence-boundary` rule.
|
||||
|
||||
**Not allowed** — business logic (services, controllers, public-api handlers,
|
||||
commands, factories) must not import `@n8n/typeorm` or `@n8n/typeorm/...`
|
||||
subpaths. The `misplaced-n8n-typeorm-import` lint rule enforces this; a new
|
||||
import — or an inline `eslint-disable` of the rule — fails CI. The same rule also
|
||||
subpaths. The `typeorm-persistence-boundary` Code Health rule enforces this. A
|
||||
new import fails CI. The same rule also
|
||||
catches the **relabel dodge**: importing a TypeORM operator/driver type (`In`,
|
||||
`Not`, `FindOptionsWhere`, `EntityManager`, …) from `@n8n/db`, which
|
||||
re-exports them from `@n8n/typeorm` — that silences the direct-import check
|
||||
without decoupling anything. Existing leaks of both kinds are tracked in two
|
||||
`files`-scoped allowlists in `eslint.config.mjs` (direct `@n8n/typeorm` imports,
|
||||
and `@n8n/db` relabels) that only ever shrink: never add to them, and never
|
||||
suppress the rule inline.
|
||||
without decoupling anything. Existing leaks of both kinds are tracked in the
|
||||
shrink-only Code Health baseline. Do not add new baseline entries.
|
||||
|
||||
Distinct from that shrink-only ratchet, two files are **permanently** exempted in
|
||||
`eslint.config.mjs` for legitimate TypeORM use outside the persistence tree —
|
||||
these are sanctioned, not migration targets, so don't try to relocate them or
|
||||
suppress the rule:
|
||||
|
||||
- `src/commands/db/revert.ts` — `MigrationExecutor` (CLI migration tooling)
|
||||
- `src/security-audit/security-audit.repository.ts` — `PackagesRepository`
|
||||
Distinct from that shrink-only ratchet, `src/commands/db/revert.ts` is an exact
|
||||
exception for CLI migration tooling. Composition-based repositories use exact
|
||||
exceptions. Repository subclasses need no exception.
|
||||
|
||||
Need an operator query (`In`, `IsNull`, `FindOptionsWhere`, …)? Add a
|
||||
use-case-named repository method (plain parameters, domain-shaped return) rather
|
||||
|
||||
@@ -68,17 +68,11 @@ export default defineConfig(
|
||||
{
|
||||
rules: {
|
||||
'n8n-local-rules/no-dynamic-import-template': 'error',
|
||||
'n8n-local-rules/misplaced-n8n-typeorm-import': 'error',
|
||||
// Ratchets: the allowlists below only shrink, so an inline disable is the one way to add a violation.
|
||||
'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-repository-in-public-api-handler',
|
||||
message: 'Call a service instead of reaching the repository.',
|
||||
@@ -270,139 +264,6 @@ export default defineConfig(
|
||||
'unicorn/filename-case': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// @n8n/typeorm belongs in the persistence layer; exempt entities/repositories.
|
||||
// Path-based (not suffix-only) so entity files without the `.entity.ts` suffix are covered.
|
||||
files: [
|
||||
'./src/databases/**/*.ts',
|
||||
'./src/modules/**/database/entities/**/*.ts',
|
||||
'./src/modules/**/database/repositories/**/*.ts',
|
||||
'./src/modules/**/*.entity.ts',
|
||||
'./src/modules/**/*.repository.ts',
|
||||
'./test/**/*.ts',
|
||||
'./src/**/__tests__/**/*.ts',
|
||||
],
|
||||
rules: {
|
||||
'n8n-local-rules/misplaced-n8n-typeorm-import': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Permanent: legitimate TypeORM use outside the persistence tree. Do not remove.
|
||||
// - db/revert.ts: MigrationExecutor (CLI migration tooling)
|
||||
// - security-audit.repository.ts: PackagesRepository, relocation tracked separately
|
||||
files: ['./src/commands/db/revert.ts', './src/security-audit/security-audit.repository.ts'],
|
||||
rules: {
|
||||
'n8n-local-rules/misplaced-n8n-typeorm-import': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Ratchet allowlist: known @n8n/typeorm leaks pending migration to @n8n/db.
|
||||
// NEVER add to this list — a new leak must fail CI. Entries are removed as each file migrates.
|
||||
files: [
|
||||
// credentials/
|
||||
'./src/credentials/credential-connection-status-provider.interface.ts',
|
||||
'./src/credentials/credential-connection-status-proxy.ts',
|
||||
'./src/credentials/credential-dependency.service.ts',
|
||||
'./src/credentials/credentials.controller.ts',
|
||||
'./src/credentials/credentials.service.ee.ts',
|
||||
'./src/credentials/credentials.service.ts',
|
||||
// workflows/
|
||||
'./src/workflows/workflow-finder.service.ts',
|
||||
'./src/workflows/workflow-history/workflow-history.service.ts',
|
||||
'./src/workflows/workflow-validation.service.ts',
|
||||
'./src/workflows/workflow.service.ee.ts',
|
||||
'./src/workflows/workflow.service.ts',
|
||||
'./src/workflows/workflows.controller.ts',
|
||||
// services/
|
||||
'./src/services/export.service.ts',
|
||||
'./src/services/folder.service.ts',
|
||||
'./src/services/hooks.service.ts',
|
||||
'./src/services/import.service.ts',
|
||||
'./src/services/project.service.ee.ts',
|
||||
'./src/services/public-api-key.service.ts',
|
||||
// commands / controllers / eventbus / evaluation / public-api
|
||||
'./src/commands/import/credentials.ts',
|
||||
'./src/evaluation.ee/test-runner/test-runner.service.ee.ts',
|
||||
// modules/** non-persistence services surfaced by narrowing the exemption
|
||||
'./src/modules/agents/agent-knowledge.service.ts',
|
||||
'./src/modules/agents/agent-publish.service.ts',
|
||||
'./src/modules/agents/agent-task.service.ts',
|
||||
'./src/modules/agents/builder/agents-builder.service.ts',
|
||||
'./src/modules/agents/instance-ai-builder-delegate.adapter.ts',
|
||||
'./src/modules/agents/integrations/n8n-memory.ts',
|
||||
'./src/modules/agents/tools/workflow-tool-workflow-resolver.ts',
|
||||
'./src/modules/breaking-changes/breaking-changes.service.ts',
|
||||
'./src/modules/chat-hub/chat-hub-credentials.service.ts',
|
||||
'./src/modules/chat-hub/chat-hub-workflow.service.ts',
|
||||
'./src/modules/chat-hub/chat-hub.attachment.service.ts',
|
||||
'./src/modules/data-table/data-table-ddl.service.ts',
|
||||
'./src/modules/data-table/data-table.service.ts',
|
||||
'./src/modules/data-table/utils/sql-utils.ts',
|
||||
'./src/modules/dynamic-credentials.ee/services/credential-connection-status.service.ts',
|
||||
'./src/modules/dynamic-credentials.ee/services/credential-resolver.service.ts',
|
||||
'./src/modules/external-secrets.ee/secrets-providers-connections.service.ee.ts',
|
||||
'./src/modules/favorites/favorites.service.ts',
|
||||
'./src/modules/instance-ai/instance-ai.adapter.service.ts',
|
||||
'./src/modules/instance-ai/mcp/instance-ai-mcp-registry.service.ts',
|
||||
'./src/modules/instance-ai/storage/typeorm-agent-checkpoint-store.ts',
|
||||
'./src/modules/instance-ai/storage/typeorm-agent-memory.ts',
|
||||
'./src/modules/instance-ai/storage/typeorm-observation-log-store.ts',
|
||||
'./src/modules/instance-ai/suspended-thread-persistence.service.ts',
|
||||
'./src/modules/log-streaming.ee/log-streaming-destination.service.ts',
|
||||
'./src/modules/mcp/mcp-api-key.service.ts',
|
||||
'./src/modules/mcp/mcp.settings.service.ts',
|
||||
'./src/modules/oauth-jwe/oauth-jwe-key.service.ts',
|
||||
'./src/modules/provisioning.ee/provisioning.service.ee.ts',
|
||||
'./src/modules/provisioning.ee/role-mapping-rule.service.ee.ts',
|
||||
'./src/modules/provisioning.ee/role-resolver.service.ee.ts',
|
||||
'./src/modules/source-control.ee/source-control-context.factory.ts',
|
||||
'./src/modules/source-control.ee/source-control-export.service.ee.ts',
|
||||
'./src/modules/source-control.ee/source-control-import.service.ee.ts',
|
||||
'./src/modules/source-control.ee/source-control-scoped.service.ts',
|
||||
'./src/modules/source-control.ee/source-control-status.service.ee.ts',
|
||||
'./src/modules/token-exchange/services/trusted-key.service.ts',
|
||||
'./src/modules/workflow-index/workflow-dependency-query.service.ts',
|
||||
],
|
||||
rules: {
|
||||
'n8n-local-rules/misplaced-n8n-typeorm-import': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Ratchet allowlist: known relabel leaks — business logic importing a TypeORM
|
||||
// operator/driver type (`In`, `Not`, `EntityManager`, `FindOptionsWhere`, …) from
|
||||
// `@n8n/db` instead of `@n8n/typeorm`. Same rule, same shrink-only contract:
|
||||
// NEVER add to this list — a new relabel must fail CI. Entries removed as each file
|
||||
// drops TypeORM in favor of a use-case repository method.
|
||||
files: [
|
||||
'./src/binary-data/database.manager.ts',
|
||||
'./src/events/relays/telemetry.event-relay.ts',
|
||||
'./src/executions/execution-data/db-store.ts',
|
||||
'./src/executions/execution-persistence.ts',
|
||||
'./src/executions/execution-recovery.service.ts',
|
||||
'./src/instance-settings-loader/loaders/log-streaming.instance-settings-loader.ts',
|
||||
'./src/modules/agents/agents.service.ts',
|
||||
'./src/modules/chat-hub/chat-hub-agent.service.ts',
|
||||
'./src/modules/chat-hub/chat-hub-title.service.ts',
|
||||
'./src/modules/chat-hub/chat-hub-tool.service.ts',
|
||||
'./src/modules/chat-hub/chat-hub.models.service.ts',
|
||||
'./src/modules/chat-hub/chat-hub.service.ts',
|
||||
'./src/modules/chat-hub/chat-hub.settings.service.ts',
|
||||
'./src/modules/dynamic-credentials.ee/services/credential-resolver-workflow.service.ts',
|
||||
'./src/permissions.ee/check-access.ts',
|
||||
'./src/scheduling/durable-job-provisioner.ts',
|
||||
'./src/scheduling/durable-scheduler.ts',
|
||||
'./src/scheduling/poll-trigger-node/poll-trigger-job-registrar.ts',
|
||||
'./src/scheduling/schedule-trigger-node/schedule-trigger-job-registrar.ts',
|
||||
'./src/security-audit/risk-reporters/credentials-risk-reporter.ts',
|
||||
'./src/services/role-cache.service.ts',
|
||||
'./src/services/role.service.ts',
|
||||
'./src/services/user.service.ts',
|
||||
'./src/workflows/workflow-creation.service.ts',
|
||||
],
|
||||
rules: {
|
||||
'n8n-local-rules/misplaced-n8n-typeorm-import': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Sanctioned `@OnLeaderTakeover` users. Permanent, but additions need review:
|
||||
// the system task runner itself, services that hold live resources on the
|
||||
|
||||
@@ -67,17 +67,11 @@ export default defineConfig({
|
||||
ignorePatterns: ['scripts/**/*.mjs', 'vitest.*.ts', 'coverage/**'],
|
||||
rules: {
|
||||
'n8n-local-rules/no-dynamic-import-template': 'error',
|
||||
'n8n-local-rules/misplaced-n8n-typeorm-import': 'error',
|
||||
// Ratchets: the allowlists below only shrink, so an inline disable is the one way to add a violation.
|
||||
'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-repository-in-public-api-handler',
|
||||
message: 'Call a service instead of reaching the repository.',
|
||||
@@ -270,142 +264,6 @@ export default defineConfig({
|
||||
'unicorn/filename-case': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// @n8n/typeorm belongs in the persistence layer; exempt entities/repositories.
|
||||
// Path-based (not suffix-only) so entity files without the `.entity.ts` suffix are covered.
|
||||
files: [
|
||||
'./src/databases/**/*.ts',
|
||||
'./src/modules/**/database/entities/**/*.ts',
|
||||
'./src/modules/**/database/repositories/**/*.ts',
|
||||
'./src/modules/**/*.entity.ts',
|
||||
'./src/modules/**/*.repository.ts',
|
||||
'./test/**/*.ts',
|
||||
'./src/**/__tests__/**/*.ts',
|
||||
],
|
||||
rules: {
|
||||
'n8n-local-rules/misplaced-n8n-typeorm-import': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Permanent: legitimate TypeORM use outside the persistence tree. Do not remove.
|
||||
// - db/revert.ts: MigrationExecutor (CLI migration tooling)
|
||||
// - security-audit.repository.ts: PackagesRepository, relocation tracked separately
|
||||
files: ['./src/commands/db/revert.ts', './src/security-audit/security-audit.repository.ts'],
|
||||
rules: {
|
||||
'n8n-local-rules/misplaced-n8n-typeorm-import': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Ratchet allowlist: known @n8n/typeorm leaks pending migration to @n8n/db.
|
||||
// NEVER add to this list — a new leak must fail CI. Entries are removed as each file migrates.
|
||||
files: [
|
||||
// credentials/
|
||||
'./src/credentials-helper.ts',
|
||||
'./src/credentials/credential-connection-status-provider.interface.ts',
|
||||
'./src/credentials/credential-connection-status-proxy.ts',
|
||||
'./src/credentials/credential-dependency.service.ts',
|
||||
'./src/credentials/credentials.controller.ts',
|
||||
'./src/credentials/credentials.service.ee.ts',
|
||||
'./src/credentials/credentials.service.ts',
|
||||
// workflows/
|
||||
'./src/workflows/workflow-finder.service.ts',
|
||||
'./src/workflows/workflow-history/workflow-history.service.ts',
|
||||
'./src/workflows/workflow-validation.service.ts',
|
||||
'./src/workflows/workflow.service.ee.ts',
|
||||
'./src/workflows/workflow.service.ts',
|
||||
'./src/workflows/workflows.controller.ts',
|
||||
// services/
|
||||
'./src/services/export.service.ts',
|
||||
'./src/services/folder.service.ts',
|
||||
'./src/services/hooks.service.ts',
|
||||
'./src/services/import.service.ts',
|
||||
'./src/services/ownership-transfer/ownership-transfer-handler.registry.ts',
|
||||
'./src/services/project.service.ee.ts',
|
||||
'./src/services/public-api-key.service.ts',
|
||||
// commands / controllers / eventbus / evaluation / public-api
|
||||
'./src/commands/import/credentials.ts',
|
||||
'./src/evaluation.ee/test-runner/test-runner.service.ee.ts',
|
||||
// modules/** non-persistence services surfaced by narrowing the exemption
|
||||
'./src/modules/agents/agent-knowledge.service.ts',
|
||||
'./src/modules/agents/agent-publish.service.ts',
|
||||
'./src/modules/agents/agent-task.service.ts',
|
||||
'./src/modules/agents/builder/agents-builder.service.ts',
|
||||
'./src/modules/agents/instance-ai-builder-delegate.adapter.ts',
|
||||
'./src/modules/agents/integrations/n8n-memory.ts',
|
||||
'./src/modules/agents/tools/workflow-tool-workflow-resolver.ts',
|
||||
'./src/modules/breaking-changes/breaking-changes.service.ts',
|
||||
'./src/modules/chat-hub/chat-hub-credentials.service.ts',
|
||||
'./src/modules/chat-hub/chat-hub-workflow.service.ts',
|
||||
'./src/modules/chat-hub/chat-hub.attachment.service.ts',
|
||||
'./src/modules/data-table/data-table-ddl.service.ts',
|
||||
'./src/modules/data-table/data-table.service.ts',
|
||||
'./src/modules/data-table/utils/sql-utils.ts',
|
||||
'./src/modules/dynamic-credentials.ee/services/credential-connection-status.service.ts',
|
||||
'./src/modules/dynamic-credentials.ee/services/credential-resolver.service.ts',
|
||||
'./src/modules/external-secrets.ee/secrets-providers-connections.service.ee.ts',
|
||||
'./src/modules/favorites/favorites.service.ts',
|
||||
'./src/modules/instance-ai/instance-ai.adapter.service.ts',
|
||||
'./src/modules/instance-ai/mcp/instance-ai-mcp-registry.service.ts',
|
||||
'./src/modules/instance-ai/storage/typeorm-agent-checkpoint-store.ts',
|
||||
'./src/modules/instance-ai/storage/typeorm-agent-memory.ts',
|
||||
'./src/modules/instance-ai/storage/typeorm-observation-log-store.ts',
|
||||
'./src/modules/instance-ai/suspended-thread-persistence.service.ts',
|
||||
'./src/modules/log-streaming.ee/log-streaming-destination.service.ts',
|
||||
'./src/modules/mcp/mcp-api-key.service.ts',
|
||||
'./src/modules/mcp/mcp.settings.service.ts',
|
||||
'./src/modules/oauth-jwe/oauth-jwe-key.service.ts',
|
||||
'./src/modules/provisioning.ee/provisioning.service.ee.ts',
|
||||
'./src/modules/provisioning.ee/role-mapping-rule.service.ee.ts',
|
||||
'./src/modules/provisioning.ee/role-resolver.service.ee.ts',
|
||||
'./src/modules/source-control.ee/source-control-context.factory.ts',
|
||||
'./src/modules/source-control.ee/source-control-export.service.ee.ts',
|
||||
'./src/modules/source-control.ee/source-control-import.service.ee.ts',
|
||||
'./src/modules/source-control.ee/source-control-scoped.service.ts',
|
||||
'./src/modules/source-control.ee/source-control-status.service.ee.ts',
|
||||
'./src/modules/token-exchange/services/trusted-key.service.ts',
|
||||
'./src/modules/workflow-index/workflow-dependency-query.service.ts',
|
||||
],
|
||||
rules: {
|
||||
'n8n-local-rules/misplaced-n8n-typeorm-import': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Ratchet allowlist: known relabel leaks — business logic importing a TypeORM
|
||||
// operator/driver type (`In`, `Not`, `EntityManager`, `FindOptionsWhere`, …) from
|
||||
// `@n8n/db` instead of `@n8n/typeorm`. Same rule, same shrink-only contract:
|
||||
// NEVER add to this list — a new relabel must fail CI. Entries removed as each file
|
||||
// drops TypeORM in favor of a use-case repository method.
|
||||
files: [
|
||||
'./src/binary-data/database.manager.ts',
|
||||
'./src/events/relays/telemetry.event-relay.ts',
|
||||
'./src/executions/execution-data/db-store.ts',
|
||||
'./src/executions/execution-persistence.ts',
|
||||
'./src/executions/execution-recovery.service.ts',
|
||||
'./src/executions/execution.service.ts',
|
||||
'./src/instance-settings-loader/loaders/log-streaming.instance-settings-loader.ts',
|
||||
'./src/modules/agents/agents.service.ts',
|
||||
'./src/modules/chat-hub/chat-hub-agent.service.ts',
|
||||
'./src/modules/chat-hub/chat-hub-title.service.ts',
|
||||
'./src/modules/chat-hub/chat-hub-tool.service.ts',
|
||||
'./src/modules/chat-hub/chat-hub.models.service.ts',
|
||||
'./src/modules/chat-hub/chat-hub.service.ts',
|
||||
'./src/modules/chat-hub/chat-hub.settings.service.ts',
|
||||
'./src/modules/dynamic-credentials.ee/services/credential-resolver-workflow.service.ts',
|
||||
'./src/permissions.ee/check-access.ts',
|
||||
'./src/scheduling/durable-job-provisioner.ts',
|
||||
'./src/scheduling/durable-scheduler.ts',
|
||||
'./src/scheduling/poll-trigger-node/poll-trigger-job-registrar.ts',
|
||||
'./src/scheduling/schedule-trigger-node/schedule-trigger-job-registrar.ts',
|
||||
'./src/security-audit/risk-reporters/credentials-risk-reporter.ts',
|
||||
'./src/services/role-cache.service.ts',
|
||||
'./src/services/role.service.ts',
|
||||
'./src/services/user.service.ts',
|
||||
'./src/workflows/workflow-creation.service.ts',
|
||||
],
|
||||
rules: {
|
||||
'n8n-local-rules/misplaced-n8n-typeorm-import': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Sanctioned `@OnLeaderTakeover` users. Permanent, but additions need review:
|
||||
// the system task runner itself, services that hold live resources on the
|
||||
|
||||
@@ -5,32 +5,4 @@ export default defineConfig({
|
||||
extends: [backendConfig],
|
||||
options: { typeAware: true },
|
||||
ignorePatterns: ['coverage/**', 'dist/**'],
|
||||
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.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'./src/**/*.entity.ts',
|
||||
'./src/**/*.repository.ts',
|
||||
'./src/**/__tests__/**/*.ts',
|
||||
'./src/**/*.test.ts',
|
||||
],
|
||||
rules: {
|
||||
'n8n-local-rules/misplaced-n8n-typeorm-import': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ Scans repository files for structural and dependency-policy violations:
|
||||
- **Hardcoded catalog deps** — dependencies using a pinned version when `pnpm-workspace.yaml` already defines a catalog entry
|
||||
- **Cross-package version drift** — the same dependency appearing in multiple packages with different versions
|
||||
- **Encryption boundary coverage** — every package that depends on `n8n-core` or `@n8n/db` composes the encryption-boundary ESLint config at `error` severity and contains no ESLint directive that silences it
|
||||
- **TypeORM persistence boundary** — business logic cannot import TypeORM directly or through guarded `@n8n/db` re-exports; entity and repository declarations can use any package layout
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { SingleInstanceLibsRule } from './rules/single-instance-libs.rule.js';
|
||||
import { SingleInstanceLockfileRule } from './rules/single-instance-lockfile.rule.js';
|
||||
import { StaleOverridesRule } from './rules/stale-overrides.rule.js';
|
||||
import { SubpathPurityRule } from './rules/subpath-purity.rule.js';
|
||||
import { TypeormPersistenceBoundaryRule } from './rules/typeorm-persistence-boundary.rule.js';
|
||||
import { WorkflowPrTargetSafetyRule } from './rules/workflow-pr-target-safety.rule.js';
|
||||
|
||||
export type { CodeHealthContext } from './context.js';
|
||||
@@ -26,6 +27,7 @@ export { SingleInstanceLockfileRule } from './rules/single-instance-lockfile.rul
|
||||
export { StaleOverridesRule } from './rules/stale-overrides.rule.js';
|
||||
export { SubpathPurityRule } from './rules/subpath-purity.rule.js';
|
||||
export type { SubpathSpec } from './rules/subpath-purity.rule.js';
|
||||
export { TypeormPersistenceBoundaryRule } from './rules/typeorm-persistence-boundary.rule.js';
|
||||
export { WorkflowPrTargetSafetyRule } from './rules/workflow-pr-target-safety.rule.js';
|
||||
|
||||
const defaultRuleSettings: RuleSettingsMap = {
|
||||
@@ -65,6 +67,11 @@ const defaultRuleSettings: RuleSettingsMap = {
|
||||
severity: 'error',
|
||||
options: {},
|
||||
},
|
||||
'typeorm-persistence-boundary': {
|
||||
enabled: true,
|
||||
severity: 'error',
|
||||
options: {},
|
||||
},
|
||||
'lint-config-layering': {
|
||||
enabled: true,
|
||||
severity: 'error',
|
||||
@@ -154,6 +161,7 @@ export function createDefaultRunner(settings?: RuleSettingsMap): RuleRunner<Code
|
||||
runner.registerRule(new SingleInstanceLibsRule());
|
||||
runner.registerRule(new SingleInstanceLockfileRule());
|
||||
runner.registerRule(new EncryptionBoundaryRule());
|
||||
runner.registerRule(new TypeormPersistenceBoundaryRule());
|
||||
runner.registerRule(new LintConfigLayeringRule());
|
||||
runner.registerRule(new StaleOverridesRule());
|
||||
runner.registerRule(new EndpointScopeCoverageRule());
|
||||
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import type { CodeHealthContext } from '../context.js';
|
||||
import { TypeormPersistenceBoundaryRule } from './typeorm-persistence-boundary.rule.js';
|
||||
|
||||
describe('TypeormPersistenceBoundaryRule', () => {
|
||||
let rootDir: string;
|
||||
let rule: TypeormPersistenceBoundaryRule;
|
||||
|
||||
beforeEach(() => {
|
||||
rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'code-health-typeorm-boundary-'));
|
||||
rule = new TypeormPersistenceBoundaryRule();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(rootDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function write(relativePath: string, content: string): void {
|
||||
const fullPath = path.join(rootDir, relativePath);
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, content);
|
||||
}
|
||||
|
||||
function writePackage(name = '@n8n/example'): void {
|
||||
write(
|
||||
'packages/example/package.json',
|
||||
JSON.stringify({
|
||||
name,
|
||||
dependencies: { '@n8n/db': 'workspace:*', '@n8n/typeorm': 'workspace:*' },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function analyze(): Promise<string[]> {
|
||||
const context: CodeHealthContext = { rootDir };
|
||||
const violations = await rule.analyze(context);
|
||||
return violations.map(
|
||||
(violation) =>
|
||||
`${path.relative(rootDir, violation.file)}:${violation.line} ${violation.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
it('rejects direct TypeORM imports from business logic in any folder', async () => {
|
||||
writePackage();
|
||||
write(
|
||||
'packages/example/src/database/workflow.service.ts',
|
||||
"import { In } from '@n8n/typeorm';\nexport class WorkflowService {}\n",
|
||||
);
|
||||
|
||||
expect(await analyze()).toEqual([
|
||||
expect.stringContaining('workflow.service.ts:1 Business logic imports TypeORM directly.'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('accepts named and namespace entity declarations in different layouts', async () => {
|
||||
writePackage();
|
||||
write(
|
||||
'packages/example/src/audit/model.ts',
|
||||
"import { Entity as OrmEntity } from '@n8n/typeorm';\n@OrmEntity()\nexport class Audit {}\n",
|
||||
);
|
||||
write(
|
||||
'packages/example/src/domain/record.ts',
|
||||
"import * as TypeOrm from '@n8n/typeorm';\[email protected]()\nexport class Record {}\n",
|
||||
);
|
||||
|
||||
expect(await analyze()).toEqual([]);
|
||||
});
|
||||
|
||||
it('accepts TypeORM Repository and n8n BaseRepository subclasses', async () => {
|
||||
writePackage();
|
||||
write(
|
||||
'packages/example/src/audit/store.ts',
|
||||
"import { Repository as OrmRepository } from '@n8n/typeorm';\nexport class AuditStore extends OrmRepository<Audit> {}\n",
|
||||
);
|
||||
write(
|
||||
'packages/example/src/domain/store.ts',
|
||||
"import * as TypeOrm from '@n8n/typeorm';\nexport class DomainStore extends TypeOrm.Repository<Audit> {}\n",
|
||||
);
|
||||
write(
|
||||
'packages/example/src/records/store.ts',
|
||||
"import { BaseRepository } from '@n8n/db';\nimport { In } from '@n8n/typeorm';\nexport class RecordStore extends BaseRepository<Record> {}\n",
|
||||
);
|
||||
|
||||
expect(await analyze()).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not infer persistence from a filename or imported repository', async () => {
|
||||
writePackage();
|
||||
write(
|
||||
'packages/example/src/workflow.repository.ts',
|
||||
"import { In } from '@n8n/typeorm';\nimport { WorkflowRepository } from '@n8n/db';\nexport class WorkflowService {}\n",
|
||||
);
|
||||
|
||||
expect(await analyze()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('always rejects guarded TypeORM re-exports from @n8n/db', async () => {
|
||||
writePackage();
|
||||
write(
|
||||
'packages/example/src/audit/store.ts',
|
||||
"import { BaseRepository, In } from '@n8n/db';\nexport class AuditStore extends BaseRepository<Audit> {}\n",
|
||||
);
|
||||
|
||||
expect(await analyze()).toEqual([
|
||||
expect.stringContaining('imports the TypeORM re-export `In` from @n8n/db'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects guarded TypeORM re-exports accessed through an @n8n/db namespace', async () => {
|
||||
writePackage();
|
||||
write(
|
||||
'packages/example/src/workflow.service.ts',
|
||||
"import * as db from '@n8n/db';\nexport const ids = db.In(['1']);\n",
|
||||
);
|
||||
|
||||
expect(await analyze()).toEqual([
|
||||
expect.stringContaining('accesses the TypeORM re-export `In` through the @n8n/db namespace'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('skips tests but scans nested production test and migration folders', async () => {
|
||||
writePackage();
|
||||
const typeormImport = "import { In } from '@n8n/typeorm';\n";
|
||||
write('packages/example/src/audit.test.ts', typeormImport);
|
||||
write('packages/example/src/__tests__/audit.ts', typeormImport);
|
||||
write('packages/example/src/domain/test/query.ts', typeormImport);
|
||||
write('packages/example/src/domain/migrations/query.ts', typeormImport);
|
||||
|
||||
const violations = await analyze();
|
||||
expect(violations).toHaveLength(2);
|
||||
expect(
|
||||
violations.every((violation) => violation.includes('Business logic imports TypeORM')),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('supports explicit migration roots', async () => {
|
||||
writePackage();
|
||||
write(
|
||||
'packages/example/src/database/migrations/1-Init.ts',
|
||||
"import { QueryRunner } from '@n8n/typeorm';\n",
|
||||
);
|
||||
rule.configure({
|
||||
options: { allowedDirectories: ['packages/example/src/database/migrations'] },
|
||||
});
|
||||
|
||||
expect(await analyze()).toEqual([]);
|
||||
});
|
||||
|
||||
it('supports exact exceptions for composition-based adapters', async () => {
|
||||
writePackage();
|
||||
write(
|
||||
'packages/example/src/query-adapter.ts',
|
||||
"import { DataSource } from '@n8n/typeorm';\nexport class QueryAdapter {}\n",
|
||||
);
|
||||
rule.configure({
|
||||
options: { allowedFiles: ['packages/example/src/query-adapter.ts'] },
|
||||
});
|
||||
|
||||
expect(await analyze()).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not scan the @n8n/db persistence package', async () => {
|
||||
writePackage('@n8n/db');
|
||||
write(
|
||||
'packages/example/src/service.ts',
|
||||
"import { In } from '@n8n/typeorm';\nexport class Service {}\n",
|
||||
);
|
||||
|
||||
expect(await analyze()).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not scan the backend test utility package', async () => {
|
||||
writePackage('@n8n/backend-test-utils');
|
||||
write(
|
||||
'packages/example/src/test-db.ts',
|
||||
"import { In } from '@n8n/typeorm';\nexport class TestDb {}\n",
|
||||
);
|
||||
|
||||
expect(await analyze()).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,234 @@
|
||||
import { BaseRule } from '@n8n/rules-engine';
|
||||
import type { Violation } from '@n8n/rules-engine';
|
||||
import { Node, Project, SyntaxKind } from 'ts-morph';
|
||||
import type { ImportDeclaration, SourceFile } from 'ts-morph';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import type { CodeHealthContext } from '../context.js';
|
||||
import {
|
||||
findPackageJsonFiles,
|
||||
parsePackageJson,
|
||||
relativeDir,
|
||||
} from '../utils/package-json-scanner.js';
|
||||
|
||||
const TYPEORM_PACKAGE = '@n8n/typeorm';
|
||||
const DB_PACKAGE = '@n8n/db';
|
||||
const GUARDED_DB_REEXPORTS = new Set([
|
||||
'In',
|
||||
'Like',
|
||||
'MoreThanOrEqual',
|
||||
'Not',
|
||||
'DataSource',
|
||||
'FindManyOptions',
|
||||
'FindOptionsWhere',
|
||||
'EntityManager',
|
||||
]);
|
||||
|
||||
const DEFAULT_ALLOWED_FILES = [
|
||||
// CLI migration tooling owns the TypeORM migration executor.
|
||||
'packages/cli/src/commands/db/revert.ts',
|
||||
// These adapters use composition instead of a repository base class.
|
||||
'packages/cli/src/modules/data-table/data-table-rows.repository.ts',
|
||||
'packages/cli/src/modules/instance-ai/repositories/instance-ai-conversation-history.repository.ts',
|
||||
// Engine v2 keeps its persistence infrastructure and composition root in one package.
|
||||
'packages/@n8n/engine/src/database/create-stores.ts',
|
||||
'packages/@n8n/engine/src/database/data-source.ts',
|
||||
'packages/@n8n/engine/src/database/typeorm-execution-store.ts',
|
||||
'packages/@n8n/engine/src/database/typeorm-execution-view-store.ts',
|
||||
'packages/@n8n/engine/src/database/typeorm-step-store.ts',
|
||||
'packages/@n8n/engine/src/runtime/create-engine-runtime.ts',
|
||||
];
|
||||
const DEFAULT_ALLOWED_DIRECTORIES = [
|
||||
'packages/cli/src/databases/migrations',
|
||||
'packages/@n8n/engine/src/database/migrations',
|
||||
];
|
||||
// This package exists only to build database-backed test fixtures.
|
||||
const DEFAULT_EXEMPT_PACKAGES = ['@n8n/backend-test-utils'];
|
||||
|
||||
const SOURCE_GLOBS = [
|
||||
'src/**/*.ts',
|
||||
'!src/**/*.test.ts',
|
||||
'!src/**/*.spec.ts',
|
||||
'!src/**/__tests__/**',
|
||||
];
|
||||
|
||||
function stringArrayOption(value: unknown, fallback: string[]): string[] {
|
||||
return Array.isArray(value) && value.every((entry): entry is string => typeof entry === 'string')
|
||||
? value
|
||||
: fallback;
|
||||
}
|
||||
|
||||
function normalizedRelativePath(rootDir: string, filePath: string): string {
|
||||
return path.relative(rootDir, filePath).split(path.sep).join('/');
|
||||
}
|
||||
|
||||
function namedImportBindings(declaration: ImportDeclaration, importedName: string): string[] {
|
||||
return declaration
|
||||
.getNamedImports()
|
||||
.filter((specifier) => specifier.getName() === importedName)
|
||||
.map((specifier) => specifier.getAliasNode()?.getText() ?? specifier.getName());
|
||||
}
|
||||
|
||||
function isPersistenceAdapter(file: SourceFile): boolean {
|
||||
const typeormImports = file
|
||||
.getImportDeclarations()
|
||||
.filter((declaration) => declaration.getModuleSpecifierValue() === TYPEORM_PACKAGE);
|
||||
const dbImports = file
|
||||
.getImportDeclarations()
|
||||
.filter((declaration) => declaration.getModuleSpecifierValue() === DB_PACKAGE);
|
||||
|
||||
const entityNames = new Set(
|
||||
typeormImports.flatMap((declaration) => namedImportBindings(declaration, 'Entity')),
|
||||
);
|
||||
const repositoryNames = new Set([
|
||||
...typeormImports.flatMap((declaration) => namedImportBindings(declaration, 'Repository')),
|
||||
...dbImports.flatMap((declaration) => namedImportBindings(declaration, 'BaseRepository')),
|
||||
]);
|
||||
const namespaceNames = new Set(
|
||||
typeormImports.flatMap((declaration) => {
|
||||
const namespace = declaration.getNamespaceImport();
|
||||
return namespace ? [namespace.getText()] : [];
|
||||
}),
|
||||
);
|
||||
|
||||
return file.getClasses().some((cls) => {
|
||||
const hasEntityDecorator = cls.getDecorators().some((decorator) => {
|
||||
const expression = decorator.getExpression();
|
||||
const target = Node.isCallExpression(expression) ? expression.getExpression() : expression;
|
||||
const name = target.getText();
|
||||
return entityNames.has(name) || [...namespaceNames].some((ns) => name === `${ns}.Entity`);
|
||||
});
|
||||
if (hasEntityDecorator) return true;
|
||||
|
||||
const base = cls.getExtends()?.getExpression().getText();
|
||||
return (
|
||||
base !== undefined &&
|
||||
(repositoryNames.has(base) || [...namespaceNames].some((ns) => base === `${ns}.Repository`))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** Enforces the TypeORM persistence boundary without prescribing a package layout. */
|
||||
export class TypeormPersistenceBoundaryRule extends BaseRule<CodeHealthContext> {
|
||||
readonly id = 'typeorm-persistence-boundary';
|
||||
readonly name = 'TypeORM Persistence Boundary';
|
||||
readonly description =
|
||||
'Business logic must not import TypeORM. Entity and repository declarations may import it in any package layout.';
|
||||
readonly severity = 'error' as const;
|
||||
|
||||
async analyze(context: CodeHealthContext): Promise<Violation[]> {
|
||||
const allowedFiles = new Set(
|
||||
stringArrayOption(this.getOptions().allowedFiles, DEFAULT_ALLOWED_FILES),
|
||||
);
|
||||
const allowedDirectories = stringArrayOption(
|
||||
this.getOptions().allowedDirectories,
|
||||
DEFAULT_ALLOWED_DIRECTORIES,
|
||||
);
|
||||
const exemptPackages = new Set(
|
||||
stringArrayOption(this.getOptions().exemptPackages, DEFAULT_EXEMPT_PACKAGES),
|
||||
);
|
||||
const violations: Violation[] = [];
|
||||
|
||||
for (const packageJsonPath of await findPackageJsonFiles(context.rootDir)) {
|
||||
const packageInfo = parsePackageJson(packageJsonPath);
|
||||
if (packageInfo.packageName === DB_PACKAGE || exemptPackages.has(packageInfo.packageName)) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
!packageInfo.deps.some((dependency) =>
|
||||
[TYPEORM_PACKAGE, DB_PACKAGE].includes(dependency.name),
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const packageDir = path.dirname(packageJsonPath);
|
||||
const project = new Project({
|
||||
skipAddingFilesFromTsConfig: true,
|
||||
skipFileDependencyResolution: true,
|
||||
});
|
||||
const packagePath = relativeDir(context.rootDir, packageJsonPath);
|
||||
const patterns = SOURCE_GLOBS.map((glob) => {
|
||||
const prefix = glob.startsWith('!') ? '!' : '';
|
||||
const relativeGlob = prefix ? glob.slice(1) : glob;
|
||||
return `${prefix}${packageDir}/${relativeGlob}`.replaceAll('\\', '/');
|
||||
});
|
||||
|
||||
project.addSourceFilesAtPaths(patterns);
|
||||
for (const file of project.getSourceFiles()) {
|
||||
const relativeFile = normalizedRelativePath(context.rootDir, file.getFilePath());
|
||||
if (
|
||||
allowedFiles.has(relativeFile) ||
|
||||
allowedDirectories.some((directory) => relativeFile.startsWith(`${directory}/`))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
violations.push(...this.analyzeFile(file, packagePath));
|
||||
}
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
private analyzeFile(file: SourceFile, packagePath: string): Violation[] {
|
||||
const violations: Violation[] = [];
|
||||
const adapter = isPersistenceAdapter(file);
|
||||
|
||||
for (const declaration of file.getImportDeclarations()) {
|
||||
const source = declaration.getModuleSpecifierValue();
|
||||
if (source === TYPEORM_PACKAGE || source.startsWith(`${TYPEORM_PACKAGE}/`)) {
|
||||
if (!adapter) {
|
||||
violations.push(
|
||||
this.importViolation(
|
||||
declaration,
|
||||
'Business logic imports TypeORM directly.',
|
||||
'Add a use-case repository method, or move this query into a persistence adapter.',
|
||||
),
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source !== DB_PACKAGE) continue;
|
||||
for (const specifier of declaration.getNamedImports()) {
|
||||
const importedName = specifier.getName();
|
||||
if (!GUARDED_DB_REEXPORTS.has(importedName)) continue;
|
||||
violations.push(
|
||||
this.importViolation(
|
||||
specifier,
|
||||
`${packagePath} imports the TypeORM re-export \`${importedName}\` from @n8n/db.`,
|
||||
'Import TypeORM directly in a persistence adapter. Add a use-case repository method for business logic.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const namespace = declaration.getNamespaceImport()?.getText();
|
||||
if (!namespace) continue;
|
||||
for (const access of file.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)) {
|
||||
if (access.getExpression().getText() !== namespace) continue;
|
||||
const importedName = access.getName();
|
||||
if (!GUARDED_DB_REEXPORTS.has(importedName)) continue;
|
||||
violations.push(
|
||||
this.importViolation(
|
||||
access,
|
||||
`${packagePath} accesses the TypeORM re-export \`${importedName}\` through the @n8n/db namespace.`,
|
||||
'Import TypeORM directly in a persistence adapter. Add a use-case repository method for business logic.',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
private importViolation(node: Node, message: string, suggestion: string): Violation {
|
||||
const position = node.getSourceFile().getLineAndColumnAtPos(node.getStart());
|
||||
return this.createViolation(
|
||||
node.getSourceFile().getFilePath(),
|
||||
position.line,
|
||||
position.column,
|
||||
message,
|
||||
suggestion,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user