Skip to content

Commit 17cb3bb

Browse files
committed
Convert into tasks server
1 parent 5c9c8bb commit 17cb3bb

File tree

11 files changed

+195
-55
lines changed

11 files changed

+195
-55
lines changed

packages/backend/src/config.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { readFile } from 'fs/promises';
2+
3+
const log = async (p: Promise<any>) => {
4+
const o = await p;
5+
console.log(o, typeof o);
6+
return o;
7+
};
8+
function pgPassword(env: Record<string, string | undefined> = process.env) {
9+
const password = env.PGPASSWORD;
10+
if (password) return password;
11+
const passwordFile = env.PGPASSWORDFILE;
12+
if (passwordFile)
13+
return () => log(readFile(passwordFile, { encoding: 'utf8' }));
14+
return undefined;
15+
}
16+
17+
export const pgconfig = (env: Record<string, string | undefined>) => ({
18+
host: env.PGHOST || 'localhost',
19+
user: env.PGUSER || 'postgres',
20+
port: parseInt(env.PGPORT || '5432', 10),
21+
database: env.PGDATABASE || 'taskdb',
22+
max: 20,
23+
idleTimeoutMillis: 30000,
24+
connectionTimeoutMillis: 2000,
25+
password: pgPassword(env),
26+
});

packages/backend/src/index.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,38 @@
1+
/* eslint-disable @typescript-eslint/require-await */
2+
import pg from 'pg';
3+
import { pgconfig } from './config.js';
14
import { serverFactory } from './server.js';
25

3-
const PORT = process.env.PORT || process.env.port || 3000;
4-
const server = serverFactory();
5-
66
export { serverFactory };
77

88
if (process.argv[2] === 'serve') {
9+
await import('dotenv').then((d) => d.config());
10+
await serve();
11+
}
12+
13+
async function serve() {
14+
const PORT = process.env.PORT || process.env.port || 3000;
15+
const pool = new pg.Pool(pgconfig(process.env));
16+
17+
const server = serverFactory(pool);
918
server.listen(PORT, () => {
1019
// eslint-disable-next-line no-console
11-
console.log(`Server running at http://localhost:${PORT}/`);
20+
console.log(`Server running: http://localhost:${PORT}/`);
21+
pool
22+
.query('SELECT NOW()')
23+
.then((res) => {
24+
// eslint-disable-next-line no-console
25+
console.log('Connected to database at', res.rows[0].now);
26+
})
27+
.catch((err) => {
28+
// eslint-disable-next-line no-console
29+
console.error(err);
30+
});
31+
});
32+
server.on('close', () => {
33+
pool.end().catch((err) => {
34+
// eslint-disable-next-line no-console
35+
console.error(err);
36+
});
1237
});
1338
}

packages/backend/src/replacer.ts

Lines changed: 0 additions & 16 deletions
This file was deleted.

packages/backend/src/server.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ if (process.env.liveUrl) {
99

1010
let app: Server;
1111

12-
beforeAll(() => {
13-
app = serverFactory();
14-
});
12+
describe('Health', () => {
13+
beforeAll(() => {
14+
app = serverFactory({} as any);
15+
});
1516

16-
describe('Server responds', () => {
1717
// eslint-disable-next-line jest/expect-expect
1818
test('Responds 200', async () => {
1919
await request(app).get('/health').expect(200);
2020
});
21+
// eslint-disable-next-line jest/expect-expect
22+
test('Responds 200 on v1', async () => {
23+
await request(app).get('/v1/health').expect(200);
24+
});
2125
});

packages/backend/src/server.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,23 @@
11
import express from 'express';
22
import { createServer, type Server } from 'http';
3-
import { examples } from 'interface';
4-
import { replace } from './replacer.js';
3+
import type { Pool } from 'pg';
4+
import { healthRouter } from './server/health.js';
5+
import { taskRouter } from './server/tasks.js';
56

6-
// See simple example here
7+
export function serverFactory(client: Pool): Server {
8+
const app = express();
79

8-
const addOneToNumbers = replace(
9-
(p) => p + 1,
10-
(t: number): t is number => typeof t === 'number',
11-
);
10+
const v1Router = express.Router();
1211

13-
let start = examples;
14-
export function serverFactory(): Server {
15-
const app = express();
12+
const health = healthRouter();
13+
14+
const tasks = taskRouter(client);
15+
16+
v1Router.use('/health', health);
17+
v1Router.use('/tasks', tasks);
1618

17-
app.get('/health', (req, res) => {
18-
start = addOneToNumbers(start);
19-
res
20-
.status(200)
21-
.send(`Hello World\r\n${JSON.stringify(start, null, 2)}\r\n`);
22-
});
19+
app.use('/v1', v1Router);
20+
app.use(v1Router);
2321

2422
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- not sure what typescript is missing here
2523
return createServer(app);

packages/backend/src/server/health.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Router } from 'express';
2+
3+
export function healthRouter(router = Router()) {
4+
router.get('/', (req, res) => {
5+
res.status(200).send();
6+
});
7+
return router;
8+
}

packages/backend/src/server/tasks.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { json, Router } from 'express';
2+
import { Task } from 'interface';
3+
import pg from 'pg';
4+
import { z } from 'zod';
5+
6+
const TaskArray = z.array(Task);
7+
8+
export function taskRouter(client: pg.Pool, router = Router()) {
9+
router.use(json());
10+
11+
router.get('/', async (req, res) => {
12+
const response = await client.query(
13+
'SELECT * FROM tasks LIMIT $1::integer',
14+
[req.query.limit || 10],
15+
);
16+
17+
const rows = await TaskArray.parseAsync(response.rows);
18+
19+
res.status(200).json(rows);
20+
});
21+
22+
router.get('/:id', async (req, res) => {
23+
const response = await client.query(
24+
'SELECT * FROM tasks WHERE id = $1::integer',
25+
[req.params.id],
26+
);
27+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
28+
const [element] = response.rows;
29+
if (!element) {
30+
res.status(404).json({ message: 'Not found' });
31+
return;
32+
}
33+
const row = await Task.parseAsync(element);
34+
res.status(200).json(row);
35+
});
36+
37+
router.delete('/:id', async (req, res) => {
38+
const response = await client.query(
39+
'DELETE FROM tasks WHERE id = $1::integer',
40+
[req.params.id],
41+
);
42+
res.status(200).json({ rowsDeleted: response.rowCount });
43+
});
44+
45+
router.post('/:id', async (req, res) => {
46+
const task = await Task.parseAsync(req.body);
47+
const response = await client.query(
48+
'UPDATE tasks SET name = $1::text, etag = etag + 1 WHERE id = $2::integer AND etag = $3::integer RETURNING etag',
49+
[task.name, req.params.id, task.etag],
50+
);
51+
52+
if (response.rowCount === 0) {
53+
res.status(409).json({ message: 'Conflict: etag does not match' });
54+
return;
55+
}
56+
57+
const returnedTask = await Task.parseAsync({
58+
...req.body,
59+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
60+
etag: response.rows[0].etag,
61+
});
62+
res.status(200).json(returnedTask);
63+
});
64+
65+
router.post('/', async (req, res) => {
66+
const task = await Task.parseAsync(req.body);
67+
const result = await client.query(
68+
'INSERT INTO tasks (name, etag) VALUES ($1::text, $2::text) RETURNING id',
69+
[task.name, task.etag],
70+
);
71+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
72+
const response = { ...req.body, id: result.rows[0].id };
73+
res.status(201).send(await Task.parseAsync(response));
74+
});
75+
76+
router.get('/debug/populate', async (req, res) => {
77+
const n = req.query.n;
78+
let amount = 10;
79+
if (typeof n === 'string') {
80+
amount = parseInt(n, 10);
81+
}
82+
83+
const promises = Array(amount)
84+
.fill(0)
85+
.map((_, i) => {
86+
return client.query('INSERT INTO tasks (name) VALUES ($1::text)', [
87+
`Task ${i}`,
88+
]);
89+
});
90+
91+
const count = (await Promise.all(promises))
92+
.map((r) => r.rowCount || 0)
93+
.reduce((a, b) => a + b, 0);
94+
95+
res.status(200).json({ message: 'Populated', count });
96+
});
97+
return router;
98+
}

packages/frontend/src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { examples } from 'interface';
1+
import { exampleTasks } from 'interface';
22
import { useState } from 'react';
33
import './App.css';
44
import reactLogo from './assets/react.svg';
@@ -7,7 +7,7 @@ import viteLogo from '/vite.svg';
77

88
function App() {
99
const [count, setCount] = useState(0);
10-
const [items] = useState(examples);
10+
const [items] = useState(exampleTasks);
1111

1212
return (
1313
<>

packages/frontend/src/Item.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type { Item } from 'interface';
1+
import type { Task } from 'interface';
22

33
interface Props {
4-
items: Item[];
4+
items: Task[];
55
}
66
export default function ItemList({ items }: Props) {
77
const listItems = items.map((item) => (

packages/interface/src/index.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import { z } from 'zod';
22

3-
export const item = z.object({
3+
export const Task = z.object({
44
name: z.string(),
5-
age: z.number(),
6-
id: z.string(),
7-
other: z.string(),
5+
etag: z.number(),
6+
id: z.number(),
87
});
98

10-
export const examples: Item[] = [
11-
{ name: 'Alice', age: 21, id: '1', other: 'foo' },
12-
];
9+
export const exampleTasks: Task[] = [{ name: 'Alice', etag: 1, id: 4 }];
1310

14-
export type Item = z.infer<typeof item>;
11+
export type Task = z.infer<typeof Task>;

0 commit comments

Comments
 (0)