1
1
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
2
// SPDX-License-Identifier: MIT-0
3
3
4
+ using System . Net ;
4
5
using System . Text . Json ;
5
6
using System . Text . RegularExpressions ;
6
7
using Amazon ;
16
17
using AWS . Lambda . Powertools . Logging ;
17
18
using AWS . Lambda . Powertools . Metrics ;
18
19
using AWS . Lambda . Powertools . Tracing ;
20
+ using Microsoft . Extensions . Diagnostics . HealthChecks ;
19
21
using Unicorn . Web . Common ;
20
22
using DynamoDBContextConfig = Amazon . DynamoDBv2 . DataModel . DynamoDBContextConfig ;
21
23
@@ -28,9 +30,11 @@ public class RequestApprovalFunction
28
30
{
29
31
private readonly IDynamoDBContext _dynamoDbContext ;
30
32
private readonly IAmazonEventBridge _eventBindingClient ;
31
- private readonly string _eventBusName ;
32
- private readonly string _serviceNamespace ;
33
-
33
+ private string ? _dynamodbTable ;
34
+ private string ? _eventBusName ;
35
+ private string ? _serviceNamespace ;
36
+ private const string Pattern = @"[a-z-]+\/[a-z-]+\/[a-z][a-z0-9-]*\/[0-9-]+" ;
37
+
34
38
/// <summary>
35
39
/// Default constructor. Initialises global variables for function.
36
40
/// </summary>
@@ -40,23 +44,16 @@ public RequestApprovalFunction()
40
44
// Instrument all AWS SDK calls
41
45
AWSSDKHandler . RegisterXRayForAllServices ( ) ;
42
46
43
- var dynamodbTable = Environment . GetEnvironmentVariable ( "DYNAMODB_TABLE" ) ?? "" ;
44
- if ( string . IsNullOrEmpty ( dynamodbTable ) )
45
- throw new Exception ( "Environment variable DYNAMODB_TABLE is not defined." ) ;
46
-
47
- _eventBusName = Environment . GetEnvironmentVariable ( "EVENT_BUS" ) ?? "" ;
48
- if ( string . IsNullOrEmpty ( _eventBusName ) )
49
- throw new Exception ( "Environment variable EVENT_BUS is not defined." ) ;
50
-
51
- _serviceNamespace = Environment . GetEnvironmentVariable ( "SERVICE_NAMESPACE" ) ?? "" ;
52
- if ( string . IsNullOrEmpty ( _eventBusName ) )
53
- throw new Exception ( "Environment variable SERVICE_NAMESPACE is not defined." ) ;
47
+ // Validate and set environment variables
48
+ SetEnvironmentVariables ( ) ;
54
49
50
+ // Initialise DDB client
55
51
AWSConfigsDynamoDB . Context . TypeMappings [ typeof ( PropertyRecord ) ] =
56
- new TypeMapping ( typeof ( PropertyRecord ) , dynamodbTable ) ;
57
-
52
+ new TypeMapping ( typeof ( PropertyRecord ) , _dynamodbTable ) ;
58
53
var config = new DynamoDBContextConfig { Conversion = DynamoDBEntryConversion . V2 } ;
59
54
_dynamoDbContext = new DynamoDBContext ( new AmazonDynamoDBClient ( ) , config ) ;
55
+
56
+ // Initialise EventBridge client
60
57
_eventBindingClient = new AmazonEventBridgeClient ( ) ;
61
58
}
62
59
@@ -65,19 +62,59 @@ public RequestApprovalFunction()
65
62
/// </summary>
66
63
/// <param name="dynamoDbContext"></param>
67
64
/// <param name="eventBindingClient"></param>
68
- /// <param name="eventBusName"></param>
69
- /// <param name="serviceNamespace"></param>
70
- public RequestApprovalFunction ( IDynamoDBContext dynamoDbContext ,
71
- IAmazonEventBridge eventBindingClient ,
72
- string eventBusName ,
73
- string serviceNamespace )
65
+ public RequestApprovalFunction ( IDynamoDBContext dynamoDbContext , IAmazonEventBridge eventBindingClient )
74
66
{
67
+ // Validate and set environment variables
68
+ SetEnvironmentVariables ( ) ;
69
+
75
70
_dynamoDbContext = dynamoDbContext ;
76
71
_eventBindingClient = eventBindingClient ;
77
- _eventBusName = eventBusName ;
78
- _serviceNamespace = serviceNamespace ;
79
72
}
80
-
73
+
74
+ /// <summary>
75
+ /// Validate and set environment variables
76
+ /// </summary>
77
+ /// <exception cref="Exception">Generic exception thrown if any of the required environment variables cannot be set.</exception>
78
+ private void SetEnvironmentVariables ( )
79
+ {
80
+ _dynamodbTable = Environment . GetEnvironmentVariable ( "DYNAMODB_TABLE" ) ?? "" ;
81
+ if ( string . IsNullOrEmpty ( _dynamodbTable ) )
82
+ throw new Exception ( "Environment variable DYNAMODB_TABLE is not defined." ) ;
83
+
84
+ _eventBusName = Environment . GetEnvironmentVariable ( "EVENT_BUS" ) ?? "" ;
85
+ if ( string . IsNullOrEmpty ( _eventBusName ) )
86
+ throw new Exception ( "Environment variable EVENT_BUS is not defined." ) ;
87
+
88
+ _serviceNamespace = Environment . GetEnvironmentVariable ( "SERVICE_NAMESPACE" ) ?? "" ;
89
+ if ( string . IsNullOrEmpty ( _eventBusName ) )
90
+ throw new Exception ( "Environment variable SERVICE_NAMESPACE is not defined." ) ;
91
+ }
92
+
93
+ /// <summary>
94
+ /// Helper method to generate APIGatewayProxyResponse
95
+ /// </summary>
96
+ /// <param name="message">The message to include in the payload.</param>
97
+ /// <param name="statusCode">the HTTP status code</param>
98
+ /// <returns>APIGatewayProxyResponse</returns>
99
+ private static Task < APIGatewayProxyResponse > ApiResponse ( string message , HttpStatusCode statusCode )
100
+ {
101
+ var body = new Dictionary < string , string >
102
+ {
103
+ { "message" , message }
104
+ } ;
105
+
106
+ return Task . FromResult ( new APIGatewayProxyResponse
107
+ {
108
+ Body = JsonSerializer . Serialize ( body ) ,
109
+ StatusCode = ( int ) statusCode ,
110
+ Headers = new Dictionary < string , string >
111
+ {
112
+ { "Content-Type" , "application/json" } ,
113
+ { "X-Custom-Header" , "application/json" }
114
+ }
115
+ } ) ;
116
+ }
117
+
81
118
/// <summary>
82
119
/// Lambda Handler for creating new Contracts.
83
120
/// </summary>
@@ -90,18 +127,8 @@ public RequestApprovalFunction(IDynamoDBContext dynamoDbContext,
90
127
public async Task < APIGatewayProxyResponse > FunctionHandler ( APIGatewayProxyRequest apigProxyEvent ,
91
128
ILambdaContext context )
92
129
{
93
- var response = new APIGatewayProxyResponse
94
- {
95
- Body = string . Empty ,
96
- StatusCode = 200 ,
97
- Headers = new Dictionary < string , string >
98
- {
99
- { "Content-Type" , "application/json" } ,
100
- { "X-Custom-Header" , "application/json" }
101
- }
102
- } ;
103
-
104
130
string propertyId ;
131
+
105
132
try
106
133
{
107
134
var request = JsonSerializer . Deserialize < RequestApprovalRequest > ( apigProxyEvent . Body ) ;
@@ -111,25 +138,14 @@ public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyReques
111
138
catch ( Exception e )
112
139
{
113
140
Logger . LogError ( e ) ;
114
- var body = new Dictionary < string , string >
115
- {
116
- { "message" , $ "Unable to parse event input as JSON: { e . Message } " }
117
- } ;
118
- response . Body = JsonSerializer . Serialize ( body ) ;
119
- response . StatusCode = 400 ;
120
- return response ;
141
+ return await ApiResponse ( "Unable to parse event input as JSON" , HttpStatusCode . BadRequest ) ;
121
142
}
122
143
123
- var pattern = @"[a-z-]+\/[a-z-]+\/[a-z][a-z0-9-]*\/[0-9-]+" ;
124
- if ( string . IsNullOrWhiteSpace ( propertyId ) || ! Regex . Match ( propertyId , pattern ) . Success )
144
+ // Validate property ID
145
+ if ( string . IsNullOrWhiteSpace ( propertyId ) || ! Regex . Match ( propertyId , Pattern ) . Success )
125
146
{
126
- var body = new Dictionary < string , string >
127
- {
128
- { "message" , $ "Input invalid; must conform to regular expression: { pattern } " }
129
- } ;
130
- response . Body = JsonSerializer . Serialize ( body ) ;
131
- response . StatusCode = 400 ;
132
- return response ;
147
+ return await ApiResponse ( $ "Input invalid; must conform to regular expression: { Pattern } ",
148
+ HttpStatusCode . BadRequest ) ;
133
149
}
134
150
135
151
var splitString = propertyId . Split ( '/' ) ;
@@ -144,54 +160,42 @@ public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyReques
144
160
try
145
161
{
146
162
var properties = await QueryTableAsync ( pk , sk ) . ConfigureAwait ( false ) ;
163
+
147
164
if ( ! properties . Any ( ) )
148
165
{
149
- var body = new Dictionary < string , string >
150
- {
151
- { "message" , "No property found in database with the requested property id" }
152
- } ;
153
- response . Body = JsonSerializer . Serialize ( body ) ;
154
- response . StatusCode = 500 ;
155
- return response ;
166
+ return await ApiResponse ( "No property found in database with the requested property id" ,
167
+ HttpStatusCode . InternalServerError ) ;
156
168
}
157
169
158
170
var property = properties . First ( ) ;
159
- if ( string . Equals ( property . Status , PropertyStatus . Approved , StringComparison . CurrentCultureIgnoreCase ) ||
160
- string . Equals ( property . Status , PropertyStatus . Declined , StringComparison . CurrentCultureIgnoreCase ) ||
161
- string . Equals ( property . Status , PropertyStatus . Pending , StringComparison . CurrentCultureIgnoreCase ) )
171
+
172
+ // Do not approve properties in an approved state
173
+ if ( string . Equals ( property . Status , PropertyStatus . Approved , StringComparison . CurrentCultureIgnoreCase ) )
162
174
{
163
- response . Body = JsonSerializer . Serialize ( new Dictionary < string , string >
164
- {
165
- { "message" , $ "Property is already { property . Status } ; no action taken" }
166
- } ) ;
167
- return response ;
175
+ return await ApiResponse ( $ "Property is already { property . Status } ; no action taken", HttpStatusCode . InternalServerError ) ;
168
176
}
169
-
177
+ // Reset status to pending, awaiting the result of the check.
170
178
property . Status = PropertyStatus . Pending ;
171
-
172
- await SendEventAsync ( propertyId , property ) . ConfigureAwait ( false ) ;
173
179
180
+ // Publish the event
181
+ await SendEventAsync ( propertyId , property ) . ConfigureAwait ( false ) ;
182
+
183
+ // Add custom metric for the number of approval requests
184
+ Metrics . AddMetric ( "ApprovalsRequested" , 1 , MetricUnit . Count ) ;
185
+
174
186
Logger . LogInformation ( $ "Storing new property in DynamoDB with PK { pk } and SK { sk } ") ;
187
+
175
188
await _dynamoDbContext . SaveAsync ( property ) . ConfigureAwait ( false ) ;
176
189
Logger . LogInformation ( $ "Stored item in DynamoDB;") ;
177
190
}
178
191
catch ( Exception e )
179
192
{
180
193
Logger . LogError ( e ) ;
181
- var body = new Dictionary < string , string >
182
- {
183
- { "message" , e . Message }
184
- } ;
185
- response . Body = JsonSerializer . Serialize ( body ) ;
186
- response . StatusCode = 500 ;
187
- return response ;
194
+ return await ApiResponse ( e . Message , HttpStatusCode . InternalServerError ) ;
195
+
188
196
}
189
-
190
- response . Body = JsonSerializer . Serialize ( new Dictionary < string , string >
191
- {
192
- { "message" , "Approval Requested" }
193
- } ) ;
194
- return response ;
197
+
198
+ return await ApiResponse ( "Approval Requested" , HttpStatusCode . OK ) ;
195
199
}
196
200
197
201
private async Task < List < PropertyRecord > > QueryTableAsync ( string partitionKey , string sortKey )
@@ -204,7 +208,7 @@ private async Task<List<PropertyRecord>> QueryTableAsync(string partitionKey, st
204
208
. GetRemainingAsync ( )
205
209
. ConfigureAwait ( false ) ;
206
210
}
207
-
211
+
208
212
private async Task SendEventAsync ( string propertyId , PropertyRecord property )
209
213
{
210
214
var requestApprovalEvent = new RequestApprovalEvent
@@ -244,9 +248,6 @@ private async Task SendEventAsync(string propertyId, PropertyRecord property)
244
248
245
249
Logger . LogInformation (
246
250
$ "Sent event to EventBridge; { response . FailedEntryCount } records failed; { response . Entries . Count } entries received") ;
247
-
248
- Metrics . AddMetric ( "ApprovalsRequested" , 1 , MetricUnit . Count ) ;
249
- }
250
-
251
251
252
+ }
252
253
}
0 commit comments