diff --git a/README.md b/README.md index 112343f7..cc24c506 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,30 @@ This project is an exercise in developing good clean architecture and CI/CD practices. It includes (or will include): -* Infrastructure as Code with [Pulumi](https://www.pulumi.com/), including staging and ephemeral dev environments (with automatic dev environment cleanup). -* CI including linting, unit, integration and mutation tests, and automated end to end tests with [Playwright](https://playwright.dev/). -* A clean architecture, including dependency inversion enforced through linting rules with [eslint-plugin-boundaries](https://github.com/javierbrea/eslint-plugin-boundaries). -* Separate domain, data and application boundaries in the application. Domain logic is completely separated from data access layers using [verified fake implementations](https://github.com/jbrunton/chat-demo/tree/main/services/api/src/data/repositories). -* Continuous deployment pipelines to staging and production environments. -* Automatic documentation of code with [TypeDoc](https://typedoc.org/) deployed using GitHub Pages to [jbrunton.github.io/chat-demo](https://jbrunton.github.io/chat-demo/), and Open API docs deployed along with the api ([chat-demo-api.jbrunton-aws.com/docs](https://chat-demo-api.jbrunton-aws.com/docs)). -* [ ] TODO: Automatic dependency updates (including automerging for minor version changes) with Renovate. +- Infrastructure as Code with [Pulumi](https://www.pulumi.com/), including staging and ephemeral dev environments (with automatic dev environment cleanup). +- CI including linting, unit, integration and mutation tests, and automated end to end tests with [Playwright](https://playwright.dev/). +- A clean architecture, including dependency inversion enforced through linting rules with [eslint-plugin-boundaries](https://github.com/javierbrea/eslint-plugin-boundaries). +- Separate domain, data and application boundaries in the application. Domain logic is completely separated from data access layers using [verified fake implementations](https://github.com/jbrunton/chat-demo/tree/main/services/api/src/data/repositories). +- Continuous deployment pipelines to staging and production environments. +- Automatic documentation of code with [TypeDoc](https://typedoc.org/) deployed using GitHub Pages to [jbrunton.github.io/chat-demo](https://jbrunton.github.io/chat-demo/), and Open API docs deployed along with the api ([chat-demo-api.jbrunton-aws.com/docs](https://chat-demo-api.jbrunton-aws.com/docs)). +- [ ] TODO: Automatic dependency updates (including automerging for minor version changes) with Renovate. ## The demo application The project implements a very basic realtime chat app at [chat-demo.jbrunton-aws.com](https://chat-demo.jbrunton-aws.com). -* From the home screen you can create "rooms". Each room has a unique URL. Anyone connecting to the room can chat using server-sent events. -* The app responds to commands prefixed with `/`. Type `/help` into the chat window to get assistance. +- From the home screen you can create "rooms". Each room has a unique URL. Anyone connecting to the room can chat using server-sent events. +- The app responds to commands prefixed with `/`. Type `/help` into the chat window to get assistance. + +## Local dev + +Clone the repository, and then: + +```console +docker compose up -d +pnpm install +pnpm dev:setup +pnpm dev +``` + +The web client will then be running locally at http://localhost:5173/. diff --git a/chat-demo.code-workspace b/chat-demo.code-workspace new file mode 100644 index 00000000..6b0ee1f6 --- /dev/null +++ b/chat-demo.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "services/api" + }, + { + "path": "client" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/client/package.json b/client/package.json index 7ca5cda0..991a2e3e 100644 --- a/client/package.json +++ b/client/package.json @@ -14,7 +14,8 @@ "lint": "eslint --max-warnings 0 'src/**/*.{ts,tsx}'", "lint:fix": "eslint --max-warnings 0 --fix 'src/**/*.{ts,tsx}'", "format": "prettier --write 'src/**/*.{ts,tsx,css,md}' --config ./.prettierrc", - "test": "vitest" + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "@auth0/auth0-react": "1.12.0", diff --git a/package.json b/package.json index e46ffa10..98be220e 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "test:e2e": "pnpm run --filter e2e test:e2e", "test:e2e:ui": "pnpm run --filter e2e test:e2e:ui", "build": "turbo build", - "dev": "turbo dev" + "dev": "turbo dev", + "dev:setup": "turbo dev:setup", + "dev:teardown": "turbo dev:teardown" }, "devDependencies": { "husky": "^8.0.2", diff --git a/services/api/.eslintrc.js b/services/api/.eslintrc.js index d74a152f..eca4b7cb 100644 --- a/services/api/.eslintrc.js +++ b/services/api/.eslintrc.js @@ -149,6 +149,7 @@ module.exports = { 'src/fixtures/**', '**/__mocks__/**', 'test/**', + 'scripts/**', ], }, }; diff --git a/services/api/package.json b/services/api/package.json index 01ace0d7..9d7ec60c 100644 --- a/services/api/package.json +++ b/services/api/package.json @@ -15,9 +15,12 @@ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "node dist/src/main", "dev": "NODE_ENV=development nest start --watch", + "dev:setup": "pnpm db:reset", + "dev:teardown": "pnpm db:drop", "start:debug": "NODE_ENV=development nest start --debug --watch", "db:init": "NODE_ENV=development ts-node ./scripts/db/init.ts", "db:drop": "NODE_ENV=development ts-node ./scripts/db/drop.ts", + "db:reset": "NODE_ENV=development ts-node ./scripts/db/reset.ts", "db:test:init": "NODE_ENV=test ts-node ./scripts/db/init.ts", "db:test:drop": "NODE_ENV=test ts-node ./scripts/db/drop.ts", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix --max-warnings 0", diff --git a/services/api/scripts/db/reset.ts b/services/api/scripts/db/reset.ts new file mode 100644 index 00000000..5771790b --- /dev/null +++ b/services/api/scripts/db/reset.ts @@ -0,0 +1,12 @@ +import { DynamoDBAdapter } from '@data/adapters/dynamodb/dynamodb.adapter'; +import { RunnableDataModule, runWithContext } from '../runWithContext'; + +runWithContext({ + rootModule: RunnableDataModule, + run: async (app, logger) => { + const db = app.get(DynamoDBAdapter); + await db.destroy(); + await db.create(); + logger.log(`Reset table ${db.tableName}`); + }, +}); diff --git a/services/api/src/data/adapters/dynamodb/dynamodb.adapter.ts b/services/api/src/data/adapters/dynamodb/dynamodb.adapter.ts index bd675007..b3acc9f1 100644 --- a/services/api/src/data/adapters/dynamodb/dynamodb.adapter.ts +++ b/services/api/src/data/adapters/dynamodb/dynamodb.adapter.ts @@ -57,6 +57,13 @@ export class DynamoDBAdapter { async destroy() { this.validateSafeEnv(); + + const exists = await this.table.exists(); + if (!exists) { + console.log(`Table ${this.tableName} does not exist, skipping destroy`); + return; + } + console.log('destroying table:', this.tableName); await this.table.deleteTable('DeleteTableForever'); } diff --git a/turbo.json b/turbo.json index bd85dd31..bb8af273 100644 --- a/turbo.json +++ b/turbo.json @@ -5,12 +5,21 @@ "dependsOn": ["^build"], "outputs": ["dist/**"] }, - "lint": {}, - "test": {}, - "test:int": {}, + "lint": { + "cache": false + }, + "test": { + "cache": false + }, + "test:int": { + "cache": false + }, "dev": { "cache": false, "persistent": true + }, + "dev:setup": { + "cache": false } } }