@starci/hfs 4.0.9 → 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/LICENSE +21 -0
- package/emit/operations.mjs +1 -1
- package/emit/type-schema.mjs +2 -1
- package/lint/run.mjs +82 -8
- package/package.json +3 -2
- 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 +247 -273
- 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 +134 -0
- package/runtime/engine/sonar-config.mjs +21 -0
- package/runtime/engine/temp-root.mjs +32 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +18 -18
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
- package/runtime/knowledge/hfs/rules.yaml +102 -69
- 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 -1
- package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
- package/runtime/modules/models/registry.yaml +25 -397
- package/runtime/modules/models/runtimes.yaml +92 -262
- 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/lib.mjs +6 -0
- 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 +36 -1
- package/runtime/scripts/api/process/resolve-real-tool.mjs +52 -0
- package/runtime/scripts/api/process/run-program.mjs +8 -0
- package/runtime/scripts/api/sops/decrypt.mjs +9 -16
- package/runtime/scripts/api/sops/lib.mjs +309 -11
- package/runtime/scripts/api/sops/seal.mjs +8 -4
- 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 +3 -2
- 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 +90 -45
- 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/fs-kind.mjs +14 -4
- 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/mutation-fence.mjs +15 -0
- package/runtime/scripts/lib/path-key.mjs +42 -9
- 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 +103 -2
- 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
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { chainParts, isSupabaseCall, isSupabaseValue, reportAt, unwrap } from './supabase-ast.mjs';
|
|
2
|
+
import { machineKit } from './machine-ast.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The front-end Supabase result checks (FE_DB_RESULT_TYPED, FE_DB_ERROR_HANDLED): a query result keeps its inferred row type, a list read is
|
|
6
|
+
* bounded, and the awaited result is either passed whole to `toOutcome` or has its error read.
|
|
7
|
+
*/
|
|
8
|
+
const FE_RESULT_TYPED = 'FE_DB_RESULT_TYPED';
|
|
9
|
+
const FE_ERROR_HANDLED = 'FE_DB_ERROR_HANDLED';
|
|
10
|
+
export const FE_DB_SLOTS = new Set(['fe.modules.db', 'fe.package.db', 'fe.modules.db.outcome']);
|
|
11
|
+
const QUERY_TERMINALS = new Set(['single', 'maybeSingle', 'returns']);
|
|
12
|
+
const QUERY_WRITES = new Set(['insert', 'update', 'upsert', 'delete']);
|
|
13
|
+
const LIST_BOUNDS = new Set(['gt', 'gte', 'lt', 'lte', 'range']);
|
|
14
|
+
|
|
15
|
+
export const symbolOf = (kit, checker, node) => kit.aliased(checker, kit.symbolAt(checker, node)) ?? kit.symbolAt(checker, node);
|
|
16
|
+
|
|
17
|
+
export const sameSymbol = (kit, checker, node, symbol) => Boolean(symbol && symbolOf(kit, checker, node) === symbol);
|
|
18
|
+
|
|
19
|
+
const fromAwaitedSupabase = (kit, checker, node, seen = new Set()) => {
|
|
20
|
+
const current = unwrap(kit.ts, node);
|
|
21
|
+
if (!current || seen.has(current)) return false;
|
|
22
|
+
seen.add(current);
|
|
23
|
+
if (kit.ts.isCallExpression(current)) return isSupabaseCall(kit, checker, current);
|
|
24
|
+
if (!kit.ts.isIdentifier(current)) return false;
|
|
25
|
+
for (const declaration of kit.declarationsOf(checker, current)) {
|
|
26
|
+
if (kit.ts.isVariableDeclaration(declaration) && declaration.initializer && fromAwaitedSupabase(kit, checker, declaration.initializer, seen)) return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const callIsToOutcome = (kit, checker, graph, call) => {
|
|
32
|
+
if (!kit.ts.isCallExpression(call)) return false;
|
|
33
|
+
const expression = call.expression;
|
|
34
|
+
let name = null;
|
|
35
|
+
if (kit.ts.isIdentifier(expression)) name = expression.text;
|
|
36
|
+
else if (kit.ts.isPropertyAccessExpression(expression)) name = expression.name.text;
|
|
37
|
+
if (name !== 'toOutcome') return false;
|
|
38
|
+
return kit.declarationsOf(checker, kit.ts.isPropertyAccessExpression(expression) ? expression.name : expression)
|
|
39
|
+
.some(declaration => {
|
|
40
|
+
const rel = kit.graphPath(declaration);
|
|
41
|
+
return rel !== null && FE_DB_SLOTS.has(graph.files.get(rel)?.slot);
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const parentCall = (ts, node) => {
|
|
46
|
+
let current = node;
|
|
47
|
+
while (current.parent && ts.isParenthesizedExpression(current.parent)) current = current.parent;
|
|
48
|
+
return ts.isCallExpression(current.parent) ? current.parent : null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** True when `node` reads `.error` / `['error']` of the result symbol, or passes the result whole to toOutcome. */
|
|
52
|
+
const readsResultError = (kit, checker, graph, node, result) => {
|
|
53
|
+
const { ts } = kit;
|
|
54
|
+
if (ts.isPropertyAccessExpression(node) && node.name.text === 'error' && sameSymbol(kit, checker, node.expression, result)) return true;
|
|
55
|
+
if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression) && node.argumentExpression.text === 'error' && sameSymbol(kit, checker, node.expression, result)) return true;
|
|
56
|
+
return ts.isCallExpression(node) && callIsToOutcome(kit, checker, graph, node)
|
|
57
|
+
&& node.arguments.some(argument => sameSymbol(kit, checker, unwrap(ts, argument), result));
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** A result bound to one identifier is handled when the file reads its error or hands it whole to toOutcome. */
|
|
61
|
+
const identifierResultHandled = (kit, checker, graph, awaitNode, declaration) => {
|
|
62
|
+
const result = symbolOf(kit, checker, declaration.name);
|
|
63
|
+
let handled = false;
|
|
64
|
+
kit.walk(awaitNode.getSourceFile(), node => {
|
|
65
|
+
if (readsResultError(kit, checker, graph, node, result)) handled = true;
|
|
66
|
+
return !handled;
|
|
67
|
+
});
|
|
68
|
+
return handled;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** A result destructured into `{ error }` is handled when the bound error is referenced beyond its binding. */
|
|
72
|
+
const destructuredResultHandled = (kit, checker, awaitNode, declaration) => {
|
|
73
|
+
const errorBinding = declaration.name.elements.find(element => {
|
|
74
|
+
const property = element.propertyName ?? element.name;
|
|
75
|
+
return kit.ts.isIdentifier(property) && property.text === 'error';
|
|
76
|
+
});
|
|
77
|
+
if (!errorBinding || !kit.ts.isIdentifier(errorBinding.name)) return false;
|
|
78
|
+
const error = symbolOf(kit, checker, errorBinding.name);
|
|
79
|
+
let references = 0;
|
|
80
|
+
kit.walk(awaitNode.getSourceFile(), node => {
|
|
81
|
+
if (kit.ts.isIdentifier(node) && sameSymbol(kit, checker, node, error)) references += 1;
|
|
82
|
+
return true;
|
|
83
|
+
});
|
|
84
|
+
return references > 1;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const handledAwait = (kit, checker, graph, awaitNode) => {
|
|
88
|
+
const direct = parentCall(kit.ts, awaitNode);
|
|
89
|
+
if (direct && callIsToOutcome(kit, checker, graph, direct) && direct.arguments.some(argument => argument === awaitNode || argument === awaitNode.parent)) return true;
|
|
90
|
+
let cursor = awaitNode;
|
|
91
|
+
while (cursor.parent && kit.ts.isParenthesizedExpression(cursor.parent)) cursor = cursor.parent;
|
|
92
|
+
const declaration = kit.ts.isVariableDeclaration(cursor.parent) ? cursor.parent : null;
|
|
93
|
+
if (!declaration) return false;
|
|
94
|
+
if (kit.ts.isIdentifier(declaration.name)) return identifierResultHandled(kit, checker, graph, awaitNode, declaration);
|
|
95
|
+
if (!kit.ts.isObjectBindingPattern(declaration.name)) return false;
|
|
96
|
+
return destructuredResultHandled(kit, checker, awaitNode, declaration);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const dataSymbolFromAwait = (kit, checker, identifier) => {
|
|
100
|
+
const symbol = symbolOf(kit, checker, identifier);
|
|
101
|
+
for (const declaration of symbol?.declarations ?? []) {
|
|
102
|
+
if (!kit.ts.isBindingElement(declaration) || !kit.ts.isObjectBindingPattern(declaration.parent)) continue;
|
|
103
|
+
const property = declaration.propertyName ?? declaration.name;
|
|
104
|
+
if (!kit.ts.isIdentifier(property) || property.text !== 'data') continue;
|
|
105
|
+
const variable = declaration.parent.parent;
|
|
106
|
+
if (kit.ts.isVariableDeclaration(variable) && variable.initializer && fromAwaitedSupabase(kit, checker, variable.initializer)) return symbol;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function inspectResultGeneric(scope, node) {
|
|
112
|
+
const { ts, kit, checker, report } = scope;
|
|
113
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && QUERY_TERMINALS.has(node.expression.name.text)
|
|
114
|
+
&& node.typeArguments?.length && isSupabaseValue(kit, checker, node.expression.expression)) {
|
|
115
|
+
report(FE_RESULT_TYPED, node, `${node.expression.name.text}<T>() supplies a result generic; derive the row type from Database and let the Supabase chain infer it.`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function inspectResultCast(scope, node) {
|
|
120
|
+
const { ts, kit, checker, report } = scope;
|
|
121
|
+
if ((ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node)) && fromAwaitedSupabase(kit, checker, node.expression)) {
|
|
122
|
+
report(FE_RESULT_TYPED, node, 'A Supabase query result is cast or non-null asserted; narrow its typed data/error outcome instead.');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Counts a multi-row read and reports it when no limit, range or keyset bound applies. */
|
|
127
|
+
function inspectListRead(scope, node, methods) {
|
|
128
|
+
const isListRead = methods.includes('select') && !methods.some(method => QUERY_WRITES.has(method)) && !methods.includes('single') && !methods.includes('maybeSingle');
|
|
129
|
+
if (!isListRead) return;
|
|
130
|
+
scope.counters.listReads += 1;
|
|
131
|
+
if (!methods.includes('limit') && !methods.some(method => LIST_BOUNDS.has(method))) {
|
|
132
|
+
scope.report(FE_RESULT_TYPED, node, 'A Supabase list read has no .limit(), .range(), or keyset bound (.gt/.gte/.lt/.lte); bound every multi-row read.');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function inspectAwaitedResult(scope, node) {
|
|
137
|
+
const { ts, kit, checker, report, graph, counters } = scope;
|
|
138
|
+
if (!ts.isAwaitExpression(node) || !isSupabaseValue(kit, checker, node.expression)) return;
|
|
139
|
+
const expression = unwrap(ts, node.expression);
|
|
140
|
+
if (!ts.isCallExpression(expression)) return;
|
|
141
|
+
counters.awaited += 1;
|
|
142
|
+
inspectListRead(scope, node, chainParts(ts, expression));
|
|
143
|
+
if (!handledAwait(kit, checker, graph, node)) {
|
|
144
|
+
report(FE_ERROR_HANDLED, node, 'The awaited Supabase result does not read its error and is not passed whole to toOutcome(result).');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function inspectSwallowedData(scope, node) {
|
|
149
|
+
const { ts, kit, checker, report } = scope;
|
|
150
|
+
if (!(ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken && ts.isArrayLiteralExpression(node.right) && node.right.elements.length === 0)) return;
|
|
151
|
+
const left = unwrap(ts, node.left);
|
|
152
|
+
const swallowed = ts.isIdentifier(left) ? Boolean(dataSymbolFromAwait(kit, checker, left))
|
|
153
|
+
: ts.isPropertyAccessExpression(left) && left.name.text === 'data' && fromAwaitedSupabase(kit, checker, left.expression);
|
|
154
|
+
if (swallowed) report(FE_ERROR_HANDLED, node, 'Supabase data is collapsed with ?? []; refused or unavailable is not empty data. Pass the whole result to toOutcome.');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function inspectQueryResultNode(scope, node) {
|
|
158
|
+
inspectResultGeneric(scope, node);
|
|
159
|
+
inspectResultCast(scope, node);
|
|
160
|
+
inspectAwaitedResult(scope, node);
|
|
161
|
+
inspectSwallowedData(scope, node);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function checkFrontendQueryResults(input) {
|
|
166
|
+
const kit = machineKit(input);
|
|
167
|
+
const violations = [];
|
|
168
|
+
const counters = { awaited: 0, listReads: 0 };
|
|
169
|
+
for (const file of input.graph.files.values()) {
|
|
170
|
+
const checker = kit.checkerOf(file.sourceFile);
|
|
171
|
+
const seen = new Set();
|
|
172
|
+
const report = (ruleId, node, message) => {
|
|
173
|
+
const key = `${ruleId}:${node.pos}:${message}`;
|
|
174
|
+
if (seen.has(key)) return;
|
|
175
|
+
seen.add(key);
|
|
176
|
+
reportAt(violations, kit, ruleId, file, node, message);
|
|
177
|
+
};
|
|
178
|
+
const scope = { ts: kit.ts, kit, checker, report, graph: input.graph, counters };
|
|
179
|
+
kit.walk(file.sourceFile, node => inspectQueryResultNode(scope, node));
|
|
180
|
+
}
|
|
181
|
+
return { violations, coverage: { status: 'checked', awaited: counters.awaited, listReads: counters.listReads } };
|
|
182
|
+
}
|
|
@@ -12,6 +12,25 @@ const propertyMembers = (typescript, members, name) => {
|
|
|
12
12
|
return property?.type && typescript.isTypeLiteralNode(property.type) ? property.type.members : null;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
function addTableNames(typescript, members, tables) {
|
|
16
|
+
if (!members) return;
|
|
17
|
+
for (const member of members) {
|
|
18
|
+
if (!typescript.isPropertySignature(member)) continue;
|
|
19
|
+
const name = propertyName(typescript, member.name);
|
|
20
|
+
if (name) tables.add(name.toLowerCase());
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readTableNames(typescript, file, tables) {
|
|
25
|
+
const source = typescript.createSourceFile(file, fs.readFileSync(file, 'utf8'), typescript.ScriptTarget.Latest, true, typescript.ScriptKind.TS);
|
|
26
|
+
if (source.parseDiagnostics.length !== 0) return;
|
|
27
|
+
const database = source.statements.find(statement => typescript.isTypeAliasDeclaration(statement) && statement.name.text === 'Database');
|
|
28
|
+
const databaseMembers = database && typescript.isTypeLiteralNode(database.type) ? database.type.members : null;
|
|
29
|
+
const publicMembers = databaseMembers ? propertyMembers(typescript, databaseMembers, 'public') : null;
|
|
30
|
+
const tableMembers = publicMembers ? propertyMembers(typescript, publicMembers, 'Tables') : null;
|
|
31
|
+
addTableNames(typescript, tableMembers, tables);
|
|
32
|
+
}
|
|
33
|
+
|
|
15
34
|
/**
|
|
16
35
|
* The lower-case keys of `Database['public']['Tables']`, parsed with the architecture context's target compiler.
|
|
17
36
|
* An absent, malformed or unreadable contract proves no table. The result is cached once per app root.
|
|
@@ -22,18 +41,7 @@ export function supabaseTablesOf(appRoot, typescript) {
|
|
|
22
41
|
const tables = new Set();
|
|
23
42
|
const file = path.join(root, 'supabase', 'types', 'database.types.ts');
|
|
24
43
|
try {
|
|
25
|
-
|
|
26
|
-
if (source.parseDiagnostics.length === 0) {
|
|
27
|
-
const database = source.statements.find(statement => typescript.isTypeAliasDeclaration(statement) && statement.name.text === 'Database');
|
|
28
|
-
const databaseMembers = database && typescript.isTypeLiteralNode(database.type) ? database.type.members : null;
|
|
29
|
-
const publicMembers = databaseMembers ? propertyMembers(typescript, databaseMembers, 'public') : null;
|
|
30
|
-
const tableMembers = publicMembers ? propertyMembers(typescript, publicMembers, 'Tables') : null;
|
|
31
|
-
if (tableMembers) for (const member of tableMembers) {
|
|
32
|
-
if (!typescript.isPropertySignature(member)) continue;
|
|
33
|
-
const name = propertyName(typescript, member.name);
|
|
34
|
-
if (name) tables.add(name.toLowerCase());
|
|
35
|
-
}
|
|
36
|
-
}
|
|
44
|
+
readTableNames(typescript, file, tables);
|
|
37
45
|
} catch {
|
|
38
46
|
// Missing and unreadable generated contracts are fail-closed: no table is proven.
|
|
39
47
|
}
|