@starci/eslint-canon-be 1.8.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/schema-authority.mjs +67 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.8.1 - 2026-09-30
|
|
4
|
+
|
|
5
|
+
- New: `no-entity-manager-wrapper` (R36) closes the renamed-repository loophole: a class that injects or holds a typeorm `EntityManager` outside a `*.service.ts` / `*.handler.ts` (`*Store`, `*Statements`, `*Dao`, `*Gateway` wrappers) and an exported function whose first parameter is an `EntityManager` (a statement module) are findings. Exempt: `platform/database`, `apps/migrate` and the test tree (`src/tests/`, whose builders take an EntityManager by design).
|
|
6
|
+
|
|
3
7
|
## 1.8.0 - 2026-09-30
|
|
4
8
|
|
|
5
9
|
- Breaking (hotfix ahead of 2.0.0, owner rule "no repositories"): `sql-only-in-repository` is removed. `sql-text-only` (R36) takes its place: raw SQL text is written only in a `<name>.sql.ts` (the capability's `persistence/<name>.sql.ts`, or a test builder's `src/tests/fixtures/builders/<area>.sql.ts`); everywhere else `.query(...)` takes an imported text constant, never a string literal or template; `createQueryBuilder` is refused everywhere. Migrations keep their inline SQL.
|
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ A sample, not the list:
|
|
|
54
54
|
| Area | What the rules hold |
|
|
55
55
|
|---|---|
|
|
56
56
|
| **Errors** | A `catch` rethrows, logs through the logger port or returns an outcome carrying its cause; an error lives in its capability's `errors/`, extends `DomainError`, and no bare `Error` or framework exception escapes (`catch-must-account`, `error-home`) |
|
|
57
|
-
| **Schema authority** | Migrations only: no `synchronize`, no `migrationsRun`, no runtime DDL, no entity glob; raw SQL text only in a `<name>.sql.ts`, no `*.repository.ts` file; no ORM entity in a contract (`no-runtime-schema`, `sql-text-only`, `no-repository-file`, `no-entity-in-contract`) |
|
|
57
|
+
| **Schema authority** | Migrations only: no `synchronize`, no `migrationsRun`, no runtime DDL, no entity glob; raw SQL text only in a `<name>.sql.ts`, no `*.repository.ts` file and no EntityManager wrapper or statement module; no ORM entity in a contract (`no-runtime-schema`, `sql-text-only`, `no-repository-file`, `no-entity-manager-wrapper`, `no-entity-in-contract`) |
|
|
58
58
|
| **Config and secrets** | `process.env` only in `platform/config`; no literal default for a secret, password, token, key or URL; secrets compare with `timingSafeEqual` (`no-direct-env-read`, `no-secret-default`, `secret-compare-timing-safe`) |
|
|
59
59
|
| **Default deny** | Typed bodies and arguments, no `GraphQLJSON` tunnel, no `switch (operation)`, and `@Public` carries a reason (`no-untyped-body`, `public-needs-reason`) |
|
|
60
60
|
| **Module shape** | `@Global()` only on the manifest allowlist; typed `ConfigurableModuleBuilder`; static `register`; one module per file; no `new` of a provider; no module-level `let` |
|
package/package.json
CHANGED
package/schema-authority.mjs
CHANGED
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
* - `sql-text-only` (R36) keeps raw SQL text inside a `<name>.sql.ts` (the owning capability's `persistence/`,
|
|
11
11
|
* or a test builder's `src/tests/fixtures/builders/<area>.sql.ts`): `.query(...)` elsewhere takes an imported
|
|
12
12
|
* text constant, never a literal or template; `createQueryBuilder` is not used.
|
|
13
|
+
* - `no-entity-manager-wrapper` (R36) refuses the repository pattern under any other name: a class that injects or
|
|
14
|
+
* holds a typeorm `EntityManager` outside a `*.service.ts` / `*.handler.ts` (a `*Store`, `*Statements`, `*Dao`,
|
|
15
|
+
* `*Gateway` wrapper), and an exported function whose first parameter is an `EntityManager` (a statement module).
|
|
16
|
+
* Exempt: `platform/database`, `apps/migrate`, the test tree (`src/tests/`, whose builders take an EntityManager by design).
|
|
13
17
|
* - `no-repository-file` (R36) refuses a `*.repository.ts` file: repositories are banned (owner rule); SQL text lives
|
|
14
18
|
* in `<name>.sql.ts` and the capability's service runs it on the shared EntityManager.
|
|
15
19
|
* - `no-entity-in-contract` (R37 `BE_ENTITY_IN_CONTRACT`) keeps ORM entities out of the types that cross a
|
|
@@ -111,6 +115,67 @@ export const sqlTextOnly = {
|
|
|
111
115
|
},
|
|
112
116
|
}
|
|
113
117
|
|
|
118
|
+
/** Files that may hold or pass an EntityManager freely: the connection owner, the migrate app and the test tree. */
|
|
119
|
+
const EM_OWNER = /\/src\/modules\/platform\/database\/|\/apps\/migrate\/|\/src\/tests\//
|
|
120
|
+
/** The roles whose class holds the shared EntityManager: services and CQRS handlers. */
|
|
121
|
+
const EM_CLASS_ROLE = /\.(?:service|handler)\.[cm]?ts$/
|
|
122
|
+
/** Spec files are judged by the spec rules, not here. */
|
|
123
|
+
const SPEC_FILE = /\.(?:e2e-|integration-|contract-)?spec\.[cm]?ts$/
|
|
124
|
+
|
|
125
|
+
/** The repository pattern under another name: an EntityManager wrapper class or a statement module. */
|
|
126
|
+
export const noEntityManagerWrapper = {
|
|
127
|
+
meta: {
|
|
128
|
+
type: "problem",
|
|
129
|
+
docs: { description: "Only a service or a handler holds the EntityManager; no wrapper class and no exported function taking an EntityManager first." },
|
|
130
|
+
schema: [],
|
|
131
|
+
messages: {
|
|
132
|
+
wrapper: "`{{name}}` wraps the EntityManager for another class: the repository pattern under another name. Put the SQL text in `<name>.sql.ts` and run it from the capability's `*.service.ts` (or the handler) on the shared EntityManager.",
|
|
133
|
+
statement: "`{{name}}` takes an EntityManager as its first parameter: a statement module, the repository pattern under another name. Put the SQL text in `<name>.sql.ts` and run it from the capability's `*.service.ts` on the shared EntityManager.",
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
create(context) {
|
|
137
|
+
const filename = normalizePath(context.filename || context.getFilename())
|
|
138
|
+
if (isDeclarationFile(filename) || isMigrationFile(filename) || EM_OWNER.test(filename) || SPEC_FILE.test(filename)) return {}
|
|
139
|
+
const managerNames = new Set()
|
|
140
|
+
const isManager = (annotation) => {
|
|
141
|
+
const type = annotation?.typeAnnotation ?? annotation
|
|
142
|
+
return type?.type === "TSTypeReference" && type.typeName.type === "Identifier" && managerNames.has(type.typeName.name)
|
|
143
|
+
}
|
|
144
|
+
const paramType = (param) => (param.type === "TSParameterProperty" ? param.parameter : param).typeAnnotation
|
|
145
|
+
const holdsManager = (klass) => klass.body.body.some((member) => {
|
|
146
|
+
if (member.type === "PropertyDefinition" && isManager(member.typeAnnotation)) return true
|
|
147
|
+
if (member.type === "MethodDefinition" && (member.kind === "constructor" || member.kind === "set")) return member.value.params.some((param) => isManager(paramType(param)))
|
|
148
|
+
return false
|
|
149
|
+
})
|
|
150
|
+
const firstIsManager = (fn) => fn?.params?.length > 0 && isManager(paramType(fn.params[0]))
|
|
151
|
+
return {
|
|
152
|
+
ImportDeclaration(node) {
|
|
153
|
+
if (node.source.value !== "typeorm") return
|
|
154
|
+
for (const spec of node.specifiers) if (spec.type === "ImportSpecifier" && keyName(spec.imported) === "EntityManager") managerNames.add(spec.local.name)
|
|
155
|
+
},
|
|
156
|
+
"ClassDeclaration, ClassExpression"(node) {
|
|
157
|
+
if (managerNames.size === 0 || EM_CLASS_ROLE.test(filename) || !holdsManager(node)) return
|
|
158
|
+
context.report({ node: node.id ?? node, messageId: "wrapper", data: { name: node.id?.name ?? "this class" } })
|
|
159
|
+
},
|
|
160
|
+
ExportNamedDeclaration(node) {
|
|
161
|
+
if (managerNames.size === 0 || !node.declaration) return
|
|
162
|
+
const declaration = node.declaration
|
|
163
|
+
if (declaration.type === "FunctionDeclaration" && firstIsManager(declaration)) {
|
|
164
|
+
context.report({ node: declaration.id ?? declaration, messageId: "statement", data: { name: declaration.id?.name ?? "function" } })
|
|
165
|
+
}
|
|
166
|
+
if (declaration.type === "VariableDeclaration") {
|
|
167
|
+
for (const declarator of declaration.declarations) {
|
|
168
|
+
const init = declarator.init
|
|
169
|
+
if ((init?.type === "ArrowFunctionExpression" || init?.type === "FunctionExpression") && firstIsManager(init)) {
|
|
170
|
+
context.report({ node: declarator.id, messageId: "statement", data: { name: declarator.id.name } })
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
|
|
114
179
|
/** Repositories are banned: a `*.repository.ts` file is a finding. */
|
|
115
180
|
export const noRepositoryFile = {
|
|
116
181
|
meta: {
|
|
@@ -197,6 +262,7 @@ export const rules = {
|
|
|
197
262
|
"no-runtime-schema": noRuntimeSchema,
|
|
198
263
|
"sql-text-only": sqlTextOnly,
|
|
199
264
|
"no-repository-file": noRepositoryFile,
|
|
265
|
+
"no-entity-manager-wrapper": noEntityManagerWrapper,
|
|
200
266
|
"no-entity-in-contract": noEntityInContract,
|
|
201
267
|
"migration-down-reversible": migrationDownReversible,
|
|
202
268
|
}
|
|
@@ -206,6 +272,7 @@ export const recommended = {
|
|
|
206
272
|
"starci-be/no-runtime-schema": "error",
|
|
207
273
|
"starci-be/sql-text-only": "error",
|
|
208
274
|
"starci-be/no-repository-file": "error",
|
|
275
|
+
"starci-be/no-entity-manager-wrapper": "error",
|
|
209
276
|
"starci-be/no-entity-in-contract": "error",
|
|
210
277
|
"starci-be/migration-down-reversible": "error",
|
|
211
278
|
}
|