Skip to content

Commit ed06de3

Browse files
committed
feat: initial commit
1 parent a010b1a commit ed06de3

File tree

83 files changed

+5481
-9
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+5481
-9
lines changed

README.md

100644100755
Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,43 @@
1-
## My Project
1+
![AWS Serverless Developer Experience Workshop Reference Architecture](/docs/workshop_logo.png)
22

3-
TODO: Fill this README out!
3+
# AWS Serverless Developer Experience workshop reference architecture (Python)
44

5-
Be sure to:
5+
This repository contains the reference architecture for the AWS Serverless Developer Experience workshop.
66

7-
* Change the title in this README
8-
* Edit your repository description on GitHub
7+
The AWS Serverless Developer Experience workshop provides you with an immersive experience of a serverless developer. The goal is to provide you with an hands-on experience building a serverless solution using the [AWS Serverless Application Model (AWS SAM)](https://aws.amazon.com/serverless/sam/) and AWS SAM CLI.
98

10-
## Security
9+
Along the way, we want to demonstrate principles of event-driven distributed architecture, orchestration, and serverless observability, and how to apply them in code.
1110

12-
See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information.
11+
We'll also explore open-source tools, core features of AWS Lambda Powertools, and serverless CI/CD deployments. You can choose choose to run this workshop in a runtime of your choice — Python, TypeScript, Java, and .NET — and work with your own developer setup or use AWS Cloud9 to build the services.
1312

14-
## License
13+
This workshop will take approximately 4 hours to complete. We are assuming that you have some practical development skills in one of the supported runtimes, and are familiar with some of the services that we will use in this solution which include: [Amazon API Gateway](https://aws.amazon.com/apigateway/), [AWS Lambda](https://aws.amazon.com/lambda/), [Amazon EventBridge](https://aws.amazon.com/eventbridge/), [AWS Step Functions](https://aws.amazon.com/step-functions/) and [Amazon DynamoDB](https://aws.amazon.com/dynamodb/).
1514

16-
This library is licensed under the MIT-0 License. See the LICENSE file.
15+
## About the Architecture
1716

17+
![AWS Serverless Developer Experience Workshop Reference Architecture](/docs/architecture.png)
18+
19+
Our use case is based on a real estate company called **Unicorn Properties**.
20+
21+
As a real estate agency, **Unicorn Properties** needs to manage the publishing of new property listings and sale contracts linked to individual properties, and provide a way for their customers to view approved property listings.
22+
23+
To support their needs, Unicorn Properties have adopted a serverless, event-driven approach to designing their architecture. This architecture is centred around two primary domains: boundaries–Contracts (managed by the Contracts Service) and Properties (which are managed by the Properties Web and Properties Services).
24+
25+
The **Contracts Service** is a simplified service that manages the contractual relationship between a seller of a property and Unicorn Properties. Contracts are drawn up that define the property for sale, the terms and conditions that Unicorn Properties sets, and how much it will cost the seller to engage the services of the agency.
26+
27+
The **Properties Web** service manages the details of a property listing to be published on the Unicorn Properties website. Every property listing has an address, a sale price, a description of the property, and some photos that members of the public can look at to get them interested in purchasing the property. **Only properties that have been approved for publication can be made visible to the public**.
28+
29+
The **Properties Service** approves a listing. This service implements a workflow that checks for the existence of a contract, makes sure that the content and the images are safe to publish, and finally checks that the contract has been approved. We don’t want to publish a property until we have an approved contract!
30+
31+
32+
## Credits
33+
34+
Throughout this workshop we wanted to introduce you to some Open Source tools that can help you build serverless applications. This is not an exhaustive list, just a small selection of what we will be using in the workshop.
35+
36+
Many thanks to all the AWS teams and community builders who have contributed to this list:
37+
38+
| Tools | Description | Download / Installation Instructions |
39+
| --------------------- | ----------- | --------------------------------------- |
40+
| cfn-lint | Validate AWS CloudFormation yaml/json templates against the AWS CloudFormation Resource Specification and additional checks. | https://github.com/aws-cloudformation/cfn-lint |
41+
| cfn-lint-serverless | Compilation of rules to validate infrastructure-as-code templates against recommended practices for serverless applications. | https://github.com/awslabs/serverless-rules |
42+
| @mhlabs/iam-policies-cli| CLI for generating AWS IAM policy documents or SAM policy templates based on the JSON definition used in the AWS Policy Generator. | https://github.com/mhlabs/iam-policies-cli |
43+
| @mhlabs/evb-cli | Pattern generator and debugging tool for Amazon EventBridge | https://github.com/mhlabs/evb-cli |

docs/architecture.png

174 KB
Loading

docs/workshop_logo.png

328 KB
Loading

unicorn_contracts/.eslintignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
.aws-sam

unicorn_contracts/.eslintrc.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module.exports = {
2+
parser: "@typescript-eslint/parser",
3+
parserOptions: {
4+
ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
5+
sourceType: "module"
6+
},
7+
extends: [
8+
"plugin:@typescript-eslint/recommended", // recommended rules from the @typescript-eslint/eslint-plugin
9+
"plugin:prettier/recommended" // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
10+
],
11+
rules: {
12+
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
13+
// e.g. "@typescript-eslint/explicit-function-return-type": "off",
14+
}
15+
};

unicorn_contracts/.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
tests/*
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
2+
export default {
3+
preset: "ts-jest",
4+
clearMocks: true,
5+
collectCoverage: true,
6+
coverageDirectory: "coverage",
7+
coverageProvider: "v8",
8+
testMatch: ["**/tests/unit/*.test.ts"],
9+
};

unicorn_contracts/package.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"name": "contracts",
3+
"version": "1.0.0",
4+
"description": "Contracts Module for Serverless Developer Experience Reference Architecture - Node",
5+
"main": "app.js",
6+
"repository": "https://github.com/awslabs/aws-sam-cli/tree/develop/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs",
7+
"author": "SAM CLI",
8+
"license": "MIT",
9+
"dependencies": {
10+
"@aws-lambda-powertools/logger": "^1.1.1",
11+
"@aws-lambda-powertools/metrics": "^1.1.1",
12+
"@aws-lambda-powertools/tracer": "^1.1.1",
13+
"@aws-sdk/client-dynamodb": "^3.87.0",
14+
"@aws-sdk/client-eventbridge": "^3.92.0",
15+
"@aws-sdk/util-dynamodb": "^3.92.0",
16+
"aws-lambda": "^1.0.7"
17+
},
18+
"scripts": {
19+
"unit": "jest --config=jest.config.test.unit.ts",
20+
"lint": "eslint '*.ts' --quiet --fix",
21+
"compile": "tsc",
22+
"test": "npm run lint && npm run compile && npm run unit"
23+
},
24+
"devDependencies": {
25+
"@types/aws-lambda": "^8.10.92",
26+
"@types/jest": "^28.1.1",
27+
"@types/node": "^17.0.13",
28+
"@typescript-eslint/eslint-plugin": "^5.10.2",
29+
"@typescript-eslint/parser": "^5.10.2",
30+
"aws-sdk-client-mock": "^0.6.2",
31+
"esbuild": "^0.14.14",
32+
"esbuild-jest": "^0.5.0",
33+
"eslint": "^8.8.0",
34+
"eslint-config-prettier": "^8.3.0",
35+
"eslint-plugin-prettier": "^4.0.0",
36+
"jest": "^28.1.1",
37+
"prettier": "^2.5.1",
38+
"ts-jest": "^28.0.4",
39+
"ts-node": "^10.4.0",
40+
"typescript": "^4.7.3"
41+
}
42+
}

unicorn_contracts/samconfig.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
version = 0.1
2+
[default]
3+
[default.deploy]
4+
[default.deploy.parameters]
5+
stack_name = "uni-prop-local-contract"
6+
s3_prefix = "uni-prop-local-contract"
7+
resolve_s3 = true
8+
capabilities = "CAPABILITY_IAM"
9+
image_repositories = []
10+
parameter_overrides = "Stage=\"Local\""
11+
12+
[default.build.parameters]
13+
beta_features = true
14+
15+
[default.sync.parameters]
16+
beta_features = true
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: MIT-0
3+
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
4+
import { DynamoDBClient, GetItemCommand, GetItemCommandInput, PutItemCommand, PutItemCommandInput, PutItemCommandOutput, UpdateItemCommand, UpdateItemCommandInput, UpdateItemCommandOutput } from '@aws-sdk/client-dynamodb';
5+
import { EventBridgeClient, PutEventsCommand, PutEventsCommandInput, PutEventsCommandOutput, PutEventsRequestEntry } from '@aws-sdk/client-eventbridge';
6+
7+
// Empty configuration for DynamoDB
8+
const ddbClient = new DynamoDBClient({});
9+
const DDB_TABLE = process.env.DYNAMODB_TABLE;
10+
11+
// Empty configuration for EventBridge
12+
const eventsClient = new EventBridgeClient({});
13+
const EVENT_BUS = process.env.EVENT_BUS;
14+
15+
// Internal data model
16+
const fields: string[] = [
17+
"address",
18+
"property_id",
19+
"seller_name"
20+
];
21+
22+
// External data types
23+
export const ContractCreatedMetric: string = "ContractCreated";
24+
export const ContractUpdatedMetric: string = "ContractUpdated";
25+
export const ContractEventMetric: string = "ContractEvent";
26+
27+
export type ContractStatusChangedEvent = {
28+
contract_last_modified_on: string;
29+
contract_id: string;
30+
property_id: string;
31+
contract_status: ContractStatusEnum;
32+
}
33+
34+
export type ContractDBType = {
35+
address?: string;
36+
property_id: string;
37+
contract_id?: string;
38+
seller_name?: string;
39+
contract_status: ContractStatusEnum;
40+
contract_created?: string;
41+
contract_last_modified_on?: string;
42+
};
43+
44+
export enum ContractStatusEnum {
45+
DRAFT = 'DRAFT',
46+
APPROVED = 'APPROVED',
47+
};
48+
49+
export interface ContractError extends Error {
50+
propertyId: string;
51+
object?: any;
52+
}
53+
54+
export interface ContractResponse {
55+
propertyId: string;
56+
metadata: any;
57+
}
58+
59+
export function validData(data: any): boolean {
60+
for (let i = 0; i < fields.length; i++) {
61+
let field = fields[i];
62+
if (!(field in data)) {
63+
return false;
64+
}
65+
}
66+
return true;
67+
}
68+
69+
export async function getContractFor(propertyId: string): Promise<ContractDBType | undefined> {
70+
const getItemCommandInput: GetItemCommandInput = {
71+
Key: {'property_id': {S: propertyId}},
72+
ProjectionExpression: 'contract_id, property_id, contract_status, address, seller_name, contract_created, contract_last_modified_on',
73+
TableName: DDB_TABLE
74+
};
75+
76+
const data = await ddbClient.send(new GetItemCommand(getItemCommandInput));
77+
if (data.Item === undefined) {
78+
return undefined;
79+
} else {
80+
const result: ContractDBType = (unmarshall(data.Item) as ContractDBType);
81+
return result;
82+
}
83+
}
84+
85+
export async function updateEntryInDB(dbEntry: ContractDBType): Promise<ContractResponse> {
86+
// Build the Command objects
87+
const ddbUpdateCommandInput: UpdateItemCommandInput = {
88+
TableName: DDB_TABLE,
89+
Key: {property_id: {S: dbEntry.property_id}},
90+
UpdateExpression: 'set contract_status = :t, modified_date = :m',
91+
ExpressionAttributeValues: {
92+
':t': {S: (dbEntry.contract_status as string)},
93+
':m': {S: (dbEntry.contract_last_modified_on as string)}
94+
}
95+
};
96+
const ddbUpdateCommand = new UpdateItemCommand(ddbUpdateCommandInput);
97+
98+
// Send the command
99+
const ddbUpdateCommandOutput: UpdateItemCommandOutput = await ddbClient.send(ddbUpdateCommand);
100+
if (ddbUpdateCommandOutput.$metadata.httpStatusCode != 200) {
101+
const error: ContractError = {
102+
propertyId: dbEntry.property_id,
103+
name: "ContractDBUpdateError",
104+
message: "Response error code: " + ddbUpdateCommandOutput.$metadata.httpStatusCode,
105+
object: ddbUpdateCommandOutput.$metadata
106+
};
107+
throw error;
108+
}
109+
110+
const response: ContractResponse = {
111+
propertyId: dbEntry.property_id,
112+
metadata: ddbUpdateCommandOutput.$metadata
113+
}
114+
return response;
115+
}
116+
117+
export async function saveEntryToDB(dbEntry: ContractDBType): Promise<ContractResponse> {
118+
// Build the Command objects
119+
const ddbPutCommandInput: PutItemCommandInput = {
120+
TableName: DDB_TABLE,
121+
Item: marshall(dbEntry, {removeUndefinedValues: true})
122+
};
123+
const ddbPutCommand = new PutItemCommand(ddbPutCommandInput);
124+
125+
// Send the command
126+
const ddbPutCommandOutput: PutItemCommandOutput = await ddbClient.send(ddbPutCommand);
127+
if (ddbPutCommandOutput.$metadata.httpStatusCode != 200) {
128+
let error: ContractError = {
129+
propertyId: dbEntry.property_id,
130+
name: "ContractDBSaveError",
131+
message: "Response error code: " + ddbPutCommandOutput.$metadata.httpStatusCode,
132+
object: ddbPutCommandOutput.$metadata
133+
};
134+
throw error;
135+
}
136+
137+
let response: ContractResponse = {
138+
propertyId: dbEntry.property_id,
139+
metadata: ddbPutCommandOutput.$metadata
140+
}
141+
return response;
142+
}
143+
144+
export async function fireContractEvent(eventDetail: ContractStatusChangedEvent, source: string, detailType: string): Promise<ContractResponse> {
145+
const propertyId = eventDetail.property_id;
146+
147+
// Build the Command objects
148+
const eventsPutEventsCommandInputEntry: PutEventsRequestEntry = {
149+
EventBusName: EVENT_BUS,
150+
Time: new Date(),
151+
Source: source,
152+
DetailType: detailType,
153+
Detail: JSON.stringify(eventDetail)
154+
};
155+
const eventsPutEventsCommandInput: PutEventsCommandInput = {
156+
Entries: [
157+
eventsPutEventsCommandInputEntry
158+
]
159+
};
160+
const eventsPutEventsCommand = new PutEventsCommand(eventsPutEventsCommandInput);
161+
162+
// Send the command
163+
const eventsPutEventsCommandOutput: PutEventsCommandOutput = await eventsClient.send(eventsPutEventsCommand);
164+
165+
if (eventsPutEventsCommandOutput.$metadata.httpStatusCode != 200) {
166+
let error: ContractError = {
167+
propertyId: propertyId,
168+
name: "ContractEventsError",
169+
message: "Response invalid: " + eventsPutEventsCommandOutput.$metadata.httpStatusCode,
170+
object: eventsPutEventsCommandOutput.$metadata
171+
};
172+
throw error;
173+
174+
}
175+
let response: ContractResponse = {
176+
propertyId: propertyId,
177+
metadata: eventsPutEventsCommandOutput.$metadata
178+
}
179+
return response;
180+
}

0 commit comments

Comments
 (0)