diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9fba45e..1fe0a3f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,6 +2,13 @@ name: Pipeline on: [push] +env: + POSTGRES_DATABASE: ${{ secrets.POSTGRES_DATABASE }} + POSTGRES_USERNAME: ${{ secrets.POSTGRES_USERNAME }} + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} + POSTGRES_HOST: ${{ secrets.POSTGRES_HOST }} + POSTGRES_PORT: ${{ secrets.POSTGRES_PORT }} + jobs: lint: runs-on: ubuntu-latest @@ -32,12 +39,27 @@ jobs: - run: yarn audit test: runs-on: ubuntu-latest + services: + postgres: + image: postgres:15.3 + env: + POSTGRES_USER: ${{ env.POSTGRES_USERNAME }} + POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }} + POSTGRES_DB: ${{ env.POSTGRES_DATABASE }} + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 18 + - env: + DATABASE_URL: postgres://${{ secrets.POSTGRES_USERNAME }}:${{ secrets.POSTGRES_PASSWORD }}@${{ secrets.POSTGRES_HOST }}:${{ secrets.POSTGRES_PORT }}/${{ secrets.POSTGRES_DATABASE }} + run: | + echo "DATABASE_URL=${DATABASE_URL}" > .env - run: yarn + - run: yarn db:migrate - run: yarn test build: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 2364e6c..10da212 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,4 @@ dist # Custom build +.env diff --git a/README.md b/README.md index afcdaa0..e44f8a6 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,11 @@ It contains basic configurations for the following: - Winston (logging) - GitHub Actions (CI/CD) +# Environment Variables + +- PORT - The port to run the server on +- `DATABASE_URL` - The URL to the database, formatted as `postgres://:@:/` + ## Commands Install dependencies: diff --git a/jest.config.ts b/jest.config.ts index e984a02..2b270f2 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -10,7 +10,8 @@ const config: Config = { testMatch: ['**/*.test.ts'], transform: { '^.+\\.ts$': '@swc/jest' - } + }, + setupFilesAfterEnv: ['./src/utils/clearDatabase.ts'] }; export default config; diff --git a/package.json b/package.json index db5971c..4152750 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,10 @@ "typecheck": "tsc", "format": "prettier --config .prettierrc 'src/**/*.ts' --write", "test": "jest --runInBand --detectOpenHandles", - "prepare": "husky install" + "prepare": "husky install", + "db:generate": "prisma generate", + "db:migrate": "prisma migrate deploy", + "db:reset": "prisma migrate reset --force" }, "lint-staged": { "*.ts": [ @@ -24,11 +27,14 @@ ] }, "dependencies": { + "@prisma/client": "5.1.1", "body-parser": "^1.20.2", "cors": "^2.8.5", + "crypto": "^1.0.1", "express": "^4.18.2", "helmet": "^7.0.0", "jsonschema": "^1.4.1", + "prisma": "^5.1.1", "winston": "^3.10.0", "winston-daily-rotate-file": "^4.7.1" }, diff --git a/prisma/migrations/20230805073126_addition_table/migration.sql b/prisma/migrations/20230805073126_addition_table/migration.sql new file mode 100644 index 0000000..df15ca6 --- /dev/null +++ b/prisma/migrations/20230805073126_addition_table/migration.sql @@ -0,0 +1,8 @@ +-- CreateTable +CREATE TABLE "Addition" ( + "id" TEXT NOT NULL, + "datetime" TIMESTAMP(3) NOT NULL, + "value" INTEGER NOT NULL, + + CONSTRAINT "Addition_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..c759ca1 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,17 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Addition { + id String @id + datetime DateTime + value Int +} \ No newline at end of file diff --git a/src/database/database.test.ts b/src/database/database.test.ts new file mode 100644 index 0000000..309ebf2 --- /dev/null +++ b/src/database/database.test.ts @@ -0,0 +1,13 @@ +import { createAddition, readAddition } from './database'; + +describe('Database', () => { + it('creates a new record in the Addition table and reads it', async () => { + const value = 13; + + const createResult = await createAddition(value); + + const readResult = await readAddition(createResult.id); + expect(readResult).toBeDefined(); + expect(readResult.value).toBe(value); + }); +}); diff --git a/src/database/database.ts b/src/database/database.ts new file mode 100644 index 0000000..f583adb --- /dev/null +++ b/src/database/database.ts @@ -0,0 +1,32 @@ +import { PrismaClient } from '@prisma/client'; +import { randomUUID } from 'crypto'; + +const prisma = new PrismaClient(); + +/** + * Create a new record in the Addition table. + * @param value - The value number to add as a new record. + * @returns Whether the database operation succeeded. + */ +export async function createAddition(value: number) { + return await prisma.addition.create({ + data: { + id: randomUUID(), + datetime: new Date(), + value + } + }); +} + +/** + * Retrieve a record from the Addition table. + * @param id - The id of the record. + * @returns The retrieved record, or an error. + */ +export async function readAddition(id: string) { + return await prisma.addition.findUniqueOrThrow({ + where: { + id + } + }); +} diff --git a/src/index.ts b/src/index.ts index 1835300..38758dd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,7 @@ import server from './server/server'; -server.listen(3000, () => { - console.log('Server running on port 3000'); +const PORT = process.env.PORT || 3000; + +server.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); }); diff --git a/src/server/server.test.ts b/src/server/server.test.ts index 91778fa..d169f95 100644 --- a/src/server/server.test.ts +++ b/src/server/server.test.ts @@ -1,15 +1,9 @@ import requests from 'supertest'; import server from './server'; -beforeAll(() => { - jest.mock('../utils/addition', () => ({ - addition: jest.fn((value: number) => value + 1) - })); -}); - -afterAll(() => { - jest.clearAllMocks(); -}); +jest.mock('../utils/addition', () => ({ + addition: jest.fn((value: number) => value + 1) +})); describe('server', () => { it('returns input value increased by one', async () => { diff --git a/src/server/server.ts b/src/server/server.ts index f82ed0f..0d7b1bb 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -22,14 +22,18 @@ const schema = { required: ['value'] }; -server.post('/', (req: Request, res: Response) => { +server.post('/', async (req: Request, res: Response) => { logger.info(`POST / with ${JSON.stringify(req.body)}`); if (!validator.validate(req.body, schema).valid) { return res.status(400).json({ message: 'Malformed query parameters' }); } + const { value } = req.body; + + const result = await addition(value); + return res.json({ - response: addition(parseInt(req.body.value)) + response: result }); }); diff --git a/src/utils/addition.test.ts b/src/utils/addition.test.ts index 5655e5b..22216d3 100644 --- a/src/utils/addition.test.ts +++ b/src/utils/addition.test.ts @@ -1,8 +1,17 @@ +import { randomUUID } from 'crypto'; import { addition } from './addition'; +jest.mock('../database/database', () => ({ + createAddition: jest.fn((value: number) => ({ + id: randomUUID(), + datetime: new Date(), + value + })) +})); + describe('call', () => { it('returns the input value increased by one', async () => { - const response = addition(3); + const response = await addition(3); expect(response).toBe(4); }); diff --git a/src/utils/addition.ts b/src/utils/addition.ts index 23b0cba..926f9ed 100644 --- a/src/utils/addition.ts +++ b/src/utils/addition.ts @@ -1,11 +1,14 @@ +import { createAddition } from '../database/database'; import { logger } from './logger'; /** - * Format a response message containing a user input. + * Increase the user imput value by one, and creates an Addition record with that value. * @param number - The user input value. * @returns The value increased by one. */ -export function addition(value: number) { +export async function addition(value: number) { logger.info(`addition(${value})`); + await createAddition(value); + return value + 1; } diff --git a/src/utils/clearDatabase.ts b/src/utils/clearDatabase.ts new file mode 100644 index 0000000..810eef4 --- /dev/null +++ b/src/utils/clearDatabase.ts @@ -0,0 +1,14 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +/** + * Clear the database of all records. + */ +export const clearDatabase = async () => { + await prisma.addition.deleteMany({}); +}; + +global.beforeAll(async () => { + await clearDatabase(); +}); diff --git a/yarn.lock b/yarn.lock index 4a7fe9a..e0fbb3b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -846,6 +846,23 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@prisma/client@5.1.1": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.1.1.tgz#ea2b0c8599bdb3f86d92e8df46fba795a744db01" + integrity sha512-fxcCeK5pMQGcgCqCrWsi+I2rpIbk0rAhdrN+ke7f34tIrgPwA68ensrpin+9+fZvuV2OtzHmuipwduSY6HswdA== + dependencies: + "@prisma/engines-version" "5.1.1-1.6a3747c37ff169c90047725a05a6ef02e32ac97e" + +"@prisma/engines-version@5.1.1-1.6a3747c37ff169c90047725a05a6ef02e32ac97e": + version "5.1.1-1.6a3747c37ff169c90047725a05a6ef02e32ac97e" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.1.1-1.6a3747c37ff169c90047725a05a6ef02e32ac97e.tgz#2e8a1f098ec09452dbe00923b24f582f95d1747c" + integrity sha512-owZqbY/wucbr65bXJ/ljrHPgQU5xXTSkmcE/JcbqE1kusuAXV/TLN3/exmz21SZ5rJ7WDkyk70J2G/n68iogbQ== + +"@prisma/engines@5.1.1": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.1.1.tgz#9c26d209f93a563e048eab63b1976f222f1707d0" + integrity sha512-NV/4nVNWFZSJCCIA3HIFJbbDKO/NARc9ej0tX5S9k2EVbkrFJC4Xt9b0u4rNZWL4V+F5LAjvta8vzEUw0rw+HA== + "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz" @@ -1922,6 +1939,11 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +crypto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037" + integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig== + dargs@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz" @@ -3936,6 +3958,13 @@ pretty-format@^29.0.0, pretty-format@^29.6.1: ansi-styles "^5.0.0" react-is "^18.0.0" +prisma@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.1.1.tgz#8f5c0f9467a828746cb94f846d694dc7b7481a9e" + integrity sha512-WJFG/U7sMmcc6TjJTTifTfpI6Wjoh55xl4AzopVwAdyK68L9/ogNo8QQ2cxuUjJf/Wa82z/uhyh3wMzvRIBphg== + dependencies: + "@prisma/engines" "5.1.1" + prompts@^2.0.1: version "2.4.2" resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz"