aws-iot-device-sdk-v2
Version:
NodeJS API for the AWS IoT service
425 lines (331 loc) • 13.7 kB
text/typescript
/*
* 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<string>;
responsePathGenerator: (request: any) => Array<RequestResponsePath>;
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<string, RequestResponseOperationModel>,
// operation name -> operation model
streamingOperations: Map<string, StreamingOperationModel>,
// shape name -> validator function
shapeValidators: Map<string, (value: any) => void>;
}
function buildResponseDeserializerMap(paths: Array<RequestResponsePath>) : Map<string, MessageDeserializer> {
return new Map<string, MessageDeserializer>(
paths.map((path) => {
return [path.topic, path.deserializer];
})
);
}
function buildResponsePaths(paths: Array<RequestResponsePath>) : Array<mqtt_request_response.ResponsePath> {
return paths.map((path) => {
let responsePath : mqtt_request_response.ResponsePath = {
topic: path.topic
};
if (path.correlationTokenJsonPath) {
responsePath.correlationTokenJsonPath = path.correlationTokenJsonPath;
}
return responsePath;
});
}
export async function doRequestResponse<ResponseType>(options: RequestResponseOperationConfig) : Promise<ResponseType> {
return new Promise<ResponseType>(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
};
if (correlationToken) {
requestOptions.correlationToken = correlationToken;
}
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);
if (!deserializer) {
reject(createServiceError(`Operation "${options.operationName}" does not have a deserializer for topic "${responseTopic}"`));
return;
}
let deserializedResponse = deserializer(responsePayload) as ResponseType;
if (wasSuccess) {
resolve(deserializedResponse);
} else {
reject(createServiceError("Request failed", undefined, deserializedResponse));
}
} catch (err) {
if (err instanceof ServiceError) {
reject(err);
} else if (err instanceof CrtError) {
reject(createServiceError("CrtError", err as CrtError));
} else {
reject(createServiceError((err as Error).toString()));
}
}
});
}
export function createServiceError(description: string, internalError?: CrtError, modeledError?: any) {
return new ServiceError({
description: description,
internalError: internalError,
modeledError: modeledError,
});
}
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) : 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) : void {
if (value === undefined) {
throwMissingPropertyError(propertyName);
}
if (typeof value !== 'string') {
throwInvalidPropertyValueError("a string", propertyName);
}
if (value.includes("/") || value.includes("#") || value.includes("+")) {
throwInvalidPropertyValueError("a valid MQTT topic", propertyName);
}
}
export function validateOptionalValueAsNumber(value: any, propertyName?: string) {
if (value === undefined) {
return;
}
validateValueAsNumber(value, propertyName);
}
export function validateValueAsNumber(value: any, propertyName?: string) {
if (value == undefined) {
throwMissingPropertyError(propertyName);
}
if (typeof value !== 'number') {
throwInvalidPropertyValueError("a number", propertyName);
}
}
//////////////
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<any, any>;
for (const key in value) {
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()}`);
}
}
let val = value[key];
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);
}