From f20d33f4e4c2df7f6505f8524279b7750b732cf9 Mon Sep 17 00:00:00 2001 From: Chris Cooksley Date: Thu, 16 Jan 2025 10:56:38 +0000 Subject: [PATCH] Implement UpdateSessionOperation in dynamo-agnostic way --- .../src/functions/adapters/dynamoDbAdapter.ts | 89 +++++++++++++++++++ .../BiometricTokenIssued.ts | 23 +++++ .../UpdateSessionOperation.ts | 9 ++ 3 files changed, 121 insertions(+) create mode 100644 backend-api/src/functions/common/session/updateOperations/BiometricTokenIssued/BiometricTokenIssued.ts create mode 100644 backend-api/src/functions/common/session/updateOperations/UpdateSessionOperation.ts diff --git a/backend-api/src/functions/adapters/dynamoDbAdapter.ts b/backend-api/src/functions/adapters/dynamoDbAdapter.ts index 208d27dd..263944a9 100644 --- a/backend-api/src/functions/adapters/dynamoDbAdapter.ts +++ b/backend-api/src/functions/adapters/dynamoDbAdapter.ts @@ -1,4 +1,5 @@ import { + AttributeValue, ConditionalCheckFailedException, DynamoDBClient, PutItemCommand, @@ -6,6 +7,7 @@ import { QueryCommand, QueryCommandInput, QueryCommandOutput, + UpdateItemCommand, } from "@aws-sdk/client-dynamodb"; import { CreateSessionAttributes } from "../services/session/sessionService"; import { NodeHttpHandler } from "@smithy/node-http-handler"; @@ -14,6 +16,13 @@ import { NativeAttributeValue, unmarshall, } from "@aws-sdk/util-dynamodb"; +import { UpdateSessionOperation } from "../common/session/updateOperations/UpdateSessionOperation"; +import { + emptySuccess, + ErrorCategory, + errorResult, + Result, +} from "../utils/result"; const sessionStates = { ASYNC_AUTH_SESSION_CREATED: "ASYNC_AUTH_SESSION_CREATED", @@ -114,7 +123,87 @@ export class DynamoDbAdapter { } } + async updateSession( + sessionId: string, + updateOperation: UpdateSessionOperation, + ): Promise> { + const updateItemCommand = new UpdateItemCommand({ + TableName: "mockTableName", + Key: { + sessionId: { + S: "mockSessionId", + }, + }, + ExpressionAttributeValues: getExpressionAttributeValues(updateOperation), + ConditionExpression: getConditionExpression(updateOperation), + UpdateExpression: getUpdateExpression(updateOperation), + }); + + try { + console.log( + "Update session attempt", + updateItemCommand.input.UpdateExpression, + ); // replace with proper logging + await this.dynamoDbClient.send(updateItemCommand); + } catch (error) { + if (error instanceof ConditionalCheckFailedException) { + console.log( + "Conditional check failed", + updateItemCommand.input.ConditionExpression, + ); // replace with proper logging + return errorResult({ + errorMessage: "Conditional check failed", + errorCategory: ErrorCategory.CLIENT_ERROR, + }); + } else { + console.log("Unexpected error", error); // replace with proper logging + return errorResult({ + errorMessage: "Unexpected error", + errorCategory: ErrorCategory.SERVER_ERROR, + }); + } + } + console.log("Update session success"); // replace with proper logging + return emptySuccess(); + } + private getTimeNowInSeconds() { return Math.floor(Date.now() / 1000); } } + +function getExpressionAttributeValues( + updateOperation: UpdateSessionOperation, +): Record { + const attributeValues: Record = { + ":sessionState": marshall(updateOperation.targetState), + }; + updateOperation.eligibleStartingStates.forEach((state) => { + attributeValues[`:${state}`] = marshall(state); + }); + Object.entries(updateOperation.getFieldUpdates()).forEach(([key, value]) => { + attributeValues[`:${key}`] = marshall(value); + }); + return attributeValues; +} + +function getConditionExpression( + updateOperation: UpdateSessionOperation, +): string { + const permissibleStatesAsAttributes: string = + updateOperation.eligibleStartingStates + .map((state) => `:${state}`) + .join(", "); + return `sessionState in (${permissibleStatesAsAttributes})`; // sessionState in (:EXAMPLE_SESSION_STATE_1, :EXAMPLE_SESSION_STATE_2) +} + +function getUpdateExpression(updateOperation: UpdateSessionOperation): string { + const fieldsToUpdate = [ + "sessionState", + ...Object.keys(updateOperation.getFieldUpdates()), + ]; + const updateExpressions = fieldsToUpdate.map( + (fieldName) => `${fieldName} = :${fieldName}`, + ); + return `set ${updateExpressions.join(", ")}`; // set sessionState = :EXAMPLE_SESSION_STATE, field1 = :field1, field2 = :field2 +} diff --git a/backend-api/src/functions/common/session/updateOperations/BiometricTokenIssued/BiometricTokenIssued.ts b/backend-api/src/functions/common/session/updateOperations/BiometricTokenIssued/BiometricTokenIssued.ts new file mode 100644 index 00000000..7e78e41b --- /dev/null +++ b/backend-api/src/functions/common/session/updateOperations/BiometricTokenIssued/BiometricTokenIssued.ts @@ -0,0 +1,23 @@ +import { + SessionState, + UpdateSessionOperation, +} from "../UpdateSessionOperation"; +import { DocumentType } from "../../../../types/document"; + +export class BiometricTokenIssued implements UpdateSessionOperation { + constructor( + private readonly documentType: DocumentType, + private readonly opaqueId: string, + ) {} + + readonly targetState = SessionState.BIOMETRIC_TOKEN_ISSUED; + + readonly eligibleStartingStates = [SessionState.AUTH_SESSION_CREATED]; + + getFieldUpdates() { + return { + documentType: this.documentType, + opaqueId: this.opaqueId, + }; + } +} diff --git a/backend-api/src/functions/common/session/updateOperations/UpdateSessionOperation.ts b/backend-api/src/functions/common/session/updateOperations/UpdateSessionOperation.ts new file mode 100644 index 00000000..753a20e4 --- /dev/null +++ b/backend-api/src/functions/common/session/updateOperations/UpdateSessionOperation.ts @@ -0,0 +1,9 @@ +import { SessionState } from "../session"; + +type SessionFieldValue = string | number; + +export interface UpdateSessionOperation { + readonly targetState: SessionState; + readonly eligibleStartingStates: SessionState[]; + getFieldUpdates(): Record; +}