diff --git a/.gitignore b/.gitignore index 97aca2e..ecff565 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .env -node_modules \ No newline at end of file +node_modules +dist diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fd1024c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM node:22-bookworm-slim AS build + +WORKDIR /app + +COPY package.json ./ +RUN npm install + +COPY tsconfig.json ./ +COPY src ./src + +RUN npm run build + +FROM node:22-bookworm-slim AS runtime + +ENV NODE_ENV=production +WORKDIR /app + +COPY package.json ./ +RUN npm install --omit=dev + +COPY --from=build /app/dist ./dist + +EXPOSE 3000 + +CMD ["node", "dist/index.js"] diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..326ebbb --- /dev/null +++ b/compose.yml @@ -0,0 +1,68 @@ +services: + mongo: + image: mongo:7.0.41 + container_name: node-ass1-mongo + restart: unless-stopped + command: ["mongod", "--bind_ip_all"] + environment: + MONGO_INITDB_DATABASE: node_ass1 + volumes: + - mongo_data:/data/db + networks: + - testnet + mem_limit: 2g + cpus: 1.0 + healthcheck: + test: + [ + "CMD", + "mongosh", + "--quiet", + "mongodb://127.0.0.1:27017/node_ass1", + "--eval", + "db.runCommand({ ping: 1 }).ok" + ] + interval: 5s + timeout: 3s + retries: 15 + + app: + build: + context: . + dockerfile: Dockerfile + container_name: node-ass1-app + init: true + environment: + NODE_ENV: production + PORT: 3000 + MONGO_URI: mongodb://mongo:27017/node_ass1 + ports: + - "127.0.0.1:3000:3000" + depends_on: + mongo: + condition: service_healthy + networks: + - testnet + mem_limit: 1g + cpus: 1.5 + + k6: + image: grafana/k6:latest + container_name: node-ass1-k6 + profiles: ["testing"] + working_dir: /scripts + volumes: + - ./k6:/scripts:ro + - ./results:/results + networks: + - testnet + mem_limit: 2g + cpus: 1.0 + entrypoint: ["k6"] + +networks: + testnet: + driver: bridge + +volumes: + mongo_data: diff --git a/k6/booking-race.js b/k6/booking-race.js new file mode 100644 index 0000000..b233453 --- /dev/null +++ b/k6/booking-race.js @@ -0,0 +1,49 @@ +import http from 'k6/http'; +import { check } from 'k6'; +import { Counter } from 'k6/metrics'; + +const seed = JSON.parse(open('./seed-data.json')); +const BASE_URL = __ENV.BASE_URL || 'http://app:3000'; + +const successfulBookings = new Counter('successful_bookings'); + +export const options = { + scenarios: { + concurrent_booking: { + executor: 'per-vu-iterations', + vus: 10, + iterations: 1, + maxDuration: '30s', + }, + }, + thresholds: { + successful_bookings: ['count==1'], + }, +}; + +export default function () { + const session = seed.sessions[0]; + const user = seed.users[__VU - 1]; + + const response = http.post( + `${BASE_URL}/sessions/${session.id}/book`, + JSON.stringify({ userId: user.id }), + { + headers: { 'Content-Type': 'application/json' }, + responseCallback: http.expectedStatuses(200, 400), + } + ); + + if (response.status === 200) { + successfulBookings.add(1); + } + + check(response, { + 'booking returned expected status': (r) => + r.status === 200 || r.status === 400, + }); + + console.log( + `VU ${__VU}: status=${response.status}, session=${session.id}` + ); +} diff --git a/k6/load.js b/k6/load.js new file mode 100644 index 0000000..832170c --- /dev/null +++ b/k6/load.js @@ -0,0 +1,50 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +const BASE_URL = __ENV.BASE_URL || 'http://app:3000'; +const seed = JSON.parse(open('./seed-data.json')); + +export const options = { + stages: [ + { duration: '15s', target: 1 }, + { duration: '20s', target: 3 }, + { duration: '20s', target: 5 }, + { duration: '10s', target: 0 }, + ], + thresholds: { + http_req_failed: ['rate<0.01'], + http_req_duration: ['p(95)<500'], + checks: ['rate>0.99'], + }, +}; + +export default function () { + const user = seed.users[(__VU + __ITER) % seed.users.length]; + const session = + seed.sessions[(__VU + __ITER) % seed.sessions.length]; + + const requests = [ + ['users', `${BASE_URL}/users/`], + ['teachers', `${BASE_URL}/teachers/`], + [ + 'available sessions', + `${BASE_URL}/sessions/available?dateTimestamp=${encodeURIComponent(session.dateTimestamp)}`, + ], + [ + 'user session history', + `${BASE_URL}/sessions/${user.id}/sessions`, + ], + ]; + + for (const [name, url] of requests) { + const response = http.get(url); + + check(response, { + [`${name}: HTTP 200`]: (r) => r.status === 200, + [`${name}: JSON response`]: (r) => + r.headers['Content-Type']?.includes('application/json'), + }); + } + + sleep(1); +} diff --git a/k6/mixed-workload.js b/k6/mixed-workload.js new file mode 100644 index 0000000..506ef6e --- /dev/null +++ b/k6/mixed-workload.js @@ -0,0 +1,127 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; +import { Counter } from 'k6/metrics'; + +const BASE_URL = __ENV.BASE_URL || 'http://app:3000'; +const seed = JSON.parse(open('./seed-data.json')); + +const writeFailures = new Counter('write_failures'); + +export const options = { + stages: [ + { duration: '20s', target: 50 }, + { duration: '20s', target: 100 }, + { duration: '30s', target: 150 }, + { duration: '30s', target: 200 }, + { duration: '30s', target: 250 }, + { duration: '30s', target: 300 }, + { duration: '30s', target: 350 }, + { duration: '30s', target: 400 }, + { duration: '20s', target: 0 }, + ], + thresholds: { + http_req_failed: ['rate<0.02'], + http_req_duration: ['p(95)<500'], + write_failures: ['count==0'], + }, +}; + +export default function () { + const unique = `${__VU}-${__ITER}-${Date.now()}`; + const startTime = new Date(Date.now() + 60 * 60 * 1000); + const endTime = new Date(startTime.getTime() + 30 * 60 * 1000); + + // 1. Insert a new user + const userResponse = http.post( + `${BASE_URL}/users/`, + JSON.stringify({ + fullName: `Load Test ${unique}`, + email: `load-${unique}@example.invalid`, + phone: `9${String(__VU).padStart(1, '0')}${String(__ITER).padStart(8, '0')}`.slice(0, 10), + }), + { headers: { 'Content-Type': 'application/json' }, tags: { name: 'POST /users' } } + ); + + const userOk = check(userResponse, { + 'user inserted (201)': (r) => r.status === 201, + }); + + if (!userOk) { + writeFailures.add(1); + return; + } + + const userId = userResponse.json('result._id'); + + // 2. Insert a session for the existing seeded teacher + const sessionResponse = http.post( + `${BASE_URL}/sessions/`, + JSON.stringify({ + teacherId: seed.teacher.id, + startTime: startTime.toISOString(), + endTime: endTime.toISOString(), + }), + { headers: { 'Content-Type': 'application/json' }, tags: { name: 'POST /sessions' } } + ); + + const sessionOk = check(sessionResponse, { + 'session inserted (201)': (r) => r.status === 201, + }); + + if (!sessionOk) { + writeFailures.add(1); + return; + } + + const sessionId = sessionResponse.json('result._id'); + + // 3. Book the newly created session + const bookingResponse = http.post( + `${BASE_URL}/sessions/${sessionId}/book`, + JSON.stringify({ userId }), + { headers: { 'Content-Type': 'application/json' }, tags: { name: 'POST /sessions/:id/book' } } + ); + + const bookingOk = check(bookingResponse, { + 'session booked (200)': (r) => r.status === 200, + }); + + if (!bookingOk) { + writeFailures.add(1); + return; + } + + // 4. Complete the booked session + const completeResponse = http.patch( + `${BASE_URL}/sessions/${sessionId}/complete`, + null, + { tags: { name: 'PATCH /sessions/:id/complete' } } + ); + + const completeOk = check(completeResponse, { + 'session completed (200)': (r) => r.status === 200, + }); + + if (!completeOk) { + writeFailures.add(1); + return; + } + + // 5. Read user session history + const historyResponse = http.get( + `${BASE_URL}/sessions/${userId}/sessions`, + { tags: { name: 'GET /sessions/:userId/sessions' } } + ); + + check(historyResponse, { + 'user history returned (200)': (r) => r.status === 200, + }); + + sleep(1); +} + +export function handleSummary(data) { + return { + '/results/endpoint-summary.json': JSON.stringify(data, null, 2), + }; +} diff --git a/k6/mixed-workload.js.bak b/k6/mixed-workload.js.bak new file mode 100644 index 0000000..e4152fa --- /dev/null +++ b/k6/mixed-workload.js.bak @@ -0,0 +1,128 @@ + +import http from 'k6/http'; +import { check, sleep } from 'k6'; +import { Counter } from 'k6/metrics'; + +const BASE_URL = __ENV.BASE_URL || 'http://app:3000'; +const seed = JSON.parse(open('./seed-data.json')); + +const writeFailures = new Counter('write_failures'); + +export const options = { + stages: [ + { duration: '20s', target: 50 }, + { duration: '20s', target: 100 }, + { duration: '30s', target: 150 }, + { duration: '30s', target: 200 }, + { duration: '30s', target: 250 }, + { duration: '30s', target: 300 }, + { duration: '30s', target: 350 }, + { duration: '30s', target: 400 }, + { duration: '20s', target: 0 }, + ], + thresholds: { + http_req_failed: ['rate<0.02'], + http_req_duration: ['p(95)<500'], + write_failures: ['count==0'], + }, +}; + +export default function () { + const unique = `${__VU}-${__ITER}-${Date.now()}`; + const startTime = new Date(Date.now() + 60 * 60 * 1000); + const endTime = new Date(startTime.getTime() + 30 * 60 * 1000); + + // 1. Insert a new user + const userResponse = http.post( + `${BASE_URL}/users/`, + JSON.stringify({ + fullName: `Load Test ${unique}`, + email: `load-${unique}@example.invalid`, + phone: `9${String(__VU).padStart(1, '0')}${String(__ITER).padStart(8, '0')}`.slice(0, 10), + }), + { headers: { 'Content-Type': 'application/json' }, tags: { name: 'POST /users' } } + ); + + const userOk = check(userResponse, { + 'user inserted (201)': (r) => r.status === 201, + }); + + if (!userOk) { + writeFailures.add(1); + return; + } + + const userId = userResponse.json('result._id'); + + // 2. Insert a session for the existing seeded teacher + const sessionResponse = http.post( + `${BASE_URL}/sessions/`, + JSON.stringify({ + teacherId: seed.teacher.id, + startTime: startTime.toISOString(), + endTime: endTime.toISOString(), + }), + { headers: { 'Content-Type': 'application/json' }, tags: { name: 'POST /sessions' } } + ); + + const sessionOk = check(sessionResponse, { + 'session inserted (201)': (r) => r.status === 201, + }); + + if (!sessionOk) { + writeFailures.add(1); + return; + } + + const sessionId = sessionResponse.json('result._id'); + + // 3. Book the newly created session + const bookingResponse = http.post( + `${BASE_URL}/sessions/${sessionId}/book`, + JSON.stringify({ userId }), + { headers: { 'Content-Type': 'application/json' }, tags: { name: 'POST /sessions/:id/book' } } + ); + + const bookingOk = check(bookingResponse, { + 'session booked (200)': (r) => r.status === 200, + }); + + if (!bookingOk) { + writeFailures.add(1); + return; + } + + // 4. Complete the booked session + const completeResponse = http.patch( + `${BASE_URL}/sessions/${sessionId}/complete`, + null, + { tags: { name: 'PATCH /sessions/:id/complete' } } + ); + + const completeOk = check(completeResponse, { + 'session completed (200)': (r) => r.status === 200, + }); + + if (!completeOk) { + writeFailures.add(1); + return; + } + + // 5. Read user session history + const historyResponse = http.get( + `${BASE_URL}/sessions/${userId}/sessions`, + { tags: { name: 'GET /sessions/:userId/sessions' } } + ); + + check(historyResponse, { + 'user history returned (200)': (r) => r.status === 200, + }); + + sleep(1); +} + +export function handleSummary(data) { + return { + '/results/endpoint-summary.json': JSON.stringify(data, null, 2), + }; +} diff --git a/k6/mixed-workload.js.before-endpoint-metrics b/k6/mixed-workload.js.before-endpoint-metrics new file mode 100644 index 0000000..47dc01f --- /dev/null +++ b/k6/mixed-workload.js.before-endpoint-metrics @@ -0,0 +1,130 @@ + +import http from 'k6/http'; +import { check, sleep } from 'k6'; +import { Counter } from 'k6/metrics'; +import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.1/index.js'; + +const BASE_URL = __ENV.BASE_URL || 'http://app:3000'; +const seed = JSON.parse(open('./seed-data.json')); + +const writeFailures = new Counter('write_failures'); + +export const options = { + stages: [ + { duration: '20s', target: 100 }, + { duration: '20s', target: 200 }, + { duration: '30s', target: 300 }, + { duration: '30s', target: 400 }, + { duration: '30s', target: 450 }, + { duration: '30s', target: 500 }, + { duration: '30s', target: 550 }, + { duration: '30s', target: 600 }, + { duration: '20s', target: 0 }, + ], + thresholds: { + http_req_failed: ['rate<0.02'], + http_req_duration: ['p(95)<500'], + write_failures: ['count==0'], + }, +}; + +export default function () { + const unique = `${__VU}-${__ITER}-${Date.now()}`; + const startTime = new Date(Date.now() + 60 * 60 * 1000); + const endTime = new Date(startTime.getTime() + 30 * 60 * 1000); + + // 1. Insert a new user + const userResponse = http.post( + `${BASE_URL}/users/`, + JSON.stringify({ + fullName: `Load Test ${unique}`, + email: `load-${unique}@example.invalid`, + phone: `9${String(__VU).padStart(1, '0')}${String(__ITER).padStart(8, '0')}`.slice(0, 10), + }), + { headers: { 'Content-Type': 'application/json' }, tags: { name: 'POST /users' } } + ); + + const userOk = check(userResponse, { + 'user inserted (201)': (r) => r.status === 201, + }); + + if (!userOk) { + writeFailures.add(1); + return; + } + + const userId = userResponse.json('result._id'); + + // 2. Insert a session for the existing seeded teacher + const sessionResponse = http.post( + `${BASE_URL}/sessions/`, + JSON.stringify({ + teacherId: seed.teacher.id, + startTime: startTime.toISOString(), + endTime: endTime.toISOString(), + }), + { headers: { 'Content-Type': 'application/json' }, tags: { name: 'POST /sessions' } } + ); + + const sessionOk = check(sessionResponse, { + 'session inserted (201)': (r) => r.status === 201, + }); + + if (!sessionOk) { + writeFailures.add(1); + return; + } + + const sessionId = sessionResponse.json('result._id'); + + // 3. Book the newly created session + const bookingResponse = http.post( + `${BASE_URL}/sessions/${sessionId}/book`, + JSON.stringify({ userId }), + { headers: { 'Content-Type': 'application/json' }, tags: { name: 'POST /sessions/:id/book' } } + ); + + const bookingOk = check(bookingResponse, { + 'session booked (200)': (r) => r.status === 200, + }); + + if (!bookingOk) { + writeFailures.add(1); + return; + } + + // 4. Complete the booked session + const completeResponse = http.patch( + `${BASE_URL}/sessions/${sessionId}/complete`, + null, + { tags: { name: 'PATCH /sessions/:id/complete' } } + ); + + const completeOk = check(completeResponse, { + 'session completed (200)': (r) => r.status === 200, + }); + + if (!completeOk) { + writeFailures.add(1); + return; + } + + // 5. Read user session history + const historyResponse = http.get( + `${BASE_URL}/sessions/${userId}/sessions`, + { tags: { name: 'GET /sessions/:userId/sessions' } } + ); + + check(historyResponse, { + 'user history returned (200)': (r) => r.status === 200, + }); + + sleep(1); +} + +export function handleSummary(data) { + return { + stdout: textSummary(data, { indent: ' ', enableColors: true }), + '/results/endpoint-summary.json': JSON.stringify(data, null, 2), + }; +} diff --git a/k6/seed-data.json b/k6/seed-data.json new file mode 100644 index 0000000..c946586 --- /dev/null +++ b/k6/seed-data.json @@ -0,0 +1,392 @@ +{ + "generatedAt": "2026-10-21T18:53:24.974Z", + "runId": "1792608804974", + "teacher": { + "id": "6ad90a253d51b696cdc51357", + "email": "teacher-1792608804974@example.invalid" + }, + "users": [ + { + "id": "6ad90a253d51b696cdc51358", + "email": "user-1792608804974-1@example.invalid" + }, + { + "id": "6ad90a253d51b696cdc51359", + "email": "user-1792608804974-2@example.invalid" + }, + { + "id": "6ad90a253d51b696cdc5135a", + "email": "user-1792608804974-3@example.invalid" + }, + { + "id": "6ad90a253d51b696cdc5135b", + "email": "user-1792608804974-4@example.invalid" + }, + { + "id": "6ad90a253d51b696cdc5135c", + "email": "user-1792608804974-5@example.invalid" + }, + { + "id": "6ad90a253d51b696cdc5135d", + "email": "user-1792608804974-6@example.invalid" + }, + { + "id": "6ad90a253d51b696cdc5135e", + "email": "user-1792608804974-7@example.invalid" + }, + { + "id": "6ad90a253d51b696cdc5135f", + "email": "user-1792608804974-8@example.invalid" + }, + { + "id": "6ad90a253d51b696cdc51360", + "email": "user-1792608804974-9@example.invalid" + }, + { + "id": "6ad90a253d51b696cdc51361", + "email": "user-1792608804974-10@example.invalid" + }, + { + "id": "6ad90a253d51b696cdc51362", + "email": "user-1792608804974-11@example.invalid" + }, + { + "id": "6ad90a253d51b696cdc51363", + "email": "user-1792608804974-12@example.invalid" + }, + { + "id": "6ad90a253d51b696cdc51364", + "email": "user-1792608804974-13@example.invalid" + }, + { + "id": "6ad90a253d51b696cdc51365", + "email": "user-1792608804974-14@example.invalid" + }, + { + "id": "6ad90a253d51b696cdc51366", + "email": "user-1792608804974-15@example.invalid" + }, + { + "id": "6ad90a253d51b696cdc51367", + "email": "user-1792608804974-16@example.invalid" + }, + { + "id": "6ad90a253d51b696cdc51368", + "email": "user-1792608804974-17@example.invalid" + }, + { + "id": "6ad90a253d51b696cdc51369", + "email": "user-1792608804974-18@example.invalid" + }, + { + "id": "6ad90a253d51b696cdc5136a", + "email": "user-1792608804974-19@example.invalid" + }, + { + "id": "6ad90a253d51b696cdc5136b", + "email": "user-1792608804974-20@example.invalid" + } + ], + "sessions": [ + { + "id": "6ad90a253d51b696cdc5136c", + "startTime": "2026-10-22T18:53:25.235Z", + "endTime": "2026-10-22T19:23:25.235Z", + "dateTimestamp": "2026-10-22T18:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc5136d", + "startTime": "2026-10-22T19:53:25.235Z", + "endTime": "2026-10-22T20:23:25.235Z", + "dateTimestamp": "2026-10-22T19:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc5136e", + "startTime": "2026-10-22T20:53:25.235Z", + "endTime": "2026-10-22T21:23:25.235Z", + "dateTimestamp": "2026-10-22T20:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc5136f", + "startTime": "2026-10-22T21:53:25.235Z", + "endTime": "2026-10-22T22:23:25.235Z", + "dateTimestamp": "2026-10-22T21:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51370", + "startTime": "2026-10-22T22:53:25.235Z", + "endTime": "2026-10-22T23:23:25.235Z", + "dateTimestamp": "2026-10-22T22:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51371", + "startTime": "2026-10-22T23:53:25.235Z", + "endTime": "2026-10-23T00:23:25.235Z", + "dateTimestamp": "2026-10-22T23:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51372", + "startTime": "2026-10-23T00:53:25.235Z", + "endTime": "2026-10-23T01:23:25.235Z", + "dateTimestamp": "2026-10-23T00:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51373", + "startTime": "2026-10-23T01:53:25.235Z", + "endTime": "2026-10-23T02:23:25.235Z", + "dateTimestamp": "2026-10-23T01:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51374", + "startTime": "2026-10-23T02:53:25.235Z", + "endTime": "2026-10-23T03:23:25.235Z", + "dateTimestamp": "2026-10-23T02:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51375", + "startTime": "2026-10-23T03:53:25.235Z", + "endTime": "2026-10-23T04:23:25.235Z", + "dateTimestamp": "2026-10-23T03:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51376", + "startTime": "2026-10-23T04:53:25.235Z", + "endTime": "2026-10-23T05:23:25.235Z", + "dateTimestamp": "2026-10-23T04:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51377", + "startTime": "2026-10-23T05:53:25.235Z", + "endTime": "2026-10-23T06:23:25.235Z", + "dateTimestamp": "2026-10-23T05:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51378", + "startTime": "2026-10-23T06:53:25.235Z", + "endTime": "2026-10-23T07:23:25.235Z", + "dateTimestamp": "2026-10-23T06:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51379", + "startTime": "2026-10-23T07:53:25.235Z", + "endTime": "2026-10-23T08:23:25.235Z", + "dateTimestamp": "2026-10-23T07:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc5137a", + "startTime": "2026-10-23T08:53:25.235Z", + "endTime": "2026-10-23T09:23:25.235Z", + "dateTimestamp": "2026-10-23T08:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc5137b", + "startTime": "2026-10-23T09:53:25.235Z", + "endTime": "2026-10-23T10:23:25.235Z", + "dateTimestamp": "2026-10-23T09:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc5137c", + "startTime": "2026-10-23T10:53:25.235Z", + "endTime": "2026-10-23T11:23:25.235Z", + "dateTimestamp": "2026-10-23T10:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc5137d", + "startTime": "2026-10-23T11:53:25.235Z", + "endTime": "2026-10-23T12:23:25.235Z", + "dateTimestamp": "2026-10-23T11:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc5137e", + "startTime": "2026-10-23T12:53:25.235Z", + "endTime": "2026-10-23T13:23:25.235Z", + "dateTimestamp": "2026-10-23T12:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc5137f", + "startTime": "2026-10-23T13:53:25.235Z", + "endTime": "2026-10-23T14:23:25.235Z", + "dateTimestamp": "2026-10-23T13:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51380", + "startTime": "2026-10-23T14:53:25.235Z", + "endTime": "2026-10-23T15:23:25.235Z", + "dateTimestamp": "2026-10-23T14:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51381", + "startTime": "2026-10-23T15:53:25.235Z", + "endTime": "2026-10-23T16:23:25.235Z", + "dateTimestamp": "2026-10-23T15:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51382", + "startTime": "2026-10-23T16:53:25.235Z", + "endTime": "2026-10-23T17:23:25.235Z", + "dateTimestamp": "2026-10-23T16:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51383", + "startTime": "2026-10-23T17:53:25.235Z", + "endTime": "2026-10-23T18:23:25.235Z", + "dateTimestamp": "2026-10-23T17:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51384", + "startTime": "2026-10-23T18:53:25.235Z", + "endTime": "2026-10-23T19:23:25.235Z", + "dateTimestamp": "2026-10-23T18:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51385", + "startTime": "2026-10-23T19:53:25.235Z", + "endTime": "2026-10-23T20:23:25.235Z", + "dateTimestamp": "2026-10-23T19:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51386", + "startTime": "2026-10-23T20:53:25.235Z", + "endTime": "2026-10-23T21:23:25.235Z", + "dateTimestamp": "2026-10-23T20:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51387", + "startTime": "2026-10-23T21:53:25.235Z", + "endTime": "2026-10-23T22:23:25.235Z", + "dateTimestamp": "2026-10-23T21:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51388", + "startTime": "2026-10-23T22:53:25.235Z", + "endTime": "2026-10-23T23:23:25.235Z", + "dateTimestamp": "2026-10-23T22:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51389", + "startTime": "2026-10-23T23:53:25.235Z", + "endTime": "2026-10-24T00:23:25.235Z", + "dateTimestamp": "2026-10-23T23:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc5138a", + "startTime": "2026-10-24T00:53:25.235Z", + "endTime": "2026-10-24T01:23:25.235Z", + "dateTimestamp": "2026-10-24T00:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc5138b", + "startTime": "2026-10-24T01:53:25.235Z", + "endTime": "2026-10-24T02:23:25.235Z", + "dateTimestamp": "2026-10-24T01:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc5138c", + "startTime": "2026-10-24T02:53:25.235Z", + "endTime": "2026-10-24T03:23:25.235Z", + "dateTimestamp": "2026-10-24T02:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc5138d", + "startTime": "2026-10-24T03:53:25.235Z", + "endTime": "2026-10-24T04:23:25.235Z", + "dateTimestamp": "2026-10-24T03:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc5138e", + "startTime": "2026-10-24T04:53:25.235Z", + "endTime": "2026-10-24T05:23:25.235Z", + "dateTimestamp": "2026-10-24T04:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc5138f", + "startTime": "2026-10-24T05:53:25.235Z", + "endTime": "2026-10-24T06:23:25.235Z", + "dateTimestamp": "2026-10-24T05:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51390", + "startTime": "2026-10-24T06:53:25.235Z", + "endTime": "2026-10-24T07:23:25.235Z", + "dateTimestamp": "2026-10-24T06:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51391", + "startTime": "2026-10-24T07:53:25.235Z", + "endTime": "2026-10-24T08:23:25.235Z", + "dateTimestamp": "2026-10-24T07:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51392", + "startTime": "2026-10-24T08:53:25.235Z", + "endTime": "2026-10-24T09:23:25.235Z", + "dateTimestamp": "2026-10-24T08:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51393", + "startTime": "2026-10-24T09:53:25.235Z", + "endTime": "2026-10-24T10:23:25.235Z", + "dateTimestamp": "2026-10-24T09:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51394", + "startTime": "2026-10-24T10:53:25.235Z", + "endTime": "2026-10-24T11:23:25.235Z", + "dateTimestamp": "2026-10-24T10:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51395", + "startTime": "2026-10-24T11:53:25.235Z", + "endTime": "2026-10-24T12:23:25.235Z", + "dateTimestamp": "2026-10-24T11:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51396", + "startTime": "2026-10-24T12:53:25.235Z", + "endTime": "2026-10-24T13:23:25.235Z", + "dateTimestamp": "2026-10-24T12:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51397", + "startTime": "2026-10-24T13:53:25.235Z", + "endTime": "2026-10-24T14:23:25.235Z", + "dateTimestamp": "2026-10-24T13:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51398", + "startTime": "2026-10-24T14:53:25.235Z", + "endTime": "2026-10-24T15:23:25.235Z", + "dateTimestamp": "2026-10-24T14:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc51399", + "startTime": "2026-10-24T15:53:25.235Z", + "endTime": "2026-10-24T16:23:25.235Z", + "dateTimestamp": "2026-10-24T15:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc5139a", + "startTime": "2026-10-24T16:53:25.235Z", + "endTime": "2026-10-24T17:23:25.235Z", + "dateTimestamp": "2026-10-24T16:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc5139b", + "startTime": "2026-10-24T17:53:25.235Z", + "endTime": "2026-10-24T18:23:25.235Z", + "dateTimestamp": "2026-10-24T17:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc5139c", + "startTime": "2026-10-24T18:53:25.235Z", + "endTime": "2026-10-24T19:23:25.235Z", + "dateTimestamp": "2026-10-24T18:53:25.235Z" + }, + { + "id": "6ad90a253d51b696cdc5139d", + "startTime": "2026-10-24T19:53:25.235Z", + "endTime": "2026-10-24T20:23:25.235Z", + "dateTimestamp": "2026-10-24T19:53:25.235Z" + } + ] +} diff --git a/k6/smoke.js b/k6/smoke.js new file mode 100644 index 0000000..46f480e --- /dev/null +++ b/k6/smoke.js @@ -0,0 +1,23 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +export const options = { + vus: 1, + iterations: 10, + thresholds: { + http_req_failed: ['rate<0.01'], + http_req_duration: ['p(95)<500'], + }, +}; + +export default function () { + const response = http.get('http://app:3000/users/'); + + check(response, { + 'status is 200': (r) => r.status === 200, + 'response is JSON': (r) => + r.headers['Content-Type']?.includes('application/json'), + }); + + sleep(1); +} diff --git a/k6/stress.js b/k6/stress.js new file mode 100644 index 0000000..8dcc3fb --- /dev/null +++ b/k6/stress.js @@ -0,0 +1,37 @@ +import http from 'k6/http'; +import { check } from 'k6'; + +const BASE_URL = __ENV.BASE_URL || 'http://app:3000'; + +export const options = { + stages: [ + { duration: '20s', target: 5 }, + { duration: '20s', target: 10 }, + { duration: '20s', target: 20 }, + { duration: '20s', target: 30 }, + { duration: '20s', target: 40 }, + { duration: '20s', target: 50 }, + { duration: '15s', target: 0 }, + ], + thresholds: { + http_req_failed: ['rate<0.01'], + http_req_duration: ['p(95)<500'], + checks: ['rate>0.99'], + }, +}; + +export default function () { + const endpoints = [ + '/users/', + '/teachers/', + '/sessions/available?dateTimestamp=2026-10-25T10%3A00%3A00.000Z', + ]; + + for (const endpoint of endpoints) { + const response = http.get(`${BASE_URL}${endpoint}`); + + check(response, { + [`${endpoint}: status 200`]: (r) => r.status === 200, + }); + } +} diff --git a/scripts/seed.mjs b/scripts/seed.mjs new file mode 100644 index 0000000..61d4a95 --- /dev/null +++ b/scripts/seed.mjs @@ -0,0 +1,126 @@ +import { writeFile } from 'node:fs/promises'; + +const BASE_URL = process.env.API_BASE_URL || 'http://localhost:3000'; +const USER_COUNT = Number(process.env.SEED_USERS || 20); +const SESSION_COUNT = Number(process.env.SEED_SESSIONS || 50); + +const runId = `${Date.now()}`; +const generated = { + generatedAt: new Date().toISOString(), + runId, + teacher: null, + users: [], + sessions: [], +}; + +async function request(path, method = 'GET', body) { + const response = await fetch(`${BASE_URL}${path}`, { + method, + headers: { 'Content-Type': 'application/json' }, + ...(body ? { body: JSON.stringify(body) } : {}), + }); + + const text = await response.text(); + let data; + + try { + data = JSON.parse(text); + } catch { + throw new Error(`${method} ${path}: Invalid JSON response: ${text}`); + } + + if (!response.ok) { + throw new Error( + `${method} ${path} failed (${response.status}): ${JSON.stringify(data)}` + ); + } + + return data; +} + +function entityId(data) { + const entity = data.result ?? data.data ?? data; + const id = entity._id ?? entity.id; + + if (!id) { + throw new Error(`Could not find created record ID: ${JSON.stringify(data)}`); + } + + return id; +} + +async function main() { + console.log(`Seeding ${BASE_URL}`); + + const teacherResponse = await request('/teachers/', 'POST', { + fullName: `Load Test Teacher ${runId}`, + email: `teacher-${runId}@example.invalid`, + specialization: 'Mathematics', + experience: '5 years', + }); + + generated.teacher = { + id: entityId(teacherResponse), + email: `teacher-${runId}@example.invalid`, + }; + + for (let i = 1; i <= USER_COUNT; i++) { + const email = `user-${runId}-${i}@example.invalid`; + + const response = await request('/users/', 'POST', { + fullName: `Load Test User ${i}`, + email, + phone: String(9000000000 + i), + }); + + generated.users.push({ + id: entityId(response), + email, + }); + + if (i % 10 === 0 || i === USER_COUNT) { + console.log(`Created ${i}/${USER_COUNT} users`); + } + } + + // Start tomorrow and spread sessions one hour apart. + const firstStart = Date.now() + 24 * 60 * 60 * 1000; + + for (let i = 0; i < SESSION_COUNT; i++) { + const start = new Date(firstStart + i * 60 * 60 * 1000); + const end = new Date(start.getTime() + 30 * 60 * 1000); + + const response = await request('/sessions/', 'POST', { + teacherId: generated.teacher.id, + startTime: start.toISOString(), + endTime: end.toISOString(), + }); + + generated.sessions.push({ + id: entityId(response), + startTime: start.toISOString(), + endTime: end.toISOString(), + dateTimestamp: start.toISOString(), + }); + + if ((i + 1) % 10 === 0 || i + 1 === SESSION_COUNT) { + console.log(`Created ${i + 1}/${SESSION_COUNT} sessions`); + } + } + + await writeFile( + new URL('../k6/seed-data.json', import.meta.url), + JSON.stringify(generated, null, 2) + '\n' + ); + + console.log('\nSeeding completed successfully.'); + console.log(`Teacher: 1`); + console.log(`Users: ${generated.users.length}`); + console.log(`Sessions: ${generated.sessions.length}`); + console.log('Saved IDs to k6/seed-data.json'); +} + +main().catch((error) => { + console.error('\nSeeding failed:', error.message); + process.exitCode = 1; +}); diff --git a/src/Sessions/Schema/sessions.schema.ts b/src/Sessions/Schema/sessions.schema.ts index a382841..7d8804c 100644 --- a/src/Sessions/Schema/sessions.schema.ts +++ b/src/Sessions/Schema/sessions.schema.ts @@ -1,31 +1,34 @@ import mongoose, { Types } from "mongoose"; -const {Schema} = mongoose; +const { Schema } = mongoose; const SessionsSchema = new Schema({ - teacherId: { - type: Types.ObjectId, - required: true - }, - userId: { - type: Types.ObjectId, - required: false - }, - startTime: { - type: Date, - }, - endTime: { - type: Date, - }, - status: { - type: String, - enum: ['BOOKED', 'AVAILABLE', 'COMPLETED'], - default: 'AVAILABLE', - required: true - }, - completedAt: { - type: Date - } -}, {timestamps: true}); + teacherId: { + type: Types.ObjectId, + required: true + }, + userId: { + type: Types.ObjectId, + required: false + }, + startTime: { + type: Date, + }, + endTime: { + type: Date, + }, + status: { + type: String, + enum: ['BOOKED', 'AVAILABLE', 'COMPLETED'], + default: 'AVAILABLE', + required: true + }, + completedAt: { + type: Date + } +}, { timestamps: true }); + +SessionsSchema.index({ userId: 1 }); +SessionsSchema.index({ status: 1, startTime: 1 }); const Sessions = mongoose.model('sessions', SessionsSchema); -export default Sessions; \ No newline at end of file +export default Sessions; diff --git a/src/Sessions/sessions.routes.ts b/src/Sessions/sessions.routes.ts index a1aa364..372c19f 100644 --- a/src/Sessions/sessions.routes.ts +++ b/src/Sessions/sessions.routes.ts @@ -7,116 +7,156 @@ const SessionsRoutes = express.Router(); // Add New Session for teacher -SessionsRoutes.post('/', async(req, res) => { - const {teacherId, startTime, endTime } = req.body; +SessionsRoutes.post('/', async (req, res) => { + const { teacherId, startTime, endTime } = req.body; - //Check for teacher existance - const existingTeacher = await Teachers.findOne({_id: teacherId}); - - console.log('existingTeacher', existingTeacher) - if(!existingTeacher){ - return res.status(400).send({message: 'Teacher Does not exist please provide valid teacherId.'}) - } + //Check for teacher existance + const existingTeacher = await Teachers.findOne({ _id: teacherId }); - // Check endTime must be greater then startTime - if(new Date(endTime) <= new Date(startTime)) { - return res.status(400).send({message: 'End time must be greater then start time.'}) - } + console.log('existingTeacher', existingTeacher) + if (!existingTeacher) { + return res.status(400).send({ message: 'Teacher Does not exist please provide valid teacherId.' }) + } - const newSession = new Sessions({teacherId, startTime, endTime}); - const result = await newSession.save(); + // Check endTime must be greater then startTime + if (new Date(endTime) <= new Date(startTime)) { + return res.status(400).send({ message: 'End time must be greater then start time.' }) + } - if(result) { - return res.status(201).send({message: 'Session Added Successfully', result}) - } + const newSession = new Sessions({ teacherId, startTime, endTime }); + const result = await newSession.save(); + + if (result) { + return res.status(201).send({ message: 'Session Added Successfully', result }) + } }) // Get Available Sessions -SessionsRoutes.get('/available', async(req: any, res) => { - const dateTimestamp = req.query.dateTimestamp; +SessionsRoutes.get('/available', async (req: any, res) => { + const dateTimestamp = req.query.dateTimestamp; - const dateString = dateTimestamp.slice(0, 10); + const dateString = dateTimestamp.slice(0, 10); - // us standard dates we used here for simplicity - const startOfDay = new Date(`${dateString}T00:00:00.000Z`); - const endOfDay = new Date(`${dateString}T23:59:59.999Z`); + // us standard dates we used here for simplicity + const startOfDay = new Date(`${dateString}T00:00:00.000Z`); + const endOfDay = new Date(`${dateString}T23:59:59.999Z`); - const availableSessions = await Sessions.aggregate([ - { - $match: { - startTime: { $lte: endOfDay }, - endTime: { $gte: startOfDay }, - status: "AVAILABLE", - } - } - ]); + const availableSessions = await Sessions.aggregate([ + { + $match: { + startTime: { $lte: endOfDay }, + endTime: { $gte: startOfDay }, + status: "AVAILABLE", + } + } + ]); - return res.status(200).send({message: 'Available Session', availableSessions}) + return res.status(200).send({ message: 'Available Session', availableSessions }) }) // Book Available Sessions -SessionsRoutes.post('/:id/book', async(req: any, res) => { - - const {id} = req.params; - const {userId} = req.body; +// SessionsRoutes.post('/:id/book', async(req: any, res) => { +// +// const {id} = req.params; +// const {userId} = req.body; +// +// const existingSession = await Sessions.findOne({_id: id}); +// +// if(!existingSession) { +// return res.status(400).send({message: 'Session does not exist.'}) +// } +// if(existingSession.status !== 'AVAILABLE') { +// return res.status(400).send({message: 'This session is not Available'}); +// } +// +// const result = await Sessions.findByIdAndUpdate( +// id, +// { status: "BOOKED", userId: userId }, +// { new: true } +// ); +// +// // console.log('re-------', result) +// +// if(result?.status === 'BOOKED') { +// return res.status(200).send({message: 'Session Booked', result}) +// } else { +// return res.status(400).send({message: 'Error during Booking session.'}) +// } +// +// +// +// }) - const existingSession = await Sessions.findOne({_id: id}); - - if(!existingSession) { - return res.status(400).send({message: 'Session does not exist.'}) - } - if(existingSession.status !== 'AVAILABLE') { - return res.status(400).send({message: 'This session is not Available'}); +// Book Available Sessions atomically +SessionsRoutes.post('/:id/book', async (req: any, res) => { + const { id } = req.params; + const { userId } = req.body; + + // Atomically book only if the session is still AVAILABLE + const result = await Sessions.findOneAndUpdate( + { _id: id, status: 'AVAILABLE' }, + { + $set: { + status: 'BOOKED', + userId: userId + } + }, + { + new: true, + runValidators: true } + ); - const result = await Sessions.findByIdAndUpdate( - id, - { status: "BOOKED", userId: userId }, - { new: true } - ); + if (result) { + return res.status(200).send({ + message: 'Session Booked', + result + }); + } - // console.log('re-------', result) + // Determine why the booking failed + const existingSession = await Sessions.findById(id); - if(result?.status === 'BOOKED') { - return res.status(200).send({message: 'Session Booked', result}) - } else { - return res.status(400).send({message: 'Error during Booking session.'}) - } - - - -}) + if (!existingSession) { + return res.status(400).send({ + message: 'Session does not exist.' + }); + } + return res.status(400).send({ + message: 'This session is not Available' + }); +}); // Update Available Sessions To Complete : User Id also needed to update -SessionsRoutes.patch('/:id/complete', async(req: any, res) => { - - const {id} = req.params; +SessionsRoutes.patch('/:id/complete', async (req: any, res) => { - const existingSession = await Sessions.findOne({_id: id, }); - - if(!existingSession) { - return res.status(400).send({message: 'Session does not exist.'}) - } - if(existingSession.status !== 'BOOKED') { - return res.status(400).send({message: 'This session is not BOOKED'}); - } + const { id } = req.params; - const result = await Sessions.findByIdAndUpdate( - id, - { status: "COMPLETED", completedAt: Date.now() }, - { new: true } - ); + const existingSession = await Sessions.findOne({ _id: id, }); - // console.log('re-------', result) + if (!existingSession) { + return res.status(400).send({ message: 'Session does not exist.' }) + } + if (existingSession.status !== 'BOOKED') { + return res.status(400).send({ message: 'This session is not BOOKED' }); + } - if(result?.status === 'COMPLETED') { - return res.status(200).send({message: 'Session Marked Completed', result}) - } else { - return res.status(400).send({message: 'Error during updating session.'}) - } + const result = await Sessions.findByIdAndUpdate( + id, + { status: "COMPLETED", completedAt: Date.now() }, + { new: true } + ); + + // console.log('re-------', result) + + if (result?.status === 'COMPLETED') { + return res.status(200).send({ message: 'Session Marked Completed', result }) + } else { + return res.status(400).send({ message: 'Error during updating session.' }) + } @@ -124,48 +164,48 @@ SessionsRoutes.patch('/:id/complete', async(req: any, res) => { // Get Sessions for User : With Seperation -SessionsRoutes.get('/:id/sessions', async(req,res) => { +SessionsRoutes.get('/:id/sessions', async (req, res) => { - let userId = new Types.ObjectId(req.params.id); - console.log('userId', userId) + let userId = new Types.ObjectId(req.params.id); + console.log('userId', userId) - const result = await Sessions.aggregate([ + const result = await Sessions.aggregate([ { - $match: { + $match: { userId: userId - } + } }, { - $facet: { - upcomingSessions: [ - { - $match: { - status: 'BOOKED' - } - } - ], - completedSessions: [ - { - $match: { - status: 'COMPLETED' - } - } - ] - } + $facet: { + upcomingSessions: [ + { + $match: { + status: 'BOOKED' + } + } + ], + completedSessions: [ + { + $match: { + status: 'COMPLETED' + } + } + ] + } } - ]); + ]); - res.status(200).json({ + res.status(200).json({ success: true, data: result[0] ?? { - upcomingSessions: [], - completedSessions: [] + upcomingSessions: [], + completedSessions: [] } - }); + }); }) -export default SessionsRoutes; \ No newline at end of file +export default SessionsRoutes; diff --git a/src/Users/Schema/users.schema.ts b/src/Users/Schema/users.schema.ts index 8e9dd29..4055a37 100644 --- a/src/Users/Schema/users.schema.ts +++ b/src/Users/Schema/users.schema.ts @@ -1,18 +1,18 @@ import mongoose from "mongoose"; -const {Schema} = mongoose; +const { Schema } = mongoose; const UsersSchema = new Schema({ - fullName: { - type: String, - }, - email: { - type: String, - unique: true, - }, - phone: { - type: String, - }, -}, {timestamps: true}); + fullName: { + type: String, + }, + email: { + type: String, + unique: true, + }, + phone: { + type: String, + }, +}, { timestamps: true }); const Users = mongoose.model('users', UsersSchema); -export default Users; \ No newline at end of file +export default Users; diff --git a/src/Users/users.routes.ts b/src/Users/users.routes.ts index 9723e04..040f147 100644 --- a/src/Users/users.routes.ts +++ b/src/Users/users.routes.ts @@ -1,34 +1,45 @@ import express from 'express'; -import Users from './Schema/users.schema.js' - +import Users from './Schema/users.schema.js'; +import mongoose from 'mongoose'; const UsersRoutes = express.Router(); -// Get All Users -UsersRoutes.get('/', async(req, res) => { +// Get all users +UsersRoutes.get('/', async (_req, res, next) => { + try { const users = await Users.find(); - return res.status(200).send({'messesage': 'success', data: {users}}) -}) + return res.status(200).send({ + message: 'success', + data: { users } + }); + } catch (error) { + next(error); + } +}); -// Add New User -UsersRoutes.post('/', async(req, res) => { - const {fullName, email, phone} = req.body; +// Add a new user +UsersRoutes.post('/', async (req, res, next) => { + try { + const { fullName, email, phone } = req.body; - //Check for email uniqeness - const existingUser = await Users.find({email}); - console.log('existingUser', existingUser) - if(existingUser.length){ - return res.status(400).send({message: 'User already exist with this email. Please Use Correct Email Address.'}) + const result = await Users.create({ fullName, email, phone }); + + return res.status(201).send({ + message: 'User added successfully', + result + }); + } catch (error: unknown) { + if ( + error instanceof mongoose.mongo.MongoServerError && + error.code === 11000 + ) { + return res.status(400).send({ + message: 'A user already exists with this email.' + }); } - const newTeacher = new Users({fullName, email, phone }); - const result = await newTeacher.save(); + next(error); + } +}); - if(result) { - return res.status(201).send({message: 'Teacher Added Successfully', result}) - } - -}) - - -export default UsersRoutes; \ No newline at end of file +export default UsersRoutes; diff --git a/tsconfig.json b/tsconfig.json index cec4a3a..bdbad8c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,8 +2,8 @@ // Visit https://aka.ms/tsconfig to read more about this file "compilerOptions": { // File Layout - // "rootDir": "./src", - // "outDir": "./dist", + "rootDir": "./src", + "outDir": "./dist", // Environment Settings // See also https://aka.ms/tsconfig/module