From a8fadd20636987b6424f9ff72885000c9e807f14 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 00:51:49 +0000 Subject: [PATCH] feat: Generate a status-discriminated action attempt union Bump @seamapi/blueprint to 1.10.0 and consume the new actionAttemptStatuses property annotation. Each action attempt now generates one union member per status from its status enum: the status property is rendered as the status literal, and any property annotated with actionAttemptStatuses is rendered as null for statuses it does not list. SucceededActionAttempt and FailedActionAttempt extract the real success and error members from the union instead of intersecting with a status that recovered nothing. Code that dereferences error or result without narrowing on status stops typechecking; runtime API behavior is unchanged. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EdWS7o3htQ9cNxhCWL5Frp --- codegen/layouts/partials/resource-object.hbs | 2 +- codegen/lib/layouts/resources.ts | 48 +- package-lock.json | 8 +- package.json | 2 +- src/lib/resolve-action-attempt.ts | 14 +- src/lib/resources/action-attempt.ts | 1302 ++++++++++++++--- .../seam/connect/action-attempt-types.test.ts | 95 ++ 7 files changed, 1256 insertions(+), 215 deletions(-) create mode 100644 test/seam/connect/action-attempt-types.test.ts diff --git a/codegen/layouts/partials/resource-object.hbs b/codegen/layouts/partials/resource-object.hbs index 72aa86ee..0c3af50a 100644 --- a/codegen/layouts/partials/resource-object.hbs +++ b/codegen/layouts/partials/resource-object.hbs @@ -1,6 +1,6 @@ { {{#each properties}} {{> doc}} - {{json name}}{{#if isOptional}}?{{/if}}: {{> resource-property-type }}{{#if isNullable}} | null{{/if}}{{#if isOptional}} | undefined{{/if}} + {{json name}}{{#if isOptional}}?{{/if}}: {{#if renderAsNull}}null{{else}}{{> resource-property-type }}{{#if isNullable}} | null{{/if}}{{/if}}{{#if isOptional}} | undefined{{/if}} {{/each}} } diff --git a/codegen/lib/layouts/resources.ts b/codegen/lib/layouts/resources.ts index 718edb96..3724ddbb 100644 --- a/codegen/lib/layouts/resources.ts +++ b/codegen/lib/layouts/resources.ts @@ -1,4 +1,10 @@ -import type { Blueprint, Resource } from '@seamapi/blueprint' +import type { + ActionAttemptStatus, + Blueprint, + EnumProperty, + Property, + Resource, +} from '@seamapi/blueprint' import { kebabCase, pascalCase } from 'change-case' export interface ResourceLayoutContext { @@ -39,7 +45,7 @@ export const getResourceLayoutContexts = ( ({ resourceType }) => !discriminatedResourceTypes.has(resourceType), ), ...blueprint.events, - ...blueprint.actionAttempts, + ...blueprint.actionAttempts.flatMap(expandActionAttemptByStatus), ] const resourceTypes = [ ...new Set(resources.map(({ resourceType }) => resourceType)), @@ -57,6 +63,44 @@ export const getResourceLayoutContexts = ( })) } +// Expand an action attempt into one union member per status from its status +// enum. In each member, the status property is rendered as the status literal, +// and any property annotated with actionAttemptStatuses is rendered as null +// for the statuses it does not list. +const expandActionAttemptByStatus = (resource: Resource): Resource[] => { + const statusProperty = resource.properties.find( + (property): property is EnumProperty => + property.name === 'status' && property.format === 'enum', + ) + if (statusProperty == null) return [resource] + + return statusProperty.values.map(({ name }) => { + const status = name as ActionAttemptStatus + return { + ...resource, + properties: resource.properties.map((property): Property => { + if (property === statusProperty) { + return { + ...statusProperty, + values: statusProperty.values.filter( + (value) => value.name === status, + ), + } + } + const { actionAttemptStatuses } = property + if (actionAttemptStatuses == null) return property + if (actionAttemptStatuses.includes(status)) return property + const nullRenderedProperty: Property & { renderAsNull: true } = { + ...property, + isNullable: false, + renderAsNull: true, + } + return nullRenderedProperty + }), + } + }) +} + const getBatchResourceLayoutContexts = ( resources: Resource[], ): BatchResourceLayoutContext[] => { diff --git a/package-lock.json b/package-lock.json index 80c11a75..cba1ecb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "axios-retry": "^4.4.2" }, "devDependencies": { - "@seamapi/blueprint": "^1.9.1", + "@seamapi/blueprint": "^1.10.0", "@seamapi/fake-seam-connect": "^2.0.4", "@seamapi/smith": "^1.1.0", "@seamapi/types": "1.1047.0", @@ -1067,9 +1067,9 @@ "license": "MIT" }, "node_modules/@seamapi/blueprint": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@seamapi/blueprint/-/blueprint-1.9.1.tgz", - "integrity": "sha512-A9H3dZE9f+ZEEnOAlMl5jSlL5F/RIKkjOF8NDFbnuahAiu/trDz/RzHOGhbQd6sd0RErIyx3eG1p4HCOJDKDCA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@seamapi/blueprint/-/blueprint-1.10.0.tgz", + "integrity": "sha512-XyP6zvbhv5naWEa9T9utNLi3FVVmvUB1Htih/IjUqS3Uz0FrIIBWy7Myk5+s4uylEt+wTdtSBaA1fGOF/6ILmA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 33740637..827f9087 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "axios-retry": "^4.4.2" }, "devDependencies": { - "@seamapi/blueprint": "^1.9.1", + "@seamapi/blueprint": "^1.10.0", "@seamapi/fake-seam-connect": "^2.0.4", "@seamapi/smith": "^1.1.0", "@seamapi/types": "1.1047.0", diff --git a/src/lib/resolve-action-attempt.ts b/src/lib/resolve-action-attempt.ts index 5e137011..4e66e964 100644 --- a/src/lib/resolve-action-attempt.ts +++ b/src/lib/resolve-action-attempt.ts @@ -162,13 +162,15 @@ const isFailedActionAttempt = ( /** * An action attempt that has succeeded. */ -export type SucceededActionAttempt = T & { - status: 'success' -} +export type SucceededActionAttempt = Extract< + T, + { status: 'success' } +> /** * An action attempt that has failed. */ -export type FailedActionAttempt = T & { - status: 'error' -} +export type FailedActionAttempt = Extract< + T, + { status: 'error' } +> diff --git a/src/lib/resources/action-attempt.ts b/src/lib/resources/action-attempt.ts index 2b6d7cd5..3a74f262 100644 --- a/src/lib/resources/action-attempt.ts +++ b/src/lib/resources/action-attempt.ts @@ -21,18 +21,7 @@ export type ActionAttempt = /** * Error associated with the action. */ - error: { - /** - * Detailed description of the error. Provides insights into the issue and potentially how to rectify it. - */ - message: string - - /** - * Type of the error. - */ - type: string - } - + error: null /** * Result of the action. */ @@ -43,7 +32,7 @@ export type ActionAttempt = was_confirmed_by_device?: boolean | undefined } - status: 'success' | 'pending' | 'error' + status: 'success' } | { /** @@ -52,9 +41,31 @@ export type ActionAttempt = action_attempt_id: string /** - * Action attempt to track the status of unlocking a door. + * Action attempt to track the status of locking a door. */ - action_type: 'UNLOCK_DOOR' + action_type: 'LOCK_DOOR' + + /** + * Error associated with the action. + */ + error: null + /** + * Result of the action. + */ + result: null + + status: 'pending' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of locking a door. + */ + action_type: 'LOCK_DOOR' /** * Error associated with the action. @@ -71,6 +82,28 @@ export type ActionAttempt = type: string } + /** + * Result of the action. + */ + result: null + + status: 'error' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of unlocking a door. + */ + action_type: 'UNLOCK_DOOR' + + /** + * Error associated with the action. + */ + error: null /** * Result of the action. */ @@ -81,7 +114,7 @@ export type ActionAttempt = was_confirmed_by_device?: boolean | undefined } - status: 'success' | 'pending' | 'error' + status: 'success' } | { /** @@ -90,10 +123,35 @@ export type ActionAttempt = action_attempt_id: string /** - * Action attempt to track the status of scanning a credential. + * Action attempt to track the status of unlocking a door. */ - action_type: 'SCAN_CREDENTIAL' + action_type: 'UNLOCK_DOOR' + + /** + * Error associated with the action. + */ + error: null + /** + * Result of the action. + */ + result: null + + status: 'pending' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + /** + * Action attempt to track the status of unlocking a door. + */ + action_type: 'UNLOCK_DOOR' + + /** + * Error associated with the action. + */ error: { /** * Detailed description of the error. Provides insights into the issue and potentially how to rectify it. @@ -101,17 +159,30 @@ export type ActionAttempt = message: string /** - * Error type to indicate that the Seam Bridge is disconnected or cannot reach the access control system. + * Type of the error. */ - type: - | 'uncategorized_error' - | 'action_attempt_expired' - | 'no_credential_on_encoder' - | 'encoder_not_online' - | 'encoder_communication_timeout' - | 'bridge_disconnected' + type: string } + /** + * Result of the action. + */ + result: null + + status: 'error' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of scanning a credential. + */ + action_type: 'SCAN_CREDENTIAL' + + error: null /** * Result of scanning a card. If the attempt was successful, includes a snapshot of credential data read from the physical encoder, the corresponding data stored on Seam and the access system, and any associated warnings. */ @@ -468,7 +539,7 @@ export type ActionAttempt = }> } - status: 'success' | 'pending' | 'error' + status: 'success' } | { /** @@ -477,9 +548,28 @@ export type ActionAttempt = action_attempt_id: string /** - * Action attempt to track the status of encoding credential data from the physical encoder onto a card. + * Action attempt to track the status of scanning a credential. */ - action_type: 'ENCODE_CREDENTIAL' + action_type: 'SCAN_CREDENTIAL' + + error: null + /** + * Result of scanning a card. If the attempt was successful, includes a snapshot of credential data read from the physical encoder, the corresponding data stored on Seam and the access system, and any associated warnings. + */ + result: null + + status: 'pending' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of scanning a credential. + */ + action_type: 'SCAN_CREDENTIAL' error: { /** @@ -488,21 +578,36 @@ export type ActionAttempt = message: string /** - * Error type to indicate that the credential was deleted and can no longer be encoded. + * Error type to indicate that the Seam Bridge is disconnected or cannot reach the access control system. */ type: | 'uncategorized_error' | 'action_attempt_expired' | 'no_credential_on_encoder' - | 'incompatible_card_format' - | 'credential_cannot_be_reissued' | 'encoder_not_online' | 'encoder_communication_timeout' | 'bridge_disconnected' - | 'encoding_interrupted' - | 'credential_deleted' } + /** + * Result of scanning a card. If the attempt was successful, includes a snapshot of credential data read from the physical encoder, the corresponding data stored on Seam and the access system, and any associated warnings. + */ + result: null + + status: 'error' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of encoding credential data from the physical encoder onto a card. + */ + action_type: 'ENCODE_CREDENTIAL' + + error: null /** * Result of an encoding attempt. If the attempt was successful, includes the credential data that was encoded onto the card. */ @@ -757,7 +862,7 @@ export type ActionAttempt = workspace_id: string } - status: 'success' | 'pending' | 'error' + status: 'success' } | { /** @@ -766,9 +871,28 @@ export type ActionAttempt = action_attempt_id: string /** - * Action attempt to track the status of scanning a physical card and assigning the credential to an ACS user. + * Action attempt to track the status of encoding credential data from the physical encoder onto a card. */ - action_type: 'SCAN_TO_ASSIGN_CREDENTIAL' + action_type: 'ENCODE_CREDENTIAL' + + error: null + /** + * Result of an encoding attempt. If the attempt was successful, includes the credential data that was encoded onto the card. + */ + result: null + + status: 'pending' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of encoding credential data from the physical encoder onto a card. + */ + action_type: 'ENCODE_CREDENTIAL' error: { /** @@ -777,14 +901,40 @@ export type ActionAttempt = message: string /** - * Error type to indicate that there is no credential on the encoder. + * Error type to indicate that the credential was deleted and can no longer be encoded. */ type: | 'uncategorized_error' | 'action_attempt_expired' | 'no_credential_on_encoder' + | 'incompatible_card_format' + | 'credential_cannot_be_reissued' + | 'encoder_not_online' + | 'encoder_communication_timeout' + | 'bridge_disconnected' + | 'encoding_interrupted' + | 'credential_deleted' } + /** + * Result of an encoding attempt. If the attempt was successful, includes the credential data that was encoded onto the card. + */ + result: null + + status: 'error' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of scanning a physical card and assigning the credential to an ACS user. + */ + action_type: 'SCAN_TO_ASSIGN_CREDENTIAL' + + error: null /** * Result of a scan to assign attempt. If the attempt was successful, includes the credential data that was scanned and assigned. */ @@ -1041,7 +1191,7 @@ export type ActionAttempt = workspace_id: string } - status: 'success' | 'pending' | 'error' + status: 'success' } | { /** @@ -1050,9 +1200,28 @@ export type ActionAttempt = action_attempt_id: string /** - * Action attempt to track the status of assigning a pre-registered card credential to an access method. + * Action attempt to track the status of scanning a physical card and assigning the credential to an ACS user. */ - action_type: 'ASSIGN_CREDENTIAL' + action_type: 'SCAN_TO_ASSIGN_CREDENTIAL' + + error: null + /** + * Result of a scan to assign attempt. If the attempt was successful, includes the credential data that was scanned and assigned. + */ + result: null + + status: 'pending' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of scanning a physical card and assigning the credential to an ACS user. + */ + action_type: 'SCAN_TO_ASSIGN_CREDENTIAL' error: { /** @@ -1061,14 +1230,33 @@ export type ActionAttempt = message: string /** - * Error type to indicate that no matching credential was found. + * Error type to indicate that there is no credential on the encoder. */ type: | 'uncategorized_error' | 'action_attempt_expired' - | 'credential_not_found' + | 'no_credential_on_encoder' } + /** + * Result of a scan to assign attempt. If the attempt was successful, includes the credential data that was scanned and assigned. + */ + result: null + + status: 'error' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of assigning a pre-registered card credential to an access method. + */ + action_type: 'ASSIGN_CREDENTIAL' + + error: null /** * Result of assigning a credential. If successful, includes the updated access method with the assigned credential. */ @@ -1243,7 +1431,7 @@ export type ActionAttempt = workspace_id: string } - status: 'success' | 'pending' | 'error' + status: 'success' } | { /** @@ -1252,31 +1440,17 @@ export type ActionAttempt = action_attempt_id: string /** - * Action attempt to track the status of resetting a sandbox workspace. - */ - action_type: 'RESET_SANDBOX_WORKSPACE' - - /** - * Error associated with the action. + * Action attempt to track the status of assigning a pre-registered card credential to an access method. */ - error: { - /** - * Detailed description of the error. Provides insights into the issue and potentially how to rectify it. - */ - message: string - - /** - * Type of the error. - */ - type: string - } + action_type: 'ASSIGN_CREDENTIAL' + error: null /** - * Result of the action. + * Result of assigning a credential. If successful, includes the updated access method with the assigned credential. */ - result: {} + result: null - status: 'success' | 'pending' | 'error' + status: 'pending' } | { /** @@ -1285,9 +1459,702 @@ export type ActionAttempt = action_attempt_id: string /** - * Action attempt to track the status of setting the fan mode on a thermostat. + * Action attempt to track the status of assigning a pre-registered card credential to an access method. + */ + action_type: 'ASSIGN_CREDENTIAL' + + error: { + /** + * Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + */ + message: string + + /** + * Error type to indicate that no matching credential was found. + */ + type: + | 'uncategorized_error' + | 'action_attempt_expired' + | 'credential_not_found' + } + + /** + * Result of assigning a credential. If successful, includes the updated access method with the assigned credential. + */ + result: null + + status: 'error' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of resetting a sandbox workspace. + */ + action_type: 'RESET_SANDBOX_WORKSPACE' + + /** + * Error associated with the action. + */ + error: null + /** + * Result of the action. + */ + result: {} + + status: 'success' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of resetting a sandbox workspace. + */ + action_type: 'RESET_SANDBOX_WORKSPACE' + + /** + * Error associated with the action. + */ + error: null + /** + * Result of the action. + */ + result: null + + status: 'pending' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of resetting a sandbox workspace. + */ + action_type: 'RESET_SANDBOX_WORKSPACE' + + /** + * Error associated with the action. + */ + error: { + /** + * Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + */ + message: string + + /** + * Type of the error. + */ + type: string + } + + /** + * Result of the action. + */ + result: null + + status: 'error' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of setting the fan mode on a thermostat. + */ + action_type: 'SET_FAN_MODE' + + /** + * Error associated with the action. + */ + error: null + /** + * Result of the action. + */ + result: {} + + status: 'success' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of setting the fan mode on a thermostat. + */ + action_type: 'SET_FAN_MODE' + + /** + * Error associated with the action. + */ + error: null + /** + * Result of the action. + */ + result: null + + status: 'pending' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of setting the fan mode on a thermostat. + */ + action_type: 'SET_FAN_MODE' + + /** + * Error associated with the action. + */ + error: { + /** + * Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + */ + message: string + + /** + * Type of the error. + */ + type: string + } + + /** + * Result of the action. + */ + result: null + + status: 'error' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of setting the HVAC mode on a thermostat. + */ + action_type: 'SET_HVAC_MODE' + + /** + * Error associated with the action. + */ + error: null + /** + * Result of the action. + */ + result: {} + + status: 'success' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of setting the HVAC mode on a thermostat. + */ + action_type: 'SET_HVAC_MODE' + + /** + * Error associated with the action. + */ + error: null + /** + * Result of the action. + */ + result: null + + status: 'pending' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of setting the HVAC mode on a thermostat. + */ + action_type: 'SET_HVAC_MODE' + + /** + * Error associated with the action. + */ + error: { + /** + * Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + */ + message: string + + /** + * Type of the error. + */ + type: string + } + + /** + * Result of the action. + */ + result: null + + status: 'error' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of a climate preset activation. + */ + action_type: 'ACTIVATE_CLIMATE_PRESET' + + /** + * Error associated with the action. + */ + error: null + /** + * Result of the action. + */ + result: {} + + status: 'success' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of a climate preset activation. + */ + action_type: 'ACTIVATE_CLIMATE_PRESET' + + /** + * Error associated with the action. + */ + error: null + /** + * Result of the action. + */ + result: null + + status: 'pending' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of a climate preset activation. + */ + action_type: 'ACTIVATE_CLIMATE_PRESET' + + /** + * Error associated with the action. + */ + error: { + /** + * Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + */ + message: string + + /** + * Type of the error. + */ + type: string + } + + /** + * Result of the action. + */ + result: null + + status: 'error' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of simulating a keypad code entry. + */ + action_type: 'SIMULATE_KEYPAD_CODE_ENTRY' + + /** + * Error associated with the action. + */ + error: null + /** + * Result of the action. + */ + result: {} + + status: 'success' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of simulating a keypad code entry. + */ + action_type: 'SIMULATE_KEYPAD_CODE_ENTRY' + + /** + * Error associated with the action. + */ + error: null + /** + * Result of the action. + */ + result: null + + status: 'pending' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of simulating a keypad code entry. + */ + action_type: 'SIMULATE_KEYPAD_CODE_ENTRY' + + /** + * Error associated with the action. + */ + error: { + /** + * Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + */ + message: string + + /** + * Type of the error. + */ + type: string + } + + /** + * Result of the action. + */ + result: null + + status: 'error' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of simulating a manual lock action using a keypad. + */ + action_type: 'SIMULATE_MANUAL_LOCK_VIA_KEYPAD' + + /** + * Error associated with the action. + */ + error: null + /** + * Result of the action. + */ + result: {} + + status: 'success' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of simulating a manual lock action using a keypad. + */ + action_type: 'SIMULATE_MANUAL_LOCK_VIA_KEYPAD' + + /** + * Error associated with the action. + */ + error: null + /** + * Result of the action. + */ + result: null + + status: 'pending' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of simulating a manual lock action using a keypad. + */ + action_type: 'SIMULATE_MANUAL_LOCK_VIA_KEYPAD' + + /** + * Error associated with the action. + */ + error: { + /** + * Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + */ + message: string + + /** + * Type of the error. + */ + type: string + } + + /** + * Result of the action. + */ + result: null + + status: 'error' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of pushing thermostat programs. + */ + action_type: 'PUSH_THERMOSTAT_PROGRAMS' + + /** + * Error associated with the action. + */ + error: null + /** + * Result of the action. + */ + result: {} + + status: 'success' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of pushing thermostat programs. + */ + action_type: 'PUSH_THERMOSTAT_PROGRAMS' + + /** + * Error associated with the action. + */ + error: null + /** + * Result of the action. + */ + result: null + + status: 'pending' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of pushing thermostat programs. + */ + action_type: 'PUSH_THERMOSTAT_PROGRAMS' + + /** + * Error associated with the action. + */ + error: { + /** + * Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + */ + message: string + + /** + * Type of the error. + */ + type: string + } + + /** + * Result of the action. + */ + result: null + + status: 'error' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of configuring the auto-lock on a lock. + */ + action_type: 'CONFIGURE_AUTO_LOCK' + + /** + * Error associated with the action. + */ + error: null + /** + * Result of the action. + */ + result: {} + + status: 'success' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of configuring the auto-lock on a lock. + */ + action_type: 'CONFIGURE_AUTO_LOCK' + + /** + * Error associated with the action. + */ + error: null + /** + * Result of the action. + */ + result: null + + status: 'pending' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Action attempt to track the status of configuring the auto-lock on a lock. + */ + action_type: 'CONFIGURE_AUTO_LOCK' + + /** + * Error associated with the action. + */ + error: { + /** + * Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + */ + message: string + + /** + * Type of the error. + */ + type: string + } + + /** + * Result of the action. + */ + result: null + + status: 'error' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Syncing access codes is pending. + */ + action_type: 'SYNC_ACCESS_CODES' + + /** + * Error associated with the action. + */ + error: null + /** + * Result of the action. + */ + result: {} + + status: 'success' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Syncing access codes is pending. + */ + action_type: 'SYNC_ACCESS_CODES' + + /** + * Error associated with the action. + */ + error: null + /** + * Result of the action. + */ + result: null + + status: 'pending' + } + | { + /** + * ID of the action attempt. */ - action_type: 'SET_FAN_MODE' + action_attempt_id: string + + /** + * Syncing access codes is pending. + */ + action_type: 'SYNC_ACCESS_CODES' /** * Error associated with the action. @@ -1307,9 +2174,9 @@ export type ActionAttempt = /** * Result of the action. */ - result: {} + result: null - status: 'success' | 'pending' | 'error' + status: 'error' } | { /** @@ -1318,31 +2185,47 @@ export type ActionAttempt = action_attempt_id: string /** - * Action attempt to track the status of setting the HVAC mode on a thermostat. + * Creating an access code is pending. */ - action_type: 'SET_HVAC_MODE' + action_type: 'CREATE_ACCESS_CODE' /** * Error associated with the action. */ - error: { - /** - * Detailed description of the error. Provides insights into the issue and potentially how to rectify it. - */ - message: string - + error: null + /** + * Result of the action. + */ + result: { /** - * Type of the error. + * Created access code. */ - type: string + access_code: Record } + status: 'success' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Creating an access code is pending. + */ + action_type: 'CREATE_ACCESS_CODE' + + /** + * Error associated with the action. + */ + error: null /** * Result of the action. */ - result: {} + result: null - status: 'success' | 'pending' | 'error' + status: 'pending' } | { /** @@ -1351,9 +2234,9 @@ export type ActionAttempt = action_attempt_id: string /** - * Action attempt to track the status of a climate preset activation. + * Creating an access code is pending. */ - action_type: 'ACTIVATE_CLIMATE_PRESET' + action_type: 'CREATE_ACCESS_CODE' /** * Error associated with the action. @@ -1373,9 +2256,9 @@ export type ActionAttempt = /** * Result of the action. */ - result: {} + result: null - status: 'success' | 'pending' | 'error' + status: 'error' } | { /** @@ -1384,31 +2267,42 @@ export type ActionAttempt = action_attempt_id: string /** - * Action attempt to track the status of simulating a keypad code entry. + * Deleting an access code is pending. */ - action_type: 'SIMULATE_KEYPAD_CODE_ENTRY' + action_type: 'DELETE_ACCESS_CODE' /** * Error associated with the action. */ - error: { - /** - * Detailed description of the error. Provides insights into the issue and potentially how to rectify it. - */ - message: string + error: null + /** + * Result of the action. + */ + result: {} - /** - * Type of the error. - */ - type: string - } + status: 'success' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Deleting an access code is pending. + */ + action_type: 'DELETE_ACCESS_CODE' + /** + * Error associated with the action. + */ + error: null /** * Result of the action. */ - result: {} + result: null - status: 'success' | 'pending' | 'error' + status: 'pending' } | { /** @@ -1417,9 +2311,9 @@ export type ActionAttempt = action_attempt_id: string /** - * Action attempt to track the status of simulating a manual lock action using a keypad. + * Deleting an access code is pending. */ - action_type: 'SIMULATE_MANUAL_LOCK_VIA_KEYPAD' + action_type: 'DELETE_ACCESS_CODE' /** * Error associated with the action. @@ -1439,9 +2333,9 @@ export type ActionAttempt = /** * Result of the action. */ - result: {} + result: null - status: 'success' | 'pending' | 'error' + status: 'error' } | { /** @@ -1450,31 +2344,47 @@ export type ActionAttempt = action_attempt_id: string /** - * Action attempt to track the status of pushing thermostat programs. + * Updating an access code is pending. */ - action_type: 'PUSH_THERMOSTAT_PROGRAMS' + action_type: 'UPDATE_ACCESS_CODE' /** * Error associated with the action. */ - error: { - /** - * Detailed description of the error. Provides insights into the issue and potentially how to rectify it. - */ - message: string - + error: null + /** + * Result of the action. + */ + result: { /** - * Type of the error. + * Updated access code. */ - type: string + access_code?: Record | undefined } + status: 'success' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Updating an access code is pending. + */ + action_type: 'UPDATE_ACCESS_CODE' + + /** + * Error associated with the action. + */ + error: null /** * Result of the action. */ - result: {} + result: null - status: 'success' | 'pending' | 'error' + status: 'pending' } | { /** @@ -1483,9 +2393,9 @@ export type ActionAttempt = action_attempt_id: string /** - * Action attempt to track the status of configuring the auto-lock on a lock. + * Updating an access code is pending. */ - action_type: 'CONFIGURE_AUTO_LOCK' + action_type: 'UPDATE_ACCESS_CODE' /** * Error associated with the action. @@ -1505,9 +2415,9 @@ export type ActionAttempt = /** * Result of the action. */ - result: {} + result: null - status: 'success' | 'pending' | 'error' + status: 'error' } | { /** @@ -1516,31 +2426,47 @@ export type ActionAttempt = action_attempt_id: string /** - * Syncing access codes is pending. + * Creating a noise threshold is pending. */ - action_type: 'SYNC_ACCESS_CODES' + action_type: 'CREATE_NOISE_THRESHOLD' /** * Error associated with the action. */ - error: { - /** - * Detailed description of the error. Provides insights into the issue and potentially how to rectify it. - */ - message: string - + error: null + /** + * Result of the action. + */ + result: { /** - * Type of the error. + * Created noise threshold. */ - type: string + noise_threshold: Record } + status: 'success' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Creating a noise threshold is pending. + */ + action_type: 'CREATE_NOISE_THRESHOLD' + + /** + * Error associated with the action. + */ + error: null /** * Result of the action. */ - result: {} + result: null - status: 'success' | 'pending' | 'error' + status: 'pending' } | { /** @@ -1549,9 +2475,9 @@ export type ActionAttempt = action_attempt_id: string /** - * Creating an access code is pending. + * Creating a noise threshold is pending. */ - action_type: 'CREATE_ACCESS_CODE' + action_type: 'CREATE_NOISE_THRESHOLD' /** * Error associated with the action. @@ -1571,14 +2497,9 @@ export type ActionAttempt = /** * Result of the action. */ - result: { - /** - * Created access code. - */ - access_code: Record - } + result: null - status: 'success' | 'pending' | 'error' + status: 'error' } | { /** @@ -1587,31 +2508,42 @@ export type ActionAttempt = action_attempt_id: string /** - * Deleting an access code is pending. + * Deleting a noise threshold is pending. */ - action_type: 'DELETE_ACCESS_CODE' + action_type: 'DELETE_NOISE_THRESHOLD' /** * Error associated with the action. */ - error: { - /** - * Detailed description of the error. Provides insights into the issue and potentially how to rectify it. - */ - message: string + error: null + /** + * Result of the action. + */ + result: {} - /** - * Type of the error. - */ - type: string - } + status: 'success' + } + | { + /** + * ID of the action attempt. + */ + action_attempt_id: string + + /** + * Deleting a noise threshold is pending. + */ + action_type: 'DELETE_NOISE_THRESHOLD' + /** + * Error associated with the action. + */ + error: null /** * Result of the action. */ - result: {} + result: null - status: 'success' | 'pending' | 'error' + status: 'pending' } | { /** @@ -1620,9 +2552,9 @@ export type ActionAttempt = action_attempt_id: string /** - * Updating an access code is pending. + * Deleting a noise threshold is pending. */ - action_type: 'UPDATE_ACCESS_CODE' + action_type: 'DELETE_NOISE_THRESHOLD' /** * Error associated with the action. @@ -1642,14 +2574,9 @@ export type ActionAttempt = /** * Result of the action. */ - result: { - /** - * Updated access code. - */ - access_code?: Record | undefined - } + result: null - status: 'success' | 'pending' | 'error' + status: 'error' } | { /** @@ -1658,36 +2585,25 @@ export type ActionAttempt = action_attempt_id: string /** - * Creating a noise threshold is pending. + * Updating a noise threshold is pending. */ - action_type: 'CREATE_NOISE_THRESHOLD' + action_type: 'UPDATE_NOISE_THRESHOLD' /** * Error associated with the action. */ - error: { - /** - * Detailed description of the error. Provides insights into the issue and potentially how to rectify it. - */ - message: string - - /** - * Type of the error. - */ - type: string - } - + error: null /** * Result of the action. */ result: { /** - * Created noise threshold. + * Updated noise threshold. */ noise_threshold: Record } - status: 'success' | 'pending' | 'error' + status: 'success' } | { /** @@ -1696,31 +2612,20 @@ export type ActionAttempt = action_attempt_id: string /** - * Deleting a noise threshold is pending. + * Updating a noise threshold is pending. */ - action_type: 'DELETE_NOISE_THRESHOLD' + action_type: 'UPDATE_NOISE_THRESHOLD' /** * Error associated with the action. */ - error: { - /** - * Detailed description of the error. Provides insights into the issue and potentially how to rectify it. - */ - message: string - - /** - * Type of the error. - */ - type: string - } - + error: null /** * Result of the action. */ - result: {} + result: null - status: 'success' | 'pending' | 'error' + status: 'pending' } | { /** @@ -1751,12 +2656,7 @@ export type ActionAttempt = /** * Result of the action. */ - result: { - /** - * Updated noise threshold. - */ - noise_threshold: Record - } + result: null - status: 'success' | 'pending' | 'error' + status: 'error' } diff --git a/test/seam/connect/action-attempt-types.test.ts b/test/seam/connect/action-attempt-types.test.ts new file mode 100644 index 00000000..9a791c55 --- /dev/null +++ b/test/seam/connect/action-attempt-types.test.ts @@ -0,0 +1,95 @@ +import test from 'ava' + +import type { + ActionAttempt, + FailedActionAttempt, + SucceededActionAttempt, +} from '@seamapi/http/connect' + +import { + type ActionAttemptsClient, + resolveActionAttempt, +} from 'lib/resolve-action-attempt.js' + +const expectType = (_value: Expected): void => {} + +type LockDoorActionAttempt = Extract< + ActionAttempt, + { action_type: 'LOCK_DOOR' } +> + +const assertUnnarrowedDereferenceFails = ( + actionAttempt: LockDoorActionAttempt, +): void => { + // @ts-expect-error The result is null unless the status is success. + expectType(actionAttempt.result.was_confirmed_by_device) + + // @ts-expect-error The error is null unless the status is error. + expectType(actionAttempt.error.message) +} + +test('action attempt result and error cannot be dereferenced without narrowing', (t) => { + t.is(typeof assertUnnarrowedDereferenceFails, 'function') +}) + +const assertSuccessNarrowing = (actionAttempt: LockDoorActionAttempt): void => { + if (actionAttempt.status === 'success') { + expectType( + actionAttempt.result.was_confirmed_by_device, + ) + expectType(actionAttempt.error) + } +} + +test('narrowing on success status needs no null check on result', (t) => { + t.is(typeof assertSuccessNarrowing, 'function') +}) + +const assertErrorNarrowing = (actionAttempt: LockDoorActionAttempt): void => { + if (actionAttempt.status === 'error') { + expectType(actionAttempt.error.message) + expectType(actionAttempt.error.type) + expectType(actionAttempt.result) + } +} + +test('narrowing on error status needs no null check on error', (t) => { + t.is(typeof assertErrorNarrowing, 'function') +}) + +const assertResolvedActionAttemptIsSucceeded = async ( + actionAttempt: LockDoorActionAttempt, + actionAttempts: ActionAttemptsClient, +): Promise => { + const resolved = await resolveActionAttempt(actionAttempt, actionAttempts, {}) + expectType>(resolved) + expectType<'success'>(resolved.status) + expectType(resolved.result.was_confirmed_by_device) + expectType(resolved.error) +} + +test('waiting for an action attempt returns the extracted success type', (t) => { + t.is(typeof assertResolvedActionAttemptIsSucceeded, 'function') +}) + +const assertPendingActionAttemptTypes = ( + actionAttempt: Extract, +): void => { + expectType(actionAttempt.error) + expectType(actionAttempt.result) +} + +test('pending action attempts type status-dependent values as null', (t) => { + t.is(typeof assertPendingActionAttemptTypes, 'function') +}) + +const assertFailedActionAttemptHasError = ( + actionAttempt: FailedActionAttempt, +): void => { + expectType(actionAttempt.error.message) + expectType(actionAttempt.result) +} + +test('failed action attempts type the error as non-null', (t) => { + t.is(typeof assertFailedActionAttemptHasError, 'function') +})