Skip to content

Commit adaaba5

Browse files
committed
Completed code examples
1 parent c2c1236 commit adaaba5

File tree

16 files changed

+1145
-3
lines changed

16 files changed

+1145
-3
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,6 @@ typings/
6464

6565
# cloudformation/SAM
6666
packaged.yaml
67+
68+
# env variables
69+
.env

lessons/06-purchase-ticket-api/README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
| Previous lesson | Next lesson |
44
| :--------------- | ---------------: |
5-
| [◀︎ 05 — Integrating API with DynamoDB](../05-api-with-dynamodb)| [07 — ... ▶︎](../) |
5+
| [◀︎ 05 — Integrating API with DynamoDB](../05-api-with-dynamodb)| [07 — SNS and SQS ▶︎](../07-sns-and-sqs) |
66

77

88
## Lesson 06 — Purchase ticket API
@@ -107,7 +107,7 @@ The API we want to implement needs to process the input, verify that it's a vali
107107

108108
- If the request body is not a valid JSON it should a `400 Bad Request` with body `{"error": "Invalid content, expected valid JSON"}`
109109

110-
- If one or more fields are missing or are not valid the APi should respond with a `400 Bad Request` with a JSON body containing a list of all the errors as per the following example:
110+
- If one or more fields are missing or are not valid the API should respond with a `400 Bad Request` with a JSON body containing a list of all the errors as per the following example:
111111

112112
```json
113113
{
@@ -295,4 +295,4 @@ If you see a green success message saying that the payment was processed correct
295295

296296
| Previous lesson | Next lesson |
297297
| :--------------- | ---------------: |
298-
| [◀︎ 05 — Integrating API with DynamoDB](../05-api-with-dynamodb)| [07 — ... ▶︎](../) |
298+
| [◀︎ 05 — Integrating API with DynamoDB](../05-api-with-dynamodb)| [07 — SNS and SQS ▶︎](../07-sns-and-sqs) |

lessons/07-sns-and-sqs/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
TODO

lessons/08-worker-lambda/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
TODO

lessons/extra/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ I hope you had fun along the way and that's just the beginning of your Serverles
1818
- Manage tickets availability and make a customer can't purchase tickets for sold-out events (this might require changes also in the frontend)
1919
- If you want to get very fancy you can also create a system that locks a ticket for a given amount of time while the user is filling the form for the purchase.
2020
- Integrate a real payment system like [Stripe](https://stripe.com/ie) or [Braintree](https://www.braintreepayments.com) in the purchase API and process the payments against them (of course in test mode 😇)
21+
- The worker scheduling is currently based on a schedule rule that allows us to process only one message per minute. This is not very scalable. Can you think of any solution that will allows us to make it more scalable?
22+
- Also there are many other architectures and AWS services that can allow you to have a queue of messages to process. You might want to have a look at [DynamoDB streams](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.Lambda.html) and [Kinesis](https://aws.amazon.com/kinesis/).
2123

2224

2325
## Resources for learning

resources/lambda/sns-sqs/deploy.sh

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env bash
2+
export DEPLOYMENT_BUCKET=ticketless-lambda-deployment-abcdefg
3+
export STACK_NAME=ticketless
4+
sam package --template-file template.yaml --s3-bucket $DEPLOYMENT_BUCKET --output-template-file packaged.yaml
5+
sam deploy --region eu-west-1 --template-file packaged.yaml --stack-name $STACK_NAME --capabilities CAPABILITY_IAM

resources/lambda/sns-sqs/src/index.js

+274
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
const AWS = require('aws-sdk')
2+
const validator = require('validator')
3+
const uuidv4 = require('uuid/v4')
4+
5+
const docClient = new AWS.DynamoDB.DocumentClient()
6+
const sns = new AWS.SNS()
7+
8+
exports.listGigs = (event, context, callback) => {
9+
const queryParams = {
10+
TableName: 'gig'
11+
}
12+
13+
docClient.scan(queryParams, (err, data) => {
14+
if (err) {
15+
console.error(err)
16+
17+
return callback(null, {
18+
statusCode: 500,
19+
headers: {
20+
'Content-Type': 'application/json',
21+
'Access-Control-Allow-Origin': '*'
22+
},
23+
body: JSON.stringify({error: 'Internal Server Error'})
24+
})
25+
}
26+
27+
const response = {
28+
statusCode: 200,
29+
headers: {
30+
'Content-Type': 'application/json',
31+
'Access-Control-Allow-Origin': '*'
32+
},
33+
body: JSON.stringify({gigs: data.Items})
34+
}
35+
36+
return callback(null, response)
37+
})
38+
}
39+
40+
exports.gig = (event, context, callback) => {
41+
const gigSlug = event.pathParameters.slug
42+
43+
const queryParams = {
44+
Key: {
45+
slug: gigSlug
46+
},
47+
TableName: 'gig'
48+
}
49+
50+
docClient.get(queryParams, (err, data) => {
51+
if (err) {
52+
console.error(err)
53+
return callback(null, {
54+
statusCode: 500,
55+
headers: {
56+
'Content-Type': 'application/json',
57+
'Access-Control-Allow-Origin': '*'
58+
},
59+
body: JSON.stringify({error: 'Internal Server Error'})
60+
})
61+
}
62+
63+
// item not found, return 404
64+
if (!data.Item) {
65+
return callback(null, {
66+
statusCode: 404,
67+
headers: {
68+
'Content-Type': 'application/json',
69+
'Access-Control-Allow-Origin': '*'
70+
},
71+
body: JSON.stringify({error: 'Gig not found'})
72+
})
73+
}
74+
75+
const response = {
76+
statusCode: 200,
77+
headers: {
78+
'Content-Type': 'application/json',
79+
'Access-Control-Allow-Origin': '*'
80+
},
81+
body: JSON.stringify(data.Item)
82+
}
83+
84+
return callback(null, response)
85+
})
86+
}
87+
88+
exports.purchaseTicket = (event, context, callback) => {
89+
// receives a JSON in the event.body containing:
90+
// - gig: needs to be an existing gig
91+
// - name: non empty string
92+
// - email: valid email
93+
// - cardNumber: valid credit card number
94+
// - cardExpiryMonth: required (int between 1 and 12)
95+
// - cardExpiryYear: required (int between 2018 and 2024) (month and year in the future)
96+
// - cardCVC: required (valid cvc)
97+
// - disclaimerAccepted: required (true)
98+
//
99+
// Must return a validation error (400 Bad request) with the following object:
100+
// {error: "Invalid request", errors: [{field: "fieldName", message: "error message"}]}
101+
//
102+
// or, in case of success a 202 (Accepted) with body { success: true }
103+
104+
let data
105+
106+
// parses the input
107+
try {
108+
data = JSON.parse(event.body)
109+
} catch (err) {
110+
return callback(null, {
111+
statusCode: 400,
112+
headers: {
113+
'Content-Type': 'application/json',
114+
'Access-Control-Allow-Origin': '*'
115+
},
116+
body: JSON.stringify({error: 'Invalid content, expected valid JSON'})
117+
})
118+
}
119+
120+
// validates every field
121+
const errors = []
122+
123+
// gig: needs to be an existing gig
124+
if (!data.gig) {
125+
errors.push({field: 'gig', message: 'field is mandatory'})
126+
// validating if the gig exists in DynamoDB is left as an exercise
127+
}
128+
129+
// name: non empty string
130+
if (!data.name) {
131+
errors.push({field: 'name', message: 'field is mandatory'})
132+
}
133+
134+
// email: valid email
135+
if (!data.email) {
136+
errors.push({field: 'email', message: 'field is mandatory'})
137+
} else if (!validator.isEmail(data.email)) {
138+
errors.push({field: 'email', message: 'field is not a valid email'})
139+
}
140+
141+
// cardNumber: valid credit card number
142+
if (!data.cardNumber) {
143+
errors.push({field: 'cardNumber', message: 'field is mandatory'})
144+
} else if (!validator.isCreditCard(data.cardNumber)) {
145+
errors.push({field: 'cardNumber', message: 'field is not a valid credit card number'})
146+
}
147+
148+
// cardExpiryMonth: required (int between 1 and 12)
149+
if (!data.cardExpiryMonth) {
150+
errors.push({field: 'cardExpiryMonth', message: 'field is mandatory'})
151+
} else if (!validator.isInt(String(data.cardExpiryMonth), {min: 1, max: 12})) {
152+
errors.push({field: 'cardExpiryMonth', message: 'field must be an integer in range [1,12]'})
153+
}
154+
155+
// cardExpiryYear: required (month and year in the future)
156+
if (!data.cardExpiryYear) {
157+
errors.push({field: 'cardExpiryYear', message: 'field is mandatory'})
158+
} else if (!validator.isInt(String(data.cardExpiryYear), {min: 2018, max: 2024})) {
159+
errors.push({field: 'cardExpiryYear', message: 'field must be an integer in range [2018,2024]'})
160+
}
161+
162+
// validating that expiry is in the future is left as exercise
163+
// (consider using a library like moment.js)
164+
165+
// cardCVC: required (valid cvc)
166+
if (!data.cardCVC) {
167+
errors.push({field: 'cardCVC', message: 'field is mandatory'})
168+
} else if (!String(data.cardCVC).match(/^[0-9]{3,4}$/)) {
169+
errors.push({field: 'cardCVC', message: 'field must be a valid CVC'})
170+
}
171+
172+
// disclaimerAccepted: required (true)
173+
if (data.disclaimerAccepted !== true) {
174+
errors.push({field: 'disclaimerAccepted', message: 'field must be true'})
175+
}
176+
177+
// if there are errors, return a 400 with the list of errors
178+
179+
if (errors.length) {
180+
return callback(null, {
181+
statusCode: 400,
182+
headers: {
183+
'Content-Type': 'application/json',
184+
'Access-Control-Allow-Origin': '*'
185+
},
186+
body: JSON.stringify({error: 'Invalid Request', errors})
187+
})
188+
}
189+
190+
// fetch gig from DynamoDB
191+
const queryParams = {
192+
Key: {
193+
slug: data.gig
194+
},
195+
TableName: 'gig'
196+
}
197+
198+
docClient.get(queryParams, (err, dynamoData) => {
199+
if (err) {
200+
console.error(err)
201+
return callback(null, {
202+
statusCode: 500,
203+
headers: {
204+
'Content-Type': 'application/json',
205+
'Access-Control-Allow-Origin': '*'
206+
},
207+
body: JSON.stringify({error: 'Internal Server Error'})
208+
})
209+
}
210+
211+
// item not found, return 404
212+
if (!dynamoData.Item) {
213+
return callback(null, {
214+
statusCode: 400,
215+
headers: {
216+
'Content-Type': 'application/json',
217+
'Access-Control-Allow-Origin': '*'
218+
},
219+
body: JSON.stringify({error: 'Invalid gig'})
220+
})
221+
}
222+
223+
const gig = dynamoData.Item
224+
// creates a ticket object
225+
const ticket = {
226+
id: uuidv4(),
227+
createdAt: Date.now(),
228+
name: data.name,
229+
email: data.email,
230+
gig: data.gig
231+
}
232+
233+
// fires an sns message with gig and ticket
234+
sns.publish({
235+
TopicArn: process.env.SNS_TOPIC_ARN,
236+
Message: JSON.stringify({ticket, gig})
237+
}, (err, data) => {
238+
if (err) {
239+
console.error(err)
240+
return callback(null, {
241+
statusCode: 500,
242+
headers: {
243+
'Content-Type': 'application/json',
244+
'Access-Control-Allow-Origin': '*'
245+
},
246+
body: JSON.stringify({error: 'Internal Server Error'})
247+
})
248+
}
249+
250+
// if everything went well return a 202 (accepted)
251+
return callback(null, {
252+
statusCode: 202,
253+
headers: {
254+
'Content-Type': 'application/json',
255+
'Access-Control-Allow-Origin': '*'
256+
},
257+
body: JSON.stringify({success: true})
258+
})
259+
})
260+
})
261+
}
262+
263+
exports.cors = (event, context, callback) => {
264+
callback(null, {
265+
statusCode: 200,
266+
headers: {
267+
'Content-Type': 'application/json',
268+
'Access-Control-Allow-Origin': '*',
269+
'Access-Control-Allow-Methods': '*',
270+
'Access-Control-Allow-Headers': 'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'
271+
},
272+
body: ''
273+
})
274+
}

resources/lambda/sns-sqs/src/package-lock.json

+18
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "src",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1"
8+
},
9+
"keywords": [],
10+
"author": "",
11+
"license": "ISC",
12+
"dependencies": {
13+
"uuid": "^3.1.0",
14+
"validator": "^9.1.1"
15+
}
16+
}

0 commit comments

Comments
 (0)