@starci/hfs 4.0.10 → 4.1.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/CHANGELOG.md +1 -1
- package/emit/operations.mjs +1 -1
- package/emit/type-schema.mjs +2 -1
- package/lint/run.mjs +3 -2
- package/package.json +1 -1
- package/report/order.mjs +2 -0
- package/runtime/config.example.yaml +187 -0
- package/runtime/engine/by-code-unit.mjs +12 -0
- package/runtime/engine/config.mjs +221 -239
- package/runtime/engine/invalid-config.mjs +5 -5
- package/runtime/engine/model-config.mjs +90 -0
- package/runtime/engine/orca-config.mjs +3 -1
- package/runtime/engine/removed-vocabulary.mjs +28 -0
- package/runtime/engine/resources-config.mjs +52 -0
- package/runtime/engine/runtime-root.mjs +17 -0
- package/runtime/engine/secrets.mjs +35 -31
- package/runtime/engine/sonar-config.mjs +21 -0
- package/runtime/engine/temp-root.mjs +32 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
- package/runtime/knowledge/hfs/rules.yaml +98 -65
- package/runtime/knowledge/hfs/slots.yaml +11 -14
- package/runtime/knowledge/patterns/be/api.yaml +7 -24
- package/runtime/knowledge/patterns/be/cli.yaml +3 -12
- package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
- package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
- package/runtime/modules/kernel/failure-codes.yaml +1 -41
- package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
- package/runtime/modules/models/registry.yaml +25 -359
- package/runtime/modules/models/runtimes.yaml +92 -256
- package/runtime/modules/models/tiers.yaml +65 -0
- package/runtime/scripts/api/fs/claim-file.mjs +30 -0
- package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
- package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
- package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
- package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
- package/runtime/scripts/api/git/lib.mjs +34 -1
- package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
- package/runtime/scripts/api/process/run-program.mjs +2 -1
- package/runtime/scripts/api/sops/lib.mjs +198 -120
- package/runtime/scripts/hfs/allows.mjs +3 -3
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
- package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
- package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
- package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
- package/runtime/scripts/hfs/architecture/config.mjs +95 -80
- package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
- package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
- package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
- package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
- package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
- package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
- package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
- package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
- package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
- package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
- package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
- package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
- package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
- package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
- package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
- package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
- package/runtime/scripts/hfs/architecture/index.mjs +158 -102
- package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
- package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
- package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
- package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
- package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
- package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
- package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
- package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
- package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
- package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
- package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
- package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
- package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
- package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
- package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
- package/runtime/scripts/hfs/architecture.mjs +3 -4
- package/runtime/scripts/hfs/check.mjs +61 -70
- package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
- package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
- package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
- package/runtime/scripts/hfs/edition-slots.mjs +5 -3
- package/runtime/scripts/hfs/linear-text.mjs +30 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
- package/runtime/scripts/hfs/path-findings.mjs +58 -38
- package/runtime/scripts/hfs/pin-findings.mjs +31 -0
- package/runtime/scripts/hfs/repo-identity.mjs +5 -2
- package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
- package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
- package/runtime/scripts/hfs/rules/cli.mjs +5 -1
- package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
- package/runtime/scripts/hfs/rules/contract.mjs +35 -27
- package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
- package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
- package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
- package/runtime/scripts/hfs/rules/database.mjs +36 -23
- package/runtime/scripts/hfs/rules/deps.mjs +64 -32
- package/runtime/scripts/hfs/rules/docker.mjs +113 -65
- package/runtime/scripts/hfs/rules/edition.mjs +118 -111
- package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
- package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
- package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
- package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
- package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
- package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
- package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
- package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
- package/runtime/scripts/hfs/rules/saga.mjs +128 -61
- package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
- package/runtime/scripts/hfs/rules/services.mjs +34 -16
- package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
- package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
- package/runtime/scripts/hfs/secret.mjs +59 -48
- package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
- package/runtime/scripts/hfs/slot-classify.mjs +65 -0
- package/runtime/scripts/hfs/slot-errors.mjs +11 -0
- package/runtime/scripts/hfs/slot-imports.mjs +76 -0
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
- package/runtime/scripts/hfs/slot-match.mjs +153 -0
- package/runtime/scripts/hfs/slot-path.mjs +8 -0
- package/runtime/scripts/hfs/slot-required.mjs +76 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
- package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
- package/runtime/scripts/hfs/slots.mjs +72 -693
- package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
- package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
- package/runtime/scripts/hfs/tree.mjs +19 -14
- package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
- package/runtime/scripts/hfs/view.mjs +1 -1
- package/runtime/scripts/lib/dockerfile.mjs +56 -33
- package/runtime/scripts/lib/event-contract.mjs +17 -13
- package/runtime/scripts/lib/git.mjs +2 -2
- package/runtime/scripts/lib/graphql-contract.mjs +158 -322
- package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
- package/runtime/scripts/lib/i18n.mjs +3 -2
- package/runtime/scripts/lib/in-order.mjs +71 -0
- package/runtime/scripts/lib/language.mjs +3 -3
- package/runtime/scripts/lib/list.mjs +8 -1
- package/runtime/scripts/lib/path-key.mjs +38 -5
- package/runtime/scripts/lib/pid-alive.mjs +7 -0
- package/runtime/scripts/lib/regex.mjs +1 -1
- package/runtime/scripts/lib/same-text.mjs +1 -1
- package/runtime/scripts/lib/secret-patterns.mjs +6 -2
- package/runtime/scripts/lib/sleep-sync.mjs +2 -2
- package/runtime/scripts/lib/sops-envelope.mjs +95 -48
- package/runtime/scripts/lib/stack-services.mjs +1 -2
- package/runtime/scripts/lib/ts-ast.mjs +9 -0
- package/runtime/scripts/lib/walk.mjs +6 -1
- package/scaffold/add-cli-lite.mjs +2 -1
- package/scaffold/add-table.mjs +2 -2
- package/scaffold/add.mjs +3 -2
- package/scaffold/app.mjs +4 -2
- package/scaffold/edition-gate.mjs +25 -25
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +5 -4
- package/sync/hygiene.mjs +15 -9
- package/sync/index.mjs +7 -3
- package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
- package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
- package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
- package/templates/app/ci-workflows/sonar-steps.yml +2 -2
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
- package/templates/app/starciwork.gitignore +1 -1
- package/templates/be/image/api/Dockerfile +1 -1
- package/templates/be/image/cli/Dockerfile +1 -1
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
- package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
- package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
- package/templates/fe/image/next/Dockerfile +2 -1
- package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
- package/upgrade/index.mjs +9 -2
- package/runtime/engine/admission.mjs +0 -308
- package/runtime/engine/canonical-json.mjs +0 -10
- package/runtime/engine/db/blob.mjs +0 -315
- package/runtime/engine/db/ledger-paths.mjs +0 -83
- package/runtime/engine/db/ledger.mjs +0 -1118
- package/runtime/engine/db/machine-connection.mjs +0 -135
- package/runtime/engine/db/machine-schema.mjs +0 -84
- package/runtime/engine/db/machine.mjs +0 -1367
- package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
- package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
- package/runtime/engine/db/provider-reservations.mjs +0 -101
- package/runtime/engine/digest.mjs +0 -16
- package/runtime/engine/refuse.mjs +0 -11
- package/runtime/modules/ops/_labels.yaml +0 -53
- package/runtime/scripts/api/node/lib.mjs +0 -14
- package/runtime/scripts/api/node/spawn-node.mjs +0 -6
- package/runtime/scripts/api/process/lib.mjs +0 -111
- package/runtime/scripts/api/process/owned-process.mjs +0 -78
- package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
- package/runtime/scripts/connectors/lib.mjs +0 -488
- package/runtime/scripts/lib/clip.mjs +0 -19
- package/runtime/scripts/lib/display-names.mjs +0 -259
- package/runtime/scripts/lib/example-refs.mjs +0 -158
- package/runtime/scripts/lib/json-schema.mjs +0 -52
- package/runtime/scripts/lib/process-identity.mjs +0 -6
- package/runtime/scripts/lib/read-yaml.mjs +0 -18
- package/runtime/scripts/lib/redact.mjs +0 -161
- package/runtime/scripts/lib/sleep.mjs +0 -7
- package/runtime/scripts/lib/source-phrases.mjs +0 -34
- package/runtime/scripts/lib/sqlite.mjs +0 -21
|
@@ -4,6 +4,140 @@ import { DATABASE_DIR } from './connection-map.mjs';
|
|
|
4
4
|
|
|
5
5
|
const camel = name => { const text = pascal(name); return text[0].toLowerCase() + text.slice(1); };
|
|
6
6
|
|
|
7
|
+
const unparenWhile = (node, isWrapper) => {
|
|
8
|
+
let current = node;
|
|
9
|
+
while (current && isWrapper(current)) current = current.expression;
|
|
10
|
+
return current;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const isExpressionWrapper = (ts, node) => ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isNonNullExpression(node);
|
|
14
|
+
|
|
15
|
+
const unparenRegistration = (ts, node) => unparenWhile(node, current => isExpressionWrapper(ts, current) || ts.isSatisfiesExpression?.(current));
|
|
16
|
+
|
|
17
|
+
const returnedExpression = (ts, declaration) => {
|
|
18
|
+
const fn = ts.isVariableDeclaration(declaration) && declaration.initializer ? unparenRegistration(ts, declaration.initializer) : declaration;
|
|
19
|
+
if (!(ts.isArrowFunction(fn) || ts.isFunctionDeclaration(fn) || ts.isFunctionExpression(fn) || ts.isMethodDeclaration(fn)) || !fn.body) return null;
|
|
20
|
+
if (!ts.isBlock(fn.body)) return fn.body;
|
|
21
|
+
const statements = fn.body.statements.filter(ts.isReturnStatement);
|
|
22
|
+
return statements.length === 1 ? statements[0].expression ?? null : null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Every `name:` property assignment of an object literal over the program: [{declaration, initializer, checker}], memoised per name. */
|
|
26
|
+
const assignmentsOf = (ctx, name) => {
|
|
27
|
+
if (ctx.propertySources.has(name)) return ctx.propertySources.get(name);
|
|
28
|
+
const { kit } = ctx;
|
|
29
|
+
const { ts } = kit;
|
|
30
|
+
const list = [];
|
|
31
|
+
for (const file of ctx.graph.files.values()) {
|
|
32
|
+
if (!file.sourceFile.text.includes(name)) continue;
|
|
33
|
+
const checker = kit.checkerOf(file.sourceFile);
|
|
34
|
+
kit.walk(file.sourceFile, node => {
|
|
35
|
+
if (ts.isPropertyAssignment(node) && kit.propertyNameText(node.name) === name && ts.isObjectLiteralExpression(node.parent)) {
|
|
36
|
+
// the property of the type the literal is checked against, so a typed const and a typed return meet the same declaration
|
|
37
|
+
const contextual = checker.getContextualType(node.parent)?.getProperty(name);
|
|
38
|
+
const symbol = contextual ?? checker.getSymbolAtLocation(node.name);
|
|
39
|
+
list.push({ declaration: symbol?.declarations?.[0] ?? null, initializer: node.initializer, checker });
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
ctx.propertySources.set(name, list);
|
|
45
|
+
return list;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const nameOfLiteral = (ctx, checker, literal, depth) => {
|
|
49
|
+
const { kit } = ctx;
|
|
50
|
+
const own = kit.propertyOf(literal, 'name');
|
|
51
|
+
if (own) return kit.stringValue(checker, kit.valueOfProperty(own));
|
|
52
|
+
for (const property of literal.properties) {
|
|
53
|
+
if (!kit.ts.isSpreadAssignment(property)) continue;
|
|
54
|
+
const value = nameOfExpression(ctx, checker, property.expression, depth + 1);
|
|
55
|
+
if (value) return value;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const nameFromCall = (ctx, checker, node, depth) => {
|
|
61
|
+
const { kit } = ctx;
|
|
62
|
+
for (const declaration of kit.declarationsOf(checker, node.expression)) {
|
|
63
|
+
const body = returnedExpression(kit.ts, declaration);
|
|
64
|
+
const value = body ? nameOfExpression(ctx, kit.checkerOf(declaration.getSourceFile()), body, depth + 1) : null;
|
|
65
|
+
if (value) return value;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const nameFromIdentifier = (ctx, checker, node, depth) => {
|
|
71
|
+
const { kit } = ctx;
|
|
72
|
+
const declaration = kit.declarationsOf(checker, node)[0];
|
|
73
|
+
return declaration && kit.ts.isVariableDeclaration(declaration) && declaration.initializer
|
|
74
|
+
? nameOfExpression(ctx, kit.checkerOf(declaration.getSourceFile()), declaration.initializer, depth + 1) : null;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const nameFromProperty = (ctx, checker, node, depth) => {
|
|
78
|
+
const target = ctx.kit.declarationsOf(checker, node.name)[0];
|
|
79
|
+
if (!target) return null;
|
|
80
|
+
const names = new Set(assignmentsOf(ctx, node.name.text).filter(item => item.declaration === target)
|
|
81
|
+
.map(item => nameOfExpression(ctx, item.checker, item.initializer, depth + 1)).filter(Boolean));
|
|
82
|
+
return names.size === 1 ? [...names][0] : null;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
function nameOfExpression(ctx, checker, expression, depth) {
|
|
86
|
+
const { ts } = ctx.kit;
|
|
87
|
+
const node = unparenRegistration(ts, expression);
|
|
88
|
+
if (!node || depth > 5) return null;
|
|
89
|
+
if (ts.isObjectLiteralExpression(node)) return nameOfLiteral(ctx, checker, node, depth);
|
|
90
|
+
if (ts.isCallExpression(node)) return nameFromCall(ctx, checker, node, depth);
|
|
91
|
+
if (ts.isIdentifier(node)) return nameFromIdentifier(ctx, checker, node, depth);
|
|
92
|
+
if (ts.isPropertyAccessExpression(node)) return nameFromProperty(ctx, checker, node, depth);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const isCapabilityArray = (owner, name) => owner?.below.length === 1 && owner.below[0] === 'connection.ts' && [`${camel(owner.capability)}Entities`, `${camel(owner.capability)}Migrations`].includes(name);
|
|
97
|
+
|
|
98
|
+
const arrayDeclarationOf = (kit, checker, node) => {
|
|
99
|
+
const { ts } = kit;
|
|
100
|
+
const declaration = kit.declarationsOf(checker, ts.isPropertyAccessExpression(node) ? node.name : node)[0];
|
|
101
|
+
return declaration && ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name) ? declaration : null;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** The capability arrays (`<c>Entities` / `<c>Migrations` of persistence/connection.ts) an expression lists. */
|
|
105
|
+
function arraysIn(ctx, checker, expression, out, depth) {
|
|
106
|
+
const { kit } = ctx;
|
|
107
|
+
const { ts } = kit;
|
|
108
|
+
const node = unparenRegistration(ts, expression);
|
|
109
|
+
if (!node || depth > 6) return;
|
|
110
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
111
|
+
for (const element of node.elements) { arraysIn(ctx, checker, ts.isSpreadElement(element) ? element.expression : element, out, depth + 1); }
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node)) return;
|
|
115
|
+
const declaration = arrayDeclarationOf(kit, checker, node);
|
|
116
|
+
if (!declaration) return;
|
|
117
|
+
const rel = kit.graphPath(declaration);
|
|
118
|
+
const owner = rel ? ctx.persistenceOf(rel) : null;
|
|
119
|
+
if (isCapabilityArray(owner, declaration.name.text)) {
|
|
120
|
+
out.push({ key: path.posix.dirname(owner.root), name: owner.capability });
|
|
121
|
+
} else if (declaration.initializer) arraysIn(ctx, kit.checkerOf(declaration.getSourceFile()), declaration.initializer, out, depth + 1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const registrationArrays = (ctx, checker, literal) => {
|
|
125
|
+
const arrays = [];
|
|
126
|
+
for (const key of ['entities', 'migrations']) {
|
|
127
|
+
const property = ctx.kit.propertyOf(literal, key);
|
|
128
|
+
if (property) arraysIn(ctx, checker, ctx.kit.valueOfProperty(property), arrays, 0);
|
|
129
|
+
}
|
|
130
|
+
return arrays;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const recordRegistration = (registered, arrays, connection, site) => {
|
|
134
|
+
for (const { key, name } of new Map(arrays.map(item => [item.key, item])).values()) {
|
|
135
|
+
if (!registered.has(key)) registered.set(key, { name, connections: new Map() });
|
|
136
|
+
const sites = registered.get(key).connections;
|
|
137
|
+
if (!sites.has(connection)) sites.set(connection, site);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
7
141
|
/**
|
|
8
142
|
* Where the persistence arrays of every capability are registered: the object literals (`DatabaseModule.register` connections and the
|
|
9
143
|
* migrate/cli app's) that list `<c>Entities` / `<c>Migrations` of a capability's `persistence/connection.ts` under a connection `name`.
|
|
@@ -15,105 +149,20 @@ const camel = name => { const text = pascal(name); return text[0].toLowerCase()
|
|
|
15
149
|
*/
|
|
16
150
|
export function collectRegistrations({ kit, graph, persistenceOf }) {
|
|
17
151
|
const { ts } = kit;
|
|
18
|
-
const
|
|
19
|
-
const returned = declaration => {
|
|
20
|
-
const fn = ts.isVariableDeclaration(declaration) && declaration.initializer ? unparen(declaration.initializer) : declaration;
|
|
21
|
-
if (!(ts.isArrowFunction(fn) || ts.isFunctionDeclaration(fn) || ts.isFunctionExpression(fn) || ts.isMethodDeclaration(fn)) || !fn.body) return null;
|
|
22
|
-
if (!ts.isBlock(fn.body)) return fn.body;
|
|
23
|
-
const statements = fn.body.statements.filter(ts.isReturnStatement);
|
|
24
|
-
return statements.length === 1 ? statements[0].expression ?? null : null;
|
|
25
|
-
};
|
|
26
|
-
const propertySources = new Map(); // property name -> [{declaration, initializer, checker}] over the program
|
|
27
|
-
const assignmentsOf = name => {
|
|
28
|
-
if (propertySources.has(name)) return propertySources.get(name);
|
|
29
|
-
const list = [];
|
|
30
|
-
for (const file of graph.files.values()) {
|
|
31
|
-
if (!file.sourceFile.text.includes(name)) continue;
|
|
32
|
-
const checker = kit.checkerOf(file.sourceFile);
|
|
33
|
-
kit.walk(file.sourceFile, node => {
|
|
34
|
-
if (ts.isPropertyAssignment(node) && kit.propertyNameText(node.name) === name && ts.isObjectLiteralExpression(node.parent)) {
|
|
35
|
-
// the property of the type the literal is checked against, so a typed const and a typed return meet the same declaration
|
|
36
|
-
const contextual = checker.getContextualType(node.parent)?.getProperty(name);
|
|
37
|
-
const symbol = contextual ?? checker.getSymbolAtLocation(node.name);
|
|
38
|
-
list.push({ declaration: symbol?.declarations?.[0] ?? null, initializer: node.initializer, checker });
|
|
39
|
-
}
|
|
40
|
-
return true;
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
propertySources.set(name, list);
|
|
44
|
-
return list;
|
|
45
|
-
};
|
|
46
|
-
const nameOfLiteral = (checker, literal, depth) => {
|
|
47
|
-
const own = kit.propertyOf(literal, 'name');
|
|
48
|
-
if (own) return kit.stringValue(checker, kit.valueOfProperty(own));
|
|
49
|
-
for (const property of literal.properties) {
|
|
50
|
-
if (!ts.isSpreadAssignment(property)) continue;
|
|
51
|
-
const value = nameOfExpression(checker, property.expression, depth + 1);
|
|
52
|
-
if (value) return value;
|
|
53
|
-
}
|
|
54
|
-
return null;
|
|
55
|
-
};
|
|
56
|
-
const nameOfExpression = (checker, expression, depth) => {
|
|
57
|
-
const node = unparen(expression);
|
|
58
|
-
if (!node || depth > 5) return null;
|
|
59
|
-
if (ts.isObjectLiteralExpression(node)) return nameOfLiteral(checker, node, depth);
|
|
60
|
-
if (ts.isCallExpression(node)) {
|
|
61
|
-
for (const declaration of kit.declarationsOf(checker, node.expression)) {
|
|
62
|
-
const body = returned(declaration);
|
|
63
|
-
const value = body ? nameOfExpression(kit.checkerOf(declaration.getSourceFile()), body, depth + 1) : null;
|
|
64
|
-
if (value) return value;
|
|
65
|
-
}
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
if (ts.isIdentifier(node)) {
|
|
69
|
-
const declaration = kit.declarationsOf(checker, node)[0];
|
|
70
|
-
return declaration && ts.isVariableDeclaration(declaration) && declaration.initializer ? nameOfExpression(kit.checkerOf(declaration.getSourceFile()), declaration.initializer, depth + 1) : null;
|
|
71
|
-
}
|
|
72
|
-
if (ts.isPropertyAccessExpression(node)) {
|
|
73
|
-
const target = kit.declarationsOf(checker, node.name)[0];
|
|
74
|
-
if (!target) return null;
|
|
75
|
-
const names = new Set(assignmentsOf(node.name.text).filter(item => item.declaration === target).map(item => nameOfExpression(item.checker, item.initializer, depth + 1)).filter(Boolean));
|
|
76
|
-
return names.size === 1 ? [...names][0] : null;
|
|
77
|
-
}
|
|
78
|
-
return null;
|
|
79
|
-
};
|
|
80
|
-
/** The capability arrays (`<c>Entities` / `<c>Migrations` of persistence/connection.ts) an expression lists. */
|
|
81
|
-
const arraysIn = (checker, expression, out, depth) => {
|
|
82
|
-
const node = unparen(expression);
|
|
83
|
-
if (!node || depth > 6) return;
|
|
84
|
-
if (ts.isArrayLiteralExpression(node)) { for (const element of node.elements) arraysIn(checker, ts.isSpreadElement(element) ? element.expression : element, out, depth + 1); return; }
|
|
85
|
-
if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node)) return;
|
|
86
|
-
const declaration = kit.declarationsOf(checker, ts.isPropertyAccessExpression(node) ? node.name : node)[0];
|
|
87
|
-
if (!declaration || !ts.isVariableDeclaration(declaration) || !ts.isIdentifier(declaration.name)) return;
|
|
88
|
-
const rel = kit.graphPath(declaration);
|
|
89
|
-
const owner = rel ? persistenceOf(rel) : null;
|
|
90
|
-
if (owner && owner.below.length === 1 && owner.below[0] === 'connection.ts' && [`${camel(owner.capability)}Entities`, `${camel(owner.capability)}Migrations`].includes(declaration.name.text)) {
|
|
91
|
-
out.push({ key: path.posix.dirname(owner.root), name: owner.capability });
|
|
92
|
-
} else if (declaration.initializer) arraysIn(kit.checkerOf(declaration.getSourceFile()), declaration.initializer, out, depth + 1);
|
|
93
|
-
};
|
|
152
|
+
const ctx = { kit, graph, persistenceOf, propertySources: new Map() }; // property name -> [{declaration, initializer, checker}] over the program
|
|
94
153
|
const all = []; // every registration literal: {file, node, connection | null when the name cannot be resolved}
|
|
95
154
|
const registered = new Map(); // capability root -> {name, connections: Map(connection -> first site)}
|
|
96
155
|
let registrations = 0;
|
|
97
156
|
for (const file of graph.files.values()) {
|
|
98
157
|
const checker = kit.checkerOf(file.sourceFile);
|
|
99
158
|
kit.walk(file.sourceFile, node => {
|
|
100
|
-
if (!ts.isObjectLiteralExpression(node)) return
|
|
101
|
-
const arrays =
|
|
102
|
-
|
|
103
|
-
const property = kit.propertyOf(node, key);
|
|
104
|
-
if (property) arraysIn(checker, kit.valueOfProperty(property), arrays, 0);
|
|
105
|
-
}
|
|
106
|
-
if (!arrays.length) return true;
|
|
159
|
+
if (!ts.isObjectLiteralExpression(node)) return;
|
|
160
|
+
const arrays = registrationArrays(ctx, checker, node);
|
|
161
|
+
if (!arrays.length) return;
|
|
107
162
|
registrations += 1;
|
|
108
|
-
const connection = nameOfLiteral(checker, node, 0);
|
|
163
|
+
const connection = nameOfLiteral(ctx, checker, node, 0);
|
|
109
164
|
all.push({ file, node, connection });
|
|
110
|
-
if (connection
|
|
111
|
-
for (const { key, name } of new Map(arrays.map(item => [item.key, item])).values()) {
|
|
112
|
-
if (!registered.has(key)) registered.set(key, { name, connections: new Map() });
|
|
113
|
-
const sites = registered.get(key).connections;
|
|
114
|
-
if (!sites.has(connection)) sites.set(connection, { file, node });
|
|
115
|
-
}
|
|
116
|
-
return true;
|
|
165
|
+
if (connection !== null) recordRegistration(registered, arrays, connection, { file, node });
|
|
117
166
|
});
|
|
118
167
|
}
|
|
119
168
|
return { registered, registrations, all };
|
|
@@ -137,6 +186,54 @@ export const persistenceOfFactory = resolver => rel => {
|
|
|
137
186
|
return { root: classified.root, capability: path.posix.basename(path.posix.dirname(classified.root)), folder: below.length > 1 ? below[0] : '', below };
|
|
138
187
|
};
|
|
139
188
|
|
|
189
|
+
/** The declared connection named by a decorator whose callee is declared in src/modules/platform/database/<conn>.decorators.ts, else null. */
|
|
190
|
+
const connectionOfDecorators = (kit, declared, checker, declaration) => {
|
|
191
|
+
const { ts } = kit;
|
|
192
|
+
for (const decorator of kit.decorators(declaration)) {
|
|
193
|
+
const callee = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
|
|
194
|
+
for (const target of kit.declarationsOf(checker, callee)) {
|
|
195
|
+
const rel = kit.graphPath(target);
|
|
196
|
+
const name = rel ? [...declared.keys()].find(connection => rel === `${DATABASE_DIR}/${connection}.decorators.ts`) : null;
|
|
197
|
+
if (name) return name;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const managerTarget = (ts, node) => {
|
|
204
|
+
if (ts.isPropertyAccessExpression(node)) return node.name;
|
|
205
|
+
return ts.isIdentifier(node) ? node : null;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/** The receiver expression of the `.transaction(callback)` call whose first callback parameter is `declaration`, else null. */
|
|
209
|
+
const transactionReceiver = (ts, declaration) => {
|
|
210
|
+
const callback = declaration.parent;
|
|
211
|
+
if (!ts.isParameter(declaration) || !(ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) || callback.parameters[0] !== declaration) return null;
|
|
212
|
+
const call = callback.parent;
|
|
213
|
+
if (!ts.isCallExpression(call) || !call.arguments.includes(callback) || !ts.isPropertyAccessExpression(call.expression) || call.expression.name.text !== 'transaction') return null;
|
|
214
|
+
return call.expression.expression;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The declared connection whose entity manager `expression` is: a property or parameter injected with `Inject<Conn>EntityManager`, or the
|
|
219
|
+
* manager parameter of the callback of a `.transaction(...)` on such a manager. Null when the origin cannot be proven (never guessed).
|
|
220
|
+
*/
|
|
221
|
+
function connectionOfManagerIn(kit, declared, checker, expression, depth = 0) {
|
|
222
|
+
const { ts } = kit;
|
|
223
|
+
const node = unparenWhile(expression, current => isExpressionWrapper(ts, current));
|
|
224
|
+
if (!node || depth > 4) return null;
|
|
225
|
+
const target = managerTarget(ts, node);
|
|
226
|
+
if (!target) return null;
|
|
227
|
+
const declaration = kit.declarationsOf(checker, target)[0];
|
|
228
|
+
if (!declaration) return null;
|
|
229
|
+
if (ts.isPropertyDeclaration(declaration) || ts.isParameter(declaration)) {
|
|
230
|
+
const direct = connectionOfDecorators(kit, declared, checker, declaration);
|
|
231
|
+
if (direct) return direct;
|
|
232
|
+
}
|
|
233
|
+
const receiver = transactionReceiver(ts, declaration);
|
|
234
|
+
return receiver ? connectionOfManagerIn(kit, declared, checker, receiver, depth + 1) : null;
|
|
235
|
+
}
|
|
236
|
+
|
|
140
237
|
/**
|
|
141
238
|
* The bounded-context model of a program: which context (declared connection) each capability belongs to, and which connection an expression's
|
|
142
239
|
* entity manager is. A context is a connection; a capability belongs to the one declared connection its persistence arrays are registered on
|
|
@@ -167,47 +264,35 @@ export function contextModelOf(kit, graph) {
|
|
|
167
264
|
if (!file?.owner || !CONTEXT_TIERS.has(file.tier)) return null;
|
|
168
265
|
return contextOfCapability(file.owner.root);
|
|
169
266
|
};
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const callee = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
|
|
175
|
-
for (const target of kit.declarationsOf(checker, callee)) {
|
|
176
|
-
const rel = kit.graphPath(target);
|
|
177
|
-
const name = rel ? [...declared.keys()].find(connection => rel === `${DATABASE_DIR}/${connection}.decorators.ts`) : null;
|
|
178
|
-
if (name) return name;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return null;
|
|
267
|
+
const model = {
|
|
268
|
+
declared, registered, registrations: all, perConnection, contextOfCapability, contextOfFile,
|
|
269
|
+
connectionOfManager: (checker, expression) => connectionOfManagerIn(kit, declared, checker, expression),
|
|
270
|
+
unparen: node => unparenWhile(node, current => isExpressionWrapper(ts, current)),
|
|
182
271
|
};
|
|
183
|
-
/**
|
|
184
|
-
* The declared connection whose entity manager `expression` is: a property or parameter injected with `Inject<Conn>EntityManager`, or the
|
|
185
|
-
* manager parameter of the callback of a `.transaction(...)` on such a manager. Null when the origin cannot be proven (never guessed).
|
|
186
|
-
*/
|
|
187
|
-
const connectionOfManager = (checker, expression, depth = 0) => {
|
|
188
|
-
const node = unparen(expression);
|
|
189
|
-
if (!node || depth > 4) return null;
|
|
190
|
-
const target = ts.isPropertyAccessExpression(node) ? node.name : (ts.isIdentifier(node) ? node : null);
|
|
191
|
-
if (!target) return null;
|
|
192
|
-
const declaration = kit.declarationsOf(checker, target)[0];
|
|
193
|
-
if (!declaration) return null;
|
|
194
|
-
if (ts.isPropertyDeclaration(declaration) || ts.isParameter(declaration)) {
|
|
195
|
-
const direct = connectionOfDecorators(checker, declaration);
|
|
196
|
-
if (direct) return direct;
|
|
197
|
-
}
|
|
198
|
-
if (ts.isParameter(declaration) && (ts.isArrowFunction(declaration.parent) || ts.isFunctionExpression(declaration.parent)) && declaration.parent.parameters[0] === declaration) {
|
|
199
|
-
const call = declaration.parent.parent;
|
|
200
|
-
if (ts.isCallExpression(call) && call.arguments.includes(declaration.parent) && ts.isPropertyAccessExpression(call.expression) && call.expression.name.text === 'transaction') {
|
|
201
|
-
return connectionOfManager(checker, call.expression.expression, depth + 1);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return null;
|
|
205
|
-
};
|
|
206
|
-
const model = { declared, registered, registrations: all, perConnection, contextOfCapability, contextOfFile, connectionOfManager, unparen };
|
|
207
272
|
MODELS.set(graph, model);
|
|
208
273
|
return model;
|
|
209
274
|
}
|
|
210
275
|
|
|
276
|
+
const composedConnection = (kit, declared, checker, inner) => {
|
|
277
|
+
const { ts } = kit;
|
|
278
|
+
const parent = inner.parent;
|
|
279
|
+
if (parent?.name === inner && (ts.isPropertyAssignment(parent) || ts.isPropertyAccessExpression(parent))) return null;
|
|
280
|
+
if (!(ts.isIdentifier(inner) || ts.isStringLiteralLike(inner) || ts.isPropertyAccessExpression(inner))) return null;
|
|
281
|
+
const value = kit.stringValue(checker, inner);
|
|
282
|
+
return value !== null && declared.has(value) ? value : null;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const collectComposedConnections = (kit, declared, checker, call, items) => {
|
|
286
|
+
for (const argument of call.arguments) {
|
|
287
|
+
kit.walk(argument, inner => {
|
|
288
|
+
const value = composedConnection(kit, declared, checker, inner);
|
|
289
|
+
if (value === null) return true;
|
|
290
|
+
items.push({ connection: value, node: inner });
|
|
291
|
+
return false;
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
211
296
|
/** The declared connection names the composition root of `app` passes to the database module registration, as [{connection, node}] in source order. */
|
|
212
297
|
export function connectionsComposedBy(kit, app) {
|
|
213
298
|
const { ts } = kit;
|
|
@@ -217,21 +302,10 @@ export function connectionsComposedBy(kit, app) {
|
|
|
217
302
|
const checker = kit.checkerOf(root.sourceFile);
|
|
218
303
|
const items = [];
|
|
219
304
|
kit.walk(root.sourceFile, node => {
|
|
220
|
-
if (!(ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'register')) return
|
|
305
|
+
if (!(ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'register')) return;
|
|
221
306
|
const owner = kit.declarationsOf(checker, node.expression.expression).map(kit.ownerOfDeclaration).find(Boolean);
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
kit.walk(argument, inner => {
|
|
225
|
-
if (ts.isPropertyAssignment(inner.parent ?? {}) && inner.parent.name === inner) return true;
|
|
226
|
-
if (ts.isPropertyAccessExpression(inner.parent ?? {}) && inner.parent.name === inner) return true;
|
|
227
|
-
if (!(ts.isIdentifier(inner) || ts.isStringLiteralLike(inner) || ts.isPropertyAccessExpression(inner))) return true;
|
|
228
|
-
const value = kit.stringValue(checker, inner);
|
|
229
|
-
if (value === null || !declared.has(value)) return true;
|
|
230
|
-
items.push({ connection: value, node: inner });
|
|
231
|
-
return false;
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
return true;
|
|
307
|
+
if (owner?.tier !== 'platform' || owner.name !== 'database') return;
|
|
308
|
+
collectComposedConnections(kit, declared, checker, node, items);
|
|
235
309
|
});
|
|
236
310
|
return { root, items };
|
|
237
311
|
}
|
|
@@ -19,6 +19,43 @@ const RULE = 'BE_CONTEXT_OWNER';
|
|
|
19
19
|
const SERVICE_KINDS = new Set(['api', 'worker']);
|
|
20
20
|
const RUNNER_KINDS = new Set(['migrate', 'cli']);
|
|
21
21
|
|
|
22
|
+
// What each app composes: [{connection, file, node}] from the database module registration and from the registration literals under its files.
|
|
23
|
+
const compositionsOf = (kit, resolver, model, appOf) => {
|
|
24
|
+
const composed = new Map(resolver.repo.apps.map(app => [app.name, { items: [], unresolved: false }]));
|
|
25
|
+
for (const app of resolver.repo.apps) {
|
|
26
|
+
const { root, items } = connectionsComposedBy(kit, app.name);
|
|
27
|
+
for (const item of items) composed.get(app.name).items.push({ ...item, file: root });
|
|
28
|
+
}
|
|
29
|
+
for (const site of model.registrations) {
|
|
30
|
+
const app = appOf(site.file.rel);
|
|
31
|
+
if (!app || !composed.has(app)) continue;
|
|
32
|
+
if (site.connection === null) composed.get(app).unresolved = true;
|
|
33
|
+
else composed.get(app).items.push({ connection: site.connection, file: site.file, node: site.node });
|
|
34
|
+
}
|
|
35
|
+
return composed;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// A service app composes only the connections it owns; a runner app composes every declared connection with tables.
|
|
39
|
+
const judgeComposition = (app, { items, unresolved }, { owners, model, kit, once }) => {
|
|
40
|
+
if (SERVICE_KINDS.has(app.kind)) {
|
|
41
|
+
for (const { connection, file, node } of items) {
|
|
42
|
+
const owner = owners.get(connection);
|
|
43
|
+
if (owner && owner !== app.name) {
|
|
44
|
+
once(`${app.name}|${connection}`, { ...kit.at(file.rel, file.sourceFile, node), message: `App ${app.name} composes connection ${connection}, the context owned by app ${owner}; an app composes only the contexts it owns. Reach that context through the API or the events of ${owner}.`, connection, app: app.name, owner });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} else if (RUNNER_KINDS.has(app.kind) && !unresolved) {
|
|
48
|
+
const root = kit.appRoot(app.name);
|
|
49
|
+
const present = new Set(items.map(item => item.connection));
|
|
50
|
+
const held = new Set([...model.registered.values()].flatMap(entry => [...entry.connections.keys()]));
|
|
51
|
+
for (const connection of owners.keys()) {
|
|
52
|
+
if (!root || present.has(connection) || !held.has(connection) || !items.length) continue;
|
|
53
|
+
once(`${app.name}|missing|${connection}`, { ...kit.at(root.rel, root.sourceFile, root.sourceFile), message: `The ${app.kind} app ${app.name} does not compose connection ${connection}; migrations run only through it, once per connection, so it registers every declared connection that holds tables.`, connection, app: app.name });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return items.length;
|
|
57
|
+
};
|
|
58
|
+
|
|
22
59
|
export function checkContextOwner(input) {
|
|
23
60
|
const { graph } = input;
|
|
24
61
|
const kit = machineKit(input);
|
|
@@ -32,38 +69,10 @@ export function checkContextOwner(input) {
|
|
|
32
69
|
const once = (key, violation) => { if (!seen.has(key)) { seen.add(key); violations.push({ ruleId: RULE, ...violation }); } };
|
|
33
70
|
const appOf = rel => { const classified = resolver.classifyPath(rel); return classified.slot?.startsWith('be.app.') ? classified.bindings?.app ?? null : null; };
|
|
34
71
|
|
|
35
|
-
|
|
36
|
-
const composed = new Map(resolver.repo.apps.map(app => [app.name, { items: [], unresolved: false }]));
|
|
37
|
-
for (const app of resolver.repo.apps) {
|
|
38
|
-
const { root, items } = connectionsComposedBy(kit, app.name);
|
|
39
|
-
for (const item of items) composed.get(app.name).items.push({ ...item, file: root });
|
|
40
|
-
}
|
|
41
|
-
for (const site of model.registrations) {
|
|
42
|
-
const app = appOf(site.file.rel);
|
|
43
|
-
if (!app || !composed.has(app)) continue;
|
|
44
|
-
if (site.connection === null) composed.get(app).unresolved = true;
|
|
45
|
-
else composed.get(app).items.push({ connection: site.connection, file: site.file, node: site.node });
|
|
46
|
-
}
|
|
72
|
+
const composed = compositionsOf(kit, resolver, model, appOf);
|
|
47
73
|
|
|
48
74
|
for (const app of resolver.repo.apps) {
|
|
49
|
-
|
|
50
|
-
compositions += items.length;
|
|
51
|
-
if (SERVICE_KINDS.has(app.kind)) {
|
|
52
|
-
for (const { connection, file, node } of items) {
|
|
53
|
-
const owner = owners.get(connection);
|
|
54
|
-
if (owner && owner !== app.name) {
|
|
55
|
-
once(`${app.name}|${connection}`, { ...kit.at(file.rel, file.sourceFile, node), message: `App ${app.name} composes connection ${connection}, the context owned by app ${owner}; an app composes only the contexts it owns. Reach that context through the API or the events of ${owner}.`, connection, app: app.name, owner });
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
} else if (RUNNER_KINDS.has(app.kind) && !unresolved) {
|
|
59
|
-
const root = kit.appRoot(app.name);
|
|
60
|
-
const present = new Set(items.map(item => item.connection));
|
|
61
|
-
const held = new Set([...model.registered.values()].flatMap(entry => [...entry.connections.keys()]));
|
|
62
|
-
for (const connection of owners.keys()) {
|
|
63
|
-
if (!root || present.has(connection) || !held.has(connection) || !items.length) continue;
|
|
64
|
-
once(`${app.name}|missing|${connection}`, { ...kit.at(root.rel, root.sourceFile, root.sourceFile), message: `The ${app.kind} app ${app.name} does not compose connection ${connection}; migrations run only through it, once per connection, so it registers every declared connection that holds tables.`, connection, app: app.name });
|
|
65
|
-
}
|
|
66
|
-
}
|
|
75
|
+
compositions += judgeComposition(app, composed.get(app.name), { owners, model, kit, once });
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
const kinds = new Map(resolver.repo.apps.map(app => [app.name, app.kind]));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { contextModelOf } from './context-map.mjs';
|
|
2
2
|
import { machineKit } from './machine-ast.mjs';
|
|
3
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* R178 `context-platform-tables` (BE_CONTEXT_PLATFORM_TABLES). A platform capability the manifest lists as `perConnection` (the event-bus outbox,
|
|
@@ -12,36 +13,49 @@ export const CONTEXT_PLATFORM_TABLES_RULE_IDS = ['BE_CONTEXT_PLATFORM_TABLES'];
|
|
|
12
13
|
|
|
13
14
|
const RULE = 'BE_CONTEXT_PLATFORM_TABLES';
|
|
14
15
|
|
|
16
|
+
function perConnectionHome(kit, model, checker, file, node) {
|
|
17
|
+
const { ts } = kit;
|
|
18
|
+
const callee = ts.isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression;
|
|
19
|
+
const home = kit.declarationsOf(checker, callee).map(kit.ownerOfDeclaration).find(Boolean);
|
|
20
|
+
if (!home || !model.perConnection.has(home.root) || file.owner?.root === home.root) return undefined;
|
|
21
|
+
return home;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function unregisteredViolations(kit, model, scope, home, connections, seen) {
|
|
25
|
+
const { file, node } = scope;
|
|
26
|
+
const registeredOn = model.registered.get(home.root)?.connections ?? new Map();
|
|
27
|
+
const violations = [];
|
|
28
|
+
for (const connection of connections) {
|
|
29
|
+
if (registeredOn.has(connection)) continue;
|
|
30
|
+
const key = `${file.rel}|${node.getStart()}|${connection}`;
|
|
31
|
+
if (seen.has(key)) continue;
|
|
32
|
+
seen.add(key);
|
|
33
|
+
violations.push({ ruleId: RULE, ...kit.at(file.rel, file.sourceFile, node), message: `${file.rel} passes the entity manager of connection ${connection} to ${home.name}, but the tables of ${home.name} are not registered on ${connection} (registered: ${[...registeredOn.keys()].sort(byCodeUnit).join(', ') || 'none'}); register its entities and migrations on ${connection} too, so the table exists in the database of that context.`, connection, capability: home.name });
|
|
34
|
+
}
|
|
35
|
+
return violations;
|
|
36
|
+
}
|
|
37
|
+
|
|
15
38
|
export function checkContextPlatformTables(input) {
|
|
16
39
|
const { graph } = input;
|
|
17
40
|
const kit = machineKit(input);
|
|
18
41
|
const { ts } = kit;
|
|
19
42
|
const model = contextModelOf(kit, graph);
|
|
20
43
|
const violations = [];
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (!model.perConnection.size) return { violations, coverage: { status: 'checked', perConnectionCapabilities: 0, calls, unproven } };
|
|
44
|
+
const counts = { calls: 0, unproven: 0 };
|
|
45
|
+
if (!model.perConnection.size) return { violations, coverage: { status: 'checked', perConnectionCapabilities: 0, ...counts } };
|
|
24
46
|
const seen = new Set();
|
|
25
47
|
for (const file of graph.files.values()) {
|
|
26
48
|
const checker = kit.checkerOf(file.sourceFile);
|
|
27
49
|
kit.walk(file.sourceFile, node => {
|
|
28
50
|
if (!ts.isCallExpression(node)) return true;
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
calls += 1;
|
|
51
|
+
const home = perConnectionHome(kit, model, checker, file, node);
|
|
52
|
+
if (!home) return true;
|
|
53
|
+
counts.calls += 1;
|
|
33
54
|
const connections = new Set(node.arguments.map(argument => model.connectionOfManager(checker, argument)).filter(Boolean));
|
|
34
|
-
if (!connections.size) { unproven += 1; return true; }
|
|
35
|
-
|
|
36
|
-
for (const connection of connections) {
|
|
37
|
-
if (registeredOn.has(connection)) continue;
|
|
38
|
-
const key = `${file.rel}|${node.getStart()}|${connection}`;
|
|
39
|
-
if (seen.has(key)) continue;
|
|
40
|
-
seen.add(key);
|
|
41
|
-
violations.push({ ruleId: RULE, ...kit.at(file.rel, file.sourceFile, node), message: `${file.rel} passes the entity manager of connection ${connection} to ${home.name}, but the tables of ${home.name} are not registered on ${connection} (registered: ${[...registeredOn.keys()].sort().join(', ') || 'none'}); register its entities and migrations on ${connection} too, so the table exists in the database of that context.`, connection, capability: home.name });
|
|
42
|
-
}
|
|
55
|
+
if (!connections.size) { counts.unproven += 1; return true; }
|
|
56
|
+
violations.push(...unregisteredViolations(kit, model, { file, node }, home, connections, seen));
|
|
43
57
|
return true;
|
|
44
58
|
});
|
|
45
59
|
}
|
|
46
|
-
return { violations, coverage: { status: 'checked', perConnectionCapabilities: model.perConnection.size,
|
|
60
|
+
return { violations, coverage: { status: 'checked', perConnectionCapabilities: model.perConnection.size, ...counts } };
|
|
47
61
|
}
|
|
@@ -13,14 +13,47 @@ export const CONTEXT_TRANSACTION_RULE_IDS = ['BE_CONTEXT_TRANSACTION'];
|
|
|
13
13
|
|
|
14
14
|
const RULE = 'BE_CONTEXT_TRANSACTION';
|
|
15
15
|
|
|
16
|
+
const SPAN_ADVICE = "one transaction touches one context's connection. Span contexts with a saga: one transaction per context, linked by events.";
|
|
17
|
+
|
|
18
|
+
/** Reports an entity manager of another connection, or a call into another context's capability, found inside a transaction callback. */
|
|
19
|
+
function inspectInner(scope, file, checker, connection, inner) {
|
|
20
|
+
const { ts, kit, model, graph, report } = scope;
|
|
21
|
+
if (ts.isPropertyAccessExpression(inner)) {
|
|
22
|
+
const other = model.connectionOfManager(checker, inner);
|
|
23
|
+
if (other && other !== connection) report(file, inner, `A transaction on connection ${connection} uses the entity manager of connection ${other}; ${SPAN_ADVICE}`, { connection, other });
|
|
24
|
+
}
|
|
25
|
+
if (!ts.isCallExpression(inner)) return;
|
|
26
|
+
const callee = ts.isPropertyAccessExpression(inner.expression) ? inner.expression.name : inner.expression;
|
|
27
|
+
const declaration = kit.declarationsOf(checker, callee)[0];
|
|
28
|
+
const rel = declaration ? kit.graphPath(declaration) : null;
|
|
29
|
+
const context = rel ? model.contextOfFile(rel) : null;
|
|
30
|
+
if (context && context !== connection) {
|
|
31
|
+
report(file, inner, `A transaction on connection ${connection} calls ${graph.files.get(rel).owner.root} of context ${context}; ${SPAN_ADVICE}`, { connection, other: context });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Judges a `.transaction(...)` call: counts it as proven or unproven and inspects its callback body; any other node is skipped. */
|
|
36
|
+
function inspectTransaction(scope, file, checker, node) {
|
|
37
|
+
const { ts, kit, model, counts } = scope;
|
|
38
|
+
if (!(ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'transaction')) return;
|
|
39
|
+
const callback = node.arguments.find(argument => ts.isArrowFunction(argument) || ts.isFunctionExpression(argument));
|
|
40
|
+
if (!callback) return;
|
|
41
|
+
const connection = model.connectionOfManager(checker, node.expression.expression);
|
|
42
|
+
if (!connection) { counts.unproven += 1; return; }
|
|
43
|
+
counts.transactions += 1;
|
|
44
|
+
kit.walk(callback.body, inner => {
|
|
45
|
+
inspectInner(scope, file, checker, connection, inner);
|
|
46
|
+
return true;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
16
50
|
export function checkContextTransaction(input) {
|
|
17
51
|
const { graph } = input;
|
|
18
52
|
const kit = machineKit(input);
|
|
19
53
|
const { ts } = kit;
|
|
20
54
|
const model = contextModelOf(kit, graph);
|
|
21
55
|
const violations = [];
|
|
22
|
-
|
|
23
|
-
let unproven = 0;
|
|
56
|
+
const counts = { transactions: 0, unproven: 0 };
|
|
24
57
|
const seen = new Set();
|
|
25
58
|
const report = (file, node, message, extra) => {
|
|
26
59
|
const key = `${file.rel}|${node.getStart()}|${message}`;
|
|
@@ -28,33 +61,13 @@ export function checkContextTransaction(input) {
|
|
|
28
61
|
seen.add(key);
|
|
29
62
|
violations.push({ ruleId: RULE, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
|
|
30
63
|
};
|
|
64
|
+
const scope = { ts, kit, model, graph, report, counts };
|
|
31
65
|
for (const file of graph.files.values()) {
|
|
32
66
|
const checker = kit.checkerOf(file.sourceFile);
|
|
33
67
|
kit.walk(file.sourceFile, node => {
|
|
34
|
-
|
|
35
|
-
const callback = node.arguments.find(argument => ts.isArrowFunction(argument) || ts.isFunctionExpression(argument));
|
|
36
|
-
if (!callback) return true;
|
|
37
|
-
const connection = model.connectionOfManager(checker, node.expression.expression);
|
|
38
|
-
if (!connection) { unproven += 1; return true; }
|
|
39
|
-
transactions += 1;
|
|
40
|
-
kit.walk(callback.body, inner => {
|
|
41
|
-
if (ts.isPropertyAccessExpression(inner)) {
|
|
42
|
-
const other = model.connectionOfManager(checker, inner);
|
|
43
|
-
if (other && other !== connection) report(file, inner, `A transaction on connection ${connection} uses the entity manager of connection ${other}; one transaction touches one context's connection. Span contexts with a saga: one transaction per context, linked by events.`, { connection, other });
|
|
44
|
-
}
|
|
45
|
-
if (ts.isCallExpression(inner)) {
|
|
46
|
-
const callee = ts.isPropertyAccessExpression(inner.expression) ? inner.expression.name : inner.expression;
|
|
47
|
-
const declaration = kit.declarationsOf(checker, callee)[0];
|
|
48
|
-
const rel = declaration ? kit.graphPath(declaration) : null;
|
|
49
|
-
const context = rel ? model.contextOfFile(rel) : null;
|
|
50
|
-
if (context && context !== connection) {
|
|
51
|
-
report(file, inner, `A transaction on connection ${connection} calls ${graph.files.get(rel).owner.root} of context ${context}; one transaction touches one context's connection. Span contexts with a saga: one transaction per context, linked by events.`, { connection, other: context });
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return true;
|
|
55
|
-
});
|
|
68
|
+
inspectTransaction(scope, file, checker, node);
|
|
56
69
|
return true;
|
|
57
70
|
});
|
|
58
71
|
}
|
|
59
|
-
return { violations, coverage: { status: 'checked', transactions, unproven } };
|
|
72
|
+
return { violations, coverage: { status: 'checked', transactions: counts.transactions, unproven: counts.unproven } };
|
|
60
73
|
}
|