@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
|
@@ -3,6 +3,7 @@ import { machineKit, pascal } from './machine-ast.mjs';
|
|
|
3
3
|
import { treeOf } from './required-files.mjs';
|
|
4
4
|
import { DATABASE_DIR } from './connection-map.mjs';
|
|
5
5
|
import { collectRegistrations, isPerConnection, persistenceOfFactory } from './context-map.mjs';
|
|
6
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* R35 `schema-owner` (BE_SCHEMA_OWNER, BE-CONVENTION 1.4.6). Schema lives with the capability that owns the table:
|
|
@@ -24,141 +25,214 @@ export const SCHEMA_OWNER_RULE_IDS = ['BE_SCHEMA_OWNER'];
|
|
|
24
25
|
|
|
25
26
|
const RULE = 'BE_SCHEMA_OWNER';
|
|
26
27
|
const PERSISTENCE_SLOT = 'be.persistence';
|
|
27
|
-
const SCHEMA_FOLDERS = ['entities', 'migrations'];
|
|
28
|
+
const SCHEMA_FOLDERS = new Set(['entities', 'migrations']);
|
|
28
29
|
const MIGRATION_FILE = /^(\d{13})-([a-z0-9]+(?:-[a-z0-9]+)*)\.ts$/u;
|
|
29
30
|
const camel = name => { const text = pascal(name); return text[0].toLowerCase() + text.slice(1); };
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const violations = [];
|
|
37
|
-
const report = (file, node, message, extra = {}) => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
|
|
38
|
-
const plain = (rel, message, extra = {}) => violations.push({ ruleId: RULE, path: rel, line: 1, column: 1, message, ...extra });
|
|
39
|
-
|
|
40
|
-
const persistenceOf = persistenceOfFactory(resolver);
|
|
41
|
-
|
|
42
|
-
let entities = 0;
|
|
43
|
-
let migrations = 0;
|
|
44
|
-
const found = new Map(); // capability root -> {name, entities, migrations}
|
|
45
|
-
const note = (persistence, kind) => {
|
|
46
|
-
const key = path.posix.dirname(persistence.root);
|
|
47
|
-
if (!found.has(key)) found.set(key, { name: persistence.capability, entities: false, migrations: false });
|
|
48
|
-
found.get(key)[kind] = true;
|
|
49
|
-
};
|
|
32
|
+
function noteSchemaOwner(found, persistence, kind) {
|
|
33
|
+
const key = path.posix.dirname(persistence.root);
|
|
34
|
+
if (!found.has(key)) found.set(key, { name: persistence.capability, entities: false, migrations: false });
|
|
35
|
+
found.get(key)[kind] = true;
|
|
36
|
+
}
|
|
50
37
|
|
|
51
|
-
|
|
52
|
-
for (const rel of [...tree.files].sort()) {
|
|
38
|
+
function reportSchemaFolders(tree, resolver, plain) {
|
|
39
|
+
for (const rel of [...tree.files].sort(byCodeUnit)) {
|
|
53
40
|
const classified = resolver.classifyPath(rel);
|
|
54
41
|
if (classified.status === 'no-slot' || !classified.slot?.startsWith('be.') || classified.slot === PERSISTENCE_SLOT) continue;
|
|
55
|
-
const folder = rel.split('/').slice(0, -1).find(segment => SCHEMA_FOLDERS.
|
|
42
|
+
const folder = rel.split('/').slice(0, -1).find(segment => SCHEMA_FOLDERS.has(segment));
|
|
56
43
|
if (folder) plain(rel, `${rel} sits in a ${folder}/ folder outside the persistence of a capability; ${folder} live only in src/modules/{domain,platform}/<capability>/persistence/${folder}/ of the capability that owns the table.`, { folder });
|
|
57
44
|
}
|
|
58
|
-
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function reportMigrationPaths(tree, persistenceOf, plain) {
|
|
48
|
+
for (const rel of [...tree.files].sort(byCodeUnit)) {
|
|
59
49
|
const persistence = persistenceOf(rel);
|
|
60
|
-
if (
|
|
50
|
+
if (persistence?.folder !== 'migrations') continue;
|
|
61
51
|
if (/\.spec\.[cm]?tsx?$/u.test(rel)) plain(rel, `${rel} is a unit spec of a migration; a migration has no spec, the e2e run over the same migrations is its proof. Delete it.`);
|
|
62
52
|
else if (!MIGRATION_FILE.test(persistence.below.at(-1)) || persistence.below.length !== 2) plain(rel, `${rel} is not named <epochMs13>-<kebab-name>.ts (13 digits of epoch milliseconds, a dash, a kebab-case name) directly in persistence/migrations/.`);
|
|
63
53
|
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isEntityDeclaration(kit, checker, statement) {
|
|
57
|
+
return kit.decorators(statement).some(decorator => {
|
|
58
|
+
const call = kit.ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
|
|
59
|
+
return kit.isImportOf(checker, call, 'Entity', 'typeorm');
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isMigrationDeclaration(kit, checker, statement) {
|
|
64
|
+
const heritage = (statement.heritageClauses ?? []).flatMap(clause => clause.types.map(type => type.expression));
|
|
65
|
+
return heritage.some(expression => kit.isImportOf(checker, expression, 'MigrationInterface', 'typeorm'));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function inspectEntityDeclaration(resolver, file, statement, persistence, found, report) {
|
|
69
|
+
// The table of a projection belongs to the projection that alone writes it: `<name>.projection-entity.ts` of slot be.projections.
|
|
70
|
+
const ownProjectionTable = resolver.classifyPath(file.rel).slot === 'be.projections' && file.rel.endsWith('.projection-entity.ts');
|
|
71
|
+
if (ownProjectionTable) noteSchemaOwner(found, { root: path.posix.join(path.posix.dirname(file.rel), 'persistence'), capability: path.posix.basename(path.posix.dirname(file.rel)) }, 'entities');
|
|
72
|
+
else if (persistence?.folder !== 'entities') report(file, statement.name, `Entity ${statement.name.text} is declared outside persistence/entities/ of a domain or platform capability; an entity lives with the capability that owns its table, in src/modules/{domain,platform}/<capability>/persistence/entities/<table>.entity.ts.`, { class: statement.name.text });
|
|
73
|
+
else noteSchemaOwner(found, persistence, 'entities');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function inspectMigrationDeclaration(kit, file, statement, persistence, checker, found, report) {
|
|
77
|
+
if (persistence?.folder !== 'migrations') {
|
|
78
|
+
report(file, statement.name, `Migration ${statement.name.text} is declared outside persistence/migrations/ of a domain or platform capability; a migration lives with the capability that owns the table, in persistence/migrations/<epochMs13>-<kebab-name>.ts.`, { class: statement.name.text });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
noteSchemaOwner(found, persistence, 'migrations');
|
|
82
|
+
const match = MIGRATION_FILE.exec(path.posix.basename(file.rel));
|
|
83
|
+
if (!match) return;
|
|
84
|
+
const expected = `${pascal(match[2])}${match[1]}`;
|
|
85
|
+
if (statement.name.text !== expected) report(file, statement.name, `Migration class ${statement.name.text} must be named ${expected}: the PascalCase of the file name followed by its 13-digit epoch, because TypeORM reads the timestamp from the class name.`, { class: statement.name.text, expected });
|
|
86
|
+
const nameProperty = statement.members.find(member => kit.ts.isPropertyDeclaration(member) && kit.propertyNameText(member.name) === 'name');
|
|
87
|
+
const nameValue = nameProperty?.initializer ? kit.stringValue(checker, nameProperty.initializer) : null;
|
|
88
|
+
if (nameValue !== statement.name.text) report(file, nameProperty ?? statement.name, `Migration ${statement.name.text} must declare \`name = "${statement.name.text}"\`, equal to its class name; TypeORM records that value in its migrations table.`, { class: statement.name.text });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function inspectSchemaClass(scan, site, statement) {
|
|
92
|
+
const { kit, found, counts, report } = scan;
|
|
93
|
+
const { file, persistence, checker } = site;
|
|
94
|
+
if (!kit.ts.isClassDeclaration(statement) || !statement.name) return;
|
|
95
|
+
const isEntity = isEntityDeclaration(kit, checker, statement);
|
|
96
|
+
const isMigration = isMigrationDeclaration(kit, checker, statement);
|
|
97
|
+
if (isEntity) {
|
|
98
|
+
counts.entities += 1;
|
|
99
|
+
inspectEntityDeclaration(kit.resolver, file, statement, persistence, found, report);
|
|
100
|
+
}
|
|
101
|
+
if (!isMigration) return;
|
|
102
|
+
counts.migrations += 1;
|
|
103
|
+
inspectMigrationDeclaration(kit, file, statement, persistence, checker, found, report);
|
|
104
|
+
}
|
|
64
105
|
|
|
106
|
+
function inspectSchemaFiles(input, kit, persistenceOf, found, report) {
|
|
107
|
+
const { graph } = input;
|
|
108
|
+
const counts = { entities: 0, migrations: 0 };
|
|
109
|
+
const scan = { kit, found, counts, report };
|
|
65
110
|
for (const file of graph.files.values()) {
|
|
66
111
|
const persistence = persistenceOf(file.rel);
|
|
67
112
|
const checker = kit.checkerOf(file.sourceFile);
|
|
68
|
-
const
|
|
69
|
-
for (const statement of file.sourceFile.statements) {
|
|
70
|
-
if (!ts.isClassDeclaration(statement) || !statement.name) continue;
|
|
71
|
-
const isEntity = kit.decorators(statement).some(decorator => {
|
|
72
|
-
const call = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
|
|
73
|
-
return kit.isImportOf(checker, call, 'Entity', 'typeorm');
|
|
74
|
-
});
|
|
75
|
-
const isMigration = heritage(statement).some(expression => kit.isImportOf(checker, expression, 'MigrationInterface', 'typeorm'));
|
|
76
|
-
if (isEntity) {
|
|
77
|
-
entities += 1;
|
|
78
|
-
// The table of a projection belongs to the projection that alone writes it: `<name>.projection-entity.ts` of slot be.projections.
|
|
79
|
-
const ownProjectionTable = resolver.classifyPath(file.rel).slot === 'be.projections' && file.rel.endsWith('.projection-entity.ts');
|
|
80
|
-
if (ownProjectionTable) note({ root: path.posix.join(path.posix.dirname(file.rel), 'persistence'), capability: path.posix.basename(path.posix.dirname(file.rel)) }, 'entities');
|
|
81
|
-
else if (persistence?.folder !== 'entities') report(file, statement.name, `Entity ${statement.name.text} is declared outside persistence/entities/ of a domain or platform capability; an entity lives with the capability that owns its table, in src/modules/{domain,platform}/<capability>/persistence/entities/<table>.entity.ts.`, { class: statement.name.text });
|
|
82
|
-
else note(persistence, 'entities');
|
|
83
|
-
}
|
|
84
|
-
if (!isMigration) continue;
|
|
85
|
-
migrations += 1;
|
|
86
|
-
if (persistence?.folder !== 'migrations') {
|
|
87
|
-
report(file, statement.name, `Migration ${statement.name.text} is declared outside persistence/migrations/ of a domain or platform capability; a migration lives with the capability that owns the table, in persistence/migrations/<epochMs13>-<kebab-name>.ts.`, { class: statement.name.text });
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
note(persistence, 'migrations');
|
|
91
|
-
const match = MIGRATION_FILE.exec(path.posix.basename(file.rel));
|
|
92
|
-
if (!match) continue;
|
|
93
|
-
const expected = `${pascal(match[2])}${match[1]}`;
|
|
94
|
-
if (statement.name.text !== expected) report(file, statement.name, `Migration class ${statement.name.text} must be named ${expected}: the PascalCase of the file name followed by its 13-digit epoch, because TypeORM reads the timestamp from the class name.`, { class: statement.name.text, expected });
|
|
95
|
-
const nameProperty = statement.members.find(member => ts.isPropertyDeclaration(member) && kit.propertyNameText(member.name) === 'name');
|
|
96
|
-
const nameValue = nameProperty?.initializer ? kit.stringValue(checker, nameProperty.initializer) : null;
|
|
97
|
-
if (nameValue !== statement.name.text) report(file, nameProperty ?? statement.name, `Migration ${statement.name.text} must declare \`name = "${statement.name.text}"\`, equal to its class name; TypeORM records that value in its migrations table.`, { class: statement.name.text });
|
|
98
|
-
}
|
|
113
|
+
for (const statement of file.sourceFile.statements) inspectSchemaClass(scan, { file, persistence, checker }, statement);
|
|
99
114
|
}
|
|
115
|
+
return counts;
|
|
116
|
+
}
|
|
100
117
|
|
|
101
|
-
|
|
118
|
+
function reportRegistrationSites(registered, connections, resolver, report) {
|
|
102
119
|
for (const [key, { name, connections: sites }] of registered) {
|
|
103
120
|
for (const [connection, site] of sites) {
|
|
104
|
-
if (!connections.has(connection)) report(site.file, site.node, `The ${name} capability is registered on connection "${connection}", which hfs.json does not declare (declared: ${[...connections].sort().join(', ') || 'none'}); the tables of a capability live on one declared connection.`, { connection, capability: name });
|
|
121
|
+
if (!connections.has(connection)) report(site.file, site.node, `The ${name} capability is registered on connection "${connection}", which hfs.json does not declare (declared: ${[...connections].sort(byCodeUnit).join(', ') || 'none'}); the tables of a capability live on one declared connection.`, { connection, capability: name });
|
|
105
122
|
}
|
|
106
123
|
if (sites.size > 1 && !isPerConnection(resolver, key, name)) {
|
|
107
|
-
const all = [...sites.keys()].sort().join(' and ');
|
|
124
|
+
const all = [...sites.keys()].sort(byCodeUnit).join(' and ');
|
|
108
125
|
for (const [connection, site] of [...sites].slice(1)) report(site.file, site.node, `The ${name} capability's entities and migrations are registered on connections ${all}; a capability's tables live on exactly one connection, so register its arrays under one connection across every app.`, { connection, capability: name });
|
|
109
126
|
}
|
|
110
127
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (!only || only.size < 1) continue;
|
|
120
|
-
// A per-connection platform capability (event-bus, jobs) lives on every connection that uses it; any other on exactly one (reported above).
|
|
121
|
-
const perConnection = only.size > 1 && isPerConnection(resolver, key, kinds.name);
|
|
122
|
-
if (only.size > 1 && !perConnection) continue;
|
|
123
|
-
const home = [...only.keys()][0];
|
|
124
|
-
for (const file of graph.files.values()) {
|
|
125
|
-
if (!file.rel.startsWith(`${key}/`)) continue;
|
|
126
|
-
const checker = kit.checkerOf(file.sourceFile);
|
|
127
|
-
kit.walk(file.sourceFile, node => {
|
|
128
|
-
if (!ts.isCallExpression(node)) return true;
|
|
129
|
-
const declaration = kit.declarationsOf(checker, node.expression)[0];
|
|
130
|
-
const rel = declaration ? kit.graphPath(declaration) : null;
|
|
131
|
-
const used = rel ? [...connections].find(name => rel === `${DATABASE_DIR}/${name}.decorators.ts`) : null;
|
|
132
|
-
if (used && perConnection && !only.has(used)) report(file, node, `${file.rel} injects the entity manager of connection ${used}, but the ${kinds.name} capability's tables are registered on ${[...only.keys()].sort().join(' and ')}; register the capability on ${used} too or inject one of its connections.`, { connection: used, expected: [...only.keys()].sort().join(','), capability: kinds.name });
|
|
133
|
-
else if (used && !perConnection && used !== home) report(file, node, `${file.rel} injects the entity manager of connection ${used}, but the ${kinds.name} capability's tables are registered on ${home}; a capability reads only its own database, so use Inject${pascal(home)}EntityManager or move the tables.`, { connection: used, expected: home, capability: kinds.name });
|
|
134
|
-
return true;
|
|
135
|
-
});
|
|
136
|
-
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function reportDeclaredPersistenceArrays(key, kinds, registered, graph, ts, plain) {
|
|
131
|
+
if (!registered.get(key)?.connections.size) plain(`${key}/persistence/connection.ts`, `The ${kinds.name} capability has entities or migrations but no app registers them; add ${camel(kinds.name)}Entities and ${camel(kinds.name)}Migrations to a connection of DatabaseModule.register and of the cli app.`, { capability: kinds.name });
|
|
132
|
+
const connectionFile = graph.files.get(`${key}/persistence/connection.ts`);
|
|
133
|
+
const declared = new Set((connectionFile?.sourceFile.statements.filter(ts.isVariableStatement).flatMap(statement => statement.declarationList.declarations) ?? []).map(item => item.name.text));
|
|
134
|
+
for (const [kind, has] of [['Entities', kinds.entities], ['Migrations', kinds.migrations]]) {
|
|
135
|
+
if (has && !declared.has(`${camel(kinds.name)}${kind}`)) plain(`${key}/persistence/connection.ts`, `${key}/persistence/connection.ts must export ${camel(kinds.name)}${kind}, the ${kind.toLowerCase()} of the ${kinds.name} capability, the one file that lists them for the apps.`, { capability: kinds.name });
|
|
137
136
|
}
|
|
137
|
+
}
|
|
138
138
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (!
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
139
|
+
function reportEntityManagerUse(kit, capability, file, checker, report) {
|
|
140
|
+
const { kinds, only, home, connections } = capability;
|
|
141
|
+
kit.walk(file.sourceFile, node => {
|
|
142
|
+
if (!kit.ts.isCallExpression(node)) return;
|
|
143
|
+
const declaration = kit.declarationsOf(checker, node.expression)[0];
|
|
144
|
+
const rel = declaration ? kit.graphPath(declaration) : null;
|
|
145
|
+
const used = rel ? [...connections].find(name => rel === `${DATABASE_DIR}/${name}.decorators.ts`) : null;
|
|
146
|
+
if (used && only.size > 1 && !only.has(used)) report(file, node, `${file.rel} injects the entity manager of connection ${used}, but the ${kinds.name} capability's tables are registered on ${[...only.keys()].sort(byCodeUnit).join(' and ')}; register the capability on ${used} too or inject one of its connections.`, { connection: used, expected: [...only.keys()].sort(byCodeUnit).join(','), capability: kinds.name });
|
|
147
|
+
else if (used && only.size === 1 && used !== home) report(file, node, `${file.rel} injects the entity manager of connection ${used}, but the ${kinds.name} capability's tables are registered on ${home}; a capability reads only its own database, so use Inject${pascal(home)}EntityManager or move the tables.`, { connection: used, expected: home, capability: kinds.name });
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function reportEntityManagerUses(input, kit, key, kinds, only, connections, report) {
|
|
152
|
+
const capability = { kinds, only, home: [...only.keys()][0], connections };
|
|
153
|
+
for (const file of input.graph.files.values()) {
|
|
154
|
+
if (!file.rel.startsWith(`${key}/`)) continue;
|
|
155
|
+
reportEntityManagerUse(kit, capability, file, kit.checkerOf(file.sourceFile), report);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function reportFoundCapability(input, kit, key, kinds, view) {
|
|
160
|
+
const { registered, connections, plain, report } = view;
|
|
161
|
+
const { graph } = input;
|
|
162
|
+
const { resolver } = kit;
|
|
163
|
+
reportDeclaredPersistenceArrays(key, kinds, registered, graph, kit.ts, plain);
|
|
164
|
+
const only = registered.get(key)?.connections;
|
|
165
|
+
if (!only || only.size < 1) return;
|
|
166
|
+
// A per-connection platform capability (event-bus, jobs) lives on every connection that uses it; any other on exactly one (reported above).
|
|
167
|
+
const perConnection = only.size > 1 && isPerConnection(resolver, key, kinds.name);
|
|
168
|
+
if (only.size > 1 && !perConnection) return;
|
|
169
|
+
reportEntityManagerUses(input, kit, key, kinds, only, connections, report);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function reportFoundCapabilities(input, kit, found, registered, connections, plain, report) {
|
|
173
|
+
const view = { registered, connections, plain, report };
|
|
174
|
+
for (const [key, kinds] of found) reportFoundCapability(input, kit, key, kinds, view);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function reportPersistenceExport(capabilityRoot, index, statement, element, wanted, report) {
|
|
178
|
+
const from = statement.moduleSpecifier?.text;
|
|
179
|
+
const inPersistence = from?.startsWith('.') && path.posix.join(capabilityRoot, from).split('/').includes('persistence');
|
|
180
|
+
if (inPersistence && !Object.values(wanted).includes(element.name.text)) {
|
|
181
|
+
report(index, element, `${element.name.text} is exported from persistence/; the persistence of a capability is exposed only as ${wanted.Entities} and ${wanted.Migrations}.`, { name: element.name.text });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function addStatementExports(kit, capabilityRoot, index, statement, wanted, report, exported) {
|
|
186
|
+
const { ts } = kit;
|
|
187
|
+
if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
188
|
+
for (const element of statement.exportClause.elements) {
|
|
189
|
+
exported.add(element.name.text);
|
|
190
|
+
reportPersistenceExport(capabilityRoot, index, statement, element, wanted, report);
|
|
161
191
|
}
|
|
192
|
+
} else if (ts.isVariableStatement(statement) && kit.isExported(statement)) {
|
|
193
|
+
for (const declaration of statement.declarationList.declarations) if (ts.isIdentifier(declaration.name)) exported.add(declaration.name.text);
|
|
162
194
|
}
|
|
163
|
-
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function collectIndexExports(kit, capabilityRoot, index, wanted, report) {
|
|
198
|
+
const exported = new Set();
|
|
199
|
+
for (const statement of index.sourceFile.statements) addStatementExports(kit, capabilityRoot, index, statement, wanted, report, exported);
|
|
200
|
+
return exported;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function reportCapabilityIndex(input, kit, capabilityRoot, kinds, report) {
|
|
204
|
+
const { graph } = input;
|
|
205
|
+
const index = graph.files.get(`${capabilityRoot}/index.ts`);
|
|
206
|
+
if (!index) return;
|
|
207
|
+
const wanted = { Entities: `${camel(kinds.name)}Entities`, Migrations: `${camel(kinds.name)}Migrations` };
|
|
208
|
+
const exported = collectIndexExports(kit, capabilityRoot, index, wanted, report);
|
|
209
|
+
for (const [kind, has] of [['Entities', kinds.entities], ['Migrations', kinds.migrations]]) {
|
|
210
|
+
if (has && !exported.has(wanted[kind])) report(index, index.sourceFile, `${capabilityRoot}/index.ts must export ${wanted[kind]}, the ${kind.toLowerCase()} of the capability; apps concatenate them per connection into DatabaseModule.register.`, { capability: kinds.name, expected: wanted[kind] });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function reportCapabilityIndexes(input, kit, found, report) {
|
|
215
|
+
// The owner's index exports `<c>Entities` and `<c>Migrations`, the only persistence names.
|
|
216
|
+
for (const [capabilityRoot, kinds] of found) reportCapabilityIndex(input, kit, capabilityRoot, kinds, report);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function checkSchemaOwner(input) {
|
|
220
|
+
const { config, graph } = input;
|
|
221
|
+
const kit = machineKit(input);
|
|
222
|
+
const { resolver } = kit;
|
|
223
|
+
const connections = new Set(resolver.repo.connections.map(item => item.name));
|
|
224
|
+
const violations = [];
|
|
225
|
+
const report = (file, node, message, extra = {}) => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
|
|
226
|
+
const plain = (rel, message, extra = {}) => violations.push({ ruleId: RULE, path: rel, line: 1, column: 1, message, ...extra });
|
|
227
|
+
const persistenceOf = persistenceOfFactory(resolver);
|
|
228
|
+
const tree = treeOf(config.root);
|
|
229
|
+
reportSchemaFolders(tree, resolver, plain);
|
|
230
|
+
reportMigrationPaths(tree, persistenceOf, plain);
|
|
231
|
+
const found = new Map(); // capability root -> {name, entities, migrations}
|
|
232
|
+
const counts = inspectSchemaFiles(input, kit, persistenceOf, found, report);
|
|
233
|
+
const { registered, registrations } = collectRegistrations({ kit, graph, persistenceOf });
|
|
234
|
+
reportRegistrationSites(registered, connections, resolver, report);
|
|
235
|
+
reportFoundCapabilities(input, kit, found, registered, connections, plain, report);
|
|
236
|
+
reportCapabilityIndexes(input, kit, found, report);
|
|
237
|
+
return { violations, coverage: { status: 'checked', entities: counts.entities, migrations: counts.migrations, capabilities: found.size, connections: connections.size, registrations } };
|
|
164
238
|
}
|