@starci/eslint-canon-be 3.0.7 → 3.0.9
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 +13 -1
- package/README.md +13 -7
- package/ambient-id.mjs +90 -0
- package/cli.mjs +181 -0
- package/comments.mjs +18 -21
- package/config-owner.mjs +20 -4
- package/connections.mjs +12 -12
- package/cookies.mjs +106 -0
- package/cqrs.mjs +22 -26
- package/data-access.mjs +9 -28
- package/default-deny.mjs +2 -1
- package/e2e-flow.mjs +4 -4
- package/event-bus.mjs +237 -0
- package/feature-thin.mjs +237 -0
- package/idempotency.mjs +3 -1
- package/index.mjs +22 -1
- package/infra-owner.mjs +1 -1
- package/injection.mjs +67 -22
- package/input-bounds.mjs +2 -1
- package/jobs.mjs +214 -0
- package/lib/ast.mjs +7 -0
- package/lib/config.mjs +1 -1
- package/lib/db-writes.mjs +1 -1
- package/lib/doors.mjs +81 -0
- package/lib/edition.mjs +45 -0
- package/lib/entity-writes.mjs +35 -0
- package/lib/hfs.mjs +3 -3
- package/lib/path.mjs +0 -6
- package/lib/persistence.mjs +11 -18
- package/lib/ports.mjs +5 -2
- package/lib/specifier.mjs +27 -0
- package/lib/transactions.mjs +37 -0
- package/lib/transport-slots.mjs +8 -3
- package/lib/unit-spec.mjs +63 -2
- package/module-layering.mjs +3 -24
- package/module-shape.mjs +2 -2
- package/naming.mjs +2 -2
- package/package.json +5 -3
- package/project-graph.mjs +8 -5
- package/projections.mjs +117 -0
- package/query-safety.mjs +1 -1
- package/realtime.mjs +181 -0
- package/runtime/engine/config.mjs +494 -0
- package/runtime/engine/invalid-config.mjs +15 -0
- package/runtime/engine/orca-config.mjs +27 -0
- package/runtime/engine/runtime-root.mjs +5 -0
- package/runtime/engine/yaml.mjs +3 -3
- package/runtime/knowledge/hfs/canon-pins.yaml +163 -32
- package/runtime/knowledge/hfs/rules.yaml +2059 -0
- package/runtime/knowledge/hfs/slots.yaml +915 -123
- package/runtime/knowledge/patterns/fe/folder.yaml +133 -27
- package/runtime/knowledge/sonar-gate.yaml +53 -19
- package/runtime/modules/models/registry.yaml +591 -0
- package/runtime/modules/models/runtimes.yaml +701 -0
- package/runtime/packages/hfs/templates/app/package-scripts/package.json +3 -10
- package/runtime/scripts/api/git/config-get.mjs +10 -0
- package/runtime/scripts/api/git/lib.mjs +17 -27
- package/runtime/scripts/api/git/ls-files.mjs +6 -0
- package/runtime/scripts/api/git/recorded-lines.mjs +19 -0
- package/runtime/scripts/api/git/remote.mjs +5 -0
- package/runtime/scripts/api/git/rev-parse-query.mjs +5 -0
- package/runtime/scripts/{lib/hfs-allows.mjs → hfs/allows.mjs} +3 -3
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +220 -0
- package/runtime/scripts/{checks → hfs}/architecture/backend.mjs +17 -24
- package/runtime/scripts/{checks → hfs}/architecture/background-unowned.mjs +16 -34
- package/runtime/scripts/{checks → hfs}/architecture/client-reaches-server.mjs +3 -1
- package/runtime/scripts/{checks → hfs}/architecture/clones.mjs +4 -4
- package/runtime/scripts/{checks → hfs}/architecture/config.mjs +11 -8
- package/runtime/scripts/{checks → hfs}/architecture/connection-map.mjs +21 -9
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +88 -0
- package/runtime/scripts/hfs/architecture/context-map.mjs +237 -0
- package/runtime/scripts/hfs/architecture/context-owner.mjs +82 -0
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -0
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +60 -0
- package/runtime/scripts/{checks → hfs}/architecture/contract-fixture-guard.mjs +5 -11
- package/runtime/scripts/{checks → hfs}/architecture/contracts.mjs +33 -162
- package/runtime/scripts/{checks → hfs}/architecture/dead-exports.mjs +58 -29
- package/runtime/scripts/{checks → hfs}/architecture/doc-language.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/entrypoint.mjs +2 -1
- package/runtime/scripts/{checks → hfs}/architecture/fe-slot-allows.mjs +4 -4
- package/runtime/scripts/{checks → hfs}/architecture/feature-shape.mjs +6 -3
- package/runtime/scripts/{checks → hfs}/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/frontend.mjs +17 -43
- package/runtime/scripts/{checks → hfs}/architecture/hfs-graph.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/hfs.mjs +27 -42
- package/runtime/scripts/{checks → hfs}/architecture/i18n-keys.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/index.mjs +17 -4
- package/runtime/scripts/{checks → hfs}/architecture/machine-ast.mjs +44 -11
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +27 -0
- package/runtime/scripts/{checks → hfs}/architecture/module-per-transport.mjs +30 -6
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +96 -0
- package/runtime/scripts/{checks → hfs}/architecture/next-data.mjs +40 -168
- package/runtime/scripts/{checks → hfs}/architecture/owners.mjs +35 -15
- package/runtime/scripts/{checks → hfs}/architecture/register-once.mjs +2 -1
- package/runtime/scripts/{checks → hfs}/architecture/registration.mjs +23 -81
- package/runtime/scripts/{checks → hfs}/architecture/required-files.mjs +21 -13
- package/runtime/scripts/{checks → hfs}/architecture/schema-owner.mjs +17 -114
- package/runtime/scripts/hfs/architecture/server-action.mjs +24 -0
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +16 -0
- package/runtime/scripts/{checks → hfs}/architecture/source-names.mjs +27 -147
- package/runtime/scripts/{checks → hfs}/architecture/sql-owner.mjs +59 -18
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +64 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +111 -0
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +131 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +42 -0
- package/runtime/scripts/hfs/architecture/supabase.mjs +434 -0
- package/runtime/scripts/{checks → hfs}/architecture/surface.mjs +10 -4
- package/runtime/scripts/{checks → hfs}/architecture/test-world-files.mjs +11 -21
- package/runtime/scripts/{checks → hfs}/architecture/tiers.mjs +47 -5
- package/runtime/scripts/{checks → hfs}/architecture/transport-owner.mjs +3 -3
- package/runtime/scripts/{checks → hfs}/architecture/typescript.mjs +13 -27
- package/runtime/scripts/hfs/declaration-shape.mjs +62 -0
- package/runtime/scripts/hfs/declaration-slots.mjs +75 -0
- package/runtime/scripts/hfs/edition-slots.mjs +66 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +247 -0
- package/runtime/scripts/hfs/param-names.mjs +18 -0
- package/runtime/scripts/{lib/hfs-path-findings.mjs → hfs/path-findings.mjs} +7 -7
- package/runtime/scripts/{lib → hfs}/project-graph.mjs +0 -0
- package/runtime/scripts/{lib → hfs}/project-rule.mjs +3 -3
- package/runtime/scripts/{lib → hfs}/repo-identity.mjs +9 -6
- package/runtime/scripts/hfs/rule-params-shape.mjs +50 -0
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/fe-no-tests.mjs +6 -5
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/read.mjs +1 -1
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/secrets.mjs +4 -4
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/spec-placement.mjs +1 -1
- package/runtime/scripts/{lib/hfs-slots.mjs → hfs/slots.mjs} +114 -150
- package/runtime/scripts/hfs/test-world-slot.mjs +6 -0
- package/runtime/scripts/{checks → hfs}/typescript-programs.mjs +0 -5
- package/runtime/scripts/{lib/hfs-view.mjs → hfs/view.mjs} +6 -5
- package/runtime/scripts/lib/dockerfile.mjs +124 -0
- package/runtime/scripts/lib/git.mjs +36 -0
- package/runtime/scripts/lib/i18n.mjs +86 -0
- package/runtime/scripts/lib/json.mjs +13 -3
- package/runtime/scripts/lib/language.mjs +26 -7
- package/runtime/scripts/lib/line-count.mjs +9 -0
- package/runtime/scripts/lib/list.mjs +20 -0
- package/runtime/scripts/lib/path-key.mjs +59 -2
- package/runtime/scripts/lib/read-text.mjs +34 -0
- package/runtime/scripts/lib/regex.mjs +4 -0
- package/runtime/scripts/lib/secret-patterns.mjs +4 -3
- package/runtime/scripts/lib/sops-envelope.mjs +58 -0
- package/runtime/scripts/lib/stack-services.mjs +7 -31
- package/runtime/scripts/lib/ts-ast.mjs +35 -0
- package/runtime/scripts/{checks/common.mjs → lib/walk.mjs} +5 -15
- package/schema-authority.mjs +14 -12
- package/service-deps.mjs +7 -13
- package/service-isolation.mjs +72 -0
- package/size-budget.mjs +3 -2
- package/test-world.mjs +5 -4
- package/testing.mjs +27 -24
- package/transport.mjs +12 -9
- package/unit-spec.mjs +1 -1
- package/user-copy.mjs +5 -4
- package/webhooks.mjs +173 -0
- package/runtime/scripts/api/sops/decrypt.mjs +0 -16
- package/runtime/scripts/lib/recorded-lines.mjs +0 -27
- package/runtime/scripts/lib/test-secrets.mjs +0 -116
- /package/runtime/scripts/{checks → hfs}/architecture/config-unread.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/constructor-deps.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/cross-app-duplicate.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/default-deny.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/error-codes.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/error-masked.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/hooks-are-hooks.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/injection-token-exported.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/package-shape.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/reachability.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/route-files-thin.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/sql-tokens.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/symbols.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/unit-spec-providers.mjs +0 -0
package/feature-thin.mjs
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rule that holds the thin-feature law (R203 `BE_FEATURE_THIN`).
|
|
3
|
+
*
|
|
4
|
+
* A file of the feature tier whose role is one of `ruleParams.be.thinRoles` of the slot manifest
|
|
5
|
+
* (`<name>.handler.ts`, `<name>.resolver.ts`, `<name>.consumer.ts`, `<name>.processor.ts`, `<name>.step.ts`,
|
|
6
|
+
* `<name>.webhook.ts`, `<name>.mapper.ts`, ...) is a door: it maps its parameters and makes ONE delegating
|
|
7
|
+
* call into the modules (a service, the command or query bus, the event bus, a queue producer). No branch,
|
|
8
|
+
* loop, `try` or `throw` on business data and no computation lives in it - the logic belongs to the modules,
|
|
9
|
+
* where every file is unit-covered at 100, and orchestration (step order, compensation, claim, fencing,
|
|
10
|
+
* retry) belongs to the platform. A `mapper` file holds only property mapping: `*.mapper.ts` calls, `new`, and the shape conversions `map` and `toISOString`
|
|
11
|
+
* expressions, no delegating call at all.
|
|
12
|
+
*
|
|
13
|
+
* Kind comes from the SLOT and the TIER (`hfs.tierOf(filename)`, `hfs.ruleParams.thinRoles`), the role from
|
|
14
|
+
* the file-name suffix the closed role vocabulary defines - never a path regex. What a receiver is comes
|
|
15
|
+
* from its TYPE (`Logger` of `platform/logging`, `WebhookSignatureService` of `platform/http-security`) and
|
|
16
|
+
* from whether the member is injected (`injectedMembers`); what a free function is comes from the file its
|
|
17
|
+
* import resolves to. A job `step` records its fence through `JobClaims` (`advance`, `runKey`) and a webhook proves its signature through `verify`: those gate calls
|
|
18
|
+
* are free, the one effect each makes is the delegating call. `.spec.ts` and `.d.ts` are never judged, and neither is a file whose tier is not
|
|
19
|
+
* `feature` or whose role is not a thin role. A clean thin file is never flagged: that is the bar.
|
|
20
|
+
*/
|
|
21
|
+
import { keyName, walk } from "./lib/ast.mjs"
|
|
22
|
+
import { aliasTarget, injectedMembers } from "./lib/doors.mjs"
|
|
23
|
+
import { hfsOf } from "./lib/hfs.mjs"
|
|
24
|
+
import { importOf } from "./lib/import-source.mjs"
|
|
25
|
+
import { baseName, isLoggerType, isOwnedType } from "./lib/ports.mjs"
|
|
26
|
+
|
|
27
|
+
/** The statements and expressions that are a decision, a repetition or an exit: none lives in a thin file. */
|
|
28
|
+
const BRANCH_NODES = new Set(["IfStatement", "SwitchStatement", "ConditionalExpression", "ForStatement", "ForInStatement", "ForOfStatement", "WhileStatement", "DoWhileStatement", "TryStatement", "ThrowStatement", "LogicalExpression"])
|
|
29
|
+
|
|
30
|
+
/** The expressions that compute a value (a `TemplateLiteral` with an expression joins them). */
|
|
31
|
+
const COMPUTE_NODES = new Set(["BinaryExpression", "UnaryExpression", "UpdateExpression", "AssignmentExpression"])
|
|
32
|
+
|
|
33
|
+
/** The nodes a function can be: the walk for calls and the nesting test use the same set. */
|
|
34
|
+
const FUNCTION_NODES = new Set(["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression", "MethodDefinition"])
|
|
35
|
+
|
|
36
|
+
/** What one decision or loop node is called in a message. */
|
|
37
|
+
const BRANCH_WHAT = {
|
|
38
|
+
IfStatement: "an `if`",
|
|
39
|
+
SwitchStatement: "a `switch`",
|
|
40
|
+
ConditionalExpression: "a ternary",
|
|
41
|
+
ForStatement: "a `for`",
|
|
42
|
+
ForInStatement: "a `for-in`",
|
|
43
|
+
ForOfStatement: "a `for-of`",
|
|
44
|
+
WhileStatement: "a `while`",
|
|
45
|
+
DoWhileStatement: "a `do-while`",
|
|
46
|
+
TryStatement: "a `try`",
|
|
47
|
+
ThrowStatement: "a `throw`",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The wraps a call may sit under while still being the call of its statement. */
|
|
51
|
+
const STATEMENT_WRAPPERS = new Set(["AwaitExpression", "TSAsExpression", "TSSatisfiesExpression", "TSNonNullExpression", "ChainExpression"])
|
|
52
|
+
|
|
53
|
+
/** The closed-vocabulary role a file name carries (`<name>.<role>.ts` gives `<role>`), or null. */
|
|
54
|
+
const roleOf = (filename) => /\.([a-z][a-z0-9-]*)\.ts$/.exec(baseName(filename))?.[1] ?? null
|
|
55
|
+
|
|
56
|
+
/** The `WebhookSignatureService` of `platform/http-security` - the `verify` a webhook door may call. */
|
|
57
|
+
const isSignatureReceiver = (context, node) => isOwnedType(context, node, { name: "WebhookSignatureService", capability: "http-security", tier: "platform" })
|
|
58
|
+
|
|
59
|
+
/** The shape conversions a mapper may make on a value it received: a collection mapped item by item and a date written as text. They decide nothing and compute nothing. */
|
|
60
|
+
const MAPPER_SHAPE_METHODS = new Set(["map", "toISOString"])
|
|
61
|
+
|
|
62
|
+
/** The `JobClaims` port of `platform/jobs` - the fence a job step records itself through (`advance`, `runKey`): bookkeeping the fenced-job pattern requires, free beside the step's one delegating call. */
|
|
63
|
+
const isClaimsReceiver = (context, node) => isOwnedType(context, node, { name: "JobClaims", capability: "jobs", tier: "platform" })
|
|
64
|
+
|
|
65
|
+
/** The `Logger` port of `platform/logging` - the one call that is always free. */
|
|
66
|
+
const isLoggerReceiver = isLoggerType
|
|
67
|
+
|
|
68
|
+
/** The name of a thin file's law is `feature-thin` (R203). */
|
|
69
|
+
export const featureThin = {
|
|
70
|
+
meta: {
|
|
71
|
+
type: "problem",
|
|
72
|
+
docs: { description: "A feature-tier file of a thin role maps its parameters and makes one delegating call into the modules: no branch, loop, `try`, `throw` or computation, and no call other than the one delegate, a modules function in mapping position, a `*.mapper.ts` function or the Logger." },
|
|
73
|
+
schema: [],
|
|
74
|
+
messages: {
|
|
75
|
+
branch: "{{what}} is a decision or a repetition in a thin file. A feature `<name>.{{role}}.ts` maps its parameters and makes one delegating call into the modules; a branch, a loop, a `try` or a `throw` is business logic that belongs in a service of the modules (where a unit spec covers it) - and orchestration (step order, compensation, claim, fencing, retry) belongs to the platform.",
|
|
76
|
+
compute: "{{what}} computes a value in a thin file. A feature `<name>.{{role}}.ts` maps its parameters and hands them over; an expression that computes is logic that belongs in the modules. Map the value there and read it back.",
|
|
77
|
+
call: "`{{what}}` is a call a thin file does not make. It calls exactly one injected member (a service, a bus `execute`, an event bus, a queue producer) or one function of the modules, and pure `*.mapper.ts` functions and the `Logger` port are free{{webhook}}; everything else - parsing, mapping by hand, a helper, a second dependency - is logic that belongs in the modules.",
|
|
78
|
+
many: "`{{name}}` makes a second delegating call here. A thin file makes exactly one; a second is orchestration - step order, compensation, claim, fencing, retry - and belongs to a capability of the platform or inside the service the file calls.",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
create(context) {
|
|
82
|
+
const hfs = hfsOf(context)
|
|
83
|
+
const name = baseName(context.filename)
|
|
84
|
+
if (name.endsWith(".spec.ts") || name.endsWith(".d.ts")) return {}
|
|
85
|
+
const role = roleOf(name)
|
|
86
|
+
if (role === null || !(hfs.ruleParams.thinRoles ?? []).includes(role)) return {}
|
|
87
|
+
if (hfs.tierOf(context.filename) !== "feature") return {}
|
|
88
|
+
const sourceCode = context.sourceCode
|
|
89
|
+
const mapperFile = role === "mapper"
|
|
90
|
+
|
|
91
|
+
/** The injected member names of a class: constructor parameter properties and properties carrying an injector decorator. */
|
|
92
|
+
const memberNames = new Map()
|
|
93
|
+
const injectedNamesOf = (classNode) => {
|
|
94
|
+
if (memberNames.has(classNode)) return memberNames.get(classNode)
|
|
95
|
+
const names = new Set()
|
|
96
|
+
for (const { node } of injectedMembers(context, classNode)) {
|
|
97
|
+
if (node.type === "TSParameterProperty" && node.parameter.type === "Identifier") names.add(node.parameter.name)
|
|
98
|
+
else if (node.type === "PropertyDefinition") {
|
|
99
|
+
const key = keyName(node.key)
|
|
100
|
+
if (key !== null) names.add(key)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
memberNames.set(classNode, names)
|
|
104
|
+
return names
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The class `this` binds at a node's position: the nearest enclosing class, or null when a non-arrow
|
|
109
|
+
* function or an object-literal method rebinds `this` in between (a method's own `FunctionExpression`
|
|
110
|
+
* value is skipped - a method's `this` is the class).
|
|
111
|
+
*/
|
|
112
|
+
const thisClassOf = (node) => {
|
|
113
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
114
|
+
if (parent.type === "ClassDeclaration" || parent.type === "ClassExpression") return parent
|
|
115
|
+
if (parent.type === "FunctionDeclaration") return null
|
|
116
|
+
if (parent.type === "FunctionExpression" && parent.parent?.type !== "MethodDefinition") return null
|
|
117
|
+
}
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The member name of `this.<member>` the call's receiver names, when it is an injected member of the enclosing class. */
|
|
122
|
+
const injectedMemberOf = (object, call) => {
|
|
123
|
+
if (object.type !== "MemberExpression" || object.computed || object.object.type !== "ThisExpression" || object.property.type !== "Identifier") return null
|
|
124
|
+
const classNode = thisClassOf(call)
|
|
125
|
+
if (classNode === null) return null
|
|
126
|
+
const member = object.property.name
|
|
127
|
+
return injectedNamesOf(classNode).has(member) ? member : null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** True for a call `f(...)` where `f` is imported from a `*.mapper.ts` (the pure mapping functions). */
|
|
131
|
+
const isMapperCall = (callee) => {
|
|
132
|
+
if (callee.type !== "Identifier") return false
|
|
133
|
+
const found = importOf(context, callee)
|
|
134
|
+
return found !== null && /(?:^|\/)[^/]+\.mapper(?:\.[cm]?[jt]s)?$/.test(found.source.replace(/\\/g, "/"))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** True for a call `f(...)` where `f` is imported and its declaration lives outside the feature tier (a function of the modules). */
|
|
138
|
+
const isModulesCall = (callee) => {
|
|
139
|
+
if (callee.type !== "Identifier" || importOf(context, callee) === null) return false
|
|
140
|
+
const symbol = aliasTarget(context, callee)
|
|
141
|
+
return (symbol?.getDeclarations() ?? []).some((declaration) => hfs.tierOf(declaration.getSourceFile().fileName) !== "feature")
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** True when the call IS the statement it sits in: `f(...)`, `await f(...)`, `return (await) f(...)`, or the whole body of a concise arrow. */
|
|
145
|
+
const isStatementCall = (call) => {
|
|
146
|
+
let current = call
|
|
147
|
+
while (STATEMENT_WRAPPERS.has(current.parent?.type)) current = current.parent
|
|
148
|
+
const parent = current.parent
|
|
149
|
+
return parent?.type === "ExpressionStatement" || parent?.type === "ReturnStatement" || (parent?.type === "ArrowFunctionExpression" && parent.body === current)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* What one call is to a thin file: `delegating` (the one call into the modules a file may make),
|
|
154
|
+
* `free` (a mapper, a logger method, a webhook `verify`, a job step's `JobClaims` fence call, a modules function in a mapping position)
|
|
155
|
+
* or `call` (a call a thin file does not make). A `mapper` file holds only mapper calls and `new`.
|
|
156
|
+
*/
|
|
157
|
+
const classify = (call) => {
|
|
158
|
+
const callee = call.callee
|
|
159
|
+
if (callee.type === "Identifier" && isMapperCall(callee)) return "free"
|
|
160
|
+
const member = callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" ? callee : null
|
|
161
|
+
if (mapperFile && member !== null && MAPPER_SHAPE_METHODS.has(member.property.name) && injectedMemberOf(member.object, call) === null) return "free"
|
|
162
|
+
// a cli group command shows its help without a sub-command: the `help` of the command nest-commander gives it
|
|
163
|
+
if (role === "cli" && member !== null && member.property.name === "help" && member.object.type === "MemberExpression" && member.object.object.type === "ThisExpression" && member.object.property.type === "Identifier" && member.object.property.name === "command") return "free"
|
|
164
|
+
if (mapperFile) return "call"
|
|
165
|
+
if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && injectedMemberOf(callee.object, call) !== null) {
|
|
166
|
+
if (isLoggerReceiver(context, callee.object)) return "free"
|
|
167
|
+
if (role === "webhook" && callee.property.name === "verify" && isSignatureReceiver(context, callee.object)) return "free"
|
|
168
|
+
if (role === "step" && isClaimsReceiver(context, callee.object)) return "free"
|
|
169
|
+
return "delegating"
|
|
170
|
+
}
|
|
171
|
+
if (callee.type === "Identifier" && isModulesCall(callee)) return isStatementCall(call) ? "delegating" : "free"
|
|
172
|
+
return "call"
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Every root function of the file (a method body, function or arrow not nested in another function and not inside a decorator subtree). */
|
|
176
|
+
const rootFunctionsOf = (program) => {
|
|
177
|
+
const roots = []
|
|
178
|
+
walk(program, (node) => {
|
|
179
|
+
if (!FUNCTION_NODES.has(node.type)) return
|
|
180
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
181
|
+
if (parent.type === "Decorator" || FUNCTION_NODES.has(parent.type)) return
|
|
182
|
+
}
|
|
183
|
+
if (node.type === "MethodDefinition" && node.kind === "constructor") return
|
|
184
|
+
roots.push(node)
|
|
185
|
+
})
|
|
186
|
+
return roots
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The name a `many` message reports for a function root. */
|
|
190
|
+
const nameOf = (fn) => {
|
|
191
|
+
if (fn.type === "MethodDefinition") return keyName(fn.key) ?? "the method"
|
|
192
|
+
if (fn.type === "FunctionDeclaration" && fn.id?.type === "Identifier") return fn.id.name
|
|
193
|
+
const parent = fn.parent
|
|
194
|
+
if (parent?.type === "VariableDeclarator" && parent.id.type === "Identifier") return parent.id.name
|
|
195
|
+
if ((parent?.type === "PropertyDefinition" || parent?.type === "Property") && keyName(parent.key)) return keyName(parent.key)
|
|
196
|
+
return "the function"
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const visitors = {}
|
|
200
|
+
for (const type of BRANCH_NODES) {
|
|
201
|
+
visitors[type] = (node) => context.report({ node, messageId: "branch", data: { what: type === "LogicalExpression" ? `a \`${node.operator}\`` : BRANCH_WHAT[type], role } })
|
|
202
|
+
}
|
|
203
|
+
for (const type of COMPUTE_NODES) {
|
|
204
|
+
visitors[type] = (node) => context.report({ node, messageId: "compute", data: { what: `an \`${node.operator}\``, role } })
|
|
205
|
+
}
|
|
206
|
+
visitors.TemplateLiteral = (node) => {
|
|
207
|
+
if (node.expressions.length > 0) context.report({ node, messageId: "compute", data: { what: "a template literal with an expression", role } })
|
|
208
|
+
}
|
|
209
|
+
visitors["Program:exit"] = (program) => {
|
|
210
|
+
for (const root of rootFunctionsOf(program)) {
|
|
211
|
+
const body = root.type === "MethodDefinition" ? root.value?.body : root.body
|
|
212
|
+
if (!body) continue
|
|
213
|
+
let delegating = 0
|
|
214
|
+
walk(body, (node) => {
|
|
215
|
+
if (node.type !== "CallExpression") return
|
|
216
|
+
const kind = classify(node)
|
|
217
|
+
if (kind === "call") context.report({ node, messageId: "call", data: { what: sourceCode.getText(node.callee), webhook: role === "webhook" ? ", plus `verify` of the signature service" : "" } })
|
|
218
|
+
else if (kind === "delegating") {
|
|
219
|
+
delegating += 1
|
|
220
|
+
if (delegating === 2) context.report({ node, messageId: "many", data: { name: nameOf(root) } })
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return visitors
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The rules this law contributes to the plugin. */
|
|
230
|
+
export const rules = {
|
|
231
|
+
"feature-thin": featureThin,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Every rule of this law at `error`. */
|
|
235
|
+
export const recommended = {
|
|
236
|
+
"starci-be/feature-thin": "error",
|
|
237
|
+
}
|
package/idempotency.mjs
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { hfsOf } from "./lib/hfs.mjs"
|
|
26
26
|
import { walk } from "./lib/ast.mjs"
|
|
27
|
-
import { baseName, isOwnedType } from "./lib/ports.mjs"
|
|
27
|
+
import { baseName, isOwnedType, ownerNameOf } from "./lib/ports.mjs"
|
|
28
28
|
import { typed } from "./lib/types.mjs"
|
|
29
29
|
|
|
30
30
|
/** The `Inbox` port of `platform/inbox`. */
|
|
@@ -99,6 +99,8 @@ export const inboxDedupeRequired = {
|
|
|
99
99
|
const filename = context.filename || context.getFilename()
|
|
100
100
|
const slot = hfs.slotOf(filename)
|
|
101
101
|
if (!baseName(filename).endsWith(".service.ts") || !slot || slot.startsWith("be.tests.")) return {}
|
|
102
|
+
// The event bus publishes events (their shape carries an `eventId`) and its runtime claims the inbox before a consumer runs: no delivery method here.
|
|
103
|
+
if (slot.startsWith("be.platform") && ownerNameOf(hfs, filename) === "event-bus") return {}
|
|
102
104
|
return {
|
|
103
105
|
MethodDefinition(node) {
|
|
104
106
|
if (node.kind !== "method" || node.static || node.key.type === "PrivateIdentifier" || !node.value.body) return
|
package/index.mjs
CHANGED
|
@@ -17,15 +17,21 @@
|
|
|
17
17
|
* leave two names for one rule and no way to tell which a message came from.
|
|
18
18
|
*/
|
|
19
19
|
import { buildBeConfig } from "./lib/config.mjs"
|
|
20
|
+
import { pluginForEdition } from "./lib/edition.mjs"
|
|
21
|
+
import { recommended as ambientIdRecommended, rules as ambientIdRules } from "./ambient-id.mjs"
|
|
20
22
|
import { recommended as asyncDisciplineRecommended, rules as asyncDisciplineRules } from "./async-discipline.mjs"
|
|
21
23
|
import { recommended as authorizationRecommended, rules as authorizationRules } from "./authorization.mjs"
|
|
22
24
|
import { recommended as buildersRecommended, rules as buildersRules } from "./builders.mjs"
|
|
23
25
|
import { recommended as commentsRecommended, rules as commentsRules } from "./comments.mjs"
|
|
24
26
|
import { recommended as configOwnerRecommended, rules as configOwnerRules } from "./config-owner.mjs"
|
|
25
27
|
import { recommended as connectionsRecommended, rules as connectionsRules } from "./connections.mjs"
|
|
28
|
+
import { recommended as cookiesRecommended, rules as cookiesRules } from "./cookies.mjs"
|
|
26
29
|
import { recommended as cqrsRecommended, rules as cqrsRules } from "./cqrs.mjs"
|
|
27
30
|
import { recommended as dataAccessRecommended, rules as dataAccessRules } from "./data-access.mjs"
|
|
28
31
|
import { recommended as defaultDenyRecommended, rules as defaultDenyRules } from "./default-deny.mjs"
|
|
32
|
+
import { recommended as eventBusRecommended, rules as eventBusRules } from "./event-bus.mjs"
|
|
33
|
+
import { recommended as jobsRecommended, rules as jobsRules } from "./jobs.mjs"
|
|
34
|
+
import { recommended as projectionsRecommended, rules as projectionsRules } from "./projections.mjs"
|
|
29
35
|
import { recommended as e2eFlowRecommended, rules as e2eFlowRules } from "./e2e-flow.mjs"
|
|
30
36
|
import { recommended as errorHandlingRecommended, rules as errorHandlingRules } from "./error-handling.mjs"
|
|
31
37
|
import { recommended as idempotencyRecommended, rules as idempotencyRules } from "./idempotency.mjs"
|
|
@@ -41,7 +47,9 @@ import { recommended as operationsRecommended, rules as operationsRules } from "
|
|
|
41
47
|
import { recommended as querySafetyRecommended, rules as querySafetyRules } from "./query-safety.mjs"
|
|
42
48
|
import { recommended as resilienceRecommended, rules as resilienceRules } from "./resilience.mjs"
|
|
43
49
|
import { recommended as schemaAuthorityRecommended, rules as schemaAuthorityRules } from "./schema-authority.mjs"
|
|
50
|
+
import { recommended as cliRecommended, rules as cliRules } from "./cli.mjs"
|
|
44
51
|
import { recommended as serviceDepsRecommended, rules as serviceDepsRules } from "./service-deps.mjs"
|
|
52
|
+
import { recommended as serviceIsolationRecommended, rules as serviceIsolationRules } from "./service-isolation.mjs"
|
|
45
53
|
import { recommended as sizeBudgetRecommended, rules as sizeBudgetRules } from "./size-budget.mjs"
|
|
46
54
|
import { recommended as specNoSkipRecommended, rules as specNoSkipRules } from "./spec-no-skip.mjs"
|
|
47
55
|
import { recommended as specQualityRecommended, rules as specQualityRules } from "./spec-quality.mjs"
|
|
@@ -53,20 +61,29 @@ import { recommended as transportRecommended, rules as transportRules } from "./
|
|
|
53
61
|
import { recommended as typeSafetyRecommended, rules as typeSafetyRules } from "./type-safety.mjs"
|
|
54
62
|
import { recommended as unitSpecRecommended, rules as unitSpecRules } from "./unit-spec.mjs"
|
|
55
63
|
import { recommended as userCopyRecommended, rules as userCopyRules } from "./user-copy.mjs"
|
|
64
|
+
import { recommended as webhooksRecommended, rules as webhooksRules } from "./webhooks.mjs"
|
|
65
|
+
import { recommended as realtimeRecommended, rules as realtimeRules } from "./realtime.mjs"
|
|
66
|
+
import { recommended as featureThinRecommended, rules as featureThinRules } from "./feature-thin.mjs"
|
|
56
67
|
import { recommended as projectGraphRecommended, rules as projectGraphRules } from "./project-graph.mjs"
|
|
57
68
|
|
|
58
69
|
/** Each law's contribution, kept separate so a duplicate name is detectable rather than silent. */
|
|
59
70
|
const CONTRIBUTIONS = [
|
|
60
71
|
{ law: "project-graph", rules: projectGraphRules, recommended: projectGraphRecommended },
|
|
72
|
+
{ law: "ambient-id", rules: ambientIdRules, recommended: ambientIdRecommended },
|
|
61
73
|
{ law: "async-discipline", rules: asyncDisciplineRules, recommended: asyncDisciplineRecommended },
|
|
62
74
|
{ law: "authorization", rules: authorizationRules, recommended: authorizationRecommended },
|
|
63
75
|
{ law: "builders", rules: buildersRules, recommended: buildersRecommended },
|
|
76
|
+
{ law: "cli", rules: cliRules, recommended: cliRecommended },
|
|
64
77
|
{ law: "comments", rules: commentsRules, recommended: commentsRecommended },
|
|
65
78
|
{ law: "config-owner", rules: configOwnerRules, recommended: configOwnerRecommended },
|
|
66
79
|
{ law: "connections", rules: connectionsRules, recommended: connectionsRecommended },
|
|
80
|
+
{ law: "cookies", rules: cookiesRules, recommended: cookiesRecommended },
|
|
67
81
|
{ law: "cqrs", rules: cqrsRules, recommended: cqrsRecommended },
|
|
68
82
|
{ law: "data-access", rules: dataAccessRules, recommended: dataAccessRecommended },
|
|
69
83
|
{ law: "default-deny", rules: defaultDenyRules, recommended: defaultDenyRecommended },
|
|
84
|
+
{ law: "event-bus", rules: eventBusRules, recommended: eventBusRecommended },
|
|
85
|
+
{ law: "jobs", rules: jobsRules, recommended: jobsRecommended },
|
|
86
|
+
{ law: "projections", rules: projectionsRules, recommended: projectionsRecommended },
|
|
70
87
|
{ law: "e2e-flow", rules: e2eFlowRules, recommended: e2eFlowRecommended },
|
|
71
88
|
{ law: "error-handling", rules: errorHandlingRules, recommended: errorHandlingRecommended },
|
|
72
89
|
{ law: "idempotency", rules: idempotencyRules, recommended: idempotencyRecommended },
|
|
@@ -83,6 +100,7 @@ const CONTRIBUTIONS = [
|
|
|
83
100
|
{ law: "resilience", rules: resilienceRules, recommended: resilienceRecommended },
|
|
84
101
|
{ law: "schema-authority", rules: schemaAuthorityRules, recommended: schemaAuthorityRecommended },
|
|
85
102
|
{ law: "service-deps", rules: serviceDepsRules, recommended: serviceDepsRecommended },
|
|
103
|
+
{ law: "service-isolation", rules: serviceIsolationRules, recommended: serviceIsolationRecommended },
|
|
86
104
|
{ law: "size-budget", rules: sizeBudgetRules, recommended: sizeBudgetRecommended },
|
|
87
105
|
{ law: "spec-no-skip", rules: specNoSkipRules, recommended: specNoSkipRecommended },
|
|
88
106
|
{ law: "spec-quality", rules: specQualityRules, recommended: specQualityRecommended },
|
|
@@ -94,6 +112,9 @@ const CONTRIBUTIONS = [
|
|
|
94
112
|
{ law: "type-safety", rules: typeSafetyRules, recommended: typeSafetyRecommended },
|
|
95
113
|
{ law: "unit-spec", rules: unitSpecRules, recommended: unitSpecRecommended },
|
|
96
114
|
{ law: "user-copy", rules: userCopyRules, recommended: userCopyRecommended },
|
|
115
|
+
{ law: "webhooks", rules: webhooksRules, recommended: webhooksRecommended },
|
|
116
|
+
{ law: "realtime", rules: realtimeRules, recommended: realtimeRecommended },
|
|
117
|
+
{ law: "feature-thin", rules: featureThinRules, recommended: featureThinRecommended },
|
|
97
118
|
]
|
|
98
119
|
|
|
99
120
|
/** Every gathered law. */
|
|
@@ -154,7 +175,7 @@ export default plugin
|
|
|
154
175
|
* @param {{ hfs: object }} input - The HFS view of the repository.
|
|
155
176
|
* @returns {Promise<Array<object>>} The flat config.
|
|
156
177
|
*/
|
|
157
|
-
export const starciBeConfig = ({ hfs }) => buildBeConfig({ hfs, plugin, recommended })
|
|
178
|
+
export const starciBeConfig = ({ hfs }) => buildBeConfig({ hfs, plugin: pluginForEdition({ plugin, hfs }), recommended })
|
|
158
179
|
|
|
159
180
|
export { linterOptions } from "./lib/config.mjs"
|
|
160
181
|
export { loadHfs } from "./lib/hfs.mjs"
|
package/infra-owner.mjs
CHANGED
|
@@ -53,7 +53,7 @@ export const infraImportOwner = {
|
|
|
53
53
|
schema: [],
|
|
54
54
|
messages: {
|
|
55
55
|
foreign: "`{{name}}` is owned by {{owners}}. Use that capability's port (injected with its `Inject*()` decorator) instead of the raw library, or move this code into the owner.",
|
|
56
|
-
nowhere: "`{{name}}` is not used in a back end: its job belongs to a platform port (config through the typed `<c>.config.ts`, events through
|
|
56
|
+
nowhere: "`{{name}}` is not used in a back end: its job belongs to a platform port (config through the typed `<c>.config.ts`, events through `eventBus.publish(event, tx)` of `platform/event-bus`, schedules and background jobs through a queue's BullMQ job scheduler of `platform/queue`). Remove it.",
|
|
57
57
|
},
|
|
58
58
|
},
|
|
59
59
|
create(context) {
|
package/injection.mjs
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { posix } from "node:path"
|
|
27
27
|
import ts from "typescript"
|
|
28
|
-
import { staticText } from "./lib/ast.mjs"
|
|
28
|
+
import { paramParts, staticText } from "./lib/ast.mjs"
|
|
29
29
|
import { hfsOf } from "./lib/hfs.mjs"
|
|
30
30
|
import { isImportedFrom } from "./lib/import-source.mjs"
|
|
31
31
|
import { normalizePath } from "./lib/path.mjs"
|
|
@@ -133,13 +133,6 @@ const inComposition = (hfs, file) => {
|
|
|
133
133
|
return owner !== null && posix.basename(owner) === "composition"
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
/** The decorators of a constructor parameter: on the parameter, its wrapper (`private readonly x`) or its inner identifier. */
|
|
137
|
-
const decoratorsOfParam = (param) => {
|
|
138
|
-
const inner = param.type === "TSParameterProperty" ? param.parameter : param
|
|
139
|
-
const target = inner.type === "AssignmentPattern" ? inner.left : inner
|
|
140
|
-
return { target, decorators: [...new Set([...(param.decorators ?? []), ...(inner.decorators ?? []), ...(target.decorators ?? [])])] }
|
|
141
|
-
}
|
|
142
|
-
|
|
143
136
|
/** The injected type `T` of a call typed `TypedParameterDecorator<T>`, else null. */
|
|
144
137
|
const injectedTypeOf = (context, call) => {
|
|
145
138
|
const { checker, toTs } = typed(context)
|
|
@@ -373,7 +366,7 @@ export const injectorTypeMatch = {
|
|
|
373
366
|
return {
|
|
374
367
|
MethodDefinition(node) {
|
|
375
368
|
for (const param of constructorParams(node)) {
|
|
376
|
-
const { target, decorators } =
|
|
369
|
+
const { target, decorators } = paramParts(param)
|
|
377
370
|
for (const decorator of decorators) {
|
|
378
371
|
if (decorator.expression.type !== "CallExpression") continue
|
|
379
372
|
const injected = injectedTypeOf(context, decorator.expression)
|
|
@@ -417,7 +410,25 @@ const isContainerBuilt = (context, constructor) => {
|
|
|
417
410
|
const declaration = constructor.parent?.parent
|
|
418
411
|
if (declaration?.type !== "ClassDeclaration" && declaration?.type !== "ClassExpression") return false
|
|
419
412
|
if ((declaration.decorators ?? []).some((decorator) => packageOfExport(context, decorator.expression)?.startsWith("@nestjs/"))) return true
|
|
420
|
-
return constructorParams(constructor).some((param) =>
|
|
413
|
+
return constructorParams(constructor).some((param) => paramParts(param).decorators.length > 0)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* The origin that makes an annotated type infrastructure: declared by a package, or by a `platform/*` or `integrations/*` owner other
|
|
418
|
+
* than `own`; null for a domain type, a type of the same owner or a language global.
|
|
419
|
+
*/
|
|
420
|
+
const infraOriginOf = (context, hfs, own, annotation) => {
|
|
421
|
+
const origins = typeOrigins(context, annotation)
|
|
422
|
+
// A language global (`ReadonlyArray`, `Array`) is declared in the TypeScript lib and may be augmented by a package
|
|
423
|
+
// (`@types/node`): the augmentation does not make it infrastructure.
|
|
424
|
+
const isLanguageGlobal = (origin) => origins.some((other) => other.name === origin.name && /\/node_modules\/typescript\/lib\//.test(other.file))
|
|
425
|
+
return origins.find((origin) => {
|
|
426
|
+
if (isLanguageGlobal(origin)) return false
|
|
427
|
+
if (origin.module !== null) return true
|
|
428
|
+
if (own !== null && hfs.ownerOf(origin.file) === own) return false
|
|
429
|
+
const tier = hfs.tierOf(origin.file)
|
|
430
|
+
return tier === "platform" || tier === "integrations"
|
|
431
|
+
}) ?? null
|
|
421
432
|
}
|
|
422
433
|
|
|
423
434
|
/** A constructor parameter of an infrastructure type carries an injector. */
|
|
@@ -441,20 +452,10 @@ export const infraNeedsInjector = {
|
|
|
441
452
|
// metadata. A plain class (a CQRS message, a factory-built adapter, a test fake) is never DI-constructed.
|
|
442
453
|
if (!isContainerBuilt(context, node)) return
|
|
443
454
|
for (const param of constructorParams(node)) {
|
|
444
|
-
const { target, decorators } =
|
|
455
|
+
const { target, decorators } = paramParts(param)
|
|
445
456
|
const annotation = target.typeAnnotation?.typeAnnotation
|
|
446
457
|
if (!annotation) continue
|
|
447
|
-
const
|
|
448
|
-
// A language global (`ReadonlyArray`, `Array`) is declared in the TypeScript lib and may be augmented by a package
|
|
449
|
-
// (`@types/node`): the augmentation does not make it infrastructure.
|
|
450
|
-
const isLanguageGlobal = (origin) => origins.some((other) => other.name === origin.name && /\/node_modules\/typescript\/lib\//.test(other.file))
|
|
451
|
-
const infra = origins.find((origin) => {
|
|
452
|
-
if (isLanguageGlobal(origin)) return false
|
|
453
|
-
if (origin.module !== null) return true
|
|
454
|
-
if (own !== null && hfs.ownerOf(origin.file) === own) return false
|
|
455
|
-
const tier = hfs.tierOf(origin.file)
|
|
456
|
-
return tier === "platform" || tier === "integrations"
|
|
457
|
-
})
|
|
458
|
+
const infra = infraOriginOf(context, hfs, own, annotation)
|
|
458
459
|
if (!infra) continue
|
|
459
460
|
const carries = decorators.some((decorator) => {
|
|
460
461
|
const expression = decorator.expression
|
|
@@ -470,6 +471,48 @@ export const infraNeedsInjector = {
|
|
|
470
471
|
},
|
|
471
472
|
}
|
|
472
473
|
|
|
474
|
+
// -- injected-param-name --------------------------------------------------------------------------
|
|
475
|
+
|
|
476
|
+
/** The `ruleParams.be.paramNames` entry for an origin's declared name, or null. */
|
|
477
|
+
const paramEntryOf = (entries, name) => entries.find((entry) => (entry.type !== undefined ? entry.type === name : name.length > entry.typeSuffix.length && name.endsWith(entry.typeSuffix))) ?? null
|
|
478
|
+
|
|
479
|
+
/** An injected port of the table of `paramNames` is named the way every class names it. */
|
|
480
|
+
export const injectedParamName = {
|
|
481
|
+
meta: {
|
|
482
|
+
type: "problem",
|
|
483
|
+
docs: { description: "A constructor parameter that injects a port named in `ruleParams.be.paramNames` (`Clock`, `Logger`, `Cache`, `CommandBus`, `QueryBus`, `EntityManager`, `<Owner>Options`) carries the name that table gives it." },
|
|
484
|
+
schema: [],
|
|
485
|
+
messages: {
|
|
486
|
+
name: "`{{param}}` injects `{{type}}`, and every class names that port {{expected}}. Rename the parameter so a reader finds the same dependency under the same name in every class.",
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
create(context) {
|
|
490
|
+
const hfs = hfsOf(context)
|
|
491
|
+
const entries = hfs.ruleParams.paramNames
|
|
492
|
+
const own = hfs.ownerOf(context.filename || context.getFilename())
|
|
493
|
+
return {
|
|
494
|
+
MethodDefinition(node) {
|
|
495
|
+
if (!isContainerBuilt(context, node)) return
|
|
496
|
+
for (const param of constructorParams(node)) {
|
|
497
|
+
const { target, decorators } = paramParts(param)
|
|
498
|
+
if (target.type !== "Identifier") continue
|
|
499
|
+
const annotation = target.typeAnnotation?.typeAnnotation
|
|
500
|
+
if (!annotation) continue
|
|
501
|
+
const injected = decorators.some((decorator) => decorator.expression.type === "CallExpression" && (INJECT_NAME.test(calleeOf(context, decorator.expression).name ?? "") || injectedTypeOf(context, decorator.expression) !== null))
|
|
502
|
+
if (!injected) continue
|
|
503
|
+
const infra = infraOriginOf(context, hfs, own, annotation)
|
|
504
|
+
if (!infra) continue
|
|
505
|
+
const entry = paramEntryOf(entries, infra.name)
|
|
506
|
+
if (!entry || entry.names.includes(target.name)) continue
|
|
507
|
+
if (entry.nameSuffix !== undefined && target.name.length > entry.nameSuffix.length && target.name.endsWith(entry.nameSuffix)) continue
|
|
508
|
+
const expected = [...entry.names.map((name) => `\`${name}\``), ...(entry.nameSuffix !== undefined ? [`\`<name>${entry.nameSuffix}\``] : [])].join(" or ")
|
|
509
|
+
context.report({ node: target, messageId: "name", data: { param: target.name, type: infra.name, expected } })
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
}
|
|
515
|
+
|
|
473
516
|
// -- no-module-ref ---------------------------------------------------------------------------------
|
|
474
517
|
|
|
475
518
|
const MODULE_REF_CALLS = new Set(["get", "resolve", "create"])
|
|
@@ -585,6 +628,7 @@ export const rules = {
|
|
|
585
628
|
"injector-shape": injectorShape,
|
|
586
629
|
"injector-type-match": injectorTypeMatch,
|
|
587
630
|
"infra-needs-injector": infraNeedsInjector,
|
|
631
|
+
"injected-param-name": injectedParamName,
|
|
588
632
|
"no-module-ref": noModuleRef,
|
|
589
633
|
"no-forward-ref": noForwardRef,
|
|
590
634
|
"no-string-token": noStringToken,
|
|
@@ -596,6 +640,7 @@ export const recommended = {
|
|
|
596
640
|
"starci-be/injector-shape": "error",
|
|
597
641
|
"starci-be/injector-type-match": "error",
|
|
598
642
|
"starci-be/infra-needs-injector": "error",
|
|
643
|
+
"starci-be/injected-param-name": "error",
|
|
599
644
|
"starci-be/no-module-ref": "error",
|
|
600
645
|
"starci-be/no-forward-ref": "error",
|
|
601
646
|
"starci-be/no-string-token": "error",
|
package/input-bounds.mjs
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* argument is `RateTier.Strict` of `platform/http-security`, on the method or on its class. A sign-in, sign-up,
|
|
17
17
|
* password-reset or callback door on the default tier is the brute-force path the strict tier exists for.
|
|
18
18
|
*/
|
|
19
|
+
import { isTransportSlot } from "./lib/transport-slots.mjs"
|
|
19
20
|
import ts from "typescript"
|
|
20
21
|
import { decoratorName, keyName, staticText } from "./lib/ast.mjs"
|
|
21
22
|
import { importsFrom, isOwnedEnumMember, partsOf, presentParts, typeOf } from "./lib/declared.mjs"
|
|
@@ -26,7 +27,7 @@ import { isPackageType, typed } from "./lib/types.mjs"
|
|
|
26
27
|
/** True when the file sits in a transport slot inside a `dto/` folder, judged by the slot and the owner-relative path. */
|
|
27
28
|
const isTransportDto = (hfs, filename) => {
|
|
28
29
|
const slot = hfs.slotOf(filename) ?? ""
|
|
29
|
-
if (!slot
|
|
30
|
+
if (!isTransportSlot(slot) && !slot.startsWith("be.feature.transport.")) return false
|
|
30
31
|
return hfs.relative(filename).split("/").slice(0, -1).includes("dto")
|
|
31
32
|
}
|
|
32
33
|
|