@starci/eslint-canon-be 1.0.1 → 1.2.0
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/README.md +69 -69
- package/cdc.mjs +107 -1
- package/comments.mjs +147 -1
- package/cqrs.mjs +103 -7
- package/data-access.mjs +144 -10
- package/e2e-flow.mjs +222 -213
- package/event-delivery.mjs +54 -1
- package/index.mjs +19 -2
- package/module-layering.mjs +268 -7
- package/naming.mjs +80 -3
- package/observability.mjs +140 -6
- package/package.json +1 -1
- package/testing.mjs +147 -46
- package/transport.mjs +65 -0
- package/type-safety.mjs +242 -8
package/data-access.mjs
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The rules that hold `data-access.md`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The first three read a constructor parameter or a decorator, all exact - there is no heuristic
|
|
5
|
+
* here and no judgement to get wrong, which is why all three can be switched on without a burn-down
|
|
6
|
+
* in a codebase that already follows them.
|
|
7
7
|
*
|
|
8
|
-
* The two
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* The next two hold a narrower, exact slice of DATA-4 (Rule 5) and DATA-5 (Rule 6) - the slice of
|
|
9
|
+
* each that a single file settles on its own, with no call graph and no knowledge of intent needed.
|
|
10
|
+
*
|
|
11
|
+
* `no-outer-manager-in-transaction` catches the one shape of DATA-4 a parser can see: a
|
|
12
|
+
* `this.<field>.transaction(async (tx) => ...)` callback whose body reaches for `this.<field>`
|
|
13
|
+
* again instead of `tx`. That field held the manager from BEFORE the transaction opened, so a call
|
|
14
|
+
* through it runs and commits on its own - the exact "half of it was written" bug DATA-4 names. What
|
|
15
|
+
* it does NOT see, and cannot: whether a HELPER defined in another file was handed `tx` by ITS
|
|
16
|
+
* caller. That still needs the call graph and stays undecidable, same as the law says.
|
|
17
|
+
*
|
|
18
|
+
* `no-eager-relation` catches the one shape of DATA-5 a parser can see: `eager: true` written on a
|
|
19
|
+
* relation decorator. What it does NOT see: whether a call site correctly asked for a relation it
|
|
20
|
+
* needed. That still needs to know what the answer is for and stays undecidable, same as the law
|
|
21
|
+
* says.
|
|
22
|
+
*
|
|
23
|
+
* Both remaining gaps are read by a person; a rule that guessed at either would fire on correct
|
|
24
|
+
* code, which is how a correct rule gets disabled.
|
|
12
25
|
*/
|
|
13
26
|
|
|
14
27
|
/** Forward-slash form of a filename, so Windows paths compare like every other path. */
|
|
@@ -149,24 +162,145 @@ export const requireEntityTableName = {
|
|
|
149
162
|
},
|
|
150
163
|
}
|
|
151
164
|
|
|
165
|
+
// -- DATA-4 (Rule 5) --------------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
/** Node types that rebind `this` - a `this.<field>` read past one of these belongs to a different call. */
|
|
168
|
+
const REBINDS_THIS = /^(?:FunctionDeclaration|FunctionExpression|ClassDeclaration|ClassExpression)$/
|
|
169
|
+
|
|
170
|
+
/** Walk every descendant of `node` that still runs under the SAME `this`, calling `visit` on each. */
|
|
171
|
+
const walkPreservingThis = (node, visit) => {
|
|
172
|
+
if (!node || typeof node.type !== "string") return
|
|
173
|
+
visit(node)
|
|
174
|
+
for (const key of Object.keys(node)) {
|
|
175
|
+
if (key === "parent") continue
|
|
176
|
+
const value = node[key]
|
|
177
|
+
if (Array.isArray(value)) {
|
|
178
|
+
for (const child of value) {
|
|
179
|
+
if (child && typeof child.type === "string" && !REBINDS_THIS.test(child.type)) walkPreservingThis(child, visit)
|
|
180
|
+
}
|
|
181
|
+
} else if (value && typeof value.type === "string" && !REBINDS_THIS.test(value.type)) {
|
|
182
|
+
walkPreservingThis(value, visit)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Everything inside a transaction receives the transactional manager, not the outer injected one. */
|
|
188
|
+
export const noOuterManagerInTransaction = {
|
|
189
|
+
meta: {
|
|
190
|
+
type: "problem",
|
|
191
|
+
docs: {
|
|
192
|
+
description:
|
|
193
|
+
"Inside `this.<field>.transaction(async (tx) => ...)`, every call uses `tx` - never `this.<field>` again (data-access.md DATA-4, Rule 5).",
|
|
194
|
+
},
|
|
195
|
+
schema: [],
|
|
196
|
+
messages: {
|
|
197
|
+
outerManager:
|
|
198
|
+
"This is inside `this.{{field}}.transaction(...)`, but it reaches for `this.{{field}}` again instead of `{{param}}`, the manager the callback received. `this.{{field}}` is the manager from BEFORE the transaction opened - a call through it runs and commits on its own, which is how half a write survives a rollback. Use `{{param}}`.",
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
create(context) {
|
|
202
|
+
return {
|
|
203
|
+
CallExpression(node) {
|
|
204
|
+
const callee = node.callee
|
|
205
|
+
if (callee.type !== "MemberExpression" || callee.computed) return
|
|
206
|
+
if (callee.property.type !== "Identifier" || callee.property.name !== "transaction") return
|
|
207
|
+
const object = callee.object
|
|
208
|
+
if (object.type !== "MemberExpression" || object.computed) return
|
|
209
|
+
if (object.object.type !== "ThisExpression") return
|
|
210
|
+
if (object.property.type !== "Identifier") return
|
|
211
|
+
const field = object.property.name
|
|
212
|
+
// Only the arrow-function callback is covered. A plain `function (tx) {}` callback does not
|
|
213
|
+
// inherit `this` from the enclosing method at all - TypeORM invokes it with no receiver, so
|
|
214
|
+
// `this.<field>` inside one throws rather than silently escaping the transaction, and this
|
|
215
|
+
// rule exists for the silent case.
|
|
216
|
+
const callback = node.arguments[0]
|
|
217
|
+
if (!callback || callback.type !== "ArrowFunctionExpression") return
|
|
218
|
+
const param = callback.params[0]
|
|
219
|
+
const paramName = param && param.type === "Identifier" ? param.name : "the transactional manager"
|
|
220
|
+
walkPreservingThis(callback.body, (descendant) => {
|
|
221
|
+
if (
|
|
222
|
+
descendant.type === "MemberExpression"
|
|
223
|
+
&& !descendant.computed
|
|
224
|
+
&& descendant.object.type === "ThisExpression"
|
|
225
|
+
&& descendant.property.type === "Identifier"
|
|
226
|
+
&& descendant.property.name === field
|
|
227
|
+
) {
|
|
228
|
+
context.report({ node: descendant, messageId: "outerManager", data: { field, param: paramName } })
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
},
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// -- DATA-5 (Rule 6) --------------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
/** Relation decorators whose `eager` option can grant the relation to every caller. */
|
|
239
|
+
const RELATION_DECORATORS = /^(?:ManyToOne|OneToOne|OneToMany|ManyToMany)$/
|
|
240
|
+
|
|
241
|
+
/** A relation is asked for by the call site that needs it; the entity grants no relation eagerly. */
|
|
242
|
+
export const noEagerRelation = {
|
|
243
|
+
meta: {
|
|
244
|
+
type: "problem",
|
|
245
|
+
docs: {
|
|
246
|
+
description: "A relation decorator carries no `eager: true` (data-access.md DATA-5, Rule 6).",
|
|
247
|
+
},
|
|
248
|
+
schema: [],
|
|
249
|
+
messages: {
|
|
250
|
+
eager:
|
|
251
|
+
"`@{{decorator}}(..., { eager: true })` grants this relation to every caller whether it asked for it or not, so a query that wants one column now pays for the whole tree. State the relation in the `relations` the call site writes, and drop `eager` here.",
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
create(context) {
|
|
255
|
+
return {
|
|
256
|
+
Decorator(node) {
|
|
257
|
+
const expression = node.expression
|
|
258
|
+
if (expression.type !== "CallExpression") return
|
|
259
|
+
if (expression.callee.type !== "Identifier" || !RELATION_DECORATORS.test(expression.callee.name)) return
|
|
260
|
+
for (const argument of expression.arguments) {
|
|
261
|
+
if (argument.type !== "ObjectExpression") continue
|
|
262
|
+
const eagerProperty = argument.properties.find(
|
|
263
|
+
(property) =>
|
|
264
|
+
property.type === "Property"
|
|
265
|
+
&& !property.computed
|
|
266
|
+
&& (property.key.name === "eager" || property.key.value === "eager"),
|
|
267
|
+
)
|
|
268
|
+
if (!eagerProperty) continue
|
|
269
|
+
if (eagerProperty.value.type === "Literal" && eagerProperty.value.value === true) {
|
|
270
|
+
context.report({ node, messageId: "eager", data: { decorator: expression.callee.name } })
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
|
|
152
278
|
/** The rules this law contributes to the plugin. */
|
|
153
279
|
export const rules = {
|
|
154
280
|
"must-inject-entity-manager": mustInjectEntityManager,
|
|
155
281
|
"no-injected-repository": noInjectedRepository,
|
|
156
282
|
"require-entity-table-name": requireEntityTableName,
|
|
283
|
+
"no-outer-manager-in-transaction": noOuterManagerInTransaction,
|
|
284
|
+
"no-eager-relation": noEagerRelation,
|
|
157
285
|
}
|
|
158
286
|
|
|
159
287
|
/**
|
|
160
288
|
* The level this law asks for, as the plugin's own opinion.
|
|
161
289
|
*
|
|
162
|
-
* All
|
|
163
|
-
*
|
|
164
|
-
* `
|
|
290
|
+
* All five measured against the reference repository (`src/**`) before shipping. The original three
|
|
291
|
+
* measured at zero debt and ship at `error`. `no-outer-manager-in-transaction` also measured zero -
|
|
292
|
+
* every `.transaction(async (tx) => ...)` callback in the reference repository already uses only the
|
|
293
|
+
* parameter it was handed - and ships at `error`. `no-eager-relation` also measured zero - no entity
|
|
294
|
+
* in the reference repository declares `eager: true` - and ships at `error` as well; a repository
|
|
295
|
+
* adopting these into a tree that is not already clean measures first and lands anything above zero
|
|
296
|
+
* at `warn` with the count beside it.
|
|
165
297
|
*/
|
|
166
298
|
export const recommended = {
|
|
167
299
|
"starci-be/must-inject-entity-manager": "error",
|
|
168
300
|
"starci-be/no-injected-repository": "error",
|
|
169
301
|
"starci-be/require-entity-table-name": "error",
|
|
302
|
+
"starci-be/no-outer-manager-in-transaction": "error",
|
|
303
|
+
"starci-be/no-eager-relation": "error",
|
|
170
304
|
}
|
|
171
305
|
|
|
172
306
|
/** Path helper shared with the other backend law modules. */
|