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') +})