@starci/hfs 4.0.10 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -1
- package/emit/operations.mjs +1 -1
- package/emit/type-schema.mjs +2 -1
- package/lint/run.mjs +65 -24
- package/package.json +1 -1
- package/report/order.mjs +2 -0
- package/runtime/config.example.yaml +194 -0
- package/runtime/engine/by-code-unit.mjs +12 -0
- package/runtime/engine/config.mjs +222 -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/release-config.mjs +28 -0
- 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 +161 -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 +268 -0
- package/runtime/modules/models/registry.yaml +25 -359
- package/runtime/modules/models/runtimes.yaml +126 -265
- package/runtime/modules/models/tiers.yaml +66 -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 +164 -104
- 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 +121 -70
- 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/catalog-once.mjs +15 -0
- package/runtime/scripts/hfs/check.mjs +63 -71
- 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 +21 -4
- 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 +74 -695
- 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/env.mjs +10 -0
- 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/runtime/scripts/lib/yaml-cached.mjs +14 -0
- 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
|
@@ -31,64 +31,98 @@ const literalStrings = (ts, node) => {
|
|
|
31
31
|
return null;
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
function isTypeormDecorator(kit, checker, node, ...names) {
|
|
35
|
+
const call = kit.ts.isCallExpression(node.expression) ? node.expression.expression : node.expression;
|
|
36
|
+
const binding = kit.importBinding(checker, call);
|
|
37
|
+
return binding?.module === TYPEORM && names.includes(binding.name);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function entityTableInfo(kit, checker, statement) {
|
|
41
|
+
const { ts } = kit;
|
|
42
|
+
const entity = kit.decorators(statement).find(decorator => isTypeormDecorator(kit, checker, decorator, 'Entity'));
|
|
43
|
+
if (!entity) return null;
|
|
44
|
+
const argument = ts.isCallExpression(entity.expression) ? entity.expression.arguments[0] : null;
|
|
45
|
+
let table = argument && ts.isStringLiteralLike(argument) ? argument.text : null;
|
|
46
|
+
if (table === null && argument && ts.isObjectLiteralExpression(argument)) {
|
|
47
|
+
const property = kit.propertyOf(argument, 'name');
|
|
48
|
+
if (property && ts.isStringLiteralLike(kit.valueOfProperty(property))) table = kit.valueOfProperty(property).text;
|
|
49
|
+
}
|
|
50
|
+
return { table };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function mappedColumnName(kit, member, property) {
|
|
54
|
+
const { ts } = kit;
|
|
55
|
+
let column = property;
|
|
56
|
+
for (const decorator of kit.decorators(member)) {
|
|
57
|
+
const args = ts.isCallExpression(decorator.expression) ? decorator.expression.arguments : [];
|
|
58
|
+
for (const item of args) if (ts.isObjectLiteralExpression(item)) {
|
|
59
|
+
const named = kit.propertyOf(item, 'name');
|
|
60
|
+
if (named && ts.isStringLiteralLike(kit.valueOfProperty(named))) column = kit.valueOfProperty(named).text;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return column;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function addUniqueColumnSet(kit, checker, decorator, column, primary, sets) {
|
|
35
67
|
const { ts } = kit;
|
|
68
|
+
const args = ts.isCallExpression(decorator.expression) ? decorator.expression.arguments : [];
|
|
69
|
+
const uniqueOption = args.some(item => ts.isObjectLiteralExpression(item) && kit.propertyOf(item, 'unique')
|
|
70
|
+
&& kit.valueOfProperty(kit.propertyOf(item, 'unique')).kind === ts.SyntaxKind.TrueKeyword);
|
|
71
|
+
if (isTypeormDecorator(kit, checker, decorator, 'PrimaryColumn', 'PrimaryGeneratedColumn')) primary.push(column.toLowerCase());
|
|
72
|
+
else if (isTypeormDecorator(kit, checker, decorator, 'Column', 'Index') && uniqueOption) sets.push([column.toLowerCase()]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function entityColumnSets(kit, checker, statement) {
|
|
76
|
+
const { ts } = kit;
|
|
77
|
+
const dbName = new Map();
|
|
78
|
+
const primary = [];
|
|
79
|
+
const sets = [];
|
|
80
|
+
for (const member of statement.members) {
|
|
81
|
+
if (!ts.isPropertyDeclaration(member) || !member.name || !ts.isIdentifier(member.name)) continue;
|
|
82
|
+
const property = member.name.text;
|
|
83
|
+
const column = mappedColumnName(kit, member, property);
|
|
84
|
+
dbName.set(property, column.toLowerCase());
|
|
85
|
+
for (const decorator of kit.decorators(member)) addUniqueColumnSet(kit, checker, decorator, column, primary, sets);
|
|
86
|
+
}
|
|
87
|
+
if (primary.length) sets.push(primary);
|
|
88
|
+
return { dbName, sets };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function addEntityUniqueSets(kit, checker, statement, dbName, sets) {
|
|
92
|
+
const { ts } = kit;
|
|
93
|
+
for (const decorator of kit.decorators(statement)) {
|
|
94
|
+
const args = ts.isCallExpression(decorator.expression) ? decorator.expression.arguments : [];
|
|
95
|
+
const list = args.find(item => ts.isArrayLiteralExpression(item) && literalStrings(ts, item));
|
|
96
|
+
const columns = list ? literalStrings(ts, list) : null;
|
|
97
|
+
const unique = isTypeormDecorator(kit, checker, decorator, 'Unique') || (isTypeormDecorator(kit, checker, decorator, 'Index') && args.some(item => ts.isObjectLiteralExpression(item) && kit.propertyOf(item, 'unique')
|
|
98
|
+
&& kit.valueOfProperty(kit.propertyOf(item, 'unique')).kind === ts.SyntaxKind.TrueKeyword));
|
|
99
|
+
if (unique && columns) sets.push(columns.map(column => (dbName.get(column) ?? column).toLowerCase()));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Records the entities one `.entity.ts` file declares into `tables`; returns how many of its entities have no readable table name. */
|
|
104
|
+
function addFileEntities(kit, file, tables) {
|
|
105
|
+
const checker = kit.checkerOf(file.sourceFile);
|
|
106
|
+
let unreadable = 0;
|
|
107
|
+
for (const statement of file.sourceFile.statements) {
|
|
108
|
+
if (!kit.ts.isClassDeclaration(statement)) continue;
|
|
109
|
+
const entity = entityTableInfo(kit, checker, statement);
|
|
110
|
+
if (!entity) continue;
|
|
111
|
+
if (entity.table === null) { unreadable += 1; continue; }
|
|
112
|
+
const { dbName, sets } = entityColumnSets(kit, checker, statement);
|
|
113
|
+
addEntityUniqueSets(kit, checker, statement, dbName, sets);
|
|
114
|
+
const key = entity.table.toLowerCase();
|
|
115
|
+
if (!tables.has(key)) tables.set(key, { table: entity.table, owner: file.owner.root, rel: file.rel, uniqueSets: sets });
|
|
116
|
+
}
|
|
117
|
+
return unreadable;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function entitiesOf(kit, graph) {
|
|
36
121
|
const tables = new Map();
|
|
37
122
|
let unreadable = 0;
|
|
38
123
|
for (const file of graph.files.values()) {
|
|
39
124
|
if (file.slot !== 'be.persistence' || !path.posix.basename(file.rel).endsWith('.entity.ts') || !file.owner) continue;
|
|
40
|
-
|
|
41
|
-
const typeorm = (node, ...names) => {
|
|
42
|
-
const call = ts.isCallExpression(node.expression) ? node.expression.expression : node.expression;
|
|
43
|
-
const binding = kit.importBinding(checker, call);
|
|
44
|
-
return binding?.module === TYPEORM && names.includes(binding.name);
|
|
45
|
-
};
|
|
46
|
-
for (const statement of file.sourceFile.statements) {
|
|
47
|
-
if (!ts.isClassDeclaration(statement)) continue;
|
|
48
|
-
const entity = kit.decorators(statement).find(decorator => typeorm(decorator, 'Entity'));
|
|
49
|
-
if (!entity) continue;
|
|
50
|
-
const argument = ts.isCallExpression(entity.expression) ? entity.expression.arguments[0] : null;
|
|
51
|
-
let table = argument && ts.isStringLiteralLike(argument) ? argument.text : null;
|
|
52
|
-
if (table === null && argument && ts.isObjectLiteralExpression(argument)) {
|
|
53
|
-
const property = kit.propertyOf(argument, 'name');
|
|
54
|
-
if (property && ts.isStringLiteralLike(kit.valueOfProperty(property))) table = kit.valueOfProperty(property).text;
|
|
55
|
-
}
|
|
56
|
-
if (table === null) { unreadable += 1; continue; }
|
|
57
|
-
const dbName = new Map();
|
|
58
|
-
const primary = [];
|
|
59
|
-
const sets = [];
|
|
60
|
-
for (const member of statement.members) {
|
|
61
|
-
if (!ts.isPropertyDeclaration(member) || !member.name || !ts.isIdentifier(member.name)) continue;
|
|
62
|
-
const property = member.name.text;
|
|
63
|
-
let column = property;
|
|
64
|
-
for (const decorator of kit.decorators(member)) {
|
|
65
|
-
const args = ts.isCallExpression(decorator.expression) ? decorator.expression.arguments : [];
|
|
66
|
-
for (const item of args) if (ts.isObjectLiteralExpression(item)) {
|
|
67
|
-
const named = kit.propertyOf(item, 'name');
|
|
68
|
-
if (named && ts.isStringLiteralLike(kit.valueOfProperty(named))) column = kit.valueOfProperty(named).text;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
dbName.set(property, column.toLowerCase());
|
|
72
|
-
for (const decorator of kit.decorators(member)) {
|
|
73
|
-
const args = ts.isCallExpression(decorator.expression) ? decorator.expression.arguments : [];
|
|
74
|
-
const uniqueOption = args.some(item => ts.isObjectLiteralExpression(item) && kit.propertyOf(item, 'unique')
|
|
75
|
-
&& kit.valueOfProperty(kit.propertyOf(item, 'unique')).kind === ts.SyntaxKind.TrueKeyword);
|
|
76
|
-
if (typeorm(decorator, 'PrimaryColumn', 'PrimaryGeneratedColumn')) primary.push(column.toLowerCase());
|
|
77
|
-
else if (typeorm(decorator, 'Column', 'Index') && uniqueOption) sets.push([column.toLowerCase()]);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (primary.length) sets.push(primary);
|
|
81
|
-
for (const decorator of kit.decorators(statement)) {
|
|
82
|
-
const args = ts.isCallExpression(decorator.expression) ? decorator.expression.arguments : [];
|
|
83
|
-
const list = args.find(item => ts.isArrayLiteralExpression(item) && literalStrings(ts, item));
|
|
84
|
-
const columns = list ? literalStrings(ts, list) : null;
|
|
85
|
-
const unique = typeorm(decorator, 'Unique') || (typeorm(decorator, 'Index') && args.some(item => ts.isObjectLiteralExpression(item) && kit.propertyOf(item, 'unique')
|
|
86
|
-
&& kit.valueOfProperty(kit.propertyOf(item, 'unique')).kind === ts.SyntaxKind.TrueKeyword));
|
|
87
|
-
if (unique && columns) sets.push(columns.map(column => (dbName.get(column) ?? column).toLowerCase()));
|
|
88
|
-
}
|
|
89
|
-
const key = table.toLowerCase();
|
|
90
|
-
if (!tables.has(key)) tables.set(key, { table, owner: file.owner.root, rel: file.rel, uniqueSets: sets });
|
|
91
|
-
}
|
|
125
|
+
unreadable += addFileEntities(kit, file, tables);
|
|
92
126
|
}
|
|
93
127
|
return { tables, unreadable };
|
|
94
128
|
}
|
|
@@ -103,11 +137,93 @@ function supabaseDeclarations(graph, appRoot, ts) {
|
|
|
103
137
|
if (file.slot !== 'be.persistence' || !file.owner) continue;
|
|
104
138
|
const key = capabilityTable(file.owner.root);
|
|
105
139
|
const declaration = tables.get(key);
|
|
106
|
-
if (declaration
|
|
140
|
+
if (declaration?.owner === null) tables.set(key, { ...declaration, owner: file.owner.root, rel: file.rel });
|
|
107
141
|
}
|
|
108
142
|
return tables;
|
|
109
143
|
}
|
|
110
144
|
|
|
145
|
+
function reportSqlWrite({ tables, supabase, file, ownTable, report }, write) {
|
|
146
|
+
const declaration = tables.get(write.table.toLowerCase());
|
|
147
|
+
if (!declaration) {
|
|
148
|
+
report(supabase
|
|
149
|
+
? `SQL writes table ${write.table}, which Database['public']['Tables'] does not declare; regenerate supabase/types/database.types.ts from the migrations.`
|
|
150
|
+
: `SQL writes table ${write.table}, which no @Entity declares; declare the entity in the owning capability's persistence/entities.`, { table: write.table });
|
|
151
|
+
} else if (supabase) {
|
|
152
|
+
if (write.table.toLowerCase() === ownTable) return;
|
|
153
|
+
const ownerNote = declaration.owner ? `, owned by ${declaration.owner}` : '';
|
|
154
|
+
report(`SQL in ${file.owner.root} writes table ${write.table}${ownerNote}; under Supabase schema authority a capability writes only its own table (${ownTable}), named for that capability.`, { table: write.table, ...(declaration.owner ? { tableOwner: declaration.owner } : {}) });
|
|
155
|
+
} else if (declaration.owner !== file.owner.root) {
|
|
156
|
+
report(`SQL in ${file.owner.root} writes table ${write.table}, owned by ${declaration.owner}; a capability writes only the tables of its own entities, call the owner's public API instead.`, { table: write.table, tableOwner: declaration.owner });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function reportSqlWrites(input) {
|
|
161
|
+
for (const write of input.result.writes) reportSqlWrite(input, write);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function reportSqlRead(tables, supabase, file, model, resolver, report, read) {
|
|
165
|
+
const declaration = tables.get(read.table.toLowerCase());
|
|
166
|
+
if (!declaration) {
|
|
167
|
+
report(supabase
|
|
168
|
+
? `SQL reads table ${read.table}, which Database['public']['Tables'] does not declare; regenerate supabase/types/database.types.ts from the migrations.`
|
|
169
|
+
: `SQL reads table ${read.table}, which no @Entity declares; declare the entity in the owning capability's persistence/entities.`, { table: read.table });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (!declaration.owner || declaration.owner === file.owner.root) return;
|
|
173
|
+
const ownContext = model.contextOfCapability(file.owner.root);
|
|
174
|
+
const tableContext = model.contextOfCapability(declaration.owner);
|
|
175
|
+
if (ownContext && tableContext && ownContext !== tableContext) {
|
|
176
|
+
report(`SQL in ${file.owner.root} (context ${ownContext}) reads table ${read.table}, owned by ${declaration.owner} (context ${tableContext}); a context reads and writes only its own context's tables, so a cross-context JOIN or subquery is refused. Keep a local copy fed by the other context's events (a reactor), or read a projection.`, { table: read.table, tableOwner: declaration.owner, context: ownContext, tableContext });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const verdict = resolver.importAllowed(file.rel, `${declaration.owner}/index.ts`);
|
|
180
|
+
if (!verdict.allowed) report(`SQL in ${file.owner.root} reads table ${read.table}, owned by ${declaration.owner}, which ${file.owner.root} may not import (${verdict.reason}); read it through the owner's public API.`, { table: read.table, tableOwner: declaration.owner, reason: verdict.reason });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function reportSqlReads({ result, tables, supabase, file, model, resolver, report }) {
|
|
184
|
+
for (const read of result.reads) reportSqlRead(tables, supabase, file, model, resolver, report, read);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function reportSqlSelects({ result, tables, report }) {
|
|
188
|
+
for (const select of result.selects) {
|
|
189
|
+
if (select.bounded || (select.table && !tables.has(select.table.toLowerCase()))) continue;
|
|
190
|
+
const tableNote = select.table ? `on ${select.table} ` : '';
|
|
191
|
+
report(`SELECT ${tableNote}has no LIMIT and does not constrain a primary key or unique column with =; bound it with LIMIT (PAGE_SIZE_MAX, LIST_ROWS_MAX or BATCH_ROWS from platform/database), filter by key, or select only aggregates.`, { table: select.table ?? undefined });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function inspectSqlTag(input, state, file, checker, node) {
|
|
196
|
+
const { kit, ts, tables, supabase, model, resolver } = input;
|
|
197
|
+
if (!ts.isTaggedTemplateExpression(node)) return true;
|
|
198
|
+
const home = kit.declarationsOf(checker, node.tag).map(kit.ownerOfDeclaration).find(Boolean);
|
|
199
|
+
if (home?.tier !== 'platform' || home?.name !== 'database') { state.foreignTags += 1; return true; }
|
|
200
|
+
state.templates += 1;
|
|
201
|
+
const template = node.template;
|
|
202
|
+
const text = ts.isNoSubstitutionTemplateLiteral(template) ? template.text
|
|
203
|
+
: template.head.text + template.templateSpans.map(span => HOLE + span.literal.text).join('');
|
|
204
|
+
const seen = new Set();
|
|
205
|
+
const report = (message, extra = {}) => {
|
|
206
|
+
const key = `${message}`;
|
|
207
|
+
if (seen.has(key)) return;
|
|
208
|
+
seen.add(key);
|
|
209
|
+
state.violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
|
|
210
|
+
};
|
|
211
|
+
const result = analyzeSql(text, { uniqueSetsOf: table => tables.get(table.toLowerCase())?.uniqueSets ?? null });
|
|
212
|
+
state.dynamic += result.dynamic;
|
|
213
|
+
const ownTable = capabilityTable(file.owner.root);
|
|
214
|
+
reportSqlWrites({ result, tables, supabase, file, ownTable, report });
|
|
215
|
+
reportSqlReads({ result, tables, supabase, file, model, resolver, report });
|
|
216
|
+
reportSqlSelects({ result, tables, report });
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function inspectSqlFile(input, state, file) {
|
|
221
|
+
const { kit } = input;
|
|
222
|
+
if (file.slot !== 'be.persistence' || !path.posix.basename(file.rel).endsWith('.sql.ts') || !file.owner) return;
|
|
223
|
+
const checker = kit.checkerOf(file.sourceFile);
|
|
224
|
+
kit.walk(file.sourceFile, node => inspectSqlTag(input, state, file, checker, node));
|
|
225
|
+
}
|
|
226
|
+
|
|
111
227
|
export function checkSqlOwner(input) {
|
|
112
228
|
const { config, graph } = input;
|
|
113
229
|
const kit = machineKit(input);
|
|
@@ -116,68 +232,9 @@ export function checkSqlOwner(input) {
|
|
|
116
232
|
const entityDeclarations = supabase ? { tables: new Map(), unreadable: 0 } : entitiesOf(kit, graph);
|
|
117
233
|
const tables = supabase ? supabaseDeclarations(graph, config.packageRoot, ts) : entityDeclarations.tables;
|
|
118
234
|
const model = contextModelOf(kit, graph);
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (file.slot !== 'be.persistence' || !path.posix.basename(file.rel).endsWith('.sql.ts') || !file.owner) continue;
|
|
125
|
-
const checker = kit.checkerOf(file.sourceFile);
|
|
126
|
-
kit.walk(file.sourceFile, node => {
|
|
127
|
-
if (!ts.isTaggedTemplateExpression(node)) return true;
|
|
128
|
-
const home = kit.declarationsOf(checker, node.tag).map(kit.ownerOfDeclaration).find(Boolean);
|
|
129
|
-
if (!home || home.tier !== 'platform' || home.name !== 'database') { foreignTags += 1; return true; }
|
|
130
|
-
templates += 1;
|
|
131
|
-
const template = node.template;
|
|
132
|
-
const text = ts.isNoSubstitutionTemplateLiteral(template) ? template.text
|
|
133
|
-
: template.head.text + template.templateSpans.map(span => HOLE + span.literal.text).join('');
|
|
134
|
-
const seen = new Set();
|
|
135
|
-
const report = (message, extra = {}) => {
|
|
136
|
-
const key = `${message}`;
|
|
137
|
-
if (seen.has(key)) return;
|
|
138
|
-
seen.add(key);
|
|
139
|
-
violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
|
|
140
|
-
};
|
|
141
|
-
const result = analyzeSql(text, { uniqueSetsOf: table => tables.get(table.toLowerCase())?.uniqueSets ?? null });
|
|
142
|
-
dynamic += result.dynamic;
|
|
143
|
-
const ownTable = capabilityTable(file.owner.root);
|
|
144
|
-
for (const write of result.writes) {
|
|
145
|
-
const declaration = tables.get(write.table.toLowerCase());
|
|
146
|
-
if (!declaration) {
|
|
147
|
-
report(supabase
|
|
148
|
-
? `SQL writes table ${write.table}, which Database['public']['Tables'] does not declare; regenerate supabase/types/database.types.ts from the migrations.`
|
|
149
|
-
: `SQL writes table ${write.table}, which no @Entity declares; declare the entity in the owning capability's persistence/entities.`, { table: write.table });
|
|
150
|
-
} else if (supabase && write.table.toLowerCase() !== ownTable) {
|
|
151
|
-
report(`SQL in ${file.owner.root} writes table ${write.table}${declaration.owner ? `, owned by ${declaration.owner}` : ''}; under Supabase schema authority a capability writes only its own table (${ownTable}), named for that capability.`, { table: write.table, ...(declaration.owner ? { tableOwner: declaration.owner } : {}) });
|
|
152
|
-
} else if (!supabase && declaration.owner !== file.owner.root) {
|
|
153
|
-
report(`SQL in ${file.owner.root} writes table ${write.table}, owned by ${declaration.owner}; a capability writes only the tables of its own entities, call the owner's public API instead.`, { table: write.table, tableOwner: declaration.owner });
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
for (const read of result.reads) {
|
|
157
|
-
const declaration = tables.get(read.table.toLowerCase());
|
|
158
|
-
if (!declaration) {
|
|
159
|
-
report(supabase
|
|
160
|
-
? `SQL reads table ${read.table}, which Database['public']['Tables'] does not declare; regenerate supabase/types/database.types.ts from the migrations.`
|
|
161
|
-
: `SQL reads table ${read.table}, which no @Entity declares; declare the entity in the owning capability's persistence/entities.`, { table: read.table });
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
if (!declaration.owner || declaration.owner === file.owner.root) continue;
|
|
165
|
-
const ownContext = model.contextOfCapability(file.owner.root);
|
|
166
|
-
const tableContext = model.contextOfCapability(declaration.owner);
|
|
167
|
-
if (ownContext && tableContext && ownContext !== tableContext) {
|
|
168
|
-
report(`SQL in ${file.owner.root} (context ${ownContext}) reads table ${read.table}, owned by ${declaration.owner} (context ${tableContext}); a context reads and writes only its own context's tables, so a cross-context JOIN or subquery is refused. Keep a local copy fed by the other context's events (a reactor), or read a projection.`, { table: read.table, tableOwner: declaration.owner, context: ownContext, tableContext });
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
const verdict = resolver.importAllowed(file.rel, `${declaration.owner}/index.ts`);
|
|
172
|
-
if (!verdict.allowed) report(`SQL in ${file.owner.root} reads table ${read.table}, owned by ${declaration.owner}, which ${file.owner.root} may not import (${verdict.reason}); read it through the owner's public API.`, { table: read.table, tableOwner: declaration.owner, reason: verdict.reason });
|
|
173
|
-
}
|
|
174
|
-
for (const select of result.selects) {
|
|
175
|
-
if (select.bounded || (select.table && !tables.has(select.table.toLowerCase()))) continue;
|
|
176
|
-
report(`SELECT ${select.table ? `on ${select.table} ` : ''}has no LIMIT and does not constrain a primary key or unique column with =; bound it with LIMIT (PAGE_SIZE_MAX, LIST_ROWS_MAX or BATCH_ROWS from platform/database), filter by key, or select only aggregates.`, { table: select.table ?? undefined });
|
|
177
|
-
}
|
|
178
|
-
return true;
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
return { violations, coverage: { status: 'checked', entities: entityDeclarations.tables.size, unreadableEntities: entityDeclarations.unreadable,
|
|
182
|
-
...(supabase ? { supabaseTables: tables.size } : {}), templates, dynamicIdentifiers: dynamic, foreignTags } };
|
|
235
|
+
const state = { violations: [], templates: 0, dynamic: 0, foreignTags: 0 };
|
|
236
|
+
const inspection = { kit, ts, tables, supabase, model, resolver };
|
|
237
|
+
for (const file of graph.files.values()) inspectSqlFile(inspection, state, file);
|
|
238
|
+
return { violations: state.violations, coverage: { status: 'checked', entities: entityDeclarations.tables.size, unreadableEntities: entityDeclarations.unreadable,
|
|
239
|
+
...(supabase ? { supabaseTables: tables.size } : {}), templates: state.templates, dynamicIdentifiers: state.dynamic, foreignTags: state.foreignTags } };
|
|
183
240
|
}
|
|
@@ -15,28 +15,47 @@ export const SQL_RETURNING_RULE_IDS = ['BE_SQL_RETURNING_SHAPE'];
|
|
|
15
15
|
|
|
16
16
|
const RULE = 'BE_SQL_RETURNING_SHAPE';
|
|
17
17
|
|
|
18
|
+
/** Ends the statement in progress, recording its leading verb when it carried RETURNING. */
|
|
19
|
+
function closeStatement(state) {
|
|
20
|
+
if (state.verb && state.returning) state.found.push(state.verb);
|
|
21
|
+
state.verb = null;
|
|
22
|
+
state.returning = false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Folds one token into the statement state: parenthesis depth, statement ends and the top-level words. */
|
|
26
|
+
function readToken(state, token) {
|
|
27
|
+
if (token.t === 'punct') {
|
|
28
|
+
if (token.v === '(') state.depth += 1;
|
|
29
|
+
else if (token.v === ')') state.depth = Math.max(0, state.depth - 1);
|
|
30
|
+
else if (token.v === ';' && state.depth === 0) closeStatement(state);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (state.depth !== 0 || token.t !== 'word') return;
|
|
34
|
+
if (state.verb === null) state.verb = token.up;
|
|
35
|
+
else if (token.up === 'RETURNING') state.returning = true;
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
/** The leading verbs of the top-level statements of `text` that RETURN from an UPDATE or DELETE without a wrapping SELECT. */
|
|
19
39
|
function unwrappedReturning(text) {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
if (depth !== 0 || token.t !== 'word') continue;
|
|
33
|
-
if (verb === null) verb = token.up;
|
|
34
|
-
else if (token.up === 'RETURNING') returning = true;
|
|
35
|
-
}
|
|
36
|
-
close();
|
|
37
|
-
return found.filter(verb => verb === 'UPDATE' || verb === 'DELETE');
|
|
40
|
+
const state = { found: [], depth: 0, verb: null, returning: false };
|
|
41
|
+
for (const token of tokenizeSql(text)) readToken(state, token);
|
|
42
|
+
closeStatement(state);
|
|
43
|
+
return state.found.filter(verb => verb === 'UPDATE' || verb === 'DELETE');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The SQL text of a tagged template, each substitution replaced by a HOLE marker. */
|
|
47
|
+
function templateText(ts, template) {
|
|
48
|
+
return ts.isNoSubstitutionTemplateLiteral(template) ? template.text
|
|
49
|
+
: template.head.text + template.templateSpans.map(span => HOLE + span.literal.text).join('');
|
|
38
50
|
}
|
|
39
51
|
|
|
52
|
+
const isDatabaseTag = (kit, checker, node) => {
|
|
53
|
+
const home = kit.declarationsOf(checker, node.tag).map(kit.ownerOfDeclaration).find(Boolean);
|
|
54
|
+
return home?.tier === 'platform' && home?.name === 'database';
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const returningViolation = (kit, file, node, verb) => ({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message: `${verb} ... RETURNING in ${file.rel} is returned by EntityManager.query as [rows, affectedCount], not as rows, so a caller reading rows reads the wrong value. Wrap it: WITH changed AS (${verb} ... RETURNING ...) SELECT ... FROM changed.`, verb });
|
|
58
|
+
|
|
40
59
|
export function checkSqlReturning(input) {
|
|
41
60
|
const { graph } = input;
|
|
42
61
|
const kit = machineKit(input);
|
|
@@ -47,17 +66,9 @@ export function checkSqlReturning(input) {
|
|
|
47
66
|
if (file.slot !== 'be.persistence' || !path.posix.basename(file.rel).endsWith('.sql.ts') || !file.owner) continue;
|
|
48
67
|
const checker = kit.checkerOf(file.sourceFile);
|
|
49
68
|
kit.walk(file.sourceFile, node => {
|
|
50
|
-
if (!ts.isTaggedTemplateExpression(node)) return
|
|
51
|
-
const home = kit.declarationsOf(checker, node.tag).map(kit.ownerOfDeclaration).find(Boolean);
|
|
52
|
-
if (!home || home.tier !== 'platform' || home.name !== 'database') return true;
|
|
69
|
+
if (!ts.isTaggedTemplateExpression(node) || !isDatabaseTag(kit, checker, node)) return;
|
|
53
70
|
templates += 1;
|
|
54
|
-
const
|
|
55
|
-
const text = ts.isNoSubstitutionTemplateLiteral(template) ? template.text
|
|
56
|
-
: template.head.text + template.templateSpans.map(span => HOLE + span.literal.text).join('');
|
|
57
|
-
for (const verb of unwrappedReturning(text)) {
|
|
58
|
-
violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message: `${verb} ... RETURNING in ${file.rel} is returned by EntityManager.query as [rows, affectedCount], not as rows, so a caller reading rows reads the wrong value. Wrap it: WITH changed AS (${verb} ... RETURNING ...) SELECT ... FROM changed.`, verb });
|
|
59
|
-
}
|
|
60
|
-
return true;
|
|
71
|
+
for (const verb of unwrappedReturning(templateText(ts, node.template))) violations.push(returningViolation(kit, file, node, verb));
|
|
61
72
|
});
|
|
62
73
|
}
|
|
63
74
|
return { violations, coverage: { status: 'checked', templates } };
|