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 |
|
| `.code-health-baseline.json` | `@n8n/code-health` violations |
|
||||||
| `.boundaries-baseline.json` | `turbo boundaries` issue count |
|
| `.boundaries-baseline.json` | `turbo boundaries` issue count |
|
||||||
| `packages/quality/testing/playwright/.janitor-baseline.json` | Playwright janitor findings |
|
| `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 |
|
| `.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.
|
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",
|
"message": "Override \"langsmith\" duplicates a catalog entry for langsmith — set the override to \"catalog:\" or remove it",
|
||||||
"hash": "9ac3dbae3d22"
|
"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
|
### Persistence layer & the TypeORM boundary
|
||||||
|
|
||||||
TypeORM (`@n8n/typeorm`) must stay in the **persistence layer** — the `@n8n/db`
|
TypeORM (`@n8n/typeorm`) must stay in the **persistence layer**. The shared
|
||||||
package or a backend module's own `database/` folder (entity/repository files).
|
`@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
|
Business logic — services, controllers, handlers, commands, factories — must not
|
||||||
import from `@n8n/typeorm` (including `@n8n/typeorm/...` subpaths). In
|
import from `@n8n/typeorm` (including `@n8n/typeorm/...` subpaths). The
|
||||||
`packages/cli` this is enforced by the `misplaced-n8n-typeorm-import` lint rule;
|
`typeorm-persistence-boundary` Code Health rule recognizes entity and repository
|
||||||
a new import (or an inline `eslint-disable` of the rule) fails CI.
|
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`,
|
- **Pattern:** when a query needs operators (`In`, `IsNull`, `LessThan`,
|
||||||
`FindOptionsWhere`, …), put it behind a **use-case-named repository method**
|
`FindOptionsWhere`, …), put it behind a **use-case-named repository method**
|
||||||
|
|||||||
@@ -7,16 +7,10 @@ export default defineConfig(
|
|||||||
backendConfig,
|
backendConfig,
|
||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
'n8n-local-rules/misplaced-n8n-typeorm-import': 'error',
|
|
||||||
'n8n-local-rules/no-guardrail-disable': [
|
'n8n-local-rules/no-guardrail-disable': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
guarded: [
|
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',
|
rule: 'no-unsealed-workflow-entity-write',
|
||||||
message: 'Route the write through a token-gated `WorkflowRepository` method.',
|
message: 'Route the write through a token-gated `WorkflowRepository` method.',
|
||||||
|
|||||||
@@ -7,16 +7,10 @@ export default defineConfig({
|
|||||||
options: { typeAware: true },
|
options: { typeAware: true },
|
||||||
ignorePatterns: ['coverage/**'],
|
ignorePatterns: ['coverage/**'],
|
||||||
rules: {
|
rules: {
|
||||||
'n8n-local-rules/misplaced-n8n-typeorm-import': 'error',
|
|
||||||
'n8n-local-rules/no-guardrail-disable': [
|
'n8n-local-rules/no-guardrail-disable': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
guarded: [
|
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',
|
rule: 'no-unsealed-workflow-entity-write',
|
||||||
message: 'Route the write through a token-gated `WorkflowRepository` method.',
|
message: 'Route the write through a token-gated `WorkflowRepository` method.',
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
PROJECT_EDITOR_ROLE_SLUG,
|
PROJECT_EDITOR_ROLE_SLUG,
|
||||||
} from '@n8n/permissions';
|
} from '@n8n/permissions';
|
||||||
|
|
||||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
|
||||||
import { In } from '@n8n/typeorm';
|
import { In } from '@n8n/typeorm';
|
||||||
import type { EntityManager } from '@n8n/typeorm';
|
import type { EntityManager } from '@n8n/typeorm';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { DatabaseConfig } from '@n8n/config';
|
import { DatabaseConfig } from '@n8n/config';
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
|
||||||
import { DataSource, QueryFailedError } from '@n8n/typeorm';
|
import { DataSource, QueryFailedError } from '@n8n/typeorm';
|
||||||
import type { EntityManager } from '@n8n/typeorm';
|
import type { EntityManager } from '@n8n/typeorm';
|
||||||
import { OperationalError } from 'n8n-workflow';
|
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 { NoSkippedTestsRule } from './no-skipped-tests.js';
|
||||||
import { NoInterpolationInRegularStringRule } from './no-interpolation-in-regular-string.js';
|
import { NoInterpolationInRegularStringRule } from './no-interpolation-in-regular-string.js';
|
||||||
import { NoDynamicImportTemplateRule } from './no-dynamic-import-template.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 { NoGuardrailDisableRule } from './no-guardrail-disable.js';
|
||||||
import { NoTypeUnsafeEventEmitterRule } from './no-type-unsafe-event-emitter.js';
|
import { NoTypeUnsafeEventEmitterRule } from './no-type-unsafe-event-emitter.js';
|
||||||
import { NoUntypedConfigClassFieldRule } from './no-untyped-config-class-field.js';
|
import { NoUntypedConfigClassFieldRule } from './no-untyped-config-class-field.js';
|
||||||
@@ -44,7 +43,6 @@ export const rules = {
|
|||||||
'no-skipped-tests': NoSkippedTestsRule,
|
'no-skipped-tests': NoSkippedTestsRule,
|
||||||
'no-interpolation-in-regular-string': NoInterpolationInRegularStringRule,
|
'no-interpolation-in-regular-string': NoInterpolationInRegularStringRule,
|
||||||
'no-dynamic-import-template': NoDynamicImportTemplateRule,
|
'no-dynamic-import-template': NoDynamicImportTemplateRule,
|
||||||
'misplaced-n8n-typeorm-import': MisplacedN8nTypeormImportRule,
|
|
||||||
'no-guardrail-disable': NoGuardrailDisableRule,
|
'no-guardrail-disable': NoGuardrailDisableRule,
|
||||||
'no-type-unsafe-event-emitter': NoTypeUnsafeEventEmitterRule,
|
'no-type-unsafe-event-emitter': NoTypeUnsafeEventEmitterRule,
|
||||||
'no-untyped-config-class-field': NoUntypedConfigClassFieldRule,
|
'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 { RuleTester } from '@typescript-eslint/rule-tester';
|
||||||
import { NoGuardrailDisableRule } from './no-guardrail-disable.js';
|
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 { 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
|
// 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`
|
// 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: {
|
plugins: {
|
||||||
'n8n-local-rules': {
|
'n8n-local-rules': {
|
||||||
rules: {
|
rules: {
|
||||||
'misplaced-n8n-typeorm-import': MisplacedN8nTypeormImportRule,
|
'no-unsealed-credentials-entity-write': NoUnsealedCredentialsEntityWriteRule,
|
||||||
'no-unsealed-workflow-entity-write': NoUnsealedWorkflowEntityWriteRule,
|
'no-unsealed-workflow-entity-write': NoUnsealedWorkflowEntityWriteRule,
|
||||||
'no-unsealed-workflow-entity-write-audit': NoUnsealedWorkflowEntityWriteRule,
|
'no-unsealed-workflow-entity-write-audit': NoUnsealedWorkflowEntityWriteRule,
|
||||||
'no-guardrail-disable': NoGuardrailDisableRule,
|
'no-guardrail-disable': NoGuardrailDisableRule,
|
||||||
@@ -23,15 +23,18 @@ const ruleTester = new RuleTester({
|
|||||||
const options: [{ guarded: Array<{ rule: string; message: string }> }] = [
|
const options: [{ guarded: Array<{ rule: string; message: string }> }] = [
|
||||||
{
|
{
|
||||||
guarded: [
|
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`.' },
|
{ rule: 'no-unsealed-workflow-entity-write', message: 'Use `updateContent`.' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const typeormHit = {
|
const credentialsHit = {
|
||||||
messageId: 'noDisable' as const,
|
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 = {
|
const sealHit = {
|
||||||
messageId: 'noDisable' as const,
|
messageId: 'noDisable' as const,
|
||||||
@@ -39,7 +42,7 @@ const sealHit = {
|
|||||||
};
|
};
|
||||||
const blanket = {
|
const blanket = {
|
||||||
messageId: 'noBlanketDisable' as const,
|
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, {
|
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-next-line no-console\nconsole.log("x");', options },
|
||||||
{ code: '/* eslint-disable no-console, no-debugger */', options },
|
{ code: '/* eslint-disable no-console, no-debugger */', options },
|
||||||
// A plain comment mentioning the rule name is not a disable directive.
|
// A plain comment mentioning the rule name is not a disable directive.
|
||||||
{ code: '// see misplaced-n8n-typeorm-import for context', options },
|
{ code: '// see no-unsealed-credentials-entity-write for context', options },
|
||||||
{ code: '// eslint-disable-next-line-ish misplaced-n8n-typeorm-import', 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.
|
// 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");',
|
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.
|
// 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;' },
|
{ 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
|
// 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: [
|
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,
|
options,
|
||||||
errors: [typeormHit],
|
errors: [credentialsHit],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: '// oxlint-disable-next-line n8n-local-rules/no-unsealed-workflow-entity-write\nexport const x = 1;',
|
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],
|
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,
|
options,
|
||||||
errors: [typeormHit, sealHit],
|
errors: [credentialsHit, sealHit],
|
||||||
},
|
},
|
||||||
// A directive with no rule list, or one that names this rule, disables the guarded rules too.
|
// 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] },
|
{ code: '// eslint-disable-next-line\nexport const x = 1;', options, errors: [blanket] },
|
||||||
@@ -120,7 +123,7 @@ ruleTester.run('no-guardrail-disable', NoGuardrailDisableRule, {
|
|||||||
errors: [blanket],
|
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,
|
options,
|
||||||
errors: [blanket],
|
errors: [blanket],
|
||||||
},
|
},
|
||||||
|
|||||||
+12
-24
@@ -7,37 +7,25 @@ for repo-wide conventions.
|
|||||||
|
|
||||||
TypeORM belongs in the **persistence layer**, not in business logic.
|
TypeORM belongs in the **persistence layer**, not in business logic.
|
||||||
|
|
||||||
**Allowed to import `@n8n/typeorm`** — entity and repository files, including the
|
**Allowed to import `@n8n/typeorm`** — files that declare an `@Entity` class or a
|
||||||
ones co-located inside `src/modules/**`:
|
repository class that extends TypeORM `Repository` or n8n `BaseRepository`.
|
||||||
|
Their folder and filename do not affect the rule. Tests and migration tooling use
|
||||||
- `src/databases/**`
|
Code Health exceptions. Helper-only persistence adapters need a narrow,
|
||||||
- a module's `database/entities/**` and `database/repositories/**`
|
auditable entry in the `typeorm-persistence-boundary` rule.
|
||||||
- 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).
|
|
||||||
|
|
||||||
**Not allowed** — business logic (services, controllers, public-api handlers,
|
**Not allowed** — business logic (services, controllers, public-api handlers,
|
||||||
commands, factories) must not import `@n8n/typeorm` or `@n8n/typeorm/...`
|
commands, factories) must not import `@n8n/typeorm` or `@n8n/typeorm/...`
|
||||||
subpaths. The `misplaced-n8n-typeorm-import` lint rule enforces this; a new
|
subpaths. The `typeorm-persistence-boundary` Code Health rule enforces this. A
|
||||||
import — or an inline `eslint-disable` of the rule — fails CI. The same rule also
|
new import fails CI. The same rule also
|
||||||
catches the **relabel dodge**: importing a TypeORM operator/driver type (`In`,
|
catches the **relabel dodge**: importing a TypeORM operator/driver type (`In`,
|
||||||
`Not`, `FindOptionsWhere`, `EntityManager`, …) from `@n8n/db`, which
|
`Not`, `FindOptionsWhere`, `EntityManager`, …) from `@n8n/db`, which
|
||||||
re-exports them from `@n8n/typeorm` — that silences the direct-import check
|
re-exports them from `@n8n/typeorm` — that silences the direct-import check
|
||||||
without decoupling anything. Existing leaks of both kinds are tracked in two
|
without decoupling anything. Existing leaks of both kinds are tracked in the
|
||||||
`files`-scoped allowlists in `eslint.config.mjs` (direct `@n8n/typeorm` imports,
|
shrink-only Code Health baseline. Do not add new baseline entries.
|
||||||
and `@n8n/db` relabels) that only ever shrink: never add to them, and never
|
|
||||||
suppress the rule inline.
|
|
||||||
|
|
||||||
Distinct from that shrink-only ratchet, two files are **permanently** exempted in
|
Distinct from that shrink-only ratchet, `src/commands/db/revert.ts` is an exact
|
||||||
`eslint.config.mjs` for legitimate TypeORM use outside the persistence tree —
|
exception for CLI migration tooling. Composition-based repositories use exact
|
||||||
these are sanctioned, not migration targets, so don't try to relocate them or
|
exceptions. Repository subclasses need no exception.
|
||||||
suppress the rule:
|
|
||||||
|
|
||||||
- `src/commands/db/revert.ts` — `MigrationExecutor` (CLI migration tooling)
|
|
||||||
- `src/security-audit/security-audit.repository.ts` — `PackagesRepository`
|
|
||||||
|
|
||||||
Need an operator query (`In`, `IsNull`, `FindOptionsWhere`, …)? Add a
|
Need an operator query (`In`, `IsNull`, `FindOptionsWhere`, …)? Add a
|
||||||
use-case-named repository method (plain parameters, domain-shaped return) rather
|
use-case-named repository method (plain parameters, domain-shaped return) rather
|
||||||
|
|||||||
@@ -68,17 +68,11 @@ export default defineConfig(
|
|||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
'n8n-local-rules/no-dynamic-import-template': 'error',
|
'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.
|
// Ratchets: the allowlists below only shrink, so an inline disable is the one way to add a violation.
|
||||||
'n8n-local-rules/no-guardrail-disable': [
|
'n8n-local-rules/no-guardrail-disable': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
guarded: [
|
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',
|
rule: 'no-repository-in-public-api-handler',
|
||||||
message: 'Call a service instead of reaching the repository.',
|
message: 'Call a service instead of reaching the repository.',
|
||||||
@@ -270,139 +264,6 @@ export default defineConfig(
|
|||||||
'unicorn/filename-case': 'off',
|
'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:
|
// Sanctioned `@OnLeaderTakeover` users. Permanent, but additions need review:
|
||||||
// the system task runner itself, services that hold live resources on the
|
// 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/**'],
|
ignorePatterns: ['scripts/**/*.mjs', 'vitest.*.ts', 'coverage/**'],
|
||||||
rules: {
|
rules: {
|
||||||
'n8n-local-rules/no-dynamic-import-template': 'error',
|
'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.
|
// Ratchets: the allowlists below only shrink, so an inline disable is the one way to add a violation.
|
||||||
'n8n-local-rules/no-guardrail-disable': [
|
'n8n-local-rules/no-guardrail-disable': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
guarded: [
|
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',
|
rule: 'no-repository-in-public-api-handler',
|
||||||
message: 'Call a service instead of reaching the repository.',
|
message: 'Call a service instead of reaching the repository.',
|
||||||
@@ -270,142 +264,6 @@ export default defineConfig({
|
|||||||
'unicorn/filename-case': 'off',
|
'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:
|
// Sanctioned `@OnLeaderTakeover` users. Permanent, but additions need review:
|
||||||
// the system task runner itself, services that hold live resources on the
|
// the system task runner itself, services that hold live resources on the
|
||||||
|
|||||||
@@ -5,32 +5,4 @@ export default defineConfig({
|
|||||||
extends: [backendConfig],
|
extends: [backendConfig],
|
||||||
options: { typeAware: true },
|
options: { typeAware: true },
|
||||||
ignorePatterns: ['coverage/**', 'dist/**'],
|
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
|
- **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
|
- **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
|
- **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
|
## Usage
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { SingleInstanceLibsRule } from './rules/single-instance-libs.rule.js';
|
|||||||
import { SingleInstanceLockfileRule } from './rules/single-instance-lockfile.rule.js';
|
import { SingleInstanceLockfileRule } from './rules/single-instance-lockfile.rule.js';
|
||||||
import { StaleOverridesRule } from './rules/stale-overrides.rule.js';
|
import { StaleOverridesRule } from './rules/stale-overrides.rule.js';
|
||||||
import { SubpathPurityRule } from './rules/subpath-purity.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';
|
import { WorkflowPrTargetSafetyRule } from './rules/workflow-pr-target-safety.rule.js';
|
||||||
|
|
||||||
export type { CodeHealthContext } from './context.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 { StaleOverridesRule } from './rules/stale-overrides.rule.js';
|
||||||
export { SubpathPurityRule } from './rules/subpath-purity.rule.js';
|
export { SubpathPurityRule } from './rules/subpath-purity.rule.js';
|
||||||
export type { SubpathSpec } 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';
|
export { WorkflowPrTargetSafetyRule } from './rules/workflow-pr-target-safety.rule.js';
|
||||||
|
|
||||||
const defaultRuleSettings: RuleSettingsMap = {
|
const defaultRuleSettings: RuleSettingsMap = {
|
||||||
@@ -65,6 +67,11 @@ const defaultRuleSettings: RuleSettingsMap = {
|
|||||||
severity: 'error',
|
severity: 'error',
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
|
'typeorm-persistence-boundary': {
|
||||||
|
enabled: true,
|
||||||
|
severity: 'error',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
'lint-config-layering': {
|
'lint-config-layering': {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
@@ -154,6 +161,7 @@ export function createDefaultRunner(settings?: RuleSettingsMap): RuleRunner<Code
|
|||||||
runner.registerRule(new SingleInstanceLibsRule());
|
runner.registerRule(new SingleInstanceLibsRule());
|
||||||
runner.registerRule(new SingleInstanceLockfileRule());
|
runner.registerRule(new SingleInstanceLockfileRule());
|
||||||
runner.registerRule(new EncryptionBoundaryRule());
|
runner.registerRule(new EncryptionBoundaryRule());
|
||||||
|
runner.registerRule(new TypeormPersistenceBoundaryRule());
|
||||||
runner.registerRule(new LintConfigLayeringRule());
|
runner.registerRule(new LintConfigLayeringRule());
|
||||||
runner.registerRule(new StaleOverridesRule());
|
runner.registerRule(new StaleOverridesRule());
|
||||||
runner.registerRule(new EndpointScopeCoverageRule());
|
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