├── .dockerignore ├── .env.example ├── .gitignore ├── Dockerfile ├── jest.config.js ├── license.md ├── package-lock.json ├── package.json ├── postman.json ├── prisma ├── migrations │ ├── 20230324153326_challange │ │ └── migration.sql │ ├── 20230324154329_fix_column_name │ │ └── migration.sql │ ├── 20230324220656_content_and_level │ │ └── migration.sql │ ├── 20230328054941_fix_table_name │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── readme.md ├── scripts └── docker-start.sh ├── src ├── app.test.ts ├── app.ts └── index.ts ├── step_by_step_setup.md └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgres://postgres:password@localhost:5434/postgres 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | .env 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:19 2 | WORKDIR /usr/src/app 3 | COPY package*.json ./ 4 | COPY prisma ./prisma/ 5 | RUN npm ci 6 | COPY . . 7 | RUN npm run build 8 | EXPOSE 3000 9 | CMD ["/bin/sh", "scripts/docker-start.sh"] 10 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | testMatch: ["/src/**/*.test.ts"], 6 | }; 7 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | Copyright © 2023 Marcus Rådell 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lab_ts_backend", 3 | "version": "1.0.0", 4 | "description": "In preparation for a live coding session.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "node build", 9 | "dev-db": "docker run -e POSTGRES_PASSWORD=password -d -p 5434:5432 postgres", 10 | "dev": "npm run build && npm start", 11 | "test": "jest -i" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/marcusradell/lab_ts_backend.git" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/marcusradell/lab_ts_backend/issues" 22 | }, 23 | "homepage": "https://github.com/marcusradell/lab_ts_backend#readme", 24 | "dependencies": { 25 | "@prisma/client": "^4.12.0", 26 | "@types/express": "^4.17.17", 27 | "@types/jest": "^29.5.0", 28 | "@types/supertest": "^2.0.12", 29 | "@types/uuid": "^9.0.1", 30 | "express": "^5.0.0-beta.1", 31 | "jest": "^29.5.0", 32 | "prisma": "^4.12.0", 33 | "supertest": "^6.3.3", 34 | "ts-jest": "^29.0.5", 35 | "typescript": "^4.9.5", 36 | "uuid": "^9.0.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /postman.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "0f5e3f03-b05c-4055-870c-38fe12730d22", 4 | "name": "code_typing", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "Challenges", 10 | "item": [ 11 | { 12 | "name": "Get All", 13 | "request": { 14 | "method": "GET", 15 | "header": [], 16 | "url": { 17 | "raw": "{{base_url}}/challenges", 18 | "host": ["{{base_url}}"], 19 | "path": ["challenges"] 20 | } 21 | }, 22 | "response": [] 23 | }, 24 | { 25 | "name": "Create", 26 | "event": [ 27 | { 28 | "listen": "prerequest", 29 | "script": { 30 | "exec": [""], 31 | "type": "text/javascript" 32 | } 33 | } 34 | ], 35 | "request": { 36 | "method": "POST", 37 | "header": [], 38 | "body": { 39 | "mode": "raw", 40 | "raw": "{\"name\": \"Hello, World!\", \"content\": \"console.log('Hello, World!')\"}", 41 | "options": { 42 | "raw": { 43 | "language": "json" 44 | } 45 | } 46 | }, 47 | "url": { 48 | "raw": "{{base_url}}/challenges", 49 | "host": ["{{base_url}}"], 50 | "path": ["challenges"] 51 | } 52 | }, 53 | "response": [] 54 | }, 55 | { 56 | "name": "Get By ID", 57 | "request": { 58 | "method": "GET", 59 | "header": [], 60 | "url": { 61 | "raw": "{{base_url}}/challenges/fdfb8d69-7572-496c-a8cf-d13752a577ea", 62 | "host": ["{{base_url}}"], 63 | "path": ["challenges", "fdfb8d69-7572-496c-a8cf-d13752a577ea"] 64 | } 65 | }, 66 | "response": [] 67 | }, 68 | { 69 | "name": "Delete", 70 | "request": { 71 | "method": "DELETE", 72 | "header": [], 73 | "url": { 74 | "raw": "{{base_url}}/challenges/fdfb8d69-7572-496c-a8cf-d13752a577ea", 75 | "host": ["{{base_url}}"], 76 | "path": ["challenges", "fdfb8d69-7572-496c-a8cf-d13752a577ea"] 77 | } 78 | }, 79 | "response": [] 80 | } 81 | ] 82 | } 83 | ], 84 | "variable": [ 85 | { 86 | "key": "base_url", 87 | "value": "http://localhost:3000/api" 88 | } 89 | ] 90 | } 91 | -------------------------------------------------------------------------------- /prisma/migrations/20230324153326_challange/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "ChallangeRow" ( 3 | "id" TEXT NOT NULL, 4 | "email" TEXT NOT NULL, 5 | 6 | CONSTRAINT "ChallangeRow_pkey" PRIMARY KEY ("id") 7 | ); 8 | 9 | -- CreateIndex 10 | CREATE UNIQUE INDEX "ChallangeRow_email_key" ON "ChallangeRow"("email"); 11 | -------------------------------------------------------------------------------- /prisma/migrations/20230324154329_fix_column_name/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `email` on the `ChallangeRow` table. All the data in the column will be lost. 5 | - A unique constraint covering the columns `[name]` on the table `ChallangeRow` will be added. If there are existing duplicate values, this will fail. 6 | - Added the required column `name` to the `ChallangeRow` table without a default value. This is not possible if the table is not empty. 7 | 8 | */ 9 | -- DropIndex 10 | DROP INDEX "ChallangeRow_email_key"; 11 | 12 | -- AlterTable 13 | ALTER TABLE "ChallangeRow" DROP COLUMN "email", 14 | ADD COLUMN "name" TEXT NOT NULL; 15 | 16 | -- CreateIndex 17 | CREATE UNIQUE INDEX "ChallangeRow_name_key" ON "ChallangeRow"("name"); 18 | -------------------------------------------------------------------------------- /prisma/migrations/20230324220656_content_and_level/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `content` to the `ChallangeRow` table without a default value. This is not possible if the table is not empty. 5 | - Added the required column `level` to the `ChallangeRow` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "ChallangeRow" ADD COLUMN "content" TEXT NOT NULL, 10 | ADD COLUMN "level" INTEGER NOT NULL; 11 | -------------------------------------------------------------------------------- /prisma/migrations/20230328054941_fix_table_name/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `ChallangeRow` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropTable 8 | DROP TABLE "ChallangeRow"; 9 | 10 | -- CreateTable 11 | CREATE TABLE "ChallengeRow" ( 12 | "id" TEXT NOT NULL, 13 | "name" TEXT NOT NULL, 14 | "content" TEXT NOT NULL, 15 | "level" INTEGER NOT NULL, 16 | 17 | CONSTRAINT "ChallengeRow_pkey" PRIMARY KEY ("id") 18 | ); 19 | 20 | -- CreateIndex 21 | CREATE UNIQUE INDEX "ChallengeRow_name_key" ON "ChallengeRow"("name"); 22 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | previewFeatures = ["jsonProtocol"] 7 | } 8 | 9 | datasource db { 10 | provider = "postgresql" 11 | url = env("DATABASE_URL") 12 | } 13 | 14 | model ChallengeRow { 15 | id String @id 16 | name String @unique 17 | content String 18 | level Int 19 | } 20 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Code Typing 2 | 3 | A speed typing practice tool for programmers. 4 | 5 | _NOTE: this is a laboration for setting up code architecture and does not try to be a complete app._ 6 | 7 | ## Prerequisites 8 | 9 | - `node.js` 10 | - `docker` (optional) 11 | - `postman` (optional) 12 | 13 | ## Setup 14 | 15 | - `npm i` 16 | - `cp .env.example .env` 17 | - `npm run dev-db` 18 | - `npx prisma dev --migrate` 19 | 20 | ## Develop 21 | 22 | `npm run dev` 23 | 24 | ## Deploy 25 | 26 | _Deploy script will only listen to one branch, and needs to be updated in GCP Cloud Build._ 27 | 28 | `git push` 29 | 30 | All development dependencies in `package.json` are included in the production build to keep the `Dockerfile` as simple as possible. 31 | -------------------------------------------------------------------------------- /scripts/docker-start.sh: -------------------------------------------------------------------------------- 1 | ./node_modules/.bin/prisma migrate deploy && node build/index.js -------------------------------------------------------------------------------- /src/app.test.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import request from "supertest"; 3 | import { appFactory } from "./app"; 4 | 5 | const challengeUrl = "/api/challenges"; 6 | 7 | const prismaClient = new PrismaClient(); 8 | 9 | const arrangeApp = async () => { 10 | const deletes = Object.keys(prismaClient) 11 | .filter((key) => !key.startsWith("_") && !key.startsWith("$")) 12 | .map((key) => (prismaClient as any)[key].deleteMany()); 13 | 14 | await Promise.all(deletes); 15 | 16 | return request(appFactory()); 17 | }; 18 | 19 | test("Server is running", async () => { 20 | const response = await request(appFactory()).get("/"); 21 | 22 | expect(response.status).toEqual(200); 23 | }); 24 | 25 | test("Empty list of challenges", async () => { 26 | const app = await arrangeApp(); 27 | 28 | const response = await app.get(challengeUrl); 29 | 30 | expect(response.status).toEqual(200); 31 | expect(response.body).toEqual([]); 32 | }); 33 | 34 | test("Add a challenge", async () => { 35 | const app = await arrangeApp(); 36 | 37 | const data = { 38 | name: "Roman Numerals", 39 | content: `if(number < 1) throw new Error("Roman numerals doesn't support 0 or negative numbers.);`, 40 | }; 41 | 42 | const postResponse = await app.post(challengeUrl).send(data); 43 | 44 | const getResponse = await app.get(challengeUrl); 45 | 46 | const deterministicResult = getResponse.body.map((challenge: any) => { 47 | // Fields level and id are non-deterministic and need to be removed from the object. 48 | const { level, id, ...deterministicResult } = challenge; 49 | 50 | return deterministicResult; 51 | }); 52 | 53 | expect(postResponse.status).toEqual(200); 54 | expect(getResponse.status).toEqual(200); 55 | expect(deterministicResult).toEqual([data]); 56 | }); 57 | 58 | test("Get challenge by ID", async () => { 59 | const app = await arrangeApp(); 60 | 61 | const data = { 62 | name: "Roman Numerals", 63 | content: `if(number < 1) throw new Error("Roman numerals doesn't support 0 or negative numbers.);`, 64 | }; 65 | 66 | const postResponse = await app.post(challengeUrl).send(data); 67 | 68 | const getByIdResponse = await app.get( 69 | `${challengeUrl}/${postResponse.body.id}` 70 | ); 71 | 72 | const { level, id, ...deterministicResult } = getByIdResponse.body; 73 | 74 | expect(getByIdResponse.status).toEqual(200); 75 | expect(deterministicResult).toEqual(data); 76 | }); 77 | 78 | test("Delete challenge", async () => { 79 | const app = await arrangeApp(); 80 | 81 | const data = { 82 | name: "Roman Numerals", 83 | content: `if(number < 1) throw new Error("Roman numerals doesn't support 0 or negative numbers.);`, 84 | }; 85 | 86 | const postResponse = await app.post(challengeUrl).send(data); 87 | 88 | const deleteResponse = await app.delete( 89 | `${challengeUrl}/${postResponse.body.id}` 90 | ); 91 | 92 | const getAllResponse = await app.get(challengeUrl); 93 | 94 | expect(deleteResponse.status).toEqual(200); 95 | expect(getAllResponse.body).toEqual([]); 96 | }); 97 | 98 | test("Add same name twice fails", async () => { 99 | const app = await arrangeApp(); 100 | 101 | const data = { 102 | name: "Keep it DRY", 103 | content: "const x = 5; const y = 5;", 104 | }; 105 | 106 | await app.post("/api/challenges").send(data); 107 | const secondAddResponse = await app.post("/api/challenges").send(data); 108 | 109 | expect(secondAddResponse.status).toEqual(400); 110 | }); 111 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import express from "express"; 3 | import { v4 } from "uuid"; 4 | import path from "path"; 5 | 6 | export const appFactory = () => { 7 | const app = express(); 8 | 9 | app.use(express.json()); 10 | 11 | const prismaClient = new PrismaClient(); 12 | 13 | app.head("/status", (req, res) => { 14 | res.sendStatus(200); 15 | }); 16 | 17 | app.get("/", (req, res) => { 18 | res.sendFile("postman.json", { root: path.resolve(__dirname, "../") }); 19 | }); 20 | 21 | app.get("/api/challenges", async (req, res) => { 22 | res.json(await prismaClient.challengeRow.findMany()); 23 | }); 24 | 25 | app.get("/api/challenges/:id", async (req, res) => { 26 | const id = req.params.id; 27 | 28 | if (typeof id !== "string") { 29 | return res.sendStatus(400); 30 | } 31 | 32 | const challenge = await prismaClient.challengeRow.findUnique({ 33 | where: { id }, 34 | }); 35 | 36 | if (!challenge) return res.sendStatus(400); 37 | 38 | res.json(challenge); 39 | }); 40 | 41 | app.post("/api/challenges", async (req, res) => { 42 | try { 43 | const { name, content } = req.body; 44 | 45 | if (typeof name !== "string" || typeof content !== "string") { 46 | return res.sendStatus(400); 47 | } 48 | 49 | const today = new Date(); 50 | const MONDAY = 1; 51 | let level = 1; 52 | 53 | if (content.length > 100 && content.includes(";")) { 54 | level = 3; 55 | } else if (today.getDay() === MONDAY) { 56 | level = 2; 57 | } 58 | 59 | const id = v4(); 60 | 61 | await prismaClient.challengeRow.create({ 62 | data: { id, name, content, level }, 63 | }); 64 | 65 | res.json({ id }); 66 | } catch (error) { 67 | return res.sendStatus(400); 68 | } 69 | }); 70 | 71 | app.delete("/api/challenges/:id", async (req, res) => { 72 | const id = req.params.id; 73 | 74 | if (typeof id !== "string") return res.sendStatus(400); 75 | 76 | await prismaClient.challengeRow.delete({ where: { id } }); 77 | res.sendStatus(200); 78 | }); 79 | 80 | return app; 81 | }; 82 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { appFactory } from "./app"; 2 | 3 | const port = 3000; 4 | 5 | const main = () => { 6 | appFactory().listen(port, () => { 7 | console.log(`Example app listening on port ${port}`); 8 | }); 9 | }; 10 | 11 | main(); 12 | -------------------------------------------------------------------------------- /step_by_step_setup.md: -------------------------------------------------------------------------------- 1 | # lab_ts_backend 2 | 3 | In preparation for a live coding session. 4 | 5 | Replace values in `{{}}` with your own values. 6 | 7 | ## I want to be able to push changes to GitHub 8 | 9 | _Done when a commit is on the GitHub repository._ 10 | 11 | Go to the web page and create a new repo. 12 | `git clone {{git@github.com:marcusradell/lab_ts_backend.git}}` 13 | 14 | Use VSCode to make a commit. 15 | 16 | ``` 17 | git remote add origin {{git@github.com:marcusradell/lab_ts_backend.git}} 18 | git branch -M main 19 | git push -u origin main 20 | ``` 21 | 22 | Check for the commit on GitHub. 23 | 24 | ## I want to have a working Express app 25 | 26 | _Done when we can visit http://localhost:3000 and see that we got a `Hello World!`._ 27 | 28 | `npm init --yes` 29 | `npm i express` 30 | Add `node_modules/` into .gitignore. 31 | 32 | Paste in the following code from http://expressjs.com/en/starter/hello-world.html into `src/index.js`: 33 | 34 | ``` 35 | const express = require('express') 36 | const app = express() 37 | const port = 3000 38 | 39 | app.get('/', (req, res) => { 40 | res.send('Hello World!') 41 | }) 42 | 43 | app.listen(port, () => { 44 | console.log(`Example app listening on port ${port}`) 45 | }) 46 | ``` 47 | 48 | `node src` 49 | 50 | Check your browser for `Hello World!` when visiting `http:localhost:3000`. 51 | 52 | Commit the code. 53 | 54 | ## I want to use TypeScript 55 | 56 | _Done when I have compiled the code using `tsc`, then run `node build, then check the browser._ 57 | 58 | Rename file into `src/index.ts`. 59 | Use quick fix `cmd+.` to convert require into an import. 60 | Use quick fix to install types for express. 61 | 62 | `npm i typescript` to install typescript and the binary `tsc`. 63 | `npx tsc --init` to create a `tsconfig.json` file. 64 | Configure `tsconfig.json` with `"rootDir": "./src"` and `"outDir": "./build"`. 65 | 66 | `npx tsc` 67 | Make sure it outputs a file with javascript into `build/index.js`. 68 | Add `build/` to .gitignore. 69 | 70 | `node build` 71 | Check `http://localhost:3000. 72 | 73 | Commit! 74 | 75 | ## I want to document the commands as npm scripts 76 | 77 | _Done when `npm run build && npm start` works (check web page)._ 78 | 79 | In `package.json` replace the content in `"script"` with: 80 | 81 | ``` 82 | "build": "tsc", 83 | "start": "node build" 84 | ``` 85 | 86 | Check and commit! 87 | 88 | ## I want to host the code using containerization 89 | 90 | _Done when `docker build -t lab_ts_backend . && docker run -p 3000:3000 --rm -it lab_ts_backend` works and you can visit the web site._ 91 | 92 | Search for `node.js dockerfile`. I got to https://nodejs.org/en/docs/guides/nodejs-docker-webapp. 93 | 94 | Modify the content of `Dockerfile` so it becomes: 95 | 96 | ``` 97 | FROM node:19 98 | WORKDIR /usr/src/app 99 | COPY package*.json ./ 100 | RUN npm ci --only=production 101 | COPY . . 102 | RUN npm run build 103 | EXPOSE 3000 104 | CMD [ "node", "build/" ] 105 | ``` 106 | 107 | Add a `.dockerignore` with `build/` and `node_modules/`. 108 | 109 | Move any `devDependencies` to `dependencies`. This can be undone with a more advanced dockerfile setup. 110 | 111 | Try and see if everything works. 112 | 113 | Commit! 114 | 115 | ## I want to use GCP Cloud Run to host the backend 116 | 117 | _Done when I can visit a Cloud Run URL that hosts the latest version of my app._ 118 | 119 | Login to GCP and create a new project named `lab-ts-backend`. 120 | 121 | Search for `Cloud Run` and create a new service. 122 | 123 | Choose option `Continuously deploy new revisions from a source repository`. 124 | Service name: `app`. 125 | Region: `europe-north1`. 126 | Max instances: `1`. 127 | Authentication: `Allow unauthenticated invocations`. 128 | Container port (under a submenu): `3000`. 129 | 130 | Scroll back up and click on `Set up cloud build`. 131 | Choose `GitHub` as the provider and authenticate. 132 | Choose `lab_ts_backend` as the repository. 133 | Create the Cloud Build. 134 | 135 | Create the service. 136 | 137 | Check results and commit! 138 | 139 | ## I want to serve a list of programming speed typing challenges 140 | 141 | _Done when `curl https://app-6dsti55tsa-lz.a.run.app/challenge/list` returns [{name: "Hello world!"}] in its body as JSON._ 142 | 143 | Add a new route to the app: 144 | 145 | ``` 146 | app.get("/challenge/list", (req, res) => { 147 | res.json([{ name: "Hello World!" }]); 148 | }); 149 | ``` 150 | 151 | Verify locally, push to prod, and verify in prod. 152 | 153 | ## I want to be able to add a new challenge 154 | 155 | _Done when `curl localhost:3000/challenge/add -d '{"name": "Fizzbuzz"}' -H "Content-Type: application/json" -v` returns status code 200 and the list endpoint shows the new challenge._ 156 | 157 | ``` 158 | app.post("/challenge/add", (req, res) => { 159 | const { name } = req.body; 160 | challenges.push({ name }); 161 | res.sendStatus(200); 162 | }); 163 | ``` 164 | 165 | Test and commit! 166 | 167 | _Skipping some instructions for brevity._ 168 | 169 | ## I want to be able to get a challenge by ID 170 | 171 | ``` 172 | curl "localhost:3000/challenge/display?id=6e2929e1-f1b4-460c-ad7f-c5c77ed1b32d" -v 173 | ``` 174 | 175 | ## I want to be able to remove a challenge 176 | 177 | Add the endpoint and filter challenges on ID to remove the specific challenge. That means that we need to add IDs for challenges. 178 | 179 | Install `uuid @types/uuid` and use `v4` to generate new UUIDs. 180 | 181 | ``` 182 | curl "localhost:3000/challenge/remove" -v -d '{"id": "6e2929e1-f1b4-460c-ad7f-c5c77ed1b32d"}' -H "Content-Type: application/json" 183 | ``` 184 | 185 | ## I want to setup Prisma with Postgres 186 | 187 | Add a script for running Postgres via Docker in `package.json`. 188 | 189 | Place a matching `.env.example` entry for the connection string. 190 | 191 | Run `npx prisma init`. 192 | 193 | Replace the `.env` `DATABASE_URL` value with your `.env.example` value (the key should be the same for both). 194 | 195 | `.gitignore` `.env`. 196 | 197 | `prisma/schema.prisma` should contain the following model: 198 | 199 | ``` 200 | model ChallangeRow { 201 | id String @id 202 | email String @unique 203 | } 204 | ``` 205 | 206 | Run `npx prisma migrate dev` to create and apply migrations and update the prisma client. 207 | 208 | _TODO: Add pitch! Spela in en egen promo med datum och pitch!_ 209 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs" /* Specify what module code is generated. */, 29 | "rootDir": "./src" /* Specify the root folder within your source files. */, 30 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | "outDir": "./build" /* Specify an output folder for all emitted files. */, 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 77 | 78 | /* Type Checking */ 79 | "strict": true /* Enable all strict type-checking options. */, 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | } 103 | } 104 | --------------------------------------------------------------------------------