@topogram/cli 0.3.63 → 0.3.65
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 +703 -0
- package/src/adoption/plan.d.ts +6 -0
- package/src/adoption/plan.js +12 -703
- package/src/adoption/reporting.d.ts +10 -0
- package/src/adoption/review-groups.d.ts +6 -0
- package/src/agent-brief.d.ts +3 -0
- package/src/agent-brief.js +495 -0
- 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 +676 -0
- package/src/agent-ops/query-builders/workflow-presets.js +341 -0
- package/src/agent-ops/query-builders.d.ts +26 -0
- package/src/agent-ops/query-builders.js +42 -5021
- package/src/archive/archive.d.ts +2 -0
- package/src/archive/compact.d.ts +1 -0
- package/src/archive/unarchive.d.ts +1 -0
- package/src/catalog/constants.js +10 -0
- package/src/catalog/copy.js +60 -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 +122 -0
- package/src/catalog/source.js +150 -0
- package/src/catalog/validation.js +252 -0
- package/src/catalog.d.ts +12 -0
- package/src/catalog.js +18 -750
- package/src/cli/catalog-alias.d.ts +1 -0
- package/src/cli/command-parser.js +38 -0
- package/src/cli/command-parsers/core.js +102 -0
- package/src/cli/command-parsers/generator.js +39 -0
- package/src/cli/command-parsers/import.js +44 -0
- package/src/cli/command-parsers/legacy-workflow.js +21 -0
- package/src/cli/command-parsers/project.js +47 -0
- package/src/cli/command-parsers/sdlc.js +47 -0
- package/src/cli/command-parsers/shared.js +51 -0
- package/src/cli/command-parsers/template.js +48 -0
- package/src/cli/commands/agent.js +47 -0
- 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 +32 -0
- package/src/cli/commands/check.js +268 -0
- package/src/cli/commands/doctor.js +268 -0
- package/src/cli/commands/emit.js +149 -0
- package/src/cli/commands/generate.js +96 -0
- 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 +17 -0
- package/src/cli/commands/generator.js +443 -0
- 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 +277 -0
- package/src/cli/commands/import/plan.js +284 -0
- package/src/cli/commands/import/refresh.js +470 -0
- package/src/cli/commands/import/status-history.js +196 -0
- package/src/cli/commands/import/workspace.js +230 -0
- package/src/cli/commands/import-runner.js +157 -0
- package/src/cli/commands/import.js +35 -0
- package/src/cli/commands/inspect.js +55 -0
- package/src/cli/commands/new.js +94 -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 +31 -0
- 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 +274 -0
- package/src/cli/commands/query.js +11 -0
- package/src/cli/commands/release-rollout.js +257 -0
- package/src/cli/commands/release-shared.js +528 -0
- package/src/cli/commands/release-status.js +429 -0
- package/src/cli/commands/release.js +107 -0
- package/src/cli/commands/sdlc.js +168 -0
- package/src/cli/commands/setup.js +76 -0
- package/src/cli/commands/source.js +291 -0
- package/src/cli/commands/template/baseline.js +100 -0
- package/src/cli/commands/template/check.js +466 -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 +198 -0
- package/src/cli/commands/template.js +43 -0
- package/src/cli/commands/trust.js +219 -0
- package/src/cli/commands/version.js +40 -0
- package/src/cli/commands/widget.js +168 -0
- package/src/cli/commands/workflow.js +63 -0
- package/src/cli/dispatcher.js +392 -0
- package/src/cli/help-dispatch.js +188 -0
- package/src/cli/help.js +296 -0
- package/src/cli/migration-guidance.js +59 -0
- package/src/cli/options.js +96 -0
- package/src/cli/output-safety.js +107 -0
- package/src/cli/path-normalization.js +29 -0
- package/src/cli.js +47 -11711
- package/src/example-implementation.d.ts +2 -0
- package/src/format.d.ts +1 -0
- 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/check.d.ts +1 -0
- package/src/generator/context/bundle.d.ts +1 -0
- 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 +44 -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/native/parity-bundle.js +2 -1
- 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/shared.d.ts +2 -0
- package/src/generator/surfaces/shared.d.ts +3 -0
- package/src/generator/surfaces/web/html-escape.js +22 -0
- package/src/generator/surfaces/web/react.js +10 -8
- package/src/generator/surfaces/web/sveltekit.js +7 -5
- package/src/generator/surfaces/web/vanilla.js +8 -4
- 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/generator.d.ts +2 -0
- package/src/github-client.js +520 -0
- package/src/import/core/context.d.ts +3 -0
- 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 +217 -0
- package/src/import/core/runner/options.js +22 -0
- package/src/import/core/runner/reports.js +50 -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 +337 -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 +67 -910
- package/src/import/extractors/api/flutter-dio.js +4 -8
- package/src/import/extractors/api/react-native-repository.js +4 -8
- package/src/import/index.d.ts +4 -0
- package/src/import/provenance.d.ts +4 -0
- package/src/new-project/constants.js +128 -0
- package/src/new-project/create.js +83 -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 +348 -0
- package/src/new-project/template-policy.js +269 -0
- package/src/new-project/template-resolution.js +368 -0
- package/src/new-project/template-snapshots.js +430 -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 -2188
- package/src/npm-safety.js +79 -0
- package/src/parser.d.ts +87 -0
- package/src/parser.js +118 -0
- package/src/path-helpers.d.ts +1 -0
- package/src/path-helpers.js +20 -0
- package/src/policy/review-boundaries.d.ts +15 -0
- package/src/project-config/index.js +564 -0
- package/src/project-config.js +19 -560
- package/src/reconcile/docs.d.ts +8 -0
- package/src/reconcile/journeys.d.ts +1 -0
- 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/resolver.d.ts +1 -0
- package/src/runtime-support.js +29 -0
- package/src/sdlc/adopt.d.ts +1 -0
- package/src/sdlc/check.d.ts +1 -0
- package/src/sdlc/explain.d.ts +1 -0
- package/src/sdlc/release.d.ts +1 -0
- package/src/sdlc/scaffold.d.ts +1 -0
- package/src/sdlc/transition.d.ts +1 -0
- 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 +7 -0
- package/src/text-helpers.js +245 -0
- package/src/topogram-config.js +306 -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/validator.d.ts +2 -0
- package/src/widget-behavior.d.ts +1 -0
- package/src/workflows/adoption/index.js +26 -0
- package/src/workflows/docs-generate.js +262 -0
- package/src/workflows/docs-scan.js +703 -0
- package/src/workflows/docs.js +15 -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 +4 -0
- package/src/workflows/import-app/db.js +538 -0
- package/src/workflows/import-app/index.js +30 -0
- package/src/workflows/import-app/shared.js +218 -0
- package/src/workflows/import-app/ui.js +443 -0
- package/src/workflows/import-app/workflow.js +159 -0
- package/src/workflows/reconcile/adoption-plan/build.js +208 -0
- package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
- package/src/workflows/reconcile/adoption-plan/outputs.js +143 -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 +32 -0
- 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 +37 -0
- package/src/workflows/reconcile/bundle-core/index.js +600 -0
- package/src/workflows/reconcile/bundle-core.js +14 -0
- package/src/workflows/reconcile/bundle-shared.js +75 -0
- package/src/workflows/reconcile/candidate-model.js +477 -0
- package/src/workflows/reconcile/canonical-surface.js +264 -0
- package/src/workflows/reconcile/gap-report.js +333 -0
- package/src/workflows/reconcile/ids.js +6 -0
- package/src/workflows/reconcile/impacts/adoption-plan.js +192 -0
- package/src/workflows/reconcile/impacts/indexes.js +101 -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 +16 -0
- package/src/workflows/reconcile/index.js +7 -0
- package/src/workflows/reconcile/renderers.js +461 -0
- package/src/workflows/reconcile/summary.js +90 -0
- package/src/workflows/reconcile/workflow.js +309 -0
- package/src/workflows/shared.js +189 -0
- package/src/workflows/types.d.ts +93 -0
- package/src/workflows.d.ts +1 -0
- package/src/workflows.js +10 -7652
- package/src/workspace-docs.d.ts +29 -0
package/src/generator/api.js
CHANGED
|
@@ -1,1196 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// @ts-check
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
function indexStatements(graph) {
|
|
6
|
-
const byId = new Map();
|
|
7
|
-
for (const statement of graph.statements) {
|
|
8
|
-
byId.set(statement.id, statement);
|
|
9
|
-
}
|
|
10
|
-
return byId;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function scalarSchema(typeName, byId) {
|
|
14
|
-
switch (typeName) {
|
|
15
|
-
case "string":
|
|
16
|
-
case "text":
|
|
17
|
-
return { type: "string" };
|
|
18
|
-
case "integer":
|
|
19
|
-
return { type: "integer" };
|
|
20
|
-
case "number":
|
|
21
|
-
return { type: "number" };
|
|
22
|
-
case "boolean":
|
|
23
|
-
return { type: "boolean" };
|
|
24
|
-
case "datetime":
|
|
25
|
-
return { type: "string", format: "date-time" };
|
|
26
|
-
case "uuid":
|
|
27
|
-
return { type: "string", format: "uuid" };
|
|
28
|
-
default: {
|
|
29
|
-
const target = byId.get(typeName);
|
|
30
|
-
if (target?.kind === "enum") {
|
|
31
|
-
return { type: "string", enum: target.values };
|
|
32
|
-
}
|
|
33
|
-
return { type: "string", "x-topogram-type": typeName };
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function coerceDefaultValue(value, schema) {
|
|
39
|
-
if (value == null) {
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (schema.type === "integer") {
|
|
44
|
-
const parsed = Number.parseInt(value, 10);
|
|
45
|
-
return Number.isNaN(parsed) ? value : parsed;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (schema.type === "number") {
|
|
49
|
-
const parsed = Number.parseFloat(value);
|
|
50
|
-
return Number.isNaN(parsed) ? value : parsed;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (schema.type === "boolean") {
|
|
54
|
-
if (value === "true") return true;
|
|
55
|
-
if (value === "false") return false;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return value;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function schemaForField(field, byId) {
|
|
62
|
-
const schema = scalarSchema(field.fieldType, byId);
|
|
63
|
-
const defaultValue = coerceDefaultValue(field.defaultValue, schema);
|
|
64
|
-
return defaultValue === undefined ? schema : { ...schema, default: defaultValue };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function generateShapeJsonSchema(shape, byId) {
|
|
68
|
-
const fields = shape.projectedFields || shape.fields || [];
|
|
69
|
-
const properties = {};
|
|
70
|
-
const required = [];
|
|
71
|
-
|
|
72
|
-
for (const field of fields) {
|
|
73
|
-
properties[field.name] = schemaForField(field, byId);
|
|
74
|
-
if (field.requiredness === "required") {
|
|
75
|
-
required.push(field.name);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const schema = {
|
|
80
|
-
$schema: JSON_SCHEMA_DRAFT,
|
|
81
|
-
$id: `topogram:shape:${shape.id}`,
|
|
82
|
-
title: shape.name || shape.id,
|
|
83
|
-
description: shape.description || undefined,
|
|
84
|
-
type: "object",
|
|
85
|
-
properties,
|
|
86
|
-
additionalProperties: false
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
if (required.length > 0) {
|
|
90
|
-
schema.required = required;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return schema;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function getCapability(graph, capabilityId) {
|
|
97
|
-
const byId = indexStatements(graph);
|
|
98
|
-
const capability = byId.get(capabilityId);
|
|
99
|
-
if (!capability || capability.kind !== "capability") {
|
|
100
|
-
throw new Error(`No capability found with id '${capabilityId}'`);
|
|
101
|
-
}
|
|
102
|
-
return capability;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function routeSegmentFromCapabilityId(id) {
|
|
106
|
-
return id.replace(/^cap_/, "").replace(/_/g, "-");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function methodFromCapability(capability) {
|
|
110
|
-
if (capability.creates.length > 0) return "POST";
|
|
111
|
-
if (capability.updates.length > 0) return "PATCH";
|
|
112
|
-
if (capability.deletes.length > 0) return "DELETE";
|
|
113
|
-
return "GET";
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function pathFromCapability(capability) {
|
|
117
|
-
const resourceRef =
|
|
118
|
-
capability.creates[0]?.target ||
|
|
119
|
-
capability.updates[0]?.target ||
|
|
120
|
-
capability.deletes[0]?.target ||
|
|
121
|
-
capability.reads[0]?.target;
|
|
122
|
-
|
|
123
|
-
const resourceSegment = resourceRef?.id ? resourceRef.id.replace(/^entity_/, "").replace(/_/g, "-") : "resource";
|
|
124
|
-
const opSegment = routeSegmentFromCapabilityId(capability.id);
|
|
125
|
-
|
|
126
|
-
if (capability.creates.length > 0) {
|
|
127
|
-
return `/${resourceSegment}`;
|
|
128
|
-
}
|
|
129
|
-
if (capability.updates.length > 0 || capability.deletes.length > 0) {
|
|
130
|
-
return `/${resourceSegment}/{id}`;
|
|
131
|
-
}
|
|
132
|
-
if (capability.id.startsWith("cap_list_")) {
|
|
133
|
-
return `/${resourceSegment}`;
|
|
134
|
-
}
|
|
135
|
-
return `/${resourceSegment}/${opSegment}`;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function normalizeResponseMetadata(responseEntry) {
|
|
139
|
-
return {
|
|
140
|
-
mode: responseEntry?.mode || null,
|
|
141
|
-
itemShapeId: responseEntry?.item?.id || null,
|
|
142
|
-
ordering: responseEntry?.sort
|
|
143
|
-
? {
|
|
144
|
-
field: responseEntry.sort.field,
|
|
145
|
-
direction: responseEntry.sort.direction
|
|
146
|
-
}
|
|
147
|
-
: null,
|
|
148
|
-
cursor: responseEntry?.cursor
|
|
149
|
-
? {
|
|
150
|
-
requestAfter: responseEntry.cursor.requestAfter,
|
|
151
|
-
responseNext: responseEntry.cursor.responseNext,
|
|
152
|
-
responsePrev: responseEntry.cursor.responsePrev || null
|
|
153
|
-
}
|
|
154
|
-
: null,
|
|
155
|
-
limit: responseEntry?.limit
|
|
156
|
-
? {
|
|
157
|
-
field: responseEntry.limit.field,
|
|
158
|
-
defaultValue: responseEntry.limit.defaultValue,
|
|
159
|
-
maxValue: responseEntry.limit.maxValue
|
|
160
|
-
}
|
|
161
|
-
: null,
|
|
162
|
-
total: responseEntry?.total
|
|
163
|
-
? {
|
|
164
|
-
included: responseEntry.total.included
|
|
165
|
-
}
|
|
166
|
-
: null
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function apiMetadataForCapability(graph, capability) {
|
|
171
|
-
const projections = graph.byKind.projection || [];
|
|
172
|
-
|
|
173
|
-
for (const projection of projections) {
|
|
174
|
-
const httpEntry = (projection.http || []).find((entry) => entry.capability?.id === capability.id);
|
|
175
|
-
if (!httpEntry) continue;
|
|
176
|
-
|
|
177
|
-
const responseEntry = (projection.httpResponses || []).find((entry) => entry.capability?.id === capability.id);
|
|
178
|
-
return {
|
|
179
|
-
projection: {
|
|
180
|
-
id: projection.id,
|
|
181
|
-
name: projection.name || projection.id
|
|
182
|
-
},
|
|
183
|
-
method: httpEntry.method || methodFromCapability(capability),
|
|
184
|
-
path: httpEntry.path || pathFromCapability(capability),
|
|
185
|
-
success: httpEntry.success || (capability.creates.length > 0 ? 201 : 200),
|
|
186
|
-
auth: httpEntry.auth || "none",
|
|
187
|
-
request: httpEntry.request || (capability.input.length > 0 ? "body" : "none"),
|
|
188
|
-
errorMappings: (projection.httpErrors || [])
|
|
189
|
-
.filter((entry) => entry.capability?.id === capability.id)
|
|
190
|
-
.map((entry) => ({
|
|
191
|
-
code: entry.code,
|
|
192
|
-
status: entry.status
|
|
193
|
-
})),
|
|
194
|
-
fieldBindings: (projection.httpFields || []).filter((entry) => entry.capability?.id === capability.id),
|
|
195
|
-
preconditions: (projection.httpPreconditions || [])
|
|
196
|
-
.filter((entry) => entry.capability?.id === capability.id)
|
|
197
|
-
.map((entry) => ({
|
|
198
|
-
header: entry.header,
|
|
199
|
-
required: entry.required,
|
|
200
|
-
error: entry.error,
|
|
201
|
-
source: entry.source,
|
|
202
|
-
code: entry.code
|
|
203
|
-
})),
|
|
204
|
-
idempotency: (projection.httpIdempotency || [])
|
|
205
|
-
.filter((entry) => entry.capability?.id === capability.id)
|
|
206
|
-
.map((entry) => ({
|
|
207
|
-
header: entry.header,
|
|
208
|
-
required: entry.required,
|
|
209
|
-
error: entry.error,
|
|
210
|
-
code: entry.code
|
|
211
|
-
})),
|
|
212
|
-
cache: (projection.httpCache || [])
|
|
213
|
-
.filter((entry) => entry.capability?.id === capability.id)
|
|
214
|
-
.map((entry) => ({
|
|
215
|
-
responseHeader: entry.responseHeader,
|
|
216
|
-
requestHeader: entry.requestHeader,
|
|
217
|
-
required: entry.required,
|
|
218
|
-
notModified: entry.notModified,
|
|
219
|
-
source: entry.source,
|
|
220
|
-
code: entry.code
|
|
221
|
-
})),
|
|
222
|
-
delete: (projection.httpDelete || [])
|
|
223
|
-
.filter((entry) => entry.capability?.id === capability.id)
|
|
224
|
-
.map((entry) => ({
|
|
225
|
-
mode: entry.mode,
|
|
226
|
-
field: entry.field,
|
|
227
|
-
value: entry.value,
|
|
228
|
-
response: entry.response
|
|
229
|
-
})),
|
|
230
|
-
async: (projection.httpAsync || [])
|
|
231
|
-
.filter((entry) => entry.capability?.id === capability.id)
|
|
232
|
-
.map((entry) => ({
|
|
233
|
-
mode: entry.mode,
|
|
234
|
-
accepted: entry.accepted,
|
|
235
|
-
locationHeader: entry.locationHeader,
|
|
236
|
-
retryAfterHeader: entry.retryAfterHeader,
|
|
237
|
-
statusPath: entry.statusPath,
|
|
238
|
-
statusCapability: entry.statusCapability,
|
|
239
|
-
job: entry.job
|
|
240
|
-
})),
|
|
241
|
-
status: (projection.httpStatus || [])
|
|
242
|
-
.filter((entry) => entry.capability?.id === capability.id)
|
|
243
|
-
.map((entry) => ({
|
|
244
|
-
asyncFor: entry.asyncFor,
|
|
245
|
-
stateField: entry.stateField,
|
|
246
|
-
completed: entry.completed,
|
|
247
|
-
failed: entry.failed,
|
|
248
|
-
expired: entry.expired,
|
|
249
|
-
downloadCapability: entry.downloadCapability,
|
|
250
|
-
downloadField: entry.downloadField,
|
|
251
|
-
errorField: entry.errorField
|
|
252
|
-
})),
|
|
253
|
-
download: (projection.httpDownload || [])
|
|
254
|
-
.filter((entry) => entry.capability?.id === capability.id)
|
|
255
|
-
.map((entry) => ({
|
|
256
|
-
asyncFor: entry.asyncFor,
|
|
257
|
-
media: entry.media,
|
|
258
|
-
filename: entry.filename,
|
|
259
|
-
disposition: entry.disposition
|
|
260
|
-
})),
|
|
261
|
-
authz: (projection.httpAuthz || [])
|
|
262
|
-
.filter((entry) => entry.capability?.id === capability.id)
|
|
263
|
-
.map((entry) => ({
|
|
264
|
-
role: entry.role,
|
|
265
|
-
permission: entry.permission,
|
|
266
|
-
claim: entry.claim,
|
|
267
|
-
claimValue: entry.claimValue,
|
|
268
|
-
ownership: entry.ownership,
|
|
269
|
-
ownershipField: entry.ownershipField
|
|
270
|
-
})),
|
|
271
|
-
callbacks: (projection.httpCallbacks || [])
|
|
272
|
-
.filter((entry) => entry.capability?.id === capability.id)
|
|
273
|
-
.map((entry) => ({
|
|
274
|
-
event: entry.event,
|
|
275
|
-
targetField: entry.targetField,
|
|
276
|
-
method: entry.method,
|
|
277
|
-
payload: entry.payload,
|
|
278
|
-
success: entry.success
|
|
279
|
-
})),
|
|
280
|
-
response: normalizeResponseMetadata(responseEntry)
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
return {
|
|
285
|
-
projection: null,
|
|
286
|
-
method: methodFromCapability(capability),
|
|
287
|
-
path: pathFromCapability(capability),
|
|
288
|
-
success: capability.creates.length > 0 ? 201 : 200,
|
|
289
|
-
auth: "none",
|
|
290
|
-
request: capability.input.length > 0 ? "body" : "none",
|
|
291
|
-
errorMappings: [],
|
|
292
|
-
fieldBindings: [],
|
|
293
|
-
preconditions: [],
|
|
294
|
-
idempotency: [],
|
|
295
|
-
cache: [],
|
|
296
|
-
delete: [],
|
|
297
|
-
async: [],
|
|
298
|
-
status: [],
|
|
299
|
-
download: [],
|
|
300
|
-
authz: [],
|
|
301
|
-
callbacks: [],
|
|
302
|
-
response: normalizeResponseMetadata(null)
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function fieldTransportBindings(contract, apiMetadata, direction) {
|
|
307
|
-
const bindings = (apiMetadata.fieldBindings || []).filter((binding) => binding.direction === direction);
|
|
308
|
-
const byField = new Map(bindings.map((binding) => [binding.field, binding]));
|
|
309
|
-
const inferredBindings = new Map();
|
|
310
|
-
|
|
311
|
-
if (direction === "input" && apiMetadata.response?.cursor?.requestAfter) {
|
|
312
|
-
inferredBindings.set(apiMetadata.response.cursor.requestAfter, { location: "query", wireName: "after" });
|
|
313
|
-
}
|
|
314
|
-
if (direction === "input" && apiMetadata.response?.limit?.field) {
|
|
315
|
-
inferredBindings.set(apiMetadata.response.limit.field, { location: "query", wireName: "limit" });
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return contract.fields.map((field) => {
|
|
319
|
-
const binding = byField.get(field.name) || byField.get(field.sourceName) || inferredBindings.get(field.name);
|
|
320
|
-
return {
|
|
321
|
-
...field,
|
|
322
|
-
transport: {
|
|
323
|
-
location: binding?.location || (direction === "input" ? apiMetadata.request : "body"),
|
|
324
|
-
wireName: binding?.wireName || field.name
|
|
325
|
-
}
|
|
326
|
-
};
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function splitFieldsByLocation(fields) {
|
|
331
|
-
return {
|
|
332
|
-
path: fields.filter((field) => field.transport.location === "path"),
|
|
333
|
-
query: fields.filter((field) => field.transport.location === "query"),
|
|
334
|
-
header: fields.filter((field) => field.transport.location === "header"),
|
|
335
|
-
body: fields.filter((field) => field.transport.location === "body")
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function fieldContract(field, byId) {
|
|
340
|
-
return {
|
|
341
|
-
name: field.name,
|
|
342
|
-
sourceName: field.sourceName ?? field.name,
|
|
343
|
-
required: field.requiredness === "required",
|
|
344
|
-
schema: schemaForField(field, byId)
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function contractFromShape(shape, byId, direction) {
|
|
349
|
-
const fields = (shape.projectedFields || shape.fields || []).map((field) => fieldContract(field, byId));
|
|
350
|
-
return {
|
|
351
|
-
type: direction === "request" ? "api_request_contract" : "api_response_contract",
|
|
352
|
-
shape: {
|
|
353
|
-
id: shape.id,
|
|
354
|
-
name: shape.name || shape.id
|
|
355
|
-
},
|
|
356
|
-
fields,
|
|
357
|
-
required: fields.filter((field) => field.required).map((field) => field.name),
|
|
358
|
-
jsonSchema: generateShapeJsonSchema(shape, byId)
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function isCollectionCapability(capability, apiMetadata) {
|
|
363
|
-
if (["collection", "paged", "cursor"].includes(apiMetadata?.response?.mode)) return true;
|
|
364
|
-
if (apiMetadata?.response?.mode === "item") return false;
|
|
365
|
-
if ((apiMetadata?.method || "").toUpperCase() !== "GET") return false;
|
|
366
|
-
return capability.id.startsWith("cap_list_");
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function policyConstraintsForCapability(graph, capability) {
|
|
370
|
-
const rules = graph.byKind.rule || [];
|
|
371
|
-
return rules
|
|
372
|
-
.filter((rule) => rule.appliesTo.some((target) => target.id === capability.id))
|
|
373
|
-
.map((rule) => ({
|
|
374
|
-
type: "api_policy_constraint",
|
|
375
|
-
rule: {
|
|
376
|
-
id: rule.id,
|
|
377
|
-
name: rule.name || rule.id
|
|
378
|
-
},
|
|
379
|
-
requirement: rule.requirementNode || null,
|
|
380
|
-
condition: rule.conditionNode || null,
|
|
381
|
-
severity: rule.severity
|
|
382
|
-
}));
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function apiErrorCasesForCapability(capability, policyConstraints, apiMetadata) {
|
|
386
|
-
const errors = [];
|
|
387
|
-
const overrideMap = new Map((apiMetadata.errorMappings || []).map((mapping) => [mapping.code, mapping.status]));
|
|
388
|
-
|
|
389
|
-
for (const policy of policyConstraints) {
|
|
390
|
-
errors.push({
|
|
391
|
-
type: "api_error_case",
|
|
392
|
-
code: policy.rule.id,
|
|
393
|
-
status: overrideMap.get(policy.rule.id) || (policy.severity === "error" ? 400 : 422),
|
|
394
|
-
source: "policy"
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (capability.input.length > 0) {
|
|
399
|
-
errors.push({
|
|
400
|
-
type: "api_error_case",
|
|
401
|
-
code: `${capability.id}_invalid_request`,
|
|
402
|
-
status: overrideMap.get(`${capability.id}_invalid_request`) || 400,
|
|
403
|
-
source: "request_contract"
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
if (apiMetadata.response?.mode === "cursor") {
|
|
408
|
-
errors.push({
|
|
409
|
-
type: "api_error_case",
|
|
410
|
-
code: `${capability.id}_invalid_cursor`,
|
|
411
|
-
status: overrideMap.get(`${capability.id}_invalid_cursor`) || 400,
|
|
412
|
-
source: "cursor_contract"
|
|
413
|
-
});
|
|
414
|
-
errors.push({
|
|
415
|
-
type: "api_error_case",
|
|
416
|
-
code: `${capability.id}_invalid_limit`,
|
|
417
|
-
status: overrideMap.get(`${capability.id}_invalid_limit`) || 400,
|
|
418
|
-
source: "cursor_contract"
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
for (const precondition of apiMetadata.preconditions || []) {
|
|
423
|
-
errors.push({
|
|
424
|
-
type: "api_error_case",
|
|
425
|
-
code: precondition.code,
|
|
426
|
-
status: precondition.error || 412,
|
|
427
|
-
source: "precondition"
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
for (const idempotency of apiMetadata.idempotency || []) {
|
|
432
|
-
errors.push({
|
|
433
|
-
type: "api_error_case",
|
|
434
|
-
code: idempotency.code,
|
|
435
|
-
status: idempotency.error || 409,
|
|
436
|
-
source: "idempotency"
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const seenCodes = new Set(errors.map((error) => error.code));
|
|
441
|
-
for (const mapping of apiMetadata.errorMappings || []) {
|
|
442
|
-
if (seenCodes.has(mapping.code)) continue;
|
|
443
|
-
errors.push({
|
|
444
|
-
type: "api_error_case",
|
|
445
|
-
code: mapping.code,
|
|
446
|
-
status: mapping.status,
|
|
447
|
-
source: "projection_mapping"
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
return errors;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function cloneSchema(value) {
|
|
455
|
-
return JSON.parse(JSON.stringify(value));
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function responseContractForCapability(shape, byId, capability, apiMetadata) {
|
|
459
|
-
const baseContract = contractFromShape(shape, byId, "response");
|
|
460
|
-
const responseMode = apiMetadata?.response?.mode || (isCollectionCapability(capability, apiMetadata) ? "collection" : "item");
|
|
461
|
-
|
|
462
|
-
if (responseMode === "item") {
|
|
463
|
-
return { ...baseContract, mode: "item", collection: false, itemJsonSchema: null, pagination: null };
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
if (responseMode === "paged") {
|
|
467
|
-
return {
|
|
468
|
-
...baseContract,
|
|
469
|
-
mode: "paged",
|
|
470
|
-
collection: true,
|
|
471
|
-
itemJsonSchema: baseContract.jsonSchema,
|
|
472
|
-
pagination: {
|
|
473
|
-
itemsProperty: "items",
|
|
474
|
-
pageProperty: "page",
|
|
475
|
-
pageSizeProperty: "page_size",
|
|
476
|
-
totalProperty: "total"
|
|
477
|
-
},
|
|
478
|
-
jsonSchema: {
|
|
479
|
-
type: "object",
|
|
480
|
-
additionalProperties: false,
|
|
481
|
-
required: ["items", "page", "page_size", "total"],
|
|
482
|
-
properties: {
|
|
483
|
-
items: { type: "array", items: cloneSchema(baseContract.jsonSchema) },
|
|
484
|
-
page: { type: "integer" },
|
|
485
|
-
page_size: { type: "integer" },
|
|
486
|
-
total: { type: "integer" }
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
if (responseMode === "cursor") {
|
|
493
|
-
const totalIncluded = apiMetadata.response.total?.included === true;
|
|
494
|
-
return {
|
|
495
|
-
...baseContract,
|
|
496
|
-
mode: "cursor",
|
|
497
|
-
collection: true,
|
|
498
|
-
itemJsonSchema: baseContract.jsonSchema,
|
|
499
|
-
pagination: null,
|
|
500
|
-
itemShape: { id: shape.id, name: shape.name || shape.id },
|
|
501
|
-
ordering: apiMetadata.response.ordering,
|
|
502
|
-
cursor: apiMetadata.response.cursor,
|
|
503
|
-
limit: apiMetadata.response.limit,
|
|
504
|
-
total: apiMetadata.response.total,
|
|
505
|
-
jsonSchema: {
|
|
506
|
-
type: "object",
|
|
507
|
-
additionalProperties: false,
|
|
508
|
-
required: ["items", apiMetadata.response.cursor?.responseNext || "next_cursor"],
|
|
509
|
-
properties: {
|
|
510
|
-
items: { type: "array", items: cloneSchema(baseContract.jsonSchema) },
|
|
511
|
-
[apiMetadata.response.cursor?.responseNext || "next_cursor"]: { type: "string" },
|
|
512
|
-
...(apiMetadata.response.cursor?.responsePrev
|
|
513
|
-
? { [apiMetadata.response.cursor.responsePrev]: { type: "string" } }
|
|
514
|
-
: {}),
|
|
515
|
-
...(totalIncluded ? { total: { type: "integer" } } : {})
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
return {
|
|
522
|
-
...baseContract,
|
|
523
|
-
mode: "collection",
|
|
524
|
-
collection: true,
|
|
525
|
-
itemJsonSchema: baseContract.jsonSchema,
|
|
526
|
-
pagination: null,
|
|
527
|
-
jsonSchema: { type: "array", items: cloneSchema(baseContract.jsonSchema) }
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function buildApiContractForCapability(graph, capability, byId) {
|
|
532
|
-
const inputShape = capability.input[0]?.id ? byId.get(capability.input[0].id) : null;
|
|
533
|
-
const policyConstraints = policyConstraintsForCapability(graph, capability);
|
|
534
|
-
const apiMetadata = apiMetadataForCapability(graph, capability);
|
|
535
|
-
const outputShapeId = apiMetadata.response.itemShapeId || capability.output[0]?.id || null;
|
|
536
|
-
const outputShape = outputShapeId ? byId.get(outputShapeId) : null;
|
|
537
|
-
const requestContract = inputShape ? contractFromShape(inputShape, byId, "request") : null;
|
|
538
|
-
const responseContract = outputShape ? responseContractForCapability(outputShape, byId, capability, apiMetadata) : null;
|
|
539
|
-
const requestFields = requestContract ? fieldTransportBindings(requestContract, apiMetadata, "input") : [];
|
|
540
|
-
const responseFields = responseContract ? fieldTransportBindings(responseContract, apiMetadata, "output") : [];
|
|
541
|
-
|
|
542
|
-
return {
|
|
543
|
-
type: "api_contract_graph",
|
|
544
|
-
capability: {
|
|
545
|
-
id: capability.id,
|
|
546
|
-
name: capability.name || capability.id,
|
|
547
|
-
description: capability.description || null
|
|
548
|
-
},
|
|
549
|
-
endpoint: {
|
|
550
|
-
type: "api_endpoint",
|
|
551
|
-
operationId: capability.id,
|
|
552
|
-
method: apiMetadata.method,
|
|
553
|
-
path: apiMetadata.path,
|
|
554
|
-
successStatus: apiMetadata.success,
|
|
555
|
-
auth: apiMetadata.auth,
|
|
556
|
-
requestPlacement: apiMetadata.request,
|
|
557
|
-
projection: apiMetadata.projection,
|
|
558
|
-
preconditions: apiMetadata.preconditions || [],
|
|
559
|
-
idempotency: apiMetadata.idempotency || [],
|
|
560
|
-
cache: apiMetadata.cache || [],
|
|
561
|
-
delete: apiMetadata.delete || [],
|
|
562
|
-
async: apiMetadata.async || [],
|
|
563
|
-
status: apiMetadata.status || [],
|
|
564
|
-
download: apiMetadata.download || [],
|
|
565
|
-
authz: apiMetadata.authz || [],
|
|
566
|
-
callbacks: apiMetadata.callbacks || [],
|
|
567
|
-
actors: capability.actors.map((actor, index) => ({
|
|
568
|
-
type: "api_actor",
|
|
569
|
-
id: actor.id,
|
|
570
|
-
kind: actor.target?.kind || null,
|
|
571
|
-
order: index
|
|
572
|
-
}))
|
|
573
|
-
},
|
|
574
|
-
requestContract: requestContract
|
|
575
|
-
? { ...requestContract, fields: requestFields, transport: splitFieldsByLocation(requestFields) }
|
|
576
|
-
: null,
|
|
577
|
-
responseContract: responseContract
|
|
578
|
-
? { ...responseContract, fields: responseFields, transport: splitFieldsByLocation(responseFields) }
|
|
579
|
-
: null,
|
|
580
|
-
policy: policyConstraints,
|
|
581
|
-
errors: apiErrorCasesForCapability(capability, policyConstraints, apiMetadata)
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
export function generateApiContractGraph(graph, options = {}) {
|
|
586
|
-
const byId = indexStatements(graph);
|
|
587
|
-
const capabilities = graph.byKind.capability || [];
|
|
588
|
-
|
|
589
|
-
if (options.capabilityId) {
|
|
590
|
-
return buildApiContractForCapability(graph, getCapability(graph, options.capabilityId), byId);
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
const output = {};
|
|
594
|
-
for (const capability of capabilities) {
|
|
595
|
-
output[capability.id] = buildApiContractForCapability(graph, capability, byId);
|
|
596
|
-
}
|
|
597
|
-
return output;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
export function generateApiContractDebug(graph, options = {}) {
|
|
601
|
-
const capabilities = options.capabilityId ? [getCapability(graph, options.capabilityId)] : graph.byKind.capability || [];
|
|
602
|
-
const byId = indexStatements(graph);
|
|
603
|
-
const lines = [];
|
|
604
|
-
|
|
605
|
-
lines.push("# API Contract Debug");
|
|
606
|
-
lines.push("");
|
|
607
|
-
lines.push(`Generated from \`${graph.root}\``);
|
|
608
|
-
lines.push("");
|
|
609
|
-
|
|
610
|
-
for (const capability of capabilities) {
|
|
611
|
-
const contract = buildApiContractForCapability(graph, capability, byId);
|
|
612
|
-
lines.push(`## \`${capability.id}\` - ${capability.name || capability.id}`);
|
|
613
|
-
lines.push("");
|
|
614
|
-
if (capability.description) {
|
|
615
|
-
lines.push(capability.description);
|
|
616
|
-
lines.push("");
|
|
617
|
-
}
|
|
618
|
-
lines.push(`Endpoint: \`${contract.endpoint.method} ${contract.endpoint.path}\``);
|
|
619
|
-
lines.push(`Success: \`${contract.endpoint.successStatus}\``);
|
|
620
|
-
lines.push(`Auth: \`${contract.endpoint.auth}\``);
|
|
621
|
-
lines.push(`Request placement: \`${contract.endpoint.requestPlacement}\``);
|
|
622
|
-
if (contract.endpoint.projection?.id) {
|
|
623
|
-
lines.push(`Projection: \`${contract.endpoint.projection.id}\``);
|
|
624
|
-
}
|
|
625
|
-
if (contract.endpoint.preconditions?.length > 0) {
|
|
626
|
-
lines.push(`Preconditions: ${contract.endpoint.preconditions.map((precondition) => `\`${precondition.header}\``).join(", ")}`);
|
|
627
|
-
}
|
|
628
|
-
if (contract.endpoint.idempotency?.length > 0) {
|
|
629
|
-
lines.push(`Idempotency: ${contract.endpoint.idempotency.map((rule) => `\`${rule.header}\``).join(", ")}`);
|
|
630
|
-
}
|
|
631
|
-
if (contract.endpoint.cache?.length > 0) {
|
|
632
|
-
lines.push(`Cache: ${contract.endpoint.cache.map((rule) => `\`${rule.responseHeader}\` via \`${rule.requestHeader}\` -> ${rule.notModified}`).join(", ")}`);
|
|
633
|
-
}
|
|
634
|
-
if (contract.endpoint.delete?.length > 0) {
|
|
635
|
-
lines.push(`Delete: ${contract.endpoint.delete.map((rule) => `\`${rule.mode}\`${rule.field ? ` via \`${rule.field}=${rule.value}\`` : ""} response \`${rule.response}\``).join(", ")}`);
|
|
636
|
-
}
|
|
637
|
-
if (contract.endpoint.async?.length > 0) {
|
|
638
|
-
lines.push(`Async: ${contract.endpoint.async.map((rule) => `\`${rule.mode}\` accepted ${rule.accepted} status \`${rule.statusPath}\`${rule.statusCapability?.id ? ` via \`${rule.statusCapability.id}\`` : ""}`).join(", ")}`);
|
|
639
|
-
}
|
|
640
|
-
if (contract.endpoint.status?.length > 0) {
|
|
641
|
-
lines.push(`Status: ${contract.endpoint.status.map((rule) => `state \`${rule.stateField}\` complete \`${rule.completed}\` fail \`${rule.failed}\`${rule.downloadCapability?.id ? ` download \`${rule.downloadCapability.id}\`` : ""}`).join(", ")}`);
|
|
642
|
-
}
|
|
643
|
-
if (contract.endpoint.download?.length > 0) {
|
|
644
|
-
lines.push(`Download: ${contract.endpoint.download.map((rule) => `\`${rule.media}\` ${rule.disposition}${rule.filename ? ` filename \`${rule.filename}\`` : ""}`).join(", ")}`);
|
|
645
|
-
}
|
|
646
|
-
if (contract.endpoint.authz?.length > 0) {
|
|
647
|
-
lines.push(`Authorization: ${contract.endpoint.authz.map((rule) => [
|
|
648
|
-
rule.role ? `role \`${rule.role}\`` : null,
|
|
649
|
-
rule.permission ? `permission \`${rule.permission}\`` : null,
|
|
650
|
-
rule.claim ? `claim \`${rule.claim}\`${rule.claimValue ? ` = \`${rule.claimValue}\`` : ""}` : null,
|
|
651
|
-
rule.ownership ? `ownership \`${rule.ownership}\`` : null
|
|
652
|
-
].filter(Boolean).join(", ")).join(" | ")}`);
|
|
653
|
-
}
|
|
654
|
-
if (contract.endpoint.callbacks?.length > 0) {
|
|
655
|
-
lines.push(`Callbacks: ${contract.endpoint.callbacks.map((rule) => `\`${rule.event}\` -> \`${rule.method}\` via \`${rule.targetField}\``).join(", ")}`);
|
|
656
|
-
}
|
|
657
|
-
lines.push(`Actors: ${symbolList(capability.actors)}`);
|
|
658
|
-
lines.push("");
|
|
659
|
-
|
|
660
|
-
lines.push("Request contract:");
|
|
661
|
-
if (!contract.requestContract) {
|
|
662
|
-
lines.push("- _none_");
|
|
663
|
-
} else {
|
|
664
|
-
lines.push(`- shape: \`${contract.requestContract.shape.id}\``);
|
|
665
|
-
for (const field of contract.requestContract.fields) {
|
|
666
|
-
lines.push(`- ${fieldSignature({
|
|
667
|
-
name: field.transport?.wireName || field.name,
|
|
668
|
-
sourceName: field.sourceName,
|
|
669
|
-
fieldType: field.schema["x-topogram-type"] || field.schema.type || "unknown",
|
|
670
|
-
requiredness: field.required ? "required" : "optional",
|
|
671
|
-
defaultValue: field.schema.default ?? null
|
|
672
|
-
})} in \`${field.transport?.location}\``);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
lines.push("");
|
|
676
|
-
|
|
677
|
-
lines.push("Response contract:");
|
|
678
|
-
if (!contract.responseContract) {
|
|
679
|
-
lines.push("- _none_");
|
|
680
|
-
} else {
|
|
681
|
-
lines.push(`- shape: \`${contract.responseContract.shape.id}\``);
|
|
682
|
-
lines.push(`- mode: \`${contract.responseContract.mode}\``);
|
|
683
|
-
if (contract.responseContract.pagination) {
|
|
684
|
-
lines.push(`- envelope: \`${contract.responseContract.pagination.itemsProperty}\`, \`${contract.responseContract.pagination.pageProperty}\`, \`${contract.responseContract.pagination.pageSizeProperty}\`, \`${contract.responseContract.pagination.totalProperty}\``);
|
|
685
|
-
}
|
|
686
|
-
if (contract.responseContract.cursor) {
|
|
687
|
-
lines.push(`- cursor: request \`${contract.responseContract.cursor.requestAfter}\`, next \`${contract.responseContract.cursor.responseNext}\`${contract.responseContract.cursor.responsePrev ? `, prev \`${contract.responseContract.cursor.responsePrev}\`` : ""}`);
|
|
688
|
-
}
|
|
689
|
-
if (contract.responseContract.limit) {
|
|
690
|
-
lines.push(`- limit: field \`${contract.responseContract.limit.field}\`, default ${contract.responseContract.limit.defaultValue}, max ${contract.responseContract.limit.maxValue}`);
|
|
691
|
-
}
|
|
692
|
-
if (contract.responseContract.ordering) {
|
|
693
|
-
lines.push(`- sort: \`${contract.responseContract.ordering.field} ${contract.responseContract.ordering.direction}\``);
|
|
694
|
-
}
|
|
695
|
-
if (contract.responseContract.total) {
|
|
696
|
-
lines.push(`- total included: \`${contract.responseContract.total.included}\``);
|
|
697
|
-
}
|
|
698
|
-
for (const field of contract.responseContract.fields) {
|
|
699
|
-
lines.push(`- ${fieldSignature({
|
|
700
|
-
name: field.transport?.wireName || field.name,
|
|
701
|
-
sourceName: field.sourceName,
|
|
702
|
-
fieldType: field.schema["x-topogram-type"] || field.schema.type || "unknown",
|
|
703
|
-
requiredness: field.required ? "required" : "optional",
|
|
704
|
-
defaultValue: field.schema.default ?? null
|
|
705
|
-
})} in \`${field.transport?.location}\``);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
lines.push("");
|
|
709
|
-
|
|
710
|
-
lines.push("Policy constraints:");
|
|
711
|
-
if (contract.policy.length === 0) {
|
|
712
|
-
lines.push("- _none_");
|
|
713
|
-
} else {
|
|
714
|
-
for (const policy of contract.policy) {
|
|
715
|
-
lines.push(`- \`${policy.rule.id}\` (${policy.severity})`);
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
lines.push("");
|
|
719
|
-
|
|
720
|
-
lines.push("Error cases:");
|
|
721
|
-
for (const error of contract.errors) {
|
|
722
|
-
lines.push(`- \`${error.code}\` -> ${error.status}`);
|
|
723
|
-
}
|
|
724
|
-
lines.push("");
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
return `${lines.join("\n").trimEnd()}\n`;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
function componentNameFromShape(shapeId, suffix) {
|
|
731
|
-
const base = shapeId.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
732
|
-
return `${base}${suffix}`;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
function componentNameFromErrorCode(code, suffix = "Error") {
|
|
736
|
-
const base = code
|
|
737
|
-
.split(/[^A-Za-z0-9]+/)
|
|
738
|
-
.filter(Boolean)
|
|
739
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
740
|
-
.join("");
|
|
741
|
-
return `${base}${suffix}`;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
function refSchema(name) {
|
|
745
|
-
return { $ref: `#/components/schemas/${name}` };
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
function openApiParameter(field, location) {
|
|
749
|
-
return {
|
|
750
|
-
name: field.transport?.wireName || field.name,
|
|
751
|
-
in: location,
|
|
752
|
-
required: location === "path" ? true : field.required,
|
|
753
|
-
schema: cloneSchema(field.schema)
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
function requestBodySchema(contract) {
|
|
758
|
-
const requestFields = contract.requestContract?.transport?.body || [];
|
|
759
|
-
const schema = { type: "object", properties: {}, additionalProperties: false };
|
|
760
|
-
const required = [];
|
|
761
|
-
|
|
762
|
-
for (const field of requestFields) {
|
|
763
|
-
schema.properties[field.transport?.wireName || field.name] = cloneSchema(field.schema);
|
|
764
|
-
if (field.required) {
|
|
765
|
-
required.push(field.transport?.wireName || field.name);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
if (required.length > 0) {
|
|
770
|
-
schema.required = required;
|
|
771
|
-
}
|
|
772
|
-
return schema;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
function responseSchemaForContract(contract, componentNames) {
|
|
776
|
-
if (!contract.responseContract) return null;
|
|
777
|
-
|
|
778
|
-
if (contract.responseContract.mode === "paged" || contract.responseContract.mode === "cursor") {
|
|
779
|
-
const envelopeSchemaName = componentNames.responseEnvelope.get(contract.capability.id);
|
|
780
|
-
if (envelopeSchemaName) return refSchema(envelopeSchemaName);
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
const responseSchemaName = componentNames.response.get(contract.responseContract.shape.id);
|
|
784
|
-
if (!responseSchemaName) return cloneSchema(contract.responseContract.jsonSchema);
|
|
785
|
-
|
|
786
|
-
if (contract.responseContract.collection) {
|
|
787
|
-
return { type: "array", items: refSchema(responseSchemaName) };
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
return refSchema(responseSchemaName);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
function successHeadersForContract(contract) {
|
|
794
|
-
const headerEntries = [
|
|
795
|
-
...(contract.endpoint.cache || []).map((rule) => [rule.responseHeader, { schema: { type: "string" } }]),
|
|
796
|
-
...(contract.endpoint.async || []).flatMap((rule) => [
|
|
797
|
-
[rule.locationHeader, { schema: { type: "string" } }],
|
|
798
|
-
[rule.retryAfterHeader, { schema: { type: "integer" } }]
|
|
799
|
-
]),
|
|
800
|
-
...(contract.endpoint.download || []).flatMap((rule) =>
|
|
801
|
-
rule.disposition ? [["Content-Disposition", { schema: { type: "string" } }]] : []
|
|
802
|
-
)
|
|
803
|
-
].filter(([name]) => Boolean(name));
|
|
804
|
-
|
|
805
|
-
return headerEntries.length > 0 ? Object.fromEntries(headerEntries) : undefined;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
function asyncLinksForContract(contract) {
|
|
809
|
-
const links = {};
|
|
810
|
-
for (const rule of contract.endpoint.async || []) {
|
|
811
|
-
if (!rule.statusCapability?.id) continue;
|
|
812
|
-
links[`${rule.statusCapability.id}Status`] = {
|
|
813
|
-
operationId: rule.statusCapability.id,
|
|
814
|
-
parameters: { job_id: "$response.body#/job_id" },
|
|
815
|
-
description: `Follow ${rule.statusCapability.id} to monitor the async job`
|
|
816
|
-
};
|
|
817
|
-
}
|
|
818
|
-
return Object.keys(links).length > 0 ? links : undefined;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
function statusLinksForContract(contract) {
|
|
822
|
-
const links = {};
|
|
823
|
-
for (const rule of contract.endpoint.status || []) {
|
|
824
|
-
if (!rule.downloadCapability?.id) continue;
|
|
825
|
-
links[`${rule.downloadCapability.id}Download`] = {
|
|
826
|
-
operationId: rule.downloadCapability.id,
|
|
827
|
-
parameters: { job_id: "$response.body#/job_id" },
|
|
828
|
-
description: `Use ${rule.downloadCapability.id} when the job is complete`
|
|
829
|
-
};
|
|
830
|
-
}
|
|
831
|
-
return Object.keys(links).length > 0 ? links : undefined;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
function authorizationExtension(contract) {
|
|
835
|
-
if (!contract.endpoint.authz || contract.endpoint.authz.length === 0) return undefined;
|
|
836
|
-
return contract.endpoint.authz.map((rule) => ({
|
|
837
|
-
...(rule.role ? { role: rule.role } : {}),
|
|
838
|
-
...(rule.permission ? { permission: rule.permission } : {}),
|
|
839
|
-
...(rule.claim ? { claim: rule.claim } : {}),
|
|
840
|
-
...(rule.claimValue ? { claimValue: rule.claimValue } : {}),
|
|
841
|
-
...(rule.ownership ? { ownership: rule.ownership } : {}),
|
|
842
|
-
...(rule.ownershipField ? { ownershipField: rule.ownershipField } : {})
|
|
843
|
-
}));
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
function callbackExpressionForField(contract, callback) {
|
|
847
|
-
const field = contract.requestContract?.fields?.find(
|
|
848
|
-
(item) => item.name === callback.targetField || item.sourceName === callback.targetField
|
|
849
|
-
);
|
|
850
|
-
const location = field?.transport?.location || contract.endpoint.requestPlacement;
|
|
851
|
-
const wireName = field?.transport?.wireName || callback.targetField;
|
|
852
|
-
|
|
853
|
-
if (location === "query") return `{$request.query.${wireName}}`;
|
|
854
|
-
if (location === "header") return `{$request.header.${wireName}}`;
|
|
855
|
-
return `{$request.body#/${wireName}}`;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
function callbackRequestBodySchema(callback, componentNames) {
|
|
859
|
-
const payloadShapeId = callback.payload?.id || null;
|
|
860
|
-
if (!payloadShapeId) return { type: "object" };
|
|
861
|
-
const schemaName = componentNames.response.get(payloadShapeId);
|
|
862
|
-
return schemaName ? refSchema(schemaName) : { type: "object" };
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
function callbacksObjectForContract(contract, componentNames) {
|
|
866
|
-
if (!contract.endpoint.callbacks || contract.endpoint.callbacks.length === 0) return undefined;
|
|
867
|
-
const callbacks = {};
|
|
868
|
-
for (const callback of contract.endpoint.callbacks) {
|
|
869
|
-
callbacks[callback.event] = {
|
|
870
|
-
[callbackExpressionForField(contract, callback)]: {
|
|
871
|
-
[callback.method.toLowerCase()]: {
|
|
872
|
-
requestBody: {
|
|
873
|
-
required: true,
|
|
874
|
-
content: {
|
|
875
|
-
"application/json": {
|
|
876
|
-
schema: callbackRequestBodySchema(callback, componentNames)
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
},
|
|
880
|
-
responses: {
|
|
881
|
-
[String(callback.success || 202)]: { description: "Callback accepted" }
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
};
|
|
886
|
-
}
|
|
887
|
-
return callbacks;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
function operationFromContract(contract, componentNames) {
|
|
891
|
-
const operation = {
|
|
892
|
-
operationId: contract.endpoint.operationId,
|
|
893
|
-
summary: contract.capability.name,
|
|
894
|
-
responses: {}
|
|
895
|
-
};
|
|
896
|
-
|
|
897
|
-
if (contract.capability.description) {
|
|
898
|
-
operation.description = contract.capability.description;
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
const authzExtension = authorizationExtension(contract);
|
|
902
|
-
if (authzExtension) {
|
|
903
|
-
operation["x-topogram-authorization"] = authzExtension;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
if (contract.endpoint.auth && contract.endpoint.auth !== "none") {
|
|
907
|
-
operation.security = [{ bearerAuth: [] }];
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
const requestFields = contract.requestContract?.transport || { path: [], query: [], header: [], body: [] };
|
|
911
|
-
const parameters = [
|
|
912
|
-
...requestFields.path.map((field) => openApiParameter(field, "path")),
|
|
913
|
-
...requestFields.query.map((field) => openApiParameter(field, "query")),
|
|
914
|
-
...requestFields.header.map((field) => openApiParameter(field, "header")),
|
|
915
|
-
...(contract.endpoint.preconditions || []).map((precondition) => ({
|
|
916
|
-
name: precondition.header,
|
|
917
|
-
in: "header",
|
|
918
|
-
required: precondition.required,
|
|
919
|
-
schema: { type: "string" }
|
|
920
|
-
})),
|
|
921
|
-
...(contract.endpoint.idempotency || []).map((rule) => ({
|
|
922
|
-
name: rule.header,
|
|
923
|
-
in: "header",
|
|
924
|
-
required: rule.required,
|
|
925
|
-
schema: { type: "string" }
|
|
926
|
-
})),
|
|
927
|
-
...(contract.endpoint.cache || []).map((rule) => ({
|
|
928
|
-
name: rule.requestHeader,
|
|
929
|
-
in: "header",
|
|
930
|
-
required: rule.required,
|
|
931
|
-
schema: { type: "string" }
|
|
932
|
-
}))
|
|
933
|
-
];
|
|
934
|
-
if (parameters.length > 0) operation.parameters = parameters;
|
|
935
|
-
|
|
936
|
-
const callbacks = callbacksObjectForContract(contract, componentNames);
|
|
937
|
-
if (callbacks) {
|
|
938
|
-
operation.callbacks = callbacks;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
if (requestFields.body.length > 0 && contract.requestContract) {
|
|
942
|
-
const requestSchemaName = componentNames.request.get(contract.capability.id);
|
|
943
|
-
operation.requestBody = {
|
|
944
|
-
required: requestFields.body.some((field) => field.required),
|
|
945
|
-
content: {
|
|
946
|
-
"application/json": {
|
|
947
|
-
schema: requestSchemaName ? refSchema(requestSchemaName) : { type: "object" }
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
if (contract.endpoint.download?.length > 0) {
|
|
954
|
-
const rule = contract.endpoint.download[0];
|
|
955
|
-
operation.responses[String(contract.endpoint.successStatus)] = {
|
|
956
|
-
description: "Success",
|
|
957
|
-
headers: successHeadersForContract(contract),
|
|
958
|
-
content: {
|
|
959
|
-
[rule.media || "application/octet-stream"]: {
|
|
960
|
-
schema: { type: "string", format: "binary" }
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
};
|
|
964
|
-
} else if (contract.responseContract) {
|
|
965
|
-
operation.responses[String(contract.endpoint.successStatus)] = {
|
|
966
|
-
description: contract.endpoint.async?.length > 0 ? "Accepted" : "Success",
|
|
967
|
-
headers: successHeadersForContract(contract),
|
|
968
|
-
content: {
|
|
969
|
-
"application/json": {
|
|
970
|
-
schema: responseSchemaForContract(contract, componentNames) || { type: "object" }
|
|
971
|
-
}
|
|
972
|
-
},
|
|
973
|
-
...(contract.endpoint.async?.length > 0 ? { links: asyncLinksForContract(contract) } : {}),
|
|
974
|
-
...(contract.endpoint.status?.length > 0 ? { links: statusLinksForContract(contract) } : {})
|
|
975
|
-
};
|
|
976
|
-
} else {
|
|
977
|
-
operation.responses[String(contract.endpoint.successStatus)] = {
|
|
978
|
-
description: contract.endpoint.async?.length > 0 ? "Accepted" : "Success",
|
|
979
|
-
headers: successHeadersForContract(contract)
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
for (const cacheRule of contract.endpoint.cache || []) {
|
|
984
|
-
const status = String(cacheRule.notModified || 304);
|
|
985
|
-
operation.responses[status] = {
|
|
986
|
-
description: "Not Modified",
|
|
987
|
-
headers: {
|
|
988
|
-
[cacheRule.responseHeader]: { schema: { type: "string" } }
|
|
989
|
-
}
|
|
990
|
-
};
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
for (const [status, errors] of groupedErrorsByStatus(contract.errors)) {
|
|
994
|
-
if (operation.responses[status]) continue;
|
|
995
|
-
if (errors.length === 1) {
|
|
996
|
-
operation.responses[status] = { $ref: `#/components/responses/${errorResponseComponentName(errors[0])}` };
|
|
997
|
-
continue;
|
|
998
|
-
}
|
|
999
|
-
operation.responses[status] = {
|
|
1000
|
-
description: `Error (${status})`,
|
|
1001
|
-
content: {
|
|
1002
|
-
"application/json": {
|
|
1003
|
-
schema: { oneOf: errors.map((error) => refSchema(errorSchemaComponentName(error))) }
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
};
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
return operation;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
function errorResponseComponentName(error) {
|
|
1013
|
-
return `${componentNameFromErrorCode(error.code)}Response`;
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
function errorSchemaComponentName(error) {
|
|
1017
|
-
return componentNameFromErrorCode(error.code);
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
function titleFromIdentifier(id) {
|
|
1021
|
-
return id.replace(/[_-]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
function errorResponseDescription(error) {
|
|
1025
|
-
return `${titleFromIdentifier(error.code)} (${error.status})`;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
function groupedErrorsByStatus(errors) {
|
|
1029
|
-
const grouped = new Map();
|
|
1030
|
-
for (const error of errors) {
|
|
1031
|
-
const status = String(error.status);
|
|
1032
|
-
if (!grouped.has(status)) grouped.set(status, []);
|
|
1033
|
-
grouped.get(status).push(error);
|
|
1034
|
-
}
|
|
1035
|
-
return grouped;
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
export function generateOpenApi(graph, options = {}) {
|
|
1039
|
-
const contractGraph = generateApiContractGraph(graph, options);
|
|
1040
|
-
const contracts = options.capabilityId ? [contractGraph] : Object.values(contractGraph);
|
|
1041
|
-
const byId = indexStatements(graph);
|
|
1042
|
-
const document = {
|
|
1043
|
-
openapi: "3.1.0",
|
|
1044
|
-
info: {
|
|
1045
|
-
title: "Topogram API",
|
|
1046
|
-
version: "0.1.0"
|
|
1047
|
-
},
|
|
1048
|
-
paths: {},
|
|
1049
|
-
components: {
|
|
1050
|
-
schemas: {
|
|
1051
|
-
ErrorResponse: {
|
|
1052
|
-
type: "object",
|
|
1053
|
-
additionalProperties: false,
|
|
1054
|
-
required: ["code", "message"],
|
|
1055
|
-
properties: {
|
|
1056
|
-
code: { type: "string" },
|
|
1057
|
-
message: { type: "string" },
|
|
1058
|
-
details: { type: "object", additionalProperties: true }
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
},
|
|
1062
|
-
responses: {},
|
|
1063
|
-
securitySchemes: {
|
|
1064
|
-
bearerAuth: { type: "http", scheme: "bearer" }
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
};
|
|
1068
|
-
|
|
1069
|
-
const componentNames = {
|
|
1070
|
-
request: new Map(),
|
|
1071
|
-
response: new Map(),
|
|
1072
|
-
responseEnvelope: new Map()
|
|
1073
|
-
};
|
|
1074
|
-
|
|
1075
|
-
for (const contract of contracts) {
|
|
1076
|
-
if (contract.requestContract && contract.requestContract.transport.body.length > 0) {
|
|
1077
|
-
const requestSchemaName = componentNameFromShape(contract.requestContract.shape.id, "Request");
|
|
1078
|
-
componentNames.request.set(contract.capability.id, requestSchemaName);
|
|
1079
|
-
document.components.schemas[requestSchemaName] = requestBodySchema(contract);
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
if (contract.responseContract) {
|
|
1083
|
-
const responseSchemaName = componentNameFromShape(contract.responseContract.shape.id, "Response");
|
|
1084
|
-
componentNames.response.set(contract.responseContract.shape.id, responseSchemaName);
|
|
1085
|
-
if (!document.components.schemas[responseSchemaName]) {
|
|
1086
|
-
document.components.schemas[responseSchemaName] = cloneSchema(
|
|
1087
|
-
contract.responseContract.collection ? contract.responseContract.itemJsonSchema : contract.responseContract.jsonSchema
|
|
1088
|
-
);
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
if (
|
|
1092
|
-
(contract.responseContract.mode === "paged" || contract.responseContract.mode === "cursor") &&
|
|
1093
|
-
(contract.responseContract.pagination || contract.responseContract.cursor)
|
|
1094
|
-
) {
|
|
1095
|
-
const envelopeSuffix = contract.responseContract.mode === "cursor" ? "CursorPageResponse" : "PageResponse";
|
|
1096
|
-
const envelopeSchemaName = componentNameFromShape(contract.responseContract.shape.id, envelopeSuffix);
|
|
1097
|
-
componentNames.responseEnvelope.set(contract.capability.id, envelopeSchemaName);
|
|
1098
|
-
if (!document.components.schemas[envelopeSchemaName]) {
|
|
1099
|
-
if (contract.responseContract.mode === "paged") {
|
|
1100
|
-
const pagination = contract.responseContract.pagination;
|
|
1101
|
-
document.components.schemas[envelopeSchemaName] = {
|
|
1102
|
-
type: "object",
|
|
1103
|
-
additionalProperties: false,
|
|
1104
|
-
required: [
|
|
1105
|
-
pagination.itemsProperty,
|
|
1106
|
-
pagination.pageProperty,
|
|
1107
|
-
pagination.pageSizeProperty,
|
|
1108
|
-
pagination.totalProperty
|
|
1109
|
-
],
|
|
1110
|
-
properties: {
|
|
1111
|
-
[pagination.itemsProperty]: { type: "array", items: refSchema(responseSchemaName) },
|
|
1112
|
-
[pagination.pageProperty]: { type: "integer" },
|
|
1113
|
-
[pagination.pageSizeProperty]: { type: "integer" },
|
|
1114
|
-
[pagination.totalProperty]: { type: "integer" }
|
|
1115
|
-
}
|
|
1116
|
-
};
|
|
1117
|
-
} else {
|
|
1118
|
-
const cursor = contract.responseContract.cursor;
|
|
1119
|
-
const includeTotal = contract.responseContract.total?.included === true;
|
|
1120
|
-
document.components.schemas[envelopeSchemaName] = {
|
|
1121
|
-
type: "object",
|
|
1122
|
-
additionalProperties: false,
|
|
1123
|
-
required: ["items", cursor.responseNext],
|
|
1124
|
-
properties: {
|
|
1125
|
-
items: { type: "array", items: refSchema(responseSchemaName) },
|
|
1126
|
-
[cursor.responseNext]: { type: "string" },
|
|
1127
|
-
...(cursor.responsePrev ? { [cursor.responsePrev]: { type: "string" } } : {}),
|
|
1128
|
-
...(includeTotal ? { total: { type: "integer" } } : {})
|
|
1129
|
-
}
|
|
1130
|
-
};
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
for (const callback of contract.endpoint.callbacks || []) {
|
|
1137
|
-
const payloadShapeId = callback.payload?.id || null;
|
|
1138
|
-
if (!payloadShapeId) continue;
|
|
1139
|
-
const payloadShape = byId.get(payloadShapeId);
|
|
1140
|
-
if (!payloadShape) continue;
|
|
1141
|
-
const responseSchemaName = componentNameFromShape(payloadShapeId, "Response");
|
|
1142
|
-
componentNames.response.set(payloadShapeId, responseSchemaName);
|
|
1143
|
-
if (!document.components.schemas[responseSchemaName]) {
|
|
1144
|
-
document.components.schemas[responseSchemaName] = cloneSchema(generateShapeJsonSchema(payloadShape, byId));
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
for (const error of contract.errors) {
|
|
1149
|
-
const schemaName = errorSchemaComponentName(error);
|
|
1150
|
-
if (!document.components.schemas[schemaName]) {
|
|
1151
|
-
document.components.schemas[schemaName] = {
|
|
1152
|
-
allOf: [
|
|
1153
|
-
refSchema("ErrorResponse"),
|
|
1154
|
-
{
|
|
1155
|
-
type: "object",
|
|
1156
|
-
properties: {
|
|
1157
|
-
code: { type: "string", const: error.code }
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
],
|
|
1161
|
-
title: titleFromIdentifier(error.code)
|
|
1162
|
-
};
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
const componentName = errorResponseComponentName(error);
|
|
1166
|
-
if (!document.components.responses[componentName]) {
|
|
1167
|
-
document.components.responses[componentName] = {
|
|
1168
|
-
description: errorResponseDescription(error),
|
|
1169
|
-
content: {
|
|
1170
|
-
"application/json": {
|
|
1171
|
-
schema: refSchema(schemaName)
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
};
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
for (const contract of contracts) {
|
|
1180
|
-
const pathKey = contract.endpoint.path.replace(/:([A-Za-z0-9_]+)/g, "{$1}");
|
|
1181
|
-
if (!document.paths[pathKey]) {
|
|
1182
|
-
document.paths[pathKey] = {};
|
|
1183
|
-
}
|
|
1184
|
-
document.paths[pathKey][contract.endpoint.method.toLowerCase()] = operationFromContract(contract, componentNames);
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
const needsSecurity = contracts.some((contract) => contract.endpoint.auth && contract.endpoint.auth !== "none");
|
|
1188
|
-
if (!needsSecurity) {
|
|
1189
|
-
delete document.components.securitySchemes;
|
|
1190
|
-
if (Object.keys(document.components.schemas || {}).length === 0) delete document.components.schemas;
|
|
1191
|
-
if (Object.keys(document.components.responses || {}).length === 0) delete document.components.responses;
|
|
1192
|
-
if (Object.keys(document.components).length === 0) delete document.components;
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
return document;
|
|
1196
|
-
}
|
|
3
|
+
export { generateApiContractDebug, generateApiContractGraph } from "./api/contracts.js";
|
|
4
|
+
export { generateOpenApi } from "./api/openapi.js";
|