@wildo-ai/saas-technical-doc 1.1.5 → 1.1.6
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/companion/openapi-example-derivation.d.ts +53 -0
- package/dist/esm/companion/openapi-example-derivation.d.ts.map +1 -0
- package/dist/esm/companion/openapi-example-derivation.js +229 -0
- package/dist/esm/companion/openapi-example-derivation.js.map +1 -0
- package/dist/esm/companion/openapi-generator.d.ts.map +1 -1
- package/dist/esm/companion/openapi-generator.js +284 -16
- package/dist/esm/companion/openapi-generator.js.map +1 -1
- package/dist/esm/companion/operation-projection.schemas.d.ts +36 -0
- package/dist/esm/companion/operation-projection.schemas.d.ts.map +1 -1
- package/dist/esm/companion/operation-projection.schemas.js +18 -0
- package/dist/esm/companion/operation-projection.schemas.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 +20 -0
- 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 +2 -0
- package/dist/esm/companion/rendering/technical-documentation-openapi-renderer.d.ts.map +1 -1
- package/dist/esm/companion/rendering/technical-documentation-openapi-renderer.js +48 -31
- package/dist/esm/companion/rendering/technical-documentation-openapi-renderer.js.map +1 -1
- package/dist/esm/companion/spec-to-operation-doc.d.ts +52 -10
- package/dist/esm/companion/spec-to-operation-doc.d.ts.map +1 -1
- package/dist/esm/companion/spec-to-operation-doc.js +125 -7
- package/dist/esm/companion/spec-to-operation-doc.js.map +1 -1
- package/dist/esm/content/application-consumer-documentation-content.techdoc.d.ts +10 -10
- package/dist/esm/content/application-consumer-documentation-content.techdoc.d.ts.map +1 -1
- package/dist/esm/content/application-consumer-documentation-content.techdoc.js +43 -28
- package/dist/esm/content/application-consumer-documentation-content.techdoc.js.map +1 -1
- package/dist/esm/openapi/api-reference-link-index.d.ts +3 -0
- package/dist/esm/openapi/api-reference-link-index.d.ts.map +1 -1
- package/dist/esm/openapi/api-reference-link-index.js +26 -15
- package/dist/esm/openapi/api-reference-link-index.js.map +1 -1
- package/dist/esm/openapi/api-reference-pages.d.ts +55 -0
- package/dist/esm/openapi/api-reference-pages.d.ts.map +1 -0
- package/dist/esm/openapi/api-reference-pages.js +229 -0
- package/dist/esm/openapi/api-reference-pages.js.map +1 -0
- package/dist/esm/openapi/api-reference-search.d.ts +53 -0
- package/dist/esm/openapi/api-reference-search.d.ts.map +1 -0
- package/dist/esm/openapi/api-reference-search.js +100 -0
- package/dist/esm/openapi/api-reference-search.js.map +1 -0
- package/dist/esm/openapi/api-reference-targets.d.ts +11 -0
- package/dist/esm/openapi/api-reference-targets.d.ts.map +1 -1
- package/dist/esm/openapi/api-reference-targets.js +8 -0
- package/dist/esm/openapi/api-reference-targets.js.map +1 -1
- package/dist/esm/openapi/index.d.ts +2 -0
- package/dist/esm/openapi/index.d.ts.map +1 -1
- package/dist/esm/openapi/index.js +2 -0
- package/dist/esm/openapi/index.js.map +1 -1
- package/dist/esm/openapi-reference-model.exports.d.ts +2 -0
- package/dist/esm/openapi-reference-model.exports.d.ts.map +1 -1
- package/dist/esm/openapi-reference-model.exports.js +2 -0
- package/dist/esm/openapi-reference-model.exports.js.map +1 -1
- package/dist/esm/runtime/AuthExchangePage.d.ts +45 -4
- package/dist/esm/runtime/AuthExchangePage.d.ts.map +1 -1
- package/dist/esm/runtime/AuthExchangePage.js +45 -12
- package/dist/esm/runtime/AuthExchangePage.js.map +1 -1
- package/dist/esm/runtime/DocsFrontendProviders.d.ts +15 -1
- package/dist/esm/runtime/DocsFrontendProviders.d.ts.map +1 -1
- package/dist/esm/runtime/DocsFrontendProviders.js +16 -3
- package/dist/esm/runtime/DocsFrontendProviders.js.map +1 -1
- package/dist/esm/runtime/DocsProviderComponent.d.ts +41 -0
- package/dist/esm/runtime/DocsProviderComponent.d.ts.map +1 -0
- package/dist/esm/runtime/DocsProviderComponent.js +17 -0
- package/dist/esm/runtime/DocsProviderComponent.js.map +1 -0
- package/dist/esm/runtime/documentation-site-translator.d.ts +53 -0
- package/dist/esm/runtime/documentation-site-translator.d.ts.map +1 -0
- package/dist/esm/runtime/documentation-site-translator.js +51 -0
- package/dist/esm/runtime/documentation-site-translator.js.map +1 -0
- package/dist/esm/runtime/index.d.ts +6 -0
- package/dist/esm/runtime/index.d.ts.map +1 -1
- package/dist/esm/runtime/index.js +6 -0
- package/dist/esm/runtime/index.js.map +1 -1
- package/dist/esm/runtime/openapi-reference-model.d.ts +25 -0
- package/dist/esm/runtime/openapi-reference-model.d.ts.map +1 -1
- package/dist/esm/runtime/openapi-reference-model.js +82 -13
- package/dist/esm/runtime/openapi-reference-model.js.map +1 -1
- package/dist/esm/runtime/openapi-reference-navigation.d.ts +50 -0
- package/dist/esm/runtime/openapi-reference-navigation.d.ts.map +1 -0
- package/dist/esm/runtime/openapi-reference-navigation.js +46 -0
- package/dist/esm/runtime/openapi-reference-navigation.js.map +1 -0
- package/dist/esm/runtime/openapi-reference-samples.d.ts +40 -0
- package/dist/esm/runtime/openapi-reference-samples.d.ts.map +1 -0
- package/dist/esm/runtime/openapi-reference-samples.js +169 -0
- package/dist/esm/runtime/openapi-reference-samples.js.map +1 -0
- package/dist/esm/runtime/openapi-reference-styles.d.ts +28 -0
- package/dist/esm/runtime/openapi-reference-styles.d.ts.map +1 -0
- package/dist/esm/runtime/openapi-reference-styles.js +292 -0
- package/dist/esm/runtime/openapi-reference-styles.js.map +1 -0
- package/dist/esm/runtime/openapi-reference-view.d.ts +40 -5
- package/dist/esm/runtime/openapi-reference-view.d.ts.map +1 -1
- package/dist/esm/runtime/openapi-reference-view.js +828 -82
- package/dist/esm/runtime/openapi-reference-view.js.map +1 -1
- package/dist/esm/runtime/openapi-reference-words-context.d.ts +12 -0
- package/dist/esm/runtime/openapi-reference-words-context.d.ts.map +1 -0
- package/dist/esm/runtime/openapi-reference-words-context.js +30 -0
- package/dist/esm/runtime/openapi-reference-words-context.js.map +1 -0
- package/dist/esm/runtime/openapi-reference-words.d.ts +205 -0
- package/dist/esm/runtime/openapi-reference-words.d.ts.map +1 -0
- package/dist/esm/runtime/openapi-reference-words.js +162 -0
- package/dist/esm/runtime/openapi-reference-words.js.map +1 -0
- package/dist/esm/runtime/provider-component-registry.techdoc.d.ts +31 -0
- package/dist/esm/runtime/provider-component-registry.techdoc.d.ts.map +1 -0
- package/dist/esm/runtime/provider-component-registry.techdoc.js +35 -0
- package/dist/esm/runtime/provider-component-registry.techdoc.js.map +1 -0
- package/dist/esm/runtime/use-docs-provider-component.d.ts +29 -0
- package/dist/esm/runtime/use-docs-provider-component.d.ts.map +1 -0
- package/dist/esm/runtime/use-docs-provider-component.js +42 -0
- package/dist/esm/runtime/use-docs-provider-component.js.map +1 -0
- package/dist/esm/runtime/use-docs-provider-scripts.d.ts +12 -5
- package/dist/esm/runtime/use-docs-provider-scripts.d.ts.map +1 -1
- package/dist/esm/runtime/use-docs-provider-scripts.js +15 -8
- package/dist/esm/runtime/use-docs-provider-scripts.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +6 -6
|
@@ -44,10 +44,11 @@
|
|
|
44
44
|
* and the Idempotency-Key vocabulary. A module has ONE public door, so the
|
|
45
45
|
* vocabulary is not re-exported from `public-runtime` as well.
|
|
46
46
|
*/
|
|
47
|
-
import { WildoHeaderKeys } from '@wildo-ai/saas-models/public-runtime';
|
|
47
|
+
import { SUBMISSION_CHALLENGE_RESPONSE_MAX_LENGTH, SubmissionChallengeRefusalCode, SubmissionChallengeResponseKind, WildoHeaderKeys, } from '@wildo-ai/saas-models/public-runtime';
|
|
48
48
|
import { COLLECTION_QUERY_PARAMETERS, ERROR_DEFINITIONS, IDEMPOTENCY_KEY_MAX_LENGTH, IdempotencyKeyRefusalCode, ErrorHandling_Strategy, ErrorSeverity, ErrorType, M2M_WEBHOOK_DELIVERY_CONTRACT, Resources_PaginationRequestSchema, } from '@wildo-ai/saas-models';
|
|
49
49
|
import { OpenApiSection, } from '../openapi/openapi-generation-output.schemas.js';
|
|
50
50
|
import { OpenApiGenerationInputSchema, OPENAPI_RESOURCE_IDENTITY_CONTRACT_VERSION, OperationProjectionAuthenticationMode, OperationProjectionVariantType, } from './operation-projection.schemas.js';
|
|
51
|
+
import { attachDerivedExamples } from './openapi-example-derivation.js';
|
|
51
52
|
import { stripImplementationNoise } from './spec-to-operation-doc.js';
|
|
52
53
|
/**
|
|
53
54
|
* Retains every projected URL-bearing operation.
|
|
@@ -603,25 +604,47 @@ function buildCollectionFilterQueryParameters(op) {
|
|
|
603
604
|
parameter['explode'] = true;
|
|
604
605
|
}
|
|
605
606
|
parameter['schema'] = isPlainObject(propertySchema) ? propertySchema : {};
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
//
|
|
611
|
-
//
|
|
607
|
+
const fieldDescription = isPlainObject(propertySchema) && typeof propertySchema.description === 'string'
|
|
608
|
+
? propertySchema.description
|
|
609
|
+
: undefined;
|
|
610
|
+
if (isObjectValued) {
|
|
611
|
+
// The wire form of a range filter is not guessable, so its deepObject bracket
|
|
612
|
+
// hint is stated whether or not the field also carries a meaning — the field's
|
|
613
|
+
// meaning (injected from its specification) says WHAT it filters, the hint says
|
|
614
|
+
// HOW to send it, and neither replaces the other.
|
|
612
615
|
const subKeys = isPlainObject(propertySchema) && isPlainObject(propertySchema.properties)
|
|
613
616
|
? Object.keys(propertySchema.properties)
|
|
614
617
|
: [];
|
|
615
618
|
const bracketHint = subKeys.length > 0
|
|
616
619
|
? subKeys.map((subKey) => `\`${name}[${subKey}]=…\``).join(' & ')
|
|
617
620
|
: `\`${name}[<field>]=…\``;
|
|
618
|
-
|
|
619
|
-
|
|
621
|
+
const wireHint = `Object-valued filter — serialise each sub-field with deepObject bracket notation (${bracketHint}).`;
|
|
622
|
+
parameter['description'] = fieldDescription === undefined ? wireHint : `${fieldDescription}\n\n${wireHint}`;
|
|
623
|
+
}
|
|
624
|
+
else if (fieldDescription !== undefined) {
|
|
625
|
+
parameter['description'] = fieldDescription;
|
|
620
626
|
}
|
|
621
627
|
parameters.push(attachParameterExamples(op, parameter));
|
|
622
628
|
}
|
|
623
629
|
return parameters;
|
|
624
630
|
}
|
|
631
|
+
/**
|
|
632
|
+
* The error-body identifiers one documented failure mode names, each written as the field a
|
|
633
|
+
* client reads it from. `error.type`, `error.customMessageReference` and `error.code` are three
|
|
634
|
+
* different fields; the prose used to call every one of them "error code", which sent a client to
|
|
635
|
+
* `error.code` for values that arrive in the other two.
|
|
636
|
+
*/
|
|
637
|
+
function errorScenarioIdentifiers(scenario) {
|
|
638
|
+
const identifiers = [];
|
|
639
|
+
if (scenario.errorType !== undefined)
|
|
640
|
+
identifiers.push(`\`error.type\`: \`${scenario.errorType}\``);
|
|
641
|
+
if (scenario.customMessageReference !== undefined) {
|
|
642
|
+
identifiers.push(`\`error.customMessageReference\`: \`${scenario.customMessageReference}\``);
|
|
643
|
+
}
|
|
644
|
+
if (scenario.errorCode !== undefined)
|
|
645
|
+
identifiers.push(`\`error.code\`: \`${scenario.errorCode}\``);
|
|
646
|
+
return identifiers;
|
|
647
|
+
}
|
|
625
648
|
/**
|
|
626
649
|
* Framework-universal pagination defaults, mirrored from the backend
|
|
627
650
|
* repository adapters (`_doList` in both the MongoDB and PostgreSQL resource
|
|
@@ -814,6 +837,24 @@ function buildOperationObject(op, operationId, errorResponseSchema) {
|
|
|
814
837
|
if (isPlainObject(wildo))
|
|
815
838
|
wildo['m2mNotifications'] = notifications.map((notification) => ({ ...notification }));
|
|
816
839
|
}
|
|
840
|
+
/*
|
|
841
|
+
* The documented failure modes as DATA, beside the prose the error responses carry. A client, an
|
|
842
|
+
* SDK generator or the reference's errors table branches on these values; prose cannot be read
|
|
843
|
+
* that way, which is how a code used to exist only inside a sentence (#1620).
|
|
844
|
+
*/
|
|
845
|
+
const errorScenarios = op.errorScenarios ?? [];
|
|
846
|
+
if (errorScenarios.length > 0) {
|
|
847
|
+
const wildo = operationObject['x-wildo'];
|
|
848
|
+
if (isPlainObject(wildo)) {
|
|
849
|
+
wildo['errorScenarios'] = errorScenarios.map((scenario) => ({
|
|
850
|
+
status: scenario.code,
|
|
851
|
+
when: scenario.when,
|
|
852
|
+
...(scenario.errorType !== undefined && { errorType: scenario.errorType }),
|
|
853
|
+
...(scenario.customMessageReference !== undefined && { customMessageReference: scenario.customMessageReference }),
|
|
854
|
+
...(scenario.errorCode !== undefined && { errorCode: scenario.errorCode }),
|
|
855
|
+
}));
|
|
856
|
+
}
|
|
857
|
+
}
|
|
817
858
|
const security = buildOperationSecurity(op);
|
|
818
859
|
operationObject['security'] = security;
|
|
819
860
|
if (op.access.authenticationMode !== OperationProjectionAuthenticationMode.PUBLIC && op.access.roleRequirements.length > 0) {
|
|
@@ -853,6 +894,22 @@ function buildOperationObject(op, operationId, errorResponseSchema) {
|
|
|
853
894
|
+ (op.idempotencyKey.required ? ' Required: a request without it is refused.' : ' Optional: a request without it runs, but its retries are not deduplicated.'),
|
|
854
895
|
});
|
|
855
896
|
}
|
|
897
|
+
if (op.submissionChallenge !== undefined) {
|
|
898
|
+
parameters.push({
|
|
899
|
+
name: op.submissionChallenge.headerName,
|
|
900
|
+
in: 'header',
|
|
901
|
+
// Optional in the reference because it is conditional: asked only when the application declares a captcha provider,
|
|
902
|
+
// and only of a caller that is not signed in. The description states both conditions.
|
|
903
|
+
required: false,
|
|
904
|
+
schema: { type: 'string', minLength: 1, maxLength: SUBMISSION_CHALLENGE_RESPONSE_MAX_LENGTH },
|
|
905
|
+
description: 'Your answer to this application\'s submission challenge, as JSON. Asked only when the application protects its '
|
|
906
|
+
+ 'public forms with a captcha provider, and only of a caller that is not signed in; a signed-in or machine caller '
|
|
907
|
+
+ `omits it. Send either \`{"kind":"${SubmissionChallengeResponseKind.PROVIDER_TOKEN}","providerRef":"<captcha provider>",`
|
|
908
|
+
+ `"token":"<vendor token>"}\` or a solved proof of work, \`{"kind":"${SubmissionChallengeResponseKind.PROOF_OF_WORK}",`
|
|
909
|
+
+ `"challenge":{…},"number":<n>}\`, for a challenge fetched from `
|
|
910
|
+
+ `GET ${op.submissionChallenge.proofOfWorkChallengePath}. Each proof of work is accepted once.`,
|
|
911
|
+
});
|
|
912
|
+
}
|
|
856
913
|
if (op.acceptsIfMatch) {
|
|
857
914
|
parameters.push({
|
|
858
915
|
name: WildoHeaderKeys.IF_MATCH,
|
|
@@ -1003,6 +1060,10 @@ const FRAMEWORK_OWNED_PROPERTY_DESCRIPTIONS = Object.freeze({
|
|
|
1003
1060
|
createdAt: 'When the record was created, set by the application and never accepted from a client.',
|
|
1004
1061
|
updatedAt: 'When the record last changed, set by the application on every write.',
|
|
1005
1062
|
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.',
|
|
1063
|
+
applicationId: 'The application this record belongs to, for a record scoped to the application rather than to one organization.',
|
|
1064
|
+
// The USER scope foreign key `declarePolymorphicSchemas` injects on a user-scoped variant, as `organizationId` is for
|
|
1065
|
+
// an organization-scoped one (#1625).
|
|
1066
|
+
userId: 'The user this record belongs to, for a record scoped to one user rather than to an organization.',
|
|
1006
1067
|
// retention-authority: N/A — an API consumer's ENGLISH description of what the two values mean,
|
|
1007
1068
|
// rendered into the OpenAPI document. There is no emitter to consume here: the audience is a
|
|
1008
1069
|
// person reading a schema, and the sentence has to say `active` and `retained` in words for the
|
|
@@ -1012,6 +1073,29 @@ const FRAMEWORK_OWNED_PROPERTY_DESCRIPTIONS = Object.freeze({
|
|
|
1012
1073
|
data: 'The page of records this response carries.',
|
|
1013
1074
|
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.',
|
|
1014
1075
|
});
|
|
1076
|
+
/**
|
|
1077
|
+
* The four fields of the `pagination` block every collection response carries (#1625). One shape,
|
|
1078
|
+
* built by `createPaginationResponseMetadataSchema` and owned by no resource, so it was 272
|
|
1079
|
+
* undescribed properties in Wonder Todos' API document alone — the same four, once per list.
|
|
1080
|
+
*
|
|
1081
|
+
* Applied only INSIDE a property named `pagination`, where these names mean exactly this; a
|
|
1082
|
+
* resource's own `page` or `total` field is never touched. `totalPages` is `0` for an empty result on
|
|
1083
|
+
* every adapter, computed by one function since #1714.
|
|
1084
|
+
*/
|
|
1085
|
+
const PAGINATION_PROPERTY_DESCRIPTIONS = Object.freeze({
|
|
1086
|
+
page: 'The page this response carries, counted from 1.',
|
|
1087
|
+
limit: 'How many records a page holds: the `limit` the request asked for, or the default when it asked for none.',
|
|
1088
|
+
total: 'How many records match the request, across every page.',
|
|
1089
|
+
totalPages: 'How many pages of `limit` records the matching set spans: `0` when nothing matches. Ask for pages 1 to `totalPages`.',
|
|
1090
|
+
});
|
|
1091
|
+
/** An authored description always wins; these fill gaps. */
|
|
1092
|
+
function fillMissingDescriptions(properties, descriptions) {
|
|
1093
|
+
for (const [name, description] of Object.entries(descriptions)) {
|
|
1094
|
+
const property = properties[name];
|
|
1095
|
+
if (isPlainObject(property) && property['description'] === undefined)
|
|
1096
|
+
property['description'] = description;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1015
1099
|
/**
|
|
1016
1100
|
* The structural signature of a MATERIALIZED resource document in a response
|
|
1017
1101
|
* schema is the `_id` property. The serializer attaches `_version` /
|
|
@@ -1102,6 +1186,9 @@ function fillFrameworkOwnedPropertyDescriptions(node) {
|
|
|
1102
1186
|
if (framework !== undefined && property['description'] === undefined) {
|
|
1103
1187
|
property['description'] = framework;
|
|
1104
1188
|
}
|
|
1189
|
+
if (name === 'pagination' && isPlainObject(property['properties'])) {
|
|
1190
|
+
fillMissingDescriptions(property['properties'], PAGINATION_PROPERTY_DESCRIPTIONS);
|
|
1191
|
+
}
|
|
1105
1192
|
fillFrameworkOwnedPropertyDescriptions(property);
|
|
1106
1193
|
}
|
|
1107
1194
|
}
|
|
@@ -1193,9 +1280,8 @@ function buildResponsesObject(op, responseBodySchema, errorResponseSchema) {
|
|
|
1193
1280
|
responses['204'] = { description: 'No Content' };
|
|
1194
1281
|
}
|
|
1195
1282
|
for (const error of op.errorScenarios ?? []) {
|
|
1196
|
-
const
|
|
1197
|
-
|
|
1198
|
-
: error.when;
|
|
1283
|
+
const identifiers = errorScenarioIdentifiers(error);
|
|
1284
|
+
const description = identifiers.length > 0 ? `${error.when} (${identifiers.join(', ')})` : error.when;
|
|
1199
1285
|
const errorJsonMedia = { schema: errorResponseSchema };
|
|
1200
1286
|
const examples = responseExamplesByStatus.get(error.code);
|
|
1201
1287
|
if (examples !== undefined) {
|
|
@@ -1231,6 +1317,12 @@ function buildResponsesObject(op, responseBodySchema, errorResponseSchema) {
|
|
|
1231
1317
|
mergeFrameworkError('400', `The \`${op.idempotencyKey.headerName}\` header is malformed (\`error.code\` \`${IdempotencyKeyRefusalCode.MALFORMED}\`)`
|
|
1232
1318
|
+ (op.idempotencyKey.required ? `, or missing (\`${IdempotencyKeyRefusalCode.REQUIRED}\`).` : '.'));
|
|
1233
1319
|
}
|
|
1320
|
+
if (op.submissionChallenge !== undefined) {
|
|
1321
|
+
mergeFrameworkError('400', `The application protects this operation with a submission challenge and a caller who is not signed in sent no \``
|
|
1322
|
+
+ `${op.submissionChallenge.headerName}\` header (\`error.code\` \`${SubmissionChallengeRefusalCode.REQUIRED}\`), or an answer `
|
|
1323
|
+
+ `it did not accept: a refused vendor token, or a proof of work that is wrong, expired or already used `
|
|
1324
|
+
+ `(\`${SubmissionChallengeRefusalCode.REFUSED}\`). Fetch a fresh proof-of-work challenge and send the request again.`);
|
|
1325
|
+
}
|
|
1234
1326
|
if (op.acceptsIfMatch) {
|
|
1235
1327
|
mergeFrameworkError('400', 'The optional `If-Match` header is present but does not contain a non-negative integer resource version.');
|
|
1236
1328
|
mergeFrameworkError('409', 'The `If-Match` version no longer matches the current resource version, so the mutation was not applied.');
|
|
@@ -1289,6 +1381,17 @@ const GENERATOR_OWNED_ERROR_RESPONSE_NAMES = Object.freeze({
|
|
|
1289
1381
|
[OperationProjectionAuthenticationMode.PUBLIC]: 'Unauthorized',
|
|
1290
1382
|
});
|
|
1291
1383
|
const INTERNAL_ERROR_RESPONSE_NAME = 'InternalServerError';
|
|
1384
|
+
/** The status each shared `components.responses` entry answers with, read by the example pass. */
|
|
1385
|
+
function errorStatusByComponentResponseName() {
|
|
1386
|
+
return new Map([
|
|
1387
|
+
[INTERNAL_ERROR_RESPONSE_NAME, '500'],
|
|
1388
|
+
...[
|
|
1389
|
+
OperationProjectionAuthenticationMode.ANONYMOUS_SESSION,
|
|
1390
|
+
OperationProjectionAuthenticationMode.ANONYMOUS_OR_AUTHENTICATED,
|
|
1391
|
+
OperationProjectionAuthenticationMode.AUTHENTICATED,
|
|
1392
|
+
].map((mode) => [unauthorizedResponseName(mode), '401']),
|
|
1393
|
+
]);
|
|
1394
|
+
}
|
|
1292
1395
|
function unauthorizedResponseName(mode) {
|
|
1293
1396
|
return GENERATOR_OWNED_ERROR_RESPONSE_NAMES[mode];
|
|
1294
1397
|
}
|
|
@@ -1316,14 +1419,16 @@ function unauthorizedDescription(mode) {
|
|
|
1316
1419
|
* entry of its own: a public operation never gets a synthesised 401.
|
|
1317
1420
|
*/
|
|
1318
1421
|
function buildErrorResponseComponents(usedNames, errorResponseSchema) {
|
|
1319
|
-
|
|
1320
|
-
|
|
1422
|
+
// One content object PER entry: the example pass writes each one's own example into it, and a
|
|
1423
|
+
// shared object would make the last write every entry's example.
|
|
1424
|
+
const content = () => ({ 'application/json': { schema: errorResponseSchema } });
|
|
1425
|
+
const all = { [INTERNAL_ERROR_RESPONSE_NAME]: { description: internalServerErrorDescription(), content: content() } };
|
|
1321
1426
|
for (const mode of [
|
|
1322
1427
|
OperationProjectionAuthenticationMode.ANONYMOUS_SESSION,
|
|
1323
1428
|
OperationProjectionAuthenticationMode.ANONYMOUS_OR_AUTHENTICATED,
|
|
1324
1429
|
OperationProjectionAuthenticationMode.AUTHENTICATED,
|
|
1325
1430
|
]) {
|
|
1326
|
-
all[unauthorizedResponseName(mode)] = { description: unauthorizedDescription(mode), content };
|
|
1431
|
+
all[unauthorizedResponseName(mode)] = { description: unauthorizedDescription(mode), content: content() };
|
|
1327
1432
|
}
|
|
1328
1433
|
return Object.fromEntries(Object.entries(all).filter(([name]) => usedNames.has(name)));
|
|
1329
1434
|
}
|
|
@@ -1521,6 +1626,44 @@ function componentSchemaName(op, role) {
|
|
|
1521
1626
|
* Returns a described COPY. The caller's schema belongs to the projection input and a pin asserts
|
|
1522
1627
|
* the generator leaves its input untouched.
|
|
1523
1628
|
*/
|
|
1629
|
+
/**
|
|
1630
|
+
* The error envelope's remaining fields, by their path under the envelope root (#1625). `type` and
|
|
1631
|
+
* `severity` are described from `ERROR_DEFINITIONS` below; these are the rest of what
|
|
1632
|
+
* `ErrorHandlerService.sendErrorResponse` writes, and each sentence states what that method puts
|
|
1633
|
+
* there. `message` and `customMessage` both carry the error's message reference, not prose: the
|
|
1634
|
+
* words are the client's to render in its reader's language.
|
|
1635
|
+
*/
|
|
1636
|
+
const ERROR_ENVELOPE_PROPERTY_DESCRIPTIONS = Object.freeze({
|
|
1637
|
+
'error': 'What went wrong. The application\'s error responses carry this one object.',
|
|
1638
|
+
'error.message': 'The message reference for this failure, the same value as `customMessageReference`. It is a key for a message catalog, not a sentence to show a reader.',
|
|
1639
|
+
'error.customMessage': 'The message reference for this failure, the same value as `message` and `customMessageReference`.',
|
|
1640
|
+
'error.customMessageReference': 'A stable key naming this failure more precisely than `type` (for example `controller_resource_not_found`). Use it to choose the message you show.',
|
|
1641
|
+
'error.code': 'A stable, operation-specific refusal code such as `LAST_ORGANIZATION_OWNER`, present when the operation declares one. Branch on this for the refusals an operation documents.',
|
|
1642
|
+
'error.operationPath': 'Which operation refused the request, as the application names it internally. Useful in a support request; do not branch on it.',
|
|
1643
|
+
'error.correlationId': 'An identifier for this request. Quote it in a support request: it joins this response to the application\'s own logs and audit records.',
|
|
1644
|
+
'error.details': 'Extra facts about the failure that are safe to return. Present only for some failures; the keys depend on the failure.',
|
|
1645
|
+
'error.details.versionConflict': 'Present when an update was refused because the record changed since it was read. Re-read the record, reapply the change, and retry.',
|
|
1646
|
+
'error.details.versionConflict.currentVersion': 'The version the record is at now.',
|
|
1647
|
+
'error.details.versionConflict.expectedVersion': 'The version the request was based on.',
|
|
1648
|
+
'error.details.versionConflict.conflictingFields': 'The field paths the request submitted whose value differs from the record as it is now.',
|
|
1649
|
+
'error.details.versionConflict.currentValues': 'The record as it is now, in the shape a read returns.',
|
|
1650
|
+
'error.details.versionConflict.submittedValues': 'The values the request submitted.',
|
|
1651
|
+
'error.timestamp': 'When the failure happened (ISO 8601, UTC).',
|
|
1652
|
+
'error.initiatorIds': 'Who the application understood the request to come from: the organization, user or application identifiers it resolved. Absent or `null` when it resolved none.',
|
|
1653
|
+
'error.validationErrors': 'Present on a validation failure: each key is a field path and each value is either a message or a `{ code, params }` entry naming the problem, so a form can show it beside the field.',
|
|
1654
|
+
'error.userGuidance': 'Present when the application can say how to resolve the failure: an explanation, a resolution and, where it applies, a next action.',
|
|
1655
|
+
'error.userGuidance.guidanceKey': 'A stable key for this guidance, to look up localized wording. When absent, use `customMessageReference`.',
|
|
1656
|
+
'error.userGuidance.explanation': 'What went wrong, in the application\'s own words.',
|
|
1657
|
+
'error.userGuidance.resolution': 'What to do about it.',
|
|
1658
|
+
'error.userGuidance.params': 'Values the localized guidance wording interpolates.',
|
|
1659
|
+
'error.userGuidance.completedSteps': 'Steps that had already completed before the failure.',
|
|
1660
|
+
'error.userGuidance.nextAction': 'The suggested next step.',
|
|
1661
|
+
'error.userGuidance.nextAction.label': 'A short label for the next step.',
|
|
1662
|
+
'error.userGuidance.nextAction.ref': 'The operation path or URL the next step goes to.',
|
|
1663
|
+
'error.userGuidance.nextAction.type': 'Whether `ref` names an operation or a URL.',
|
|
1664
|
+
'error.userGuidance.retryAfterSeconds': 'On a rate limit: how many seconds to wait before retrying.',
|
|
1665
|
+
'error.userGuidance.missingConfiguration': 'On a configuration failure: the configuration the application is missing.',
|
|
1666
|
+
});
|
|
1524
1667
|
function describeFrameworkErrorEnvelopeEnums(schema) {
|
|
1525
1668
|
if (!isPlainObject(schema))
|
|
1526
1669
|
return schema;
|
|
@@ -1559,6 +1702,27 @@ function describeFrameworkErrorEnvelopeEnums(schema) {
|
|
|
1559
1702
|
visit(entry);
|
|
1560
1703
|
};
|
|
1561
1704
|
visit(described);
|
|
1705
|
+
const describeProperties = (node, prefix) => {
|
|
1706
|
+
if (!isPlainObject(node))
|
|
1707
|
+
return;
|
|
1708
|
+
for (const key of ['anyOf', 'oneOf', 'allOf']) {
|
|
1709
|
+
const branches = node[key];
|
|
1710
|
+
if (Array.isArray(branches))
|
|
1711
|
+
for (const branch of branches)
|
|
1712
|
+
describeProperties(branch, prefix);
|
|
1713
|
+
}
|
|
1714
|
+
const properties = node['properties'];
|
|
1715
|
+
if (!isPlainObject(properties))
|
|
1716
|
+
return;
|
|
1717
|
+
for (const [name, property] of Object.entries(properties)) {
|
|
1718
|
+
const path = prefix === '' ? name : `${prefix}.${name}`;
|
|
1719
|
+
const description = ERROR_ENVELOPE_PROPERTY_DESCRIPTIONS[path];
|
|
1720
|
+
if (isPlainObject(property) && description !== undefined && property['description'] === undefined)
|
|
1721
|
+
property['description'] = description;
|
|
1722
|
+
describeProperties(property, path);
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
describeProperties(described, '');
|
|
1562
1726
|
return described;
|
|
1563
1727
|
}
|
|
1564
1728
|
function createSchemaRegistry() {
|
|
@@ -1609,6 +1773,91 @@ function createSchemaRegistry() {
|
|
|
1609
1773
|
},
|
|
1610
1774
|
};
|
|
1611
1775
|
}
|
|
1776
|
+
const COMPONENT_SCHEMA_REF_PREFIX = '#/components/schemas/';
|
|
1777
|
+
/**
|
|
1778
|
+
* One object definition per resource: "the Tasks object" (#1621).
|
|
1779
|
+
*
|
|
1780
|
+
* Every operation hoists its own named body component, so a resource's representation used to be
|
|
1781
|
+
* published once per operation that returns it. Measured on the Wonder Todos document of 2026-09-24:
|
|
1782
|
+
* 135 response components and 80 list-row schemas were byte-identical to their resource's read
|
|
1783
|
+
* representation, each carrying the same field documentation — 215 copies, and no single place a
|
|
1784
|
+
* reader could learn what a task IS.
|
|
1785
|
+
*
|
|
1786
|
+
* This pass names each resource's canonical read representation after the resource (`Tasks`) and
|
|
1787
|
+
* folds into it every response component OF THAT RESOURCE whose content is identical (ignoring the
|
|
1788
|
+
* per-operation description), plus every list's `data[]` rows that are. A context or summary shape
|
|
1789
|
+
* that differs keeps its own component: identity is decided by CONTENT, never assumed from the verb.
|
|
1790
|
+
*
|
|
1791
|
+
* Folding removes per-operation component names, which are public identities. That is safe because
|
|
1792
|
+
* no authored guide links to a component schema (guides link resources and operation families), and
|
|
1793
|
+
* the link index fails publication closed if one ever does.
|
|
1794
|
+
*
|
|
1795
|
+
* Mutates `paths`, `schemas` and `canonicalReadResponseRefByResource` (so the webhooks block, which
|
|
1796
|
+
* sends the resource document itself, references the object). Returns the object name per resource.
|
|
1797
|
+
*/
|
|
1798
|
+
function consolidateResourceObjects(paths, schemas, canonicalReadResponseRefByResource, responseComponentOwner, purposeByResourceIdentifier) {
|
|
1799
|
+
const contentOf = (schema) => {
|
|
1800
|
+
if (!isPlainObject(schema))
|
|
1801
|
+
return stableStringify(schema);
|
|
1802
|
+
const { description: _description, $schema: _dialect, ...content } = schema;
|
|
1803
|
+
return stableStringify(content);
|
|
1804
|
+
};
|
|
1805
|
+
const objectNameByResource = new Map();
|
|
1806
|
+
const replacement = new Map();
|
|
1807
|
+
const objectContentByName = new Map();
|
|
1808
|
+
for (const [resourceIdentifier, readRef] of [...canonicalReadResponseRefByResource.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
1809
|
+
const readSchema = schemas[readRef.slice(COMPONENT_SCHEMA_REF_PREFIX.length)];
|
|
1810
|
+
if (!isPlainObject(readSchema))
|
|
1811
|
+
continue;
|
|
1812
|
+
let objectName = pascal(resourceIdentifier);
|
|
1813
|
+
for (let suffix = 2; schemas[objectName] !== undefined; suffix += 1)
|
|
1814
|
+
objectName = `${pascal(resourceIdentifier)}Object${suffix === 2 ? '' : suffix}`;
|
|
1815
|
+
const purpose = purposeByResourceIdentifier.get(resourceIdentifier);
|
|
1816
|
+
const { description: _description, ...content } = readSchema;
|
|
1817
|
+
schemas[objectName] = {
|
|
1818
|
+
...content,
|
|
1819
|
+
description: `The \`${resourceIdentifier}\` resource as the API returns it.${purpose === undefined ? '' : ` ${stripImplementationNoise(purpose)}`}`,
|
|
1820
|
+
};
|
|
1821
|
+
const objectContent = contentOf(readSchema);
|
|
1822
|
+
objectNameByResource.set(resourceIdentifier, objectName);
|
|
1823
|
+
objectContentByName.set(objectName, objectContent);
|
|
1824
|
+
for (const [ref, owner] of responseComponentOwner) {
|
|
1825
|
+
if (owner !== resourceIdentifier)
|
|
1826
|
+
continue;
|
|
1827
|
+
const name = ref.slice(COMPONENT_SCHEMA_REF_PREFIX.length);
|
|
1828
|
+
const candidate = schemas[name];
|
|
1829
|
+
if (!isPlainObject(candidate))
|
|
1830
|
+
continue;
|
|
1831
|
+
if (contentOf(candidate) === objectContent) {
|
|
1832
|
+
replacement.set(ref, `${COMPONENT_SCHEMA_REF_PREFIX}${objectName}`);
|
|
1833
|
+
continue;
|
|
1834
|
+
}
|
|
1835
|
+
const data = isPlainObject(candidate['properties']) ? candidate['properties']['data'] : undefined;
|
|
1836
|
+
if (isPlainObject(data) && data['type'] === 'array' && contentOf(data['items']) === objectContent) {
|
|
1837
|
+
data['items'] = { $ref: `${COMPONENT_SCHEMA_REF_PREFIX}${objectName}` };
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
canonicalReadResponseRefByResource.set(resourceIdentifier, `${COMPONENT_SCHEMA_REF_PREFIX}${objectName}`);
|
|
1841
|
+
}
|
|
1842
|
+
for (const ref of replacement.keys())
|
|
1843
|
+
delete schemas[ref.slice(COMPONENT_SCHEMA_REF_PREFIX.length)];
|
|
1844
|
+
const rewrite = (node) => {
|
|
1845
|
+
if (Array.isArray(node)) {
|
|
1846
|
+
for (const entry of node)
|
|
1847
|
+
rewrite(entry);
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
if (!isPlainObject(node))
|
|
1851
|
+
return;
|
|
1852
|
+
const ref = node['$ref'];
|
|
1853
|
+
if (typeof ref === 'string' && replacement.has(ref))
|
|
1854
|
+
node['$ref'] = replacement.get(ref);
|
|
1855
|
+
for (const entry of Object.values(node))
|
|
1856
|
+
rewrite(entry);
|
|
1857
|
+
};
|
|
1858
|
+
rewrite(paths);
|
|
1859
|
+
return objectNameByResource;
|
|
1860
|
+
}
|
|
1612
1861
|
/**
|
|
1613
1862
|
* Replace an operation object's inline request/response BODY schemas with
|
|
1614
1863
|
* `$ref`s into `components/schemas` (via `registry`). Walks only the
|
|
@@ -1782,6 +2031,9 @@ function buildOpenApiDocument(section, operations, input) {
|
|
|
1782
2031
|
* reference rather than a guess — and a resource with no read operation simply gets prose.
|
|
1783
2032
|
*/
|
|
1784
2033
|
const canonicalReadResponseRefByResource = new Map();
|
|
2034
|
+
// The resource each response component was hoisted for, so the resource-object pass below only
|
|
2035
|
+
// ever folds a resource's OWN representations into its object.
|
|
2036
|
+
const responseComponentOwner = new Map();
|
|
1785
2037
|
const paths = {};
|
|
1786
2038
|
const ownershipByPathAndVerb = new Map();
|
|
1787
2039
|
const tagSet = new Set();
|
|
@@ -1794,6 +2046,9 @@ function buildOpenApiDocument(section, operations, input) {
|
|
|
1794
2046
|
// ones get `2`, `3`, … — applied here rather than in the projector because
|
|
1795
2047
|
// uniqueness is a per-document (per-section) property.
|
|
1796
2048
|
const operationIdUseCount = new Map();
|
|
2049
|
+
// Each published operation id's projection, so the example pass can compose an error body from
|
|
2050
|
+
// that operation's own documented scenarios.
|
|
2051
|
+
const projectionByOperationId = new Map();
|
|
1797
2052
|
const schemaRegistry = createSchemaRegistry();
|
|
1798
2053
|
// The schema comes from the same model the backend error handler serializes.
|
|
1799
2054
|
// Register before per-operation hoisting so every non-success response shares
|
|
@@ -1823,6 +2078,7 @@ function buildOpenApiDocument(section, operations, input) {
|
|
|
1823
2078
|
operationIdUseCount.set(baseOperationId, priorUses + 1);
|
|
1824
2079
|
const operationId = priorUses === 0 ? baseOperationId : `${baseOperationId}${priorUses + 1}`;
|
|
1825
2080
|
const operationObject = buildOperationObject(op, operationId, errorResponseSchema);
|
|
2081
|
+
projectionByOperationId.set(operationId, op);
|
|
1826
2082
|
for (const response of Object.values((operationObject['responses'] ?? {}))) {
|
|
1827
2083
|
const ref = isPlainObject(response) ? response['$ref'] : undefined;
|
|
1828
2084
|
if (typeof ref === 'string' && ref.startsWith('#/components/responses/')) {
|
|
@@ -1852,6 +2108,13 @@ function buildOpenApiDocument(section, operations, input) {
|
|
|
1852
2108
|
operationObject['description'] = existing.length > 0 ? `${existing}\n\n${note}` : note;
|
|
1853
2109
|
}
|
|
1854
2110
|
hoistOperationSchemas(operationObject, op, schemaRegistry, purposeByResourceIdentifier.get(op.resourceIdentifier));
|
|
2111
|
+
for (const response of Object.values((operationObject['responses'] ?? {}))) {
|
|
2112
|
+
const media = isPlainObject(response) && isPlainObject(response['content']) ? response['content']['application/json'] : undefined;
|
|
2113
|
+
const ref = isPlainObject(media) && isPlainObject(media['schema']) ? media['schema']['$ref'] : undefined;
|
|
2114
|
+
if (typeof ref === 'string' && ref.startsWith(COMPONENT_SCHEMA_REF_PREFIX) && !responseComponentOwner.has(ref)) {
|
|
2115
|
+
responseComponentOwner.set(ref, op.resourceIdentifier);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
1855
2118
|
if (op.baseOperationIdentifier === 'read' && splitOperationQualifiers(op).via.length === 0 && !canonicalReadResponseRefByResource.has(op.resourceIdentifier)) {
|
|
1856
2119
|
const okSchema = (operationObject['responses']?.['200']?.content?.['application/json']?.schema);
|
|
1857
2120
|
if (typeof okSchema?.$ref === 'string')
|
|
@@ -1875,8 +2138,9 @@ function buildOpenApiDocument(section, operations, input) {
|
|
|
1875
2138
|
}
|
|
1876
2139
|
resourceTagsByName.set(tag.name, tag);
|
|
1877
2140
|
}
|
|
2141
|
+
const objectSchemaNameByResource = consolidateResourceObjects(paths, schemaRegistry.schemas(), canonicalReadResponseRefByResource, responseComponentOwner, purposeByResourceIdentifier);
|
|
1878
2142
|
const webhooks = buildWebhooksObject(sortedOps, canonicalReadResponseRefByResource);
|
|
1879
|
-
|
|
2143
|
+
const document = {
|
|
1880
2144
|
openapi: '3.1.0',
|
|
1881
2145
|
info: {
|
|
1882
2146
|
title: `${titleBase} ${sectionLabel}`,
|
|
@@ -1900,6 +2164,7 @@ function buildOpenApiDocument(section, operations, input) {
|
|
|
1900
2164
|
...(descriptor?.lifecycleRole === undefined ? {} : { lifecycleRole: stripImplementationNoise(descriptor.lifecycleRole) }),
|
|
1901
2165
|
...(descriptor?.relationships === undefined ? {} : { relationships: descriptor.relationships }),
|
|
1902
2166
|
...(descriptor?.category === undefined ? {} : { category: descriptor.category }),
|
|
2167
|
+
...(objectSchemaNameByResource.has(tagName) ? { objectSchema: objectSchemaNameByResource.get(tagName) } : {}),
|
|
1903
2168
|
};
|
|
1904
2169
|
const tag = {
|
|
1905
2170
|
name: tagName,
|
|
@@ -1928,6 +2193,9 @@ function buildOpenApiDocument(section, operations, input) {
|
|
|
1928
2193
|
securitySchemes: SECURITY_SCHEMES,
|
|
1929
2194
|
},
|
|
1930
2195
|
};
|
|
2196
|
+
// Every JSON body with no authored example gets a derived one, marked as generated (#1619).
|
|
2197
|
+
attachDerivedExamples(document, projectionByOperationId, errorStatusByComponentResponseName());
|
|
2198
|
+
return document;
|
|
1931
2199
|
}
|
|
1932
2200
|
/**
|
|
1933
2201
|
* Counts the unique `resourceIdentifier` values among a set of
|