@topogram/cli 0.3.64 → 0.3.66
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/package.json +1 -1
- package/src/adoption/plan/index.js +716 -0
- package/src/adoption/plan.js +12 -703
- package/src/adoption/reporting.js +1 -1
- package/src/agent-brief.js +7 -21
- package/src/agent-ops/query-builders/auth.js +375 -0
- package/src/agent-ops/query-builders/change-risk/change-plan.js +123 -0
- package/src/agent-ops/query-builders/change-risk/import-plan.js +49 -0
- package/src/agent-ops/query-builders/change-risk/maintained.js +286 -0
- package/src/agent-ops/query-builders/change-risk/review-packets.js +123 -0
- package/src/agent-ops/query-builders/change-risk/risk.js +189 -0
- package/src/agent-ops/query-builders/change-risk.js +25 -0
- package/src/agent-ops/query-builders/common.js +149 -0
- package/src/agent-ops/query-builders/maintained-risk.js +539 -0
- package/src/agent-ops/query-builders/maintained-shared.js +120 -0
- package/src/agent-ops/query-builders/multi-agent.js +547 -0
- package/src/agent-ops/query-builders/projection-impacts.js +514 -0
- package/src/agent-ops/query-builders/work-packets.js +417 -0
- package/src/agent-ops/query-builders/workflow-context-shared.js +300 -0
- package/src/agent-ops/query-builders/workflow-context.js +398 -0
- package/src/agent-ops/query-builders/workflow-presets-core.js +677 -0
- package/src/agent-ops/query-builders/workflow-presets.js +341 -0
- package/src/agent-ops/query-builders.d.ts +26 -26
- package/src/agent-ops/query-builders.js +42 -5021
- package/src/archive/jsonl.js +2 -2
- package/src/archive/resolver-bridge.js +1 -1
- package/src/archive/unarchive.js +2 -1
- package/src/catalog/constants.js +10 -0
- package/src/catalog/copy.js +65 -0
- package/src/catalog/diagnostics.js +15 -0
- package/src/catalog/entries.js +42 -0
- package/src/catalog/files.js +67 -0
- package/src/catalog/provenance.js +123 -0
- package/src/catalog/source.js +150 -0
- package/src/catalog/validation.js +252 -0
- package/src/catalog.d.ts +2 -0
- package/src/catalog.js +18 -746
- package/src/cli/command-parsers/project.js +3 -0
- package/src/cli/command-parsers/shared.js +1 -1
- package/src/cli/commands/agent.js +2 -2
- package/src/cli/commands/catalog/check.js +31 -0
- package/src/cli/commands/catalog/copy.js +59 -0
- package/src/cli/commands/catalog/doctor.js +248 -0
- package/src/cli/commands/catalog/help.js +21 -0
- package/src/cli/commands/catalog/list.js +52 -0
- package/src/cli/commands/catalog/runner.js +92 -0
- package/src/cli/commands/catalog/shared.js +17 -0
- package/src/cli/commands/catalog/show.js +134 -0
- package/src/cli/commands/catalog.js +30 -615
- package/src/cli/commands/check.js +3 -3
- package/src/cli/commands/doctor.js +2 -9
- package/src/cli/commands/generator-policy/package-info.js +162 -0
- package/src/cli/commands/generator-policy/payloads.js +372 -0
- package/src/cli/commands/generator-policy/printers.js +159 -0
- package/src/cli/commands/generator-policy/runner.js +81 -0
- package/src/cli/commands/generator-policy/shared.js +39 -0
- package/src/cli/commands/generator-policy.js +15 -783
- package/src/cli/commands/import/adopt.js +170 -0
- package/src/cli/commands/import/check.js +91 -0
- package/src/cli/commands/import/diff.js +84 -0
- package/src/cli/commands/import/help.js +47 -0
- package/src/cli/commands/import/paths.js +269 -0
- package/src/cli/commands/import/plan.js +292 -0
- package/src/cli/commands/import/refresh.js +471 -0
- package/src/cli/commands/import/status-history.js +196 -0
- package/src/cli/commands/import/workspace.js +233 -0
- package/src/cli/commands/import.js +33 -1732
- package/src/cli/commands/migrate.js +153 -0
- package/src/cli/commands/package/constants.js +17 -0
- package/src/cli/commands/package/doctor.js +240 -0
- package/src/cli/commands/package/help.js +27 -0
- package/src/cli/commands/package/lockfile.js +135 -0
- package/src/cli/commands/package/npm.js +97 -0
- package/src/cli/commands/package/reporting.js +35 -0
- package/src/cli/commands/package/runner.js +33 -0
- package/src/cli/commands/package/shared.js +9 -0
- package/src/cli/commands/package/update-cli.js +252 -0
- package/src/cli/commands/package/versions.js +35 -0
- package/src/cli/commands/package.js +29 -813
- package/src/cli/commands/query/change-plan.js +68 -0
- package/src/cli/commands/query/definitions.js +202 -0
- package/src/cli/commands/query/import-adopt.js +121 -0
- package/src/cli/commands/query/runner/artifacts.js +102 -0
- package/src/cli/commands/query/runner/boundaries.js +211 -0
- package/src/cli/commands/query/runner/change.js +182 -0
- package/src/cli/commands/query/runner/import-adopt.js +111 -0
- package/src/cli/commands/query/runner/index.js +31 -0
- package/src/cli/commands/query/runner/output.js +12 -0
- package/src/cli/commands/query/runner/workflow.js +241 -0
- package/src/cli/commands/query/runner.js +3 -0
- package/src/cli/commands/query/workflow-context.js +5 -0
- package/src/cli/commands/query/workspace.js +270 -0
- package/src/cli/commands/query.js +9 -1300
- package/src/cli/commands/source.js +3 -12
- package/src/cli/commands/template/baseline.js +100 -0
- package/src/cli/commands/template/check.js +467 -0
- package/src/cli/commands/template/constants.js +8 -0
- package/src/cli/commands/template/diagnostics.js +26 -0
- package/src/cli/commands/template/help.js +28 -0
- package/src/cli/commands/template/lifecycle.js +404 -0
- package/src/cli/commands/template/list-show.js +287 -0
- package/src/cli/commands/template/policy.js +422 -0
- package/src/cli/commands/template/shared.js +127 -0
- package/src/cli/commands/template/updates.js +352 -0
- package/src/cli/commands/template-runner.js +6 -6
- package/src/cli/commands/template.js +41 -2143
- package/src/cli/commands/trust.js +1 -1
- package/src/cli/commands/workflow.js +6 -1
- package/src/cli/dispatcher.js +6 -1
- package/src/cli/help.js +15 -14
- package/src/cli/migration-guidance.js +1 -1
- package/src/cli/output-safety.js +2 -1
- package/src/cli/path-normalization.js +3 -13
- package/src/generator/api/contracts.js +497 -0
- package/src/generator/api/metadata.js +221 -0
- package/src/generator/api/openapi.js +559 -0
- package/src/generator/api/schema.js +124 -0
- package/src/generator/api/types.d.ts +98 -0
- package/src/generator/api.js +3 -1195
- package/src/generator/context/domain-page.js +1 -1
- package/src/generator/context/shared/domain-sdlc.js +282 -0
- package/src/generator/context/shared/maintained-boundary.js +665 -0
- package/src/generator/context/shared/metrics.js +85 -0
- package/src/generator/context/shared/primitives.js +64 -0
- package/src/generator/context/shared/relationships.js +453 -0
- package/src/generator/context/shared/summaries.js +263 -0
- package/src/generator/context/shared/types.d.ts +207 -0
- package/src/generator/context/shared.d.ts +42 -0
- package/src/generator/context/shared.js +80 -1390
- package/src/generator/context/slice/core.js +397 -0
- package/src/generator/context/slice/sdlc.js +417 -0
- package/src/generator/context/slice/ui-packets.js +183 -0
- package/src/generator/context/slice.js +2 -859
- package/src/generator/context/task-mode.js +2 -2
- package/src/generator/registry/index.js +507 -0
- package/src/generator/registry.js +18 -504
- package/src/generator/runtime/environment/index.js +666 -0
- package/src/generator/runtime/environment.js +4 -666
- package/src/generator/runtime/runtime-check/index.js +554 -0
- package/src/generator/runtime/runtime-check.js +4 -554
- package/src/generator/runtime/shared/index.js +572 -0
- package/src/generator/runtime/shared.js +19 -570
- package/src/generator/sdlc/doc-page.js +1 -1
- package/src/generator/shared.d.ts +2 -0
- package/src/generator/surfaces/databases/lifecycle-shared.js +1 -1
- package/src/generator/surfaces/native/swiftui-templates/README.generated.md +1 -1
- package/src/generator/surfaces/shared.d.ts +3 -0
- package/src/generator/widget-conformance/behavior-report.js +258 -0
- package/src/generator/widget-conformance/checks.js +371 -0
- package/src/generator/widget-conformance/projection-context.js +200 -0
- package/src/generator/widget-conformance/report.js +166 -0
- package/src/generator/widget-conformance/types.d.ts +121 -0
- package/src/generator/widget-conformance.js +3 -824
- package/src/import/core/context.d.ts +3 -0
- package/src/import/core/context.js +5 -7
- package/src/import/core/contracts.d.ts +1 -0
- package/src/import/core/registry.d.ts +4 -0
- package/src/import/core/runner/candidates.js +337 -0
- package/src/import/core/runner/options.js +22 -0
- package/src/import/core/runner/reports.js +51 -0
- package/src/import/core/runner/run.js +79 -0
- package/src/import/core/runner/tracks.js +150 -0
- package/src/import/core/runner/ui-drafts.js +393 -0
- package/src/import/core/runner.js +3 -698
- package/src/import/core/shared/api-routes.js +221 -0
- package/src/import/core/shared/candidates.js +97 -0
- package/src/import/core/shared/files.js +177 -0
- package/src/import/core/shared/next-app.js +389 -0
- package/src/import/core/shared/types.d.ts +51 -0
- package/src/import/core/shared/ui-routes.js +230 -0
- package/src/import/core/shared.js +60 -861
- package/src/new-project/constants.js +128 -0
- package/src/new-project/create.js +90 -0
- package/src/new-project/json.js +28 -0
- package/src/new-project/metadata.js +96 -0
- package/src/new-project/package-spec.js +161 -0
- package/src/new-project/project-files.js +351 -0
- package/src/new-project/template-policy.js +269 -0
- package/src/new-project/template-resolution.js +370 -0
- package/src/new-project/template-snapshots.js +442 -0
- package/src/new-project/template-updates.js +512 -0
- package/src/new-project/types.d.ts +83 -0
- package/src/new-project.js +6 -2277
- package/src/parser.d.ts +87 -1
- package/src/parser.js +118 -0
- package/src/policy/review-boundaries.d.ts +15 -0
- package/src/project-config/index.js +591 -0
- package/src/project-config.js +19 -561
- package/src/resolver/enrich/acceptance-criterion.js +2 -0
- package/src/resolver/enrich/bug.js +2 -0
- package/src/resolver/enrich/pitch.js +2 -0
- package/src/resolver/enrich/requirement.js +2 -0
- package/src/resolver/enrich/task.js +2 -0
- package/src/resolver/index.js +19 -2089
- package/src/resolver/normalize.js +384 -1
- package/src/resolver/plans.js +168 -0
- package/src/resolver/projections-api.js +494 -0
- package/src/resolver/projections-db.js +133 -0
- package/src/resolver/projections-ui.js +317 -0
- package/src/resolver/shapes.js +251 -0
- package/src/resolver/shared.js +278 -0
- package/src/resolver/widgets.js +132 -0
- package/src/sdlc/adopt.js +6 -5
- package/src/sdlc/paths.js +3 -5
- package/src/sdlc/scaffold.js +2 -1
- package/src/template-trust/constants.js +62 -0
- package/src/template-trust/content.js +258 -0
- package/src/template-trust/diff.js +92 -0
- package/src/template-trust/policy.js +61 -0
- package/src/template-trust/record.js +90 -0
- package/src/template-trust/status.js +182 -0
- package/src/template-trust.js +24 -687
- package/src/text-helpers.d.ts +1 -0
- package/src/topogram-types.d.ts +69 -0
- package/src/validator/common.js +488 -0
- package/src/validator/data-model.js +237 -0
- package/src/validator/docs.js +167 -0
- package/src/validator/expressions.js +146 -1
- package/src/validator/index.d.ts +23 -0
- package/src/validator/index.js +32 -3585
- package/src/validator/kinds.d.ts +41 -0
- package/src/validator/kinds.js +2 -0
- package/src/validator/model-helpers.js +46 -0
- package/src/validator/per-kind/acceptance-criterion.js +5 -0
- package/src/validator/per-kind/bug.js +6 -0
- package/src/validator/per-kind/domain.js +15 -2
- package/src/validator/per-kind/pitch.js +7 -0
- package/src/validator/per-kind/requirement.js +5 -0
- package/src/validator/per-kind/task.js +7 -0
- package/src/validator/per-kind/widget.js +14 -0
- package/src/validator/projections/api-http-async.js +410 -0
- package/src/validator/projections/api-http-authz.js +88 -0
- package/src/validator/projections/api-http-core.js +205 -0
- package/src/validator/projections/api-http-policies.js +339 -0
- package/src/validator/projections/api-http-responses.js +233 -0
- package/src/validator/projections/api-http.js +44 -0
- package/src/validator/projections/db.js +353 -0
- package/src/validator/projections/generator-defaults.js +45 -0
- package/src/validator/projections/helpers.js +87 -0
- package/src/validator/projections/ui-helpers.js +214 -0
- package/src/validator/projections/ui-navigation.js +344 -0
- package/src/validator/projections/ui-structure.js +364 -0
- package/src/validator/projections/ui-widgets.js +493 -0
- package/src/validator/projections/ui.js +46 -0
- package/src/validator/registry.js +48 -1
- package/src/validator/utils.d.ts +20 -0
- package/src/validator/utils.js +115 -12
- package/src/widget-behavior.d.ts +1 -0
- package/src/workflows/import-app/api/collect.js +221 -0
- package/src/workflows/import-app/api/openapi.js +257 -0
- package/src/workflows/import-app/api/routes.js +327 -0
- package/src/workflows/import-app/api/sources.js +22 -0
- package/src/workflows/import-app/api.js +2 -797
- package/src/workflows/reconcile/adoption-plan/build.js +212 -0
- package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
- package/src/workflows/reconcile/adoption-plan/outputs.js +153 -0
- package/src/workflows/reconcile/adoption-plan/paths.js +58 -0
- package/src/workflows/reconcile/adoption-plan/projection-patches.js +177 -0
- package/src/workflows/reconcile/adoption-plan/reasons.js +107 -0
- package/src/workflows/reconcile/adoption-plan.js +30 -740
- package/src/workflows/reconcile/auth/closures.js +115 -0
- package/src/workflows/reconcile/auth/formatters.js +142 -0
- package/src/workflows/reconcile/auth/inference.js +330 -0
- package/src/workflows/reconcile/auth/roles.js +122 -0
- package/src/workflows/reconcile/auth.js +35 -690
- package/src/workflows/reconcile/bundle-core/index.js +600 -0
- package/src/workflows/reconcile/bundle-core.js +12 -598
- package/src/workflows/reconcile/candidate-model.js +18 -2
- package/src/workflows/reconcile/canonical-surface.js +1 -1
- package/src/workflows/reconcile/impacts/adoption-plan.js +196 -0
- package/src/workflows/reconcile/impacts/indexes.js +105 -0
- package/src/workflows/reconcile/impacts/patches.js +252 -0
- package/src/workflows/reconcile/impacts/reports.js +80 -0
- package/src/workflows/reconcile/impacts.js +14 -623
- package/src/workflows/reconcile/renderers.js +41 -6
- package/src/workflows/shared.js +5 -11
- package/src/workspace-docs.d.ts +29 -0
- package/src/workspace-paths.js +328 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
blockSymbolItems,
|
|
5
|
+
getFieldValue,
|
|
6
|
+
pushError,
|
|
7
|
+
symbolValues
|
|
8
|
+
} from "../utils.js";
|
|
9
|
+
import { resolveCapabilityContractFields } from "./helpers.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {ValidationErrors} errors
|
|
13
|
+
* @param {TopogramStatement} statement
|
|
14
|
+
* @param {TopogramFieldMap} fieldMap
|
|
15
|
+
* @param {TopogramRegistry} registry
|
|
16
|
+
* @returns {void}
|
|
17
|
+
*/
|
|
18
|
+
export function validateProjectionHttpResponses(errors, statement, fieldMap, registry) {
|
|
19
|
+
if (statement.kind !== "projection") {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const httpResponsesField = fieldMap.get("responses")?.[0];
|
|
24
|
+
if (!httpResponsesField || httpResponsesField.value.type !== "block") {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
29
|
+
for (const entry of httpResponsesField.value.entries) {
|
|
30
|
+
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
31
|
+
const capabilityId = tokens[0];
|
|
32
|
+
const capability = registry.get(capabilityId);
|
|
33
|
+
|
|
34
|
+
if (!capability) {
|
|
35
|
+
pushError(errors, `Projection ${statement.id} responses references missing capability '${capabilityId}'`, entry.loc);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (capability.kind !== "capability") {
|
|
39
|
+
pushError(errors, `Projection ${statement.id} responses must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
40
|
+
}
|
|
41
|
+
if (!realized.has(capabilityId)) {
|
|
42
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const directives = parseProjectionHttpResponsesDirectives(tokens.slice(1));
|
|
46
|
+
for (const message of directives.errors) {
|
|
47
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' ${message}`, entry.loc);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!directives.mode) {
|
|
51
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'mode'`, entry.loc);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const mode = directives.mode;
|
|
55
|
+
if (mode && !["item", "collection", "paged", "cursor"].includes(mode)) {
|
|
56
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' has invalid mode '${mode}'`, entry.loc);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const itemShapeId = directives.item;
|
|
60
|
+
if (mode && mode !== "item" && !itemShapeId) {
|
|
61
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'item' for mode '${mode}'`, entry.loc);
|
|
62
|
+
}
|
|
63
|
+
if (itemShapeId) {
|
|
64
|
+
const itemShape = registry.get(itemShapeId);
|
|
65
|
+
if (!itemShape) {
|
|
66
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' references missing shape '${itemShapeId}'`, entry.loc);
|
|
67
|
+
} else if (itemShape.kind !== "shape") {
|
|
68
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must reference a shape for 'item', found ${itemShape.kind} '${itemShape.id}'`, entry.loc);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (mode === "cursor") {
|
|
73
|
+
if (!directives.cursor?.requestAfter) {
|
|
74
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'cursor request_after <field>'`, entry.loc);
|
|
75
|
+
}
|
|
76
|
+
if (!directives.cursor?.responseNext) {
|
|
77
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'cursor response_next <wire_name>'`, entry.loc);
|
|
78
|
+
}
|
|
79
|
+
if (!directives.limit) {
|
|
80
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'limit field <field> default <n> max <n>'`, entry.loc);
|
|
81
|
+
}
|
|
82
|
+
if (!directives.sort) {
|
|
83
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'sort by <field> direction <asc|desc>'`, entry.loc);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (directives.sort && !["asc", "desc"].includes(directives.sort.direction || "")) {
|
|
88
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' has invalid sort direction '${directives.sort.direction}'`, entry.loc);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (directives.total && !["true", "false"].includes(directives.total.included || "")) {
|
|
92
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' has invalid total included value '${directives.total.included}'`, entry.loc);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (directives.limit) {
|
|
96
|
+
const defaultValue = Number.parseInt(directives.limit.defaultValue || "", 10);
|
|
97
|
+
const maxValue = Number.parseInt(directives.limit.maxValue || "", 10);
|
|
98
|
+
if (!Number.isInteger(defaultValue) || !Number.isInteger(maxValue)) {
|
|
99
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must use integer default/max values for 'limit'`, entry.loc);
|
|
100
|
+
} else if (defaultValue > maxValue) {
|
|
101
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must use default <= max for 'limit'`, entry.loc);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const inputFields = resolveCapabilityContractFields(registry, capabilityId, "input");
|
|
106
|
+
const outputFields = resolveCapabilityContractFields(registry, capabilityId, "output");
|
|
107
|
+
if (directives.cursor?.requestAfter && inputFields.size > 0 && !inputFields.has(directives.cursor.requestAfter)) {
|
|
108
|
+
pushError(errors, `Projection ${statement.id} responses references unknown input field '${directives.cursor.requestAfter}' for cursor request_after on ${capabilityId}`, entry.loc);
|
|
109
|
+
}
|
|
110
|
+
if (directives.limit?.field && inputFields.size > 0 && !inputFields.has(directives.limit.field)) {
|
|
111
|
+
pushError(errors, `Projection ${statement.id} responses references unknown input field '${directives.limit.field}' for limit on ${capabilityId}`, entry.loc);
|
|
112
|
+
}
|
|
113
|
+
if (directives.sort?.field && outputFields.size > 0 && !outputFields.has(directives.sort.field)) {
|
|
114
|
+
pushError(errors, `Projection ${statement.id} responses references unknown output field '${directives.sort.field}' for sort on ${capabilityId}`, entry.loc);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {string[]} tokens
|
|
121
|
+
* @returns {{
|
|
122
|
+
* mode: string | null,
|
|
123
|
+
* item: string | null,
|
|
124
|
+
* cursor: { requestAfter: string | null, responseNext: string | null, responsePrev: string | null } | null,
|
|
125
|
+
* limit: { field: string | null, defaultValue: string | null, maxValue: string | null } | null,
|
|
126
|
+
* sort: { field: string | null, direction: string | null } | null,
|
|
127
|
+
* total: { included: string | null } | null,
|
|
128
|
+
* errors: string[]
|
|
129
|
+
* }}
|
|
130
|
+
*/
|
|
131
|
+
export function parseProjectionHttpResponsesDirectives(tokens) {
|
|
132
|
+
/** @type {{
|
|
133
|
+
* mode: string | null,
|
|
134
|
+
* item: string | null,
|
|
135
|
+
* cursor: { requestAfter: string | null, responseNext: string | null, responsePrev: string | null } | null,
|
|
136
|
+
* limit: { field: string | null, defaultValue: string | null, maxValue: string | null } | null,
|
|
137
|
+
* sort: { field: string | null, direction: string | null } | null,
|
|
138
|
+
* total: { included: string | null } | null,
|
|
139
|
+
* errors: string[]
|
|
140
|
+
* }}
|
|
141
|
+
*/
|
|
142
|
+
const result = {
|
|
143
|
+
mode: null,
|
|
144
|
+
item: null,
|
|
145
|
+
cursor: null,
|
|
146
|
+
limit: null,
|
|
147
|
+
sort: null,
|
|
148
|
+
total: null,
|
|
149
|
+
errors: []
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
153
|
+
const token = tokens[i];
|
|
154
|
+
if (token === "mode") {
|
|
155
|
+
result.mode = tokens[i + 1] || null;
|
|
156
|
+
if (!tokens[i + 1]) {
|
|
157
|
+
result.errors.push("is missing a value for 'mode'");
|
|
158
|
+
}
|
|
159
|
+
i += 1;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (token === "item") {
|
|
163
|
+
result.item = tokens[i + 1] || null;
|
|
164
|
+
if (!tokens[i + 1]) {
|
|
165
|
+
result.errors.push("is missing a value for 'item'");
|
|
166
|
+
}
|
|
167
|
+
i += 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (token === "cursor") {
|
|
171
|
+
const requestKeyword = tokens[i + 1];
|
|
172
|
+
const requestField = tokens[i + 2];
|
|
173
|
+
const responseKeyword = tokens[i + 3];
|
|
174
|
+
const responseNext = tokens[i + 4];
|
|
175
|
+
let responsePrev = null;
|
|
176
|
+
let consumed = 4;
|
|
177
|
+
if (tokens[i + 5] === "response_prev") {
|
|
178
|
+
responsePrev = tokens[i + 6] || null;
|
|
179
|
+
consumed = 6;
|
|
180
|
+
}
|
|
181
|
+
result.cursor = {
|
|
182
|
+
requestAfter: requestKeyword === "request_after" ? requestField : null,
|
|
183
|
+
responseNext: responseKeyword === "response_next" ? responseNext : null,
|
|
184
|
+
responsePrev
|
|
185
|
+
};
|
|
186
|
+
if (requestKeyword !== "request_after") {
|
|
187
|
+
result.errors.push("must use 'cursor request_after <field>'");
|
|
188
|
+
}
|
|
189
|
+
if (responseKeyword !== "response_next") {
|
|
190
|
+
result.errors.push("must use 'cursor response_next <wire_name>'");
|
|
191
|
+
}
|
|
192
|
+
i += consumed;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (token === "limit") {
|
|
196
|
+
result.limit = {
|
|
197
|
+
field: tokens[i + 1] === "field" ? tokens[i + 2] || null : null,
|
|
198
|
+
defaultValue: tokens[i + 3] === "default" ? tokens[i + 4] || null : null,
|
|
199
|
+
maxValue: tokens[i + 5] === "max" ? tokens[i + 6] || null : null
|
|
200
|
+
};
|
|
201
|
+
if (tokens[i + 1] !== "field" || tokens[i + 3] !== "default" || tokens[i + 5] !== "max") {
|
|
202
|
+
result.errors.push("must use 'limit field <field> default <n> max <n>'");
|
|
203
|
+
}
|
|
204
|
+
i += 6;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (token === "sort") {
|
|
208
|
+
result.sort = {
|
|
209
|
+
field: tokens[i + 1] === "by" ? tokens[i + 2] || null : null,
|
|
210
|
+
direction: tokens[i + 3] === "direction" ? tokens[i + 4] || null : null
|
|
211
|
+
};
|
|
212
|
+
if (tokens[i + 1] !== "by" || tokens[i + 3] !== "direction") {
|
|
213
|
+
result.errors.push("must use 'sort by <field> direction <asc|desc>'");
|
|
214
|
+
}
|
|
215
|
+
i += 4;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (token === "total") {
|
|
219
|
+
result.total = {
|
|
220
|
+
included: tokens[i + 1] === "included" ? tokens[i + 2] || null : null
|
|
221
|
+
};
|
|
222
|
+
if (tokens[i + 1] !== "included") {
|
|
223
|
+
result.errors.push("must use 'total included <true|false>'");
|
|
224
|
+
}
|
|
225
|
+
i += 2;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
result.errors.push(`has unknown directive '${token}'`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
validateProjectionHttp,
|
|
5
|
+
validateProjectionHttpErrors,
|
|
6
|
+
validateProjectionHttpFields
|
|
7
|
+
} from "./api-http-core.js";
|
|
8
|
+
import { validateProjectionHttpResponses } from "./api-http-responses.js";
|
|
9
|
+
import {
|
|
10
|
+
validateProjectionHttpPreconditions,
|
|
11
|
+
validateProjectionHttpIdempotency,
|
|
12
|
+
validateProjectionHttpCache,
|
|
13
|
+
validateProjectionHttpDelete
|
|
14
|
+
} from "./api-http-policies.js";
|
|
15
|
+
import {
|
|
16
|
+
validateProjectionHttpAsync,
|
|
17
|
+
validateProjectionHttpStatus,
|
|
18
|
+
validateProjectionHttpDownload,
|
|
19
|
+
validateProjectionHttpCallbacks
|
|
20
|
+
} from "./api-http-async.js";
|
|
21
|
+
import { validateProjectionHttpAuthz } from "./api-http-authz.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {ValidationErrors} errors
|
|
25
|
+
* @param {TopogramStatement} statement
|
|
26
|
+
* @param {TopogramFieldMap} fieldMap
|
|
27
|
+
* @param {TopogramRegistry} registry
|
|
28
|
+
* @returns {void}
|
|
29
|
+
*/
|
|
30
|
+
export function validateApiHttpProjection(errors, statement, fieldMap, registry) {
|
|
31
|
+
validateProjectionHttp(errors, statement, fieldMap, registry);
|
|
32
|
+
validateProjectionHttpErrors(errors, statement, fieldMap, registry);
|
|
33
|
+
validateProjectionHttpFields(errors, statement, fieldMap, registry);
|
|
34
|
+
validateProjectionHttpResponses(errors, statement, fieldMap, registry);
|
|
35
|
+
validateProjectionHttpPreconditions(errors, statement, fieldMap, registry);
|
|
36
|
+
validateProjectionHttpIdempotency(errors, statement, fieldMap, registry);
|
|
37
|
+
validateProjectionHttpCache(errors, statement, fieldMap, registry);
|
|
38
|
+
validateProjectionHttpDelete(errors, statement, fieldMap, registry);
|
|
39
|
+
validateProjectionHttpAsync(errors, statement, fieldMap, registry);
|
|
40
|
+
validateProjectionHttpStatus(errors, statement, fieldMap, registry);
|
|
41
|
+
validateProjectionHttpDownload(errors, statement, fieldMap, registry);
|
|
42
|
+
validateProjectionHttpAuthz(errors, statement, fieldMap, registry);
|
|
43
|
+
validateProjectionHttpCallbacks(errors, statement, fieldMap, registry);
|
|
44
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
blockSymbolItems,
|
|
5
|
+
getFieldValue,
|
|
6
|
+
pushError,
|
|
7
|
+
symbolValues
|
|
8
|
+
} from "../utils.js";
|
|
9
|
+
import { statementFieldNames } from "../model-helpers.js";
|
|
10
|
+
import { parseUiDirectiveMap } from "./helpers.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {ValidationErrors} errors
|
|
14
|
+
* @param {TopogramStatement} statement
|
|
15
|
+
* @param {TopogramFieldMap} fieldMap
|
|
16
|
+
* @param {TopogramRegistry} registry
|
|
17
|
+
* @returns {void}
|
|
18
|
+
*/
|
|
19
|
+
function validateProjectionDbTables(errors, statement, fieldMap, registry) {
|
|
20
|
+
if (statement.kind !== "projection") {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const dbTablesField = fieldMap.get("tables")?.[0];
|
|
25
|
+
if (!dbTablesField || dbTablesField.value.type !== "block") {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
30
|
+
const seenTables = new Set();
|
|
31
|
+
for (const entry of dbTablesField.value.entries) {
|
|
32
|
+
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
33
|
+
const [entityId, tableKeyword, tableName] = tokens;
|
|
34
|
+
const entity = registry.get(entityId);
|
|
35
|
+
|
|
36
|
+
if (!entity) {
|
|
37
|
+
pushError(errors, `Projection ${statement.id} tables references missing entity '${entityId}'`, entry.loc);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (entity.kind !== "entity") {
|
|
41
|
+
pushError(errors, `Projection ${statement.id} tables must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
42
|
+
}
|
|
43
|
+
if (!realized.has(entityId)) {
|
|
44
|
+
pushError(errors, `Projection ${statement.id} tables entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
45
|
+
}
|
|
46
|
+
if (tableKeyword !== "table") {
|
|
47
|
+
pushError(errors, `Projection ${statement.id} tables for '${entityId}' must use 'table'`, entry.loc);
|
|
48
|
+
}
|
|
49
|
+
if (!tableName) {
|
|
50
|
+
pushError(errors, `Projection ${statement.id} tables for '${entityId}' must include a table name`, entry.loc);
|
|
51
|
+
} else if (seenTables.has(tableName)) {
|
|
52
|
+
pushError(errors, `Projection ${statement.id} tables has duplicate table name '${tableName}'`, entry.loc);
|
|
53
|
+
}
|
|
54
|
+
seenTables.add(tableName);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {ValidationErrors} errors
|
|
60
|
+
* @param {TopogramStatement} statement
|
|
61
|
+
* @param {TopogramFieldMap} fieldMap
|
|
62
|
+
* @param {TopogramRegistry} registry
|
|
63
|
+
* @returns {void}
|
|
64
|
+
*/
|
|
65
|
+
function validateProjectionDbColumns(errors, statement, fieldMap, registry) {
|
|
66
|
+
if (statement.kind !== "projection") {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const dbColumnsField = fieldMap.get("columns")?.[0];
|
|
71
|
+
if (!dbColumnsField || dbColumnsField.value.type !== "block") {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
76
|
+
for (const entry of dbColumnsField.value.entries) {
|
|
77
|
+
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
78
|
+
const [entityId, fieldKeyword, fieldName, columnKeyword, columnName] = tokens;
|
|
79
|
+
const entity = registry.get(entityId);
|
|
80
|
+
|
|
81
|
+
if (!entity) {
|
|
82
|
+
pushError(errors, `Projection ${statement.id} columns references missing entity '${entityId}'`, entry.loc);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (entity.kind !== "entity") {
|
|
86
|
+
pushError(errors, `Projection ${statement.id} columns must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
87
|
+
}
|
|
88
|
+
if (!realized.has(entityId)) {
|
|
89
|
+
pushError(errors, `Projection ${statement.id} columns entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
90
|
+
}
|
|
91
|
+
if (fieldKeyword !== "field") {
|
|
92
|
+
pushError(errors, `Projection ${statement.id} columns for '${entityId}' must use 'field'`, entry.loc);
|
|
93
|
+
}
|
|
94
|
+
if (columnKeyword !== "column") {
|
|
95
|
+
pushError(errors, `Projection ${statement.id} columns for '${entityId}' must use 'column'`, entry.loc);
|
|
96
|
+
}
|
|
97
|
+
const entityFieldNames = new Set(statementFieldNames(entity));
|
|
98
|
+
if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
|
|
99
|
+
pushError(errors, `Projection ${statement.id} columns references unknown field '${fieldName}' on ${entityId}`, entry.loc);
|
|
100
|
+
}
|
|
101
|
+
if (!columnName) {
|
|
102
|
+
pushError(errors, `Projection ${statement.id} columns for '${entityId}.${fieldName}' must include a column name`, entry.loc);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {ValidationErrors} errors
|
|
109
|
+
* @param {TopogramStatement} statement
|
|
110
|
+
* @param {TopogramFieldMap} fieldMap
|
|
111
|
+
* @param {TopogramRegistry} registry
|
|
112
|
+
* @returns {void}
|
|
113
|
+
*/
|
|
114
|
+
function validateProjectionDbKeys(errors, statement, fieldMap, registry) {
|
|
115
|
+
if (statement.kind !== "projection") {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const dbKeysField = fieldMap.get("keys")?.[0];
|
|
120
|
+
if (!dbKeysField || dbKeysField.value.type !== "block") {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
125
|
+
for (const entry of dbKeysField.value.entries) {
|
|
126
|
+
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
127
|
+
const [entityId, keyType] = tokens;
|
|
128
|
+
const entity = registry.get(entityId);
|
|
129
|
+
|
|
130
|
+
if (!entity) {
|
|
131
|
+
pushError(errors, `Projection ${statement.id} keys references missing entity '${entityId}'`, entry.loc);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (entity.kind !== "entity") {
|
|
135
|
+
pushError(errors, `Projection ${statement.id} keys must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
136
|
+
}
|
|
137
|
+
if (!realized.has(entityId)) {
|
|
138
|
+
pushError(errors, `Projection ${statement.id} keys entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
139
|
+
}
|
|
140
|
+
if (!["primary", "unique"].includes(keyType || "")) {
|
|
141
|
+
pushError(errors, `Projection ${statement.id} keys for '${entityId}' has invalid key type '${keyType}'`, entry.loc);
|
|
142
|
+
}
|
|
143
|
+
const fieldList = entry.items[2];
|
|
144
|
+
if (!fieldList || fieldList.type !== "list" || fieldList.items.length === 0) {
|
|
145
|
+
pushError(errors, `Projection ${statement.id} keys for '${entityId}' must include a non-empty field list`, entry.loc);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const entityFieldNames = new Set(statementFieldNames(entity));
|
|
149
|
+
for (const item of fieldList.items) {
|
|
150
|
+
if (item.type === "symbol" && entityFieldNames.size > 0 && !entityFieldNames.has(item.value)) {
|
|
151
|
+
pushError(errors, `Projection ${statement.id} keys references unknown field '${item.value}' on ${entityId}`, item.loc);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {ValidationErrors} errors
|
|
159
|
+
* @param {TopogramStatement} statement
|
|
160
|
+
* @param {TopogramFieldMap} fieldMap
|
|
161
|
+
* @param {TopogramRegistry} registry
|
|
162
|
+
* @returns {void}
|
|
163
|
+
*/
|
|
164
|
+
function validateProjectionDbIndexes(errors, statement, fieldMap, registry) {
|
|
165
|
+
if (statement.kind !== "projection") {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const dbIndexesField = fieldMap.get("indexes")?.[0];
|
|
170
|
+
if (!dbIndexesField || dbIndexesField.value.type !== "block") {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
175
|
+
for (const entry of dbIndexesField.value.entries) {
|
|
176
|
+
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
177
|
+
const [entityId, indexType] = tokens;
|
|
178
|
+
const entity = registry.get(entityId);
|
|
179
|
+
|
|
180
|
+
if (!entity) {
|
|
181
|
+
pushError(errors, `Projection ${statement.id} indexes references missing entity '${entityId}'`, entry.loc);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (entity.kind !== "entity") {
|
|
185
|
+
pushError(errors, `Projection ${statement.id} indexes must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
186
|
+
}
|
|
187
|
+
if (!realized.has(entityId)) {
|
|
188
|
+
pushError(errors, `Projection ${statement.id} indexes entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
189
|
+
}
|
|
190
|
+
if (!["index", "unique"].includes(indexType || "")) {
|
|
191
|
+
pushError(errors, `Projection ${statement.id} indexes for '${entityId}' has invalid index type '${indexType}'`, entry.loc);
|
|
192
|
+
}
|
|
193
|
+
const fieldList = entry.items[2];
|
|
194
|
+
if (!fieldList || fieldList.type !== "list" || fieldList.items.length === 0) {
|
|
195
|
+
pushError(errors, `Projection ${statement.id} indexes for '${entityId}' must include a non-empty field list`, entry.loc);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const entityFieldNames = new Set(statementFieldNames(entity));
|
|
199
|
+
for (const item of fieldList.items) {
|
|
200
|
+
if (item.type === "symbol" && entityFieldNames.size > 0 && !entityFieldNames.has(item.value)) {
|
|
201
|
+
pushError(errors, `Projection ${statement.id} indexes references unknown field '${item.value}' on ${entityId}`, item.loc);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* @param {ValidationErrors} errors
|
|
209
|
+
* @param {TopogramStatement} statement
|
|
210
|
+
* @param {TopogramFieldMap} fieldMap
|
|
211
|
+
* @param {TopogramRegistry} registry
|
|
212
|
+
* @returns {void}
|
|
213
|
+
*/
|
|
214
|
+
function validateProjectionDbRelations(errors, statement, fieldMap, registry) {
|
|
215
|
+
if (statement.kind !== "projection") {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const dbRelationsField = fieldMap.get("relations")?.[0];
|
|
220
|
+
if (!dbRelationsField || dbRelationsField.value.type !== "block") {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
225
|
+
for (const entry of dbRelationsField.value.entries) {
|
|
226
|
+
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
227
|
+
const [entityId, relationType, fieldName, referencesKeyword, targetRef, onDeleteKeyword, onDeleteValue] = tokens;
|
|
228
|
+
const entity = registry.get(entityId);
|
|
229
|
+
|
|
230
|
+
if (!entity) {
|
|
231
|
+
pushError(errors, `Projection ${statement.id} relations references missing entity '${entityId}'`, entry.loc);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (entity.kind !== "entity") {
|
|
235
|
+
pushError(errors, `Projection ${statement.id} relations must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
236
|
+
}
|
|
237
|
+
if (!realized.has(entityId)) {
|
|
238
|
+
pushError(errors, `Projection ${statement.id} relations entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
239
|
+
}
|
|
240
|
+
if (relationType !== "foreign_key") {
|
|
241
|
+
pushError(errors, `Projection ${statement.id} relations for '${entityId}' must use 'foreign_key'`, entry.loc);
|
|
242
|
+
}
|
|
243
|
+
if (referencesKeyword !== "references") {
|
|
244
|
+
pushError(errors, `Projection ${statement.id} relations for '${entityId}' must use 'references'`, entry.loc);
|
|
245
|
+
}
|
|
246
|
+
if (onDeleteKeyword && onDeleteKeyword !== "on_delete") {
|
|
247
|
+
pushError(errors, `Projection ${statement.id} relations for '${entityId}' has unexpected token '${onDeleteKeyword}'`, entry.loc);
|
|
248
|
+
}
|
|
249
|
+
if (onDeleteValue && !["cascade", "restrict", "set_null", "no_action"].includes(onDeleteValue)) {
|
|
250
|
+
pushError(errors, `Projection ${statement.id} relations for '${entityId}' has invalid on_delete '${onDeleteValue}'`, entry.loc);
|
|
251
|
+
}
|
|
252
|
+
const entityFieldNames = new Set(statementFieldNames(entity));
|
|
253
|
+
if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
|
|
254
|
+
pushError(errors, `Projection ${statement.id} relations references unknown field '${fieldName}' on ${entityId}`, entry.loc);
|
|
255
|
+
}
|
|
256
|
+
const [targetEntityId, targetFieldName] = (targetRef || "").split(".");
|
|
257
|
+
const targetEntity = registry.get(targetEntityId);
|
|
258
|
+
if (!targetEntity) {
|
|
259
|
+
pushError(errors, `Projection ${statement.id} relations references missing target entity '${targetEntityId}'`, entry.loc);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (targetEntity.kind !== "entity") {
|
|
263
|
+
pushError(errors, `Projection ${statement.id} relations must reference an entity target, found ${targetEntity.kind} '${targetEntity.id}'`, entry.loc);
|
|
264
|
+
}
|
|
265
|
+
const targetFieldNames = new Set(statementFieldNames(targetEntity));
|
|
266
|
+
if (targetFieldName && targetFieldNames.size > 0 && !targetFieldNames.has(targetFieldName)) {
|
|
267
|
+
pushError(errors, `Projection ${statement.id} relations references unknown target field '${targetFieldName}' on ${targetEntityId}`, entry.loc);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* @param {ValidationErrors} errors
|
|
274
|
+
* @param {TopogramStatement} statement
|
|
275
|
+
* @param {TopogramFieldMap} fieldMap
|
|
276
|
+
* @param {TopogramRegistry} registry
|
|
277
|
+
* @returns {void}
|
|
278
|
+
*/
|
|
279
|
+
function validateProjectionDbLifecycle(errors, statement, fieldMap, registry) {
|
|
280
|
+
if (statement.kind !== "projection") {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const dbLifecycleField = fieldMap.get("lifecycle")?.[0];
|
|
285
|
+
if (!dbLifecycleField || dbLifecycleField.value.type !== "block") {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
290
|
+
for (const entry of dbLifecycleField.value.entries) {
|
|
291
|
+
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
292
|
+
const [entityId, lifecycleType] = tokens;
|
|
293
|
+
const entity = registry.get(entityId);
|
|
294
|
+
|
|
295
|
+
if (!entity) {
|
|
296
|
+
pushError(errors, `Projection ${statement.id} lifecycle references missing entity '${entityId}'`, entry.loc);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (entity.kind !== "entity") {
|
|
300
|
+
pushError(errors, `Projection ${statement.id} lifecycle must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
301
|
+
}
|
|
302
|
+
if (!realized.has(entityId)) {
|
|
303
|
+
pushError(errors, `Projection ${statement.id} lifecycle entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `lifecycle for '${entityId}'`);
|
|
307
|
+
if (!["soft_delete", "timestamps"].includes(lifecycleType || "")) {
|
|
308
|
+
pushError(errors, `Projection ${statement.id} lifecycle for '${entityId}' has invalid lifecycle '${lifecycleType}'`, entry.loc);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const entityFieldNames = new Set(statementFieldNames(entity));
|
|
313
|
+
if (lifecycleType === "soft_delete") {
|
|
314
|
+
for (const requiredKey of ["field", "value"]) {
|
|
315
|
+
if (!directives.has(requiredKey)) {
|
|
316
|
+
pushError(errors, `Projection ${statement.id} lifecycle for '${entityId}' must include '${requiredKey}' for soft_delete`, entry.loc);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const fieldName = directives.get("field");
|
|
320
|
+
if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
|
|
321
|
+
pushError(errors, `Projection ${statement.id} lifecycle references unknown field '${fieldName}' on ${entityId}`, entry.loc);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (lifecycleType === "timestamps") {
|
|
326
|
+
for (const requiredKey of ["created_at", "updated_at"]) {
|
|
327
|
+
if (!directives.has(requiredKey)) {
|
|
328
|
+
pushError(errors, `Projection ${statement.id} lifecycle for '${entityId}' must include '${requiredKey}' for timestamps`, entry.loc);
|
|
329
|
+
}
|
|
330
|
+
const fieldName = directives.get(requiredKey);
|
|
331
|
+
if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
|
|
332
|
+
pushError(errors, `Projection ${statement.id} lifecycle references unknown field '${fieldName}' on ${entityId}`, entry.loc);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* @param {ValidationErrors} errors
|
|
341
|
+
* @param {TopogramStatement} statement
|
|
342
|
+
* @param {TopogramFieldMap} fieldMap
|
|
343
|
+
* @param {TopogramRegistry} registry
|
|
344
|
+
* @returns {void}
|
|
345
|
+
*/
|
|
346
|
+
export function validateDbProjection(errors, statement, fieldMap, registry) {
|
|
347
|
+
validateProjectionDbTables(errors, statement, fieldMap, registry);
|
|
348
|
+
validateProjectionDbColumns(errors, statement, fieldMap, registry);
|
|
349
|
+
validateProjectionDbKeys(errors, statement, fieldMap, registry);
|
|
350
|
+
validateProjectionDbIndexes(errors, statement, fieldMap, registry);
|
|
351
|
+
validateProjectionDbRelations(errors, statement, fieldMap, registry);
|
|
352
|
+
validateProjectionDbLifecycle(errors, statement, fieldMap, registry);
|
|
353
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
blockSymbolItems,
|
|
5
|
+
pushError
|
|
6
|
+
} from "../utils.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {ValidationErrors} errors
|
|
10
|
+
* @param {TopogramStatement} statement
|
|
11
|
+
* @param {TopogramFieldMap} fieldMap
|
|
12
|
+
* @returns {void}
|
|
13
|
+
*/
|
|
14
|
+
export function validateProjectionGeneratorDefaults(errors, statement, fieldMap) {
|
|
15
|
+
if (statement.kind !== "projection") {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const generatorField = fieldMap.get("generator_defaults")?.[0];
|
|
20
|
+
if (!generatorField || generatorField.value.type !== "block") {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const entry of generatorField.value.entries) {
|
|
25
|
+
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
26
|
+
const [key, value] = tokens;
|
|
27
|
+
if (!["profile", "language", "styling"].includes(key || "")) {
|
|
28
|
+
pushError(errors, `Projection ${statement.id} generator_defaults has unknown key '${key}'`, entry.loc);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (!value) {
|
|
32
|
+
pushError(errors, `Projection ${statement.id} generator_defaults is missing a value for '${key}'`, entry.loc);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (key === "profile" && !["vanilla", "sveltekit", "react", "swiftui", "postgres_sql", "sqlite_sql", "prisma", "drizzle"].includes(value)) {
|
|
36
|
+
pushError(errors, `Projection ${statement.id} generator_defaults has unsupported profile '${value}'`, entry.loc);
|
|
37
|
+
}
|
|
38
|
+
if (key === "language" && !["typescript", "javascript", "swift", "sql"].includes(value)) {
|
|
39
|
+
pushError(errors, `Projection ${statement.id} generator_defaults has unsupported language '${value}'`, entry.loc);
|
|
40
|
+
}
|
|
41
|
+
if (key === "styling" && !["tailwind", "css"].includes(value)) {
|
|
42
|
+
pushError(errors, `Projection ${statement.id} generator_defaults has unsupported styling '${value}'`, entry.loc);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|