Skip to content

refurbish the sample application with several enhancements #25

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 13 additions & 20 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -20,40 +20,27 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 22

- name: Start LocalStack
uses: LocalStack/setup-localstack@v0.2.2
uses: LocalStack/setup-localstack@v0.2.4
env:
LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }}
DNS_ADDRESS: 0
DEBUG: 1
LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }}
with:
use-pro: 'true'

- name: Install tflocal
run: pip install terraform-local

- name: Install CDK
run: |
npm install -g aws-cdk-local aws-cdk
cdklocal --version

- name: Install dependencies
run: yarn

- name: Prepare Lambda functions
run: yarn build:backend

- name: Bootstrap the infrastructure
run: |
yarn cdklocal bootstrap
sleep 10
run: make install

- name: Deploy the infrastructure
uses: nick-fields/retry@v2
@@ -63,7 +50,13 @@ jobs:
timeout_seconds: 120
retry_wait_seconds: 10
command: |
yarn cdklocal deploy
make deploy

- name: Deploy the frontend
run: make frontend

- name: Run the tests
run: make test

- name: Send a Slack notification
if: failure() || github.event_name != 'pull_request'
@@ -85,7 +78,7 @@ jobs:

- name: Upload the Diagnostic Report
if: failure()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: diagnose.json.gz
path: ./diagnose.json.gz
33 changes: 22 additions & 11 deletions .github/workflows/preview.yml
Original file line number Diff line number Diff line change
@@ -13,10 +13,20 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 20

- name: Install CDK
run: |
npm install -g aws-cdk-local aws-cdk
cdklocal --version

- name: Deploy Preview
uses: LocalStack/setup-localstack@v0.2.2
uses: LocalStack/setup-localstack@endpoint_s3_ephemeral
env:
LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }}
LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
state-backend: ephemeral
@@ -26,14 +36,15 @@ jobs:
# Multi line commands are folding for some reason, so we enforce new lines
# For more predictable usage, use script files
preview-cmd: |
npm install -g aws-cdk-local aws-cdk;
make build;
make bootstrap;
make deploy;
make prepare-frontend-local;
make build-frontend;
make bootstrap-frontend;
make deploy-frontend;
npm i -g yarn;
export NODE_OPTIONS='--tls-min-v1.2 --tls-max-v1.2 --tls-cipher-list="DEFAULT@SECLEVEL=1"'
yarn install;
yarn build:backend;
yarn cdklocal bootstrap;
yarn cdklocal deploy;
yarn prepare:frontend-local;
yarn build:frontend;
yarn cdklocal bootstrap --app="node dist/aws-sdk-js-notes-app-frontend.js";
yarn cdklocal deploy --app="node dist/aws-sdk-js-notes-app-frontend.js";
distributionId=$(awslocal cloudfront list-distributions | jq -r '.DistributionList.Items[0].Id');
echo LS_PREVIEW_URL=$AWS_ENDPOINT_URL/cloudfront/$distributionId/ >> $GITHUB_ENV;

57 changes: 34 additions & 23 deletions Makefile
Original file line number Diff line number Diff line change
@@ -7,39 +7,50 @@ SHELL := /bin/bash
usage:
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'

## Check if all required prerequisites are installed
check:
@command -v docker > /dev/null 2>&1 || { echo "Docker is not installed. Please install Docker and try again."; exit 1; }
@command -v node > /dev/null 2>&1 || { echo "Node.js is not installed. Please install Node.js and try again."; exit 1; }
@command -v aws > /dev/null 2>&1 || { echo "AWS CLI is not installed. Please install AWS CLI and try again."; exit 1; }
@command -v localstack > /dev/null 2>&1 || { echo "LocalStack is not installed. Please install LocalStack and try again."; exit 1; }
@command -v cdk > /dev/null 2>&1 || { echo "CDK is not installed. Please install CDK and try again."; exit 1; }
@command -v cdklocal > /dev/null 2>&1 || { echo "cdklocal is not installed. Please install cdklocal and try again."; exit 1; }
@command -v yarn > /dev/null 2>&1 || { echo "Yarn is not installed. Please install Yarn and try again."; exit 1; }
@command -v aws > /dev/null 2>&1 || { echo "AWS CLI is not installed. Please install AWS CLI and try again."; exit 1; }
@command -v awslocal > /dev/null 2>&1 || { echo "awslocal is not installed. Please install awslocal and try again."; exit 1; }
@echo "All required prerequisites are available."

## Install dependencies
install:
@which localstack || pip install localstack
@which awslocal || pip install awscli-local

# Deploy the infrastructure
build:
yarn && yarn build:backend;
@if [ ! -d "node_modules" ]; then \
echo "node_modules not found. Running yarn install..."; \
yarn install; \
fi
@echo "All required dependencies are available."

bootstrap:
yarn cdklocal bootstrap;
## Build and deploy the frontend
frontend:
yarn prepare:frontend-local
yarn build:frontend
yarn cdklocal bootstrap --app="node dist/aws-sdk-js-notes-app-frontend.js"
yarn cdklocal deploy --app="node dist/aws-sdk-js-notes-app-frontend.js"
@distributionId=$$(awslocal cloudfront list-distributions | jq -r '.DistributionList.Items[0].Id') && \
echo "Access the frontend at: http://localhost:4566/cloudfront/$$distributionId/"

## Deploy the infrastructure
deploy:
yarn build:backend;
yarn cdklocal bootstrap;
yarn cdklocal deploy;

## Run the tests
test:
yarn test

## Start LocalStack in detached mode
start:
localstack start -d

## export configs for web app
prepare-frontend-local:
yarn prepare:frontend-local

build-frontend:
yarn build:frontend

bootstrap-frontend:
yarn cdklocal bootstrap --app="node dist/aws-sdk-js-notes-app-frontend.js";

deploy-frontend:
yarn cdklocal deploy --app="node dist/aws-sdk-js-notes-app-frontend.js";

## Stop the Running LocalStack container
stop:
@echo
@@ -50,8 +61,8 @@ ready:
@echo Waiting on the LocalStack container...
@localstack wait -t 30 && echo LocalStack is ready to use! || (echo Gave up waiting on LocalStack, exiting. && exit 1)

## Save the logs in a separate file, since the LS container will only contain the logs of the last sample run.
## Save the logs in a separate file
logs:
@localstack logs > logs.txt

.PHONY: usage install run start stop ready logs
.PHONY: usage install check start ready deploy frontend logs stop
17 changes: 14 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -13,12 +13,20 @@
"prepare:frontend-local": "node packages/scripts/populate-frontend-config.js --local",
"build:frontend": "cd packages/frontend && yarn build",
"start:frontend": "cd packages/frontend && yarn start",
"serve:frontend": "cd packages/frontend && yarn build && yarn serve"
"serve:frontend": "cd packages/frontend && yarn build && yarn serve",
"test": "jest -c tests/jest.config.js",
"test:watch": "jest -c tests/jest.config.js --watch"
},
"devDependencies": {
"@types/jest": "^29.5.11",
"@types/node": "^18.11.18",
"axios": "^1.6.5",
"husky": "^4.2.3",
"jest": "^29.7.0",
"lint-staged": "^10.1.2",
"prettier": "2.4.1"
"prettier": "2.4.1",
"ts-jest": "^29.1.1",
"typescript": "~4.9.4"
},
"husky": {
"hooks": {
@@ -28,5 +36,8 @@
"lint-staged": {
"*.{ts,js,md}": "prettier --write"
},
"packageManager": "yarn@3.3.1"
"packageManager": "yarn@3.3.1",
"dependencies": {
"glob": "^11.0.1"
}
}
2 changes: 1 addition & 1 deletion packages/backend/build.js
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ buildSync({
minify: true,
outdir: "dist",
platform: "node",
target: "node18",
target: "node22",
mainFields: ["module", "main"],
logLevel: "info",
});
16 changes: 13 additions & 3 deletions packages/frontend/vite.config.ts
Original file line number Diff line number Diff line change
@@ -2,8 +2,8 @@ import { defineConfig, loadEnv } from "vite";
import reactRefresh from "@vitejs/plugin-react-refresh";

// https://vitejs.dev/config/
export default ({mode}) => {
process.env = {...process.env, ...loadEnv(mode, process.cwd())};
export default ({ mode }) => {
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
return defineConfig({
plugins: [reactRefresh()],
base: process.env.VITE_BASE_URL,
@@ -12,5 +12,15 @@ export default ({mode}) => {
"./runtimeConfig": "./runtimeConfig.browser",
},
},
build: {
rollupOptions: {
onwarn(warning, warn) {
if (warning.code === "MODULE_LEVEL_DIRECTIVE") {
return;
}
warn(warning);
},
},
},
});
}
};
43 changes: 24 additions & 19 deletions packages/infra/cdk/aws-sdk-js-notes-app-stack.ts
Original file line number Diff line number Diff line change
@@ -134,30 +134,35 @@ export class AwsSdkJsNotesAppStack extends Stack {
bucketName: "notes-app-frontend",
});

const distribution = new cloudfront.Distribution(this, "WebsiteDistribution", {
defaultRootObject: "index.html",
defaultBehavior: {
origin: new origins.S3Origin(websiteBucket),
},
errorResponses: [
{
httpStatus: 403,
responseHttpStatus: 200,
responsePagePath: '/index.html',
},
{
httpStatus: 404,
responseHttpStatus: 200,
responsePagePath: '/index.html',
const distribution = new cloudfront.Distribution(
this,
"WebsiteDistribution",
{
defaultRootObject: "index.html",
defaultBehavior: {
origin: origins.S3BucketOrigin.withOriginAccessControl(websiteBucket),
},
],
});
errorResponses: [
{
httpStatus: 403,
responseHttpStatus: 200,
responsePagePath: "/index.html",
},
{
httpStatus: 404,
responseHttpStatus: 200,
responsePagePath: "/index.html",
},
],
}
);

new CfnOutput(this, "FilesBucket", { value: filesBucket.bucketName });
new CfnOutput(this, "GatewayId", { value: api.restApiId });
new CfnOutput(this, "IdentityPoolId", { value: identityPool.ref });
new CfnOutput(this, "Region", { value: this.region });
new CfnOutput(this, "FrontendDistributionId", { value: distribution.distributionId });

new CfnOutput(this, "FrontendDistributionId", {
value: distribution.distributionId,
});
}
}
2 changes: 1 addition & 1 deletion packages/infra/cdk/notes-api.ts
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@ export class NotesApi extends Construct {
const { table, grantActions } = props;

this.handler = new lambda.Function(this, "handler", {
runtime: lambda.Runtime.NODEJS_18_X,
runtime: lambda.Runtime.NODEJS_22_X,
handler: "app.handler",
// ToDo: find a better way to pass lambda code
code: lambda.Code.fromAsset(`../backend/dist/${id}`),
4 changes: 2 additions & 2 deletions packages/infra/package.json
Original file line number Diff line number Diff line change
@@ -10,11 +10,11 @@
},
"devDependencies": {
"@types/node": "^18.11.18",
"aws-cdk": "2.59.0",
"aws-cdk": "2.1010.0",
"typescript": "~4.9.4"
},
"dependencies": {
"aws-cdk-lib": "2.59.0",
"aws-cdk-lib": "2.190.0",
"constructs": "10.1.215"
}
}
11 changes: 11 additions & 0 deletions tests/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
roots: ["<rootDir>"],
transform: {
"^.+\\.tsx?$": "ts-jest",
},
testRegex: ".*\\.test\\.tsx?$",
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
testTimeout: 30000, // 30 seconds timeout for API calls
};
94 changes: 94 additions & 0 deletions tests/notes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import axios from "axios";
import { readFileSync } from "fs";
import { join } from "path";

const configFile = join(__dirname, "../packages/frontend/.env");
const envContent = readFileSync(configFile, "utf-8");
const envVars = Object.fromEntries(
envContent
.split("\n")
.filter(Boolean)
.map((line) => line.trim())
.filter((line) => line.includes("="))
.map((line) => line.split("="))
);

const API_URL = `http://localhost:4566/_aws/execute-api/${envVars.VITE_GATEWAY_ID}/prod`;
console.log("API URL:", API_URL);

describe("Notes API Integration Tests", () => {
let noteId: string;

const testNote = {
content: "Test note content",
};

const updatedNote = {
content: "Updated note content",
};

const api = axios.create({
baseURL: API_URL,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
});

test("should create a new note", async () => {
const response = await api.post("/notes", testNote);
expect(response.status).toBe(200);
const data = response.data;
expect(data).toBeDefined();
expect(data.content.S).toBe(testNote.content);
noteId = data.noteId.S;
});

test("should list all notes", async () => {
const response = await api.get("/notes");
expect(response.status).toBe(200);
const data = response.data;
expect(Array.isArray(data)).toBeTruthy();
expect(data.length).toBeGreaterThan(0);
const createdNote = data.find((note: any) => note.noteId === noteId);
expect(createdNote).toBeDefined();
expect(createdNote.content).toBe(testNote.content);
});

test("should get a specific note", async () => {
const response = await api.get(`/notes/${noteId}`);
expect(response.status).toBe(200);
const data = response.data;
expect(data).toBeDefined();
expect(data.noteId).toBe(noteId);
expect(data.content).toBe(testNote.content);
});

test("should update a note", async () => {
const response = await api.put(`/notes/${noteId}`, updatedNote);
expect(response.status).toBe(200);
const data = response.data;
expect(data).toBeDefined();
expect(data.status).toBe(true);

// Verify the update
const getResponse = await api.get(`/notes/${noteId}`);
expect(getResponse.status).toBe(200);
expect(getResponse.data.content).toBe(updatedNote.content);
});

test("should delete a note", async () => {
const response = await api.delete(`/notes/${noteId}`);
expect(response.status).toBe(200);
expect(response.data.status).toBe(true);

// Verify the note is deleted
const getResponse = await api.get(`/notes`);

// Check if the note is not in the list
const data = getResponse.data;
expect(Array.isArray(data)).toBeTruthy();
const deletedNote = data.find((note: any) => note.noteId === noteId);
expect(deletedNote).toBeUndefined();
});
});
15 changes: 15 additions & 0 deletions tests/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"lib": ["es2018"],
"strict": true,
"noImplicitAny": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"types": ["jest", "node"]
},
"include": ["**/*.ts"],
"exclude": ["node_modules"]
}
2,838 changes: 2,707 additions & 131 deletions yarn.lock

Large diffs are not rendered by default.