diff --git a/demo/apis.json b/demo/apis.json index 8bb3941..d134377 100644 --- a/demo/apis.json +++ b/demo/apis.json @@ -27,5 +27,7 @@ "APIC-671/APIC-671.yaml": { "type": "OAS 3.0", "mime": "application/yaml" }, "APIC-743/APIC-743.yaml": { "type": "OAS 3.0", "mime": "application/yaml" }, "shopper-products/shopper-products.yaml": { "type": "OAS 3.0", "mime": "application/yaml" }, - "W-12142859/W-12142859.yaml": "OAS 2.0" + "W-12142859/W-12142859.yaml": "OAS 2.0", + "nulleable/nulleable.yaml": "OAS 3.0", + "nullable-test/nullable-test.yaml": { "type": "OAS 3.0", "mime": "application/yaml" } } diff --git a/demo/index.js b/demo/index.js index 8d50262..94d02a8 100644 --- a/demo/index.js +++ b/demo/index.js @@ -99,6 +99,8 @@ class ApiDemo extends ApiDemoPage { _apiListTemplate() { return [ + ['nullable-test', 'Nullable Test (Comprehensive)'], + ['nulleable', 'Nulleable test'], ['grpc-test', 'GRPC test'], ['shopper-products', 'shopper-products'], ['demo-api', 'Demo API'], diff --git a/demo/nullable-test/nullable-test.yaml b/demo/nullable-test/nullable-test.yaml new file mode 100644 index 0000000..c1e2993 --- /dev/null +++ b/demo/nullable-test/nullable-test.yaml @@ -0,0 +1,340 @@ +openapi: 3.0.0 +info: + title: Nullable Types Test API + description: Comprehensive test for nullable rendering optimization + version: 1.0.0 + +paths: + /test: + post: + summary: Test endpoint + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TestRequest' + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/TestResponse' + +components: + schemas: + # ============================================ + # SCALAR NULLABLES (Should show "Type or null") + # ============================================ + ScalarNullables: + type: object + description: All scalar types with nullable true - should render as "Type or null" + properties: + nullableString: + type: string + nullable: true + description: Should show "String or null" + example: "hello" + + nullableInteger: + type: integer + nullable: true + description: Should show "Integer or null" + example: 42 + + nullableNumber: + type: number + nullable: true + description: Should show "Number or null" + example: 3.14 + + nullableBoolean: + type: boolean + nullable: true + description: Should show "Boolean or null" + example: true + + nullableDateTime: + type: string + format: date-time + nullable: true + description: Should show "DateTime or null" + example: "2026-01-02T10:30:00Z" + + nullableDate: + type: string + format: date + nullable: true + description: Should show "Date or null" + example: "2026-01-02" + + nullableEmail: + type: string + format: email + nullable: true + description: Should show "String or null" (with format email) + example: "test@example.com" + + nullableUuid: + type: string + format: uuid + nullable: true + description: Should show "String or null" (with format uuid) + example: "550e8400-e29b-41d4-a716-446655440000" + + # ============================================ + # COMPLEX NULLABLES (Should keep Union selector) + # ============================================ + ComplexNullables: + type: object + description: Complex types with nullable - should render as Union + properties: + nullableArray: + type: array + nullable: true + description: Should show "Array or null" + items: + type: string + example: ["item1", "item2"] + + nullableObject: + type: object + nullable: true + description: Should show "Object or null" + properties: + name: + type: string + example: "John Doe" + age: + type: integer + example: 30 + example: + name: "Jane Smith" + age: 25 + + nullableArrayOfObjects: + type: array + nullable: true + description: Should show "Array or null" + items: + type: object + properties: + id: + type: string + example: "uuid-123" + value: + type: number + example: 42.5 + example: + - id: "obj-1" + value: 10.5 + - id: "obj-2" + value: 20.3 + + # ============================================ + # REAL UNIONS (Should keep Union selector) + # ============================================ + RealUnions: + type: object + description: Real unions that are not just nullable + properties: + stringOrNumber: + description: Real union - should show Union selector + oneOf: + - type: string + - type: number + + multipleTypes: + description: Real union with 3+ types - should show Union selector + oneOf: + - type: string + - type: integer + - type: boolean + + objectOrString: + description: Real union - should show Union selector + anyOf: + - type: object + properties: + data: + type: string + - type: string + + # ============================================ + # NESTED NULLABLES + # ============================================ + NestedNullables: + type: object + description: Objects with nested nullable properties + properties: + user: + type: object + properties: + id: + type: string + description: Required user ID + email: + type: string + nullable: true + description: Optional email - should show "String or null" + phoneNumber: + type: string + nullable: true + description: Optional phone - should show "String or null" + metadata: + type: object + nullable: true + description: Optional metadata object - should show Union + properties: + created: + type: string + format: date-time + lastLogin: + type: string + format: date-time + nullable: true + required: + - id + + # ============================================ + # ARRAYS WITH NULLABLE ITEMS + # ============================================ + ArraysWithNullableItems: + type: object + description: Arrays containing nullable items + properties: + arrayOfNullableStrings: + type: array + description: Array of strings where each item can be null + items: + type: string + nullable: true + + arrayOfNullableObjects: + type: array + description: Array of objects where each object can be null + items: + type: object + nullable: true + properties: + name: + type: string + value: + type: number + + # ============================================ + # EDGE CASES + # ============================================ + EdgeCases: + type: object + description: Edge cases and special scenarios + properties: + enumNullable: + type: string + enum: ["option1", "option2", "option3"] + nullable: true + description: Enum with nullable - should show "String or null" + + numberWithConstraints: + type: number + minimum: 0 + maximum: 100 + nullable: true + description: Number with constraints and nullable - should show "Number or null" + + stringWithPattern: + type: string + pattern: "^[A-Z]{3}$" + nullable: true + description: String with pattern and nullable - should show "String or null" + + readOnlyNullable: + type: string + nullable: true + readOnly: true + description: Read-only nullable field - should show "String or null" + + deprecatedNullable: + type: integer + nullable: true + deprecated: true + description: Deprecated nullable field - should show "Integer or null" + + # ============================================ + # COMBINED REQUEST/RESPONSE + # ============================================ + TestRequest: + type: object + required: + - userId + - action + properties: + userId: + type: string + description: Required user ID + action: + type: string + enum: ["create", "update", "delete"] + timezone: + type: string + nullable: true + description: Client timezone (nullable) - should show "String or null" + example: "America/Los_Angeles" + metadata: + type: object + nullable: true + description: Optional metadata (nullable object) - should show "Object or null" + properties: + source: + type: string + example: "web-app" + timestamp: + type: string + format: date-time + example: "2026-01-02T10:30:00Z" + tags: + type: array + nullable: true + description: Optional tags (nullable array) - should show "Array or null" + items: + type: string + example: + userId: "user-123" + action: "create" + timezone: "America/Los_Angeles" + metadata: + source: "mobile-app" + timestamp: "2026-01-02T15:45:30Z" + tags: ["tag1", "tag2", "tag3"] + + TestResponse: + type: object + properties: + success: + type: boolean + data: + type: object + nullable: true + description: Response data (nullable object) - should show "Object or null" + properties: + id: + type: string + status: + type: string + message: + type: string + nullable: true + description: Optional message - should show "String or null" + errorCode: + type: integer + nullable: true + description: Optional error code - should show "Integer or null" + example: + success: true + data: + id: "result-456" + status: "completed" + message: "Operation successful" + errorCode: null + diff --git a/demo/nulleable/nulleable.yaml b/demo/nulleable/nulleable.yaml new file mode 100644 index 0000000..8684f67 --- /dev/null +++ b/demo/nulleable/nulleable.yaml @@ -0,0 +1,1632 @@ +openapi: 3.0.0 + +info: + title: Einstein Bots API (BETA) + version: 'v5' + +servers: + - url: https://runtime-api-na-west.prod.chatbots.sfdc.sh + description: Einstein Bots API - NA West + - url: https://runtime-api-na-east.prod.chatbots.sfdc.sh + description: Einstein Bots API - NA East + - url: https://runtime-api-eu-west.prod.chatbots.sfdc.sh + description: Einstein Bots API - EU West + - url: https://runtime-api-eu-east.prod.chatbots.sfdc.sh + description: Einstein Bots API - EU East + - url: https://runtime-api-ap-west.prod.chatbots.sfdc.sh + description: Einstein Bots API - AP West + - url: https://runtime-api-ap-east.prod.chatbots.sfdc.sh + description: Einstein Bots API - AP East + +paths: + /status: + get: + tags: + - 'health' + summary: 'Check the health status' + description: 'Returns whether the status of Einstein Bots API is up or down.' + operationId: 'getHealthStatus' + responses: + '200': + $ref: '#/components/responses/StatusResponse' + default: + $ref: '#/components/responses/ErrorResponse' + + /v5.0.0/bots/{bot-id}/sessions: + post: + tags: + - 'bot' + summary: 'Start a session' + description: 'Send a message to the bots API to start a new session.' + operationId: 'startSession' + security: + - chatbotAuth: ['chatbot_api'] + - jwtBearer: [] + parameters: + - in: 'path' + name: 'bot-id' + description: 'ID of the Einstein Bot that you want to interact with.' + required: true + schema: + type: 'string' + - in: 'header' + name: 'X-Org-Id' + description: '18-character org ID of the Salesforce org associated with the bot.' + example: '00Dx0000000ALskEAG' + required: true + schema: + type: 'string' + allowEmptyValue: false + - in: 'header' + name: 'X-Request-ID' + description: 'Request ID. A UUID in string format created by you to help with request tracking.' + example: '36a73651-a46d-4d16-9a8a-fd436ed62e1a' + required: true + schema: + type: 'string' + requestBody: + description: 'Request payload to initiate a session.' + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/InitMessageEnvelope' + responses: + '200': + $ref: '#/components/responses/SuccessfulResponse' + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + '422': + $ref: '#/components/responses/RequestProcessingException' + '423': + $ref: '#/components/responses/ServerBusyError' + '429': + $ref: '#/components/responses/TooManyRequestsError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + default: + $ref: '#/components/responses/ErrorResponse' + + /v5.0.0/sessions/{session-id}/messages: + post: + tags: + - 'bot' + summary: 'Continue an active session' + description: 'Send a message to the bots API on an active session.' + operationId: 'continueSession' + security: + - chatbotAuth: ['chatbot_api'] + - jwtBearer: [] + parameters: + - in: 'path' + name: 'session-id' + description: 'ID of an active session. This is the `sessionId` returned by the bot when you start a session.' + required: true + schema: + type: 'string' + - in: 'header' + name: 'X-Org-Id' + description: '18-character org ID of the Salesforce org associated with the bot.' + example: '00Dx0000000ALskEAG' + required: true + schema: + type: 'string' + # allowEmptyValue: false + - in: 'header' + name: 'X-Request-ID' + description: 'Request ID. A UUID in string format created by you to help with request tracking.' + example: '36a73651-a46d-4d16-9a8a-fd436ed62e1a' + required: true + schema: + type: 'string' + - in: 'header' + name: 'X-Runtime-CRC' + description: 'Internal runtime CRC. This is the `X-Runtime-CRC` value from previous response.' + schema: + type: 'string' + requestBody: + description: 'Request payload to continue the chat.' + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChatMessageEnvelope' + responses: + '200': + $ref: '#/components/responses/SuccessfulChatMessageResponse' + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + '422': + $ref: '#/components/responses/RequestProcessingException' + '423': + $ref: '#/components/responses/ServerBusyError' + '429': + $ref: '#/components/responses/TooManyRequestsError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + default: + $ref: '#/components/responses/ErrorResponse' + + /v5.0.0/sessions/{session-id}: + delete: + tags: + - 'bot' + summary: 'End an active session' + description: 'Send a message to the bots API to end a session' + operationId: 'endSession' + security: + - chatbotAuth: ['chatbot_api'] + - jwtBearer: [] + parameters: + - in: 'path' + name: 'session-id' + description: 'ID of an active session. This is the `sessionId` returned by the bot when you start a session.' + required: true + schema: + type: 'string' + - in: 'header' + name: 'X-Org-Id' + description: '18-character org ID of the Salesforce org associated with the bot.' + example: '00Dx0000000ALskEAG' + required: true + schema: + type: 'string' + allowEmptyValue: false + - in: 'header' + name: 'X-Session-End-Reason' + description: 'Reason sesssion ended.' + example: 'Transfer' + required: true + schema: + $ref: '#/components/schemas/EndSessionReason' + allowEmptyValue: false + - in: 'header' + name: 'X-Request-ID' + description: 'Request ID. A UUID in string format created by you to help with request tracking.' + example: '36a73651-a46d-4d16-9a8a-fd436ed62e1a' + required: true + schema: + type: 'string' + - in: 'header' + name: 'X-Runtime-CRC' + description: 'Internal runtime CRC. This is the `X-Runtime-CRC` value from previous response.' + schema: + type: 'string' + responses: + '200': + $ref: '#/components/responses/SuccessfulChatMessageResponse' + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + '422': + $ref: '#/components/responses/RequestProcessingException' + '423': + $ref: '#/components/responses/ServerBusyError' + '429': + $ref: '#/components/responses/TooManyRequestsError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + default: + $ref: '#/components/responses/ErrorResponse' + +components: + securitySchemes: + chatbotAuth: + type: oauth2 + flows: + authorizationCode: + authorizationUrl: https://login.salesforce.com/services/oauth2/authorize + tokenUrl: https://login.salesforce.com/services/oauth2/token + scopes: + chatbot_api: 'Access Chatbot Apis' + implicit: + authorizationUrl: https://login.salesforce.com/services/oauth2/authorize + scopes: + chatbot_api: 'Access Chatbot Apis' + jwtBearer: + type: http + scheme: bearer + description: 'Salesforce OAuth access token obtained using the JWT Bearer flow. Use the `chatbot_api` scope with this flow to enable access to the Einstein Bots services.' + + schemas: + ResponseSessionId: + description: 'Bot runtime session ID.' + type: 'string' + example: '57904eb6-5352-4c5e-adf6-5f100572cf5d' + nullable: false + + ExternalSessionKey: + description: "UUID in string format for the conversation. You can use this parameter to trace the conversation in your bot's event logs." + type: 'string' + example: '57904eb6-5352-4c5e-adf6-5f100572cf5d' + nullable: false + + BotVersion: + description: 'Bot configuration version.' + type: 'string' + example: '0X9RM0000004CD00AM' + + InitMessageEnvelope: + type: 'object' + properties: + externalSessionKey: + $ref: '#/components/schemas/ExternalSessionKey' + message: + $ref: '#/components/schemas/TextInitMessage' + forceConfig: + $ref: '#/components/schemas/ForceConfig' + responseOptions: + $ref: '#/components/schemas/ResponseOptions' + tz: + description: 'Client timezone where the customer starts the chat.' + type: 'string' + nullable: true + example: 'America/Los_Angeles' + variables: + $ref: '#/components/schemas/Variables' + referrers: + description: 'Array of referrers of transferring the session to another bot.' + type: 'array' + items: + $ref: '#/components/schemas/Referrer' + nullable: true + richContentCapabilities: + $ref: '#/components/schemas/RichContentCapability' + required: + - 'forceConfig' + - 'externalSessionKey' + additionalProperties: false + + RichContentCapability: + description: 'Rich content configuration supported by the client.' + type: 'object' + properties: + messageTypes: + description: 'Array of message types supporting rich content and their output formats.' + type: 'array' + minimum: 1 + items: + $ref: '#/components/schemas/MessageTypeCapability' + required: + - messageTypes + + MessageTypeCapability: + type: 'object' + properties: + messageType: + $ref: '#/components/schemas/MessageType' + formatTypes: + description: 'Array of output format types.' + type: 'array' + minimum: 1 + items: + $ref: '#/components/schemas/FormatTypeCapability' + required: + - messageType + - formatTypes + + FormatTypeCapability: + type: 'object' + properties: + formatType: + $ref: '#/components/schemas/FormatType' + required: + - formatType + + FormatType: + description: | + Message format that is supported by the message type. + + * `Attachments`—Shows the attachment as a downloadable file (rich content). + * `text`—Shows file as a URL link. Use this type if your client doesn’t support rich content. + type: 'string' + enum: ['Attachments', 'Text'] + example: 'Attachments' + + MessageType: + description: | + Message type that supports rich content output formats. + + * `StaticContentMessage`—Represents static or non-interactive content, such as links and attachments. + type: 'string' + enum: ['StaticContentMessage'] + example: 'StaticContentMessage' + + ChatMessageEnvelope: + type: 'object' + properties: + message: + description: 'Message to the bot.' + oneOf: + - $ref: '#/components/schemas/ChoiceMessage' + - $ref: '#/components/schemas/TextMessage' + - $ref: '#/components/schemas/TransferSucceededRequestMessage' + - $ref: '#/components/schemas/TransferFailedRequestMessage' + - $ref: '#/components/schemas/RedirectMessage' + responseOptions: + $ref: '#/components/schemas/ResponseOptions' + required: + - 'message' + additionalProperties: false + + ResponseEnvelope: + type: 'object' + properties: + sessionId: + $ref: '#/components/schemas/ResponseSessionId' + botVersion: + $ref: '#/components/schemas/BotVersion' + processedSequenceIds: + description: 'Sequence IDs of messages that the bot processed. Except for the initial request to start the session, all requests require a `sequenceId`.' + type: 'array' + items: + format: 'int64' + type: 'integer' + example: 1 + messages: + description: 'Messages from the bot.' + type: 'array' + minimum: 0 + items: + oneOf: + - $ref: '#/components/schemas/TextResponseMessage' + - $ref: '#/components/schemas/ChoicesResponseMessage' + - $ref: '#/components/schemas/StaticContentMessage' + - $ref: '#/components/schemas/EscalateResponseMessage' + - $ref: '#/components/schemas/SessionEndedResponseMessage' + variables: + $ref: '#/components/schemas/Variables' + metrics: + description: 'Session metrics.' + type: 'object' + additionalProperties: true + intents: + $ref: '#/components/schemas/Intents' + entities: + $ref: '#/components/schemas/Entities' + _links: + $ref: '#/components/schemas/Links' + required: + - 'sessionId' + - 'botVersion' + - 'processedSequenceIds' + - 'messages' + - '_links' + additionalProperties: false + + ChatMessageResponseEnvelope: + type: 'object' + properties: + botVersion: + $ref: '#/components/schemas/BotVersion' + processedSequenceIds: + description: 'Sequence IDs of processed messages.' + type: 'array' + items: + format: 'int64' + type: 'integer' + example: 1, 2 + messages: + description: 'Messages from the bot.' + type: 'array' + minimum: 0 + items: + oneOf: + - $ref: '#/components/schemas/TextResponseMessage' + - $ref: '#/components/schemas/ChoicesResponseMessage' + - $ref: '#/components/schemas/StaticContentMessage' + - $ref: '#/components/schemas/EscalateResponseMessage' + - $ref: '#/components/schemas/SessionEndedResponseMessage' + variables: + $ref: '#/components/schemas/Variables' + metrics: + description: 'Session metrics.' + type: 'object' + additionalProperties: true + intents: + $ref: '#/components/schemas/Intents' + entities: + $ref: '#/components/schemas/Entities' + _links: + $ref: '#/components/schemas/Links' + required: + - 'botVersion' + - 'processedSequenceIds' + - 'messages' + - '_links' + additionalProperties: false + + StaticContentMessage: + type: 'object' + properties: + type: + description: 'Message type of message with static (non-interactive) content.' + type: 'string' + enum: ['messageDefinition'] + id: + $ref: '#/components/schemas/MessageId' + schedule: + $ref: '#/components/schemas/Schedule' + messageType: + $ref: '#/components/schemas/MessageType' + references: + type: 'array' + description: 'Array of Salesforce records.' + items: + $ref: '#/components/schemas/RecordReference' + staticContent: + description: 'Static content, such as files, delivered to the client.' + anyOf: + - $ref: '#/components/schemas/StaticContentAttachments' + - $ref: '#/components/schemas/StaticContentText' + required: + - 'id' + - 'type' + - 'messageType' + - 'staticContent' + + StaticContentText: + type: 'object' + properties: + formatType: + type: 'string' + description: 'Format type of files that appear as a URL. The customer needs to go to the URL to access the file. This is in text format.' + example: 'Text' + enum: ['Text'] + text: + $ref: '#/components/schemas/Text' + required: + - 'formatType' + - 'text' + + StaticContentAttachments: + type: 'object' + properties: + formatType: + type: 'string' + description: 'Format type of files that appear as downloadable attachments. This is in rich content format.' + example: 'Attachments' + enum: ['Attachments'] + attachments: + type: 'array' + description: 'Array of file attachments with a message.' + minItems: 1 + items: + $ref: '#/components/schemas/Attachment' + required: + - 'formatType' + - 'attachments' + + Attachment: + type: 'object' + properties: + id: + $ref: '#/components/schemas/AttachmentId' + name: + type: 'string' + description: 'Name of the file sent as an attachment.' + mimeType: + $ref: '#/components/schemas/MimeType' + url: + $ref: '#/components/schemas/Url' + referenceId: + $ref: '#/components/schemas/ReferenceId' + required: + - id + - name + - mimeType + - url + additionalProperties: false + + Text: + description: 'URL where the customer can access the file.' + type: 'string' + + MimeType: + description: 'Mime type of the attachment.' + type: 'string' + + Url: + description: 'URL to the file.' + type: 'string' + + RecordId: + type: 'string' + description: 'ID of an Salesforce record.' + example: '00P2S000005qKg1' + + ReferenceId: + type: 'string' + description: 'ID of the reference to the static content record.' + example: 'cbd4e3fe-f210-43b8-a4b7-dd77e39ee5e1' + + AttachmentId: + type: 'string' + description: 'ID of the attachment.' + example: 'cbd4e3fe-f210-43b8-a4b7-dd77e39ee5e1' + + RecordReference: + type: 'object' + properties: + id: + $ref: '#/components/schemas/ReferenceId' + recordId: + $ref: '#/components/schemas/RecordId' + required: + - 'id' + - 'recordId' + additionalProperties: false + + MessageId: + description: 'UUID that references this message.' + type: 'string' + example: 'a133c185-73a7-4adf-b6d9-b7fd62babb4e' + + SequenceId: + description: 'Client generated sequence number of the message in a session. Increase this number for each subsequent message.' + format: 'int64' + type: 'integer' + example: 1 + + Intents: + description: | + Array of the intents detected by the bot when processing the request. For a bot, "intent" refers to the goal the customer + (see [Use Intents to Understand Your Customers](https://help.salesforce.com/s/articleView?id=sf.bots_service_train_bot.htm&type=5&_ga=2.5901875.1281711304.1657558186-1967832163.1652719780)). + type: 'array' + minimum: 0 + items: + $ref: '#/components/schemas/NormalizedIntent' + + Entities: + description: 'Array of all entities detected by the bot when processing this request. For a bot, "Entities" refer to the modifiers the customer uses to describe their issue and intent is what they really mean.' + type: 'array' + minimum: 0 + items: + $ref: '#/components/schemas/NormalizedEntity' + + NormalizedIntent: + type: 'object' + properties: + label: + description: 'Name or ID of the intent.' + type: 'string' + confidenceScore: + description: 'Probability number that the AI assigns to this intent.' + type: 'number' + intentSource: + $ref: '#/components/schemas/IntentSource' + required: + - 'label' + - 'confidenceScore' + - 'intentSource' + additionalProperties: false + + NormalizedEntity: + type: 'object' + properties: + confidenceScore: + description: 'Probability number that the AI assigns to this entity.' + type: 'number' + value: + description: 'Value of the entity that the customer enters.' + type: 'string' + example: 'TBD' + type: + $ref: '#/components/schemas/EntityType' + entitySource: + $ref: '#/components/schemas/EntitySource' + required: + - 'confidenceScore' + - 'value' + - 'type' + additionalProperties: false + + EntityType: + description: 'System or custom entity Type ([What’s an Entity?](https://help.salesforce.com/s/articleView?id=sf.bots_service_whats_an_entity.htm&type=5)).' + type: 'string' + example: 'TBD' + + IntentSource: + description: | + Prediction system that detects the intent. + + * `DIRECT`—Utterance search or an exact match + * `CORE_NLP`—Third-party Natural Language Processing (NLP) + * `EINSTEIN_NLP`—Einstein NLP + type: 'string' + enum: + - 'DIRECT' + - 'CORE_NLP' + - 'EINSTEIN_NLP' + + EntitySource: + description: | + "Prediction system that detects the entity. The Bot API can call NLPs or it can figure out the entity by itself." + + * `BOT_SERVICE`—Bot detects entity without Natural Language Processing (NLP) + * `CORE_NLP`—Third-party NLP + * `EINSTEIN_NLP`—Einstein NLP + type: 'string' + enum: + - 'BOT_SERVICE' + - 'CORE_NLP' + - 'EINSTEIN_NLP' + + Links: + description: 'List of Einstein Bots API endpoints for HATEOS compliancy.' + type: 'object' + properties: + self: + $ref: '#/components/schemas/SelfLink' + messages: + $ref: '#/components/schemas/MessageLink' + session: + $ref: '#/components/schemas/EndSessionLink' + required: + - 'self' + additionalProperties: false + + SelfLink: + description: 'Endpoint of this request.' + type: 'object' + properties: + href: + description: 'URL of the endpoint.' + type: 'string' + example: 'https://runtime-api-ap-east.prod.chatbots.sfdc.sh/v5.0.0/bots/{bot-id}/sessions' + + MessageLink: + description: 'Endpoint to continue an active session.' + type: 'object' + properties: + href: + description: 'URL of the endpoint.' + type: 'string' + example: 'https://runtime-api-ap-east.prod.chatbots.sfdc.sh/v5.0.0/sessions/{session-id}/messages' + + EndSessionLink: + description: 'Endpoint to end a session.' + type: 'object' + properties: + href: + description: 'URL of the endpoint.' + type: 'string' + example: 'https://runtime-api-ap-east.prod.chatbots.sfdc.sh/v5.0.0/sessions/{session-id}' + + InReplyToMessageId: + description: 'Message ID of the previous response you are replying to.' + type: 'string' + example: 'a133c185-73a7-4adf-b6d9-b7fd62babb4e' + + BooleanVariable: + type: 'object' + properties: + name: + description: 'API name of a variable defined in the bot definition.' + type: 'string' + example: 'isShipped' + type: + description: 'Variable type.' + type: 'string' + enum: ['boolean'] + example: 'boolean' + value: + description: 'Variable value.' + type: 'boolean' + nullable: true + example: true + required: + - 'name' + - 'type' + - 'value' + additionalProperties: false + + DateVariable: + type: 'object' + properties: + name: + description: 'API name of a variable defined in the bot definition.' + type: 'string' + example: 'orderDate' + type: + description: 'Variable type.' + type: 'string' + enum: ['date'] + example: 'date' + value: + description: "Variable value in format ISO_LOCAL_DATE 'YYYY-MM-DD'." + type: 'string' + nullable: true + example: '2021-09-21' + required: + - 'name' + - 'type' + - 'value' + additionalProperties: false + + DateTimeVariable: + type: 'object' + properties: + name: + description: 'API name of a variable defined in the bot definition.' + type: 'string' + example: 'orderDateTime' + type: + description: 'Variable type.' + type: 'string' + enum: ['dateTime'] + example: 'dateTime' + value: + description: "Variable value in format ISO_LOCAL_DATE_TIME 'YYYY-MM-DDTHH:MM:SS'." + type: 'string' + nullable: true + example: '2018-09-21T14:30:00' + required: + - 'name' + - 'type' + - 'value' + additionalProperties: false + + MoneyVariable: + type: 'object' + properties: + name: + description: 'API name of a variable defined in the bot definition.' + type: 'string' + example: 'orderAmount' + type: + description: 'Variable type.' + type: 'string' + enum: ['money'] + example: 'money' + value: + description: 'Variable value in format $3_letter_currency_code $amount.' + type: 'string' + nullable: true + example: 'USD 10.40' + required: + - 'name' + - 'type' + - 'value' + additionalProperties: false + + NumberVariable: + type: 'object' + properties: + name: + description: 'API name of a variable defined in the bot definition.' + type: 'string' + example: 'qty' + type: + description: 'Variable type.' + type: 'string' + enum: ['number'] + example: 'number' + value: + description: 'Variable value.' + type: 'number' + nullable: true + example: 10 + required: + - 'name' + - 'type' + - 'value' + additionalProperties: false + + TextVariable: + type: 'object' + properties: + name: + description: 'API name of a variable defined in the bot definition.' + type: 'string' + example: 'note' + type: + description: 'Variable type.' + type: 'string' + enum: ['text'] + example: 'text' + value: + description: 'Variable value.' + type: 'string' + nullable: true + example: 'Thanks for your order!' + required: + - 'name' + - 'type' + - 'value' + additionalProperties: false + + ObjectVariable: + type: 'object' + properties: + name: + description: 'API name of a variable defined in the bot definition.' + type: 'string' + example: 'contact' + type: + description: 'Variable type.' + type: 'string' + enum: ['object'] + example: 'object' + value: + description: 'Variable value.' + nullable: true + allOf: + - $ref: '#/components/schemas/Variables' + example: [{ 'name': 'fullName', 'type': 'text', 'value': 'Matt Smith' }] + required: + - 'name' + - 'type' + - 'value' + additionalProperties: false + + RefVariable: + type: 'object' + properties: + name: + description: 'API name of a variable defined in the bot definition.' + type: 'string' + example: 'target' + type: + description: 'Variable type.' + type: 'string' + enum: ['ref'] + example: 'ref' + value: + description: 'Variable value.' + type: 'string' + nullable: true + example: '1M5xx000000000BCAQ' + required: + - 'name' + - 'type' + - 'value' + additionalProperties: false + + ListVariable: + type: 'object' + properties: + name: + description: 'API name of a variable defined in the bot definition.' + type: 'string' + example: 'target' + type: + description: 'Variable type.' + type: 'string' + enum: ['list'] + example: 'list' + value: + description: 'Array of objects with variable type and value.' + type: 'array' + nullable: true + example: [{ 'type': 'ref', 'value': '1M5xx000000000BCAQ' }] + items: + type: 'object' + properties: + type: + description: 'Variable type in bot definition.' + type: 'string' + example: 'ref' + value: + description: 'Variable value.' + type: 'string' + example: '1M5xx000000000BCAQ' + required: + - 'name' + - 'type' + - 'value' + additionalProperties: false + + Variables: + type: 'array' + description: 'Array of custom and context variables passed to the bot when initiating a session (see [What’s a Variable?](https://help.salesforce.com/s/articleView?id=sf.bots_service_whats_a_variable.htm&type=5&_ga=2.50520745.1281711304.1657558186-1967832163.1652719780)).' + items: + anyOf: + - $ref: '#/components/schemas/BooleanVariable' + - $ref: '#/components/schemas/DateVariable' + - $ref: '#/components/schemas/DateTimeVariable' + - $ref: '#/components/schemas/MoneyVariable' + - $ref: '#/components/schemas/NumberVariable' + - $ref: '#/components/schemas/TextVariable' + - $ref: '#/components/schemas/ObjectVariable' + - $ref: '#/components/schemas/RefVariable' + - $ref: '#/components/schemas/ListVariable' + nullable: true + + Referrer: + type: 'object' + properties: + type: + description: 'Referrer type.' + type: 'string' + enum: ['Salesforce:Core:Bot:Id', 'Salesforce:BotRuntime:Session:Id'] + example: 'Salesforce:Core:Bot:Id' + value: + type: 'string' + description: 'ID of referrer.' + example: 'string' + required: + - 'type' + - 'value' + + TransferFailedRequestMessage: + type: 'object' + properties: + type: + description: 'Message type for the client to inform the bot that the client failed to transfer the session to another target.' + type: 'string' + enum: ['transferFailed'] + example: 'transferFailed' + sequenceId: + $ref: '#/components/schemas/SequenceId' + inReplyToMessageId: + $ref: '#/components/schemas/InReplyToMessageId' + reason: + description: 'Reason for the failed transfer.' + type: 'string' + enum: ['NoAgentAvailable', 'Error'] + example: 'NoAgentAvailable' + description: + description: 'Description of why the transfer failed.' + type: 'string' + nullable: true + required: + - 'type' + - 'sequenceId' + - 'reason' + additionalProperties: false + + TransferSucceededRequestMessage: + type: 'object' + properties: + type: + description: 'Message type for the client to inform the bot that the client successfully transfered the session to another target.' + type: 'string' + enum: ['transferSucceeded'] + example: 'transferSucceeded' + sequenceId: + $ref: '#/components/schemas/SequenceId' + inReplyToMessageId: + $ref: '#/components/schemas/InReplyToMessageId' + required: + - 'type' + - 'sequenceId' + additionalProperties: false + + TextInitMessage: + type: 'object' + properties: + text: + description: 'Initial message from the customer to the bot to begin a session.' + type: 'string' + example: 'Hello' + additionalProperties: false + + # Used in private spec only + EndSessionMessage: + type: 'object' + description: 'Client request to end the session.' + properties: + type: + description: 'Message type for the client requesting the session to end.' + type: 'string' + enum: ['endSession'] + example: 'endSession' + sequenceId: + $ref: '#/components/schemas/SequenceId' + inReplyToMessageId: + $ref: '#/components/schemas/InReplyToMessageId' + reason: + $ref: '#/components/schemas/EndSessionReason' + required: + - 'type' + - 'sequenceId' + - 'reason' + additionalProperties: false + + EndSessionReason: + type: 'string' + enum: + - 'UserRequest' + - 'Transfer' + - 'Expiration' + - 'Error' + - 'Other' + example: 'Transfer' + nullable: false + + TextMessage: + type: 'object' + properties: + type: + description: 'Message type for replying to the bot through text.' + type: 'string' + enum: ['text'] + example: 'text' + sequenceId: + $ref: '#/components/schemas/SequenceId' + inReplyToMessageId: + $ref: '#/components/schemas/InReplyToMessageId' + text: + description: 'Text reply to the bot. You can answer a question with choices by typing the `choiceLabel` in a text message, instead of clicking an answer in the widget.' + type: 'string' + required: + - 'type' + - 'sequenceId' + - 'text' + additionalProperties: false + + ChoiceMessage: + type: 'object' + properties: + type: + description: 'Message type for answering a question with choices from the bot.' + type: 'string' + enum: ['choice'] + example: 'choice' + sequenceId: + $ref: '#/components/schemas/SequenceId' + inReplyToMessageId: + $ref: '#/components/schemas/InReplyToMessageId' + choiceIndex: + description: 'Index or alias of the chosen answer. The index starts with 1. Either `choiceIndex` or `choiceId` is required.' + type: 'integer' + minimum: 1 + example: 1 + nullable: true + choiceId: + description: 'ID of the answer when you answer by clicking a choice button. Either `choiceIndex` or `choiceId` is required.' + type: 'string' + example: '8a9a745f-0c09-4b13-955c-1ab9e06c7ad7' + nullable: true + required: + - 'type' + - 'sequenceId' + additionalProperties: false + + RedirectMessage: + type: 'object' + properties: + type: + description: 'Message type for the redirecting the client to another bot flow.' + type: 'string' + enum: ['redirect'] + example: 'redirect' + sequenceId: + $ref: '#/components/schemas/SequenceId' + dialogId: + type: 'string' + description: 'Dialog ID to redirect to.' + example: '68f934fb-e022-37a7-612e-b74fc87191d9' + required: + - 'type' + - 'sequenceId' + - 'dialogId' + additionalProperties: false + + Schedule: + type: 'object' + properties: + responseDelayMilliseconds: + description: "Delay in ms to display the bot's message to the user. This parameter is set by the bot admin when creating the bot." + type: 'integer' + format: 'int32' + example: 1200 + required: + - 'responseDelayMilliseconds' + additionalProperties: false + + SessionEndedResponseMessage: + type: 'object' + properties: + type: + description: 'Message type informing that the session ended.' + type: 'string' + enum: ['sessionEnded'] + example: 'sessionEnded' + id: + $ref: '#/components/schemas/MessageId' + reason: + description: 'Reason the session ended.' + type: 'string' + enum: + [ + 'ClientRequest', + 'TransferFailedNotConfigured', + 'Action', + 'Error', + 'InfiniteLoopDetected', + ] + example: 'ClientRequest' + nullable: false + schedule: + $ref: '#/components/schemas/Schedule' + required: + - 'type' + - 'id' + - 'reason' + additionalProperties: false + + TextResponseMessage: + type: 'object' + properties: + type: + description: 'Message type of a text message to the customer.' + type: 'string' + enum: ['text'] + example: 'text' + id: + $ref: '#/components/schemas/MessageId' + text: + description: 'Message from the bot.' + type: 'string' + example: 'Hello world!' + schedule: + $ref: '#/components/schemas/Schedule' + required: + - 'type' + - 'id' + - 'text' + additionalProperties: false + + ChoicesResponseMessage: + type: 'object' + properties: + type: + description: 'Message type of a question with choices.' + type: 'string' + enum: ['choices'] + example: 'choices' + id: + $ref: '#/components/schemas/MessageId' + choices: + description: 'Array of answers to choose from.' + type: 'array' + minItems: 1 + items: + type: 'object' + description: 'Choice' + properties: + label: + description: 'Choice label.' + type: 'string' + example: 'Order Status' + alias: + description: 'Choice alias, for example, a number to represent the choice.' + type: 'string' + example: '1' + nullable: true + id: + description: 'Choice ID.' + type: 'string' + example: '8a9a745f-0c09-4b13-955c-1ab9e06c7ad7' + required: + - 'label' + - 'id' + widget: + description: | + "Widget type of how answers appear in the client. + + * `buttons`—Answers appear in clickable pill boxes. + * `menu`—Answers appear in clickable boxes vertically stacked." + type: 'string' + enum: ['buttons', 'menu'] + example: 'buttons' + schedule: + $ref: '#/components/schemas/Schedule' + required: + - 'type' + - 'id' + - 'choices' + - 'widget' + additionalProperties: false + + EscalateResponseMessage: + type: 'object' + properties: + type: + description: 'Message type of an escalation message.' + type: 'string' + enum: ['escalate'] + example: 'escalate' + id: + $ref: '#/components/schemas/MessageId' + schedule: + $ref: '#/components/schemas/Schedule' + targets: + type: 'array' + minimum: 0 + description: 'Array of transfer targets.' + items: + type: 'object' + properties: + type: + type: 'string' + description: Type of the transfer target. + enum: + [ + 'Salesforce:Core:Bot:Id', + 'Salesforce:Core:Queue:Id', + 'Salesforce:Core:Skill:Id', + 'Salesforce:Core:Flow:Id', + ] + example: 'Salesforce:Core:Bot:Id' + value: + type: 'string' + description: 'ID of the transfer target.' + required: + - 'value' + - 'type' + nullable: false + additionalProperties: false + nullable: false + required: + - 'type' + - 'id' + - 'targets' + additionalProperties: false + + ForceConfig: + type: 'object' + description: 'API configuration parameters.' + properties: + endpoint: + description: 'Instance URL of your Salesforce org. You can find the value in the **ForceConfig Endpoint** field of the **Add Connection** dialog when you add the connected app to the bot (see [Get Started with Einstein Bot API](https://developer.salesforce.com/docs/service/einstein-bot-api/guide/prerequisites.html)).' + type: 'string' + example: 'https://d5e000009s7bceah-dev-ed.my.salesforce.com/' + required: + - 'endpoint' + additionalProperties: true + + ResponseOptions: + type: 'object' + description: 'Configuration of additional information returned in the response payload.' + properties: + variables: + $ref: '#/components/schemas/ResponseOptionsVariables' + metrics: + type: 'boolean' + description: 'Indicates whether or not to include metrics in the response.' + intents: + type: 'boolean' + description: 'Indicates whether or not to include all intents detected when processing the current request.' + entities: + type: 'boolean' + description: 'Indicates whether or not to include all entities detected when processing the current request.' + additionalProperties: false + + ResponseOptionsVariables: + type: 'object' + description: 'Variables returned in the response.' + properties: + include: + type: 'boolean' + example: true + description: 'Indicates whether or not to include variables in the response.' + filter: + type: 'array' + example: ['OrderQty', 'OrderType'] + description: 'Array of variable names to limit the returned variables. If missing, null, or empty, no filtering is applied.' + items: + type: 'string' + description: 'Variable name to filter the returned variables on.' + example: 'OrderQty' + nullable: true + onlyChanged: + type: 'boolean' + example: true + description: 'Indicates whether or not the response contains only changed variables.' + required: + - 'include' + - 'onlyChanged' + additionalProperties: false + + Status: + type: 'object' + properties: + status: + type: 'string' + description: 'Health status of Einstein Bots API.' + enum: ['UP', 'DOWN'] + example: 'UP' + required: + - 'status' + additionalProperties: false + + # Do not show examples here as this Error schema is used + # generically for all error codes. + # The examples specific to the error code are shown elsewhere. + Error: + type: 'object' + properties: + status: + description: 'HTTP status.' + type: 'integer' + format: 'int32' + # example: 500 + path: + description: 'Request path.' + type: 'string' + # example: "/v1/00DRM00000067To/chatbots/HelloWorldBot/messages" + requestId: + description: 'Request ID. A UUID in string format to help with request tracking.' + type: 'string' + # example: "19c056ab-d909-49df-b976-65e56b6ab214" + error: + description: 'Error class name.' + type: 'string' + # example: "NullPointerException" + message: + description: 'Exception message.' + type: 'string' + # example: "Something went wrong" + timestamp: + description: 'Unix timestamp.' + type: 'integer' + format: 'int64' + # example: 1531245973799 + required: + - 'status' + - 'path' + - 'requestId' + - 'error' + - 'message' + - 'timestamp' + additionalProperties: true + + responses: + StatusResponse: + description: 'OK' + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + + ErrorResponse: + description: 'Something went wrong' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + headers: + 'X-Request-ID': + description: 'Request ID. A UUID in string format to help with request tracking.' + example: '36a73651-a46d-4d16-9a8a-fd436ed62e1a' + schema: + type: 'string' + + SuccessfulResponse: + description: 'OK' + content: + application/json: + schema: + $ref: '#/components/schemas/ResponseEnvelope' + headers: + 'X-Request-ID': + description: 'Request ID. A UUID in string format to help with request tracking.' + example: '36a73651-a46d-4d16-9a8a-fd436ed62e1a' + schema: + type: 'string' + 'X-Runtime-CRC': + description: 'Internal runtime CRC unique to the request.' + schema: + type: 'string' + 'X-Bot-Mode': + description: 'Bot mode.' + example: 'default' + schema: + type: 'string' + + SuccessfulChatMessageResponse: + description: 'OK' + content: + application/json: + schema: + $ref: '#/components/schemas/ChatMessageResponseEnvelope' + headers: + 'X-Request-ID': + description: 'Request ID. A UUID in string format to help with request tracking.' + example: '36a73651-a46d-4d16-9a8a-fd436ed62e1a' + schema: + type: 'string' + 'X-Runtime-CRC': + description: 'Internal runtime CRC.' + schema: + type: 'string' + + BadRequestError: + description: 'Bad Request' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + status: 400 + path: '/v1/00DRM00000067To/chatbots/HelloWorldBot/messages' + requestId: '19c056ab-d909-49df-b976-65e56b6ab214' + error: 'BadRequestError' + message: 'Bad Request' + timestamp: 1531245973799 + headers: + 'X-Request-ID': + description: 'Request ID. A UUID in string format to help with request tracking.' + example: '36a73651-a46d-4d16-9a8a-fd436ed62e1a' + schema: + type: 'string' + + UnauthorizedError: + description: 'Access bearer token is missing or invalid' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + status: 401 + path: '/v1/00DRM00000067To/chatbots/HelloWorldBot/messages' + requestId: '19c056ab-d909-49df-b976-65e56b6ab214' + error: 'UnauthorizedError' + message: 'Access bearer token is missing or invalid' + timestamp: 1531245973799 + headers: + 'X-Request-ID': + description: 'Request ID. A UUID in string format to help with request tracking.' + example: '36a73651-a46d-4d16-9a8a-fd436ed62e1a' + schema: + type: 'string' + + ForbiddenError: + description: 'User forbidden from accessing the resource' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + status: 403 + path: '/v1/00DRM00000067To/chatbots/HelloWorldBot/messages' + requestId: '19c056ab-d909-49df-b976-65e56b6ab214' + error: 'ForbiddenError' + message: 'User forbidden from accessing the resource' + timestamp: 1531245973799 + headers: + 'X-Request-ID': + description: 'Request ID. A UUID in string format to help with request tracking.' + example: '36a73651-a46d-4d16-9a8a-fd436ed62e1a' + schema: + type: 'string' + + NotFoundError: + description: 'Resource not found' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + status: 404 + path: '/v1/00DRM00000067To/chatbots/HelloWorldBot/messages' + requestId: '19c056ab-d909-49df-b976-65e56b6ab214' + error: 'NotFoundError' + message: 'Resource not found' + timestamp: 1531245973799 + headers: + 'X-Request-ID': + description: 'Request ID. A UUID in string format to help with request tracking.' + example: 36a73651-a46d-4d16-9a8a-fd436ed62e1a + schema: + type: 'string' + + NotAvailableError: + description: 'Resource not available at the time of the request' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + status: 410 + path: '/v1/00DRM00000067To/chatbots/HelloWorldBot/messages' + requestId: '19c056ab-d909-49df-b976-65e56b6ab214' + error: 'NotAvailableError' + message: 'Resource not available at the time of the request' + timestamp: 1531245973799 + headers: + 'X-Request-ID': + description: 'Request ID. A UUID in string format to help with request tracking.' + example: '36a73651-a46d-4d16-9a8a-fd436ed62e1a' + schema: + type: 'string' + + RequestProcessingException: + description: 'Any exception that occurred during the request execution' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + status: 422 + path: 'v4.0.0/messages' + requestId: '19c056ab-d909-49df-b976-65e56b6ab214' + error: 'RequestProcessingException' + message: 'Cannot determine the active version for the bot' + timestamp: 1531245973799 + headers: + 'X-Request-ID': + description: 'Request ID. A UUID in string format to help with request tracking.' + example: '36a73651-a46d-4d16-9a8a-fd436ed62e1a' + schema: + type: 'string' + + ServerBusyError: + description: 'Server is busy and cannot process the request at this time' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + status: 423 + path: '/v1/00DRM00000067To/chatbots/HelloWorldBot/messages' + requestId: '19c056ab-d909-49df-b976-65e56b6ab214' + error: 'ServerBusyError' + message: 'Server is busy and cannot process the request at this time' + timestamp: 1531245973799 + headers: + 'X-Request-ID': + description: 'Request ID. A UUID in string format to help with request tracking.' + example: '36a73651-a46d-4d16-9a8a-fd436ed62e1a' + schema: + type: 'string' + + TooManyRequestsError: + description: 'Too many requests for the server to handle' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + status: 429 + path: '/v1/00DRM00000067To/chatbots/HelloWorldBot/messages' + requestId: '19c056ab-d909-49df-b976-65e56b6ab214' + error: 'TooManyRequestsError' + message: 'Too many requests for the server to handle' + timestamp: 1531245973799 + headers: + 'X-Request-ID': + description: 'Request ID. A UUID in string format to help with request tracking.' + example: '36a73651-a46d-4d16-9a8a-fd436ed62e1a' + schema: + type: 'string' + + ServiceUnavailable: + description: 'Service is unavailable possibly because Apex or Flow calls timed out' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + status: 503 + path: '/v1/00DRM00000067To/chatbots/HelloWorldBot/messages' + requestId: '19c056ab-d909-49df-b976-65e56b6ab214' + error: 'ServiceUnavailable' + message: 'Service is unavailable possibly because Apex/Flow calls timed out' + timestamp: 1531245973799 + headers: + 'X-Request-ID': + description: 'Request ID. A UUID in string format to help with request tracking.' + example: '36a73651-a46d-4d16-9a8a-fd436ed62e1a' + schema: + type: 'string' diff --git a/src/ApiTypeDocument.js b/src/ApiTypeDocument.js index 66cda01..d26732b 100644 --- a/src/ApiTypeDocument.js +++ b/src/ApiTypeDocument.js @@ -448,9 +448,16 @@ export class ApiTypeDocument extends PropertyDocumentMixin(LitElement) { } else if ( this._hasType(type, shapesKey.UnionShape) ) { - isUnion = true; - key = this._getAmfKey(shapesKey.anyOf); - this.unionTypes = this._computeTypes(type, key); + // Check if this is a nullable union (type | null) which should be rendered as scalar + const nullableCheck = this._checkNullableUnion(type); + if (nullableCheck && nullableCheck.isNullable) { + // Treat nullable types as scalar for cleaner rendering + isScalar = true; + } else { + isUnion = true; + key = this._getAmfKey(shapesKey.anyOf); + this.unionTypes = this._computeTypes(type, key); + } } else if (this._hasProperty(type, this.ns.w3.shacl.xone)) { isOneOf = true; key = this._getAmfKey(this.ns.w3.shacl.xone); diff --git a/src/PropertyDocumentMixin.d.ts b/src/PropertyDocumentMixin.d.ts index 810bafb..ed80bff 100644 --- a/src/PropertyDocumentMixin.d.ts +++ b/src/PropertyDocumentMixin.d.ts @@ -49,6 +49,15 @@ interface PropertyDocumentMixin extends AmfHelperMixin { */ graph: boolean; + /** + * Checks if a union shape represents a nullable type (union with null). + * A nullable type is a union of exactly 2 members where one is NilShape. + * + * @param range AMF range object (should be UnionShape) + * @returns Returns {baseType, isNullable: true} if nullable, undefined otherwise + */ + _checkNullableUnion(range: any): { baseType: any; isNullable: boolean } | undefined; + /** * Computes type from a `http://raml.org/vocabularies/shapes#range` object * diff --git a/src/PropertyDocumentMixin.js b/src/PropertyDocumentMixin.js index 286f6a7..5decc06 100644 --- a/src/PropertyDocumentMixin.js +++ b/src/PropertyDocumentMixin.js @@ -107,6 +107,65 @@ const mxFunction = (base) => { this._hasMediaType = false; } + /** + * Checks if a union shape represents a nullable type (union with null). + * A nullable type is a union of exactly 2 members where one is NilShape + * and the other is any type (scalar, array, object, etc.). + * + * This is specifically for OpenAPI 3.0 nullable: true which AMF converts + * to union of type + null. This simplifies the rendering to "Type or null" + * instead of showing a full union selector. + * + * @param {any} range AMF range object (should be UnionShape) + * @return {Object|undefined} Returns {baseType, isNullable: true} if nullable, undefined otherwise + */ + _checkNullableUnion(range) { + if (!range || !this._hasType(range, this.ns.aml.vocabularies.shapes.UnionShape)) { + return undefined; + } + + const key = this._getAmfKey(this.ns.aml.vocabularies.shapes.anyOf); + const unionMembers = this._ensureArray(range[key]); + + if (!unionMembers || unionMembers.length !== 2) { + return undefined; + } + + // Check if one member is NilShape + let nilIndex = -1; + let baseTypeIndex = -1; + + for (let i = 0; i < unionMembers.length; i++) { + let member = unionMembers[i]; + if (Array.isArray(member)) { + [member] = member; + } + member = this._resolve(member); + + if (this._hasType(member, this.ns.aml.vocabularies.shapes.NilShape)) { + nilIndex = i; + } else { + baseTypeIndex = i; + } + } + + // If we found exactly one nil and one non-nil type, it's a nullable + if (nilIndex !== -1 && baseTypeIndex !== -1) { + let baseType = unionMembers[baseTypeIndex]; + if (Array.isArray(baseType)) { + [baseType] = baseType; + } + baseType = this._resolve(baseType); + + return { + baseType, + isNullable: true + }; + } + + return undefined; + } + /** * Computes type from a `http://raml.org/vocabularies/shapes#range` object * @@ -122,6 +181,12 @@ const mxFunction = (base) => { return this._computeScalarDataType(range); } if (this._hasType(range, rs.UnionShape)) { + // Check if this is a nullable union (type | null) + const nullableCheck = this._checkNullableUnion(range); + if (nullableCheck && nullableCheck.isNullable) { + const baseTypeName = this._computeRangeDataType(nullableCheck.baseType); + return `${baseTypeName} or null`; + } return 'Union'; } if (this._hasType(range, rs.ArrayShape)) { @@ -145,9 +210,6 @@ const mxFunction = (base) => { if (this._hasType(range, rs.TupleShape)) { return 'Tuple'; } - if (this._hasType(range, rs.UnionShape)) { - return 'Union'; - } if (this._hasType(range, rs.RecursiveShape)) { return 'Recursive'; } @@ -313,7 +375,12 @@ const mxFunction = (base) => { * @return {Boolean} */ _computeIsUnion(range) { - return this._hasType(range, this.ns.aml.vocabularies.shapes.UnionShape); + if (!this._hasType(range, this.ns.aml.vocabularies.shapes.UnionShape)) { + return false; + } + // Check if it's a nullable union (which we don't treat as union for UI) + const nullableCheck = this._checkNullableUnion(range); + return !nullableCheck; // Only true if NOT nullable } /** @@ -325,7 +392,15 @@ const mxFunction = (base) => { * @return {Boolean} */ _computeIsObject(range) { - return this._hasType(range, this.ns.w3.shacl.NodeShape); + if (this._hasType(range, this.ns.w3.shacl.NodeShape)) { + return true; + } + // Check if it's a nullable object + const nullableCheck = this._checkNullableUnion(range); + if (nullableCheck) { + return this._hasType(nullableCheck.baseType, this.ns.w3.shacl.NodeShape); + } + return false; } /** @@ -337,7 +412,15 @@ const mxFunction = (base) => { * @return {Boolean} */ _computeIsArray(range) { - return this._hasType(range, this.ns.aml.vocabularies.shapes.ArrayShape); + if (this._hasType(range, this.ns.aml.vocabularies.shapes.ArrayShape)) { + return true; + } + // Check if it's a nullable array + const nullableCheck = this._checkNullableUnion(range); + if (nullableCheck) { + return this._hasType(nullableCheck.baseType, this.ns.aml.vocabularies.shapes.ArrayShape); + } + return false; } /** diff --git a/src/PropertyShapeDocument.js b/src/PropertyShapeDocument.js index fb46ac0..8b7f0e6 100644 --- a/src/PropertyShapeDocument.js +++ b/src/PropertyShapeDocument.js @@ -735,7 +735,14 @@ export class PropertyShapeDocument extends PropertyDocumentMixin(LitElement) { if (!this.isComplex || !this.opened || this.isScalarArray) { return ''; } - const range = this._resolve(this.range); + let range = this._resolve(this.range); + + // If this is a nullable complex type, extract the base type + const nullableCheck = this._checkNullableUnion(range); + if (nullableCheck) { + range = nullableCheck.baseType; + } + const parentTypeName = this._getParentTypeName(); return html`', () => { element = await basicFixture(); }); - it('should render union toggle as "Array of String"', async () => { + it('should render nullable array as "Array or null"', async () => { const data = await AmfLoader.loadType('test2', compact, 'APIC-631'); element.amf = data[0]; - element._typeChanged(element._resolve(data[1])); - await nextFrame(); - const firstToggle = element.shadowRoot.querySelectorAll('.union-toggle')[0] - assert.equal(firstToggle.textContent.toLowerCase(), 'array of string'); + element.type = data[1]; + await aTimeout(100); + // test2 is string[] | nil which should now render as scalar with "Array or null" + const shapeDoc = element.shadowRoot.querySelector('property-shape-document'); + assert.exists(shapeDoc, 'Should have property-shape-document'); + const dataType = shapeDoc.shadowRoot.querySelector('.data-type'); + assert.exists(dataType, 'Should have data-type element'); + // Should show "Array or null" instead of Union selector + assert.include(dataType.textContent.toLowerCase(), 'array'); + assert.include(dataType.textContent.toLowerCase(), 'null'); }); it('should not render type name as "undefined" for inline type', async () => { diff --git a/test/nullable-types.test.js b/test/nullable-types.test.js new file mode 100644 index 0000000..4c59176 --- /dev/null +++ b/test/nullable-types.test.js @@ -0,0 +1,218 @@ +import { fixture, assert, nextFrame, aTimeout } from '@open-wc/testing'; +import { AmfLoader } from './amf-loader.js'; +import '../api-type-document.js'; + +/** @typedef {import('..').ApiTypeDocument} ApiTypeDocument */ + +describe('Nullable Types Rendering', () => { + /** + * @param {ApiTypeDocument} element + * @param {string} typeName + * @param {string} propertyName + * @returns {HTMLElement|null} + */ + async function getPropertyElement(element, typeName, propertyName) { + const data = await AmfLoader.loadType(typeName, false, 'nullable-test'); + element.amf = data[0]; + element.type = data[1]; + await aTimeout(100); + + const properties = element.shadowRoot.querySelectorAll('property-shape-document'); + for (const prop of properties) { + const nameEl = prop.shadowRoot.querySelector('.property-name'); + if (nameEl && nameEl.textContent.trim() === propertyName) { + return prop; + } + } + return null; + } + + /** + * @param {HTMLElement} propertyElement + * @returns {string} + */ + function getDataType(propertyElement) { + const dataTypeEl = propertyElement.shadowRoot.querySelector('.data-type'); + return dataTypeEl ? dataTypeEl.textContent.trim() : ''; + } + + /** + * @param {HTMLElement} propertyElement + * @returns {boolean} + */ + function hasUnionSelector(propertyElement) { + return !!propertyElement.shadowRoot.querySelector('.union-type-selector, .any-of, .one-of'); + } + + [ + ['Regular model', false], + ['Compact model', true], + ].forEach((item) => { + describe(String(item[0]), () => { + /** @type ApiTypeDocument */ + let element; + + beforeEach(async () => { + element = await fixture(``); + }); + + describe('Scalar Nullables (should show "Type or null")', () => { + const scalarTests = [ + ['nullableString', 'String or null'], + ['nullableInteger', 'Integer or null'], + ['nullableNumber', 'Number or null'], + ['nullableBoolean', 'Boolean or null'], + ['nullableDateTime', 'DateTime or null'], + ['nullableDate', 'Date or null'], + ['nullableEmail', 'String or null'], + ['nullableUuid', 'String or null'], + ]; + + scalarTests.forEach(([propertyName, expectedType]) => { + it(`${propertyName} should show "${expectedType}"`, async () => { + const prop = await getPropertyElement(element, 'ScalarNullables', propertyName); + assert.exists(prop, `Property ${propertyName} should exist`); + + const dataType = getDataType(prop); + assert.equal(dataType, expectedType, `Data type should be ${expectedType}`); + + const hasSelector = hasUnionSelector(prop); + assert.isFalse(hasSelector, 'Should NOT have union selector'); + }); + }); + }); + + describe('Complex Nullables (should also show "Type or null")', () => { + const complexTests = [ + ['nullableArray', 'Array or null'], + ['nullableObject', 'Object or null'], + ['nullableArrayOfObjects', 'Array or null'], + ]; + + complexTests.forEach(([propertyName, expectedType]) => { + it(`${propertyName} should show "${expectedType}"`, async () => { + const prop = await getPropertyElement(element, 'ComplexNullables', propertyName); + assert.exists(prop, `Property ${propertyName} should exist`); + + const dataType = getDataType(prop); + assert.equal(dataType, expectedType, `Data type should be ${expectedType}`); + + const hasSelector = hasUnionSelector(prop); + assert.isFalse(hasSelector, 'Should NOT have union selector'); + }); + }); + }); + + describe('Real Unions (should keep Union selector)', () => { + it('stringOrNumber should show Union selector', async () => { + const prop = await getPropertyElement(element, 'RealUnions', 'stringOrNumber'); + assert.exists(prop, 'Property should exist'); + + const dataType = getDataType(prop); + // Real unions should not be simplified + assert.notInclude(dataType, 'or null', 'Should not show as nullable'); + }); + + it('multipleTypes should show Union selector', async () => { + const prop = await getPropertyElement(element, 'RealUnions', 'multipleTypes'); + assert.exists(prop, 'Property should exist'); + + const dataType = getDataType(prop); + assert.notInclude(dataType, 'or null', 'Should not show as nullable'); + }); + }); + + describe('Nested Nullables', () => { + it('nested scalar nullable (email) should show "String or null"', async () => { + const data = await AmfLoader.loadType('NestedNullables', false, 'nullable-test'); + element.amf = data[0]; + element.type = data[1]; + await aTimeout(150); + + // Find the user object property + const userProp = Array.from( + element.shadowRoot.querySelectorAll('property-shape-document') + ).find(p => { + const name = p.shadowRoot.querySelector('.property-name'); + return name && name.textContent.trim() === 'user'; + }); + + assert.exists(userProp, 'User property should exist'); + + // Expand the user object + const toggleBtn = userProp.shadowRoot.querySelector('.complex-toggle'); + if (toggleBtn) { + toggleBtn.click(); + await aTimeout(100); + } + + // Now check for email inside + const apiTypeDoc = userProp.shadowRoot.querySelector('api-type-document'); + if (apiTypeDoc) { + const emailProp = Array.from( + apiTypeDoc.shadowRoot.querySelectorAll('property-shape-document') + ).find(p => { + const name = p.shadowRoot.querySelector('.property-name'); + return name && name.textContent.trim() === 'email'; + }); + + if (emailProp) { + const dataType = getDataType(emailProp); + assert.equal(dataType, 'String or null', 'Nested email should show "String or null"'); + } + } + }); + }); + + describe('Edge Cases', () => { + const edgeCaseTests = [ + ['enumNullable', 'String or null'], + ['numberWithConstraints', 'Number or null'], + ['stringWithPattern', 'String or null'], + ['readOnlyNullable', 'String or null'], + ['deprecatedNullable', 'Integer or null'], + ]; + + edgeCaseTests.forEach(([propertyName, expectedType]) => { + it(`${propertyName} should show "${expectedType}"`, async () => { + const prop = await getPropertyElement(element, 'EdgeCases', propertyName); + assert.exists(prop, `Property ${propertyName} should exist`); + + const dataType = getDataType(prop); + assert.equal(dataType, expectedType, `Data type should be ${expectedType}`); + + const hasSelector = hasUnionSelector(prop); + assert.isFalse(hasSelector, 'Should NOT have union selector'); + }); + }); + }); + + describe('Request/Response Schemas', () => { + it('TestRequest.timezone should show "String or null"', async () => { + const prop = await getPropertyElement(element, 'TestRequest', 'timezone'); + assert.exists(prop, 'Property should exist'); + + const dataType = getDataType(prop); + assert.equal(dataType, 'String or null'); + }); + + it('TestResponse.message should show "String or null"', async () => { + const prop = await getPropertyElement(element, 'TestResponse', 'message'); + assert.exists(prop, 'Property should exist'); + + const dataType = getDataType(prop); + assert.equal(dataType, 'String or null'); + }); + + it('TestResponse.errorCode should show "Integer or null"', async () => { + const prop = await getPropertyElement(element, 'TestResponse', 'errorCode'); + assert.exists(prop, 'Property should exist'); + + const dataType = getDataType(prop); + assert.equal(dataType, 'Integer or null'); + }); + }); + }); + }); +}); + diff --git a/test/property-shape-document.test.js b/test/property-shape-document.test.js index e5087b9..fdf32e4 100644 --- a/test/property-shape-document.test.js +++ b/test/property-shape-document.test.js @@ -178,10 +178,11 @@ describe('PropertyShapeDocument', () => { assert.isFalse(element.isEnum); }); - it('sets isUnion to true when is union', async () => { + it('sets isUnion to false when is nullable (not a real union)', async () => { const shape = getPropertyShape(element, type, 'nillable'); element.shape = shape; - assert.isTrue(element.isUnion); + // nillable is string | nil, which is now treated as nullable, not union + assert.isFalse(element.isUnion); }); it('sets isUnion to false when is not union', async () => { @@ -221,10 +222,11 @@ describe('PropertyShapeDocument', () => { assert.isTrue(element.isComplex); }); - it('sets isComplex to true when is union', async () => { + it('sets isComplex to false when is nullable scalar (not complex)', async () => { const shape = getPropertyShape(element, type, 'nillable'); element.shape = shape; - assert.isTrue(element.isComplex); + // nillable is string | nil, which is now treated as scalar nullable (not complex) + assert.isFalse(element.isComplex); }); it('sets isComplex to true when array property', async () => { @@ -308,7 +310,7 @@ describe('PropertyShapeDocument', () => { ['fietype', 'File'], ['etag', 'String'], ['age', 'Integer'], - ['nillable', 'Union'], + ['nillable', 'String or null'], ['birthday', 'Date'], ].forEach(([property, dataType]) => { it(`sets propertyDataType to ${dataType}`, () => {