From e7a1a74b9424dccf2b5efa01a0a93a60de8bf5d6 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Fri, 10 May 2024 10:07:26 -0700 Subject: [PATCH 01/79] WIP --- lib/iotshadow/clientv2.ts | 351 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 lib/iotshadow/clientv2.ts diff --git a/lib/iotshadow/clientv2.ts b/lib/iotshadow/clientv2.ts new file mode 100644 index 00000000..a8f9f0f1 --- /dev/null +++ b/lib/iotshadow/clientv2.ts @@ -0,0 +1,351 @@ + +import {CrtError, mqtt, mqtt5, mqtt_request_response} from 'aws-crt'; + +import * as model from './model'; +import {EventEmitter} from "events"; +import {fromUtf8, toUtf8} from "@aws-sdk/util-utf8-browser"; +import {v4 as uuid} from "uuid"; + +export function validateValueAsTopicSegment(value : any, propertyName?: string, type?: string) : void { + if (value === undefined) { + throw new CrtError("NYI"); + } + + if (typeof value !== 'string') { + throw new CrtError("NYI"); + } + + if (value.includes("/") || value.includes("#") || value.includes("+")) { + throw new CrtError("NYI"); + } +} + +function normalizeGetNamedShadowRequest(value: model.GetNamedShadowRequest) : any { + let normalizedValue : any = {}; + if (value.clientToken) { + normalizedValue.clientToken = value.clientToken; + } + + return normalizedValue; +} + +function buildGetNamedShadowRequestPayload(request: any) : ArrayBuffer { + let value = normalizeGetNamedShadowRequest(request as model.GetNamedShadowRequest); + + return fromUtf8(JSON.stringify(value)); +} + +function buildGetNamedShadowRequestSubscriptions(request: any) : Array { + let typedRequest: model.GetNamedShadowRequest = request; + + return new Array( + `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/get/+` + ); +} + +function deserializeGetShadowResponse(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + +function deserializeErrorResponse(payload: ArrayBuffer): any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + +function buildGetNamedShadowRequestResponsePaths(request: any) : Array { + let typedRequest: model.GetNamedShadowRequest = request; + + return new Array( + { + topic: `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/get/accepted`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeGetShadowResponse, + }, + { + topic: `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/get/rejected`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeErrorResponse, + }, + ) +} + +function buildGetNamedShadowRequestPublishTopic(request: any) : string { + let typedRequest: model.GetNamedShadowRequest = request; + + return `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/get`; +} + +function applyCorrelationTokenToGetNamedShadowRequest(request: any) : [any, string | undefined] { + let typedRequest: model.GetNamedShadowRequest = request; + + let correlationToken = uuid(); + + typedRequest.clientToken = correlationToken; + + return [typedRequest, correlationToken]; +} + +function createRequestResponseOperationServiceModelMap() : Map { + return new Map([ + ["getNamedShadow", { + inputShapeName: "GetNamedShadowRequest", + payloadTransformer: buildGetNamedShadowRequestPayload, + subscriptionGenerator: buildGetNamedShadowRequestSubscriptions, + responsePathGenerator: buildGetNamedShadowRequestResponsePaths, + publishTopicGenerator: buildGetNamedShadowRequestPublishTopic, + correlationTokenApplicator: applyCorrelationTokenToGetNamedShadowRequest, + }], + ]); +} + +function buildNamedShadowDeltaUpdatedSubscriptionTopicFilter(config: any) : string { + const typedConfig : model.NamedShadowDeltaUpdatedSubscriptionRequest = config; + + return `$aws/things/${typedConfig.thingName}/shadow/name/${typedConfig.shadowName}/update/delta`; +} + +function deserializeNamedShadowDeltaUpdatedPayload(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + +function createStreamingOperationServiceModelMap() : Map { + return new Map([ + ["createNamedShadowDeltaUpdatedEventStream", { + inputShapeName : "NamedShadowDeltaUpdatedSubscriptionRequest", + subscriptionGenerator: buildNamedShadowDeltaUpdatedSubscriptionTopicFilter, + deserializer: deserializeNamedShadowDeltaUpdatedPayload, + }], + ]); +} + +function validateGetNamedShadowRequest(request: any) : void { + let typedRequest : model.GetNamedShadowRequest = request; + + validateValueAsTopicSegment(typedRequest.thingName, "thingName", "GetNamedShadowRequest"); + validateValueAsTopicSegment(typedRequest.shadowName, "shadowName", "GetNamedShadowRequest"); +} + +function validateNamedShadowDeltaUpdatedSubscriptionRequest(request: any) : void { + let typedRequest : model.NamedShadowDeltaUpdatedSubscriptionRequest = request; + + validateValueAsTopicSegment(typedRequest.thingName, "thingName", "NamedShadowDeltaUpdatedSubscriptionRequest"); + validateValueAsTopicSegment(typedRequest.shadowName, "shadowName", "NamedShadowDeltaUpdatedSubscriptionRequest"); +} + +function createValidatorMap() : Map void> { + return new Map void>([ + ["GetNamedShadowRequest", validateGetNamedShadowRequest], + ["NamedShadowDeltaUpdatedSubscriptionRequest", validateNamedShadowDeltaUpdatedSubscriptionRequest] + ]); +} + +function makeServiceModel() : RequestResponseServiceModel { + let model : RequestResponseServiceModel = { + requestResponseOperations: createRequestResponseOperationServiceModelMap(), + streamingOperations: createStreamingOperationServiceModelMap(), + shapeValidators: createValidatorMap() + }; + + return model; +} + +export class IotShadowClientv2 { + private rrClient : mqtt_request_response.RequestResponseClient; + private serviceModel : RequestResponseServiceModel; + + private constructor(rrClient: mqtt_request_response.RequestResponseClient) { + this.rrClient = rrClient; + this.serviceModel = makeServiceModel(); + } + + static new_from_mqtt311(protocolClient: mqtt.MqttClientConnection, options: mqtt_request_response.RequestResponseClientOptions) : IotShadowClientv2 { + let rrClient = mqtt_request_response.RequestResponseClient.newFromMqtt311(protocolClient, options); + let client = new IotShadowClientv2(rrClient); + + return client; + } + + static newFromMqtt5(protocolClient: mqtt5.Mqtt5Client, options: mqtt_request_response.RequestResponseClientOptions) : IotShadowClientv2 { + let rrClient = mqtt_request_response.RequestResponseClient.newFromMqtt5(protocolClient, options); + let client = new IotShadowClientv2(rrClient); + + return client; + } + + close() { + this.rrClient.close(); + } + + async getNamedShadow(request: model.GetNamedShadowRequest) : Promise { + let rrConfig : RequestResponseOperationConfig = { + operationName: "getNamedShadow", + serviceModel: this.serviceModel, + client: this.rrClient, + request: request + }; + + let operation : RequestResponseOperation = RequestResponseOperation.create(rrConfig); + + let response = await operation.submit(); + + return response; + } + + createNamedShadowDeltaUpdatedStream(config: model.NamedShadowDeltaUpdatedSubscriptionRequest) : StreamingOperation { + let streamingOperationConfig : StreamingOperationConfig = { + operationName: "createNamedShadowDeltaUpdatedEventStream", + serviceModel: this.serviceModel, + client: this.rrClient, + modelConfig: config, + }; + + return StreamingOperation.create(streamingOperationConfig); + } +} + + + + + +export interface StreamingOperationConfig { + operationName: string, + + serviceModel: RequestResponseServiceModel, + + client: mqtt_request_response.RequestResponseClient, + + modelConfig: any, +} + +export interface RequestResponseOperationConfig { + operationName: string, + + serviceModel: RequestResponseServiceModel, + + client: mqtt_request_response.RequestResponseClient, + + request: any, +} + +export interface RequestResponsePath { + topic: string, + + correlationTokenJsonPath?: string, + + deserializer: (payload: ArrayBuffer) => any; +} + +export interface RequestResponseOperationModel { + inputShapeName: string; + + payloadTransformer: (request: any) => ArrayBuffer; + + subscriptionGenerator: (request: any) => Array; + + responsePathGenerator: (request: any) => Array; + + publishTopicGenerator: (request: any) => string; + + correlationTokenApplicator: (request: any) => [any, string | undefined]; +} + +export interface StreamingOperationModel { + inputShapeName: string; + + subscriptionGenerator: (config: any) => string; + + deserializer: (payload: ArrayBuffer) => any; +} + +export interface RequestResponseServiceModel { + + // operation name -> operation model + requestResponseOperations: Map, + + // operation name -> operation model + streamingOperations: Map, + + // shape name -> validator function + shapeValidators: Map void>; +} + +export class RequestResponseOperation extends EventEmitter { + + private config: RequestResponseOperationConfig; + + private constructor(config: RequestResponseOperationConfig) { + super(); + this.config = config; + } + + static create(config: RequestResponseOperationConfig) : RequestResponseOperation { + let operation = new RequestResponseOperation(config); + + return operation; + } + + async submit() : Promise { + + } +} + +export class StreamingOperation extends EventEmitter { + private operation: mqtt_request_response.StreamingOperationBase; + + private constructor(config: StreamingOperationConfig) { + super(); + + // validate + let streamingOperationModel = config.serviceModel.streamingOperations.get(config.operationName); + if (!streamingOperationModel) { + throw new CrtError("NYI"); + } + + let validator = config.serviceModel.shapeValidators.get(streamingOperationModel.inputShapeName); + if (!validator) { + throw new CrtError("NYI"); + } + + validator(config.modelConfig); + + let streamOptions : mqtt_request_response.StreamingOperationOptions = { + subscriptionTopicFilter: streamingOperationModel.subscriptionGenerator(config.modelConfig), + }; + + // create native operation + this.operation = config.client.createStream(streamOptions); + } + + static create(config: StreamingOperationConfig) : StreamingOperation { + let operation = new StreamingOperation(config); + + return operation; + } + + open() { + this.operation.open(); + } + + close() { + this.operation.close(); + } +} + + + +// validate request + +// map request to serializable payload object + +// serialize payload to buffer + +// map request to response path set and other options + +// await submit request + +// deserialize response appropriately \ No newline at end of file From ee1d07b3fa9da647bf132fd195e1000de599d997 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Fri, 10 May 2024 13:11:01 -0700 Subject: [PATCH 02/79] implementation sketch --- lib/iotshadow/clientv2.ts | 95 ++++++++++++++++++++++++++++----------- 1 file changed, 68 insertions(+), 27 deletions(-) diff --git a/lib/iotshadow/clientv2.ts b/lib/iotshadow/clientv2.ts index a8f9f0f1..05e145e5 100644 --- a/lib/iotshadow/clientv2.ts +++ b/lib/iotshadow/clientv2.ts @@ -189,11 +189,7 @@ export class IotShadowClientv2 { request: request }; - let operation : RequestResponseOperation = RequestResponseOperation.create(rrConfig); - - let response = await operation.submit(); - - return response; + return await doRequestResponse(rrConfig); } createNamedShadowDeltaUpdatedStream(config: model.NamedShadowDeltaUpdatedSubscriptionRequest) : StreamingOperation { @@ -232,12 +228,14 @@ export interface RequestResponseOperationConfig { request: any, } +type ResponseDeserializer = (payload: ArrayBuffer) => any; + export interface RequestResponsePath { topic: string, correlationTokenJsonPath?: string, - deserializer: (payload: ArrayBuffer) => any; + deserializer: ResponseDeserializer; } export interface RequestResponseOperationModel { @@ -274,26 +272,80 @@ export interface RequestResponseServiceModel { shapeValidators: Map void>; } -export class RequestResponseOperation extends EventEmitter { +// validate request - private config: RequestResponseOperationConfig; +// map request to serializable payload object - private constructor(config: RequestResponseOperationConfig) { - super(); - this.config = config; - } +// serialize payload to buffer - static create(config: RequestResponseOperationConfig) : RequestResponseOperation { - let operation = new RequestResponseOperation(config); +// map request to response path set and other options - return operation; +// await submit request + +// deserialize response appropriately + +function buildResponseDeserializerMap(paths: Array) : Map { + return new Map( + paths.map((path) => { + return [path.topic, path.deserializer]; + }) + ); +} + +function buildResponsePaths(paths: Array) : Array { + return paths.map((path) => { + return { + topic: path.topic, + correlationTokenJsonPath: path.correlationTokenJsonPath, + }; + }); +} + +async function doRequestResponse(options: RequestResponseOperationConfig) : Promise { + let operationModel = options.serviceModel.requestResponseOperations.get(options.operationName); + if (!operationModel) { + throw new CrtError("NYI"); + } + + let validator = options.serviceModel.shapeValidators.get(operationModel.inputShapeName); + if (!validator) { + throw new CrtError("NYI"); } - async submit() : Promise { + validator(options.request); + + let publishTopic = operationModel.publishTopicGenerator(options.request); + let subscriptionsNeeded = operationModel.subscriptionGenerator(options.request); + let modelPaths = operationModel.responsePathGenerator(options.request); + let deserializerMap = buildResponseDeserializerMap(modelPaths); + let responsePaths = buildResponsePaths(modelPaths); + let [request, correlationToken] = operationModel.correlationTokenApplicator(options.request); + + let payload = operationModel.payloadTransformer(request); + + let requestOptions : mqtt_request_response.RequestResponseOperationOptions = { + subscriptionTopicFilters : subscriptionsNeeded, + responsePaths: responsePaths, + publishTopic: publishTopic, + payload: payload, + correlationToken: correlationToken + }; + + let response = await options.client.submitRequest(requestOptions); + + let responseTopic = response.topic; + let responsePayload = response.payload; + + let deserializer = deserializerMap.get(responseTopic); + if (!deserializer) { + throw new CrtError("NYI"); } + + return deserializer(responsePayload) as ResponseType; } + export class StreamingOperation extends EventEmitter { private operation: mqtt_request_response.StreamingOperationBase; @@ -338,14 +390,3 @@ export class StreamingOperation extends EventEmitter { -// validate request - -// map request to serializable payload object - -// serialize payload to buffer - -// map request to response path set and other options - -// await submit request - -// deserialize response appropriately \ No newline at end of file From 412ac6d5b597c873899c2a0f61ab37da28f62445 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Mon, 13 May 2024 16:13:34 -0700 Subject: [PATCH 03/79] Checkpoint --- lib/index.ts | 4 +- lib/iotshadow/clientv2.ts | 389 ++++------------------------- lib/iotshadow/clientv2_utils.ts | 250 ++++++++++++++++++ lib/mqtt_request_response.ts | 177 +++++++++++++ lib/mqtt_request_response_utils.ts | 171 +++++++++++++ 5 files changed, 644 insertions(+), 347 deletions(-) create mode 100644 lib/iotshadow/clientv2_utils.ts create mode 100644 lib/mqtt_request_response.ts create mode 100644 lib/mqtt_request_response_utils.ts diff --git a/lib/index.ts b/lib/index.ts index db51661a..b6c6c9ae 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -20,6 +20,7 @@ import * as iotjobs from './iotjobs/iotjobsclient'; import * as iotshadow from './iotshadow/iotshadowclient'; import * as eventstream_rpc from './eventstream_rpc'; import * as greengrasscoreipc from './greengrasscoreipc'; +import * as mqtt_request_response from './mqtt_request_response'; import { auth, @@ -44,5 +45,6 @@ export { iotshadow, mqtt, mqtt5, - CrtError + mqtt_request_response, + CrtError, } diff --git a/lib/iotshadow/clientv2.ts b/lib/iotshadow/clientv2.ts index 05e145e5..425ee09a 100644 --- a/lib/iotshadow/clientv2.ts +++ b/lib/iotshadow/clientv2.ts @@ -1,177 +1,36 @@ - -import {CrtError, mqtt, mqtt5, mqtt_request_response} from 'aws-crt'; - +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + * + * This file is generated + */ + +import {mqtt, mqtt5} from 'aws-crt'; +import {mqtt_request_response as mqtt_rr_internal} from 'aws-crt'; +import * as mqtt_request_response from '../mqtt_request_response'; +import * as mqtt_request_response_utils from '../mqtt_request_response_utils'; import * as model from './model'; -import {EventEmitter} from "events"; -import {fromUtf8, toUtf8} from "@aws-sdk/util-utf8-browser"; -import {v4 as uuid} from "uuid"; - -export function validateValueAsTopicSegment(value : any, propertyName?: string, type?: string) : void { - if (value === undefined) { - throw new CrtError("NYI"); - } - - if (typeof value !== 'string') { - throw new CrtError("NYI"); - } - - if (value.includes("/") || value.includes("#") || value.includes("+")) { - throw new CrtError("NYI"); - } -} - -function normalizeGetNamedShadowRequest(value: model.GetNamedShadowRequest) : any { - let normalizedValue : any = {}; - if (value.clientToken) { - normalizedValue.clientToken = value.clientToken; - } - - return normalizedValue; -} - -function buildGetNamedShadowRequestPayload(request: any) : ArrayBuffer { - let value = normalizeGetNamedShadowRequest(request as model.GetNamedShadowRequest); - - return fromUtf8(JSON.stringify(value)); -} - -function buildGetNamedShadowRequestSubscriptions(request: any) : Array { - let typedRequest: model.GetNamedShadowRequest = request; - - return new Array( - `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/get/+` - ); -} - -function deserializeGetShadowResponse(payload: ArrayBuffer) : any { - const payload_text = toUtf8(new Uint8Array(payload)); - - return JSON.parse(payload_text); -} - -function deserializeErrorResponse(payload: ArrayBuffer): any { - const payload_text = toUtf8(new Uint8Array(payload)); - - return JSON.parse(payload_text); -} - -function buildGetNamedShadowRequestResponsePaths(request: any) : Array { - let typedRequest: model.GetNamedShadowRequest = request; - - return new Array( - { - topic: `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/get/accepted`, - correlationTokenJsonPath: "clientToken", - deserializer: deserializeGetShadowResponse, - }, - { - topic: `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/get/rejected`, - correlationTokenJsonPath: "clientToken", - deserializer: deserializeErrorResponse, - }, - ) -} - -function buildGetNamedShadowRequestPublishTopic(request: any) : string { - let typedRequest: model.GetNamedShadowRequest = request; - - return `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/get`; -} - -function applyCorrelationTokenToGetNamedShadowRequest(request: any) : [any, string | undefined] { - let typedRequest: model.GetNamedShadowRequest = request; - - let correlationToken = uuid(); - - typedRequest.clientToken = correlationToken; - - return [typedRequest, correlationToken]; -} - -function createRequestResponseOperationServiceModelMap() : Map { - return new Map([ - ["getNamedShadow", { - inputShapeName: "GetNamedShadowRequest", - payloadTransformer: buildGetNamedShadowRequestPayload, - subscriptionGenerator: buildGetNamedShadowRequestSubscriptions, - responsePathGenerator: buildGetNamedShadowRequestResponsePaths, - publishTopicGenerator: buildGetNamedShadowRequestPublishTopic, - correlationTokenApplicator: applyCorrelationTokenToGetNamedShadowRequest, - }], - ]); -} - -function buildNamedShadowDeltaUpdatedSubscriptionTopicFilter(config: any) : string { - const typedConfig : model.NamedShadowDeltaUpdatedSubscriptionRequest = config; - - return `$aws/things/${typedConfig.thingName}/shadow/name/${typedConfig.shadowName}/update/delta`; -} - -function deserializeNamedShadowDeltaUpdatedPayload(payload: ArrayBuffer) : any { - const payload_text = toUtf8(new Uint8Array(payload)); - - return JSON.parse(payload_text); -} +import * as clientv2_utils from './clientv2_utils'; -function createStreamingOperationServiceModelMap() : Map { - return new Map([ - ["createNamedShadowDeltaUpdatedEventStream", { - inputShapeName : "NamedShadowDeltaUpdatedSubscriptionRequest", - subscriptionGenerator: buildNamedShadowDeltaUpdatedSubscriptionTopicFilter, - deserializer: deserializeNamedShadowDeltaUpdatedPayload, - }], - ]); -} - -function validateGetNamedShadowRequest(request: any) : void { - let typedRequest : model.GetNamedShadowRequest = request; - - validateValueAsTopicSegment(typedRequest.thingName, "thingName", "GetNamedShadowRequest"); - validateValueAsTopicSegment(typedRequest.shadowName, "shadowName", "GetNamedShadowRequest"); -} - -function validateNamedShadowDeltaUpdatedSubscriptionRequest(request: any) : void { - let typedRequest : model.NamedShadowDeltaUpdatedSubscriptionRequest = request; - - validateValueAsTopicSegment(typedRequest.thingName, "thingName", "NamedShadowDeltaUpdatedSubscriptionRequest"); - validateValueAsTopicSegment(typedRequest.shadowName, "shadowName", "NamedShadowDeltaUpdatedSubscriptionRequest"); -} - -function createValidatorMap() : Map void> { - return new Map void>([ - ["GetNamedShadowRequest", validateGetNamedShadowRequest], - ["NamedShadowDeltaUpdatedSubscriptionRequest", validateNamedShadowDeltaUpdatedSubscriptionRequest] - ]); -} - -function makeServiceModel() : RequestResponseServiceModel { - let model : RequestResponseServiceModel = { - requestResponseOperations: createRequestResponseOperationServiceModelMap(), - streamingOperations: createStreamingOperationServiceModelMap(), - shapeValidators: createValidatorMap() - }; - - return model; -} export class IotShadowClientv2 { - private rrClient : mqtt_request_response.RequestResponseClient; - private serviceModel : RequestResponseServiceModel; + private rrClient : mqtt_rr_internal.RequestResponseClient; + private serviceModel : mqtt_request_response_utils.RequestResponseServiceModel; - private constructor(rrClient: mqtt_request_response.RequestResponseClient) { + private constructor(rrClient: mqtt_rr_internal.RequestResponseClient) { this.rrClient = rrClient; - this.serviceModel = makeServiceModel(); + this.serviceModel = clientv2_utils.makeServiceModel(); } - static new_from_mqtt311(protocolClient: mqtt.MqttClientConnection, options: mqtt_request_response.RequestResponseClientOptions) : IotShadowClientv2 { - let rrClient = mqtt_request_response.RequestResponseClient.newFromMqtt311(protocolClient, options); + static new_from_mqtt311(protocolClient: mqtt.MqttClientConnection, options: mqtt_rr_internal.RequestResponseClientOptions) : IotShadowClientv2 { + let rrClient = mqtt_rr_internal.RequestResponseClient.newFromMqtt311(protocolClient, options); let client = new IotShadowClientv2(rrClient); return client; } - static newFromMqtt5(protocolClient: mqtt5.Mqtt5Client, options: mqtt_request_response.RequestResponseClientOptions) : IotShadowClientv2 { - let rrClient = mqtt_request_response.RequestResponseClient.newFromMqtt5(protocolClient, options); + static newFromMqtt5(protocolClient: mqtt5.Mqtt5Client, options: mqtt_rr_internal.RequestResponseClientOptions) : IotShadowClientv2 { + let rrClient = mqtt_rr_internal.RequestResponseClient.newFromMqtt5(protocolClient, options); let client = new IotShadowClientv2(rrClient); return client; @@ -181,210 +40,48 @@ export class IotShadowClientv2 { this.rrClient.close(); } - async getNamedShadow(request: model.GetNamedShadowRequest) : Promise { - let rrConfig : RequestResponseOperationConfig = { - operationName: "getNamedShadow", + async deleteNamedShadow(request: model.DeleteNamedShadowRequest) : Promise { + let rrConfig : mqtt_request_response_utils.RequestResponseOperationConfig = { + operationName: "DeleteNamedShadow", serviceModel: this.serviceModel, client: this.rrClient, request: request }; - return await doRequestResponse(rrConfig); + return await mqtt_request_response_utils.doRequestResponse(rrConfig); } - createNamedShadowDeltaUpdatedStream(config: model.NamedShadowDeltaUpdatedSubscriptionRequest) : StreamingOperation { - let streamingOperationConfig : StreamingOperationConfig = { - operationName: "createNamedShadowDeltaUpdatedEventStream", + async getNamedShadow(request: model.GetNamedShadowRequest) : Promise { + let rrConfig : mqtt_request_response_utils.RequestResponseOperationConfig = { + operationName: "GetNamedShadow", serviceModel: this.serviceModel, client: this.rrClient, - modelConfig: config, + request: request }; - return StreamingOperation.create(streamingOperationConfig); + return await mqtt_request_response_utils.doRequestResponse(rrConfig); } -} - - - - - -export interface StreamingOperationConfig { - operationName: string, - - serviceModel: RequestResponseServiceModel, - - client: mqtt_request_response.RequestResponseClient, - - modelConfig: any, -} - -export interface RequestResponseOperationConfig { - operationName: string, - - serviceModel: RequestResponseServiceModel, - - client: mqtt_request_response.RequestResponseClient, - - request: any, -} - -type ResponseDeserializer = (payload: ArrayBuffer) => any; - -export interface RequestResponsePath { - topic: string, - - correlationTokenJsonPath?: string, - - deserializer: ResponseDeserializer; -} - -export interface RequestResponseOperationModel { - inputShapeName: string; - - payloadTransformer: (request: any) => ArrayBuffer; - - subscriptionGenerator: (request: any) => Array; - - responsePathGenerator: (request: any) => Array; - - publishTopicGenerator: (request: any) => string; - - correlationTokenApplicator: (request: any) => [any, string | undefined]; -} - -export interface StreamingOperationModel { - inputShapeName: string; - - subscriptionGenerator: (config: any) => string; - - deserializer: (payload: ArrayBuffer) => any; -} - -export interface RequestResponseServiceModel { - - // operation name -> operation model - requestResponseOperations: Map, - - // operation name -> operation model - streamingOperations: Map, - - // shape name -> validator function - shapeValidators: Map void>; -} - -// validate request - -// map request to serializable payload object - -// serialize payload to buffer - -// map request to response path set and other options - -// await submit request - -// deserialize response appropriately - -function buildResponseDeserializerMap(paths: Array) : Map { - return new Map( - paths.map((path) => { - return [path.topic, path.deserializer]; - }) - ); -} -function buildResponsePaths(paths: Array) : Array { - return paths.map((path) => { - return { - topic: path.topic, - correlationTokenJsonPath: path.correlationTokenJsonPath, + createNamedShadowDeltaUpdatedStream(config: model.NamedShadowDeltaUpdatedSubscriptionRequest) : mqtt_request_response.StreamingOperation { + let streamingOperationConfig : mqtt_request_response_utils.StreamingOperationConfig = { + operationName: "createNamedShadowDeltaUpdatedEventStream", + serviceModel: this.serviceModel, + client: this.rrClient, + modelConfig: config, }; - }); -} - -async function doRequestResponse(options: RequestResponseOperationConfig) : Promise { - let operationModel = options.serviceModel.requestResponseOperations.get(options.operationName); - if (!operationModel) { - throw new CrtError("NYI"); - } - - let validator = options.serviceModel.shapeValidators.get(operationModel.inputShapeName); - if (!validator) { - throw new CrtError("NYI"); - } - validator(options.request); - - let publishTopic = operationModel.publishTopicGenerator(options.request); - let subscriptionsNeeded = operationModel.subscriptionGenerator(options.request); - let modelPaths = operationModel.responsePathGenerator(options.request); - let deserializerMap = buildResponseDeserializerMap(modelPaths); - let responsePaths = buildResponsePaths(modelPaths); - - let [request, correlationToken] = operationModel.correlationTokenApplicator(options.request); - - let payload = operationModel.payloadTransformer(request); - - let requestOptions : mqtt_request_response.RequestResponseOperationOptions = { - subscriptionTopicFilters : subscriptionsNeeded, - responsePaths: responsePaths, - publishTopic: publishTopic, - payload: payload, - correlationToken: correlationToken - }; - - let response = await options.client.submitRequest(requestOptions); - - let responseTopic = response.topic; - let responsePayload = response.payload; - - let deserializer = deserializerMap.get(responseTopic); - if (!deserializer) { - throw new CrtError("NYI"); + return mqtt_request_response.StreamingOperation.create(streamingOperationConfig); } - return deserializer(responsePayload) as ResponseType; -} - - -export class StreamingOperation extends EventEmitter { - private operation: mqtt_request_response.StreamingOperationBase; - - private constructor(config: StreamingOperationConfig) { - super(); - - // validate - let streamingOperationModel = config.serviceModel.streamingOperations.get(config.operationName); - if (!streamingOperationModel) { - throw new CrtError("NYI"); - } - - let validator = config.serviceModel.shapeValidators.get(streamingOperationModel.inputShapeName); - if (!validator) { - throw new CrtError("NYI"); - } - - validator(config.modelConfig); - - let streamOptions : mqtt_request_response.StreamingOperationOptions = { - subscriptionTopicFilter: streamingOperationModel.subscriptionGenerator(config.modelConfig), + createNamedShadowUpdatedStream(config: model.NamedShadowUpdatedSubscriptionRequest) : mqtt_request_response.StreamingOperation { + let streamingOperationConfig : mqtt_request_response_utils.StreamingOperationConfig = { + operationName: "createNamedShadowUpdatedEventStream", + serviceModel: this.serviceModel, + client: this.rrClient, + modelConfig: config, }; - // create native operation - this.operation = config.client.createStream(streamOptions); - } - - static create(config: StreamingOperationConfig) : StreamingOperation { - let operation = new StreamingOperation(config); - - return operation; - } - - open() { - this.operation.open(); - } - - close() { - this.operation.close(); + return mqtt_request_response.StreamingOperation.create(streamingOperationConfig); } } diff --git a/lib/iotshadow/clientv2_utils.ts b/lib/iotshadow/clientv2_utils.ts new file mode 100644 index 00000000..811e60fa --- /dev/null +++ b/lib/iotshadow/clientv2_utils.ts @@ -0,0 +1,250 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + * + * This file is generated + */ + + +import * as model from "./model"; +import {fromUtf8, toUtf8} from "@aws-sdk/util-utf8-browser"; +import * as mqtt_request_response_utils from "../mqtt_request_response_utils"; +import {v4 as uuid} from "uuid"; + +function normalizeGetNamedShadowRequest(value: model.GetNamedShadowRequest) : any { + let normalizedValue : any = {}; + if (value.clientToken) { + normalizedValue.clientToken = value.clientToken; + } + + return normalizedValue; +} + +function buildGetNamedShadowRequestPayload(request: any) : ArrayBuffer { + let value = normalizeGetNamedShadowRequest(request as model.GetNamedShadowRequest); + + return fromUtf8(JSON.stringify(value)); +} + +function buildGetNamedShadowRequestSubscriptions(request: any) : Array { + let typedRequest: model.GetNamedShadowRequest = request; + + return new Array( + `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/get/+` + ); +} + +function deserializeGetShadowResponse(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + +function deserializeErrorResponse(payload: ArrayBuffer): any { + const payload_text = toUtf8(new Uint8Array(payload)); + let errorResponse = JSON.parse(payload_text); + + throw mqtt_request_response_utils.createServiceError("Operation failed with modeled service error", undefined, errorResponse); +} + +function buildGetNamedShadowRequestResponsePaths(request: any) : Array { + let typedRequest: model.GetNamedShadowRequest = request; + + return new Array( + { + topic: `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/get/accepted`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeGetShadowResponse, + }, + { + topic: `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/get/rejected`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeErrorResponse, + }, + ) +} + +function buildGetNamedShadowRequestPublishTopic(request: any) : string { + let typedRequest: model.GetNamedShadowRequest = request; + + return `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/get`; +} + +function applyCorrelationTokenToGetNamedShadowRequest(request: any) : [any, string | undefined] { + let typedRequest: model.GetNamedShadowRequest = request; + + let correlationToken = uuid(); + + typedRequest.clientToken = correlationToken; + + return [typedRequest, correlationToken]; +} + +function normalizeDeleteNamedShadowRequest(value: model.DeleteNamedShadowRequest) : any { + let normalizedValue : any = {}; + if (value.clientToken) { + normalizedValue.clientToken = value.clientToken; + } + + return normalizedValue; +} + +function buildDeleteNamedShadowRequestPayload(request: any) : ArrayBuffer { + let value = normalizeDeleteNamedShadowRequest(request as model.DeleteNamedShadowRequest); + + return fromUtf8(JSON.stringify(value)); +} + +function buildDeleteNamedShadowRequestSubscriptions(request: any) : Array { + let typedRequest: model.DeleteNamedShadowRequest = request; + + return new Array( + `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/delete/+` + ); +} + +function deserializeDeleteShadowResponse(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + +function buildDeleteNamedShadowRequestResponsePaths(request: any) : Array { + let typedRequest: model.DeleteNamedShadowRequest = request; + + return new Array( + { + topic: `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/delete/accepted`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeDeleteShadowResponse, + }, + { + topic: `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/delete/rejected`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeErrorResponse, + }, + ) +} + +function buildDeleteNamedShadowRequestPublishTopic(request: any) : string { + let typedRequest: model.DeleteNamedShadowRequest = request; + + return `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/delete`; +} + +function applyCorrelationTokenToDeleteNamedShadowRequest(request: any) : [any, string | undefined] { + let typedRequest: model.DeleteNamedShadowRequest = request; + + let correlationToken = uuid(); + + typedRequest.clientToken = correlationToken; + + return [typedRequest, correlationToken]; +} + +function createRequestResponseOperationServiceModelMap() : Map { + return new Map([ + ["GetNamedShadow", { + inputShapeName: "GetNamedShadowRequest", + payloadTransformer: buildGetNamedShadowRequestPayload, + subscriptionGenerator: buildGetNamedShadowRequestSubscriptions, + responsePathGenerator: buildGetNamedShadowRequestResponsePaths, + publishTopicGenerator: buildGetNamedShadowRequestPublishTopic, + correlationTokenApplicator: applyCorrelationTokenToGetNamedShadowRequest, + }], + ["DeleteNamedShadow", { + inputShapeName: "DeleteNamedShadowRequest", + payloadTransformer: buildDeleteNamedShadowRequestPayload, + subscriptionGenerator: buildDeleteNamedShadowRequestSubscriptions, + responsePathGenerator: buildDeleteNamedShadowRequestResponsePaths, + publishTopicGenerator: buildDeleteNamedShadowRequestPublishTopic, + correlationTokenApplicator: applyCorrelationTokenToDeleteNamedShadowRequest, + }], + ]); +} + +function buildNamedShadowDeltaUpdatedSubscriptionTopicFilter(config: any) : string { + const typedConfig : model.NamedShadowDeltaUpdatedSubscriptionRequest = config; + + return `$aws/things/${typedConfig.thingName}/shadow/name/${typedConfig.shadowName}/update/delta`; +} + +function deserializeNamedShadowDeltaUpdatedPayload(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + +function buildNamedShadowUpdatedSubscriptionTopicFilter(config: any) : string { + const typedConfig : model.NamedShadowUpdatedSubscriptionRequest = config; + + return `$aws/things/${typedConfig.thingName}/shadow/name/${typedConfig.shadowName}/update/document`; +} + +function deserializeNamedShadowUpdatedPayload(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + +function createStreamingOperationServiceModelMap() : Map { + return new Map([ + ["createNamedShadowDeltaUpdatedEventStream", { + inputShapeName : "NamedShadowDeltaUpdatedSubscriptionRequest", + subscriptionGenerator: buildNamedShadowDeltaUpdatedSubscriptionTopicFilter, + deserializer: deserializeNamedShadowDeltaUpdatedPayload, + }], + ["createNamedShadowUpdatedEventStream", { + inputShapeName : "NamedShadowUpdatedSubscriptionRequest", + subscriptionGenerator: buildNamedShadowUpdatedSubscriptionTopicFilter, + deserializer: deserializeNamedShadowUpdatedPayload, + }], + ]); +} + +function validateDeleteNamedShadowRequest(request: any) : void { + let typedRequest : model.DeleteNamedShadowRequest = request; + + mqtt_request_response_utils.validateValueAsTopicSegment(typedRequest.thingName, "thingName", "DeleteNamedShadowRequest"); + mqtt_request_response_utils.validateValueAsTopicSegment(typedRequest.shadowName, "shadowName", "DeleteNamedShadowRequest"); +} + +function validateGetNamedShadowRequest(request: any) : void { + let typedRequest : model.GetNamedShadowRequest = request; + + mqtt_request_response_utils.validateValueAsTopicSegment(typedRequest.thingName, "thingName", "GetNamedShadowRequest"); + mqtt_request_response_utils.validateValueAsTopicSegment(typedRequest.shadowName, "shadowName", "GetNamedShadowRequest"); +} + +function validateNamedShadowDeltaUpdatedSubscriptionRequest(request: any) : void { + let typedRequest : model.NamedShadowDeltaUpdatedSubscriptionRequest = request; + + mqtt_request_response_utils.validateValueAsTopicSegment(typedRequest.thingName, "thingName", "NamedShadowDeltaUpdatedSubscriptionRequest"); + mqtt_request_response_utils.validateValueAsTopicSegment(typedRequest.shadowName, "shadowName", "NamedShadowDeltaUpdatedSubscriptionRequest"); +} + +function validateNamedShadowUpdatedSubscriptionRequest(request: any) : void { + let typedRequest : model.NamedShadowUpdatedSubscriptionRequest = request; + + mqtt_request_response_utils.validateValueAsTopicSegment(typedRequest.thingName, "thingName", "NamedShadowUpdatedSubscriptionRequest"); + mqtt_request_response_utils.validateValueAsTopicSegment(typedRequest.shadowName, "shadowName", "NamedShadowUpdatedSubscriptionRequest"); +} + +function createValidatorMap() : Map void> { + return new Map void>([ + ["DeleteNamedShadowRequest", validateDeleteNamedShadowRequest], + ["GetNamedShadowRequest", validateGetNamedShadowRequest], + ["NamedShadowDeltaUpdatedSubscriptionRequest", validateNamedShadowDeltaUpdatedSubscriptionRequest], + ["NamedShadowUpdatedSubscriptionRequest", validateNamedShadowUpdatedSubscriptionRequest] + ]); +} + +export function makeServiceModel() : mqtt_request_response_utils.RequestResponseServiceModel { + let model : mqtt_request_response_utils.RequestResponseServiceModel = { + requestResponseOperations: createRequestResponseOperationServiceModelMap(), + streamingOperations: createStreamingOperationServiceModelMap(), + shapeValidators: createValidatorMap() + }; + + return model; +} \ No newline at end of file diff --git a/lib/mqtt_request_response.ts b/lib/mqtt_request_response.ts new file mode 100644 index 00000000..af81396c --- /dev/null +++ b/lib/mqtt_request_response.ts @@ -0,0 +1,177 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +import {EventEmitter} from "events"; +import {CrtError, mqtt_request_response} from "aws-crt"; +import * as mqtt_request_response_utils from "./mqtt_request_response_utils"; + +export interface IncomingPublishEvent { + message: T +} + +/** + * Signature for a handler that listens to incoming publish events. + */ +export type IncomingPublishListener = (eventData: IncomingPublishEvent) => void; + +export interface IncomingPublishErrorEvent { + payload: ArrayBuffer, + + error: ServiceError, +} + +/** + * Signature for a handler that listens to incoming publish error events + */ +export type IncomingPublishErrorListener = (eventData: IncomingPublishErrorEvent) => void; + +export type SubscriptionStatusEventListener = mqtt_request_response.SubscriptionStatusListener; + +export class StreamingOperation extends EventEmitter { + private operation: mqtt_request_response.StreamingOperationBase; + private deserializer: mqtt_request_response_utils.MessageDeserializer; + + private constructor(config: mqtt_request_response_utils.StreamingOperationConfig) { + super(); + + // validate + let streamingOperationModel = config.serviceModel.streamingOperations.get(config.operationName); + if (!streamingOperationModel) { + throw new CrtError("NYI"); + } + + let validator = config.serviceModel.shapeValidators.get(streamingOperationModel.inputShapeName); + if (!validator) { + throw new CrtError("NYI"); + } + + validator(config.modelConfig); + + let streamOptions : mqtt_request_response.StreamingOperationOptions = { + subscriptionTopicFilter: streamingOperationModel.subscriptionGenerator(config.modelConfig), + }; + + // create native operation + this.deserializer = streamingOperationModel.deserializer; + this.operation = config.client.createStream(streamOptions); + + this.operation.addListener(mqtt_request_response.StreamingOperationBase.SUBSCRIPTION_STATUS, this.onSubscriptionStatusChanged.bind(this)); + this.operation.addListener(mqtt_request_response.StreamingOperationBase.INCOMING_PUBLISH, this.onIncomingPublish.bind(this)); + } + + static create(config: mqtt_request_response_utils.StreamingOperationConfig) : StreamingOperation { + let operation = new StreamingOperation(config); + + return operation; + } + + open() { + this.operation.open(); + } + + close() { + this.operation.close(); + } + + /** + * Event emitted when the stream's subscription status changes. + * + * Listener type: {@link mqtt_request_response.SubscriptionStatusListener} + * + * @event + */ + static SUBSCRIPTION_STATUS : string = 'subscriptionStatus'; + + /** + * Event emitted when a stream message is received + * + * Listener type: {@link IncomingPublishListener} + * + * @event + */ + static INCOMING_PUBLISH : string = 'incomingPublish'; + + /** + * Event emitted when a stream message is received but handling it resulted in an error + * + * Listener type: {@link IncomingPublishErrorListener} + * + * @event + */ + static INCOMING_PUBLISH_ERROR : string = 'incomingPublishError'; + + on(event: 'subscriptionStatus', listener: mqtt_request_response.SubscriptionStatusListener): this; + + on(event: 'incomingPublish', listener: IncomingPublishListener): this; + + on(event: 'incomingPublishError', listener: IncomingPublishErrorListener): this; + + on(event: string | symbol, listener: (...args: any[]) => void): this { + super.on(event, listener); + return this; + } + + private onSubscriptionStatusChanged(eventData: mqtt_request_response.SubscriptionStatusEvent) : void { + setImmediate(async () => { + this.emit(StreamingOperation.SUBSCRIPTION_STATUS, eventData); + }) + } + + private onIncomingPublish(eventData: mqtt_request_response.IncomingPublishEvent) : void { + try { + let message = this.deserializer(eventData.payload); + + setImmediate(async () => { + this.emit(StreamingOperation.INCOMING_PUBLISH, { + message: message, + }); + }); + } catch (error) { + let serviceError = mqtt_request_response_utils.createServiceError((error as Error).toString()); + + setImmediate(async () => { + this.emit(StreamingOperation.INCOMING_PUBLISH_ERROR, { + payload: eventData.payload, + error: serviceError, + }); + }); + } + } +} + + +/** + * @internal + */ +interface ServiceErrorOptions { + description: string; + + internalError?: CrtError; + + modeledError?: any; +} + + +export class ServiceError extends Error { + + /** Optional inner/triggering error that can contain additional context. */ + readonly internalError?: CrtError; + + /** Optional service-specific modelled error data */ + readonly modeledError?: any; + + /** @internal */ + constructor(options: ServiceErrorOptions) { + super(options.description); + + if (options.internalError) { + this.internalError = options.internalError; + } + if (options.modeledError) { + this.modeledError = options.modeledError; + } + } +} + diff --git a/lib/mqtt_request_response_utils.ts b/lib/mqtt_request_response_utils.ts new file mode 100644 index 00000000..d1fbed12 --- /dev/null +++ b/lib/mqtt_request_response_utils.ts @@ -0,0 +1,171 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +import {CrtError, mqtt_request_response} from "aws-crt"; +import {ServiceError} from "./mqtt_request_response"; + +export interface StreamingOperationConfig { + operationName: string, + + serviceModel: RequestResponseServiceModel, + + client: mqtt_request_response.RequestResponseClient, + + modelConfig: any, +} + +export interface RequestResponseOperationConfig { + operationName: string, + + serviceModel: RequestResponseServiceModel, + + client: mqtt_request_response.RequestResponseClient, + + request: any, +} + +export type MessageDeserializer = (payload: ArrayBuffer) => any; + +export interface RequestResponsePath { + topic: string, + + correlationTokenJsonPath?: string, + + deserializer: MessageDeserializer; +} + +export interface RequestResponseOperationModel { + inputShapeName: string; + + payloadTransformer: (request: any) => ArrayBuffer; + + subscriptionGenerator: (request: any) => Array; + + responsePathGenerator: (request: any) => Array; + + publishTopicGenerator: (request: any) => string; + + correlationTokenApplicator: (request: any) => [any, string | undefined]; +} + +export interface StreamingOperationModel { + inputShapeName: string; + + subscriptionGenerator: (config: any) => string; + + deserializer: MessageDeserializer; +} + +export interface RequestResponseServiceModel { + + // operation name -> operation model + requestResponseOperations: Map, + + // operation name -> operation model + streamingOperations: Map, + + // shape name -> validator function + shapeValidators: Map void>; +} + +function buildResponseDeserializerMap(paths: Array) : Map { + return new Map( + paths.map((path) => { + return [path.topic, path.deserializer]; + }) + ); +} + +function buildResponsePaths(paths: Array) : Array { + return paths.map((path) => { + return { + topic: path.topic, + correlationTokenJsonPath: path.correlationTokenJsonPath, + }; + }); +} + +export async function doRequestResponse(options: RequestResponseOperationConfig) : Promise { + return new Promise(async (resolve, reject) => { + try { + let operationModel = options.serviceModel.requestResponseOperations.get(options.operationName); + if (!operationModel) { + reject(createServiceError(`Operation "${options.operationName}" not in client's service model`)); + return; + } + + let validator = options.serviceModel.shapeValidators.get(operationModel.inputShapeName); + if (!validator) { + reject(createServiceError(`Operation "${options.operationName}" does not have an input validator`)); + return; + } + + validator(options.request); + + let publishTopic = operationModel.publishTopicGenerator(options.request); + let subscriptionsNeeded = operationModel.subscriptionGenerator(options.request); + let modelPaths = operationModel.responsePathGenerator(options.request); + let deserializerMap = buildResponseDeserializerMap(modelPaths); + let responsePaths = buildResponsePaths(modelPaths); + + let [request, correlationToken] = operationModel.correlationTokenApplicator(options.request); + + let payload = operationModel.payloadTransformer(request); + + let requestOptions: mqtt_request_response.RequestResponseOperationOptions = { + subscriptionTopicFilters: subscriptionsNeeded, + responsePaths: responsePaths, + publishTopic: publishTopic, + payload: payload, + correlationToken: correlationToken + }; + + let response = await options.client.submitRequest(requestOptions); + + let responseTopic = response.topic; + let responsePayload = response.payload; + + let deserializer = deserializerMap.get(responseTopic); + if (!deserializer) { + reject(createServiceError(`Operation "${options.operationName}" does not have a deserializer for topic "${responseTopic}"`)); + return; + } + + resolve(deserializer(responsePayload) as ResponseType); + } catch (err) { + if (err instanceof ServiceError) { + throw err; + } else if (err instanceof CrtError) { + throw createServiceError("", err as CrtError); + } else { + throw createServiceError((err as Error).toString()); + } + } + }); + + +} + +export function createServiceError(description: string, internalError?: CrtError, modeledError?: any) { + return new ServiceError({ + description: description, + internalError: internalError, + modeledError: modeledError, + }); +} + +export function validateValueAsTopicSegment(value : any, propertyName?: string, type?: string) : void { + if (value === undefined) { + throw new CrtError("NYI"); + } + + if (typeof value !== 'string') { + throw new CrtError("NYI"); + } + + if (value.includes("/") || value.includes("#") || value.includes("+")) { + throw new CrtError("NYI"); + } +} \ No newline at end of file From c529ef5d6a7b1cbdf919d0c765fd3b1f7296ca8c Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Wed, 15 May 2024 11:54:47 -0700 Subject: [PATCH 04/79] Full named shadow client initial impl --- lib/index.ts | 2 + lib/iotshadow/clientv2.ts | 45 +++++------ lib/iotshadow/clientv2_utils.ts | 124 +++++++++++++++++++++++++---- lib/mqtt_request_response_utils.ts | 50 ++++++++++-- 4 files changed, 177 insertions(+), 44 deletions(-) diff --git a/lib/index.ts b/lib/index.ts index b6c6c9ae..4088d191 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -18,6 +18,7 @@ import * as iotidentity from './iotidentity/iotidentityclient'; import * as greengrass from './greengrass/discoveryclient'; import * as iotjobs from './iotjobs/iotjobsclient'; import * as iotshadow from './iotshadow/iotshadowclient'; +import * as iotshadowv2 from './iotshadow/clientv2'; import * as eventstream_rpc from './eventstream_rpc'; import * as greengrasscoreipc from './greengrasscoreipc'; import * as mqtt_request_response from './mqtt_request_response'; @@ -43,6 +44,7 @@ export { iotidentity, iotjobs, iotshadow, + iotshadowv2, mqtt, mqtt5, mqtt_request_response, diff --git a/lib/iotshadow/clientv2.ts b/lib/iotshadow/clientv2.ts index 425ee09a..6425c346 100644 --- a/lib/iotshadow/clientv2.ts +++ b/lib/iotshadow/clientv2.ts @@ -40,47 +40,46 @@ export class IotShadowClientv2 { this.rrClient.close(); } - async deleteNamedShadow(request: model.DeleteNamedShadowRequest) : Promise { - let rrConfig : mqtt_request_response_utils.RequestResponseOperationConfig = { - operationName: "DeleteNamedShadow", + private createRequestResponseConfig(operationName: string, request: any) : mqtt_request_response_utils.RequestResponseOperationConfig { + return { + operationName: operationName, serviceModel: this.serviceModel, client: this.rrClient, request: request }; - - return await mqtt_request_response_utils.doRequestResponse(rrConfig); } - async getNamedShadow(request: model.GetNamedShadowRequest) : Promise { - let rrConfig : mqtt_request_response_utils.RequestResponseOperationConfig = { - operationName: "GetNamedShadow", + private createStreamingOperationConfig(operationName: string, config: any) : mqtt_request_response_utils.StreamingOperationConfig { + return { + operationName: operationName, serviceModel: this.serviceModel, client: this.rrClient, - request: request + modelConfig: config, }; + } + async deleteNamedShadow(request: model.DeleteNamedShadowRequest) : Promise { + let rrConfig = this.createRequestResponseConfig("DeleteNamedShadow", request); return await mqtt_request_response_utils.doRequestResponse(rrConfig); } - createNamedShadowDeltaUpdatedStream(config: model.NamedShadowDeltaUpdatedSubscriptionRequest) : mqtt_request_response.StreamingOperation { - let streamingOperationConfig : mqtt_request_response_utils.StreamingOperationConfig = { - operationName: "createNamedShadowDeltaUpdatedEventStream", - serviceModel: this.serviceModel, - client: this.rrClient, - modelConfig: config, - }; + async getNamedShadow(request: model.GetNamedShadowRequest) : Promise { + let rrConfig = this.createRequestResponseConfig("GetNamedShadow", request); + return await mqtt_request_response_utils.doRequestResponse(rrConfig); + } + + async updateNamedShadow(request: model.UpdateNamedShadowRequest) : Promise { + let rrConfig = this.createRequestResponseConfig("UpdateNamedShadow", request); + return await mqtt_request_response_utils.doRequestResponse(rrConfig); + } + createNamedShadowDeltaUpdatedStream(config: model.NamedShadowDeltaUpdatedSubscriptionRequest) : mqtt_request_response.StreamingOperation { + let streamingOperationConfig = this.createStreamingOperationConfig("createNamedShadowDeltaUpdatedEventStream", config); return mqtt_request_response.StreamingOperation.create(streamingOperationConfig); } createNamedShadowUpdatedStream(config: model.NamedShadowUpdatedSubscriptionRequest) : mqtt_request_response.StreamingOperation { - let streamingOperationConfig : mqtt_request_response_utils.StreamingOperationConfig = { - operationName: "createNamedShadowUpdatedEventStream", - serviceModel: this.serviceModel, - client: this.rrClient, - modelConfig: config, - }; - + let streamingOperationConfig = this.createStreamingOperationConfig("createNamedShadowUpdatedEventStream", config); return mqtt_request_response.StreamingOperation.create(streamingOperationConfig); } } diff --git a/lib/iotshadow/clientv2_utils.ts b/lib/iotshadow/clientv2_utils.ts index 811e60fa..0526de1c 100644 --- a/lib/iotshadow/clientv2_utils.ts +++ b/lib/iotshadow/clientv2_utils.ts @@ -142,6 +142,76 @@ function applyCorrelationTokenToDeleteNamedShadowRequest(request: any) : [any, s return [typedRequest, correlationToken]; } +function normalizeUpdateNamedShadowRequest(value: model.UpdateNamedShadowRequest) : any { + let normalizedValue : any = {}; + + if (value.clientToken) { + normalizedValue.clientToken = value.clientToken; + } + + if (value.version) { + normalizedValue.version = value.clientToken; + } + + normalizedValue.state = value.state; + + return normalizedValue; +} + +function buildUpdateNamedShadowRequestPayload(request: any) : ArrayBuffer { + let value = normalizeUpdateNamedShadowRequest(request as model.UpdateNamedShadowRequest); + + return fromUtf8(JSON.stringify(value)); +} + +function buildUpdateNamedShadowRequestSubscriptions(request: any) : Array { + let typedRequest: model.UpdateNamedShadowRequest = request; + + return new Array( + `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/delete/accepted`, + `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/delete/rejected` + ); +} + +function deserializeUpdateShadowResponse(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + +function buildUpdateNamedShadowRequestResponsePaths(request: any) : Array { + let typedRequest: model.UpdateNamedShadowRequest = request; + + return new Array( + { + topic: `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/update/accepted`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeUpdateShadowResponse, + }, + { + topic: `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/update/rejected`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeErrorResponse, + }, + ) +} + +function buildUpdateNamedShadowRequestPublishTopic(request: any) : string { + let typedRequest: model.UpdateNamedShadowRequest = request; + + return `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/update`; +} + +function applyCorrelationTokenToUpdateNamedShadowRequest(request: any) : [any, string | undefined] { + let typedRequest: model.UpdateNamedShadowRequest = request; + + let correlationToken = uuid(); + + typedRequest.clientToken = correlationToken; + + return [typedRequest, correlationToken]; +} + function createRequestResponseOperationServiceModelMap() : Map { return new Map([ ["GetNamedShadow", { @@ -160,6 +230,14 @@ function createRequestResponseOperationServiceModelMap() : Map void> { return new Map void>([ ["DeleteNamedShadowRequest", validateDeleteNamedShadowRequest], ["GetNamedShadowRequest", validateGetNamedShadowRequest], + ["UpdateNamedShadowRequest", validateUpdateNamedShadowRequest], ["NamedShadowDeltaUpdatedSubscriptionRequest", validateNamedShadowDeltaUpdatedSubscriptionRequest], ["NamedShadowUpdatedSubscriptionRequest", validateNamedShadowUpdatedSubscriptionRequest] ]); diff --git a/lib/mqtt_request_response_utils.ts b/lib/mqtt_request_response_utils.ts index d1fbed12..04c2bebe 100644 --- a/lib/mqtt_request_response_utils.ts +++ b/lib/mqtt_request_response_utils.ts @@ -156,16 +156,56 @@ export function createServiceError(description: string, internalError?: CrtError }); } -export function validateValueAsTopicSegment(value : any, propertyName?: string, type?: string) : void { +function throwMissingPropertyError(propertyName?: string, shapeType?: string) : void { + if (propertyName && shapeType) { + throw createServiceError(`validation failure - missing required property '${propertyName}' of type '${shapeType}'`); + } else { + throw createServiceError(`validation failure - missing required property`); + } +} + +function throwInvalidPropertyValueError(valueDescription: string, propertyName?: string, shapeType?: string) : void { + if (propertyName && shapeType) { + throw createServiceError(`validation failure - property '${propertyName}' of type '${shapeType}' must be ${valueDescription}`); + } else { + throw createServiceError(`validation failure - property must be ${valueDescription}`); + } +} + +export function validateValueAsTopicSegment(value : any, propertyName?: string, shapeType?: string) : void { if (value === undefined) { - throw new CrtError("NYI"); + throwMissingPropertyError(propertyName, shapeType); } if (typeof value !== 'string') { - throw new CrtError("NYI"); + throwInvalidPropertyValueError("a string", propertyName, shapeType); } if (value.includes("/") || value.includes("#") || value.includes("+")) { - throw new CrtError("NYI"); + throwInvalidPropertyValueError("a valid MQTT topic", propertyName, shapeType); + } +} + +export function validateOptionalValueAsNumber(value: any, propertyName?: string, shapeType?: string) { + if (value === undefined) { + return; } -} \ No newline at end of file + + validateValueAsNumber(value, propertyName, shapeType); +} + +export function validateValueAsNumber(value: any, propertyName?: string, shapeType?: string) { + if (value == undefined) { + throwMissingPropertyError(propertyName, shapeType); + } + + if (typeof value !== 'number') { + throwInvalidPropertyValueError("a number", propertyName, shapeType); + } +} + +export function validateValueAsObject(value: any, propertyName?: string, shapeType?: string) { + if (value == undefined) { + throwMissingPropertyError(propertyName, shapeType); + } +} From 0c72ed6adb2b454c3ab6bc82f9c68179f00b9340 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 16 May 2024 14:33:06 -0700 Subject: [PATCH 05/79] Named shadow success tests + sample cleanup --- lib/iotshadow/clientv2.spec.ts | 412 ++++++++++++++++++ lib/iotshadow/clientv2.ts | 6 +- lib/iotshadow/clientv2_utils.ts | 26 +- lib/mqtt_request_response_utils.ts | 9 +- .../custom_authorizer_connect/package.json | 2 +- samples/browser/pub_sub_mqtt5/package.json | 2 +- .../browser/shared_subscription/package.json | 2 +- samples/node/pkcs12_connect/index.ts | 2 +- samples/node/pkcs12_connect/package.json | 4 +- test/native/jest.config.js | 3 +- 10 files changed, 449 insertions(+), 19 deletions(-) create mode 100644 lib/iotshadow/clientv2.spec.ts diff --git a/lib/iotshadow/clientv2.spec.ts b/lib/iotshadow/clientv2.spec.ts new file mode 100644 index 00000000..c64c436b --- /dev/null +++ b/lib/iotshadow/clientv2.spec.ts @@ -0,0 +1,412 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + + +import {iot, mqtt5, mqtt as mqtt311, mqtt_request_response} from "aws-crt"; +import {v4 as uuid} from "uuid"; +import {StreamingOperation} from "../mqtt_request_response"; +import {once} from "events"; +import {IotShadowClientv2} from "./clientv2"; +import * as model from "./model"; + +jest.setTimeout(1000000); + +function hasTestEnvironment() : boolean { + if (process.env.AWS_TEST_MQTT5_IOT_CORE_HOST === undefined) { + return false; + } + + if (process.env.AWS_TEST_MQTT5_IOT_CORE_RSA_CERT === undefined) { + return false; + } + + if (process.env.AWS_TEST_MQTT5_IOT_CORE_RSA_KEY === undefined) { + return false; + } + + return true; +} + +const conditional_test = (condition : boolean) => condition ? it : it.skip; + +function build_protocol_client_mqtt5() : mqtt5.Mqtt5Client { + let builder = iot.AwsIotMqtt5ClientConfigBuilder.newDirectMqttBuilderWithMtlsFromPath( + // @ts-ignore + process.env.AWS_TEST_MQTT5_IOT_CORE_HOST, + process.env.AWS_TEST_MQTT5_IOT_CORE_RSA_CERT, + process.env.AWS_TEST_MQTT5_IOT_CORE_RSA_KEY + ); + + builder.withConnectProperties({ + clientId : uuid(), + keepAliveIntervalSeconds: 1200, + }); + + return new mqtt5.Mqtt5Client(builder.build()); +} + +function build_protocol_client_mqtt311() : mqtt311.MqttClientConnection { + // @ts-ignore + let builder = iot.AwsIotMqttConnectionConfigBuilder.new_mtls_builder_from_path(process.env.AWS_TEST_MQTT5_IOT_CORE_RSA_CERT, process.env.AWS_TEST_MQTT5_IOT_CORE_RSA_KEY); + // @ts-ignore + builder.with_endpoint(process.env.AWS_TEST_MQTT5_IOT_CORE_HOST); + builder.with_client_id(uuid()); + + let client = new mqtt311.MqttClient(); + return client.new_connection(builder.build()); +} + +enum ProtocolVersion { + Mqtt311, + Mqtt5 +} + +interface TestingOptions { + version: ProtocolVersion, + timeoutSeconds?: number, +} + +class ShadowTestingContext { + + mqtt311Client?: mqtt311.MqttClientConnection; + mqtt5Client?: mqtt5.Mqtt5Client; + + client: IotShadowClientv2; + + private protocolStarted : boolean = false; + + async startProtocolClient() { + if (!this.protocolStarted) { + this.protocolStarted = true; + if (this.mqtt5Client) { + let connected = once(this.mqtt5Client, mqtt5.Mqtt5Client.CONNECTION_SUCCESS); + this.mqtt5Client.start(); + + await connected; + } + + if (this.mqtt311Client) { + await this.mqtt311Client.connect(); + } + } + } + + async stopProtocolClient() { + if (this.protocolStarted) { + this.protocolStarted = false; + if (this.mqtt5Client) { + let stopped = once(this.mqtt5Client, mqtt5.Mqtt5Client.STOPPED); + this.mqtt5Client.stop(); + await stopped; + + this.mqtt5Client.close(); + } + + if (this.mqtt311Client) { + await this.mqtt311Client.disconnect(); + } + } + } + + constructor(options: TestingOptions) { + if (options.version == ProtocolVersion.Mqtt5) { + this.mqtt5Client = build_protocol_client_mqtt5(); + + let rrOptions : mqtt_request_response.RequestResponseClientOptions = { + maxRequestResponseSubscriptions : 6, + maxStreamingSubscriptions : 2, + operationTimeoutInSeconds : options.timeoutSeconds ?? 60, + } + + this.client = IotShadowClientv2.newFromMqtt5(this.mqtt5Client, rrOptions); + } else { + this.mqtt311Client = build_protocol_client_mqtt311(); + + let rrOptions : mqtt_request_response.RequestResponseClientOptions = { + maxRequestResponseSubscriptions : 6, + maxStreamingSubscriptions : 2, + operationTimeoutInSeconds : options.timeoutSeconds ?? 60, + } + + this.client = IotShadowClientv2.newFromMqtt311(this.mqtt311Client, rrOptions); + } + } + + async open() { + await this.startProtocolClient(); + } + + async close() { + this.client.close(); + await this.stopProtocolClient(); + } +} + +async function doCreateDestroyTest(version: ProtocolVersion) { + let context = new ShadowTestingContext({ + version: version + }); + await context.open(); + + await context.close(); +} + +conditional_test(hasTestEnvironment())('shadowv2 - create destroy mqtt5', async () => { + await doCreateDestroyTest(ProtocolVersion.Mqtt5); +}); + +conditional_test(hasTestEnvironment())('shadowv2 - create destroy mqtt311', async () => { + await doCreateDestroyTest(ProtocolVersion.Mqtt311); +}); + +async function getNonexistentShadow(client: IotShadowClientv2, thingName: string, shadowName: string) { + let request : model.GetNamedShadowRequest = { + thingName: thingName, + shadowName: shadowName, + }; + + try { + await client.getNamedShadow(request); + expect(false); + } catch (err: any) { + expect(err.message).toContain("failed with modeled service error"); + expect(err.modeledError).toBeDefined(); + expect(err.modeledError.code).toEqual(404); + expect(err.modeledError.message).toContain("No shadow exists with name"); + } +} + +async function doGetNonexistentShadowTest(version: ProtocolVersion) { + let context = new ShadowTestingContext({ + version: version + }); + await context.open(); + + await getNonexistentShadow(context.client, uuid(), uuid()); + + await context.close(); +} + +conditional_test(hasTestEnvironment())('shadowv2 - get non-existent shadow mqtt5', async () => { + await doGetNonexistentShadowTest(ProtocolVersion.Mqtt5); +}); + +conditional_test(hasTestEnvironment())('shadowv2 - get non-existent shadow mqtt311', async () => { + await doGetNonexistentShadowTest(ProtocolVersion.Mqtt311); +}); + +async function createShadow(client: IotShadowClientv2, thingName: string, shadowName: string, document: any) { + let request : model.UpdateNamedShadowRequest = { + thingName: thingName, + shadowName: shadowName, + state: { + desired: document, + reported: document, + } + }; + + try { + let response = await client.updateNamedShadow(request); + expect(response.state).toBeDefined(); + // @ts-ignore + expect(response.state.desired).toBeDefined(); + // @ts-ignore + expect(response.state.desired).toEqual(document); + // @ts-ignore + expect(response.state.reported).toBeDefined(); + // @ts-ignore + expect(response.state.reported).toEqual(document); + } catch (err) { + throw err; + } +} + +async function getShadow(client: IotShadowClientv2, thingName: string, shadowName: string, expectedDocument: any) { + let request : model.GetNamedShadowRequest = { + thingName: thingName, + shadowName: shadowName, + }; + + let response = await client.getNamedShadow(request); + expect(response.state).toBeDefined(); + // @ts-ignore + expect(response.state.desired).toBeDefined(); + // @ts-ignore + expect(response.state.desired).toEqual(expectedDocument); + // @ts-ignore + expect(response.state.reported).toBeDefined(); + // @ts-ignore + expect(response.state.reported).toEqual(expectedDocument); +} + +async function deleteShadow(client: IotShadowClientv2, thingName: string, shadowName: string, expectedVersion: number) { + let request : model.DeleteNamedShadowRequest = { + thingName: thingName, + shadowName: shadowName, + }; + + let response = await client.deleteNamedShadow(request); + expect(response.version).toEqual(expectedVersion); +} + +async function doCreateDeleteShadowTest(version: ProtocolVersion) { + let context = new ShadowTestingContext({ + version: version + }); + await context.open(); + + let thingName = uuid(); + let shadowName = uuid(); + let document = { + color: "green", + on: true, + }; + + // shouldn't exist yet + await getNonexistentShadow(context.client, thingName, shadowName); + + try { + await createShadow(context.client, thingName, shadowName, document); + + await getShadow(context.client, thingName, shadowName, document); + } finally { + await deleteShadow(context.client, thingName, shadowName, 1); + } + + await getNonexistentShadow(context.client, thingName, shadowName); + + await context.close(); +} + +conditional_test(hasTestEnvironment())('shadowv2 - create-destroy shadow mqtt5', async () => { + await doCreateDeleteShadowTest(ProtocolVersion.Mqtt5); +}); + +conditional_test(hasTestEnvironment())('shadowv2 - create-destroy shadow mqtt311', async () => { + await doCreateDeleteShadowTest(ProtocolVersion.Mqtt311); +}); + +async function updateShadowDesired(client: IotShadowClientv2, thingName: string, shadowName: string, document: any) { + let request : model.UpdateNamedShadowRequest = { + thingName: thingName, + shadowName: shadowName, + state: { + desired: document, + }, + }; + + try { + let response = await client.updateNamedShadow(request); + expect(response.state).toBeDefined(); + // @ts-ignore + expect(response.state.desired).toBeDefined(); + // @ts-ignore + expect(response.state.desired).toEqual(document); + } catch (err) { + throw err; + } +} + +async function updateShadowReported(client: IotShadowClientv2, thingName: string, shadowName: string, document: any) { + let request : model.UpdateNamedShadowRequest = { + thingName: thingName, + shadowName: shadowName, + state: { + reported: document, + }, + }; + + try { + let response = await client.updateNamedShadow(request); + expect(response.state).toBeDefined(); + // @ts-ignore + expect(response.state.reported).toBeDefined(); + // @ts-ignore + expect(response.state.reported).toEqual(document); + } catch (err) { + throw err; + } +} + +async function doUpdateShadowTest(version: ProtocolVersion) { + let context = new ShadowTestingContext({ + version: version + }); + await context.open(); + + let thingName = uuid(); + let shadowName = uuid(); + let document = { + color: "green", + on: true, + }; + + let deltaEventStream = context.client.createNamedShadowDeltaUpdatedStream({ + thingName: thingName, + shadowName: shadowName, + }); + let deltaStatusUpdate = once(deltaEventStream, StreamingOperation.SUBSCRIPTION_STATUS); + deltaEventStream.open(); + let deltaStatus = (await deltaStatusUpdate)[0]; + expect(deltaStatus.type).toEqual(mqtt_request_response.SubscriptionStatusEventType.SubscriptionEstablished); + + let stateStream = context.client.createNamedShadowDeltaUpdatedStream({ + thingName: thingName, + shadowName: shadowName, + }); + let stateStatusUpdate = once(stateStream, StreamingOperation.SUBSCRIPTION_STATUS); + stateStream.open(); + let stateStatus = (await stateStatusUpdate)[0]; + expect(stateStatus.type).toEqual(mqtt_request_response.SubscriptionStatusEventType.SubscriptionEstablished); + + // shouldn't exist yet + await getNonexistentShadow(context.client, thingName, shadowName); + + try { + await createShadow(context.client, thingName, shadowName, document); + + await getShadow(context.client, thingName, shadowName, document); + + let updateDocument = { + color: "blue", + on: false, + }; + + let deltaEvent = once(deltaEventStream, StreamingOperation.INCOMING_PUBLISH); + let stateEvent = once(stateStream, StreamingOperation.INCOMING_PUBLISH); + + await updateShadowDesired(context.client, thingName, shadowName, updateDocument); + + let deltaResult = (await deltaEvent)[0]; + expect(deltaResult).toBeDefined(); + expect(deltaResult.message.state).toEqual(updateDocument); + + let stateResult = (await stateEvent)[0]; + expect(stateResult).toBeDefined(); + expect(stateResult.message.state).toEqual(updateDocument); + + await updateShadowReported(context.client, thingName, shadowName, updateDocument); + + await getShadow(context.client, thingName, shadowName, updateDocument); + + } finally { + deltaEventStream.close(); + stateStream.close(); + + await deleteShadow(context.client, thingName, shadowName, 3); + } + + await getNonexistentShadow(context.client, thingName, shadowName); + + await context.close(); +} + +test('shadowv2 - update shadow mqtt5', async () => { + await doUpdateShadowTest(ProtocolVersion.Mqtt5); +}); + +conditional_test(hasTestEnvironment())('shadowv2 - update shadow mqtt311', async () => { + await doUpdateShadowTest(ProtocolVersion.Mqtt311); +}); \ No newline at end of file diff --git a/lib/iotshadow/clientv2.ts b/lib/iotshadow/clientv2.ts index 6425c346..ecd92760 100644 --- a/lib/iotshadow/clientv2.ts +++ b/lib/iotshadow/clientv2.ts @@ -22,7 +22,7 @@ export class IotShadowClientv2 { this.serviceModel = clientv2_utils.makeServiceModel(); } - static new_from_mqtt311(protocolClient: mqtt.MqttClientConnection, options: mqtt_rr_internal.RequestResponseClientOptions) : IotShadowClientv2 { + static newFromMqtt311(protocolClient: mqtt.MqttClientConnection, options: mqtt_rr_internal.RequestResponseClientOptions) : IotShadowClientv2 { let rrClient = mqtt_rr_internal.RequestResponseClient.newFromMqtt311(protocolClient, options); let client = new IotShadowClientv2(rrClient); @@ -74,12 +74,12 @@ export class IotShadowClientv2 { } createNamedShadowDeltaUpdatedStream(config: model.NamedShadowDeltaUpdatedSubscriptionRequest) : mqtt_request_response.StreamingOperation { - let streamingOperationConfig = this.createStreamingOperationConfig("createNamedShadowDeltaUpdatedEventStream", config); + let streamingOperationConfig = this.createStreamingOperationConfig("CreateNamedShadowDeltaUpdatedEventStream", config); return mqtt_request_response.StreamingOperation.create(streamingOperationConfig); } createNamedShadowUpdatedStream(config: model.NamedShadowUpdatedSubscriptionRequest) : mqtt_request_response.StreamingOperation { - let streamingOperationConfig = this.createStreamingOperationConfig("createNamedShadowUpdatedEventStream", config); + let streamingOperationConfig = this.createStreamingOperationConfig("CreateNamedShadowUpdatedEventStream", config); return mqtt_request_response.StreamingOperation.create(streamingOperationConfig); } } diff --git a/lib/iotshadow/clientv2_utils.ts b/lib/iotshadow/clientv2_utils.ts index 0526de1c..b43d6405 100644 --- a/lib/iotshadow/clientv2_utils.ts +++ b/lib/iotshadow/clientv2_utils.ts @@ -142,6 +142,20 @@ function applyCorrelationTokenToDeleteNamedShadowRequest(request: any) : [any, s return [typedRequest, correlationToken]; } +function normalizeShadowState(value: model.ShadowState) : any { + let normalizedValue : any = {}; + + if (value.desired) { + normalizedValue.desired = value.desired; + } + + if (value.reported) { + normalizedValue.reported = value.reported; + } + + return normalizedValue; +} + function normalizeUpdateNamedShadowRequest(value: model.UpdateNamedShadowRequest) : any { let normalizedValue : any = {}; @@ -153,7 +167,9 @@ function normalizeUpdateNamedShadowRequest(value: model.UpdateNamedShadowRequest normalizedValue.version = value.clientToken; } - normalizedValue.state = value.state; + if (value.state) { + normalizedValue.state = normalizeShadowState(value.state); + } return normalizedValue; } @@ -168,8 +184,8 @@ function buildUpdateNamedShadowRequestSubscriptions(request: any) : Array( - `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/delete/accepted`, - `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/delete/rejected` + `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/update/accepted`, + `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/update/rejected` ); } @@ -267,12 +283,12 @@ function deserializeNamedShadowUpdatedPayload(payload: ArrayBuffer) : any { function createStreamingOperationServiceModelMap() : Map { return new Map([ - ["createNamedShadowDeltaUpdatedEventStream", { + ["CreateNamedShadowDeltaUpdatedEventStream", { inputShapeName : "NamedShadowDeltaUpdatedSubscriptionRequest", subscriptionGenerator: buildNamedShadowDeltaUpdatedSubscriptionTopicFilter, deserializer: deserializeNamedShadowDeltaUpdatedPayload, }], - ["createNamedShadowUpdatedEventStream", { + ["CreateNamedShadowUpdatedEventStream", { inputShapeName : "NamedShadowUpdatedSubscriptionRequest", subscriptionGenerator: buildNamedShadowUpdatedSubscriptionTopicFilter, deserializer: deserializeNamedShadowUpdatedPayload, diff --git a/lib/mqtt_request_response_utils.ts b/lib/mqtt_request_response_utils.ts index 04c2bebe..1d0df39a 100644 --- a/lib/mqtt_request_response_utils.ts +++ b/lib/mqtt_request_response_utils.ts @@ -133,14 +133,15 @@ export async function doRequestResponse(options: RequestResponseOp return; } - resolve(deserializer(responsePayload) as ResponseType); + let deserializedResponse = deserializer(responsePayload) as ResponseType; + resolve(deserializedResponse); } catch (err) { if (err instanceof ServiceError) { - throw err; + reject(err); } else if (err instanceof CrtError) { - throw createServiceError("", err as CrtError); + reject(createServiceError("??", err as CrtError)); } else { - throw createServiceError((err as Error).toString()); + reject(createServiceError((err as Error).toString())); } } }); diff --git a/samples/browser/custom_authorizer_connect/package.json b/samples/browser/custom_authorizer_connect/package.json index 83f2a488..579b21da 100644 --- a/samples/browser/custom_authorizer_connect/package.json +++ b/samples/browser/custom_authorizer_connect/package.json @@ -1,5 +1,5 @@ { - "name": "pub_sub", + "name": "custom_authorizer_connect", "version": "1.0.0", "description": "Publish/Subscribe sample for AWS IoT Browser SDK", "homepage": "https://github.com/aws/aws-iot-device-sdk-js-v2", diff --git a/samples/browser/pub_sub_mqtt5/package.json b/samples/browser/pub_sub_mqtt5/package.json index eb320ac0..81b94bac 100644 --- a/samples/browser/pub_sub_mqtt5/package.json +++ b/samples/browser/pub_sub_mqtt5/package.json @@ -1,5 +1,5 @@ { - "name": "mqtt5", + "name": "pub_sub_mqtt5", "version": "1.0.0", "description": "Publish/Subscribe sample for AWS IoT Browser SDK", "homepage": "https://github.com/aws/aws-iot-device-sdk-js-v2", diff --git a/samples/browser/shared_subscription/package.json b/samples/browser/shared_subscription/package.json index 96bfb0d5..332f02dd 100644 --- a/samples/browser/shared_subscription/package.json +++ b/samples/browser/shared_subscription/package.json @@ -1,5 +1,5 @@ { - "name": "mqtt5", + "name": "shared_subscription", "version": "1.0.0", "description": "Shared Subscription sample for AWS IoT Browser SDK", "homepage": "https://github.com/aws/aws-iot-device-sdk-js-v2", diff --git a/samples/node/pkcs12_connect/index.ts b/samples/node/pkcs12_connect/index.ts index b4a85dac..e6473969 100644 --- a/samples/node/pkcs12_connect/index.ts +++ b/samples/node/pkcs12_connect/index.ts @@ -15,7 +15,7 @@ const yargs = require('yargs'); const common_args = require('../../../util/cli_args'); yargs.command('*', false, (yargs: any) => { - yargs.usage("Connect using using a private key stored on a PKCS#11 device."); + yargs.usage("Connect using using a certificate and private key stored together in a PKCS#12-encoded file."); common_args.add_universal_arguments(yargs); common_args.add_common_mqtt_arguments(yargs); diff --git a/samples/node/pkcs12_connect/package.json b/samples/node/pkcs12_connect/package.json index dc5e6dc9..6a85bd70 100644 --- a/samples/node/pkcs12_connect/package.json +++ b/samples/node/pkcs12_connect/package.json @@ -1,7 +1,7 @@ { - "name": "pkcs11-connect", + "name": "pkcs12-connect", "version": "1.0.0", - "description": "NodeJS IoT SDK v2 PKCS11 Connect Sample", + "description": "NodeJS IoT SDK v2 PKCS12 Connect Sample", "homepage": "https://github.com/aws/aws-iot-device-sdk-js-v2", "repository": { "type": "git", diff --git a/test/native/jest.config.js b/test/native/jest.config.js index c7b79845..7c2cb1b1 100644 --- a/test/native/jest.config.js +++ b/test/native/jest.config.js @@ -4,7 +4,8 @@ module.exports = { rootDir: '../../', testMatch: [ '/lib/*.spec.ts', - '/lib/echo_rpc/*.spec.ts' + '/lib/echo_rpc/*.spec.ts', + '/lib/iotshadow/*.spec.ts' ], preset: 'ts-jest', globals: { From a602169a316414df29d6ca6840f881f0066d1032 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 16 May 2024 14:46:18 -0700 Subject: [PATCH 06/79] Check sub status --- lib/iotshadow/clientv2.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iotshadow/clientv2.spec.ts b/lib/iotshadow/clientv2.spec.ts index c64c436b..fb187feb 100644 --- a/lib/iotshadow/clientv2.spec.ts +++ b/lib/iotshadow/clientv2.spec.ts @@ -403,7 +403,7 @@ async function doUpdateShadowTest(version: ProtocolVersion) { await context.close(); } -test('shadowv2 - update shadow mqtt5', async () => { +conditional_test(hasTestEnvironment())('shadowv2 - update shadow mqtt5', async () => { await doUpdateShadowTest(ProtocolVersion.Mqtt5); }); From 1d27cb042ab624410931b3f90b1b4c065861c533 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Wed, 19 Jun 2024 09:47:29 -0700 Subject: [PATCH 07/79] Shadow client generated checkpoint --- lib/index.ts | 4 +- lib/iotshadow/clientv2.ts | 88 ----- lib/iotshadow/iotshadow.ts | 21 ++ lib/iotshadow/iotshadowclient.ts | 2 - ...ntv2.spec.ts => iotshadowclientv2.spec.ts} | 2 +- lib/iotshadow/iotshadowclientv2.ts | 323 ++++++++++++++++++ .../{clientv2_utils.ts => v2utils.ts} | 10 +- 7 files changed, 351 insertions(+), 99 deletions(-) delete mode 100644 lib/iotshadow/clientv2.ts create mode 100644 lib/iotshadow/iotshadow.ts rename lib/iotshadow/{clientv2.spec.ts => iotshadowclientv2.spec.ts} (99%) create mode 100644 lib/iotshadow/iotshadowclientv2.ts rename lib/iotshadow/{clientv2_utils.ts => v2utils.ts} (98%) diff --git a/lib/index.ts b/lib/index.ts index 4088d191..e9be6f98 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -17,8 +17,7 @@ import * as iotidentity from './iotidentity/iotidentityclient'; import * as greengrass from './greengrass/discoveryclient'; import * as iotjobs from './iotjobs/iotjobsclient'; -import * as iotshadow from './iotshadow/iotshadowclient'; -import * as iotshadowv2 from './iotshadow/clientv2'; +import * as iotshadow from './iotshadow/iotshadow'; import * as eventstream_rpc from './eventstream_rpc'; import * as greengrasscoreipc from './greengrasscoreipc'; import * as mqtt_request_response from './mqtt_request_response'; @@ -44,7 +43,6 @@ export { iotidentity, iotjobs, iotshadow, - iotshadowv2, mqtt, mqtt5, mqtt_request_response, diff --git a/lib/iotshadow/clientv2.ts b/lib/iotshadow/clientv2.ts deleted file mode 100644 index ecd92760..00000000 --- a/lib/iotshadow/clientv2.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - * - * This file is generated - */ - -import {mqtt, mqtt5} from 'aws-crt'; -import {mqtt_request_response as mqtt_rr_internal} from 'aws-crt'; -import * as mqtt_request_response from '../mqtt_request_response'; -import * as mqtt_request_response_utils from '../mqtt_request_response_utils'; -import * as model from './model'; -import * as clientv2_utils from './clientv2_utils'; - - -export class IotShadowClientv2 { - private rrClient : mqtt_rr_internal.RequestResponseClient; - private serviceModel : mqtt_request_response_utils.RequestResponseServiceModel; - - private constructor(rrClient: mqtt_rr_internal.RequestResponseClient) { - this.rrClient = rrClient; - this.serviceModel = clientv2_utils.makeServiceModel(); - } - - static newFromMqtt311(protocolClient: mqtt.MqttClientConnection, options: mqtt_rr_internal.RequestResponseClientOptions) : IotShadowClientv2 { - let rrClient = mqtt_rr_internal.RequestResponseClient.newFromMqtt311(protocolClient, options); - let client = new IotShadowClientv2(rrClient); - - return client; - } - - static newFromMqtt5(protocolClient: mqtt5.Mqtt5Client, options: mqtt_rr_internal.RequestResponseClientOptions) : IotShadowClientv2 { - let rrClient = mqtt_rr_internal.RequestResponseClient.newFromMqtt5(protocolClient, options); - let client = new IotShadowClientv2(rrClient); - - return client; - } - - close() { - this.rrClient.close(); - } - - private createRequestResponseConfig(operationName: string, request: any) : mqtt_request_response_utils.RequestResponseOperationConfig { - return { - operationName: operationName, - serviceModel: this.serviceModel, - client: this.rrClient, - request: request - }; - } - - private createStreamingOperationConfig(operationName: string, config: any) : mqtt_request_response_utils.StreamingOperationConfig { - return { - operationName: operationName, - serviceModel: this.serviceModel, - client: this.rrClient, - modelConfig: config, - }; - } - - async deleteNamedShadow(request: model.DeleteNamedShadowRequest) : Promise { - let rrConfig = this.createRequestResponseConfig("DeleteNamedShadow", request); - return await mqtt_request_response_utils.doRequestResponse(rrConfig); - } - - async getNamedShadow(request: model.GetNamedShadowRequest) : Promise { - let rrConfig = this.createRequestResponseConfig("GetNamedShadow", request); - return await mqtt_request_response_utils.doRequestResponse(rrConfig); - } - - async updateNamedShadow(request: model.UpdateNamedShadowRequest) : Promise { - let rrConfig = this.createRequestResponseConfig("UpdateNamedShadow", request); - return await mqtt_request_response_utils.doRequestResponse(rrConfig); - } - - createNamedShadowDeltaUpdatedStream(config: model.NamedShadowDeltaUpdatedSubscriptionRequest) : mqtt_request_response.StreamingOperation { - let streamingOperationConfig = this.createStreamingOperationConfig("CreateNamedShadowDeltaUpdatedEventStream", config); - return mqtt_request_response.StreamingOperation.create(streamingOperationConfig); - } - - createNamedShadowUpdatedStream(config: model.NamedShadowUpdatedSubscriptionRequest) : mqtt_request_response.StreamingOperation { - let streamingOperationConfig = this.createStreamingOperationConfig("CreateNamedShadowUpdatedEventStream", config); - return mqtt_request_response.StreamingOperation.create(streamingOperationConfig); - } -} - - - diff --git a/lib/iotshadow/iotshadow.ts b/lib/iotshadow/iotshadow.ts new file mode 100644 index 00000000..da518135 --- /dev/null +++ b/lib/iotshadow/iotshadow.ts @@ -0,0 +1,21 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + * + * This file is generated + */ + +/** + * @packageDocumentation + * @module shadow + */ + +import * as model from "./model"; +import { IotShadowClient } from "./iotshadowclient"; +import { IotShadowClientv2 } from "./iotshadowclientv2"; + +export { + IotShadowClient, + IotShadowClientv2, + model +}; diff --git a/lib/iotshadow/iotshadowclient.ts b/lib/iotshadow/iotshadowclient.ts index 430bb9bd..8705cee6 100644 --- a/lib/iotshadow/iotshadowclient.ts +++ b/lib/iotshadow/iotshadowclient.ts @@ -15,8 +15,6 @@ import { mqtt, mqtt5 } from "aws-crt"; import { toUtf8 } from "@aws-sdk/util-utf8-browser" import * as service_client_mqtt_adapter from "../service_client_mqtt_adapter"; -export { model }; - /** * Error subclass for IotShadow service errors * diff --git a/lib/iotshadow/clientv2.spec.ts b/lib/iotshadow/iotshadowclientv2.spec.ts similarity index 99% rename from lib/iotshadow/clientv2.spec.ts rename to lib/iotshadow/iotshadowclientv2.spec.ts index fb187feb..c32fab99 100644 --- a/lib/iotshadow/clientv2.spec.ts +++ b/lib/iotshadow/iotshadowclientv2.spec.ts @@ -8,7 +8,7 @@ import {iot, mqtt5, mqtt as mqtt311, mqtt_request_response} from "aws-crt"; import {v4 as uuid} from "uuid"; import {StreamingOperation} from "../mqtt_request_response"; import {once} from "events"; -import {IotShadowClientv2} from "./clientv2"; +import {IotShadowClientv2} from "./iotshadowclientv2"; import * as model from "./model"; jest.setTimeout(1000000); diff --git a/lib/iotshadow/iotshadowclientv2.ts b/lib/iotshadow/iotshadowclientv2.ts new file mode 100644 index 00000000..ab87d406 --- /dev/null +++ b/lib/iotshadow/iotshadowclientv2.ts @@ -0,0 +1,323 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + * + * This file is generated + */ + +/** + * @packageDocumentation + * @module shadow + */ + +import {mqtt, mqtt5} from 'aws-crt'; +import {mqtt_request_response as mqtt_rr_internal} from 'aws-crt'; +import * as mqtt_request_response from '../mqtt_request_response'; +import * as mqtt_request_response_utils from '../mqtt_request_response_utils'; +import * as model from './model'; +import * as clientv2_utils from './v2utils'; + +/** + * The AWS IoT Device Shadow service adds shadows to AWS IoT thing objects. Shadows are a simple data store for device properties and state. Shadows can make a device’s state available to apps and other services whether the device is connected to AWS IoT or not. + * + * AWS documentation: https://docs.aws.amazon.com/iot/latest/developerguide/device-shadow-mqtt.html + * + * @category IotShadow + */ +export class IotShadowClientv2 { + private rrClient : mqtt_rr_internal.RequestResponseClient; + private serviceModel : mqtt_request_response_utils.RequestResponseServiceModel; + + private constructor(rrClient: mqtt_rr_internal.RequestResponseClient) { + this.rrClient = rrClient; + this.serviceModel = clientv2_utils.makeServiceModel(); + } + + /** + * Creates a new service client that will use an SDK MQTT 311 client as transport. + * + * @param protocolClient the MQTT 311 client to use for transport + * @param options additional service client configuration options + * + * @return a new service client + * + */ + static newFromMqtt311(protocolClient: mqtt.MqttClientConnection, options: mqtt_rr_internal.RequestResponseClientOptions) : IotShadowClientv2 { + let rrClient = mqtt_rr_internal.RequestResponseClient.newFromMqtt311(protocolClient, options); + return new IotShadowClientv2(rrClient); + } + + /** + * Creates a new service client that will use an SDK MQTT 5 client as transport. + * + * @param protocolClient the MQTT 5 client to use for transport + * @param options additional service client configuration options + * + * @return a new service client + * + */ + static newFromMqtt5(protocolClient: mqtt5.Mqtt5Client, options: mqtt_rr_internal.RequestResponseClientOptions) : IotShadowClientv2 { + let rrClient = mqtt_rr_internal.RequestResponseClient.newFromMqtt5(protocolClient, options); + return new IotShadowClientv2(rrClient); + } + + /** + * Triggers cleanup of all resources associated with the service client. Closing a client will fail + * all incomplete requests and close all unclosed streaming operations. + * + * This must be called when finished with a client; otherwise, native resources will leak. + */ + close() { + this.rrClient.close(); + } + + /** + * Deletes a named shadow for an AWS IoT thing. + * + * + * AWS documentation: https://docs.aws.amazon.com/iot/latest/developerguide/device-shadow-mqtt.html#delete-pub-sub-topic + * + * @param request operation to perform + * + * @returns Promise which resolves into the response to the request + * + * @category IotShadowV2 + */ + async deleteNamedShadow(request: model.DeleteNamedShadowRequest) : Promise { + + let config = { + operationName: "deleteNamedShadow", + serviceModel: this.serviceModel, + client: this.rrClient, + request: request + }; + + return await mqtt_request_response_utils.doRequestResponse(config); + } + + /** + * Deletes the (classic) shadow for an AWS IoT thing. + * + * + * AWS documentation: https://docs.aws.amazon.com/iot/latest/developerguide/device-shadow-mqtt.html#delete-pub-sub-topic + * + * @param request operation to perform + * + * @returns Promise which resolves into the response to the request + * + * @category IotShadowV2 + */ + async deleteShadow(request: model.DeleteShadowRequest) : Promise { + + let config = { + operationName: "deleteShadow", + serviceModel: this.serviceModel, + client: this.rrClient, + request: request + }; + + return await mqtt_request_response_utils.doRequestResponse(config); + } + + /** + * Gets a named shadow for an AWS IoT thing. + * + * + * AWS documentation: https://docs.aws.amazon.com/iot/latest/developerguide/device-shadow-mqtt.html#get-pub-sub-topic + * + * @param request operation to perform + * + * @returns Promise which resolves into the response to the request + * + * @category IotShadowV2 + */ + async getNamedShadow(request: model.GetNamedShadowRequest) : Promise { + + let config = { + operationName: "getNamedShadow", + serviceModel: this.serviceModel, + client: this.rrClient, + request: request + }; + + return await mqtt_request_response_utils.doRequestResponse(config); + } + + /** + * Gets the (classic) shadow for an AWS IoT thing. + * + * + * AWS documentation: https://docs.aws.amazon.com/iot/latest/developerguide/device-shadow-mqtt.html#get-pub-sub-topic + * + * @param request operation to perform + * + * @returns Promise which resolves into the response to the request + * + * @category IotShadowV2 + */ + async getShadow(request: model.GetShadowRequest) : Promise { + + let config = { + operationName: "getShadow", + serviceModel: this.serviceModel, + client: this.rrClient, + request: request + }; + + return await mqtt_request_response_utils.doRequestResponse(config); + } + + /** + * Update a named shadow for a device. + * + * + * AWS documentation: https://docs.aws.amazon.com/iot/latest/developerguide/device-shadow-mqtt.html#update-pub-sub-topic + * + * @param request operation to perform + * + * @returns Promise which resolves into the response to the request + * + * @category IotShadowV2 + */ + async updateNamedShadow(request: model.UpdateNamedShadowRequest) : Promise { + + let config = { + operationName: "updateNamedShadow", + serviceModel: this.serviceModel, + client: this.rrClient, + request: request + }; + + return await mqtt_request_response_utils.doRequestResponse(config); + } + + /** + * Update a device's (classic) shadow. + * + * + * AWS documentation: https://docs.aws.amazon.com/iot/latest/developerguide/device-shadow-mqtt.html#update-pub-sub-topic + * + * @param request operation to perform + * + * @returns Promise which resolves into the response to the request + * + * @category IotShadowV2 + */ + async updateShadow(request: model.UpdateShadowRequest) : Promise { + + let config = { + operationName: "updateShadow", + serviceModel: this.serviceModel, + client: this.rrClient, + request: request + }; + + return await mqtt_request_response_utils.doRequestResponse(config); + } + + + /** + * Create a stream for NamedShadowDelta events for a named shadow of an AWS IoT thing. + * + * + * AWS documentation: https://docs.aws.amazon.com/iot/latest/developerguide/device-shadow-mqtt.html#update-delta-pub-sub-topic + * + * @param config streaming operation configuration options + * + * @returns a streaming operation which will emit an event every time a message is received on the + * associated MQTT topic + * + * @category IotShadowV2 + */ + createNamedShadowDeltaUpdatedStream(config: model.NamedShadowDeltaUpdatedSubscriptionRequest) + : mqtt_request_response.StreamingOperation { + + let streamingOperationConfig : mqtt_request_response_utils.StreamingOperationConfig = { + operationName: "createNamedShadowDeltaUpdatedStream", + serviceModel: this.serviceModel, + client: this.rrClient, + modelConfig: config, + }; + + return mqtt_request_response.StreamingOperation.create(streamingOperationConfig); + } + + /** + * Create a stream for ShadowUpdated events for a named shadow of an AWS IoT thing. + * + * + * AWS documentation: https://docs.aws.amazon.com/iot/latest/developerguide/device-shadow-mqtt.html#update-documents-pub-sub-topic + * + * @param config streaming operation configuration options + * + * @returns a streaming operation which will emit an event every time a message is received on the + * associated MQTT topic + * + * @category IotShadowV2 + */ + createNamedShadowUpdatedStream(config: model.NamedShadowUpdatedSubscriptionRequest) + : mqtt_request_response.StreamingOperation { + + let streamingOperationConfig : mqtt_request_response_utils.StreamingOperationConfig = { + operationName: "createNamedShadowUpdatedStream", + serviceModel: this.serviceModel, + client: this.rrClient, + modelConfig: config, + }; + + return mqtt_request_response.StreamingOperation.create(streamingOperationConfig); + } + + /** + * Create a stream for ShadowDelta events for the (classic) shadow of an AWS IoT thing. + * + * + * AWS documentation: https://docs.aws.amazon.com/iot/latest/developerguide/device-shadow-mqtt.html#update-delta-pub-sub-topic + * + * @param config streaming operation configuration options + * + * @returns a streaming operation which will emit an event every time a message is received on the + * associated MQTT topic + * + * @category IotShadowV2 + */ + createShadowDeltaUpdatedStream(config: model.ShadowDeltaUpdatedSubscriptionRequest) + : mqtt_request_response.StreamingOperation { + + let streamingOperationConfig : mqtt_request_response_utils.StreamingOperationConfig = { + operationName: "createShadowDeltaUpdatedStream", + serviceModel: this.serviceModel, + client: this.rrClient, + modelConfig: config, + }; + + return mqtt_request_response.StreamingOperation.create(streamingOperationConfig); + } + + /** + * Create a stream for ShadowUpdated events for the (classic) shadow of an AWS IoT thing. + * + * + * AWS documentation: https://docs.aws.amazon.com/iot/latest/developerguide/device-shadow-mqtt.html#update-documents-pub-sub-topic + * + * @param config streaming operation configuration options + * + * @returns a streaming operation which will emit an event every time a message is received on the + * associated MQTT topic + * + * @category IotShadowV2 + */ + createShadowUpdatedStream(config: model.ShadowUpdatedSubscriptionRequest) + : mqtt_request_response.StreamingOperation { + + let streamingOperationConfig : mqtt_request_response_utils.StreamingOperationConfig = { + operationName: "createShadowUpdatedStream", + serviceModel: this.serviceModel, + client: this.rrClient, + modelConfig: config, + }; + + return mqtt_request_response.StreamingOperation.create(streamingOperationConfig); + } + +} diff --git a/lib/iotshadow/clientv2_utils.ts b/lib/iotshadow/v2utils.ts similarity index 98% rename from lib/iotshadow/clientv2_utils.ts rename to lib/iotshadow/v2utils.ts index b43d6405..290ad50f 100644 --- a/lib/iotshadow/clientv2_utils.ts +++ b/lib/iotshadow/v2utils.ts @@ -230,7 +230,7 @@ function applyCorrelationTokenToUpdateNamedShadowRequest(request: any) : [any, s function createRequestResponseOperationServiceModelMap() : Map { return new Map([ - ["GetNamedShadow", { + ["getNamedShadow", { inputShapeName: "GetNamedShadowRequest", payloadTransformer: buildGetNamedShadowRequestPayload, subscriptionGenerator: buildGetNamedShadowRequestSubscriptions, @@ -238,7 +238,7 @@ function createRequestResponseOperationServiceModelMap() : Map { return new Map([ - ["CreateNamedShadowDeltaUpdatedEventStream", { + ["createNamedShadowDeltaUpdatedStream", { inputShapeName : "NamedShadowDeltaUpdatedSubscriptionRequest", subscriptionGenerator: buildNamedShadowDeltaUpdatedSubscriptionTopicFilter, deserializer: deserializeNamedShadowDeltaUpdatedPayload, }], - ["CreateNamedShadowUpdatedEventStream", { + ["createNamedShadowUpdatedStream", { inputShapeName : "NamedShadowUpdatedSubscriptionRequest", subscriptionGenerator: buildNamedShadowUpdatedSubscriptionTopicFilter, deserializer: deserializeNamedShadowUpdatedPayload, From b676e1357e053c361a75b5c3a8761fe2b14187e2 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Tue, 25 Jun 2024 13:14:29 -0700 Subject: [PATCH 08/79] Checkpoint --- lib/index.ts | 4 +- lib/iotidentity/iotidentity.ts | 19 + lib/iotidentity/iotidentityclientv2.ts | 145 +++++++ lib/iotidentity/v2utils.ts | 262 ++++++++++++ lib/iotjobs/iotjobs.ts | 19 + lib/iotjobs/iotjobsclientv2.ts | 222 ++++++++++ lib/iotjobs/v2utils.ts | 464 +++++++++++++++++++++ lib/iotshadow/iotshadow.ts | 2 - lib/iotshadow/iotshadowclientv2.ts | 5 +- lib/iotshadow/v2utils.ts | 548 ++++++++++++++++++------- lib/mqtt_request_response_utils.ts | 242 ++++++++++- 11 files changed, 1768 insertions(+), 164 deletions(-) create mode 100644 lib/iotidentity/iotidentity.ts create mode 100644 lib/iotidentity/iotidentityclientv2.ts create mode 100644 lib/iotidentity/v2utils.ts create mode 100644 lib/iotjobs/iotjobs.ts create mode 100644 lib/iotjobs/iotjobsclientv2.ts create mode 100644 lib/iotjobs/v2utils.ts diff --git a/lib/index.ts b/lib/index.ts index e9be6f98..3c2a7dfa 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -14,9 +14,9 @@ * @packageDocumentation */ -import * as iotidentity from './iotidentity/iotidentityclient'; +import * as iotidentity from './iotidentity/iotidentity'; import * as greengrass from './greengrass/discoveryclient'; -import * as iotjobs from './iotjobs/iotjobsclient'; +import * as iotjobs from './iotjobs/iotjobs'; import * as iotshadow from './iotshadow/iotshadow'; import * as eventstream_rpc from './eventstream_rpc'; import * as greengrasscoreipc from './greengrasscoreipc'; diff --git a/lib/iotidentity/iotidentity.ts b/lib/iotidentity/iotidentity.ts new file mode 100644 index 00000000..8d3dce5d --- /dev/null +++ b/lib/iotidentity/iotidentity.ts @@ -0,0 +1,19 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +/** + * @packageDocumentation + * @module identity + */ + +import * as model from "./model"; +import { IotIdentityClient } from "./iotidentityclient"; +import { IotIdentityClientv2 } from "./iotidentityclientv2"; + +export { + IotIdentityClient, + IotIdentityClientv2, + model +}; diff --git a/lib/iotidentity/iotidentityclientv2.ts b/lib/iotidentity/iotidentityclientv2.ts new file mode 100644 index 00000000..fd31bdb8 --- /dev/null +++ b/lib/iotidentity/iotidentityclientv2.ts @@ -0,0 +1,145 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + * + * This file is generated + */ + +/** + * @packageDocumentation + * @module identity + */ + +import {mqtt, mqtt5} from 'aws-crt'; +import {mqtt_request_response as mqtt_rr_internal} from 'aws-crt'; +import * as mqtt_request_response_utils from '../mqtt_request_response_utils'; +import * as model from './model'; +import * as v2utils from './v2utils'; + +/** + * An AWS IoT service that assists with provisioning a device and installing unique client certificates on it + * + * AWS documentation: https://docs.aws.amazon.com/iot/latest/developerguide/provision-wo-cert.html + * + * @category IotIdentity + */ +export class IotIdentityClientv2 { + private rrClient : mqtt_rr_internal.RequestResponseClient; + private serviceModel : mqtt_request_response_utils.RequestResponseServiceModel; + + private constructor(rrClient: mqtt_rr_internal.RequestResponseClient) { + this.rrClient = rrClient; + this.serviceModel = v2utils.makeServiceModel(); + } + + /** + * Creates a new service client that will use an SDK MQTT 311 client as transport. + * + * @param protocolClient the MQTT 311 client to use for transport + * @param options additional service client configuration options + * + * @return a new service client + * + */ + static newFromMqtt311(protocolClient: mqtt.MqttClientConnection, options: mqtt_rr_internal.RequestResponseClientOptions) : IotIdentityClientv2 { + let rrClient = mqtt_rr_internal.RequestResponseClient.newFromMqtt311(protocolClient, options); + return new IotIdentityClientv2(rrClient); + } + + /** + * Creates a new service client that will use an SDK MQTT 5 client as transport. + * + * @param protocolClient the MQTT 5 client to use for transport + * @param options additional service client configuration options + * + * @return a new service client + * + */ + static newFromMqtt5(protocolClient: mqtt5.Mqtt5Client, options: mqtt_rr_internal.RequestResponseClientOptions) : IotIdentityClientv2 { + let rrClient = mqtt_rr_internal.RequestResponseClient.newFromMqtt5(protocolClient, options); + return new IotIdentityClientv2(rrClient); + } + + /** + * Triggers cleanup of all resources associated with the service client. Closing a client will fail + * all incomplete requests and close all unclosed streaming operations. + * + * This must be called when finished with a client; otherwise, native resources will leak. + */ + close() { + this.rrClient.close(); + } + + /** + * Creates a certificate from a certificate signing request (CSR). AWS IoT provides client certificates that are signed by the Amazon Root certificate authority (CA). The new certificate has a PENDING_ACTIVATION status. When you call RegisterThing to provision a thing with this certificate, the certificate status changes to ACTIVE or INACTIVE as described in the template. + * + * + * AWS documentation: https://docs.aws.amazon.com/iot/latest/developerguide/provision-wo-cert.html#fleet-provision-api + * + * @param request operation to perform + * + * @returns Promise which resolves into the response to the request + * + * @category IotIdentityV2 + */ + async createCertificateFromCsr(request: model.CreateCertificateFromCsrRequest) : Promise { + + let config = { + operationName: "createCertificateFromCsr", + serviceModel: this.serviceModel, + client: this.rrClient, + request: request + }; + + return await mqtt_request_response_utils.doRequestResponse(config); + } + + /** + * Creates new keys and a certificate. AWS IoT provides client certificates that are signed by the Amazon Root certificate authority (CA). The new certificate has a PENDING_ACTIVATION status. When you call RegisterThing to provision a thing with this certificate, the certificate status changes to ACTIVE or INACTIVE as described in the template. + * + * + * AWS documentation: https://docs.aws.amazon.com/iot/latest/developerguide/provision-wo-cert.html#fleet-provision-api + * + * @param request operation to perform + * + * @returns Promise which resolves into the response to the request + * + * @category IotIdentityV2 + */ + async createKeysAndCertificate(request: model.CreateKeysAndCertificateRequest) : Promise { + + let config = { + operationName: "createKeysAndCertificate", + serviceModel: this.serviceModel, + client: this.rrClient, + request: request + }; + + return await mqtt_request_response_utils.doRequestResponse(config); + } + + /** + * Provisions an AWS IoT thing using a pre-defined template. + * + * + * AWS documentation: https://docs.aws.amazon.com/iot/latest/developerguide/provision-wo-cert.html#fleet-provision-api + * + * @param request operation to perform + * + * @returns Promise which resolves into the response to the request + * + * @category IotIdentityV2 + */ + async registerThing(request: model.RegisterThingRequest) : Promise { + + let config = { + operationName: "registerThing", + serviceModel: this.serviceModel, + client: this.rrClient, + request: request + }; + + return await mqtt_request_response_utils.doRequestResponse(config); + } + +} diff --git a/lib/iotidentity/v2utils.ts b/lib/iotidentity/v2utils.ts new file mode 100644 index 00000000..2d6d5a21 --- /dev/null +++ b/lib/iotidentity/v2utils.ts @@ -0,0 +1,262 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + * + * This file is generated + */ + +import * as model from "./model"; +import {fromUtf8, toUtf8} from "@aws-sdk/util-utf8-browser"; +import * as model_validation_utils from "../mqtt_request_response_utils"; +import * as mqtt_request_response_utils from "../mqtt_request_response_utils"; + +function normalizeCreateCertificateFromCsrRequest(value: model.CreateCertificateFromCsrRequest) : any { + let normalizedValue : any = {}; + + if (value.certificateSigningRequest) { + normalizedValue.certificateSigningRequest = value.certificateSigningRequest; + } + + return normalizedValue; +} + +function buildCreateCertificateFromCsrRequestPayload(request: any) : ArrayBuffer { + let value = normalizeCreateCertificateFromCsrRequest(request as model.CreateCertificateFromCsrRequest); + + return fromUtf8(JSON.stringify(value)); +} + +function applyCorrelationTokenToCreateCertificateFromCsrRequest(request: any) : [any, string | undefined] { + return [request, undefined]; +} + +function normalizeCreateKeysAndCertificateRequest(value: model.CreateKeysAndCertificateRequest) : any { + let normalizedValue : any = {}; + + + return normalizedValue; +} + +function buildCreateKeysAndCertificateRequestPayload(request: any) : ArrayBuffer { + let value = normalizeCreateKeysAndCertificateRequest(request as model.CreateKeysAndCertificateRequest); + + return fromUtf8(JSON.stringify(value)); +} + +function applyCorrelationTokenToCreateKeysAndCertificateRequest(request: any) : [any, string | undefined] { + return [request, undefined]; +} + +function normalizeRegisterThingRequest(value: model.RegisterThingRequest) : any { + let normalizedValue : any = {}; + + if (value.certificateOwnershipToken) { + normalizedValue.certificateOwnershipToken = value.certificateOwnershipToken; + } + if (value.parameters) { + normalizedValue.parameters = value.parameters; + } + + return normalizedValue; +} + +function buildRegisterThingRequestPayload(request: any) : ArrayBuffer { + let value = normalizeRegisterThingRequest(request as model.RegisterThingRequest); + + return fromUtf8(JSON.stringify(value)); +} + +function applyCorrelationTokenToRegisterThingRequest(request: any) : [any, string | undefined] { + return [request, undefined]; +} + +function buildCreateCertificateFromCsrSubscriptions(request: any) : Array { + + return new Array( + `$aws/certificates/create-from-csr/json/+`, + ); +} + +function buildCreateCertificateFromCsrPublishTopic(request: any) : string { + + return `$aws/certificates/create-from-csr/json`; +} + +function buildCreateCertificateFromCsrResponsePaths(request: any) : Array { + + return new Array( + { + topic: `$aws/certificates/create-from-csr/json/accepted`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeCreateCertificateFromCsrResponse, + }, + { + topic: `$aws/certificates/create-from-csr/json/rejected`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeV2ServiceError, + }, + ); +} + +function buildCreateKeysAndCertificateSubscriptions(request: any) : Array { + + return new Array( + `$aws/certificates/create/json/+`, + ); +} + +function buildCreateKeysAndCertificatePublishTopic(request: any) : string { + + return `$aws/certificates/create/json`; +} + +function buildCreateKeysAndCertificateResponsePaths(request: any) : Array { + + return new Array( + { + topic: `$aws/certificates/create/json/accepted`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeCreateKeysAndCertificateResponse, + }, + { + topic: `$aws/certificates/create/json/rejected`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeV2ServiceError, + }, + ); +} + +function buildRegisterThingSubscriptions(request: any) : Array { + let typedRequest: model.RegisterThingRequest = request; + + return new Array( + `$aws/provisioning-templates/${typedRequest.templateName}/provision/json/+`, + ); +} + +function buildRegisterThingPublishTopic(request: any) : string { + let typedRequest: model.RegisterThingRequest = request; + + return `$aws/provisioning-templates/${typedRequest.templateName}/provision/json`; +} + +function buildRegisterThingResponsePaths(request: any) : Array { + let typedRequest: model.RegisterThingRequest = request; + + return new Array( + { + topic: `$aws/provisioning-templates/${typedRequest.templateName}/provision/json/accepted`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeRegisterThingResponse, + }, + { + topic: `$aws/provisioning-templates/${typedRequest.templateName}/provision/json/rejected`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeV2ServiceError, + }, + ); +} + +function deserializeCreateCertificateFromCsrResponse(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + +function deserializeCreateKeysAndCertificateResponse(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + +function deserializeRegisterThingResponse(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + +function deserializeV2ServiceError(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + +function createRequestResponseOperationServiceModelMap() : Map { + return new Map([ + ["createCertificateFromCsr", { + inputShapeName: "CreateCertificateFromCsrRequest", + payloadTransformer: buildCreateCertificateFromCsrRequestPayload, + subscriptionGenerator: buildCreateCertificateFromCsrSubscriptions, + responsePathGenerator: buildCreateCertificateFromCsrResponsePaths, + publishTopicGenerator: buildCreateCertificateFromCsrPublishTopic, + correlationTokenApplicator: applyCorrelationTokenToCreateCertificateFromCsrRequest, + }], + ["createKeysAndCertificate", { + inputShapeName: "CreateKeysAndCertificateRequest", + payloadTransformer: buildCreateKeysAndCertificateRequestPayload, + subscriptionGenerator: buildCreateKeysAndCertificateSubscriptions, + responsePathGenerator: buildCreateKeysAndCertificateResponsePaths, + publishTopicGenerator: buildCreateKeysAndCertificatePublishTopic, + correlationTokenApplicator: applyCorrelationTokenToCreateKeysAndCertificateRequest, + }], + ["registerThing", { + inputShapeName: "RegisterThingRequest", + payloadTransformer: buildRegisterThingRequestPayload, + subscriptionGenerator: buildRegisterThingSubscriptions, + responsePathGenerator: buildRegisterThingResponsePaths, + publishTopicGenerator: buildRegisterThingPublishTopic, + correlationTokenApplicator: applyCorrelationTokenToRegisterThingRequest, + }], + ]); +} + + + +function createStreamingOperationServiceModelMap() : Map { + return new Map([ + ]); +} + +function validateCreateCertificateFromCsrRequest(value: any) : void { + + // @ts-ignore + let typedValue : model.CreateCertificateFromCsrRequest = value; + + model_validation_utils.validateValueAsString(value.certificateSigningRequest, 'certificateSigningRequest'); +} + +function validateCreateKeysAndCertificateRequest(value: any) : void { + + // @ts-ignore + let typedValue : model.CreateKeysAndCertificateRequest = value; + +} + +function validateRegisterThingRequest(value: any) : void { + + // @ts-ignore + let typedValue : model.RegisterThingRequest = value; + + model_validation_utils.validateValueAsTopicSegment(value.templateName, 'templateName'); + model_validation_utils.validateValueAsString(value.certificateOwnershipToken, 'certificateOwnershipToken'); + model_validation_utils.validateValueAsOptionalMap(value.parameters, model_validation_utils.validateValueAsString, model_validation_utils.validateValueAsString, 'parameters'); +} + + +function createValidatorMap() : Map void> { + return new Map void>([ + ["CreateCertificateFromCsrRequest", validateCreateCertificateFromCsrRequest], + ["CreateKeysAndCertificateRequest", validateCreateKeysAndCertificateRequest], + ["RegisterThingRequest", validateRegisterThingRequest], + ]); +} + +export function makeServiceModel() : mqtt_request_response_utils.RequestResponseServiceModel { + let model : mqtt_request_response_utils.RequestResponseServiceModel = { + requestResponseOperations: createRequestResponseOperationServiceModelMap(), + streamingOperations: createStreamingOperationServiceModelMap(), + shapeValidators: createValidatorMap() + }; + + return model; +} diff --git a/lib/iotjobs/iotjobs.ts b/lib/iotjobs/iotjobs.ts new file mode 100644 index 00000000..dacea634 --- /dev/null +++ b/lib/iotjobs/iotjobs.ts @@ -0,0 +1,19 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +/** + * @packageDocumentation + * @module jobs + */ + +import * as model from "./model"; +import { IotJobsClient } from "./iotjobsclient"; +import { IotJobsClientv2 } from "./iotjobsclientv2"; + +export { + IotJobsClient, + IotJobsClientv2, + model +}; diff --git a/lib/iotjobs/iotjobsclientv2.ts b/lib/iotjobs/iotjobsclientv2.ts new file mode 100644 index 00000000..e54059bc --- /dev/null +++ b/lib/iotjobs/iotjobsclientv2.ts @@ -0,0 +1,222 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + * + * This file is generated + */ + +/** + * @packageDocumentation + * @module jobs + */ + +import {mqtt, mqtt5} from 'aws-crt'; +import {mqtt_request_response as mqtt_rr_internal} from 'aws-crt'; +import * as mqtt_request_response from '../mqtt_request_response'; +import * as mqtt_request_response_utils from '../mqtt_request_response_utils'; +import * as model from './model'; +import * as v2utils from './v2utils'; + +/** + * The AWS IoT jobs service can be used to define a set of remote operations that are sent to and executed on one or more devices connected to AWS IoT. + * + * AWS documentation: https://docs.aws.amazon.com/iot/latest/developerguide/jobs-api.html#jobs-mqtt-api + * + * @category IotJobs + */ +export class IotJobsClientv2 { + private rrClient : mqtt_rr_internal.RequestResponseClient; + private serviceModel : mqtt_request_response_utils.RequestResponseServiceModel; + + private constructor(rrClient: mqtt_rr_internal.RequestResponseClient) { + this.rrClient = rrClient; + this.serviceModel = v2utils.makeServiceModel(); + } + + /** + * Creates a new service client that will use an SDK MQTT 311 client as transport. + * + * @param protocolClient the MQTT 311 client to use for transport + * @param options additional service client configuration options + * + * @return a new service client + * + */ + static newFromMqtt311(protocolClient: mqtt.MqttClientConnection, options: mqtt_rr_internal.RequestResponseClientOptions) : IotJobsClientv2 { + let rrClient = mqtt_rr_internal.RequestResponseClient.newFromMqtt311(protocolClient, options); + return new IotJobsClientv2(rrClient); + } + + /** + * Creates a new service client that will use an SDK MQTT 5 client as transport. + * + * @param protocolClient the MQTT 5 client to use for transport + * @param options additional service client configuration options + * + * @return a new service client + * + */ + static newFromMqtt5(protocolClient: mqtt5.Mqtt5Client, options: mqtt_rr_internal.RequestResponseClientOptions) : IotJobsClientv2 { + let rrClient = mqtt_rr_internal.RequestResponseClient.newFromMqtt5(protocolClient, options); + return new IotJobsClientv2(rrClient); + } + + /** + * Triggers cleanup of all resources associated with the service client. Closing a client will fail + * all incomplete requests and close all unclosed streaming operations. + * + * This must be called when finished with a client; otherwise, native resources will leak. + */ + close() { + this.rrClient.close(); + } + + /** + * Gets detailed information about a job execution. + * + * + * AWS documentation: https://docs.aws.amazon.com/iot/latest/developerguide/jobs-api.html#mqtt-describejobexecution + * + * @param request operation to perform + * + * @returns Promise which resolves into the response to the request + * + * @category IotJobsV2 + */ + async describeJobExecution(request: model.DescribeJobExecutionRequest) : Promise { + + let config = { + operationName: "describeJobExecution", + serviceModel: this.serviceModel, + client: this.rrClient, + request: request + }; + + return await mqtt_request_response_utils.doRequestResponse(config); + } + + /** + * Gets the list of all jobs for a thing that are not in a terminal state. + * + * + * AWS documentation: https://docs.aws.amazon.com/iot/latest/developerguide/jobs-api.html#mqtt-getpendingjobexecutions + * + * @param request operation to perform + * + * @returns Promise which resolves into the response to the request + * + * @category IotJobsV2 + */ + async getPendingJobExecutions(request: model.GetPendingJobExecutionsRequest) : Promise { + + let config = { + operationName: "getPendingJobExecutions", + serviceModel: this.serviceModel, + client: this.rrClient, + request: request + }; + + return await mqtt_request_response_utils.doRequestResponse(config); + } + + /** + * Gets and starts the next pending job execution for a thing (status IN_PROGRESS or QUEUED). + * + * + * AWS documentation: https://docs.aws.amazon.com/iot/latest/developerguide/jobs-api.html#mqtt-startnextpendingjobexecution + * + * @param request operation to perform + * + * @returns Promise which resolves into the response to the request + * + * @category IotJobsV2 + */ + async startNextPendingJobExecution(request: model.StartNextPendingJobExecutionRequest) : Promise { + + let config = { + operationName: "startNextPendingJobExecution", + serviceModel: this.serviceModel, + client: this.rrClient, + request: request + }; + + return await mqtt_request_response_utils.doRequestResponse(config); + } + + /** + * Updates the status of a job execution. You can optionally create a step timer by setting a value for the stepTimeoutInMinutes property. If you don't update the value of this property by running UpdateJobExecution again, the job execution times out when the step timer expires. + * + * + * AWS documentation: https://docs.aws.amazon.com/iot/latest/developerguide/jobs-api.html#mqtt-updatejobexecution + * + * @param request operation to perform + * + * @returns Promise which resolves into the response to the request + * + * @category IotJobsV2 + */ + async updateJobExecution(request: model.UpdateJobExecutionRequest) : Promise { + + let config = { + operationName: "updateJobExecution", + serviceModel: this.serviceModel, + client: this.rrClient, + request: request + }; + + return await mqtt_request_response_utils.doRequestResponse(config); + } + + /** + * Creates a stream of JobExecutionsChanged notifications for a given IoT thing. + * + * + * AWS documentation: https://docs.aws.amazon.com/iot/latest/developerguide/jobs-api.html#mqtt-jobexecutionschanged + * + * @param config streaming operation configuration options + * + * @returns a streaming operation which will emit an event every time a message is received on the + * associated MQTT topic + * + * @category IotJobsV2 + */ + createJobExecutionsChangedStream(config: model.JobExecutionsChangedSubscriptionRequest) + : mqtt_request_response.StreamingOperation { + + let streamingOperationConfig : mqtt_request_response_utils.StreamingOperationConfig = { + operationName: "createJobExecutionsChangedStream", + serviceModel: this.serviceModel, + client: this.rrClient, + modelConfig: config, + }; + + return mqtt_request_response.StreamingOperation.create(streamingOperationConfig); + } + + /** + * + * + * + * AWS documentation: https://docs.aws.amazon.com/iot/latest/developerguide/jobs-api.html#mqtt-nextjobexecutionchanged + * + * @param config streaming operation configuration options + * + * @returns a streaming operation which will emit an event every time a message is received on the + * associated MQTT topic + * + * @category IotJobsV2 + */ + createNextJobExecutionChangedStream(config: model.NextJobExecutionChangedSubscriptionRequest) + : mqtt_request_response.StreamingOperation { + + let streamingOperationConfig : mqtt_request_response_utils.StreamingOperationConfig = { + operationName: "createNextJobExecutionChangedStream", + serviceModel: this.serviceModel, + client: this.rrClient, + modelConfig: config, + }; + + return mqtt_request_response.StreamingOperation.create(streamingOperationConfig); + } + +} diff --git a/lib/iotjobs/v2utils.ts b/lib/iotjobs/v2utils.ts new file mode 100644 index 00000000..ee08ec3b --- /dev/null +++ b/lib/iotjobs/v2utils.ts @@ -0,0 +1,464 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + * + * This file is generated + */ + +import * as model from "./model"; +import {fromUtf8, toUtf8} from "@aws-sdk/util-utf8-browser"; +import * as model_validation_utils from "../mqtt_request_response_utils"; +import * as mqtt_request_response_utils from "../mqtt_request_response_utils"; +import {v4 as uuid} from "uuid"; + +function normalizeDescribeJobExecutionRequest(value: model.DescribeJobExecutionRequest) : any { + let normalizedValue : any = {}; + + if (value.clientToken) { + normalizedValue.clientToken = value.clientToken; + } + if (value.executionNumber) { + normalizedValue.executionNumber = value.executionNumber; + } + if (value.includeJobDocument) { + normalizedValue.includeJobDocument = value.includeJobDocument; + } + + return normalizedValue; +} + +function buildDescribeJobExecutionRequestPayload(request: any) : ArrayBuffer { + let value = normalizeDescribeJobExecutionRequest(request as model.DescribeJobExecutionRequest); + + return fromUtf8(JSON.stringify(value)); +} + +function applyCorrelationTokenToDescribeJobExecutionRequest(request: any) : [any, string | undefined] { + let typedRequest: model.DescribeJobExecutionRequest = request; + + let correlationToken = uuid(); + + typedRequest.clientToken = correlationToken; + + return [typedRequest, correlationToken]; +} + +function normalizeGetPendingJobExecutionsRequest(value: model.GetPendingJobExecutionsRequest) : any { + let normalizedValue : any = {}; + + if (value.clientToken) { + normalizedValue.clientToken = value.clientToken; + } + + return normalizedValue; +} + +function buildGetPendingJobExecutionsRequestPayload(request: any) : ArrayBuffer { + let value = normalizeGetPendingJobExecutionsRequest(request as model.GetPendingJobExecutionsRequest); + + return fromUtf8(JSON.stringify(value)); +} + +function applyCorrelationTokenToGetPendingJobExecutionsRequest(request: any) : [any, string | undefined] { + let typedRequest: model.GetPendingJobExecutionsRequest = request; + + let correlationToken = uuid(); + + typedRequest.clientToken = correlationToken; + + return [typedRequest, correlationToken]; +} + +function normalizeStartNextPendingJobExecutionRequest(value: model.StartNextPendingJobExecutionRequest) : any { + let normalizedValue : any = {}; + + if (value.clientToken) { + normalizedValue.clientToken = value.clientToken; + } + if (value.stepTimeoutInMinutes) { + normalizedValue.stepTimeoutInMinutes = value.stepTimeoutInMinutes; + } + if (value.statusDetails) { + normalizedValue.statusDetails = value.statusDetails; + } + + return normalizedValue; +} + +function buildStartNextPendingJobExecutionRequestPayload(request: any) : ArrayBuffer { + let value = normalizeStartNextPendingJobExecutionRequest(request as model.StartNextPendingJobExecutionRequest); + + return fromUtf8(JSON.stringify(value)); +} + +function applyCorrelationTokenToStartNextPendingJobExecutionRequest(request: any) : [any, string | undefined] { + let typedRequest: model.StartNextPendingJobExecutionRequest = request; + + let correlationToken = uuid(); + + typedRequest.clientToken = correlationToken; + + return [typedRequest, correlationToken]; +} + +function normalizeUpdateJobExecutionRequest(value: model.UpdateJobExecutionRequest) : any { + let normalizedValue : any = {}; + + if (value.status) { + normalizedValue.status = value.status; + } + if (value.clientToken) { + normalizedValue.clientToken = value.clientToken; + } + if (value.statusDetails) { + normalizedValue.statusDetails = value.statusDetails; + } + if (value.expectedVersion) { + normalizedValue.expectedVersion = value.expectedVersion; + } + if (value.executionNumber) { + normalizedValue.executionNumber = value.executionNumber; + } + if (value.includeJobExecutionState) { + normalizedValue.includeJobExecutionState = value.includeJobExecutionState; + } + if (value.includeJobDocument) { + normalizedValue.includeJobDocument = value.includeJobDocument; + } + if (value.stepTimeoutInMinutes) { + normalizedValue.stepTimeoutInMinutes = value.stepTimeoutInMinutes; + } + + return normalizedValue; +} + +function buildUpdateJobExecutionRequestPayload(request: any) : ArrayBuffer { + let value = normalizeUpdateJobExecutionRequest(request as model.UpdateJobExecutionRequest); + + return fromUtf8(JSON.stringify(value)); +} + +function applyCorrelationTokenToUpdateJobExecutionRequest(request: any) : [any, string | undefined] { + let typedRequest: model.UpdateJobExecutionRequest = request; + + let correlationToken = uuid(); + + typedRequest.clientToken = correlationToken; + + return [typedRequest, correlationToken]; +} + +function buildDescribeJobExecutionSubscriptions(request: any) : Array { + let typedRequest: model.DescribeJobExecutionRequest = request; + + return new Array( + `$aws/things/${typedRequest.thingName}/jobs/${typedRequest.jobId}/get/+`, + ); +} + +function buildDescribeJobExecutionPublishTopic(request: any) : string { + let typedRequest: model.DescribeJobExecutionRequest = request; + + return `$aws/things/${typedRequest.thingName}/jobs/${typedRequest.jobId}/get`; +} + +function buildDescribeJobExecutionResponsePaths(request: any) : Array { + let typedRequest: model.DescribeJobExecutionRequest = request; + + return new Array( + { + topic: `$aws/things/${typedRequest.thingName}/jobs/${typedRequest.jobId}/get/accepted`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeDescribeJobExecutionResponse, + }, + { + topic: `$aws/things/${typedRequest.thingName}/jobs/${typedRequest.jobId}/get/rejected`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeV2ServiceError, + }, + ); +} + +function buildGetPendingJobExecutionsSubscriptions(request: any) : Array { + let typedRequest: model.GetPendingJobExecutionsRequest = request; + + return new Array( + `$aws/things/${typedRequest.thingName}/jobs/get/+`, + ); +} + +function buildGetPendingJobExecutionsPublishTopic(request: any) : string { + let typedRequest: model.GetPendingJobExecutionsRequest = request; + + return `$aws/things/${typedRequest.thingName}/jobs/get`; +} + +function buildGetPendingJobExecutionsResponsePaths(request: any) : Array { + let typedRequest: model.GetPendingJobExecutionsRequest = request; + + return new Array( + { + topic: `$aws/things/${typedRequest.thingName}/jobs/get/accepted`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeGetPendingJobExecutionsResponse, + }, + { + topic: `$aws/things/${typedRequest.thingName}/jobs/get/rejected`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeV2ServiceError, + }, + ); +} + +function buildStartNextPendingJobExecutionSubscriptions(request: any) : Array { + let typedRequest: model.StartNextPendingJobExecutionRequest = request; + + return new Array( + `$aws/things/${typedRequest.thingName}/jobs/start-next/+`, + ); +} + +function buildStartNextPendingJobExecutionPublishTopic(request: any) : string { + let typedRequest: model.StartNextPendingJobExecutionRequest = request; + + return `$aws/things/${typedRequest.thingName}/jobs/start-next`; +} + +function buildStartNextPendingJobExecutionResponsePaths(request: any) : Array { + let typedRequest: model.StartNextPendingJobExecutionRequest = request; + + return new Array( + { + topic: `$aws/things/${typedRequest.thingName}/jobs/start-next/accepted`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeStartNextJobExecutionResponse, + }, + { + topic: `$aws/things/${typedRequest.thingName}/jobs/start-next/rejected`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeV2ServiceError, + }, + ); +} + +function buildUpdateJobExecutionSubscriptions(request: any) : Array { + let typedRequest: model.UpdateJobExecutionRequest = request; + + return new Array( + `$aws/things/${typedRequest.thingName}/jobs/${typedRequest.jobId}/update/+`, + ); +} + +function buildUpdateJobExecutionPublishTopic(request: any) : string { + let typedRequest: model.UpdateJobExecutionRequest = request; + + return `$aws/things/${typedRequest.thingName}/jobs/${typedRequest.jobId}/update`; +} + +function buildUpdateJobExecutionResponsePaths(request: any) : Array { + let typedRequest: model.UpdateJobExecutionRequest = request; + + return new Array( + { + topic: `$aws/things/${typedRequest.thingName}/jobs/${typedRequest.jobId}/update/accepted`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeUpdateJobExecutionResponse, + }, + { + topic: `$aws/things/${typedRequest.thingName}/jobs/${typedRequest.jobId}/update/rejected`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeV2ServiceError, + }, + ); +} + +function deserializeDescribeJobExecutionResponse(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + +function deserializeGetPendingJobExecutionsResponse(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + +function deserializeStartNextJobExecutionResponse(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + +function deserializeUpdateJobExecutionResponse(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + +function deserializeV2ServiceError(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + +function createRequestResponseOperationServiceModelMap() : Map { + return new Map([ + ["describeJobExecution", { + inputShapeName: "DescribeJobExecutionRequest", + payloadTransformer: buildDescribeJobExecutionRequestPayload, + subscriptionGenerator: buildDescribeJobExecutionSubscriptions, + responsePathGenerator: buildDescribeJobExecutionResponsePaths, + publishTopicGenerator: buildDescribeJobExecutionPublishTopic, + correlationTokenApplicator: applyCorrelationTokenToDescribeJobExecutionRequest, + }], + ["getPendingJobExecutions", { + inputShapeName: "GetPendingJobExecutionsRequest", + payloadTransformer: buildGetPendingJobExecutionsRequestPayload, + subscriptionGenerator: buildGetPendingJobExecutionsSubscriptions, + responsePathGenerator: buildGetPendingJobExecutionsResponsePaths, + publishTopicGenerator: buildGetPendingJobExecutionsPublishTopic, + correlationTokenApplicator: applyCorrelationTokenToGetPendingJobExecutionsRequest, + }], + ["startNextPendingJobExecution", { + inputShapeName: "StartNextPendingJobExecutionRequest", + payloadTransformer: buildStartNextPendingJobExecutionRequestPayload, + subscriptionGenerator: buildStartNextPendingJobExecutionSubscriptions, + responsePathGenerator: buildStartNextPendingJobExecutionResponsePaths, + publishTopicGenerator: buildStartNextPendingJobExecutionPublishTopic, + correlationTokenApplicator: applyCorrelationTokenToStartNextPendingJobExecutionRequest, + }], + ["updateJobExecution", { + inputShapeName: "UpdateJobExecutionRequest", + payloadTransformer: buildUpdateJobExecutionRequestPayload, + subscriptionGenerator: buildUpdateJobExecutionSubscriptions, + responsePathGenerator: buildUpdateJobExecutionResponsePaths, + publishTopicGenerator: buildUpdateJobExecutionPublishTopic, + correlationTokenApplicator: applyCorrelationTokenToUpdateJobExecutionRequest, + }], + ]); +} + +function buildCreateJobExecutionsChangedStreamTopicFilter(config: any) : string { + const typedConfig : model.JobExecutionsChangedSubscriptionRequest = config; + + return `$aws/things/${typedConfig.thingName}/jobs/notify`; +} + +function buildCreateNextJobExecutionChangedStreamTopicFilter(config: any) : string { + const typedConfig : model.NextJobExecutionChangedSubscriptionRequest = config; + + return `$aws/things/${typedConfig.thingName}/jobs/notify-next`; +} + + +function deserializeJobExecutionsChangedEventPayload(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + +function deserializeNextJobExecutionChangedEventPayload(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + + +function createStreamingOperationServiceModelMap() : Map { + return new Map([ + ["createJobExecutionsChangedStream", { + inputShapeName: "JobExecutionsChangedSubscriptionRequest", + subscriptionGenerator: buildCreateJobExecutionsChangedStreamTopicFilter, + deserializer: deserializeJobExecutionsChangedEventPayload, + }], + ["createNextJobExecutionChangedStream", { + inputShapeName: "NextJobExecutionChangedSubscriptionRequest", + subscriptionGenerator: buildCreateNextJobExecutionChangedStreamTopicFilter, + deserializer: deserializeNextJobExecutionChangedEventPayload, + }], + ]); +} + +function validateDescribeJobExecutionRequest(value: any) : void { + + // @ts-ignore + let typedValue : model.DescribeJobExecutionRequest = value; + + model_validation_utils.validateValueAsTopicSegment(value.thingName, 'thingName'); + model_validation_utils.validateValueAsTopicSegment(value.jobId, 'jobId'); + model_validation_utils.validateValueAsOptionalInteger(value.executionNumber, 'executionNumber'); + model_validation_utils.validateValueAsOptionalBoolean(value.includeJobDocument, 'includeJobDocument'); +} + +function validateGetPendingJobExecutionsRequest(value: any) : void { + + // @ts-ignore + let typedValue : model.GetPendingJobExecutionsRequest = value; + + model_validation_utils.validateValueAsTopicSegment(value.thingName, 'thingName'); +} + +function validateJobExecutionsChangedSubscriptionRequest(value: any) : void { + + // @ts-ignore + let typedValue : model.JobExecutionsChangedSubscriptionRequest = value; + + model_validation_utils.validateValueAsTopicSegment(value.thingName, 'thingName'); +} + +function validateNextJobExecutionChangedSubscriptionRequest(value: any) : void { + + // @ts-ignore + let typedValue : model.NextJobExecutionChangedSubscriptionRequest = value; + + model_validation_utils.validateValueAsTopicSegment(value.thingName, 'thingName'); +} + +function validateStartNextPendingJobExecutionRequest(value: any) : void { + + // @ts-ignore + let typedValue : model.StartNextPendingJobExecutionRequest = value; + + model_validation_utils.validateValueAsTopicSegment(value.thingName, 'thingName'); + model_validation_utils.validateValueAsOptionalInteger(value.stepTimeoutInMinutes, 'stepTimeoutInMinutes'); + model_validation_utils.validateValueAsOptionalMap(value.statusDetails, model_validation_utils.validateValueAsString, model_validation_utils.validateValueAsString, 'statusDetails'); +} + +function validateUpdateJobExecutionRequest(value: any) : void { + + // @ts-ignore + let typedValue : model.UpdateJobExecutionRequest = value; + + model_validation_utils.validateValueAsTopicSegment(value.thingName, 'thingName'); + model_validation_utils.validateValueAsTopicSegment(value.jobId, 'jobId'); + model_validation_utils.validateValueAsString(value.status, 'status'); + model_validation_utils.validateValueAsOptionalMap(value.statusDetails, model_validation_utils.validateValueAsString, model_validation_utils.validateValueAsString, 'statusDetails'); + model_validation_utils.validateValueAsOptionalInteger(value.expectedVersion, 'expectedVersion'); + model_validation_utils.validateValueAsOptionalInteger(value.executionNumber, 'executionNumber'); + model_validation_utils.validateValueAsOptionalBoolean(value.includeJobExecutionState, 'includeJobExecutionState'); + model_validation_utils.validateValueAsOptionalBoolean(value.includeJobDocument, 'includeJobDocument'); + model_validation_utils.validateValueAsOptionalInteger(value.stepTimeoutInMinutes, 'stepTimeoutInMinutes'); +} + + +function createValidatorMap() : Map void> { + return new Map void>([ + ["JobExecutionsChangedSubscriptionRequest", validateJobExecutionsChangedSubscriptionRequest], + ["NextJobExecutionChangedSubscriptionRequest", validateNextJobExecutionChangedSubscriptionRequest], + ["DescribeJobExecutionRequest", validateDescribeJobExecutionRequest], + ["GetPendingJobExecutionsRequest", validateGetPendingJobExecutionsRequest], + ["StartNextPendingJobExecutionRequest", validateStartNextPendingJobExecutionRequest], + ["UpdateJobExecutionRequest", validateUpdateJobExecutionRequest], + ]); +} + +export function makeServiceModel() : mqtt_request_response_utils.RequestResponseServiceModel { + let model : mqtt_request_response_utils.RequestResponseServiceModel = { + requestResponseOperations: createRequestResponseOperationServiceModelMap(), + streamingOperations: createStreamingOperationServiceModelMap(), + shapeValidators: createValidatorMap() + }; + + return model; +} diff --git a/lib/iotshadow/iotshadow.ts b/lib/iotshadow/iotshadow.ts index da518135..ada647fd 100644 --- a/lib/iotshadow/iotshadow.ts +++ b/lib/iotshadow/iotshadow.ts @@ -1,8 +1,6 @@ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0. - * - * This file is generated */ /** diff --git a/lib/iotshadow/iotshadowclientv2.ts b/lib/iotshadow/iotshadowclientv2.ts index ab87d406..95e061e8 100644 --- a/lib/iotshadow/iotshadowclientv2.ts +++ b/lib/iotshadow/iotshadowclientv2.ts @@ -15,7 +15,7 @@ import {mqtt_request_response as mqtt_rr_internal} from 'aws-crt'; import * as mqtt_request_response from '../mqtt_request_response'; import * as mqtt_request_response_utils from '../mqtt_request_response_utils'; import * as model from './model'; -import * as clientv2_utils from './v2utils'; +import * as v2utils from './v2utils'; /** * The AWS IoT Device Shadow service adds shadows to AWS IoT thing objects. Shadows are a simple data store for device properties and state. Shadows can make a device’s state available to apps and other services whether the device is connected to AWS IoT or not. @@ -30,7 +30,7 @@ export class IotShadowClientv2 { private constructor(rrClient: mqtt_rr_internal.RequestResponseClient) { this.rrClient = rrClient; - this.serviceModel = clientv2_utils.makeServiceModel(); + this.serviceModel = v2utils.makeServiceModel(); } /** @@ -215,7 +215,6 @@ export class IotShadowClientv2 { return await mqtt_request_response_utils.doRequestResponse(config); } - /** * Create a stream for NamedShadowDelta events for a named shadow of an AWS IoT thing. * diff --git a/lib/iotshadow/v2utils.ts b/lib/iotshadow/v2utils.ts index 290ad50f..840806a0 100644 --- a/lib/iotshadow/v2utils.ts +++ b/lib/iotshadow/v2utils.ts @@ -5,14 +5,67 @@ * This file is generated */ - import * as model from "./model"; import {fromUtf8, toUtf8} from "@aws-sdk/util-utf8-browser"; +import * as model_validation_utils from "../mqtt_request_response_utils"; import * as mqtt_request_response_utils from "../mqtt_request_response_utils"; import {v4 as uuid} from "uuid"; +function normalizeDeleteNamedShadowRequest(value: model.DeleteNamedShadowRequest) : any { + let normalizedValue : any = {}; + + if (value.clientToken) { + normalizedValue.clientToken = value.clientToken; + } + + return normalizedValue; +} + +function buildDeleteNamedShadowRequestPayload(request: any) : ArrayBuffer { + let value = normalizeDeleteNamedShadowRequest(request as model.DeleteNamedShadowRequest); + + return fromUtf8(JSON.stringify(value)); +} + +function applyCorrelationTokenToDeleteNamedShadowRequest(request: any) : [any, string | undefined] { + let typedRequest: model.DeleteNamedShadowRequest = request; + + let correlationToken = uuid(); + + typedRequest.clientToken = correlationToken; + + return [typedRequest, correlationToken]; +} + +function normalizeDeleteShadowRequest(value: model.DeleteShadowRequest) : any { + let normalizedValue : any = {}; + + if (value.clientToken) { + normalizedValue.clientToken = value.clientToken; + } + + return normalizedValue; +} + +function buildDeleteShadowRequestPayload(request: any) : ArrayBuffer { + let value = normalizeDeleteShadowRequest(request as model.DeleteShadowRequest); + + return fromUtf8(JSON.stringify(value)); +} + +function applyCorrelationTokenToDeleteShadowRequest(request: any) : [any, string | undefined] { + let typedRequest: model.DeleteShadowRequest = request; + + let correlationToken = uuid(); + + typedRequest.clientToken = correlationToken; + + return [typedRequest, correlationToken]; +} + function normalizeGetNamedShadowRequest(value: model.GetNamedShadowRequest) : any { let normalizedValue : any = {}; + if (value.clientToken) { normalizedValue.clientToken = value.clientToken; } @@ -26,52 +79,66 @@ function buildGetNamedShadowRequestPayload(request: any) : ArrayBuffer { return fromUtf8(JSON.stringify(value)); } -function buildGetNamedShadowRequestSubscriptions(request: any) : Array { +function applyCorrelationTokenToGetNamedShadowRequest(request: any) : [any, string | undefined] { let typedRequest: model.GetNamedShadowRequest = request; - return new Array( - `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/get/+` - ); + let correlationToken = uuid(); + + typedRequest.clientToken = correlationToken; + + return [typedRequest, correlationToken]; } -function deserializeGetShadowResponse(payload: ArrayBuffer) : any { - const payload_text = toUtf8(new Uint8Array(payload)); +function normalizeGetShadowRequest(value: model.GetShadowRequest) : any { + let normalizedValue : any = {}; - return JSON.parse(payload_text); + if (value.clientToken) { + normalizedValue.clientToken = value.clientToken; + } + + return normalizedValue; } -function deserializeErrorResponse(payload: ArrayBuffer): any { - const payload_text = toUtf8(new Uint8Array(payload)); - let errorResponse = JSON.parse(payload_text); +function buildGetShadowRequestPayload(request: any) : ArrayBuffer { + let value = normalizeGetShadowRequest(request as model.GetShadowRequest); - throw mqtt_request_response_utils.createServiceError("Operation failed with modeled service error", undefined, errorResponse); + return fromUtf8(JSON.stringify(value)); } -function buildGetNamedShadowRequestResponsePaths(request: any) : Array { - let typedRequest: model.GetNamedShadowRequest = request; +function applyCorrelationTokenToGetShadowRequest(request: any) : [any, string | undefined] { + let typedRequest: model.GetShadowRequest = request; - return new Array( - { - topic: `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/get/accepted`, - correlationTokenJsonPath: "clientToken", - deserializer: deserializeGetShadowResponse, - }, - { - topic: `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/get/rejected`, - correlationTokenJsonPath: "clientToken", - deserializer: deserializeErrorResponse, - }, - ) + let correlationToken = uuid(); + + typedRequest.clientToken = correlationToken; + + return [typedRequest, correlationToken]; } -function buildGetNamedShadowRequestPublishTopic(request: any) : string { - let typedRequest: model.GetNamedShadowRequest = request; +function normalizeUpdateNamedShadowRequest(value: model.UpdateNamedShadowRequest) : any { + let normalizedValue : any = {}; - return `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/get`; + if (value.clientToken) { + normalizedValue.clientToken = value.clientToken; + } + if (value.state) { + normalizedValue.state = value.state; + } + if (value.version) { + normalizedValue.version = value.version; + } + + return normalizedValue; } -function applyCorrelationTokenToGetNamedShadowRequest(request: any) : [any, string | undefined] { - let typedRequest: model.GetNamedShadowRequest = request; +function buildUpdateNamedShadowRequestPayload(request: any) : ArrayBuffer { + let value = normalizeUpdateNamedShadowRequest(request as model.UpdateNamedShadowRequest); + + return fromUtf8(JSON.stringify(value)); +} + +function applyCorrelationTokenToUpdateNamedShadowRequest(request: any) : [any, string | undefined] { + let typedRequest: model.UpdateNamedShadowRequest = request; let correlationToken = uuid(); @@ -80,36 +147,53 @@ function applyCorrelationTokenToGetNamedShadowRequest(request: any) : [any, stri return [typedRequest, correlationToken]; } -function normalizeDeleteNamedShadowRequest(value: model.DeleteNamedShadowRequest) : any { +function normalizeUpdateShadowRequest(value: model.UpdateShadowRequest) : any { let normalizedValue : any = {}; + if (value.clientToken) { normalizedValue.clientToken = value.clientToken; } + if (value.state) { + normalizedValue.state = value.state; + } + if (value.version) { + normalizedValue.version = value.version; + } return normalizedValue; } -function buildDeleteNamedShadowRequestPayload(request: any) : ArrayBuffer { - let value = normalizeDeleteNamedShadowRequest(request as model.DeleteNamedShadowRequest); +function buildUpdateShadowRequestPayload(request: any) : ArrayBuffer { + let value = normalizeUpdateShadowRequest(request as model.UpdateShadowRequest); return fromUtf8(JSON.stringify(value)); } -function buildDeleteNamedShadowRequestSubscriptions(request: any) : Array { +function applyCorrelationTokenToUpdateShadowRequest(request: any) : [any, string | undefined] { + let typedRequest: model.UpdateShadowRequest = request; + + let correlationToken = uuid(); + + typedRequest.clientToken = correlationToken; + + return [typedRequest, correlationToken]; +} + +function buildDeleteNamedShadowSubscriptions(request: any) : Array { let typedRequest: model.DeleteNamedShadowRequest = request; return new Array( - `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/delete/+` + `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/delete/+`, ); } -function deserializeDeleteShadowResponse(payload: ArrayBuffer) : any { - const payload_text = toUtf8(new Uint8Array(payload)); +function buildDeleteNamedShadowPublishTopic(request: any) : string { + let typedRequest: model.DeleteNamedShadowRequest = request; - return JSON.parse(payload_text); + return `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/delete`; } -function buildDeleteNamedShadowRequestResponsePaths(request: any) : Array { +function buildDeleteNamedShadowResponsePaths(request: any) : Array { let typedRequest: model.DeleteNamedShadowRequest = request; return new Array( @@ -121,81 +205,120 @@ function buildDeleteNamedShadowRequestResponsePaths(request: any) : Array { + let typedRequest: model.DeleteShadowRequest = request; - return `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/delete`; + return new Array( + `$aws/things/${typedRequest.thingName}/shadow/delete/+`, + ); } -function applyCorrelationTokenToDeleteNamedShadowRequest(request: any) : [any, string | undefined] { - let typedRequest: model.DeleteNamedShadowRequest = request; +function buildDeleteShadowPublishTopic(request: any) : string { + let typedRequest: model.DeleteShadowRequest = request; - let correlationToken = uuid(); + return `$aws/things/${typedRequest.thingName}/shadow/delete`; +} - typedRequest.clientToken = correlationToken; +function buildDeleteShadowResponsePaths(request: any) : Array { + let typedRequest: model.DeleteShadowRequest = request; - return [typedRequest, correlationToken]; + return new Array( + { + topic: `$aws/things/${typedRequest.thingName}/shadow/delete/accepted`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeDeleteShadowResponse, + }, + { + topic: `$aws/things/${typedRequest.thingName}/shadow/delete/rejected`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeV2ServiceError, + }, + ); } -function normalizeShadowState(value: model.ShadowState) : any { - let normalizedValue : any = {}; +function buildGetNamedShadowSubscriptions(request: any) : Array { + let typedRequest: model.GetNamedShadowRequest = request; - if (value.desired) { - normalizedValue.desired = value.desired; - } + return new Array( + `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/get/+`, + ); +} - if (value.reported) { - normalizedValue.reported = value.reported; - } +function buildGetNamedShadowPublishTopic(request: any) : string { + let typedRequest: model.GetNamedShadowRequest = request; - return normalizedValue; + return `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/get`; } -function normalizeUpdateNamedShadowRequest(value: model.UpdateNamedShadowRequest) : any { - let normalizedValue : any = {}; +function buildGetNamedShadowResponsePaths(request: any) : Array { + let typedRequest: model.GetNamedShadowRequest = request; - if (value.clientToken) { - normalizedValue.clientToken = value.clientToken; - } + return new Array( + { + topic: `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/get/accepted`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeGetShadowResponse, + }, + { + topic: `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/get/rejected`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeV2ServiceError, + }, + ); +} - if (value.version) { - normalizedValue.version = value.clientToken; - } +function buildGetShadowSubscriptions(request: any) : Array { + let typedRequest: model.GetShadowRequest = request; - if (value.state) { - normalizedValue.state = normalizeShadowState(value.state); - } + return new Array( + `$aws/things/${typedRequest.thingName}/shadow/get/+`, + ); +} - return normalizedValue; +function buildGetShadowPublishTopic(request: any) : string { + let typedRequest: model.GetShadowRequest = request; + + return `$aws/things/${typedRequest.thingName}/shadow/get`; } -function buildUpdateNamedShadowRequestPayload(request: any) : ArrayBuffer { - let value = normalizeUpdateNamedShadowRequest(request as model.UpdateNamedShadowRequest); +function buildGetShadowResponsePaths(request: any) : Array { + let typedRequest: model.GetShadowRequest = request; - return fromUtf8(JSON.stringify(value)); + return new Array( + { + topic: `$aws/things/${typedRequest.thingName}/shadow/get/accepted`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeGetShadowResponse, + }, + { + topic: `$aws/things/${typedRequest.thingName}/shadow/get/rejected`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeV2ServiceError, + }, + ); } -function buildUpdateNamedShadowRequestSubscriptions(request: any) : Array { +function buildUpdateNamedShadowSubscriptions(request: any) : Array { let typedRequest: model.UpdateNamedShadowRequest = request; return new Array( `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/update/accepted`, - `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/update/rejected` + `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/update/rejected`, ); } -function deserializeUpdateShadowResponse(payload: ArrayBuffer) : any { - const payload_text = toUtf8(new Uint8Array(payload)); +function buildUpdateNamedShadowPublishTopic(request: any) : string { + let typedRequest: model.UpdateNamedShadowRequest = request; - return JSON.parse(payload_text); + return `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/update`; } -function buildUpdateNamedShadowRequestResponsePaths(request: any) : Array { +function buildUpdateNamedShadowResponsePaths(request: any) : Array { let typedRequest: model.UpdateNamedShadowRequest = request; return new Array( @@ -207,143 +330,294 @@ function buildUpdateNamedShadowRequestResponsePaths(request: any) : Array { + let typedRequest: model.UpdateShadowRequest = request; - return `$aws/things/${typedRequest.thingName}/shadow/name/${typedRequest.shadowName}/update`; + return new Array( + `$aws/things/${typedRequest.thingName}/shadow/update/accepted`, + `$aws/things/${typedRequest.thingName}/shadow/update/rejected`, + ); } -function applyCorrelationTokenToUpdateNamedShadowRequest(request: any) : [any, string | undefined] { - let typedRequest: model.UpdateNamedShadowRequest = request; +function buildUpdateShadowPublishTopic(request: any) : string { + let typedRequest: model.UpdateShadowRequest = request; - let correlationToken = uuid(); + return `$aws/things/${typedRequest.thingName}/shadow/update`; +} - typedRequest.clientToken = correlationToken; +function buildUpdateShadowResponsePaths(request: any) : Array { + let typedRequest: model.UpdateShadowRequest = request; - return [typedRequest, correlationToken]; + return new Array( + { + topic: `$aws/things/${typedRequest.thingName}/shadow/update/accepted`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeUpdateShadowResponse, + }, + { + topic: `$aws/things/${typedRequest.thingName}/shadow/update/rejected`, + correlationTokenJsonPath: "clientToken", + deserializer: deserializeV2ServiceError, + }, + ); +} + +function deserializeDeleteShadowResponse(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + +function deserializeGetShadowResponse(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + +function deserializeUpdateShadowResponse(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + +function deserializeV2ServiceError(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); } function createRequestResponseOperationServiceModelMap() : Map { return new Map([ + ["deleteNamedShadow", { + inputShapeName: "DeleteNamedShadowRequest", + payloadTransformer: buildDeleteNamedShadowRequestPayload, + subscriptionGenerator: buildDeleteNamedShadowSubscriptions, + responsePathGenerator: buildDeleteNamedShadowResponsePaths, + publishTopicGenerator: buildDeleteNamedShadowPublishTopic, + correlationTokenApplicator: applyCorrelationTokenToDeleteNamedShadowRequest, + }], + ["deleteShadow", { + inputShapeName: "DeleteShadowRequest", + payloadTransformer: buildDeleteShadowRequestPayload, + subscriptionGenerator: buildDeleteShadowSubscriptions, + responsePathGenerator: buildDeleteShadowResponsePaths, + publishTopicGenerator: buildDeleteShadowPublishTopic, + correlationTokenApplicator: applyCorrelationTokenToDeleteShadowRequest, + }], ["getNamedShadow", { inputShapeName: "GetNamedShadowRequest", payloadTransformer: buildGetNamedShadowRequestPayload, - subscriptionGenerator: buildGetNamedShadowRequestSubscriptions, - responsePathGenerator: buildGetNamedShadowRequestResponsePaths, - publishTopicGenerator: buildGetNamedShadowRequestPublishTopic, + subscriptionGenerator: buildGetNamedShadowSubscriptions, + responsePathGenerator: buildGetNamedShadowResponsePaths, + publishTopicGenerator: buildGetNamedShadowPublishTopic, correlationTokenApplicator: applyCorrelationTokenToGetNamedShadowRequest, }], - ["deleteNamedShadow", { - inputShapeName: "DeleteNamedShadowRequest", - payloadTransformer: buildDeleteNamedShadowRequestPayload, - subscriptionGenerator: buildDeleteNamedShadowRequestSubscriptions, - responsePathGenerator: buildDeleteNamedShadowRequestResponsePaths, - publishTopicGenerator: buildDeleteNamedShadowRequestPublishTopic, - correlationTokenApplicator: applyCorrelationTokenToDeleteNamedShadowRequest, + ["getShadow", { + inputShapeName: "GetShadowRequest", + payloadTransformer: buildGetShadowRequestPayload, + subscriptionGenerator: buildGetShadowSubscriptions, + responsePathGenerator: buildGetShadowResponsePaths, + publishTopicGenerator: buildGetShadowPublishTopic, + correlationTokenApplicator: applyCorrelationTokenToGetShadowRequest, }], ["updateNamedShadow", { inputShapeName: "UpdateNamedShadowRequest", payloadTransformer: buildUpdateNamedShadowRequestPayload, - subscriptionGenerator: buildUpdateNamedShadowRequestSubscriptions, - responsePathGenerator: buildUpdateNamedShadowRequestResponsePaths, - publishTopicGenerator: buildUpdateNamedShadowRequestPublishTopic, + subscriptionGenerator: buildUpdateNamedShadowSubscriptions, + responsePathGenerator: buildUpdateNamedShadowResponsePaths, + publishTopicGenerator: buildUpdateNamedShadowPublishTopic, correlationTokenApplicator: applyCorrelationTokenToUpdateNamedShadowRequest, }], + ["updateShadow", { + inputShapeName: "UpdateShadowRequest", + payloadTransformer: buildUpdateShadowRequestPayload, + subscriptionGenerator: buildUpdateShadowSubscriptions, + responsePathGenerator: buildUpdateShadowResponsePaths, + publishTopicGenerator: buildUpdateShadowPublishTopic, + correlationTokenApplicator: applyCorrelationTokenToUpdateShadowRequest, + }], ]); } -function buildNamedShadowDeltaUpdatedSubscriptionTopicFilter(config: any) : string { +function buildCreateNamedShadowDeltaUpdatedStreamTopicFilter(config: any) : string { const typedConfig : model.NamedShadowDeltaUpdatedSubscriptionRequest = config; return `$aws/things/${typedConfig.thingName}/shadow/name/${typedConfig.shadowName}/update/delta`; } -function deserializeNamedShadowDeltaUpdatedPayload(payload: ArrayBuffer) : any { - const payload_text = toUtf8(new Uint8Array(payload)); +function buildCreateNamedShadowUpdatedStreamTopicFilter(config: any) : string { + const typedConfig : model.NamedShadowUpdatedSubscriptionRequest = config; - return JSON.parse(payload_text); + return `$aws/things/${typedConfig.thingName}/shadow/name/${typedConfig.shadowName}/update/documents`; } -function buildNamedShadowUpdatedSubscriptionTopicFilter(config: any) : string { - const typedConfig : model.NamedShadowUpdatedSubscriptionRequest = config; +function buildCreateShadowDeltaUpdatedStreamTopicFilter(config: any) : string { + const typedConfig : model.ShadowDeltaUpdatedSubscriptionRequest = config; - return `$aws/things/${typedConfig.thingName}/shadow/name/${typedConfig.shadowName}/update/document`; + return `$aws/things/${typedConfig.thingName}/shadow/update/delta`; } -function deserializeNamedShadowUpdatedPayload(payload: ArrayBuffer) : any { +function buildCreateShadowUpdatedStreamTopicFilter(config: any) : string { + const typedConfig : model.ShadowUpdatedSubscriptionRequest = config; + + return `$aws/things/${typedConfig.thingName}/shadow/update/documents`; +} + + +function deserializeShadowDeltaUpdatedEventPayload(payload: ArrayBuffer) : any { const payload_text = toUtf8(new Uint8Array(payload)); return JSON.parse(payload_text); } +function deserializeShadowUpdatedEventPayload(payload: ArrayBuffer) : any { + const payload_text = toUtf8(new Uint8Array(payload)); + + return JSON.parse(payload_text); +} + + function createStreamingOperationServiceModelMap() : Map { return new Map([ ["createNamedShadowDeltaUpdatedStream", { - inputShapeName : "NamedShadowDeltaUpdatedSubscriptionRequest", - subscriptionGenerator: buildNamedShadowDeltaUpdatedSubscriptionTopicFilter, - deserializer: deserializeNamedShadowDeltaUpdatedPayload, + inputShapeName: "NamedShadowDeltaUpdatedSubscriptionRequest", + subscriptionGenerator: buildCreateNamedShadowDeltaUpdatedStreamTopicFilter, + deserializer: deserializeShadowDeltaUpdatedEventPayload, }], ["createNamedShadowUpdatedStream", { - inputShapeName : "NamedShadowUpdatedSubscriptionRequest", - subscriptionGenerator: buildNamedShadowUpdatedSubscriptionTopicFilter, - deserializer: deserializeNamedShadowUpdatedPayload, + inputShapeName: "NamedShadowUpdatedSubscriptionRequest", + subscriptionGenerator: buildCreateNamedShadowUpdatedStreamTopicFilter, + deserializer: deserializeShadowUpdatedEventPayload, + }], + ["createShadowDeltaUpdatedStream", { + inputShapeName: "ShadowDeltaUpdatedSubscriptionRequest", + subscriptionGenerator: buildCreateShadowDeltaUpdatedStreamTopicFilter, + deserializer: deserializeShadowDeltaUpdatedEventPayload, + }], + ["createShadowUpdatedStream", { + inputShapeName: "ShadowUpdatedSubscriptionRequest", + subscriptionGenerator: buildCreateShadowUpdatedStreamTopicFilter, + deserializer: deserializeShadowUpdatedEventPayload, }], ]); } function validateDeleteNamedShadowRequest(value: any) : void { + + // @ts-ignore let typedValue : model.DeleteNamedShadowRequest = value; - mqtt_request_response_utils.validateValueAsTopicSegment(typedValue.thingName, "thingName", "DeleteNamedShadowRequest"); - mqtt_request_response_utils.validateValueAsTopicSegment(typedValue.shadowName, "shadowName", "DeleteNamedShadowRequest"); + model_validation_utils.validateValueAsTopicSegment(value.thingName, 'thingName'); + model_validation_utils.validateValueAsTopicSegment(value.shadowName, 'shadowName'); } -function validateGetNamedShadowRequest(value: any) : void { - let typedValue : model.GetNamedShadowRequest = value; +function validateDeleteShadowRequest(value: any) : void { + + // @ts-ignore + let typedValue : model.DeleteShadowRequest = value; - mqtt_request_response_utils.validateValueAsTopicSegment(typedValue.thingName, "thingName", "GetNamedShadowRequest"); - mqtt_request_response_utils.validateValueAsTopicSegment(typedValue.shadowName, "shadowName", "GetNamedShadowRequest"); + model_validation_utils.validateValueAsTopicSegment(value.thingName, 'thingName'); } -function validateShadowState(value: any) : void { +function validateGetNamedShadowRequest(value: any) : void { + + // @ts-ignore + let typedValue : model.GetNamedShadowRequest = value; + + model_validation_utils.validateValueAsTopicSegment(value.thingName, 'thingName'); + model_validation_utils.validateValueAsTopicSegment(value.shadowName, 'shadowName'); } -function validateUpdateNamedShadowRequest(value: any) : void { - let typedValue : model.UpdateNamedShadowRequest = value; +function validateGetShadowRequest(value: any) : void { - mqtt_request_response_utils.validateValueAsTopicSegment(typedValue.thingName, "thingName", "UpdateNamedShadowRequest"); - mqtt_request_response_utils.validateValueAsTopicSegment(typedValue.shadowName, "shadowName", "UpdateNamedShadowRequest"); - mqtt_request_response_utils.validateOptionalValueAsNumber(typedValue.version, "version", "UpdateNamedShadowRequest"); + // @ts-ignore + let typedValue : model.GetShadowRequest = value; - validateShadowState(typedValue.state); + model_validation_utils.validateValueAsTopicSegment(value.thingName, 'thingName'); } function validateNamedShadowDeltaUpdatedSubscriptionRequest(value: any) : void { + + // @ts-ignore let typedValue : model.NamedShadowDeltaUpdatedSubscriptionRequest = value; - mqtt_request_response_utils.validateValueAsTopicSegment(typedValue.thingName, "thingName", "NamedShadowDeltaUpdatedSubscriptionRequest"); - mqtt_request_response_utils.validateValueAsTopicSegment(typedValue.shadowName, "shadowName", "NamedShadowDeltaUpdatedSubscriptionRequest"); + model_validation_utils.validateValueAsTopicSegment(value.thingName, 'thingName'); + model_validation_utils.validateValueAsTopicSegment(value.shadowName, 'shadowName'); } function validateNamedShadowUpdatedSubscriptionRequest(value: any) : void { + + // @ts-ignore let typedValue : model.NamedShadowUpdatedSubscriptionRequest = value; - mqtt_request_response_utils.validateValueAsTopicSegment(typedValue.thingName, "thingName", "NamedShadowUpdatedSubscriptionRequest"); - mqtt_request_response_utils.validateValueAsTopicSegment(typedValue.shadowName, "shadowName", "NamedShadowUpdatedSubscriptionRequest"); + model_validation_utils.validateValueAsTopicSegment(value.thingName, 'thingName'); + model_validation_utils.validateValueAsTopicSegment(value.shadowName, 'shadowName'); +} + +function validateShadowDeltaUpdatedSubscriptionRequest(value: any) : void { + + // @ts-ignore + let typedValue : model.ShadowDeltaUpdatedSubscriptionRequest = value; + + model_validation_utils.validateValueAsTopicSegment(value.thingName, 'thingName'); } +function validateShadowState(value: any) : void { + + // @ts-ignore + let typedValue : model.ShadowState = value; + + model_validation_utils.validateValueAsOptionalAny(value.desired, 'desired'); + model_validation_utils.validateValueAsOptionalAny(value.reported, 'reported'); +} + +function validateShadowUpdatedSubscriptionRequest(value: any) : void { + + // @ts-ignore + let typedValue : model.ShadowUpdatedSubscriptionRequest = value; + + model_validation_utils.validateValueAsTopicSegment(value.thingName, 'thingName'); +} + +function validateUpdateNamedShadowRequest(value: any) : void { + + // @ts-ignore + let typedValue : model.UpdateNamedShadowRequest = value; + + model_validation_utils.validateValueAsTopicSegment(value.thingName, 'thingName'); + model_validation_utils.validateValueAsTopicSegment(value.shadowName, 'shadowName'); + model_validation_utils.validateValueAsOptionalObject(value.state, validateShadowState, 'state'); + model_validation_utils.validateValueAsOptionalInteger(value.version, 'version'); +} + +function validateUpdateShadowRequest(value: any) : void { + + // @ts-ignore + let typedValue : model.UpdateShadowRequest = value; + + model_validation_utils.validateValueAsTopicSegment(value.thingName, 'thingName'); + model_validation_utils.validateValueAsOptionalObject(value.state, validateShadowState, 'state'); + model_validation_utils.validateValueAsOptionalInteger(value.version, 'version'); +} + + function createValidatorMap() : Map void> { return new Map void>([ + ["NamedShadowDeltaUpdatedSubscriptionRequest", validateNamedShadowDeltaUpdatedSubscriptionRequest], + ["NamedShadowUpdatedSubscriptionRequest", validateNamedShadowUpdatedSubscriptionRequest], + ["ShadowDeltaUpdatedSubscriptionRequest", validateShadowDeltaUpdatedSubscriptionRequest], + ["ShadowUpdatedSubscriptionRequest", validateShadowUpdatedSubscriptionRequest], ["DeleteNamedShadowRequest", validateDeleteNamedShadowRequest], + ["DeleteShadowRequest", validateDeleteShadowRequest], ["GetNamedShadowRequest", validateGetNamedShadowRequest], + ["GetShadowRequest", validateGetShadowRequest], ["UpdateNamedShadowRequest", validateUpdateNamedShadowRequest], - ["NamedShadowDeltaUpdatedSubscriptionRequest", validateNamedShadowDeltaUpdatedSubscriptionRequest], - ["NamedShadowUpdatedSubscriptionRequest", validateNamedShadowUpdatedSubscriptionRequest] + ["UpdateShadowRequest", validateUpdateShadowRequest], ]); } @@ -355,4 +629,4 @@ export function makeServiceModel() : mqtt_request_response_utils.RequestResponse }; return model; -} \ No newline at end of file +} diff --git a/lib/mqtt_request_response_utils.ts b/lib/mqtt_request_response_utils.ts index 1d0df39a..150534c8 100644 --- a/lib/mqtt_request_response_utils.ts +++ b/lib/mqtt_request_response_utils.ts @@ -118,10 +118,13 @@ export async function doRequestResponse(options: RequestResponseOp subscriptionTopicFilters: subscriptionsNeeded, responsePaths: responsePaths, publishTopic: publishTopic, - payload: payload, - correlationToken: correlationToken + payload: payload }; + if (correlationToken) { + requestOptions.correlationToken = correlationToken; + } + let response = await options.client.submitRequest(requestOptions); let responseTopic = response.topic; @@ -157,56 +160,255 @@ export function createServiceError(description: string, internalError?: CrtError }); } -function throwMissingPropertyError(propertyName?: string, shapeType?: string) : void { - if (propertyName && shapeType) { - throw createServiceError(`validation failure - missing required property '${propertyName}' of type '${shapeType}'`); +function throwMissingPropertyError(propertyName?: string) : void { + if (propertyName) { + throw createServiceError(`validation failure - missing required property '${propertyName}'`); } else { throw createServiceError(`validation failure - missing required property`); } } -function throwInvalidPropertyValueError(valueDescription: string, propertyName?: string, shapeType?: string) : void { - if (propertyName && shapeType) { - throw createServiceError(`validation failure - property '${propertyName}' of type '${shapeType}' must be ${valueDescription}`); +function throwInvalidPropertyValueError(valueDescription: string, propertyName?: string) : void { + if (propertyName) { + throw createServiceError(`validation failure - property '${propertyName}' must be ${valueDescription}`); } else { throw createServiceError(`validation failure - property must be ${valueDescription}`); } } -export function validateValueAsTopicSegment(value : any, propertyName?: string, shapeType?: string) : void { +export function validateValueAsTopicSegment(value : any, propertyName?: string) : void { if (value === undefined) { - throwMissingPropertyError(propertyName, shapeType); + throwMissingPropertyError(propertyName); } if (typeof value !== 'string') { - throwInvalidPropertyValueError("a string", propertyName, shapeType); + throwInvalidPropertyValueError("a string", propertyName); } if (value.includes("/") || value.includes("#") || value.includes("+")) { - throwInvalidPropertyValueError("a valid MQTT topic", propertyName, shapeType); + throwInvalidPropertyValueError("a valid MQTT topic", propertyName); } } -export function validateOptionalValueAsNumber(value: any, propertyName?: string, shapeType?: string) { +export function validateOptionalValueAsNumber(value: any, propertyName?: string) { if (value === undefined) { return; } - validateValueAsNumber(value, propertyName, shapeType); + validateValueAsNumber(value, propertyName); } -export function validateValueAsNumber(value: any, propertyName?: string, shapeType?: string) { +export function validateValueAsNumber(value: any, propertyName?: string) { if (value == undefined) { - throwMissingPropertyError(propertyName, shapeType); + throwMissingPropertyError(propertyName); } if (typeof value !== 'number') { - throwInvalidPropertyValueError("a number", propertyName, shapeType); + throwInvalidPropertyValueError("a number", propertyName); } } -export function validateValueAsObject(value: any, propertyName?: string, shapeType?: string) { - if (value == undefined) { - throwMissingPropertyError(propertyName, shapeType); +////////////// + +export function validateValueAsString(value : any, propertyName?: string) : void { + if (value === undefined) { + throwMissingPropertyError(propertyName); + } + + if (typeof value !== 'string') { + throwInvalidPropertyValueError('a string value', propertyName); + } +} + +export function validateValueAsOptionalString(value : any, propertyName?: string) : void { + if (value === undefined) { + return; + } + + validateValueAsString(value, propertyName); +} + +export function validateValueAsInteger(value : any, propertyName?: string) { + if (value === undefined) { + throwMissingPropertyError(propertyName); + } + + if (typeof value !== 'number' || !Number.isSafeInteger(value as number)) { + throwInvalidPropertyValueError('an integer value', propertyName); + } +} + +export function validateValueAsOptionalInteger(value : any, propertyName?: string) { + if (value === undefined) { + return; + } + + validateValueAsInteger(value, propertyName); +} + +export function validateValueAsBoolean(value : any, propertyName?: string) { + if (value === undefined) { + throwMissingPropertyError(propertyName); + } + + if (typeof value !== 'boolean') { + throwInvalidPropertyValueError('a boolean value', propertyName); + } +} + +export function validateValueAsOptionalBoolean(value : any, propertyName?: string) { + if (value === undefined) { + return; + } + + validateValueAsBoolean(value, propertyName); +} + +export function validateValueAsDate(value : any, propertyName?: string) { + if (value === undefined) { + throwMissingPropertyError(propertyName); + } + + if (!(value instanceof Date) || isNaN((value as Date).getTime())) { + throwInvalidPropertyValueError('a Date value', propertyName); + } +} + +export function validateValueAsOptionalDate(value : any, propertyName?: string) { + if (value === undefined) { + return; + } + + validateValueAsDate(value, propertyName); +} + +export function validateValueAsBlob(value : any, propertyName?: string) { + if (value === undefined) { + throwMissingPropertyError(propertyName); + } + + /* there doesn't seem to be a good way of checking if something is an ArrayBuffer */ + if ((typeof value !== 'string') && !ArrayBuffer.isView(value) && (!value.byteLength || !value.maxByteLength)) { + throwInvalidPropertyValueError('a value convertible to a binary payload', propertyName); + } +} + +export function validateValueAsOptionalBlob(value : any, propertyName?: string) { + if (value === undefined) { + return; + } + + validateValueAsBlob(value, propertyName); +} + +export function validateValueAsAny(value : any, propertyName?: string) { + if (value === undefined) { + throwMissingPropertyError(propertyName); } } + +export function validateValueAsOptionalAny(value : any, propertyName?: string) { + if (value === undefined) { + return; + } + + validateValueAsAny(value, propertyName); +} + +export type ElementValidator = (value : any) => void; + +export function validateValueAsArray(value : any, elementValidator : ElementValidator, propertyName?: string) { + if (value === undefined) { + throwMissingPropertyError(propertyName); + } + + if (!Array.isArray(value)) { + throwInvalidPropertyValueError('an array value', propertyName); + } + + for (const element of value) { + try { + elementValidator(element); + } catch (err) { + let serviceError : ServiceError = err as ServiceError; + if (propertyName) { + throw createServiceError(`Array property '${propertyName}' contains an invalid value: ${serviceError.toString()}`); + } else { + throw createServiceError(`Array contains an invalid value: ${serviceError.toString()}`); + } + } + } +} + +export function validateValueAsOptionalArray(value : any, elementValidator : ElementValidator, propertyName?: string) { + if (value === undefined) { + return; + } + + validateValueAsArray(value, elementValidator, propertyName); +} + +export function validateValueAsMap(value : any, keyValidator : ElementValidator, valueValidator : ElementValidator, propertyName?: string) { + if (value === undefined) { + return; + } + + if (!(value instanceof Map)) { + throwInvalidPropertyValueError('a map value', propertyName); + } + + let valueAsMap = value as Map; + for (const [key, val] of valueAsMap) { + try { + keyValidator(key); + } catch (err) { + let serviceError : ServiceError = err as ServiceError; + if (propertyName) { + throw createServiceError(`Map property '${propertyName}' contains an invalid key: ${serviceError.toString()}`); + } else { + throw createServiceError(`Map contains an invalid key: ${serviceError.toString()}`); + } + } + + try { + valueValidator(val); + } catch (err) { + let serviceError : ServiceError = err as ServiceError; + if (propertyName) { + throw createServiceError(`Map property '${propertyName}' contains an invalid value: ${serviceError.toString()}`); + } else { + throw createServiceError(`Map contains an invalid value: ${serviceError.toString()}`); + } + } + } +} + +export function validateValueAsOptionalMap(value : any, keyValidator : ElementValidator, valueValidator : ElementValidator, propertyName?: string) { + if (value === undefined) { + return; + } + + validateValueAsMap(value, keyValidator, valueValidator, propertyName); +} + +export function validateValueAsObject(value : any, elementValidator : ElementValidator, propertyName: string) { + if (value === undefined) { + throwMissingPropertyError(propertyName); + } + + try { + elementValidator(value); + } catch (err) { + let serviceError : ServiceError = err as ServiceError; + throw createServiceError(`Property '${propertyName}' contains an invalid value: ${serviceError.toString()}`); + } +} + +export function validateValueAsOptionalObject(value : any, elementValidator : ElementValidator, propertyName: string,) { + if (value === undefined) { + return; + } + + validateValueAsObject(value, elementValidator, propertyName); +} + From 915e595890243ff1468e9740105875e048a2920a Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Tue, 25 Jun 2024 16:48:18 -0700 Subject: [PATCH 09/79] Create/destroy tests --- lib/iotjobs/iotjobsclientv2.spec.ts | 161 ++++++++++++++++++++++++++++ test/native/jest.config.js | 1 + 2 files changed, 162 insertions(+) create mode 100644 lib/iotjobs/iotjobsclientv2.spec.ts diff --git a/lib/iotjobs/iotjobsclientv2.spec.ts b/lib/iotjobs/iotjobsclientv2.spec.ts new file mode 100644 index 00000000..e9999057 --- /dev/null +++ b/lib/iotjobs/iotjobsclientv2.spec.ts @@ -0,0 +1,161 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + + +import {iot, mqtt5, mqtt as mqtt311, mqtt_request_response} from "aws-crt"; +import {v4 as uuid} from "uuid"; +import {once} from "events"; +import {IotJobsClientv2} from "./iotjobsclientv2"; +//import * as model from "./model"; + +jest.setTimeout(1000000); + +function hasTestEnvironment() : boolean { + if (process.env.AWS_TEST_MQTT5_IOT_CORE_HOST === undefined) { + return false; + } + + if (process.env.AWS_TEST_MQTT5_IOT_CORE_RSA_CERT === undefined) { + return false; + } + + if (process.env.AWS_TEST_MQTT5_IOT_CORE_RSA_KEY === undefined) { + return false; + } + + return true; +} + +const conditional_test = (condition : boolean) => condition ? it : it.skip; + +function build_protocol_client_mqtt5() : mqtt5.Mqtt5Client { + let builder = iot.AwsIotMqtt5ClientConfigBuilder.newDirectMqttBuilderWithMtlsFromPath( + // @ts-ignore + process.env.AWS_TEST_MQTT5_IOT_CORE_HOST, + process.env.AWS_TEST_MQTT5_IOT_CORE_RSA_CERT, + process.env.AWS_TEST_MQTT5_IOT_CORE_RSA_KEY + ); + + builder.withConnectProperties({ + clientId : uuid(), + keepAliveIntervalSeconds: 1200, + }); + + return new mqtt5.Mqtt5Client(builder.build()); +} + +function build_protocol_client_mqtt311() : mqtt311.MqttClientConnection { + // @ts-ignore + let builder = iot.AwsIotMqttConnectionConfigBuilder.new_mtls_builder_from_path(process.env.AWS_TEST_MQTT5_IOT_CORE_RSA_CERT, process.env.AWS_TEST_MQTT5_IOT_CORE_RSA_KEY); + // @ts-ignore + builder.with_endpoint(process.env.AWS_TEST_MQTT5_IOT_CORE_HOST); + builder.with_client_id(uuid()); + + let client = new mqtt311.MqttClient(); + return client.new_connection(builder.build()); +} + +enum ProtocolVersion { + Mqtt311, + Mqtt5 +} + +interface TestingOptions { + version: ProtocolVersion, + timeoutSeconds?: number, +} + +class JobsTestingContext { + + mqtt311Client?: mqtt311.MqttClientConnection; + mqtt5Client?: mqtt5.Mqtt5Client; + + client: IotJobsClientv2; + + private protocolStarted : boolean = false; + + async startProtocolClient() { + if (!this.protocolStarted) { + this.protocolStarted = true; + if (this.mqtt5Client) { + let connected = once(this.mqtt5Client, mqtt5.Mqtt5Client.CONNECTION_SUCCESS); + this.mqtt5Client.start(); + + await connected; + } + + if (this.mqtt311Client) { + await this.mqtt311Client.connect(); + } + } + } + + async stopProtocolClient() { + if (this.protocolStarted) { + this.protocolStarted = false; + if (this.mqtt5Client) { + let stopped = once(this.mqtt5Client, mqtt5.Mqtt5Client.STOPPED); + this.mqtt5Client.stop(); + await stopped; + + this.mqtt5Client.close(); + } + + if (this.mqtt311Client) { + await this.mqtt311Client.disconnect(); + } + } + } + + constructor(options: TestingOptions) { + if (options.version == ProtocolVersion.Mqtt5) { + this.mqtt5Client = build_protocol_client_mqtt5(); + + let rrOptions : mqtt_request_response.RequestResponseClientOptions = { + maxRequestResponseSubscriptions : 6, + maxStreamingSubscriptions : 2, + operationTimeoutInSeconds : options.timeoutSeconds ?? 60, + } + + this.client = IotJobsClientv2.newFromMqtt5(this.mqtt5Client, rrOptions); + } else { + this.mqtt311Client = build_protocol_client_mqtt311(); + + let rrOptions : mqtt_request_response.RequestResponseClientOptions = { + maxRequestResponseSubscriptions : 6, + maxStreamingSubscriptions : 2, + operationTimeoutInSeconds : options.timeoutSeconds ?? 60, + } + + this.client = IotJobsClientv2.newFromMqtt311(this.mqtt311Client, rrOptions); + } + } + + async open() { + await this.startProtocolClient(); + } + + async close() { + this.client.close(); + await this.stopProtocolClient(); + } +} + +async function doCreateDestroyTest(version: ProtocolVersion) { + let context = new JobsTestingContext({ + version: version + }); + await context.open(); + + await context.close(); +} + +conditional_test(hasTestEnvironment())('jobsv2 - create destroy mqtt5', async () => { + await doCreateDestroyTest(ProtocolVersion.Mqtt5); +}); + +conditional_test(hasTestEnvironment())('jobsv2 - create destroy mqtt311', async () => { + await doCreateDestroyTest(ProtocolVersion.Mqtt311); +}); \ No newline at end of file diff --git a/test/native/jest.config.js b/test/native/jest.config.js index 7c2cb1b1..42525629 100644 --- a/test/native/jest.config.js +++ b/test/native/jest.config.js @@ -5,6 +5,7 @@ module.exports = { testMatch: [ '/lib/*.spec.ts', '/lib/echo_rpc/*.spec.ts', + '/lib/iotjobs/*.spec.ts', '/lib/iotshadow/*.spec.ts' ], preset: 'ts-jest', From 6de1fe9125e33bfb304c3e22cbfab974d5c492ca Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 27 Jun 2024 08:39:18 -0700 Subject: [PATCH 10/79] Test checkpoint --- lib/iotjobs/iotjobsclientv2.spec.ts | 193 ++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) diff --git a/lib/iotjobs/iotjobsclientv2.spec.ts b/lib/iotjobs/iotjobsclientv2.spec.ts index e9999057..9b2bd86a 100644 --- a/lib/iotjobs/iotjobsclientv2.spec.ts +++ b/lib/iotjobs/iotjobsclientv2.spec.ts @@ -158,4 +158,197 @@ conditional_test(hasTestEnvironment())('jobsv2 - create destroy mqtt5', async () conditional_test(hasTestEnvironment())('jobsv2 - create destroy mqtt311', async () => { await doCreateDestroyTest(ProtocolVersion.Mqtt311); +}); + +interface TestResources { + thingGroupName?: string, + thingGroupArn?: string, + thingName?: string, + + jobId1?: string, + jobId2?: string, +} + +import { + AddThingToThingGroupCommand, + CreateJobCommand, + CreateThingCommand, + CreateThingGroupCommand, + DeleteJobCommand, + DeleteThingCommand, + DeleteThingGroupCommand, + IoTClient +} from "@aws-sdk/client-iot"; +import * as model from "./model"; + +//@ts-ignore +let jobResources : TestResources = {}; + +async function createJob(client : IoTClient, index: number) : Promise { + let jobId = 'jobid-' + uuid(); + let jobDocument = { + test: `do-something${index}` + }; + + const createJobCommand = new CreateJobCommand({ + jobId: jobId, + targets: [ jobResources.thingGroupArn ?? "" ], + document: JSON.stringify(jobDocument), + targetSelection: "CONTINUOUS" + }); + + await client.send(createJobCommand); + + return jobId; +} + +async function deleteJob(client: IoTClient, jobId: string | undefined) : Promise { + if (jobId) { + const command = new DeleteJobCommand({ + jobId: jobId, + force: true + }); + + await client.send(command); + } +} + +beforeAll(async () => { + const client = new IoTClient({}); + + let thingGroupName = 'tgn-' + uuid(); + + const createThingGroupCommand = new CreateThingGroupCommand({ + thingGroupName: thingGroupName + }); + + const createThingGroupResponse = await client.send(createThingGroupCommand); + jobResources.thingGroupName = thingGroupName; + jobResources.thingGroupArn = createThingGroupResponse.thingGroupArn; + + let thingName = 't-' + uuid(); + const createThingCommand = new CreateThingCommand({ + thingName: thingName + }); + + await client.send(createThingCommand); + jobResources.thingName = thingName; + + await new Promise(r => setTimeout(r, 1000)); + + jobResources.jobId1 = await createJob(client, 1); + + await new Promise(r => setTimeout(r, 1000)); +}); + +afterAll(async () => { + const client = new IoTClient({}); + + await new Promise(r => setTimeout(r, 1000)); + + await deleteJob(client, jobResources.jobId1); + await deleteJob(client, jobResources.jobId2); + + await new Promise(r => setTimeout(r, 1000)); + + if (jobResources.thingName) { + const command = new DeleteThingCommand({ + thingName: jobResources.thingName + }); + + await client.send(command); + + await new Promise(r => setTimeout(r, 1000)); + } + + if (jobResources.thingGroupName) { + const command = new DeleteThingGroupCommand({ + thingGroupName: jobResources.thingGroupName + }); + + await client.send(command); + } +}); + +async function verifyNoJobExecutions(context: JobsTestingContext) { + let response = await context.client.getPendingJobExecutions({ + thingName: jobResources.thingName ?? "" + }); + // @ts-ignore + expect(response.inProgressJobs.length).toEqual(0); + // @ts-ignore + expect(response.queuedJobs.length).toEqual(0); +} + +async function attachThingToThingGroup(client: IoTClient) { + + const addThingToThingGroupCommand = new AddThingToThingGroupCommand({ + thingName: jobResources.thingName, + thingGroupName: jobResources.thingGroupName + }); + + await client.send(addThingToThingGroupCommand); +} + +async function doProcessingTest(version: ProtocolVersion) { + const client = new IoTClient({}); + + let context = new JobsTestingContext({ + version: version + }); + await context.open(); + + let jobExecutionChangedStream = context.client.createJobExecutionsChangedStream({ + thingName: jobResources.thingName ?? "" + }); + jobExecutionChangedStream.on('incomingPublish', (event) => { + console.log(JSON.stringify(event)); + }) + jobExecutionChangedStream.open(); + + let initialExecutionChangedWaiter = once(jobExecutionChangedStream, 'incomingPublish'); + + let nextJobExecutionChangedStream = context.client.createNextJobExecutionChangedStream({ + thingName: jobResources.thingName ?? "" + }); + nextJobExecutionChangedStream.on('incomingPublish', (event) => { + console.log(JSON.stringify(event)); + }) + nextJobExecutionChangedStream.open(); + + //let initialNextJobExecutionChangedWaiter = once(nextJobExecutionChangedStream, 'incomingPublish'); + + await verifyNoJobExecutions(context); + await attachThingToThingGroup(client); + + let initialJobExecutionChanged : model.JobExecutionsChangedEvent = (await initialExecutionChangedWaiter)[0].message; + // @ts-ignore + expect(initialJobExecutionChanged.jobs['QUEUED'].length).toEqual(1); + // @ts-ignore + expect(initialJobExecutionChanged.jobs['QUEUED'][0].jobId).toEqual(jobResources.jobId1); + + //jobResources.jobId2 = await createJob(client, 2); + + /* + let testResponse = await context.client.startNextPendingJobExecution({ + thingName: jobResources.thingName ?? "" + }); + console.log(JSON.stringify(testResponse)); +*/ + await new Promise(r => setTimeout(r, 10000)); + + let response = await context.client.getPendingJobExecutions({ + thingName: jobResources.thingName ?? "" + }); + console.log(JSON.stringify(response)); + + await context.close(); +} + +test('jobsv2 processing mqtt5', async () => { + await doProcessingTest(ProtocolVersion.Mqtt5); +}); + +conditional_test(hasTestEnvironment())('jobsv2 processing mqtt311', async () => { + await doProcessingTest(ProtocolVersion.Mqtt311); }); \ No newline at end of file From 0f2807e360bc3b5f98b5eaeb9188d142daa0ce1f Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 27 Jun 2024 16:10:40 -0700 Subject: [PATCH 11/79] New shadow sample --- lib/mqtt_request_response_utils.ts | 7 +- samples/node/deprecated/shadow/README.md | 138 ++++++ samples/node/deprecated/shadow/index.ts | 399 ++++++++++++++++ samples/node/deprecated/shadow/package.json | 27 ++ samples/node/deprecated/shadow/tsconfig.json | 62 +++ samples/node/shadow/index.ts | 460 +++++-------------- 6 files changed, 741 insertions(+), 352 deletions(-) create mode 100644 samples/node/deprecated/shadow/README.md create mode 100644 samples/node/deprecated/shadow/index.ts create mode 100644 samples/node/deprecated/shadow/package.json create mode 100644 samples/node/deprecated/shadow/tsconfig.json diff --git a/lib/mqtt_request_response_utils.ts b/lib/mqtt_request_response_utils.ts index 150534c8..cfeb5271 100644 --- a/lib/mqtt_request_response_utils.ts +++ b/lib/mqtt_request_response_utils.ts @@ -128,6 +128,7 @@ export async function doRequestResponse(options: RequestResponseOp let response = await options.client.submitRequest(requestOptions); let responseTopic = response.topic; + let wasSuccess = responseTopic.endsWith("accepted"); // May need to eventually model let responsePayload = response.payload; let deserializer = deserializerMap.get(responseTopic); @@ -137,7 +138,11 @@ export async function doRequestResponse(options: RequestResponseOp } let deserializedResponse = deserializer(responsePayload) as ResponseType; - resolve(deserializedResponse); + if (wasSuccess) { + resolve(deserializedResponse); + } else { + reject(createServiceError("Request failed", undefined, deserializedResponse)); + } } catch (err) { if (err instanceof ServiceError) { reject(err); diff --git a/samples/node/deprecated/shadow/README.md b/samples/node/deprecated/shadow/README.md new file mode 100644 index 00000000..ca06d8f1 --- /dev/null +++ b/samples/node/deprecated/shadow/README.md @@ -0,0 +1,138 @@ +# Node: Shadow + +[**Return to main sample list**](../../README.md) + +This sample uses the AWS IoT [Device Shadow](https://docs.aws.amazon.com/iot/latest/developerguide/iot-device-shadows.html) Service to keep a property in sync between device and server. Imagine a light whose color may be changed through an app, or set by a local user. + +Once connected, type a value in the terminal and press Enter to update the property's "reported" value. The sample also responds when the "desired" value changes on the server. To observe this, edit the Shadow document in the AWS Console and set a new "desired" value. + +On startup, the sample requests the shadow document to learn the property's initial state. The sample also subscribes to "delta" events from the server, which are sent when a property's "desired" value differs from its "reported" value. When the sample learns of a new desired value, that value is changed on the device and an update is sent to the server with the new "reported" value. + +Your IoT Core Thing's [Policy](https://docs.aws.amazon.com/iot/latest/developerguide/iot-policies.html) must provide privileges for this sample to connect, subscribe, publish, and receive. Below is a sample policy that can be used on your IoT Core Thing that will allow this sample to run as intended. + +
+Sample Policy +
+{
+  "Version": "2012-10-17",
+  "Statement": [
+    {
+      "Effect": "Allow",
+      "Action": [
+        "iot:Publish"
+      ],
+      "Resource": [
+        "arn:aws:iot:region:account:topic/$aws/things/thingname/shadow/get",
+        "arn:aws:iot:region:account:topic/$aws/things/thingname/shadow/update"
+      ]
+    },
+    {
+      "Effect": "Allow",
+      "Action": [
+        "iot:Receive"
+      ],
+      "Resource": [
+        "arn:aws:iot:region:account:topic/$aws/things/thingname/shadow/get/accepted",
+        "arn:aws:iot:region:account:topic/$aws/things/thingname/shadow/get/rejected",
+        "arn:aws:iot:region:account:topic/$aws/things/thingname/shadow/update/accepted",
+        "arn:aws:iot:region:account:topic/$aws/things/thingname/shadow/update/rejected",
+        "arn:aws:iot:region:account:topic/$aws/things/thingname/shadow/update/delta"
+      ]
+    },
+    {
+      "Effect": "Allow",
+      "Action": [
+        "iot:Subscribe"
+      ],
+      "Resource": [
+        "arn:aws:iot:region:account:topicfilter/$aws/things/thingname/shadow/get/accepted",
+        "arn:aws:iot:region:account:topicfilter/$aws/things/thingname/shadow/get/rejected",
+        "arn:aws:iot:region:account:topicfilter/$aws/things/thingname/shadow/update/accepted",
+        "arn:aws:iot:region:account:topicfilter/$aws/things/thingname/shadow/update/rejected",
+        "arn:aws:iot:region:account:topicfilter/$aws/things/thingname/shadow/update/delta"
+      ]
+    },
+    {
+      "Effect": "Allow",
+      "Action": "iot:Connect",
+      "Resource": "arn:aws:iot:region:account:client/test-*"
+    }
+  ]
+}
+
+ +Replace with the following with the data from your AWS account: +* ``: The AWS IoT Core region where you created your AWS IoT Core thing you wish to use with this sample. For example `us-east-1`. +* ``: Your AWS IoT Core account ID. This is the set of numbers in the top right next to your AWS account name when using the AWS IoT Core website. +* ``: The name of your AWS IoT Core thing you want the device connection to be associated with + +Note that in a real application, you may want to avoid the use of wildcards in your ClientID or use them selectively. Please follow best practices when working with AWS on production applications using the SDK. Also, for the purposes of this sample, please make sure your policy allows a client ID of `test-*` to connect or use `--client_id ` to send the client ID your policy supports. + +
+ +## How to run + +To run the Shadow sample, go to the `node/shadow` folder and run the following commands: + +``` sh +npm install +node dist/index.js --endpoint --cert --key --thing_name --shadow_property +``` + +You can also pass `--mqtt5` to run the sample with Mqtt5 Client +```sh +npm install +node dist/index.js --endpoint --cert --key --thing_name --shadow_property --mqtt5 +``` + +You can also pass a Certificate Authority file (CA) if your certificate and key combination requires it: + +``` sh +npm install +node dist/index.js --endpoint --cert --key --thing_name --shadow_property --ca_file +``` + + +## Service Client Notes +### Differences between MQTT5 and MQTT311 +The service client with Mqtt5 client is almost identical to Mqtt3 one. The only difference is that you would need setup up a Mqtt5 Client and pass it to the service client. +For how to setup a Mqtt5 Client, please refer to [MQTT5 User Guide](https://github.com/awslabs/aws-crt-nodejs/blob/main/MQTT5-UserGuide.md) and [MQTT5 PubSub Sample](../pub_sub_mqtt5/README.md) + + + + + + + + + + +
Create a IoTShadowClient with Mqtt5Create a IoTShadowClient with Mqtt311
+ +```js + // Create a Mqtt5 Client + config_builder = iot.AwsIotMqtt5ClientConfigBuilder.newDirectMqttBuilderWithMtlsFromPath(argv.endpoint, argv.cert, argv.key); + client = new mqtt5.Mqtt5Client(config_builder.build()); + + // Create the shadow client from Mqtt5 Client + shadow = iotshadow.IotShadowClient.newFromMqtt5Client(client5); +``` + + + +```js + // Create a Mqtt311 Connection from the command line data + config_builder = iot.AwsIotMqttConnectionConfigBuilder.new_mtls_builder_from_path(argv.cert, argv.key); + config_builder.with_client_id(argv.client_id || "test-" + Math.floor(Math.random() * 100000000)); + config_builder.with_endpoint(argv.endpoint); + client = new mqtt.MqttClient(); + connection = client.new_connection(config); + + // Create the shadow client from Mqtt311 Connection + shadow = new iotshadow.IotShadowClient(connection); +``` + +
+ +### mqtt5.QoS v.s. mqtt.QoS +As the service client interface is unchanged for both Mqtt3 Connection and Mqtt5 Client,the service client will use mqtt.QoS instead of mqtt5.QoS even with a Mqtt5 Client. diff --git a/samples/node/deprecated/shadow/index.ts b/samples/node/deprecated/shadow/index.ts new file mode 100644 index 00000000..d2b316fa --- /dev/null +++ b/samples/node/deprecated/shadow/index.ts @@ -0,0 +1,399 @@ +import { mqtt, iotshadow } from 'aws-iot-device-sdk-v2'; +import { stringify } from 'querystring'; +import readline from 'readline'; +import {once} from "events"; + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); +const prompt = (query: string) => new Promise((resolve) => rl.question(query, resolve)); +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +type Args = { [index: string]: any }; +const yargs = require('yargs'); + +// The relative path is '../../util/cli_args' from here, but the compiled javascript file gets put one level +// deeper inside the 'dist' folder +const common_args = require('../../../../util/cli_args'); + +var shadow_value: unknown; +var shadow_property: string; + +var shadow_update_complete = false; + +yargs.command('*', false, (yargs: any) => { + common_args.add_direct_connection_establishment_arguments(yargs); + common_args.add_shadow_arguments(yargs); +}, main).parse(); + +async function sub_to_shadow_update(shadow: iotshadow.IotShadowClient, argv: Args) { + return new Promise(async (resolve, reject) => { + try { + function updateAccepted(error?: iotshadow.IotShadowError, response?: iotshadow.model.UpdateShadowResponse) { + if (response) { + + if (response.clientToken !== undefined) { + console.log("Succcessfully updated shadow for clientToken: " + response.clientToken + "."); + } + else { + console.log("Succcessfully updated shadow."); + } + if (response.state?.desired !== undefined) { + console.log("\t desired state: " + stringify(response.state.desired)); + } + if (response.state?.reported !== undefined) { + console.log("\t reported state: " + stringify(response.state.reported)); + } + } + + if (error || !response) { + console.log("Updated shadow is missing the target property."); + } + resolve(true); + } + + function updateRejected(error?: iotshadow.IotShadowError, response?: iotshadow.model.ErrorResponse) { + if (response) { + console.log("Update request was rejected."); + } + + if (error) { + console.log("Error occurred..") + } + reject(error); + } + + console.log("Subscribing to Update events.."); + const updateShadowSubRequest: iotshadow.model.UpdateNamedShadowSubscriptionRequest = { + shadowName: argv.shadow_property, + thingName: argv.thing_name + }; + + await shadow.subscribeToUpdateShadowAccepted( + updateShadowSubRequest, + mqtt.QoS.AtLeastOnce, + (error, response) => updateAccepted(error, response)); + + await shadow.subscribeToUpdateShadowRejected( + updateShadowSubRequest, + mqtt.QoS.AtLeastOnce, + (error, response) => updateRejected(error, response)); + + resolve(true); + } + catch (error) { + reject(error); + } + }); +} + +async function sub_to_shadow_get(shadow: iotshadow.IotShadowClient, argv: Args) { + return new Promise(async (resolve, reject) => { + try { + function getAccepted(error?: iotshadow.IotShadowError, response?: iotshadow.model.GetShadowResponse) { + + if (response?.state) { + if (response?.state.delta) { + const value = response.state.delta; + if (value) { + console.log("Shadow contains delta value '" + stringify(value) + "'."); + change_shadow_value(shadow, argv, value); + } + } + + if (response?.state.reported) { + const value_any: any = response.state.reported; + if (value_any) { + let found_property = false; + for (var prop in value_any) { + if (prop === shadow_property) { + found_property = true; + console.log("Shadow contains '" + prop + "'. Reported value: '" + String(value_any[prop]) + "'."); + break; + } + } + if (found_property === false) { + console.log("Shadow does not contain '" + shadow_property + "' property."); + } + } + } + } + + if (error || !response) { + console.log("Error occurred.."); + } + shadow_update_complete = true; + resolve(true); + } + + function getRejected(error?: iotshadow.IotShadowError, response?: iotshadow.model.ErrorResponse) { + + if (response) { + console.log("In getRejected response."); + } + + if (error) { + console.log("Error occurred.."); + } + + shadow_update_complete = true; + reject(error); + } + + console.log("Subscribing to Get events.."); + const getShadowSubRequest: iotshadow.model.GetShadowSubscriptionRequest = { + thingName: argv.thing_name + }; + + await shadow.subscribeToGetShadowAccepted( + getShadowSubRequest, + mqtt.QoS.AtLeastOnce, + (error, response) => getAccepted(error, response)); + + await shadow.subscribeToGetShadowRejected( + getShadowSubRequest, + mqtt.QoS.AtLeastOnce, + (error, response) => getRejected(error, response)); + + resolve(true); + } + catch (error) { + reject(error); + } + }); +} + +async function sub_to_shadow_delta(shadow: iotshadow.IotShadowClient, argv: Args) { + return new Promise(async (resolve, reject) => { + try { + function deltaEvent(error?: iotshadow.IotShadowError, response?: iotshadow.model.GetShadowResponse) { + console.log("\nReceived shadow delta event."); + + if (response?.clientToken != null) { + console.log(" ClientToken: " + response.clientToken); + } + + if (response?.state !== null) { + let value_any: any = response?.state; + if (value_any === null || value_any === undefined) { + console.log("Delta reports that '" + shadow_property + "' was deleted. Resetting defaults.."); + let data_to_send: any = {}; + data_to_send[shadow_property] = argv.shadow_value; + change_shadow_value(shadow, argv, data_to_send); + } + else { + if (value_any[shadow_property] !== undefined) { + if (value_any[shadow_property] !== shadow_value) { + console.log("Delta reports that desired value is '" + value_any[shadow_property] + "'. Changing local value.."); + let data_to_send: any = {}; + data_to_send[shadow_property] = value_any[shadow_property]; + change_shadow_value(shadow, argv, data_to_send); + } + else { + console.log("Delta did not report a change in '" + shadow_property + "'."); + } + } + else { + console.log("Desired value not found in delta. Skipping.."); + } + } + } + else { + console.log("Delta did not report a change in '" + shadow_property + "'."); + } + + resolve(true); + } + + console.log("Subscribing to Delta events.."); + const deltaShadowSubRequest: iotshadow.model.ShadowDeltaUpdatedSubscriptionRequest = { + thingName: argv.thing_name + }; + + await shadow.subscribeToShadowDeltaUpdatedEvents( + deltaShadowSubRequest, + mqtt.QoS.AtLeastOnce, + (error, response) => deltaEvent(error, response)); + + resolve(true); + } + catch (error) { + reject(error); + } + }); +} + +async function get_current_shadow(shadow: iotshadow.IotShadowClient, argv: Args) { + return new Promise(async (resolve, reject) => { + try { + const getShadow: iotshadow.model.GetShadowRequest = { + thingName: argv.thing_name + } + + shadow_update_complete = false; + console.log("Requesting current shadow state.."); + shadow.publishGetShadow( + getShadow, + mqtt.QoS.AtLeastOnce); + + await get_current_shadow_update_wait(); + resolve(true); + } + catch (error) { + reject(error); + } + }); +} + + +async function get_current_shadow_update_wait() { + // Wait until shadow_update_complete is true, showing the result returned + return await new Promise(resolve => { + const interval = setInterval(() => { + if (shadow_update_complete == true) { + resolve(true); + clearInterval(interval); + }; + }, 200); + }); +} + +function change_shadow_value(shadow: iotshadow.IotShadowClient, argv: Args, new_value?: object) { + return new Promise(async (resolve, reject) => { + try { + if (typeof new_value !== 'undefined') { + let new_value_any: any = new_value; + let skip_send = false; + + if (new_value_any !== null) { + if (new_value_any[shadow_property] == shadow_value) { + skip_send = true; + } + } + if (skip_send == false) { + if (new_value_any === null) { + shadow_value = new_value_any; + } + else { + shadow_value = new_value_any[shadow_property]; + } + + console.log("Changed local shadow value to '" + shadow_value + "'."); + + var updateShadow: iotshadow.model.UpdateShadowRequest = { + state: { + desired: new_value, + reported: new_value + }, + thingName: argv.thing_name + }; + + await shadow.publishUpdateShadow( + updateShadow, + mqtt.QoS.AtLeastOnce) + + console.log("Update request published."); + } + } + } + catch (error) { + console.log("Failed to publish update request.") + reject(error); + } + resolve(true) + }); +} + +async function main(argv: Args) { + common_args.apply_sample_arguments(argv); + + shadow_property = argv.shadow_property; + + var connection; + var client5; + var shadow; + + console.log("Connecting..."); + if (argv.mqtt5) { // Build the mqtt5 client + client5 = common_args.build_mqtt5_client_from_cli_args(argv); + shadow = iotshadow.IotShadowClient.newFromMqtt5Client(client5); + + const connectionSuccess = once(client5, "connectionSuccess"); + client5.start(); + await connectionSuccess; + console.log("Connected with Mqtt5 Client..."); + } else { // Build the mqtt3 based connection + connection = common_args.build_connection_from_cli_args(argv); + shadow = new iotshadow.IotShadowClient(connection); + + await connection.connect(); + console.log("Connected with Mqtt3 Client..."); + } + + try { + await sub_to_shadow_update(shadow, argv); + await sub_to_shadow_get(shadow, argv); + await sub_to_shadow_delta(shadow, argv); + await get_current_shadow(shadow, argv); + + await sleep(500); // wait half a second + + // Take console input when this sample is not running in CI + if (argv.is_ci == false) { + while (true) { + const userInput = await prompt("Enter desired value: "); + if (userInput === "quit") { + break; + } + else { + let data_to_send: any = {}; + + if (userInput == "clear_shadow") { + data_to_send = null; + } + else if (userInput == "null") { + data_to_send[shadow_property] = null; + } + else { + data_to_send[shadow_property] = userInput; + } + + await change_shadow_value(shadow, argv, data_to_send); + await get_current_shadow(shadow, argv); + } + } + } + // If this is running in CI, then automatically update the shadow + else { + var messages_sent = 0; + while (messages_sent < 5) { + let data_to_send: any = {} + data_to_send[shadow_property] = "Shadow_Value_" + messages_sent.toString() + await change_shadow_value(shadow, argv, data_to_send); + await get_current_shadow(shadow, argv); + messages_sent += 1 + } + } + + } catch (error) { + console.log(error); + } + + console.log("Disconnecting.."); + + if (connection) { + await connection.disconnect(); + } else { + let stopped = once(client5, "stopped"); + client5.stop(); + await stopped; + client5.close(); + } + + // force node to wait a second before quitting to finish any promises + await sleep(1000); + console.log("Disconnected"); + // Quit NodeJS + process.exit(0); +} diff --git a/samples/node/deprecated/shadow/package.json b/samples/node/deprecated/shadow/package.json new file mode 100644 index 00000000..085c0446 --- /dev/null +++ b/samples/node/deprecated/shadow/package.json @@ -0,0 +1,27 @@ +{ + "name": "shadow", + "version": "1.0.0", + "description": "NodeJS IoT SDK v2 Shadow Sample", + "homepage": "https://github.com/aws/aws-iot-device-sdk-js-v2", + "repository": { + "type": "git", + "url": "git+https://github.com/aws/aws-iot-device-sdk-js-v2.git" + }, + "contributors": [ + "AWS SDK Common Runtime Team " + ], + "license": "Apache-2.0", + "main": "./dist/index.js", + "scripts": { + "tsc": "tsc", + "prepare": "npm run tsc" + }, + "devDependencies": { + "@types/node": "^10.17.50", + "typescript": "^4.7.4" + }, + "dependencies": { + "aws-iot-device-sdk-v2": "../../../", + "yargs": "^16.2.0" + } +} diff --git a/samples/node/deprecated/shadow/tsconfig.json b/samples/node/deprecated/shadow/tsconfig.json new file mode 100644 index 00000000..92617173 --- /dev/null +++ b/samples/node/deprecated/shadow/tsconfig.json @@ -0,0 +1,62 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./dist", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "removeComments": false, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, /* Enable strict null checks. */ + "strictFunctionTypes": true, /* Enable strict checking of function types. */ + "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + /* Additional Checks */ + "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + }, + "include": [ + "*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/samples/node/shadow/index.ts b/samples/node/shadow/index.ts index e5d941b2..99f0562f 100644 --- a/samples/node/shadow/index.ts +++ b/samples/node/shadow/index.ts @@ -1,14 +1,11 @@ -import { mqtt, iotshadow } from 'aws-iot-device-sdk-v2'; -import { stringify } from 'querystring'; +import { iotshadow } from 'aws-iot-device-sdk-v2'; import readline from 'readline'; import {once} from "events"; -const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout -}); -const prompt = (query: string) => new Promise((resolve) => rl.question(query, resolve)); -const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); +interface SampleContext { + thingName: string, + client: iotshadow.IotShadowClientv2 +} type Args = { [index: string]: any }; const yargs = require('yargs'); @@ -17,383 +14,144 @@ const yargs = require('yargs'); // deeper inside the 'dist' folder const common_args = require('../../../util/cli_args'); -var shadow_value: unknown; -var shadow_property: string; - -var shadow_update_complete = false; - yargs.command('*', false, (yargs: any) => { common_args.add_direct_connection_establishment_arguments(yargs); common_args.add_shadow_arguments(yargs); }, main).parse(); -async function sub_to_shadow_update(shadow: iotshadow.IotShadowClient, argv: Args) { - return new Promise(async (resolve, reject) => { - try { - function updateAccepted(error?: iotshadow.IotShadowError, response?: iotshadow.model.UpdateShadowResponse) { - if (response) { - - if (response.clientToken !== undefined) { - console.log("Succcessfully updated shadow for clientToken: " + response.clientToken + "."); - } - else { - console.log("Succcessfully updated shadow."); - } - if (response.state?.desired !== undefined) { - console.log("\t desired state: " + stringify(response.state.desired)); - } - if (response.state?.reported !== undefined) { - console.log("\t reported state: " + stringify(response.state.reported)); - } - } - - if (error || !response) { - console.log("Updated shadow is missing the target property."); - } - resolve(true); - } - - function updateRejected(error?: iotshadow.IotShadowError, response?: iotshadow.model.ErrorResponse) { - if (response) { - console.log("Update request was rejected."); - } - - if (error) { - console.log("Error occurred..") - } - reject(error); - } - - console.log("Subscribing to Update events.."); - const updateShadowSubRequest: iotshadow.model.UpdateNamedShadowSubscriptionRequest = { - shadowName: argv.shadow_property, - thingName: argv.thing_name - }; - - await shadow.subscribeToUpdateShadowAccepted( - updateShadowSubRequest, - mqtt.QoS.AtLeastOnce, - (error, response) => updateAccepted(error, response)); - - await shadow.subscribeToUpdateShadowRejected( - updateShadowSubRequest, - mqtt.QoS.AtLeastOnce, - (error, response) => updateRejected(error, response)); - - resolve(true); - } - catch (error) { - reject(error); - } - }); +function printHelp() { + console.log('Supported commands:'); + console.log(" get - gets the current value of the IoT thing's shadow"); + console.log(" delete - deletes the IoT thing's shadow"); + console.log(" update-desired - updates the desired state of the IoT thing's shadow. If the shadow does not exist, it will be created."); + console.log(" update-reported - updates the reported state of the IoT thing's shadow. If the shadow does not exist, it will be created."); + console.log(" quit - quits the sample application\n"); } -async function sub_to_shadow_get(shadow: iotshadow.IotShadowClient, argv: Args) { - return new Promise(async (resolve, reject) => { - try { - function getAccepted(error?: iotshadow.IotShadowError, response?: iotshadow.model.GetShadowResponse) { - - if (response?.state) { - if (response?.state.delta) { - const value = response.state.delta; - if (value) { - console.log("Shadow contains delta value '" + stringify(value) + "'."); - change_shadow_value(shadow, argv, value); - } +async function handleCommand(context: SampleContext, input: string) : Promise { + try { + let command = input.split(' ')[0]; + let remaining = input.substring(command.length); + console.log(""); + + switch (command) { + case "get": + let getResponse = await context.client.getShadow({ + thingName: context.thingName + }); + console.log(`Get response: ${JSON.stringify(getResponse)}\n`); + break; + + case "delete": + let deleteResponse = await context.client.deleteShadow({ + thingName: context.thingName + }); + console.log(`\nDelete response: ${JSON.stringify(deleteResponse)}\n`); + break; + + case "update-desired": + let updateDesiredResponse = await context.client.updateShadow({ + thingName: context.thingName, + state: { + desired: JSON.parse(remaining) } - - if (response?.state.reported) { - const value_any: any = response.state.reported; - if (value_any) { - let found_property = false; - for (var prop in value_any) { - if (prop === shadow_property) { - found_property = true; - console.log("Shadow contains '" + prop + "'. Reported value: '" + String(value_any[prop]) + "'."); - break; - } - } - if (found_property === false) { - console.log("Shadow does not contain '" + shadow_property + "' property."); - } - } + }); + console.log(`Update Desired response: ${JSON.stringify(updateDesiredResponse)}\n`); + break; + + case "update-reported": + let updateReportedResponse = await context.client.updateShadow({ + thingName: context.thingName, + state: { + reported: JSON.parse(remaining) } - } + }); + console.log(`Update Reported response: ${JSON.stringify(updateReportedResponse)}\n`); + break; - if (error || !response) { - console.log("Error occurred.."); - } - shadow_update_complete = true; - resolve(true); - } + case "quit": + return true; - function getRejected(error?: iotshadow.IotShadowError, response?: iotshadow.model.ErrorResponse) { + case "help": + printHelp(); + break; - if (response) { - console.log("In getRejected response."); - } - - if (error) { - console.log("Error occurred.."); - } - - shadow_update_complete = true; - reject(error); - } - - console.log("Subscribing to Get events.."); - const getShadowSubRequest: iotshadow.model.GetShadowSubscriptionRequest = { - thingName: argv.thing_name - }; - - await shadow.subscribeToGetShadowAccepted( - getShadowSubRequest, - mqtt.QoS.AtLeastOnce, - (error, response) => getAccepted(error, response)); - - await shadow.subscribeToGetShadowRejected( - getShadowSubRequest, - mqtt.QoS.AtLeastOnce, - (error, response) => getRejected(error, response)); - - resolve(true); - } - catch (error) { - reject(error); + default: + console.log(`Unknown command: ${command}\n`); + printHelp(); + break; } - }); -} - -async function sub_to_shadow_delta(shadow: iotshadow.IotShadowClient, argv: Args) { - return new Promise(async (resolve, reject) => { - try { - function deltaEvent(error?: iotshadow.IotShadowError, response?: iotshadow.model.GetShadowResponse) { - console.log("\nReceived shadow delta event."); - - if (response?.clientToken != null) { - console.log(" ClientToken: " + response.clientToken); - } + } catch (error) { + console.log(`Error processing command: ${JSON.stringify(error)}\n`); + } - if (response?.state !== null) { - let value_any: any = response?.state; - if (value_any === null || value_any === undefined) { - console.log("Delta reports that '" + shadow_property + "' was deleted. Resetting defaults.."); - let data_to_send: any = {}; - data_to_send[shadow_property] = argv.shadow_value; - change_shadow_value(shadow, argv, data_to_send); - } - else { - if (value_any[shadow_property] !== undefined) { - if (value_any[shadow_property] !== shadow_value) { - console.log("Delta reports that desired value is '" + value_any[shadow_property] + "'. Changing local value.."); - let data_to_send: any = {}; - data_to_send[shadow_property] = value_any[shadow_property]; - change_shadow_value(shadow, argv, data_to_send); - } - else { - console.log("Delta did not report a change in '" + shadow_property + "'."); - } - } - else { - console.log("Desired value not found in delta. Skipping.."); - } - } - } - else { - console.log("Delta did not report a change in '" + shadow_property + "'."); - } + return false; +} - resolve(true); - } +async function main(argv: Args) { + common_args.apply_sample_arguments(argv); - console.log("Subscribing to Delta events.."); - const deltaShadowSubRequest: iotshadow.model.ShadowDeltaUpdatedSubscriptionRequest = { - thingName: argv.thing_name - }; + console.log("Connecting..."); - await shadow.subscribeToShadowDeltaUpdatedEvents( - deltaShadowSubRequest, - mqtt.QoS.AtLeastOnce, - (error, response) => deltaEvent(error, response)); + let protocolClient = common_args.build_mqtt5_client_from_cli_args(argv); + const connectionSuccess = once(protocolClient, "connectionSuccess"); + protocolClient.start(); + await connectionSuccess; + console.log("Connected!"); - resolve(true); - } - catch (error) { - reject(error); - } + let shadowClient = iotshadow.IotShadowClientv2.newFromMqtt5(protocolClient, { + maxRequestResponseSubscriptions: 5, + maxStreamingSubscriptions: 2, + operationTimeoutInSeconds: 60 }); -} - -async function get_current_shadow(shadow: iotshadow.IotShadowClient, argv: Args) { - return new Promise(async (resolve, reject) => { - try { - const getShadow: iotshadow.model.GetShadowRequest = { - thingName: argv.thing_name - } - shadow_update_complete = false; - console.log("Requesting current shadow state.."); - shadow.publishGetShadow( - getShadow, - mqtt.QoS.AtLeastOnce); + let context: SampleContext = { + thingName: argv.thing_name, + client: shadowClient + }; - await get_current_shadow_update_wait(); - resolve(true); - } - catch (error) { - reject(error); - } + // invoked when the shadow state changes + let shadowUpdatedStream = shadowClient.createShadowUpdatedStream({ + thingName: context.thingName }); -} - - -async function get_current_shadow_update_wait() { - // Wait until shadow_update_complete is true, showing the result returned - return await new Promise(resolve => { - const interval = setInterval(() => { - if (shadow_update_complete == true) { - resolve(true); - clearInterval(interval); - }; - }, 200); + shadowUpdatedStream.on('incomingPublish', (event) => { + console.log(`Received ShadowUpdated event: ${JSON.stringify(event.message)}\n`) + }) + shadowUpdatedStream.open(); + + // invoked when there's a change to the delta between reported and desired + let shadowDeltaUpdatedStream = shadowClient.createShadowDeltaUpdatedStream({ + thingName: context.thingName }); -} - -function change_shadow_value(shadow: iotshadow.IotShadowClient, argv: Args, new_value?: object) { - return new Promise(async (resolve, reject) => { - try { - if (typeof new_value !== 'undefined') { - let new_value_any: any = new_value; - let skip_send = false; - - if (new_value_any !== null) { - if (new_value_any[shadow_property] == shadow_value) { - skip_send = true; - } - } - if (skip_send == false) { - if (new_value_any === null) { - shadow_value = new_value_any; - } - else { - shadow_value = new_value_any[shadow_property]; - } - - console.log("Changed local shadow value to '" + shadow_value + "'."); - - var updateShadow: iotshadow.model.UpdateShadowRequest = { - state: { - desired: new_value, - reported: new_value - }, - thingName: argv.thing_name - }; - - await shadow.publishUpdateShadow( - updateShadow, - mqtt.QoS.AtLeastOnce) - - console.log("Update request published."); - } - } - } - catch (error) { - console.log("Failed to publish update request.") - reject(error); - } - resolve(true) + shadowDeltaUpdatedStream.on('incomingPublish', (event) => { + console.log(`Received ShadowDeltaUpdated event: ${JSON.stringify(event.message)}\n`) + }) + shadowDeltaUpdatedStream.open(); + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout }); -} - -async function main(argv: Args) { - common_args.apply_sample_arguments(argv); - - shadow_property = argv.shadow_property; - - var connection; - var client5; - var shadow; - - console.log("Connecting..."); - if (argv.mqtt5) { // Build the mqtt5 client - client5 = common_args.build_mqtt5_client_from_cli_args(argv); - shadow = iotshadow.IotShadowClient.newFromMqtt5Client(client5); - - const connectionSuccess = once(client5, "connectionSuccess"); - client5.start(); - await connectionSuccess; - console.log("Connected with Mqtt5 Client..."); - } else { // Build the mqtt3 based connection - connection = common_args.build_connection_from_cli_args(argv); - shadow = new iotshadow.IotShadowClient(connection); - - await connection.connect(); - console.log("Connected with Mqtt3 Client..."); - } try { - await sub_to_shadow_update(shadow, argv); - await sub_to_shadow_get(shadow, argv); - await sub_to_shadow_delta(shadow, argv); - await get_current_shadow(shadow, argv); - - await sleep(500); // wait half a second - - // Take console input when this sample is not running in CI - if (argv.is_ci == false) { - while (true) { - const userInput = await prompt("Enter desired value: "); - if (userInput === "quit") { - break; - } - else { - let data_to_send: any = {}; - - if (userInput == "clear_shadow") { - data_to_send = null; - } - else if (userInput == "null") { - data_to_send[shadow_property] = null; - } - else { - data_to_send[shadow_property] = userInput; - } - - await change_shadow_value(shadow, argv, data_to_send); - await get_current_shadow(shadow, argv); - } - } - } - // If this is running in CI, then automatically update the shadow - else { - var messages_sent = 0; - while (messages_sent < 5) { - let data_to_send: any = {} - data_to_send[shadow_property] = "Shadow_Value_" + messages_sent.toString() - await change_shadow_value(shadow, argv, data_to_send); - await get_current_shadow(shadow, argv); - messages_sent += 1 - } + let done = false; + while (!done) { + const userInput : string = await new Promise((resolve) => rl.question("Enter command:\n", resolve)); + done = await handleCommand(context, userInput.trimStart()); } - } catch (error) { console.log(error); } + shadowClient.close(); console.log("Disconnecting.."); - if (connection) { - await connection.disconnect(); - } else { - let stopped = once(client5, "stopped"); - client5.stop(); - await stopped; - client5.close(); - } + let stopped = once(protocolClient, "stopped"); + protocolClient.stop(); + await stopped; + protocolClient.close(); - // force node to wait a second before quitting to finish any promises - await sleep(1000); - console.log("Disconnected"); + console.log("Stopped"); // Quit NodeJS process.exit(0); } From 73aa7c93f572f3e0c614a6fb7a8d83376cce0c9e Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Fri, 28 Jun 2024 10:34:54 -0700 Subject: [PATCH 12/79] Job processing test and deprecated v1 samples --- .github/workflows/ci.yml | 12 +- .../ci_run_fleet_provisioning_cfg.json | 2 +- .../ci_run_fleet_provisioning_mqtt5_cfg.json | 2 +- .github/workflows/ci_run_jobs_cfg.json | 2 +- .github/workflows/ci_run_jobs_mqtt5_cfg.json | 2 +- .github/workflows/ci_run_shadow_cfg.json | 2 +- .../workflows/ci_run_shadow_mqtt5_cfg.json | 2 +- lib/iotjobs/iotjobsclientv2.spec.ts | 97 +++-- .../deprecated/fleet_provisioning/README.md | 366 ++++++++++++++++++ .../deprecated/fleet_provisioning/index.ts | 283 ++++++++++++++ .../fleet_provisioning/package.json | 27 ++ .../fleet_provisioning/tsconfig.json | 61 +++ samples/node/deprecated/jobs/README.md | 134 +++++++ samples/node/deprecated/jobs/index.ts | 286 ++++++++++++++ samples/node/deprecated/jobs/package.json | 27 ++ samples/node/deprecated/jobs/tsconfig.json | 62 +++ 16 files changed, 1321 insertions(+), 46 deletions(-) create mode 100644 samples/node/deprecated/fleet_provisioning/README.md create mode 100644 samples/node/deprecated/fleet_provisioning/index.ts create mode 100644 samples/node/deprecated/fleet_provisioning/package.json create mode 100644 samples/node/deprecated/fleet_provisioning/tsconfig.json create mode 100644 samples/node/deprecated/jobs/README.md create mode 100644 samples/node/deprecated/jobs/index.ts create mode 100644 samples/node/deprecated/jobs/package.json create mode 100644 samples/node/deprecated/jobs/tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dcc0f9c2..eadda7ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -334,10 +334,10 @@ jobs: with: role-to-assume: ${{ env.CI_SHADOW_ROLE }} aws-region: ${{ env.AWS_DEFAULT_REGION }} - - name: run Shadow sample + - name: run v1 Shadow sample run: | python3 ./aws-iot-device-sdk-js-v2/utils/run_in_ci.py --file ./aws-iot-device-sdk-js-v2/.github/workflows/ci_run_shadow_cfg.json - - name: run Mqtt5 Shadow sample + - name: run v1 Mqtt5 Shadow sample run: | python3 ./aws-iot-device-sdk-js-v2/utils/run_in_ci.py --file ./aws-iot-device-sdk-js-v2/.github/workflows/ci_run_shadow_mqtt5_cfg.json - name: configure AWS credentials (Jobs) @@ -345,10 +345,10 @@ jobs: with: role-to-assume: ${{ env.CI_JOBS_ROLE }} aws-region: ${{ env.AWS_DEFAULT_REGION }} - - name: run Jobs sample + - name: run v1 Jobs sample run: | python3 ./aws-iot-device-sdk-js-v2/utils/run_in_ci.py --file ./aws-iot-device-sdk-js-v2/.github/workflows/ci_run_jobs_cfg.json - - name: run Mqtt5 Jobs sample + - name: run v1 Mqtt5 Jobs sample run: | python3 ./aws-iot-device-sdk-js-v2/utils/run_in_ci.py --file ./aws-iot-device-sdk-js-v2/.github/workflows/ci_run_jobs_mqtt5_cfg.json - name: configure AWS credentials (Fleet provisioning) @@ -356,10 +356,10 @@ jobs: with: role-to-assume: ${{ env.CI_FLEET_PROVISIONING_ROLE }} aws-region: ${{ env.AWS_DEFAULT_REGION }} - - name: run Fleet Provisioning sample + - name: run v1 Fleet Provisioning sample run: | python3 ./aws-iot-device-sdk-js-v2/utils/run_fleet_provisioning_sample.py --file ./aws-iot-device-sdk-js-v2/.github/workflows/ci_run_fleet_provisioning_cfg.json --thing-name-prefix "Fleet_Thing_" - - name: run Mqtt5 Fleet Provisioning sample + - name: run v1 Mqtt5 Fleet Provisioning sample run: | python3 ./aws-iot-device-sdk-js-v2/utils/run_fleet_provisioning_sample.py --file ./aws-iot-device-sdk-js-v2/.github/workflows/ci_run_fleet_provisioning_mqtt5_cfg.json --thing-name-prefix "Fleet_Thing_" - name: configure AWS credentials (MQTT5 PubSub) diff --git a/.github/workflows/ci_run_fleet_provisioning_cfg.json b/.github/workflows/ci_run_fleet_provisioning_cfg.json index 89de917d..d4e2d143 100644 --- a/.github/workflows/ci_run_fleet_provisioning_cfg.json +++ b/.github/workflows/ci_run_fleet_provisioning_cfg.json @@ -1,6 +1,6 @@ { "language": "Javascript", - "runnable_file": "./aws-iot-device-sdk-js-v2/samples/node/fleet_provisioning", + "runnable_file": "./aws-iot-device-sdk-js-v2/samples/node/deprecated/fleet_provisioning", "runnable_region": "us-east-1", "runnable_main_class": "", "arguments": [ diff --git a/.github/workflows/ci_run_fleet_provisioning_mqtt5_cfg.json b/.github/workflows/ci_run_fleet_provisioning_mqtt5_cfg.json index 79965ad5..58a1e101 100644 --- a/.github/workflows/ci_run_fleet_provisioning_mqtt5_cfg.json +++ b/.github/workflows/ci_run_fleet_provisioning_mqtt5_cfg.json @@ -1,6 +1,6 @@ { "language": "Javascript", - "runnable_file": "./aws-iot-device-sdk-js-v2/samples/node/fleet_provisioning", + "runnable_file": "./aws-iot-device-sdk-js-v2/samples/node/deprecated/fleet_provisioning", "runnable_region": "us-east-1", "runnable_main_class": "", "arguments": [ diff --git a/.github/workflows/ci_run_jobs_cfg.json b/.github/workflows/ci_run_jobs_cfg.json index bd506248..2da64432 100644 --- a/.github/workflows/ci_run_jobs_cfg.json +++ b/.github/workflows/ci_run_jobs_cfg.json @@ -1,6 +1,6 @@ { "language": "Javascript", - "runnable_file": "./aws-iot-device-sdk-js-v2/samples/node/jobs", + "runnable_file": "./aws-iot-device-sdk-js-v2/samples/node/deprecated/jobs", "runnable_region": "us-east-1", "runnable_main_class": "", "arguments": [ diff --git a/.github/workflows/ci_run_jobs_mqtt5_cfg.json b/.github/workflows/ci_run_jobs_mqtt5_cfg.json index f0f10834..f585a72c 100644 --- a/.github/workflows/ci_run_jobs_mqtt5_cfg.json +++ b/.github/workflows/ci_run_jobs_mqtt5_cfg.json @@ -1,6 +1,6 @@ { "language": "Javascript", - "runnable_file": "./aws-iot-device-sdk-js-v2/samples/node/jobs", + "runnable_file": "./aws-iot-device-sdk-js-v2/samples/node/deprecated/jobs", "runnable_region": "us-east-1", "runnable_main_class": "", "arguments": [ diff --git a/.github/workflows/ci_run_shadow_cfg.json b/.github/workflows/ci_run_shadow_cfg.json index 51550638..25311f69 100644 --- a/.github/workflows/ci_run_shadow_cfg.json +++ b/.github/workflows/ci_run_shadow_cfg.json @@ -1,6 +1,6 @@ { "language": "Javascript", - "runnable_file": "./aws-iot-device-sdk-js-v2/samples/node/shadow", + "runnable_file": "./aws-iot-device-sdk-js-v2/samples/node/deprecated/shadow", "runnable_region": "us-east-1", "runnable_main_class": "", "arguments": [ diff --git a/.github/workflows/ci_run_shadow_mqtt5_cfg.json b/.github/workflows/ci_run_shadow_mqtt5_cfg.json index 78d1515a..e3bd93bc 100644 --- a/.github/workflows/ci_run_shadow_mqtt5_cfg.json +++ b/.github/workflows/ci_run_shadow_mqtt5_cfg.json @@ -1,6 +1,6 @@ { "language": "Javascript", - "runnable_file": "./aws-iot-device-sdk-js-v2/samples/node/shadow", + "runnable_file": "./aws-iot-device-sdk-js-v2/samples/node/deprecated/shadow", "runnable_region": "us-east-1", "runnable_main_class": "", "arguments": [ diff --git a/lib/iotjobs/iotjobsclientv2.spec.ts b/lib/iotjobs/iotjobsclientv2.spec.ts index 9b2bd86a..87fa967b 100644 --- a/lib/iotjobs/iotjobsclientv2.spec.ts +++ b/lib/iotjobs/iotjobsclientv2.spec.ts @@ -4,13 +4,23 @@ */ -import {iot, mqtt5, mqtt as mqtt311, mqtt_request_response} from "aws-crt"; +import {iot, mqtt as mqtt311, mqtt5, mqtt_request_response} from "aws-crt"; import {v4 as uuid} from "uuid"; import {once} from "events"; import {IotJobsClientv2} from "./iotjobsclientv2"; -//import * as model from "./model"; +import { + AddThingToThingGroupCommand, + CreateJobCommand, + CreateThingCommand, + CreateThingGroupCommand, + DeleteJobCommand, + DeleteThingCommand, + DeleteThingGroupCommand, + IoTClient +} from "@aws-sdk/client-iot"; +import * as model from "./model"; -jest.setTimeout(1000000); +jest.setTimeout(30000); function hasTestEnvironment() : boolean { if (process.env.AWS_TEST_MQTT5_IOT_CORE_HOST === undefined) { @@ -169,18 +179,6 @@ interface TestResources { jobId2?: string, } -import { - AddThingToThingGroupCommand, - CreateJobCommand, - CreateThingCommand, - CreateThingGroupCommand, - DeleteJobCommand, - DeleteThingCommand, - DeleteThingGroupCommand, - IoTClient -} from "@aws-sdk/client-iot"; -import * as model from "./model"; - //@ts-ignore let jobResources : TestResources = {}; @@ -213,7 +211,8 @@ async function deleteJob(client: IoTClient, jobId: string | undefined) : Promise } } -beforeAll(async () => { +beforeEach(async () => { + jobResources = {} const client = new IoTClient({}); let thingGroupName = 'tgn-' + uuid(); @@ -241,7 +240,7 @@ beforeAll(async () => { await new Promise(r => setTimeout(r, 1000)); }); -afterAll(async () => { +afterEach(async () => { const client = new IoTClient({}); await new Promise(r => setTimeout(r, 1000)); @@ -268,6 +267,8 @@ afterAll(async () => { await client.send(command); } + + jobResources = {} }); async function verifyNoJobExecutions(context: JobsTestingContext) { @@ -298,27 +299,25 @@ async function doProcessingTest(version: ProtocolVersion) { }); await context.open(); + // set up streaming operations for our test's thing let jobExecutionChangedStream = context.client.createJobExecutionsChangedStream({ thingName: jobResources.thingName ?? "" }); - jobExecutionChangedStream.on('incomingPublish', (event) => { - console.log(JSON.stringify(event)); - }) jobExecutionChangedStream.open(); - let initialExecutionChangedWaiter = once(jobExecutionChangedStream, 'incomingPublish'); - let nextJobExecutionChangedStream = context.client.createNextJobExecutionChangedStream({ thingName: jobResources.thingName ?? "" }); - nextJobExecutionChangedStream.on('incomingPublish', (event) => { - console.log(JSON.stringify(event)); - }) nextJobExecutionChangedStream.open(); - //let initialNextJobExecutionChangedWaiter = once(nextJobExecutionChangedStream, 'incomingPublish'); + let initialExecutionChangedWaiter = once(jobExecutionChangedStream, 'incomingPublish'); + let initialNextJobExecutionChangedWaiter = once(nextJobExecutionChangedStream, 'incomingPublish'); + // thing is brand new, nothing should be pending await verifyNoJobExecutions(context); + + // as soon as we attach the thing to the thing group which has a continuous job associated with it, a + // job execution should become queued for our thing await attachThingToThingGroup(client); let initialJobExecutionChanged : model.JobExecutionsChangedEvent = (await initialExecutionChangedWaiter)[0].message; @@ -327,20 +326,50 @@ async function doProcessingTest(version: ProtocolVersion) { // @ts-ignore expect(initialJobExecutionChanged.jobs['QUEUED'][0].jobId).toEqual(jobResources.jobId1); - //jobResources.jobId2 = await createJob(client, 2); + let initialNextJobExecutionChanged : model.NextJobExecutionChangedEvent = (await initialNextJobExecutionChangedWaiter)[0].message; + expect(initialNextJobExecutionChanged.execution?.jobId).toEqual(jobResources.jobId1); + expect(initialNextJobExecutionChanged.execution?.status).toEqual(model.JobStatus.QUEUED); + + let finalExecutionChangedWaiter = once(jobExecutionChangedStream, 'incomingPublish'); + let finalNextJobExecutionChangedWaiter = once(nextJobExecutionChangedStream, 'incomingPublish'); - /* - let testResponse = await context.client.startNextPendingJobExecution({ + // tell the service we'll run the next job + let startNextResponse = await context.client.startNextPendingJobExecution({ thingName: jobResources.thingName ?? "" }); - console.log(JSON.stringify(testResponse)); -*/ - await new Promise(r => setTimeout(r, 10000)); + expect(startNextResponse.execution?.jobId).toEqual(jobResources.jobId1); - let response = await context.client.getPendingJobExecutions({ + // pretend to do the job + await new Promise(r => setTimeout(r, 1000)); + + // job execution should be in progress + let describeResponse = await context.client.describeJobExecution({ + thingName: jobResources.thingName ?? "", + jobId: jobResources.jobId1 ?? "", + }); + expect(describeResponse.execution?.jobId).toEqual(jobResources.jobId1); + expect(describeResponse.execution?.status).toEqual(model.JobStatus.IN_PROGRESS); + + // tell the service we completed the job successfully + await context.client.updateJobExecution({ + thingName: jobResources.thingName ?? "", + jobId: jobResources.jobId1 ?? "", + status: model.JobStatus.SUCCEEDED + }); + + await new Promise(r => setTimeout(r, 3000)); + + let finalJobExecutionChanged : model.JobExecutionsChangedEvent = (await finalExecutionChangedWaiter)[0].message; + expect(finalJobExecutionChanged.jobs).toEqual({}); + + let finalNextJobExecutionChanged : model.NextJobExecutionChangedEvent = (await finalNextJobExecutionChangedWaiter)[0].message; + expect(finalNextJobExecutionChanged.timestamp).toBeDefined(); + + let getPendingResponse = await context.client.getPendingJobExecutions({ thingName: jobResources.thingName ?? "" }); - console.log(JSON.stringify(response)); + expect(getPendingResponse.queuedJobs?.length).toEqual(0); + expect(getPendingResponse.inProgressJobs?.length).toEqual(0); await context.close(); } diff --git a/samples/node/deprecated/fleet_provisioning/README.md b/samples/node/deprecated/fleet_provisioning/README.md new file mode 100644 index 00000000..1372569f --- /dev/null +++ b/samples/node/deprecated/fleet_provisioning/README.md @@ -0,0 +1,366 @@ +# Node: Fleet provisioning + +[**Return to main sample list**](../../README.md) + +This sample uses the AWS IoT [Fleet provisioning](https://docs.aws.amazon.com/iot/latest/developerguide/provision-wo-cert.html) to provision devices using either a CSR or Keys-And-Certificate and subsequently calls RegisterThing. This allows you to create new AWS IoT Core things using a Fleet Provisioning Template. + +On startup, the script subscribes to topics based on the request type of either CSR or Keys topics, publishes the request to corresponding topic and calls RegisterThing. + +Your IoT Core Thing's [Policy](https://docs.aws.amazon.com/iot/latest/developerguide/iot-policies.html) must provide privileges for this sample to connect, subscribe, publish, and receive. Below is a sample policy that can be used on your IoT Core Thing that will allow this sample to run as intended. + +
+(see sample policy) +
+{
+  "Version": "2012-10-17",
+  "Statement": [
+    {
+      "Effect": "Allow",
+      "Action": "iot:Publish",
+      "Resource": [
+        "arn:aws:iot:region:account:topic/$aws/certificates/create/json",
+        "arn:aws:iot:region:account:topic/$aws/certificates/create-from-csr/json",
+        "arn:aws:iot:region:account:topic/$aws/provisioning-templates/templatename/provision/json"
+      ]
+    },
+    {
+      "Effect": "Allow",
+      "Action": [
+        "iot:Receive"
+      ],
+      "Resource": [
+        "arn:aws:iot:region:account:topic/$aws/certificates/create/json/accepted",
+        "arn:aws:iot:region:account:topic/$aws/certificates/create/json/rejected",
+        "arn:aws:iot:region:account:topic/$aws/certificates/create-from-csr/json/accepted",
+        "arn:aws:iot:region:account:topic/$aws/certificates/create-from-csr/json/rejected",
+        "arn:aws:iot:region:account:topic/$aws/provisioning-templates/templatename/provision/json/accepted",
+        "arn:aws:iot:region:account:topic/$aws/provisioning-templates/templatename/provision/json/rejected"
+      ]
+    },
+    {
+      "Effect": "Allow",
+      "Action": [
+        "iot:Subscribe"
+      ],
+      "Resource": [
+        "arn:aws:iot:region:account:topicfilter/$aws/certificates/create/json/accepted",
+        "arn:aws:iot:region:account:topicfilter/$aws/certificates/create/json/rejected",
+        "arn:aws:iot:region:account:topicfilter/$aws/certificates/create-from-csr/json/accepted",
+        "arn:aws:iot:region:account:topicfilter/$aws/certificates/create-from-csr/json/rejected",
+        "arn:aws:iot:region:account:topicfilter/$aws/provisioning-templates/templatename/provision/json/accepted",
+        "arn:aws:iot:region:account:topicfilter/$aws/provisioning-templates/templatename/provision/json/rejected"
+      ]
+    },
+    {
+      "Effect": "Allow",
+      "Action": "iot:Connect",
+      "Resource": "arn:aws:iot:region:account:client/test-*"
+    }
+  ]
+}
+
+ +Replace with the following with the data from your AWS account: +* ``: The AWS IoT Core region where you created your AWS IoT Core thing you wish to use with this sample. For example `us-east-1`. +* ``: Your AWS IoT Core account ID. This is the set of numbers in the top right next to your AWS account name when using the AWS IoT Core website. +* ``: The name of your AWS Fleet Provisioning template you want to use to create new AWS IoT Core Things. + +Note that in a real application, you may want to avoid the use of wildcards in your ClientID or use them selectively. Please follow best practices when working with AWS on production applications using the SDK. Also, for the purposes of this sample, please make sure your policy allows a client ID of `test-*` to connect or use `--client_id ` to send the client ID your policy supports. + +
+ +## How to run + +There are many different ways to run the Fleet Provisioning sample because of how many different ways there are to setup a Fleet Provisioning template in AWS IoT Core. **The easiest and most common way is to run the sample with the following**: + +``` sh +# from the node/fleet_provisioning folder +npm install +node ./index.js --endpoint --cert --key --template_name