Skip to content

Commit 27aa27d

Browse files
authored
Merge branch 'code-differently:main' into feature/lesson_26
2 parents c0de66f + b012a30 commit 27aa27d

File tree

12 files changed

+5603
-1067
lines changed

12 files changed

+5603
-1067
lines changed

lesson_27/api/jest.config.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/** @type {import('ts-jest').JestConfigWithTsJest} **/
2+
export default {
3+
testEnvironment: 'node',
4+
transform: {
5+
'^.+.tsx?$': ['ts-jest', {useESM: true}],
6+
},
7+
moduleNameMapper: {
8+
'^(\\.\\.?\\/.+)\\.js$': '$1',
9+
},
10+
extensionsToTreatAsEsm: ['.ts'],
11+
};

lesson_27/api/package-lock.json

Lines changed: 5357 additions & 1000 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lesson_27/api/package.json

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,39 @@
22
"name": "codedifferently-web",
33
"version": "1.0.0",
44
"description": "",
5-
"main": "server.js",
5+
"main": "index.js",
6+
"type": "module",
67
"scripts": {
78
"build:deps": "cd ../types && npm install && npm run build",
89
"build": "npm run build:deps && npx tsc",
9-
"start": "npm run build:deps && node dist/server.js",
10-
"dev": "npm run build:deps && nodemon src/server.ts --quiet"
10+
"start": "npm run build:deps && tsx src/index.ts",
11+
"dev": "npm run build:deps && tsx src/index.ts",
12+
"test": "jest",
13+
"test:watch": "jest --watch",
14+
"fix": "prettier --write ."
1115
},
1216
"author": "",
1317
"license": "ISC",
1418
"devDependencies": {
1519
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
16-
"nodemon": "^3.1.0",
17-
"prettier": "3.2.5",
20+
"@types/jest": "^29.5.14",
21+
"@types/supertest": "^6.0.2",
22+
"jest": "^29.7.0",
23+
"nodemon": "^3.1.7",
24+
"prettier": "^3.4.1",
25+
"supertest": "^7.0.0",
26+
"ts-jest": "^29.2.5",
1827
"ts-node-dev": "^2.0.0",
19-
"typescript": "^5.4.4"
28+
"tsx": "^4.19.2",
29+
"typescript": "^5.7.2"
2030
},
2131
"dependencies": {
32+
"@code-differently/types": "file:../types",
2233
"@types/cors": "^2.8.17",
2334
"@types/express": "^4.17.21",
2435
"@types/node": "^20.12.5",
2536
"cors": "^2.8.5",
26-
"express": "^4.19.2"
37+
"express": "^4.21.1",
38+
"lowdb": "^7.0.1"
2739
}
28-
}
40+
}

lesson_27/api/src/data/programs.json

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
1-
[
1+
{
2+
"programs": [
23
{
3-
"id": "28348204-a665-48ea-a436-d962bca07e2d",
4-
"title": "1000 Kids Coding",
5-
"description": "The Code Differently 1000 Kids Coding program was created to expose New Castle County students to computing and programming. The 1000 Kids Coding courses are designed for all experience levels, no experience required."
4+
"id": "28348204-a665-48ea-a436-d962bca07e2d",
5+
"title": "1000 Kids Coding",
6+
"description": "The Code Differently 1000 Kids Coding program was created to expose New Castle County students to computing and programming. The 1000 Kids Coding courses are designed for all experience levels, no experience required."
67
},
78
{
8-
"id": "66521c31-f37d-47b2-9f5e-6ddeb8bc218a",
9-
"title": "Return Ready",
10-
"description": "The Code Differently Workforce Training Initiatives were created to help individuals underrepresented in tech reinvent their skills to align with the changing workforce market. If you are ready to start your tech journey, join our talent community today."
9+
"id": "66521c31-f37d-47b2-9f5e-6ddeb8bc218a",
10+
"title": "Return Ready",
11+
"description": "The Code Differently Workforce Training Initiatives were created to help individuals underrepresented in tech reinvent their skills to align with the changing workforce market. If you are ready to start your tech journey, join our talent community today."
1112
},
1213
{
13-
"id": "516190b1-89cf-4e75-858a-11e728034022",
14-
"title": "Pipeline DevShops",
15-
"description": "Pipeline DevShop is a youth work-based learning program. Youth participants experience working in a real software development environment while sharpening their technology and soft skills."
14+
"id": "516190b1-89cf-4e75-858a-11e728034022",
15+
"title": "Pipeline DevShops",
16+
"description": "Pipeline DevShop is a youth work-based learning program. Youth participants experience working in a real software development environment while sharpening their technology and soft skills."
1617
},
1718
{
18-
"id": "a06f970a-03b7-4cbb-9efd-f4e99029a456",
19-
"title": "Platform Programs",
20-
"description": "Platform programs are designed for high school graduates, college students, career changers, or professionals looking to develop the technology job readiness skills for today’s workforce."
19+
"id": "a06f970a-03b7-4cbb-9efd-f4e99029a456",
20+
"title": "Platform Programs",
21+
"description": "Platform programs are designed for high school graduates, college students, career changers, or professionals looking to develop the technology job readiness skills for today’s workforce."
2122
}
22-
]
23+
]
24+
}

lesson_27/api/src/db.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {Program} from '@code-differently/types';
2+
import {randomUUID} from 'crypto';
3+
import {Low} from 'lowdb';
4+
import {JSONFilePreset} from 'lowdb/node';
5+
6+
export interface Db {
7+
getPrograms: () => Promise<Program[]>;
8+
getProgram: (id: string) => Promise<Program | null>;
9+
addProgram: (program: Program) => Promise<void>;
10+
}
11+
12+
export interface DbData {
13+
programs: Program[];
14+
}
15+
16+
export class DbImpl implements Db {
17+
private readonly db: Promise<Low<DbData>>;
18+
19+
constructor(filePath: string) {
20+
this.db = this.loadDb(filePath);
21+
}
22+
23+
async loadDb(filePath: string): Promise<Low<DbData>> {
24+
const defaultData: DbData = {programs: []};
25+
return await JSONFilePreset(filePath, defaultData);
26+
}
27+
28+
async getPrograms(): Promise<Program[]> {
29+
const db = await this.db;
30+
return db.data.programs;
31+
}
32+
33+
async getProgram(id: string): Promise<Program | null> {
34+
const db = await this.db;
35+
return db.data.programs.find(p => p.id === id) || null;
36+
}
37+
38+
async addProgram(program: Program): Promise<void> {
39+
const db = await this.db;
40+
if (!program.id) {
41+
program.id = randomUUID();
42+
}
43+
db.data.programs.push(program);
44+
await db.write();
45+
}
46+
}

lesson_27/api/src/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import {DbImpl} from './db.js';
2+
import {createServer} from './server.js';
3+
import path from 'path';
4+
5+
const __dirname = path.dirname(new URL(import.meta.url).pathname);
6+
const PROGRAMS_FILE = path.resolve(__dirname, './data/programs.json');
7+
8+
const db = new DbImpl(PROGRAMS_FILE);
9+
createServer(db);

lesson_27/api/src/server.e2e.spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {Db} from './db.js';
2+
import {createServer} from './server.js';
3+
import {Express} from 'express';
4+
import request from 'supertest';
5+
6+
describe('Server (e2e)', () => {
7+
let mockDb: jest.MockedObjectDeep<Db> = jest.mocked({
8+
getPrograms: jest.fn(),
9+
getProgram: jest.fn(),
10+
addProgram: jest.fn(),
11+
});
12+
let app: Express = createServer(mockDb);
13+
14+
it('/programs (GET)', async () => {
15+
// Arrange
16+
const programs = [
17+
{
18+
id: '12345',
19+
title: 'Pipeline DevShops',
20+
description:
21+
'Pipeline DevShop is a youth work-based learning program. Youth participants experience working in a real software development environment while sharpening their technology and soft skills.',
22+
},
23+
];
24+
mockDb.getPrograms.mockResolvedValue(programs);
25+
26+
// Act
27+
const result = request(app).get('/programs');
28+
29+
// Assert
30+
await result.expect(200).then(res => {
31+
expect(res.body).toEqual(programs);
32+
});
33+
});
34+
35+
it('/programs/:id (GET)', async () => {
36+
// Arrange
37+
const program = {
38+
id: 'a06f970a-03b7-4cbb-9efd-f4e99029a456',
39+
title: 'Platform Programs',
40+
description:
41+
'Platform programs are designed for high school graduates, college students, career changers, or professionals looking to develop the technology job readiness skills for today’s workforce.',
42+
};
43+
mockDb.getProgram.mockResolvedValue(program);
44+
45+
// Act
46+
const result = request(app).get(
47+
'/programs/516190b1-89cf-4e75-858a-11e728034022'
48+
);
49+
50+
// Assert
51+
await result.expect(200).then(res => {
52+
expect(res.body).toEqual(program);
53+
});
54+
});
55+
56+
it('/programs (POST)', async () => {
57+
// Arrange
58+
const program = {
59+
title: 'Pipeline DevShops',
60+
description:
61+
'Pipeline DevShop is a youth work-based learning program. Youth participants experience working in a real software development environment while sharpening their technology and soft skills.',
62+
};
63+
mockDb.addProgram.mockResolvedValue();
64+
65+
// Act
66+
const result = request(app).post('/programs').send(program);
67+
68+
// Assert
69+
await result.expect(201).then(() => {
70+
expect(mockDb.addProgram).toHaveBeenCalledWith(program);
71+
});
72+
});
73+
});

lesson_27/api/src/server.ts

Lines changed: 50 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,63 @@
1+
import {Db} from './db.js';
2+
import {Program} from '@code-differently/types';
13
import cors from 'cors';
2-
import fs from 'fs';
34
import express, {Express, Request, Response} from 'express';
4-
import programs from './data/programs.json';
5-
import { randomUUID, UUID } from 'crypto';
6-
import {Program} from '../../types';
75

8-
const PROGRAMS_FILE = './data/programs.json';
9-
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i
6+
const UUID_PATTERN =
7+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
108

11-
const app: Express = express();
9+
export const createServer = (db: Db): Express => {
10+
const app: Express = express();
1211

13-
app.use(express.static('public'));
14-
app.use(express.json())
15-
app.use(express.urlencoded({extended: true}));
16-
app.use(cors());
12+
app.use(express.static('public'));
13+
app.use(express.json());
14+
app.use(express.urlencoded({extended: true}));
15+
app.use(cors());
1716

18-
app.get('/programs/:id', async (req: Request, res: Response<Program>) => {
19-
if (!isUuidValid(req.params.id)) {
20-
res.status(400).send();
21-
return;
22-
}
23-
const program = programs.find(p => p.id === req.params.id);
17+
app.get('/programs/:id', async (req: Request, res: Response<Program>) => {
18+
if (!isUuidValid(req.params.id)) {
19+
res.status(400).send();
20+
return;
21+
}
22+
const program = await db.getProgram(req.params.id);
2423

25-
if (!program) {
26-
res.status(404).send();
27-
return;
28-
}
24+
if (!program) {
25+
res.status(404).send();
26+
return;
27+
}
2928

30-
res.status(200).send(program);
31-
});
29+
res.status(200).send(program);
30+
});
3231

33-
function isUuidValid(uuid: string): boolean {
34-
return !!uuid && !!uuid.match(UUID_PATTERN);
35-
}
32+
function isUuidValid(uuid: string): boolean {
33+
return !!uuid && !!uuid.match(UUID_PATTERN);
34+
}
3635

37-
app.get('/programs', async (req: Request, res: Response<Program[]>) => {
38-
// Send the raw data back to the client as JSON.
39-
res.status(200).send(programs);
40-
});
36+
app.get('/programs', async (req: Request, res: Response<Program[]>) => {
37+
// Send the raw data back to the client as JSON.
38+
const programs = await db.getPrograms();
39+
res.status(200).send(programs);
40+
});
4141

42-
app.post('/programs', async (req: Request<Partial<Program>>, res: Response) => {
43-
const newProgram = req.body;
44-
programs.push({id: randomUUID(), ...newProgram});
45-
fs.writeFile(PROGRAMS_FILE, JSON.stringify(programs, null, 2), (err) => {
46-
if (err) return console.log(err);
47-
console.log(`Updated ${PROGRAMS_FILE}`);
42+
app.post(
43+
'/programs',
44+
async (req: Request<Partial<Program>>, res: Response) => {
45+
const newProgram = req.body;
46+
try {
47+
db.addProgram(newProgram as Program);
48+
} catch (error: unknown) {
49+
res.status(500).send({error: 'Failed to add program.'});
50+
return;
51+
}
52+
console.log('Added new program');
53+
res.status(201).send();
54+
}
55+
);
56+
57+
const port = process.env.port || 4000;
58+
app.listen(port, () => {
59+
console.log(`Server is running on http://localhost:${port}`);
4860
});
49-
});
5061

51-
const port = 4000;
52-
app.listen(port, () => {
53-
console.log(`Server is running on http://localhost:${port}`);
54-
});
62+
return app;
63+
};

lesson_27/api/tsconfig.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"compilerOptions": {
33
"rootDir": "./src",
44
"outDir": "./dist",
5-
"target": "es2016",
6-
"module": "commonjs",
5+
"target": "ES2020",
6+
"module": "NodeNext",
77
"strict": true,
88
"esModuleInterop": true,
99
"skipLibCheck": true,

lesson_27/template/package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)