@starci/eslint-canon-be 3.0.8 → 3.0.10
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/LICENSE +21 -0
- package/README.md +12 -6
- package/ambient-id.mjs +90 -0
- package/cli.mjs +181 -0
- package/comments.mjs +24 -36
- 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 +57 -6
- 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/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 -2
- package/project-graph.mjs +5 -2
- package/projections.mjs +117 -0
- package/query-safety.mjs +1 -1
- package/realtime.mjs +181 -0
- package/runtime/engine/config.mjs +486 -0
- package/runtime/engine/invalid-config.mjs +15 -0
- package/runtime/engine/orca-config.mjs +27 -0
- package/runtime/engine/secrets.mjs +130 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +162 -31
- package/runtime/knowledge/hfs/rules.yaml +2059 -0
- package/runtime/knowledge/hfs/slots.yaml +912 -120
- package/runtime/knowledge/patterns/fe/folder.yaml +131 -25
- package/runtime/knowledge/sonar-gate.yaml +52 -18
- package/runtime/modules/models/registry.yaml +553 -0
- package/runtime/modules/models/runtimes.yaml +695 -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 +18 -26
- package/runtime/scripts/api/git/ls-files.mjs +6 -0
- package/runtime/scripts/api/git/remote.mjs +5 -0
- package/runtime/scripts/api/git/rev-parse-query.mjs +5 -0
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +220 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +16 -23
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +16 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +3 -1
- package/runtime/scripts/hfs/architecture/clones.mjs +4 -4
- package/runtime/scripts/hfs/architecture/config.mjs +9 -6
- package/runtime/scripts/hfs/architecture/connection-map.mjs +20 -8
- 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/hfs/architecture/contract-fixture-guard.mjs +4 -10
- package/runtime/scripts/hfs/architecture/contracts.mjs +33 -162
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +58 -29
- package/runtime/scripts/hfs/architecture/doc-language.mjs +1 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +2 -1
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -2
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +5 -2
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend.mjs +17 -43
- package/runtime/scripts/hfs/architecture/hfs.mjs +24 -39
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +1 -1
- package/runtime/scripts/hfs/architecture/index.mjs +17 -4
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +44 -11
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +27 -0
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +30 -6
- package/runtime/scripts/hfs/architecture/next-data.mjs +22 -63
- package/runtime/scripts/hfs/architecture/owners.mjs +35 -15
- package/runtime/scripts/hfs/architecture/register-once.mjs +2 -1
- package/runtime/scripts/hfs/architecture/registration.mjs +23 -81
- package/runtime/scripts/hfs/architecture/required-files.mjs +20 -12
- package/runtime/scripts/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/hfs/architecture/source-names.mjs +27 -147
- package/runtime/scripts/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/hfs/architecture/surface.mjs +9 -3
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +9 -19
- package/runtime/scripts/hfs/architecture/tiers.mjs +47 -5
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +3 -3
- package/runtime/scripts/hfs/architecture/typescript.mjs +12 -26
- 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 +109 -32
- package/runtime/scripts/hfs/param-names.mjs +18 -0
- package/runtime/scripts/hfs/path-findings.mjs +1 -1
- package/runtime/scripts/hfs/project-graph.mjs +3 -3
- package/runtime/scripts/hfs/repo-identity.mjs +9 -6
- package/runtime/scripts/hfs/rule-params-shape.mjs +50 -0
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +6 -5
- package/runtime/scripts/hfs/rules/read.mjs +1 -1
- package/runtime/scripts/hfs/rules/secrets.mjs +3 -3
- package/runtime/scripts/hfs/rules/spec-placement.mjs +1 -1
- package/runtime/scripts/hfs/slots.mjs +78 -108
- package/runtime/scripts/hfs/test-world-slot.mjs +6 -0
- package/runtime/scripts/hfs/typescript-programs.mjs +0 -5
- package/runtime/scripts/hfs/view.mjs +3 -2
- 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/jsdoc.mjs +35 -0
- package/runtime/scripts/lib/json.mjs +13 -3
- package/runtime/scripts/lib/language.mjs +24 -5
- package/runtime/scripts/lib/list.mjs +20 -0
- package/runtime/scripts/lib/mutation-fence.mjs +15 -0
- package/runtime/scripts/lib/path-key.mjs +57 -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 +112 -0
- package/runtime/scripts/lib/stack-services.mjs +7 -31
- package/runtime/scripts/lib/ts-ast.mjs +35 -0
- package/runtime/scripts/lib/walk.mjs +4 -3
- package/schema-authority.mjs +14 -12
- package/service-deps.mjs +7 -13
- package/service-isolation.mjs +72 -0
- package/size-budget.mjs +1 -1
- 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/test-secrets.mjs +0 -116
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which entities a database write call targets, found by TYPE: a write method called on a typeorm database receiver (`EntityManager`,
|
|
3
|
+
* `DataSource`, `QueryRunner`) or on a typeorm query builder, whose arguments (or, for a query builder, the arguments of any call
|
|
4
|
+
* of the same chain) are the entity class or an instance of it. No variable, class or table name is read.
|
|
5
|
+
*/
|
|
6
|
+
import { isDatabaseReceiver } from "./db-writes.mjs"
|
|
7
|
+
import { typeOrigins } from "./types.mjs"
|
|
8
|
+
|
|
9
|
+
/** The methods of a manager or query builder that change rows. */
|
|
10
|
+
export const WRITE_METHODS = Object.freeze(["update", "save", "insert", "upsert", "delete", "remove", "increment", "decrement", "softDelete", "softRemove", "recover", "restore", "clear"])
|
|
11
|
+
|
|
12
|
+
/** True when the node's type is a query builder of `typeorm` (`SelectQueryBuilder`, `UpdateQueryBuilder` ...). */
|
|
13
|
+
const isQueryBuilder = (context, node) => typeOrigins(context, node).some((origin) => origin.module === "typeorm" && origin.name.endsWith("QueryBuilder"))
|
|
14
|
+
|
|
15
|
+
/** The declarations (class origins) of the entities a database write call targets, or `[]` when the node is not such a call. */
|
|
16
|
+
export const writtenEntityOrigins = (context, node) => {
|
|
17
|
+
const callee = node.callee
|
|
18
|
+
if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier" || !WRITE_METHODS.includes(callee.property.name)) return []
|
|
19
|
+
if (!isDatabaseReceiver(context, callee.object) && !isQueryBuilder(context, callee.object)) return []
|
|
20
|
+
const candidates = [...node.arguments]
|
|
21
|
+
for (let link = callee.object; link; ) {
|
|
22
|
+
if (link.type === "CallExpression") {
|
|
23
|
+
candidates.push(...link.arguments)
|
|
24
|
+
link = link.callee.type === "MemberExpression" ? link.callee.object : null
|
|
25
|
+
} else if (link.type === "MemberExpression") link = link.object
|
|
26
|
+
else if (link.type === "TSNonNullExpression" || link.type === "AwaitExpression") link = link.expression ?? link.argument
|
|
27
|
+
else link = null
|
|
28
|
+
}
|
|
29
|
+
// The entity of a builder write may come later in the same chain (`.delete().from(Entity)`, `.insert().into(Entity)`).
|
|
30
|
+
for (let outer = node; outer.parent?.type === "MemberExpression" && outer.parent.object === outer && outer.parent.parent?.type === "CallExpression" && outer.parent.parent.callee === outer.parent; ) {
|
|
31
|
+
outer = outer.parent.parent
|
|
32
|
+
candidates.push(...outer.arguments)
|
|
33
|
+
}
|
|
34
|
+
return candidates.flatMap((candidate) => typeOrigins(context, candidate)).filter((origin) => origin.module === null)
|
|
35
|
+
}
|
package/lib/path.mjs
CHANGED
|
@@ -4,12 +4,6 @@ export const normalizePath = (filename) => String(filename || "").replace(/\\/g,
|
|
|
4
4
|
/** A spec file: the unit lane, where a fixture may build a deliberately wrong value. */
|
|
5
5
|
export const isSpecFile = (filename) => /\.(?:spec|test)\.[cm]?[jt]sx?$|-spec\.[cm]?[jt]sx?$/.test(normalizePath(filename))
|
|
6
6
|
|
|
7
|
-
/** Any file of the test lanes: a spec, or anything under `src/tests/`. */
|
|
8
|
-
export const isTestLane = (filename) => {
|
|
9
|
-
const file = normalizePath(filename)
|
|
10
|
-
return isSpecFile(file) || file.includes("/src/tests/")
|
|
11
|
-
}
|
|
12
|
-
|
|
13
7
|
/** A migration: the one place schema-changing SQL is allowed to be written. */
|
|
14
8
|
export const isMigrationFile = (filename) => /\/persistence\/migrations\/[^/]+\.[cm]?[jt]s$/.test(normalizePath(filename))
|
|
15
9
|
|
package/lib/persistence.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* `typeorm`'s; a file is the database capability's because the HFS slot manifest says so.
|
|
7
7
|
*/
|
|
8
8
|
import { normalizePath } from "./path.mjs"
|
|
9
|
+
import { ownerNameOf } from "./ports.mjs"
|
|
9
10
|
import { typeOrigins } from "./types.mjs"
|
|
10
11
|
|
|
11
12
|
/** The `typeorm` types a class must never receive except through the platform database capability. */
|
|
@@ -65,24 +66,14 @@ export const inDatabaseCapability = (hfs, file) => {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
/**
|
|
68
|
-
* Whether a file
|
|
69
|
+
* Whether a file is the command line of the back end: the cli app (slot `be.app.cli`) or the cli feature root (slot `be.cli`,
|
|
70
|
+
* `src/features/cli/`), where every one-off action runs, the migrate and seed commands among them.
|
|
69
71
|
*
|
|
70
72
|
* @param {object} hfs - The HFS view of the repository.
|
|
71
73
|
* @param {string} file - A file path.
|
|
72
|
-
* @returns {boolean} True for
|
|
74
|
+
* @returns {boolean} True for the files of the cli app and the cli feature root.
|
|
73
75
|
*/
|
|
74
|
-
export const
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Whether a file is the test world: slot `be.tests.world` (`src/tests/world/**`), the one test location that owns
|
|
78
|
-
* the shared infrastructure (containers, apps/migrate bootstrap, teardown) and so may build and hold a `DataSource`.
|
|
79
|
-
* Specs (unit and e2e) take `world.db.<connection>` instead (owner ruling on R83/R47, 2026-09-30).
|
|
80
|
-
*
|
|
81
|
-
* @param {object} hfs - The HFS view of the repository.
|
|
82
|
-
* @param {string} file - A file path.
|
|
83
|
-
* @returns {boolean} True for `be.tests.world` files.
|
|
84
|
-
*/
|
|
85
|
-
export const inTestBootstrap = (hfs, file) => hfs.slotOf(file) === "be.tests.world"
|
|
76
|
+
export const inCli = (hfs, file) => hfs.slotOf(file) === "be.app.cli" || hfs.slotOf(file) === "be.cli"
|
|
86
77
|
|
|
87
78
|
/**
|
|
88
79
|
* Whether a file is a migration: a `be.persistence` file below its `migrations/` folder.
|
|
@@ -113,11 +104,11 @@ export const connectionFileOf = (hfs, file, role) => {
|
|
|
113
104
|
return connectionsOf(hfs).find((connection) => connection.name === name) ?? null
|
|
114
105
|
}
|
|
115
106
|
|
|
116
|
-
/** The platform capabilities that own a `persistence/` and so may hold an `EntityManager
|
|
117
|
-
const PERSISTENCE_CAPABILITIES = ["database", "inbox", "
|
|
107
|
+
/** The platform capabilities that own a `persistence/` and so may hold an `EntityManager` (a pattern capability is named by its folder). */
|
|
108
|
+
const PERSISTENCE_CAPABILITIES = ["database", "inbox", "event-bus", "queue", "jobs", "saga", "outbox"]
|
|
118
109
|
|
|
119
110
|
/**
|
|
120
|
-
* Whether a class in this file may receive an `EntityManager`: an application handler, a domain service, a platform persistence capability.
|
|
111
|
+
* Whether a class in this file may receive an `EntityManager`: an application handler, a domain service, a projection, a typed queue producer (it takes the transaction of its caller), a platform persistence capability.
|
|
121
112
|
*
|
|
122
113
|
* @param {object} hfs - The HFS view of the repository.
|
|
123
114
|
* @param {string} file - A file path.
|
|
@@ -128,7 +119,9 @@ export const mayHoldEntityManager = (hfs, file) => {
|
|
|
128
119
|
const base = baseNameOf(file)
|
|
129
120
|
if (found.slot === "be.feature.application") return base.endsWith(".handler.ts")
|
|
130
121
|
if (found.slot === "be.domain") return base.endsWith(".service.ts")
|
|
131
|
-
|
|
122
|
+
if (found.slot === "be.projections") return base.endsWith(".projection.ts")
|
|
123
|
+
if (found.slot === "be.queues") return base.endsWith(".queue.ts")
|
|
124
|
+
return typeof found.slot === "string" && found.slot.startsWith("be.platform") && PERSISTENCE_CAPABILITIES.includes(ownerNameOf(hfs, file))
|
|
132
125
|
}
|
|
133
126
|
|
|
134
127
|
/**
|
package/lib/ports.mjs
CHANGED
|
@@ -23,7 +23,10 @@ export const baseName = (file) => String(file).replace(/\\/g, "/").split("/").po
|
|
|
23
23
|
*/
|
|
24
24
|
export const ownerNameOf = (hfs, file) => {
|
|
25
25
|
const bindings = hfs.classify(file).bindings ?? {}
|
|
26
|
-
|
|
26
|
+
if (bindings.capability ?? bindings.feature) return bindings.capability ?? bindings.feature
|
|
27
|
+
// A pattern slot names its capability literally (`src/modules/platform/event-bus/`), so no variable binds it: the owner folder is its name.
|
|
28
|
+
const root = hfs.ownerOf(file)
|
|
29
|
+
return root === null || root === undefined ? null : root.split("/").pop()
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
/**
|
|
@@ -33,7 +36,7 @@ export const ownerNameOf = (hfs, file) => {
|
|
|
33
36
|
* @param {string} file - A file path.
|
|
34
37
|
* @returns {string | null} The slot-relative path.
|
|
35
38
|
*/
|
|
36
|
-
|
|
39
|
+
const pathInSlot = (hfs, file) => {
|
|
37
40
|
const found = hfs.classify(file)
|
|
38
41
|
if (!found.slot) return null
|
|
39
42
|
const rel = hfs.relative(file)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an import specifier points. A `compilerOptions.paths` alias resolves through the program's own options, so a
|
|
3
|
+
* rule asks the same question TypeScript asks instead of maintaining a second alias table.
|
|
4
|
+
*/
|
|
5
|
+
import { posix } from "node:path"
|
|
6
|
+
import { typed } from "./types.mjs"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The absolute path an alias specifier maps to under the program's `compilerOptions.paths`, or null when no pattern matches.
|
|
10
|
+
*
|
|
11
|
+
* @param {object} context - The ESLint rule context.
|
|
12
|
+
* @param {string} specifier - An import specifier.
|
|
13
|
+
* @returns {{ target: string, rest: string } | null} The absolute target without extension and the part the wildcard matched.
|
|
14
|
+
*/
|
|
15
|
+
export const resolveAlias = (context, specifier) => {
|
|
16
|
+
const options = typed(context).program.getCompilerOptions()
|
|
17
|
+
const base = options.baseUrl ?? options.pathsBasePath
|
|
18
|
+
if (!options.paths || typeof base !== "string") return null
|
|
19
|
+
for (const [pattern, targets] of Object.entries(options.paths)) {
|
|
20
|
+
if (!pattern.endsWith("/*") || !targets[0]) continue
|
|
21
|
+
const prefix = pattern.slice(0, -1)
|
|
22
|
+
if (!specifier.startsWith(prefix)) continue
|
|
23
|
+
const rest = specifier.slice(prefix.length)
|
|
24
|
+
return { target: posix.normalize(`${base.replace(/\\/g, "/")}/${targets[0].replace("*", rest)}`), rest }
|
|
25
|
+
}
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Questions a rule asks about database transactions: the callback of a `.transaction(...)` call and the variable an identifier
|
|
3
|
+
* resolves to. The receiver of `.transaction` is judged by its TYPE (typeorm's `EntityManager` or `DataSource`), never by its name.
|
|
4
|
+
*/
|
|
5
|
+
import { infraTypeOf } from "./persistence.mjs"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The callback of a `.transaction(...)` call on a manager or data source, or null.
|
|
9
|
+
*
|
|
10
|
+
* @param {object} context - The ESLint rule context.
|
|
11
|
+
* @param {object} node - A CallExpression.
|
|
12
|
+
* @returns {object | null} The arrow or function expression that receives the transactional manager.
|
|
13
|
+
*/
|
|
14
|
+
export const transactionCallback = (context, node) => {
|
|
15
|
+
const callee = node.callee
|
|
16
|
+
if (callee.type !== "MemberExpression" || callee.computed) return null
|
|
17
|
+
if (callee.property.type !== "Identifier" || callee.property.name !== "transaction") return null
|
|
18
|
+
const receiver = infraTypeOf(context, callee.object)
|
|
19
|
+
if (receiver !== "EntityManager" && receiver !== "DataSource") return null
|
|
20
|
+
const callback = node.arguments[node.arguments.length - 1]
|
|
21
|
+
return callback && (callback.type === "ArrowFunctionExpression" || callback.type === "FunctionExpression") ? callback : null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The variable an identifier resolves to, looking outward from the identifier's own scope.
|
|
26
|
+
*
|
|
27
|
+
* @param {object} sourceCode - The ESLint source code.
|
|
28
|
+
* @param {object} node - An Identifier.
|
|
29
|
+
* @returns {object | null} The scope variable.
|
|
30
|
+
*/
|
|
31
|
+
export const resolveVariable = (sourceCode, node) => {
|
|
32
|
+
for (let scope = sourceCode.getScope(node); scope; scope = scope.upper) {
|
|
33
|
+
const variable = scope.set.get(node.name)
|
|
34
|
+
if (variable) return variable
|
|
35
|
+
}
|
|
36
|
+
return null
|
|
37
|
+
}
|
package/lib/transport-slots.mjs
CHANGED
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
* The transport slots of the HFS manifest, in one place.
|
|
3
3
|
*
|
|
4
4
|
* A slot id is manifest vocabulary, not a path: every rule that judges "a transport file" asks this instead of spelling
|
|
5
|
-
* the ids itself.
|
|
5
|
+
* the ids itself. A cli command is no transport: it is an action runner of the cli feature root (slot `be.cli`). The
|
|
6
|
+
* webhook and realtime kinds hold their doors in slots of their own (`be.feature.webhooks.http`, `be.feature.realtime.graphql`, `be.feature.realtime.websocket`): a provider webhook and a
|
|
7
|
+
* push channel are doors like any other and answer to the door laws (default deny, bounded input, no envelope).
|
|
6
8
|
*/
|
|
7
9
|
|
|
10
|
+
/** The slots of the feature kinds that are doors but are not an api's `transport/<protocol>/` folders. */
|
|
11
|
+
export const KIND_DOOR_SLOTS = Object.freeze(["be.feature.webhooks.http", "be.feature.realtime.graphql", "be.feature.realtime.websocket"])
|
|
12
|
+
|
|
8
13
|
/**
|
|
9
|
-
* True for a slot that holds transport code (`be.transport.<protocol>` and the
|
|
14
|
+
* True for a slot that holds transport code (`be.transport.<protocol>` and the door slots of the webhook and realtime kinds).
|
|
10
15
|
*
|
|
11
16
|
* @param {string | null} slot - A slot id from `hfs.slotOf`.
|
|
12
17
|
* @returns {boolean} Whether files of that slot are transport files.
|
|
13
18
|
*/
|
|
14
|
-
export const isTransportSlot = (slot) => typeof slot === "string" && (slot.startsWith("be.transport.") || slot
|
|
19
|
+
export const isTransportSlot = (slot) => typeof slot === "string" && (slot.startsWith("be.transport.") || KIND_DOOR_SLOTS.includes(slot))
|
package/lib/unit-spec.mjs
CHANGED
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
* What is a unit spec, decided by the slot manifest and the file's role suffix, never by a path pattern.
|
|
3
3
|
*
|
|
4
4
|
* A unit spec is a `*.spec.ts` outside the folders that own the other test kinds (`src/tests/{e2e,integration,contract,world}`)
|
|
5
|
-
* only. Only a `<name>.
|
|
6
|
-
*
|
|
5
|
+
* only. Only a `<name>.<role>.spec.ts` beside `<name>.<role>.ts` is a legitimate unit spec (R47): of a spec-required unit role
|
|
6
|
+
* (`ruleParams.be.unitRoles`: a service, a cli command) anywhere its slot admits, and of any other LOGIC role (`ruleParams.be.logicRoles`: a policy,
|
|
7
|
+
* a client, a guard, a mapper, ...) inside a slot whose `coverage` is required, where the spec is optional (the file owes 100 per file, not a spec of
|
|
8
|
+
* its own). So is the spec of a door of a feature kind that has no service of its own
|
|
9
|
+
* (`<provider>.webhook.spec.ts`, `<channel>.gateway.spec.ts`, `<channel>.subscription.spec.ts` beside its door, in the kind's slot);
|
|
10
|
+
* the quality rules of R48 judge the service specs.
|
|
7
11
|
*/
|
|
8
12
|
import { basename } from "node:path"
|
|
9
13
|
|
|
@@ -29,5 +33,62 @@ export const isUnitSpecFile = (hfs, filename) => {
|
|
|
29
33
|
/** Whether a file is a `<name>.service.spec.ts` unit spec. */
|
|
30
34
|
export const isServiceSpecFile = (hfs, filename) => /\.service\.spec\.ts$/.test(baseOf(filename)) && isUnitSpecFile(hfs, filename)
|
|
31
35
|
|
|
36
|
+
/**
|
|
37
|
+
* The unit-tested roles of the back end: `ruleParams.be.unitRoles` of the slot manifest, the ONE list (`[{ role, spec, slot? }]`).
|
|
38
|
+
*
|
|
39
|
+
* @param {object} hfs - The HFS view.
|
|
40
|
+
* @returns {ReadonlyArray<{ role: string, spec: string, slot?: string }>} The roles.
|
|
41
|
+
*/
|
|
42
|
+
export const unitRoles = (hfs) => hfs.ruleParams.unitRoles ?? []
|
|
43
|
+
|
|
44
|
+
/** Whether the slot that owns a file is measured at 100 per file (`coverage: required`): the home of the optional specs of the logic roles. */
|
|
45
|
+
const inCoverageSlot = (hfs, filename) => {
|
|
46
|
+
const slot = hfs.slotOf(filename)
|
|
47
|
+
return slot !== null && hfs.slot(slot)?.coverage === "required"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The roles whose spec is legal but not required: every logic role that is not a spec-required unit role. */
|
|
51
|
+
const optionalRoles = (hfs) => (hfs.ruleParams.logicRoles ?? []).filter((role) => !unitRoles(hfs).some((unit) => unit.role === role)).map((role) => ({ role, spec: `${role}.spec`, optional: true }))
|
|
52
|
+
|
|
53
|
+
/** Whether a role admits a file by its slot (a role with no slot admits any slot; an optional logic role only a coverage-required slot). */
|
|
54
|
+
const inRoleSlot = (hfs, role, filename) => (role.optional === true ? inCoverageSlot(hfs, filename) : role.slot === undefined || hfs.slotOf(filename) === role.slot)
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The unit-tested role whose spec a file is (`<name>.<role>.spec.ts`, in the role's slot), or null.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} hfs - The HFS view.
|
|
60
|
+
* @param {string} filename - The linted file.
|
|
61
|
+
* @returns {object | null} The role entry.
|
|
62
|
+
*/
|
|
63
|
+
export const unitRoleOfSpec = (hfs, filename) => {
|
|
64
|
+
if (!isUnitSpecFile(hfs, filename)) return null
|
|
65
|
+
const name = baseOf(filename)
|
|
66
|
+
return [...unitRoles(hfs), ...optionalRoles(hfs)].find((role) => name.endsWith(`.${role.spec}.ts`) && inRoleSlot(hfs, role, filename)) ?? null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The unit-tested role whose subject a file is (`<name>.<role>.ts`, in the role's slot, outside the test tree), or null.
|
|
71
|
+
*
|
|
72
|
+
* @param {object} hfs - The HFS view.
|
|
73
|
+
* @param {string} filename - The linted file.
|
|
74
|
+
* @returns {object | null} The role entry.
|
|
75
|
+
*/
|
|
76
|
+
export const unitRoleOfSubject = (hfs, filename) => {
|
|
77
|
+
const name = baseOf(filename)
|
|
78
|
+
const slot = hfs.slotOf(filename)
|
|
79
|
+
if (!slot || slot.startsWith("be.tests.")) return null
|
|
80
|
+
return unitRoles(hfs).find((role) => name.endsWith(`.${role.role}.ts`) && !name.endsWith(`.${role.spec}.ts`) && inRoleSlot(hfs, role, filename)) ?? null
|
|
81
|
+
}
|
|
82
|
+
|
|
32
83
|
/** The `<name>` of a `<name>.service.spec.ts`. */
|
|
33
84
|
export const serviceNameOfSpec = (filename) => baseOf(filename).replace(/\.service\.spec\.ts$/, "")
|
|
85
|
+
|
|
86
|
+
/** The slots of the feature kinds whose door carries its own unit spec beside it. */
|
|
87
|
+
const DOOR_SPEC_SLOTS = new Set(["be.feature.webhooks.http", "be.feature.realtime.graphql", "be.feature.realtime.websocket"])
|
|
88
|
+
|
|
89
|
+
/** The door a `<name>.<role>.spec.ts` of a feature kind tests, or null: `payment.webhook.spec.ts` gives `payment.webhook.ts`. */
|
|
90
|
+
export const doorOfSpec = (hfs, filename) => {
|
|
91
|
+
if (!isUnitSpecFile(hfs, filename) || !DOOR_SPEC_SLOTS.has(hfs.slotOf(filename) ?? "")) return null
|
|
92
|
+
const match = /^(.+.(?:webhook|gateway|subscription)).spec.ts$/.exec(baseOf(filename))
|
|
93
|
+
return match ? `${match[1]}.ts` : null
|
|
94
|
+
}
|
package/module-layering.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* view; none of them reads a path pattern, and an alias is resolved through the program's own `compilerOptions.paths`:
|
|
7
7
|
*
|
|
8
8
|
* - `import-owner-entry` - a cross-owner import targets the owner's `index.ts` (`@modules/domain/plan`,
|
|
9
|
-
* `@features/plan`), never a file inside it, and an alias that names a tier but no owner (`@modules/domain`) names
|
|
9
|
+
* `@features/api/plan`), never a file inside it, and an alias that names a tier but no owner (`@modules/domain`) names
|
|
10
10
|
* nothing importable. A same-owner import is relative and never goes through the owner's own `index.ts`.
|
|
11
11
|
* - `no-self-module-alias` - inside an owner, imports are relative, never the owner's own alias.
|
|
12
12
|
* - `no-relative-capability-escape` - a relative import never walks out of its own owner.
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { posix } from "node:path"
|
|
21
21
|
import { hfsOf } from "./lib/hfs.mjs"
|
|
22
22
|
import { baseName } from "./lib/ports.mjs"
|
|
23
|
-
import {
|
|
23
|
+
import { resolveAlias } from "./lib/specifier.mjs"
|
|
24
24
|
|
|
25
25
|
/** The tiers whose files belong to an owner with a root `index.ts`. */
|
|
26
26
|
const OWNER_TIERS = new Set(["feature", "domain", "platform", "integrations"])
|
|
@@ -45,27 +45,6 @@ const absolute = (hfs, rel) => `${hfs.repoRoot.replace(/\\/g, "/")}/${rel}`
|
|
|
45
45
|
/** The owner an import path (no extension, absolute) reaches: the owner of `<path>/index.ts`, which any file or folder of that owner shares. */
|
|
46
46
|
const ownerOfTarget = (hfs, target) => ownerDirOf(hfs, `${target}/index.ts`)
|
|
47
47
|
|
|
48
|
-
/**
|
|
49
|
-
* The absolute path an alias specifier maps to under the program's `compilerOptions.paths`, or null when no pattern matches.
|
|
50
|
-
*
|
|
51
|
-
* @param {object} context - The ESLint rule context.
|
|
52
|
-
* @param {string} specifier - An import specifier.
|
|
53
|
-
* @returns {{ target: string, rest: string } | null} The absolute target without extension and the part the wildcard matched.
|
|
54
|
-
*/
|
|
55
|
-
const resolveAlias = (context, specifier) => {
|
|
56
|
-
const options = typed(context).program.getCompilerOptions()
|
|
57
|
-
const base = options.baseUrl ?? options.pathsBasePath
|
|
58
|
-
if (!options.paths || typeof base !== "string") return null
|
|
59
|
-
for (const [pattern, targets] of Object.entries(options.paths)) {
|
|
60
|
-
if (!pattern.endsWith("/*") || !targets[0]) continue
|
|
61
|
-
const prefix = pattern.slice(0, -1)
|
|
62
|
-
if (!specifier.startsWith(prefix)) continue
|
|
63
|
-
const rest = specifier.slice(prefix.length)
|
|
64
|
-
return { target: posix.normalize(`${base.replace(/\\/g, "/")}/${targets[0].replace("*", rest)}`), rest }
|
|
65
|
-
}
|
|
66
|
-
return null
|
|
67
|
-
}
|
|
68
|
-
|
|
69
48
|
/** A specifier that names a directory by construction: `.`, `./`, `..`, `../`, or a trailing `/`. */
|
|
70
49
|
const isBareFolderSpecifier = (specifier) =>
|
|
71
50
|
specifier === "." || specifier === "./" || specifier === ".." || specifier === "../" || specifier.endsWith("/")
|
|
@@ -105,7 +84,7 @@ export const importOwnerEntry = {
|
|
|
105
84
|
schema: [],
|
|
106
85
|
messages: {
|
|
107
86
|
barrel:
|
|
108
|
-
"`{{specifier}}` names a tier, not an owner. Import an owner's public entry, for example `@modules/domain/plan` or `@features/plan`.",
|
|
87
|
+
"`{{specifier}}` names a tier, not an owner. Import an owner's public entry, for example `@modules/domain/plan` or `@features/api/plan`.",
|
|
109
88
|
deep:
|
|
110
89
|
"`{{specifier}}` reaches into a file of another owner. Import the owner's public entry (its `index.ts`) and export what you need from there.",
|
|
111
90
|
ownIndex:
|
package/module-shape.mjs
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
import ts from "typescript"
|
|
30
30
|
import { decoratorName, keyName, walk } from "./lib/ast.mjs"
|
|
31
31
|
import { importsFrom } from "./lib/declared.mjs"
|
|
32
|
-
import { hfsOf } from "./lib/hfs.mjs"
|
|
32
|
+
import { hfsOf, inTestWorld } from "./lib/hfs.mjs"
|
|
33
33
|
import { isDeclarationFile, isSpecFile, normalizePath } from "./lib/path.mjs"
|
|
34
34
|
import { typed } from "./lib/types.mjs"
|
|
35
35
|
|
|
@@ -70,7 +70,7 @@ const isAppModule = (hfs, filename) => (hfs.slotOf(filename) ?? "").startsWith("
|
|
|
70
70
|
* capability modules with `isGlobal: true` the way `apps/<app>/src/app.module.ts` does in production. A spec (integration,
|
|
71
71
|
* contract, e2e) does not compose and stays refused: it calls `useTestWorld(...)` only.
|
|
72
72
|
*/
|
|
73
|
-
const isWorld =
|
|
73
|
+
const isWorld = inTestWorld
|
|
74
74
|
|
|
75
75
|
const isTrue = (node) => node?.type === "Literal" && node.value === true
|
|
76
76
|
|
package/naming.mjs
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* that. Those are read by a person, which is why the law states them with the scars attached.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
import { hfsOf } from "./lib/hfs.mjs"
|
|
33
|
+
import { hfsOf, inTestWorld } from "./lib/hfs.mjs"
|
|
34
34
|
import { normalizePath } from "./lib/path.mjs"
|
|
35
35
|
|
|
36
36
|
/** A schema generation baked into an identifier. */
|
|
@@ -203,7 +203,7 @@ export const noBareVerbExport = {
|
|
|
203
203
|
// -- no-default-export -------------------------------------------------------------------------------------------
|
|
204
204
|
|
|
205
205
|
/** The one file kind that may default-export: a Jest global setup, whose API is a default function. */
|
|
206
|
-
const isGlobalSetup = (context) => hfsOf(context)
|
|
206
|
+
const isGlobalSetup = (context) => inTestWorld(hfsOf(context), context.filename) && /\/global-(?:setup|teardown)\.ts$/.test(normalizePath(context.filename))
|
|
207
207
|
|
|
208
208
|
/** Named exports only: a default export has no name to grep, rename or import-check. */
|
|
209
209
|
export const noDefaultExport = {
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/eslint-canon-be",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.10",
|
|
4
4
|
"description": "The StarCi back-end lint canon: every rule its laws publish, gathered as one flat-config plugin.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"license": "
|
|
6
|
+
"license": "MIT",
|
|
7
7
|
"private": false,
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./index.mjs"
|
|
10
10
|
},
|
|
11
11
|
"main": "./index.mjs",
|
|
12
12
|
"files": [
|
|
13
|
+
"LICENSE",
|
|
13
14
|
"*.mjs",
|
|
14
15
|
"lib/*.mjs",
|
|
15
16
|
"runtime/**",
|
|
@@ -28,6 +29,8 @@
|
|
|
28
29
|
"access": "public"
|
|
29
30
|
},
|
|
30
31
|
"scripts": {
|
|
32
|
+
"prepack": "node ../../hfs/prep-runtime.mjs",
|
|
33
|
+
"pretest": "node ../../hfs/prep-runtime.mjs",
|
|
31
34
|
"test": "node --test \"*.spec.mjs\""
|
|
32
35
|
},
|
|
33
36
|
"keywords": [
|
package/project-graph.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* One graph per lint run: `scripts/hfs/project-graph.mjs` (a byte copy in this package's runtime/) builds the machine's module
|
|
5
5
|
* graph and slot manifest once for the repository, caches it, and each rule below reports the findings of its own codes on
|
|
6
6
|
* the file ESLint is visiting, on the line the machine names. There is no second graph and no second judge: the list of
|
|
7
|
-
* rules and their codes is `LINT_ENFORCERS` of the machine (scripts/hfs/architecture/surface.mjs); `
|
|
7
|
+
* rules and their codes is `LINT_ENFORCERS` of the machine (scripts/hfs/architecture/surface.mjs); `starci app check` keeps the
|
|
8
8
|
* findings that have no TypeScript file to sit on. A graph that cannot be built stops the run with the machine's message.
|
|
9
9
|
*/
|
|
10
10
|
import { LINT_ENFORCERS } from "./runtime/scripts/hfs/architecture/surface.mjs"
|
|
@@ -17,7 +17,10 @@ const RUNTIME = join(dirname(fileURLToPath(import.meta.url)), "runtime")
|
|
|
17
17
|
const mine = LINT_ENFORCERS.filter((enforcer) => enforcer.plugins.includes("be"))
|
|
18
18
|
|
|
19
19
|
/** The rules this law contributes to the plugin: one per graph enforcer of this profile. */
|
|
20
|
-
export const rules = Object.fromEntries(mine.map((
|
|
20
|
+
export const rules = Object.fromEntries(mine.map((enforcer) => [
|
|
21
|
+
enforcer.id,
|
|
22
|
+
projectRule({ enforcer, runtimeRoot: RUNTIME, hfsOf }),
|
|
23
|
+
]))
|
|
21
24
|
|
|
22
25
|
/** Every one at error. */
|
|
23
26
|
export const recommended = Object.fromEntries(mine.map((e) => [`starci-be/${e.id}`, "error"]))
|
package/projections.mjs
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rules that hold the projection pattern (catalog R161 `BE_PROJECTION_WRITE_OWNER`, R162 `BE_PROJECTION_SHAPE`).
|
|
3
|
+
*
|
|
4
|
+
* A projection is a read model: `modules/projections/<name>/` holds `<name>.projection.ts` (the only class that touches its table:
|
|
5
|
+
* `recompute*` writes it idempotently from the source facts and can replay from scratch, `get*` reads it) and
|
|
6
|
+
* `<name>.projection-entity.ts` (the table's entity). A reactor, a job or a cli command triggers a recompute; an api feature only calls `get*`.
|
|
7
|
+
* A projection publishes no event (R152 refuses `eventBus.publish` outside `modules/domain`).
|
|
8
|
+
*
|
|
9
|
+
* Entities are found by TYPE ORIGIN; the owner and the kind of a file by the slot view. The vocabulary `recompute` and `get` is the
|
|
10
|
+
* pattern's own method contract.
|
|
11
|
+
*/
|
|
12
|
+
import { writtenEntityOrigins } from "./lib/entity-writes.mjs"
|
|
13
|
+
import { hfsOf, inTestWorld } from "./lib/hfs.mjs"
|
|
14
|
+
import { baseName } from "./lib/ports.mjs"
|
|
15
|
+
import { typed } from "./lib/types.mjs"
|
|
16
|
+
|
|
17
|
+
const PROJECTION_SLOT = "be.projections"
|
|
18
|
+
const ENTITY_SUFFIX = ".projection-entity.ts"
|
|
19
|
+
const PROJECTION_SUFFIX = ".projection.ts"
|
|
20
|
+
|
|
21
|
+
/** True for the test composition, which seeds and inspects tables directly. */
|
|
22
|
+
const isTestComposition = (hfs, file) => {
|
|
23
|
+
const tier = hfs.tierOf(file)
|
|
24
|
+
return tier === "e2e" || tier === "fixtures" || inTestWorld(hfs, file)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The word a method name starts with (`recomputeUserXp` gives `recompute`), or null when the name has no camelCase boundary. */
|
|
28
|
+
const verbOf = (name) => {
|
|
29
|
+
const match = /^[a-z]+/.exec(name)
|
|
30
|
+
return match && (match[0].length === name.length || /[A-Z]/.test(name[match[0].length])) ? match[0] : null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Only a projection writes its table, from its own `<name>.projection.ts`. */
|
|
34
|
+
export const projectionWriteOwner = {
|
|
35
|
+
meta: {
|
|
36
|
+
type: "problem",
|
|
37
|
+
docs: { description: "A projection's entity is written only by the `*.projection.ts` of the same projection folder." },
|
|
38
|
+
schema: [],
|
|
39
|
+
messages: {
|
|
40
|
+
foreign: "`{{call}}` writes the read-model table of a projection from outside its `<name>.projection.ts`. A read model has one writer: its projection's `recompute*` method, which is idempotent and can replay from the source facts. Ask the projection to recompute (from a reactor or a job) instead of writing its table.",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
create(context) {
|
|
44
|
+
const hfs = hfsOf(context)
|
|
45
|
+
const file = context.filename
|
|
46
|
+
if (isTestComposition(hfs, file)) return {}
|
|
47
|
+
return {
|
|
48
|
+
CallExpression(node) {
|
|
49
|
+
const entities = writtenEntityOrigins(context, node).filter((origin) => hfs.slotOf(origin.file) === PROJECTION_SLOT && baseName(origin.file).endsWith(ENTITY_SUFFIX))
|
|
50
|
+
if (entities.length === 0) return
|
|
51
|
+
const mine = hfs.slotOf(file) === PROJECTION_SLOT && baseName(file).endsWith(PROJECTION_SUFFIX)
|
|
52
|
+
if (mine && entities.every((origin) => hfs.ownerOf(origin.file) === hfs.ownerOf(file))) return
|
|
53
|
+
context.report({ node, messageId: "foreign", data: { call: context.sourceCode.getText(node.callee) } })
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The slot of an owner's root file: its kind lives there (`trigger`). */
|
|
60
|
+
const triggerOfOwner = (hfs, file) => {
|
|
61
|
+
const root = hfs.ownerOf(file)
|
|
62
|
+
const slot = root ? hfs.slotOf(`${root}/index.ts`) : null
|
|
63
|
+
return slot ? (hfs.slot(slot)?.trigger ?? null) : null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The projection class speaks only `recompute*` and `get*`, never exports its entity, and an api feature never recomputes. */
|
|
67
|
+
export const projectionShape = {
|
|
68
|
+
meta: {
|
|
69
|
+
type: "problem",
|
|
70
|
+
docs: { description: "A `*.projection.ts` class has only `recompute*` and `get*` public methods, a projection's `index.ts` exports no projection entity, and an api feature calls only `get*`." },
|
|
71
|
+
schema: [],
|
|
72
|
+
messages: {
|
|
73
|
+
vocabulary: "`{{name}}` is a public method of a projection that is neither `recompute*` nor `get*`. A projection recomputes its table from the source facts (idempotent, replayable) or reads it; anything else belongs in a domain service.",
|
|
74
|
+
entityExported: "This `index.ts` exports from `{{source}}`. The entity of a projection stays inside its folder: the rest of the code reads through `get*` and never sees the table's shape.",
|
|
75
|
+
apiRecompute: "`{{call}}` recomputes a projection from an api feature. The api reads only through `get*`; a reactor, a job or a cli command keeps the read model current.",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
create(context) {
|
|
79
|
+
const hfs = hfsOf(context)
|
|
80
|
+
const file = context.filename
|
|
81
|
+
const found = hfs.classify(file)
|
|
82
|
+
const base = baseName(file)
|
|
83
|
+
const inProjection = found.slot === PROJECTION_SLOT
|
|
84
|
+
const trigger = triggerOfOwner(hfs, file)
|
|
85
|
+
return {
|
|
86
|
+
MethodDefinition(node) {
|
|
87
|
+
if (!inProjection || !base.endsWith(PROJECTION_SUFFIX) || node.kind !== "method" || node.computed || node.key.type !== "Identifier") return
|
|
88
|
+
if (node.accessibility === "private" || node.accessibility === "protected" || node.key.type === "PrivateIdentifier") return
|
|
89
|
+
const verb = verbOf(node.key.name)
|
|
90
|
+
if (verb !== "recompute" && verb !== "get") context.report({ node: node.key, messageId: "vocabulary", data: { name: node.key.name } })
|
|
91
|
+
},
|
|
92
|
+
ExportNamedDeclaration(node) {
|
|
93
|
+
if (!inProjection || base !== "index.ts" || !node.source) return
|
|
94
|
+
if (String(node.source.value).endsWith(".projection-entity")) context.report({ node, messageId: "entityExported", data: { source: node.source.value } })
|
|
95
|
+
},
|
|
96
|
+
CallExpression(node) {
|
|
97
|
+
if (trigger !== "api" || node.callee.type !== "MemberExpression" || node.callee.computed || node.callee.property.type !== "Identifier") return
|
|
98
|
+
if (verbOf(node.callee.property.name) !== "recompute") return
|
|
99
|
+
const { checker, toTs } = typed(context)
|
|
100
|
+
const tsNode = toTs(node)
|
|
101
|
+
const declaration = tsNode ? checker.getResolvedSignature(tsNode)?.declaration : undefined
|
|
102
|
+
if (declaration && hfs.slotOf(declaration.getSourceFile().fileName) === PROJECTION_SLOT) context.report({ node, messageId: "apiRecompute", data: { call: context.sourceCode.getText(node.callee) } })
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const rules = {
|
|
109
|
+
"projection-write-owner": projectionWriteOwner,
|
|
110
|
+
"projection-shape": projectionShape,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The level these laws ask for: `error`, switched off nowhere. */
|
|
114
|
+
export const recommended = {
|
|
115
|
+
"starci-be/projection-write-owner": "error",
|
|
116
|
+
"starci-be/projection-shape": "error",
|
|
117
|
+
}
|
package/query-safety.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* - `query-needs-limit` (R69 `BE_QUERY_UNBOUNDED`) - `find`, `findBy` and `findAndCount` on an `EntityManager` carry `take`.
|
|
16
16
|
* - `no-query-in-loop` (R77 `BE_QUERY_IN_LOOP`) - an `EntityManager` read does not run once per element of a loop.
|
|
17
17
|
*
|
|
18
|
-
* A migration is hand-written DDL run once by
|
|
18
|
+
* A migration is hand-written DDL run once by the cli migrate command; its `queryRunner.query(...)` takes plain text.
|
|
19
19
|
*/
|
|
20
20
|
import { keyName } from "./lib/ast.mjs"
|
|
21
21
|
import { hfsOf } from "./lib/hfs.mjs"
|