@wildo-ai/saas-technical-doc 1.1.2 → 1.1.4
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/dist/esm/build/csp-emit.d.ts.map +1 -1
- package/dist/esm/build/load-materialized-frontend-providers.d.ts.map +1 -1
- package/dist/esm/companion/application-documentation/application-administration-documentation.d.ts.map +1 -1
- package/dist/esm/companion/application-documentation/application-administration-documentation.js.map +1 -1
- package/dist/esm/companion/application-documentation/application-authentication-documentation.d.ts.map +1 -1
- package/dist/esm/companion/application-documentation/application-connection-documentation.d.ts +25 -1
- package/dist/esm/companion/application-documentation/application-connection-documentation.d.ts.map +1 -1
- package/dist/esm/companion/application-documentation/application-connection-documentation.js +28 -1
- package/dist/esm/companion/application-documentation/application-connection-documentation.js.map +1 -1
- package/dist/esm/companion/application-documentation/application-domain-documentation.d.ts +133 -0
- package/dist/esm/companion/application-documentation/application-domain-documentation.d.ts.map +1 -0
- package/dist/esm/companion/application-documentation/application-domain-documentation.js +243 -0
- package/dist/esm/companion/application-documentation/application-domain-documentation.js.map +1 -0
- package/dist/esm/companion/application-documentation/application-integration-documentation.d.ts.map +1 -1
- package/dist/esm/companion/application-documentation/application-integration-documentation.js +23 -0
- package/dist/esm/companion/application-documentation/application-integration-documentation.js.map +1 -1
- package/dist/esm/companion/application-documentation/application-organization-role-documentation.d.ts +30 -0
- package/dist/esm/companion/application-documentation/application-organization-role-documentation.d.ts.map +1 -1
- package/dist/esm/companion/application-documentation/application-organization-role-documentation.js +38 -0
- package/dist/esm/companion/application-documentation/application-organization-role-documentation.js.map +1 -1
- package/dist/esm/companion/application-documentation/application-organization-unit-resource-documentation.d.ts.map +1 -1
- package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.d.ts +61 -1
- package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.d.ts.map +1 -1
- package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.js +255 -218
- package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.js.map +1 -1
- package/dist/esm/companion/application-documentation/technical-documentation-operational-evidence.d.ts.map +1 -1
- package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.d.ts +2 -0
- package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.d.ts.map +1 -1
- package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.js +1 -0
- package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.js.map +1 -1
- package/dist/esm/companion/application-documentation/technical-documentation-publication-policy.d.ts.map +1 -1
- package/dist/esm/companion/application-documentation/technical-documentation-publication-policy.js +1 -1
- package/dist/esm/companion/application-documentation/technical-documentation-publication-policy.js.map +1 -1
- package/dist/esm/companion/content/application-consumer-documentation-content-loader.d.ts.map +1 -1
- package/dist/esm/companion/index.d.ts +2 -1
- package/dist/esm/companion/index.d.ts.map +1 -1
- package/dist/esm/companion/index.js +2 -1
- package/dist/esm/companion/index.js.map +1 -1
- package/dist/esm/companion/manual-controller-route-projection.d.ts +112 -0
- package/dist/esm/companion/manual-controller-route-projection.d.ts.map +1 -0
- package/dist/esm/companion/manual-controller-route-projection.js +249 -0
- package/dist/esm/companion/manual-controller-route-projection.js.map +1 -0
- package/dist/esm/companion/openapi-generator.d.ts +16 -0
- package/dist/esm/companion/openapi-generator.d.ts.map +1 -1
- package/dist/esm/companion/openapi-generator.js +493 -26
- package/dist/esm/companion/openapi-generator.js.map +1 -1
- package/dist/esm/companion/operation-projection.schemas.d.ts +44 -0
- package/dist/esm/companion/operation-projection.schemas.d.ts.map +1 -1
- package/dist/esm/companion/operation-projection.schemas.js +37 -0
- package/dist/esm/companion/operation-projection.schemas.js.map +1 -1
- package/dist/esm/companion/publish-result.types.d.ts +37 -4
- package/dist/esm/companion/publish-result.types.d.ts.map +1 -1
- package/dist/esm/companion/publish-result.types.js.map +1 -1
- package/dist/esm/companion/rendering/technical-documentation-authorized-bundle-reader.d.ts.map +1 -1
- package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.d.ts +7 -1
- package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.d.ts.map +1 -1
- package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.js +34 -18
- package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.js.map +1 -1
- package/dist/esm/companion/rendering/technical-documentation-managed-tree-validator.d.ts.map +1 -1
- package/dist/esm/companion/rendering/technical-documentation-managed-tree-validator.js +8 -1
- package/dist/esm/companion/rendering/technical-documentation-managed-tree-validator.js.map +1 -1
- package/dist/esm/companion/rendering/technical-documentation-openapi-renderer.d.ts.map +1 -1
- package/dist/esm/companion/rendering/technical-documentation-render-model.d.ts.map +1 -1
- package/dist/esm/companion/rendering/technical-documentation-render-model.js +30 -13
- package/dist/esm/companion/rendering/technical-documentation-render-model.js.map +1 -1
- package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.d.ts +10 -0
- package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.d.ts.map +1 -1
- package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.js +10 -15
- package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.js.map +1 -1
- package/dist/esm/companion/technical-documentation-asset-path.d.ts.map +1 -1
- package/dist/esm/companion/technical-documentation-capture-execution-port.d.ts +27 -1
- package/dist/esm/companion/technical-documentation-capture-execution-port.d.ts.map +1 -1
- package/dist/esm/companion/technical-documentation-capture-execution-port.js.map +1 -1
- package/dist/esm/companion/technical-documentation-diagram-definitions.d.ts +33 -0
- package/dist/esm/companion/technical-documentation-diagram-definitions.d.ts.map +1 -0
- package/dist/esm/companion/technical-documentation-diagram-definitions.js +54 -0
- package/dist/esm/companion/technical-documentation-diagram-definitions.js.map +1 -0
- package/dist/esm/companion/technical-documentation-diagram-materializer.d.ts +10 -18
- package/dist/esm/companion/technical-documentation-diagram-materializer.d.ts.map +1 -1
- package/dist/esm/companion/technical-documentation-diagram-materializer.js +9 -39
- package/dist/esm/companion/technical-documentation-diagram-materializer.js.map +1 -1
- package/dist/esm/companion/technical-documentation-placeholder-materializer.d.ts.map +1 -1
- package/dist/esm/companion/zod-to-openapi.d.ts.map +1 -1
- package/dist/esm/companion-exports.d.ts.map +1 -1
- package/dist/esm/config/define-tech-doc-config.d.ts.map +1 -1
- package/dist/esm/config/index.d.ts.map +1 -1
- package/dist/esm/config/wildo-tech-doc-config.schemas.d.ts +8 -4
- package/dist/esm/config/wildo-tech-doc-config.schemas.d.ts.map +1 -1
- package/dist/esm/config/wildo-tech-doc-config.schemas.js +8 -4
- package/dist/esm/config/wildo-tech-doc-config.schemas.js.map +1 -1
- package/dist/esm/content/application-consumer-documentation-content-manifest.schemas.d.ts.map +1 -1
- package/dist/esm/content/application-consumer-documentation-content.techdoc.d.ts +42 -64
- package/dist/esm/content/application-consumer-documentation-content.techdoc.d.ts.map +1 -1
- package/dist/esm/content/application-consumer-documentation-content.techdoc.js +164 -925
- package/dist/esm/content/application-consumer-documentation-content.techdoc.js.map +1 -1
- package/dist/esm/content.exports.d.ts.map +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/openapi/api-reference-link-index.d.ts.map +1 -1
- package/dist/esm/openapi/index.d.ts.map +1 -1
- package/dist/esm/openapi/openapi-generation-output.schemas.d.ts.map +1 -1
- package/dist/esm/openapi-reference-model.exports.d.ts.map +1 -1
- package/dist/esm/runtime/AuthExchangePage.d.ts.map +1 -1
- package/dist/esm/runtime/DocsAuthContext.d.ts +16 -1
- package/dist/esm/runtime/DocsAuthContext.d.ts.map +1 -1
- package/dist/esm/runtime/DocsAuthContext.js +18 -2
- package/dist/esm/runtime/DocsAuthContext.js.map +1 -1
- package/dist/esm/runtime/decode-jwt-claims.d.ts.map +1 -1
- package/dist/esm/runtime/docs-auth-client.d.ts.map +1 -1
- package/dist/esm/runtime/docs-auth-session.schemas.d.ts.map +1 -1
- package/dist/esm/runtime/frontend-provider-registry.techdoc.d.ts +35 -13
- package/dist/esm/runtime/frontend-provider-registry.techdoc.d.ts.map +1 -1
- package/dist/esm/runtime/frontend-provider-registry.techdoc.js +28 -19
- package/dist/esm/runtime/frontend-provider-registry.techdoc.js.map +1 -1
- package/dist/esm/runtime/index.d.ts +1 -0
- package/dist/esm/runtime/index.d.ts.map +1 -1
- package/dist/esm/runtime/index.js +1 -0
- package/dist/esm/runtime/index.js.map +1 -1
- package/dist/esm/runtime/openapi-reference-conservation.d.ts.map +1 -1
- package/dist/esm/runtime/use-docs-auth-session.d.ts.map +1 -1
- package/dist/esm/runtime/use-docs-frontend-provider-registry.d.ts.map +1 -1
- package/dist/esm/runtime/use-docs-provider-sdks.d.ts +21 -0
- package/dist/esm/runtime/use-docs-provider-sdks.d.ts.map +1 -0
- package/dist/esm/runtime/use-docs-provider-sdks.js +49 -0
- package/dist/esm/runtime/use-docs-provider-sdks.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +6 -5
- package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.d.ts +0 -9
- package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.d.ts.map +0 -1
- package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.js +0 -111
- package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.js.map +0 -1
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
* barrel enters this semantic generator.
|
|
42
42
|
*/
|
|
43
43
|
import { WildoHeaderKeys } from '@wildo-ai/saas-models/public-runtime';
|
|
44
|
-
import { COLLECTION_QUERY_PARAMETERS, Resources_PaginationRequestSchema } from '@wildo-ai/saas-models';
|
|
44
|
+
import { COLLECTION_QUERY_PARAMETERS, ERROR_DEFINITIONS, ErrorHandling_Strategy, ErrorSeverity, ErrorType, M2M_WEBHOOK_DELIVERY_CONTRACT, Resources_PaginationRequestSchema, } from '@wildo-ai/saas-models';
|
|
45
45
|
import { OpenApiSection, } from '../openapi/openapi-generation-output.schemas.js';
|
|
46
46
|
import { OpenApiGenerationInputSchema, OPENAPI_RESOURCE_IDENTITY_CONTRACT_VERSION, OperationProjectionAuthenticationMode, OperationProjectionVariantType, } from './operation-projection.schemas.js';
|
|
47
47
|
import { stripImplementationNoise } from './spec-to-operation-doc.js';
|
|
@@ -345,6 +345,58 @@ function extractPathParameters(path) {
|
|
|
345
345
|
const matches = path.matchAll(/\{([^}]+)\}/gu);
|
|
346
346
|
return Array.from(matches, (match) => match[1]);
|
|
347
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* What a path placeholder identifies, read off the path rather than guessed from its name.
|
|
350
|
+
*
|
|
351
|
+
* Measured on Wonder CRM: 0 of 911 path parameters carried a description while query parameters
|
|
352
|
+
* were 85% covered and headers 100% — so a reader looking at `/organizations/{organizationId}/deal/
|
|
353
|
+
* {dealId}` was told what every filter meant and nothing about the two values they must supply.
|
|
354
|
+
*
|
|
355
|
+
* The segment BEFORE a placeholder names the collection it addresses, which is the fact that makes
|
|
356
|
+
* this derivable rather than invented. The last placeholder addresses the operation's own resource;
|
|
357
|
+
* the earlier ones are the scope it lives in, and saying so is what tells a caller which id goes
|
|
358
|
+
* where. The collection name is quoted verbatim, never humanized: it is the identifier the API
|
|
359
|
+
* itself uses, so a reader can match it against the path, the tag and the payload.
|
|
360
|
+
*/
|
|
361
|
+
function describePathParameter(path, parameterName, resourceIdentifier) {
|
|
362
|
+
const segments = path.split('/').filter((segment) => segment.length > 0);
|
|
363
|
+
const placeholderIndex = segments.indexOf(`{${parameterName}}`);
|
|
364
|
+
const owningCollection = placeholderIndex > 0 ? segments[placeholderIndex - 1] : undefined;
|
|
365
|
+
if (owningCollection === undefined || owningCollection.startsWith('{')) {
|
|
366
|
+
return `Identifier supplied in the request path as \`${parameterName}\`.`;
|
|
367
|
+
}
|
|
368
|
+
return owningCollection === resourceIdentifier
|
|
369
|
+
? `Identifier of the \`${owningCollection}\` this operation addresses.`
|
|
370
|
+
: `Identifier of the \`${owningCollection}\` the addressed record belongs to.`;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Splits an operation's qualifier tail into the two kinds that look alike and mean opposite things.
|
|
374
|
+
*
|
|
375
|
+
* `Via<Parent>` qualifiers are ROUTING: the same operation reached through a different parent, so
|
|
376
|
+
* `GET /organizations/{organizationId}/organization-members/{id}` and
|
|
377
|
+
* `GET /users/{userId}/organization-members/{id}` are one operation at two addresses. Every other
|
|
378
|
+
* qualifier — `summary`, `context`, `BULK` — is a genuinely different operation with a different
|
|
379
|
+
* response.
|
|
380
|
+
*
|
|
381
|
+
* Conflating them is what makes the reference look four times its real size. Measured on Wonder
|
|
382
|
+
* CRM: 546 operations at 439 paths, of which 486 are another path's operation repeated —
|
|
383
|
+
* `organizationMembers` publishes its whole surface three times.
|
|
384
|
+
*/
|
|
385
|
+
function splitOperationQualifiers(op) {
|
|
386
|
+
const tail = op.operationKey.startsWith(op.baseOperationIdentifier)
|
|
387
|
+
? op.operationKey.slice(op.baseOperationIdentifier.length)
|
|
388
|
+
: '';
|
|
389
|
+
const segments = tail.split('_').filter(Boolean);
|
|
390
|
+
return {
|
|
391
|
+
via: segments.filter((segment) => segment.startsWith('Via')),
|
|
392
|
+
shape: segments.filter((segment) => !segment.startsWith('Via')),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/** The identity an alias shares with the one canonical route of the same operation. */
|
|
396
|
+
function canonicalRouteKey(op) {
|
|
397
|
+
const { shape } = splitOperationQualifiers(op);
|
|
398
|
+
return [op.resourceIdentifier, op.baseOperationIdentifier, shape.join('_'), op.httpVerb].join('\u0000');
|
|
399
|
+
}
|
|
348
400
|
function isPlainObject(value) {
|
|
349
401
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
350
402
|
}
|
|
@@ -738,6 +790,26 @@ function buildOperationObject(op, operationId, errorResponseSchema) {
|
|
|
738
790
|
// Auth (Tier-1 framework knowledge). The source projector resolves access
|
|
739
791
|
// semantics once; this generator only transports that fact into standard
|
|
740
792
|
// OpenAPI `security` and the companion `x-wildo` extension.
|
|
793
|
+
/*
|
|
794
|
+
* Say that this call notifies, where the reader is standing.
|
|
795
|
+
*
|
|
796
|
+
* A developer looking at `changeStatus` needs to know it does more than change a record: every
|
|
797
|
+
* endpoint configured on that channel receives a delivery. The mechanics — retries, signature,
|
|
798
|
+
* how to verify — are the webhook contract's and are described once at the document's `webhooks`
|
|
799
|
+
* block, not restated on each of the operations that fire one.
|
|
800
|
+
*/
|
|
801
|
+
const notifications = op.m2mNotifications ?? [];
|
|
802
|
+
if (notifications.length > 0) {
|
|
803
|
+
const channels = [...new Set(notifications.map((notification) => notification.channel))].sort();
|
|
804
|
+
const note = `A successful call also delivers an outbound webhook on ${channels.length === 1 ? 'the ' : ''}`
|
|
805
|
+
+ `${channels.map((channel) => `\`${channel}\``).join(' and ')} channel${channels.length === 1 ? '' : 's'}`
|
|
806
|
+
+ ', to every endpoint an organization has configured and enabled there. See this document\'s `webhooks` for the delivery contract.';
|
|
807
|
+
const existing = typeof operationObject['description'] === 'string' ? operationObject['description'] : '';
|
|
808
|
+
operationObject['description'] = existing.length > 0 ? `${existing}\n\n${note}` : note;
|
|
809
|
+
const wildo = operationObject['x-wildo'];
|
|
810
|
+
if (isPlainObject(wildo))
|
|
811
|
+
wildo['m2mNotifications'] = notifications.map((notification) => ({ ...notification }));
|
|
812
|
+
}
|
|
741
813
|
const security = buildOperationSecurity(op);
|
|
742
814
|
operationObject['security'] = security;
|
|
743
815
|
if (op.access.authenticationMode !== OperationProjectionAuthenticationMode.PUBLIC && op.access.roleRequirements.length > 0) {
|
|
@@ -751,6 +823,7 @@ function buildOperationObject(op, operationId, errorResponseSchema) {
|
|
|
751
823
|
in: 'path',
|
|
752
824
|
required: true,
|
|
753
825
|
schema: { type: 'string' },
|
|
826
|
+
description: describePathParameter(op.path, paramName, op.resourceIdentifier),
|
|
754
827
|
})));
|
|
755
828
|
}
|
|
756
829
|
if (op.stepUpAuthentication !== undefined) {
|
|
@@ -890,6 +963,38 @@ const ENGINE_METADATA_PROPERTY_SCHEMAS = {
|
|
|
890
963
|
+ 'conflict-free merges of concurrent edits. Opaque to clients — safe to ignore.',
|
|
891
964
|
},
|
|
892
965
|
};
|
|
966
|
+
/**
|
|
967
|
+
* Descriptions for the properties EVERY resource carries and NO resource authors.
|
|
968
|
+
*
|
|
969
|
+
* `_version` and `_field_meta` above are injected by the generator, so they were described where
|
|
970
|
+
* they are created. These are different: they are real fields on the document, present on every
|
|
971
|
+
* resource, and therefore owned by no single resource specification — which is exactly why they
|
|
972
|
+
* were the most undescribed properties in the whole reference. Measured on Wonder CRM:
|
|
973
|
+
*
|
|
974
|
+
* data 123 · pagination 123 · retentionStatus 104 · impersonalizedAt 104 · _id 90 ·
|
|
975
|
+
* createdAt 65 · updatedAt 64 · organizationId 48
|
|
976
|
+
*
|
|
977
|
+
* — 721 of the 1,091 undescribed top-level properties, from eight names that mean the same thing
|
|
978
|
+
* everywhere. Asking every resource to describe them would be hundreds of copies of one sentence.
|
|
979
|
+
*
|
|
980
|
+
* An AUTHORED description always wins: this fills a gap, it never overrides a specification. That
|
|
981
|
+
* matters for names an application could legitimately reuse — a resource of its own with a field
|
|
982
|
+
* called `data` describes it, and this map stays out of the way.
|
|
983
|
+
*/
|
|
984
|
+
const FRAMEWORK_OWNED_PROPERTY_DESCRIPTIONS = Object.freeze({
|
|
985
|
+
_id: 'The record’s identifier. Stable for the life of the record and the value every path parameter and reference uses.',
|
|
986
|
+
createdAt: 'When the record was created, set by the application and never accepted from a client.',
|
|
987
|
+
updatedAt: 'When the record last changed, set by the application on every write.',
|
|
988
|
+
organizationId: 'The organization this record belongs to. It scopes every read and write: a caller only ever addresses records in an organization they are a member of.',
|
|
989
|
+
// retention-authority: N/A — an API consumer's ENGLISH description of what the two values mean,
|
|
990
|
+
// rendered into the OpenAPI document. There is no emitter to consume here: the audience is a
|
|
991
|
+
// person reading a schema, and the sentence has to say `active` and `retained` in words for the
|
|
992
|
+
// document to be worth anything. It decides nothing and is read by no code.
|
|
993
|
+
retentionStatus: 'Whether the record is still `active` or has been `retained` — kept for a legal or accounting obligation after the person it concerned was erased. A retained record has had its personal detail removed.',
|
|
994
|
+
impersonalizedAt: 'When personal detail was stripped from this record, if it ever was. Absent on a record that has not been through erasure.',
|
|
995
|
+
data: 'The page of records this response carries.',
|
|
996
|
+
pagination: 'Where this page sits in the full result set — the page number, the page size, and the totals needed to ask for the next one.',
|
|
997
|
+
});
|
|
893
998
|
/**
|
|
894
999
|
* The structural signature of a MATERIALIZED resource document in a response
|
|
895
1000
|
* schema is the `_id` property. The serializer attaches `_version` /
|
|
@@ -955,11 +1060,47 @@ function injectEngineMetadataIntoEntitySchemas(schema) {
|
|
|
955
1060
|
* (`stableStringify`) still collapses them onto one component. Non-object
|
|
956
1061
|
* schemas (`null` / scalar) pass through unchanged.
|
|
957
1062
|
*/
|
|
1063
|
+
/**
|
|
1064
|
+
* Fills a description on the properties every resource carries and none authors.
|
|
1065
|
+
*
|
|
1066
|
+
* Recursive, because these appear at every depth: `data` and `pagination` wrap the envelope,
|
|
1067
|
+
* `_id` and `createdAt` sit on the document inside it, and a `context`-mode response nests another
|
|
1068
|
+
* collection again. Never overrides an authored description — a specification that describes its
|
|
1069
|
+
* own field wins, which is what keeps this safe for a name an application might reuse.
|
|
1070
|
+
*/
|
|
1071
|
+
function fillFrameworkOwnedPropertyDescriptions(node) {
|
|
1072
|
+
if (Array.isArray(node)) {
|
|
1073
|
+
for (const entry of node)
|
|
1074
|
+
fillFrameworkOwnedPropertyDescriptions(entry);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
if (!isPlainObject(node))
|
|
1078
|
+
return;
|
|
1079
|
+
const properties = node['properties'];
|
|
1080
|
+
if (isPlainObject(properties)) {
|
|
1081
|
+
for (const [name, property] of Object.entries(properties)) {
|
|
1082
|
+
if (!isPlainObject(property))
|
|
1083
|
+
continue;
|
|
1084
|
+
const framework = FRAMEWORK_OWNED_PROPERTY_DESCRIPTIONS[name];
|
|
1085
|
+
if (framework !== undefined && property['description'] === undefined) {
|
|
1086
|
+
property['description'] = framework;
|
|
1087
|
+
}
|
|
1088
|
+
fillFrameworkOwnedPropertyDescriptions(property);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
for (const key of ['items', 'allOf', 'oneOf', 'anyOf', 'additionalProperties']) {
|
|
1092
|
+
if (key in node)
|
|
1093
|
+
fillFrameworkOwnedPropertyDescriptions(node[key]);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
958
1096
|
function augmentResponseSchemaWithEngineMetadata(responseBodySchema) {
|
|
959
1097
|
if (!isPlainObject(responseBodySchema)) {
|
|
960
1098
|
return responseBodySchema;
|
|
961
1099
|
}
|
|
962
1100
|
const clone = structuredClone(responseBodySchema);
|
|
1101
|
+
// Safe to mutate: `clone` is this function's own deep copy, and the caller's schema is the
|
|
1102
|
+
// operation projection's — a generator that edited it would be editing the data it was handed.
|
|
1103
|
+
fillFrameworkOwnedPropertyDescriptions(clone);
|
|
963
1104
|
const properties = clone['properties'];
|
|
964
1105
|
const dataSchema = isPlainObject(properties) ? properties['data'] : undefined;
|
|
965
1106
|
if (isPlainObject(dataSchema) && (dataSchema['type'] === 'array' || 'items' in dataSchema)) {
|
|
@@ -1093,27 +1234,151 @@ function buildResponsesObject(op, responseBodySchema, errorResponseSchema) {
|
|
|
1093
1234
|
// 401 contract. A resource-owned 401 is more specific (for example a
|
|
1094
1235
|
// step-up flow) and deliberately takes precedence.
|
|
1095
1236
|
if (op.access.authenticationMode !== OperationProjectionAuthenticationMode.PUBLIC && responses['401'] === undefined) {
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
})();
|
|
1110
|
-
responses['401'] = {
|
|
1111
|
-
description,
|
|
1112
|
-
content: { 'application/json': { schema: errorResponseSchema } },
|
|
1113
|
-
};
|
|
1237
|
+
// Referenced, not inlined: the wording is the generator's and identical for every operation
|
|
1238
|
+
// with the same access mode, so `unauthorizedDescription` states it once and the component
|
|
1239
|
+
// carries it. Repeating the content block on 546 operations said nothing a reader could use.
|
|
1240
|
+
responses['401'] = { $ref: `#/components/responses/${unauthorizedResponseName(op.access.authenticationMode)}` };
|
|
1241
|
+
}
|
|
1242
|
+
/*
|
|
1243
|
+
* Every operation can fail inside the application, and not one said so. Added last and only when
|
|
1244
|
+
* absent, so an authored 500 — which can say something specific about THIS operation — wins.
|
|
1245
|
+
*/
|
|
1246
|
+
if (responses['500'] === undefined) {
|
|
1247
|
+
responses['500'] = { $ref: `#/components/responses/${INTERNAL_ERROR_RESPONSE_NAME}` };
|
|
1114
1248
|
}
|
|
1115
1249
|
return responses;
|
|
1116
1250
|
}
|
|
1251
|
+
/**
|
|
1252
|
+
* The error responses the GENERATOR owns, named once in `components.responses`.
|
|
1253
|
+
*
|
|
1254
|
+
* Only the ones whose wording is the generator's own are shared. An authored response carries a
|
|
1255
|
+
* description written for that one operation — "No release record exists for the supplied id in the
|
|
1256
|
+
* caller's scope" — and that sentence is the most useful prose in the document; folding it into a
|
|
1257
|
+
* shared component would replace it with something generic, so authored responses stay inline.
|
|
1258
|
+
*
|
|
1259
|
+
* The 401 wording is derived from the operation's ACCESS MODE, which is why there are three: the
|
|
1260
|
+
* credential a caller is missing differs, and saying "no valid bearer token" to an anonymous-session
|
|
1261
|
+
* endpoint would be wrong.
|
|
1262
|
+
*/
|
|
1263
|
+
const GENERATOR_OWNED_ERROR_RESPONSE_NAMES = Object.freeze({
|
|
1264
|
+
[OperationProjectionAuthenticationMode.ANONYMOUS_SESSION]: 'UnauthorizedAnonymousSession',
|
|
1265
|
+
[OperationProjectionAuthenticationMode.ANONYMOUS_OR_AUTHENTICATED]: 'UnauthorizedAnonymousOrAuthenticated',
|
|
1266
|
+
[OperationProjectionAuthenticationMode.AUTHENTICATED]: 'Unauthorized',
|
|
1267
|
+
[OperationProjectionAuthenticationMode.PUBLIC]: 'Unauthorized',
|
|
1268
|
+
});
|
|
1269
|
+
const INTERNAL_ERROR_RESPONSE_NAME = 'InternalServerError';
|
|
1270
|
+
function unauthorizedResponseName(mode) {
|
|
1271
|
+
return GENERATOR_OWNED_ERROR_RESPONSE_NAMES[mode];
|
|
1272
|
+
}
|
|
1273
|
+
/** The one place each 401 wording is written; the components below are built from it. */
|
|
1274
|
+
function unauthorizedDescription(mode) {
|
|
1275
|
+
switch (mode) {
|
|
1276
|
+
case OperationProjectionAuthenticationMode.ANONYMOUS_SESSION:
|
|
1277
|
+
return 'The anonymous-session token is missing, malformed, expired, or invalid.';
|
|
1278
|
+
case OperationProjectionAuthenticationMode.ANONYMOUS_OR_AUTHENTICATED:
|
|
1279
|
+
return 'No valid anonymous-session token, bearer token, or API key was supplied.';
|
|
1280
|
+
case OperationProjectionAuthenticationMode.AUTHENTICATED:
|
|
1281
|
+
case OperationProjectionAuthenticationMode.PUBLIC:
|
|
1282
|
+
return 'No valid bearer token or API key was supplied.';
|
|
1283
|
+
default: {
|
|
1284
|
+
const exhaustiveAuthenticationMode = mode;
|
|
1285
|
+
throw new Error(`OpenAPI generator: unsupported authentication mode ${String(exhaustiveAuthenticationMode)}`);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* The `components.responses` block, built from the same wording the operations reference.
|
|
1291
|
+
*
|
|
1292
|
+
* Emitted only for the names an operation actually used, so a document never carries a component
|
|
1293
|
+
* nothing points at — the reason `PUBLIC` shares the authenticated name rather than earning an
|
|
1294
|
+
* entry of its own: a public operation never gets a synthesised 401.
|
|
1295
|
+
*/
|
|
1296
|
+
function buildErrorResponseComponents(usedNames, errorResponseSchema) {
|
|
1297
|
+
const content = { 'application/json': { schema: errorResponseSchema } };
|
|
1298
|
+
const all = { [INTERNAL_ERROR_RESPONSE_NAME]: { description: internalServerErrorDescription(), content } };
|
|
1299
|
+
for (const mode of [
|
|
1300
|
+
OperationProjectionAuthenticationMode.ANONYMOUS_SESSION,
|
|
1301
|
+
OperationProjectionAuthenticationMode.ANONYMOUS_OR_AUTHENTICATED,
|
|
1302
|
+
OperationProjectionAuthenticationMode.AUTHENTICATED,
|
|
1303
|
+
]) {
|
|
1304
|
+
all[unauthorizedResponseName(mode)] = { description: unauthorizedDescription(mode), content };
|
|
1305
|
+
}
|
|
1306
|
+
return Object.fromEntries(Object.entries(all).filter(([name]) => usedNames.has(name)));
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Every operation can fail with the framework's error envelope, and until now not one of the 546
|
|
1310
|
+
* published operations said so — measured on Wonder CRM, `500` appeared zero times.
|
|
1311
|
+
*
|
|
1312
|
+
* Synthesised rather than authored, for the same reason the 401 is: it is true of every operation
|
|
1313
|
+
* by construction, so making each specification restate it would be 546 copies of one fact with 546
|
|
1314
|
+
* chances to drift. An authored 500 takes precedence, exactly as an authored 401 does.
|
|
1315
|
+
*/
|
|
1316
|
+
function internalServerErrorDescription() {
|
|
1317
|
+
return 'The request failed inside the application. The body is the standard error envelope and carries a `correlationId` to quote when reporting it; the request was not necessarily rejected, so do not assume it had no effect until you have checked.';
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* The document's `webhooks` block — what a RECEIVER is sent, described where OpenAPI 3.1 puts it.
|
|
1321
|
+
*
|
|
1322
|
+
* Until now the reference described only what a client CALLS. An integrator writing the other half
|
|
1323
|
+
* — the endpoint this application posts to — had no machine-readable contract at all, and every
|
|
1324
|
+
* operation that fires a delivery said nothing about it.
|
|
1325
|
+
*
|
|
1326
|
+
* Two facts make these entries honest rather than plausible:
|
|
1327
|
+
*
|
|
1328
|
+
* · THE BODY IS THE RESOURCE DOCUMENT ITSELF, not an envelope around it. The delivery worker
|
|
1329
|
+
* posts the stored payload verbatim, and a backend test exists specifically to fail if anyone
|
|
1330
|
+
* wraps it — because the body's bytes must hash to the signature's `bodyHash` claim. So the
|
|
1331
|
+
* schema referenced here is the resource's own read response, and when that component cannot
|
|
1332
|
+
* be resolved the entry carries prose instead of a shape it cannot prove.
|
|
1333
|
+
* · THE DELIVERY CONTRACT IS ONE OBJECT. Retries, timeout, signature algorithm and claim names
|
|
1334
|
+
* come from `M2M_WEBHOOK_DELIVERY_CONTRACT`, the same source the delivery worker derives its
|
|
1335
|
+
* tunables from, so the published contract cannot drift from what is actually sent.
|
|
1336
|
+
*/
|
|
1337
|
+
function buildWebhooksObject(operations, canonicalReadResponseRefByResource) {
|
|
1338
|
+
const webhooks = {};
|
|
1339
|
+
for (const op of operations) {
|
|
1340
|
+
for (const notification of op.m2mNotifications ?? []) {
|
|
1341
|
+
const key = `${op.resourceIdentifier}.${op.baseOperationIdentifier}`;
|
|
1342
|
+
if (webhooks[key] !== undefined)
|
|
1343
|
+
continue;
|
|
1344
|
+
const bodyRef = canonicalReadResponseRefByResource.get(op.resourceIdentifier);
|
|
1345
|
+
const contract = M2M_WEBHOOK_DELIVERY_CONTRACT;
|
|
1346
|
+
webhooks[key] = {
|
|
1347
|
+
post: {
|
|
1348
|
+
summary: `${op.resourceIdentifier} ${op.baseOperationIdentifier}`,
|
|
1349
|
+
description: [
|
|
1350
|
+
`Sent to every endpoint configured and enabled on the \`${notification.channel}\` channel when \`${op.resourceIdentifier}.${op.baseOperationIdentifier}\` succeeds.`,
|
|
1351
|
+
'',
|
|
1352
|
+
`The body is the record itself, exactly as the API returns it — there is no envelope around it, and the bytes you receive are the bytes the signature was computed over.`,
|
|
1353
|
+
'',
|
|
1354
|
+
`Verify before trusting it: the \`${contract.signatureHeader}\` header carries a ${contract.signatureAlgorithm} JWT whose \`${contract.signatureClaims.bodyHash}\` claim is the ${contract.bodyHashAlgorithm} digest of the raw body. Its \`${contract.signatureClaims.deliveryId}\` claim is the delivery id and is identical across every retry, so use it to deduplicate.`,
|
|
1355
|
+
'',
|
|
1356
|
+
`Answer 2xx to acknowledge. Anything else is retried up to ${contract.maxAttempts} attempts with a fixed backoff, and each attempt is abandoned after ${Math.round(contract.requestTimeoutMs / 1000)} seconds.`,
|
|
1357
|
+
].join('\n'),
|
|
1358
|
+
'x-wildo': {
|
|
1359
|
+
resourceIdentifier: op.resourceIdentifier,
|
|
1360
|
+
operationIdentifier: op.baseOperationIdentifier,
|
|
1361
|
+
eventChannel: notification.channel,
|
|
1362
|
+
notificationIdentifier: notification.identifier,
|
|
1363
|
+
},
|
|
1364
|
+
requestBody: {
|
|
1365
|
+
required: true,
|
|
1366
|
+
content: {
|
|
1367
|
+
'application/json': bodyRef === undefined
|
|
1368
|
+
// No read response to point at — say what the body is rather than assert a shape.
|
|
1369
|
+
? { schema: { type: 'object', description: `The \`${op.resourceIdentifier}\` record as the API returns it.` } }
|
|
1370
|
+
: { schema: { $ref: bodyRef } },
|
|
1371
|
+
},
|
|
1372
|
+
},
|
|
1373
|
+
responses: {
|
|
1374
|
+
'2XX': { description: 'The delivery was accepted. Any 2xx ends the delivery; the body is ignored.' },
|
|
1375
|
+
},
|
|
1376
|
+
},
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
return webhooks;
|
|
1381
|
+
}
|
|
1117
1382
|
/**
|
|
1118
1383
|
* Builds the OpenAPI request-body `examples` map from authored examples that
|
|
1119
1384
|
* carry a `request` payload. Returns undefined when there are none.
|
|
@@ -1175,6 +1440,23 @@ function stableStringify(value) {
|
|
|
1175
1440
|
}
|
|
1176
1441
|
return JSON.stringify(value) ?? 'null';
|
|
1177
1442
|
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Which half of an operation a generated component schema models.
|
|
1445
|
+
*
|
|
1446
|
+
* A closed two-member vocabulary, named rather than inlined because the two members are not
|
|
1447
|
+
* symmetric and the difference is worth stating once: a REQUEST schema is what a caller must
|
|
1448
|
+
* SEND and is therefore the contract they are held to, while a RESPONSE schema is what the
|
|
1449
|
+
* application PROMISES to return. Both the schema NAME and its description are derived from
|
|
1450
|
+
* this value, so the member is the single input that decides how an integrator reads the card.
|
|
1451
|
+
*
|
|
1452
|
+
* The member VALUES are the PascalCase suffixes appended to a schema name (`TodosCreateRequest`),
|
|
1453
|
+
* so they are wire-visible in the published OpenAPI document and not free to rename.
|
|
1454
|
+
*/
|
|
1455
|
+
export var OpenApiComponentSchemaRole;
|
|
1456
|
+
(function (OpenApiComponentSchemaRole) {
|
|
1457
|
+
OpenApiComponentSchemaRole["REQUEST"] = "Request";
|
|
1458
|
+
OpenApiComponentSchemaRole["RESPONSE"] = "Response";
|
|
1459
|
+
})(OpenApiComponentSchemaRole || (OpenApiComponentSchemaRole = {}));
|
|
1178
1460
|
/**
|
|
1179
1461
|
* Component name for an operation's request/response body. Mirrors the
|
|
1180
1462
|
* verb-first operationId namer (shares `pascal`): `<Resource><BaseVerb><Quals><Role>`,
|
|
@@ -1195,15 +1477,84 @@ function componentSchemaName(op, role) {
|
|
|
1195
1477
|
.join('');
|
|
1196
1478
|
return `${pascal(op.resourceIdentifier)}${pascal(op.baseOperationIdentifier)}${schemaQualifiers}${role}`;
|
|
1197
1479
|
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Describe the two closed vocabularies inside the framework's own error envelope, from the
|
|
1482
|
+
* definitions the runtime already carries.
|
|
1483
|
+
*
|
|
1484
|
+
* `error.type` is the field an integrator branches on, and it arrived as twenty bare tokens with no
|
|
1485
|
+
* explanation — the ONLY enum in the document that no authoring could reach, because the sweep that
|
|
1486
|
+
* describes enum values reads a field specification and this schema belongs to no resource. The
|
|
1487
|
+
* words are not written here: `ERROR_DEFINITIONS` is a `Record<ErrorType, …>` the error builder
|
|
1488
|
+
* itself consults, so the sentence, the HTTP status and whether the framework's own strategy is to
|
|
1489
|
+
* retry all come from the same source that decides them at runtime.
|
|
1490
|
+
*
|
|
1491
|
+
* Matching is by EXACT VALUE SET, and that is only sound because it is scoped to this one
|
|
1492
|
+
* generator-owned schema. Across the document at large it would not be: `['active', 'expired',
|
|
1493
|
+
* 'inactive']` occurs on an OAuth client and on an API key with different meanings, so propagating
|
|
1494
|
+
* one description to the other by value set would put "Client may mint tokens" on an API key.
|
|
1495
|
+
*
|
|
1496
|
+
* `severity` gets one sentence rather than six invented ones: it drives the framework's own logging
|
|
1497
|
+
* and alerting, and a client has nothing to do with it.
|
|
1498
|
+
*
|
|
1499
|
+
* Returns a described COPY. The caller's schema belongs to the projection input and a pin asserts
|
|
1500
|
+
* the generator leaves its input untouched.
|
|
1501
|
+
*/
|
|
1502
|
+
function describeFrameworkErrorEnvelopeEnums(schema) {
|
|
1503
|
+
if (!isPlainObject(schema))
|
|
1504
|
+
return schema;
|
|
1505
|
+
const errorTypeValues = new Set(Object.values(ErrorType));
|
|
1506
|
+
const severityValues = new Set(Object.values(ErrorSeverity));
|
|
1507
|
+
const errorTypeMeanings = {};
|
|
1508
|
+
for (const value of Object.values(ErrorType)) {
|
|
1509
|
+
const definition = ERROR_DEFINITIONS[value];
|
|
1510
|
+
const retries = definition.errorHandlingStrategy === ErrorHandling_Strategy.RETRY;
|
|
1511
|
+
errorTypeMeanings[value] = `${definition.description} (HTTP ${String(definition.statusCode)})`
|
|
1512
|
+
+ `${retries ? ' — retrying is expected to help.' : '.'}`;
|
|
1513
|
+
}
|
|
1514
|
+
const described = structuredClone(schema);
|
|
1515
|
+
const visit = (node) => {
|
|
1516
|
+
if (Array.isArray(node)) {
|
|
1517
|
+
for (const entry of node)
|
|
1518
|
+
visit(entry);
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
if (!isPlainObject(node))
|
|
1522
|
+
return;
|
|
1523
|
+
const values = node['enum'];
|
|
1524
|
+
if (Array.isArray(values) && node['description'] === undefined) {
|
|
1525
|
+
const present = new Set(values.map((value) => String(value)));
|
|
1526
|
+
if (present.size === errorTypeValues.size && [...present].every((value) => errorTypeValues.has(value))) {
|
|
1527
|
+
node['description'] = `Which kind of failure this was. Branch on this rather than on wording.\n\nValues:\n`
|
|
1528
|
+
+ Object.entries(errorTypeMeanings).map(([value, meaning]) => `- \`${value}\`: ${meaning}`).join('\n');
|
|
1529
|
+
node['x-enum-descriptions'] = errorTypeMeanings;
|
|
1530
|
+
}
|
|
1531
|
+
else if (present.size === severityValues.size && [...present].every((value) => severityValues.has(value))) {
|
|
1532
|
+
node['description'] = 'How seriously the application itself treats this failure — it drives logging and alerting '
|
|
1533
|
+
+ 'on the server. A client has nothing to decide from it; branch on `type` and `code`.';
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
for (const entry of Object.values(node))
|
|
1537
|
+
visit(entry);
|
|
1538
|
+
};
|
|
1539
|
+
visit(described);
|
|
1540
|
+
return described;
|
|
1541
|
+
}
|
|
1198
1542
|
function createSchemaRegistry() {
|
|
1199
1543
|
const nameBySemanticIdentityAndContent = new Map();
|
|
1200
1544
|
const contentByName = new Map();
|
|
1201
1545
|
const schemas = {};
|
|
1202
1546
|
return {
|
|
1203
|
-
ref(schema, desiredName) {
|
|
1547
|
+
ref(schema, desiredName, description) {
|
|
1204
1548
|
if (!isHoistableSchema(schema)) {
|
|
1205
1549
|
return schema;
|
|
1206
1550
|
}
|
|
1551
|
+
/*
|
|
1552
|
+
* Canonicalised BEFORE the description is attached, deliberately. Deduplication is by
|
|
1553
|
+
* name-and-content, so folding the description into the identity would split one component
|
|
1554
|
+
* into several whenever two operations produced the same shape — the opposite of what a
|
|
1555
|
+
* description is for. A component name maps to exactly one desired name, hence to one
|
|
1556
|
+
* operation's resource, so the description attached here describes the schema that is stored.
|
|
1557
|
+
*/
|
|
1207
1558
|
const canonical = stableStringify(schema);
|
|
1208
1559
|
const semanticIdentityAndContent = `${desiredName}\u0000${canonical}`;
|
|
1209
1560
|
const existingName = nameBySemanticIdentityAndContent.get(semanticIdentityAndContent);
|
|
@@ -1218,7 +1569,17 @@ function createSchemaRegistry() {
|
|
|
1218
1569
|
}
|
|
1219
1570
|
nameBySemanticIdentityAndContent.set(semanticIdentityAndContent, name);
|
|
1220
1571
|
contentByName.set(name, canonical);
|
|
1221
|
-
|
|
1572
|
+
/*
|
|
1573
|
+
* COPIED, never mutated. The caller's schema object belongs to the operation projection, and
|
|
1574
|
+
* a pin asserts the generator leaves its input untouched — writing the description onto it
|
|
1575
|
+
* would have made this generator edit the data it was handed, which is the one thing that
|
|
1576
|
+
* test exists to prevent. A shallow copy suffices: only a top-level key is added, and an
|
|
1577
|
+
* authored description already on the schema outranks the derived one.
|
|
1578
|
+
*/
|
|
1579
|
+
const stored = description !== undefined && isPlainObject(schema) && schema['description'] === undefined
|
|
1580
|
+
? { ...schema, description }
|
|
1581
|
+
: schema;
|
|
1582
|
+
schemas[name] = stored;
|
|
1222
1583
|
return { $ref: `#/components/schemas/${name}` };
|
|
1223
1584
|
},
|
|
1224
1585
|
schemas() {
|
|
@@ -1234,12 +1595,29 @@ function createSchemaRegistry() {
|
|
|
1234
1595
|
* inline. All success statuses share one response schema object, so they
|
|
1235
1596
|
* resolve to the same `$ref`. Mutates `operationObject` in place.
|
|
1236
1597
|
*/
|
|
1237
|
-
|
|
1598
|
+
/**
|
|
1599
|
+
* What a component schema IS, taken from the operation that hoisted it.
|
|
1600
|
+
*
|
|
1601
|
+
* Measured on Wonder CRM: 0 of 474 component schemas carried a description, so the schema list read
|
|
1602
|
+
* as 474 names — `DealReadResponse`, `DealReadContextResponse`, `DealReadSummaryResponse` — with
|
|
1603
|
+
* nothing to tell them apart or say what a deal is.
|
|
1604
|
+
*
|
|
1605
|
+
* Derived from the OPERATION rather than from the schema's name. 473 of the 474 names do start with
|
|
1606
|
+
* their resource's tag, so a prefix match would have worked and would have been a guess: the
|
|
1607
|
+
* hoisting operation is the authoritative link, and it costs nothing to use it. The resource's own
|
|
1608
|
+
* purpose is appended when its specification authored one, because a reader who reached a schema
|
|
1609
|
+
* card may never have seen the tag.
|
|
1610
|
+
*/
|
|
1611
|
+
function describeComponentSchema(op, role, resourcePurpose) {
|
|
1612
|
+
const what = `${role === OpenApiComponentSchemaRole.REQUEST ? 'Request body' : 'Response body'} for \`${op.resourceIdentifier}.${op.baseOperationIdentifier}\`.`;
|
|
1613
|
+
return resourcePurpose === undefined ? what : `${what} ${resourcePurpose}`;
|
|
1614
|
+
}
|
|
1615
|
+
function hoistOperationSchemas(operationObject, op, registry, resourcePurpose) {
|
|
1238
1616
|
const requestBody = operationObject['requestBody'];
|
|
1239
1617
|
if (isPlainObject(requestBody) && isPlainObject(requestBody['content'])) {
|
|
1240
1618
|
const media = requestBody['content']['application/json'];
|
|
1241
1619
|
if (isPlainObject(media) && isHoistableSchema(media['schema'])) {
|
|
1242
|
-
media['schema'] = registry.ref(media['schema'], componentSchemaName(op,
|
|
1620
|
+
media['schema'] = registry.ref(media['schema'], componentSchemaName(op, OpenApiComponentSchemaRole.REQUEST), describeComponentSchema(op, OpenApiComponentSchemaRole.REQUEST, resourcePurpose));
|
|
1243
1621
|
}
|
|
1244
1622
|
}
|
|
1245
1623
|
const responses = operationObject['responses'];
|
|
@@ -1250,7 +1628,7 @@ function hoistOperationSchemas(operationObject, op, registry) {
|
|
|
1250
1628
|
}
|
|
1251
1629
|
const media = entry['content']['application/json'];
|
|
1252
1630
|
if (isPlainObject(media) && isHoistableSchema(media['schema'])) {
|
|
1253
|
-
media['schema'] = registry.ref(media['schema'], componentSchemaName(op,
|
|
1631
|
+
media['schema'] = registry.ref(media['schema'], componentSchemaName(op, OpenApiComponentSchemaRole.RESPONSE), describeComponentSchema(op, OpenApiComponentSchemaRole.RESPONSE, resourcePurpose));
|
|
1254
1632
|
}
|
|
1255
1633
|
}
|
|
1256
1634
|
}
|
|
@@ -1344,6 +1722,44 @@ function buildOpenApiDocument(section, operations, input) {
|
|
|
1344
1722
|
const sectionLabel = section === OpenApiSection.API_REFERENCE
|
|
1345
1723
|
? 'API reference'
|
|
1346
1724
|
: 'Application administration API reference';
|
|
1725
|
+
/*
|
|
1726
|
+
* Resource purposes, indexed before the operation loop so a hoisted schema can carry what its
|
|
1727
|
+
* resource IS. The tag descriptions below read the same map; building it once keeps the schema
|
|
1728
|
+
* card and the tag card quoting one string rather than two copies that can drift.
|
|
1729
|
+
*/
|
|
1730
|
+
const purposeByResourceIdentifier = new Map();
|
|
1731
|
+
for (const tag of input.resourceTags ?? []) {
|
|
1732
|
+
if (tag.description !== undefined && !purposeByResourceIdentifier.has(tag.name)) {
|
|
1733
|
+
purposeByResourceIdentifier.set(tag.name, tag.description);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
/*
|
|
1737
|
+
* The canonical route of every operation that has one, indexed before any operation object is
|
|
1738
|
+
* built. A route with no `Via` qualifier is the operation's own address; the `Via` ones are the
|
|
1739
|
+
* same operation reached through a parent.
|
|
1740
|
+
*
|
|
1741
|
+
* Indexed rather than inferred per operation, because "shortest path wins" would be a guess and
|
|
1742
|
+
* this is a fact the operation key already carries. An operation reachable ONLY through a parent
|
|
1743
|
+
* has no canonical entry, and is deliberately left unmarked: calling it an alias of nothing would
|
|
1744
|
+
* tell a reader to go somewhere that does not exist.
|
|
1745
|
+
*/
|
|
1746
|
+
const canonicalPathByRoute = new Map();
|
|
1747
|
+
for (const op of sortedOps) {
|
|
1748
|
+
if (splitOperationQualifiers(op).via.length > 0)
|
|
1749
|
+
continue;
|
|
1750
|
+
const key = canonicalRouteKey(op);
|
|
1751
|
+
if (!canonicalPathByRoute.has(key))
|
|
1752
|
+
canonicalPathByRoute.set(key, op.path);
|
|
1753
|
+
}
|
|
1754
|
+
/** Exactly the shared error responses some operation referenced, so none is emitted unused. */
|
|
1755
|
+
const usedErrorResponseNames = new Set();
|
|
1756
|
+
/**
|
|
1757
|
+
* Each resource's canonical read-response component, for the `webhooks` block to point at.
|
|
1758
|
+
*
|
|
1759
|
+
* The read operation's response IS the record a delivery carries, so this is a resolved
|
|
1760
|
+
* reference rather than a guess — and a resource with no read operation simply gets prose.
|
|
1761
|
+
*/
|
|
1762
|
+
const canonicalReadResponseRefByResource = new Map();
|
|
1347
1763
|
const paths = {};
|
|
1348
1764
|
const ownershipByPathAndVerb = new Map();
|
|
1349
1765
|
const tagSet = new Set();
|
|
@@ -1360,7 +1776,15 @@ function buildOpenApiDocument(section, operations, input) {
|
|
|
1360
1776
|
// The schema comes from the same model the backend error handler serializes.
|
|
1361
1777
|
// Register before per-operation hoisting so every non-success response shares
|
|
1362
1778
|
// the stable, named component instead of producing resource-local copies.
|
|
1363
|
-
const errorResponseSchema = schemaRegistry.ref(input.errorResponseSchema, 'ErrorResponse'
|
|
1779
|
+
const errorResponseSchema = schemaRegistry.ref(describeFrameworkErrorEnvelopeEnums(input.errorResponseSchema), 'ErrorResponse',
|
|
1780
|
+
// The one schema the GENERATOR owns rather than projects, so it was the one schema with no
|
|
1781
|
+
// description while all 473 others had one: the sweep that described component schemas words
|
|
1782
|
+
// them from the operation and the resource, and this belongs to neither.
|
|
1783
|
+
'The body returned whenever a request does not succeed. `error.type` says which kind of failure it '
|
|
1784
|
+
+ 'was and `error.code`, when present, is the stable identifier to branch on in code — branch on '
|
|
1785
|
+
+ 'those rather than on wording. `error.customMessage` is the text meant for a person, and '
|
|
1786
|
+
+ '`error.customMessageReference` is its translation key; `error.message` is the technical '
|
|
1787
|
+
+ 'explanation. `error.context` is present only on validation, conflict and rate-limit failures.');
|
|
1364
1788
|
for (const op of sortedOps) {
|
|
1365
1789
|
if (paths[op.path] === undefined) {
|
|
1366
1790
|
paths[op.path] = {};
|
|
@@ -1377,7 +1801,40 @@ function buildOpenApiDocument(section, operations, input) {
|
|
|
1377
1801
|
operationIdUseCount.set(baseOperationId, priorUses + 1);
|
|
1378
1802
|
const operationId = priorUses === 0 ? baseOperationId : `${baseOperationId}${priorUses + 1}`;
|
|
1379
1803
|
const operationObject = buildOperationObject(op, operationId, errorResponseSchema);
|
|
1380
|
-
|
|
1804
|
+
for (const response of Object.values((operationObject['responses'] ?? {}))) {
|
|
1805
|
+
const ref = isPlainObject(response) ? response['$ref'] : undefined;
|
|
1806
|
+
if (typeof ref === 'string' && ref.startsWith('#/components/responses/')) {
|
|
1807
|
+
usedErrorResponseNames.add(ref.slice('#/components/responses/'.length));
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
/*
|
|
1811
|
+
* Mark an alias route AS one, and point at the operation's own address.
|
|
1812
|
+
*
|
|
1813
|
+
* OpenAPI has no concept of "the same operation at another path", so a reader meeting
|
|
1814
|
+
* `organizationMembers` for the first time sees its whole surface three times with no way to
|
|
1815
|
+
* tell that two of them are conveniences. The description says so in words a reader acts on,
|
|
1816
|
+
* and `x-wildo.aliasOfPath` gives a renderer something to group or fold on.
|
|
1817
|
+
*
|
|
1818
|
+
* NOT `deprecated`: an alias is a supported route and a client calling it is doing nothing
|
|
1819
|
+
* wrong. Saying "prefer" would be an instruction nobody asked this generator to give.
|
|
1820
|
+
*/
|
|
1821
|
+
const canonicalPath = splitOperationQualifiers(op).via.length === 0
|
|
1822
|
+
? undefined
|
|
1823
|
+
: canonicalPathByRoute.get(canonicalRouteKey(op));
|
|
1824
|
+
if (canonicalPath !== undefined && canonicalPath !== op.path) {
|
|
1825
|
+
const wildo = operationObject['x-wildo'];
|
|
1826
|
+
if (isPlainObject(wildo))
|
|
1827
|
+
wildo['aliasOfPath'] = canonicalPath;
|
|
1828
|
+
const existing = typeof operationObject['description'] === 'string' ? operationObject['description'] : '';
|
|
1829
|
+
const note = `The same operation is published at \`${op.httpVerb.toUpperCase()} ${canonicalPath}\`. This route reaches it through its parent, which is a convenience rather than a different operation — either address does the same thing.`;
|
|
1830
|
+
operationObject['description'] = existing.length > 0 ? `${existing}\n\n${note}` : note;
|
|
1831
|
+
}
|
|
1832
|
+
hoistOperationSchemas(operationObject, op, schemaRegistry, purposeByResourceIdentifier.get(op.resourceIdentifier));
|
|
1833
|
+
if (op.baseOperationIdentifier === 'read' && splitOperationQualifiers(op).via.length === 0 && !canonicalReadResponseRefByResource.has(op.resourceIdentifier)) {
|
|
1834
|
+
const okSchema = (operationObject['responses']?.['200']?.content?.['application/json']?.schema);
|
|
1835
|
+
if (typeof okSchema?.$ref === 'string')
|
|
1836
|
+
canonicalReadResponseRefByResource.set(op.resourceIdentifier, okSchema.$ref);
|
|
1837
|
+
}
|
|
1381
1838
|
paths[op.path][op.httpVerb] = operationObject;
|
|
1382
1839
|
ownershipByPathAndVerb.set(collisionKey, `${op.resourceIdentifier}.${op.operationKey}`);
|
|
1383
1840
|
tagSet.add(op.resourceIdentifier);
|
|
@@ -1396,6 +1853,7 @@ function buildOpenApiDocument(section, operations, input) {
|
|
|
1396
1853
|
}
|
|
1397
1854
|
resourceTagsByName.set(tag.name, tag);
|
|
1398
1855
|
}
|
|
1856
|
+
const webhooks = buildWebhooksObject(sortedOps, canonicalReadResponseRefByResource);
|
|
1399
1857
|
return {
|
|
1400
1858
|
openapi: '3.1.0',
|
|
1401
1859
|
info: {
|
|
@@ -1429,6 +1887,9 @@ function buildOpenApiDocument(section, operations, input) {
|
|
|
1429
1887
|
return description !== undefined && description.length > 0 ? { ...tag, description } : tag;
|
|
1430
1888
|
}),
|
|
1431
1889
|
paths,
|
|
1890
|
+
// `webhooks` (OpenAPI 3.1) = what this application SENDS. The reference described only what a
|
|
1891
|
+
// client calls; an integrator writing the receiving endpoint had no machine-readable contract.
|
|
1892
|
+
...(Object.keys(webhooks).length > 0 ? { webhooks } : {}),
|
|
1432
1893
|
// `components.schemas` = hoisted, content-deduped request/response body
|
|
1433
1894
|
// models (named `<Resource><Verb><Quals>Request|Response`); `securitySchemes`
|
|
1434
1895
|
// = framework-universal auth, referenced by each op's `security`.
|
|
@@ -1436,6 +1897,12 @@ function buildOpenApiDocument(section, operations, input) {
|
|
|
1436
1897
|
...(Object.keys(schemaRegistry.schemas()).length > 0
|
|
1437
1898
|
? { schemas: schemaRegistry.schemas() }
|
|
1438
1899
|
: {}),
|
|
1900
|
+
// `responses` = the error responses the GENERATOR words (the access-mode 401s and the
|
|
1901
|
+
// universal 500). An authored response stays inline on its operation, because its
|
|
1902
|
+
// description is written for that one operation and is the document's most useful prose.
|
|
1903
|
+
...(usedErrorResponseNames.size > 0
|
|
1904
|
+
? { responses: buildErrorResponseComponents(usedErrorResponseNames, errorResponseSchema) }
|
|
1905
|
+
: {}),
|
|
1439
1906
|
securitySchemes: SECURITY_SCHEMES,
|
|
1440
1907
|
},
|
|
1441
1908
|
};
|