Skip to content

Commit d83d334

Browse files
feat: added drafts to routes, services, types, and database (#39)
* feat: added drafts to routes, services, types, and database * chore(db): added defaults for drafts table * chore(drafts): cleaned up routes\drafts.ts and updated zod object * chore(drafts): cleaned up draft.ts * chore(drafts): added patch route to drafts.ts and cleaned up code
1 parent 8f6100e commit d83d334

6 files changed

Lines changed: 227 additions & 0 deletions

File tree

app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import express from "express";
22
import ProposalsRouter from "./routes/proposals";
33
import TopicsRouter from "./routes/topics";
4+
import DraftsRouter from "./routes/drafts"
45
import cors from 'cors';
56
const app = express();
67

@@ -9,6 +10,7 @@ app.use(express.json());
910

1011
app.use("/proposals", ProposalsRouter);
1112
app.use("/topics", TopicsRouter);
13+
app.use("/drafts", DraftsRouter);
1214

1315
app.get("/", (req, res) => {
1416
res.send("Hello World!");

database/db_init.sql

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,21 @@ CREATE OR REPLACE TRIGGER set_updated
4141
BEFORE UPDATE ON votes
4242
FOR EACH ROW
4343
EXECUTE PROCEDURE trigger_set_updated();
44+
45+
/*
46+
SETUP DRAFTS TABLE
47+
*/
48+
CREATE TABLE if NOT EXISTS drafts (
49+
title varchar(100) DEFAULT '' NOT NULL,
50+
summary TEXT DEFAULT '' NOT NULL,
51+
description TEXT DEFAULT '' NOT NULL,
52+
type varchar(32) DEFAULT '' NOT NULL,
53+
id SERIAL PRIMARY KEY,
54+
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
55+
updated TIMESTAMP
56+
);
57+
58+
CREATE OR REPLACE TRIGGER set_updated
59+
BEFORE UPDATE ON drafts
60+
FOR EACH ROW
61+
EXECUTE PROCEDURE trigger_set_updated();

routes/drafts.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import express from 'express';
2+
import DraftsService from '../services/drafts';
3+
import { SchemaValidationError } from 'slonik';
4+
import { formatQueryErrorResponse } from '../helpers';
5+
import { PendingDraft, DraftUpdate } from '../types/draft';
6+
7+
const router = express.Router();
8+
9+
router.get("/", async (req, res) => {
10+
const { recordId } = req.query;
11+
try {
12+
if (recordId) {
13+
const draft = await DraftsService.show(recordId as string);
14+
return res.status(200).json(draft);
15+
} else {
16+
const drafts = await DraftsService.index();
17+
return res.status(200).json(drafts);
18+
}
19+
}catch(e){
20+
if (e instanceof SchemaValidationError) {
21+
return res.status(400)
22+
.json({message: 'Error fetching drafts: ' + formatQueryErrorResponse(e)})
23+
}
24+
25+
console.log(e)
26+
return res.status(500).json({message: 'Server Error'})
27+
}
28+
});
29+
30+
router.post("/", async (req, res) => {
31+
const data = req.body;
32+
const validationResult = PendingDraft.safeParse(data);
33+
if(!validationResult.success){
34+
return res.status(422).json({message: 'Invalid data', error: validationResult.error})
35+
}
36+
37+
try{
38+
const draft = await DraftsService.store(data);
39+
return res.status(201).json({ success: true, draft: JSON.stringify(draft)});
40+
}catch(e){
41+
console.log(e)
42+
return res.status(500).json({message: 'Server Error'})
43+
}
44+
});
45+
46+
router.put("/", async (req, res) => {
47+
const { recordId } = req.query;
48+
const data = req.body;
49+
const validationResult = PendingDraft.safeParse(data);
50+
if(!validationResult.success){
51+
return res.status(422).json({message: 'Invalid data', error: validationResult.error})
52+
}
53+
54+
try{
55+
const result = await DraftsService.update(recordId as string, validationResult.data);
56+
res.status(200).json(result);
57+
}catch(e){
58+
console.log(e)
59+
return res.status(500).json({message: 'Server Error'})
60+
}
61+
});
62+
63+
router.patch("/", async (req, res) => {
64+
const { recordId } = req.query;
65+
const data = req.body;
66+
const validationResult = DraftUpdate.safeParse(data);
67+
if(!validationResult.success){
68+
return res.status(422).json({message: 'Invalid data', error: validationResult.error})
69+
}
70+
71+
try{
72+
const result = await DraftsService.update(recordId as string, validationResult.data);
73+
res.status(200).json(result);
74+
}catch(e){
75+
console.log(e)
76+
return res.status(500).json({message: 'Server Error'})
77+
}
78+
});
79+
80+
router.delete("/", async (req, res) => {
81+
const { recordId } = req.query;
82+
try {
83+
const result = await DraftsService.destroy(recordId as string);
84+
return res.status(200).json({count: result.rowCount, rows:result.rows});
85+
}catch(e){
86+
console.log(e)
87+
return res.status(500).json({message: 'Server Error'})
88+
}
89+
});
90+
91+
export default router;

services/drafts.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { getPool } from '../database';
2+
import { sql} from 'slonik';
3+
import {update as slonikUpdate} from 'slonik-utilities';
4+
import { Draft, PendingDraft, DraftUpdate } from '../types/draft';
5+
6+
async function index(): Promise<readonly Draft[]> {
7+
const pool = await getPool();
8+
return await pool.connect(async (connection) => {
9+
const rows = await connection.any(
10+
sql.type(Draft)`
11+
SELECT * FROM drafts ORDER BY id;`)
12+
13+
return rows;
14+
});
15+
}
16+
17+
async function store(data: PendingDraft): Promise<Draft> {
18+
const pool = await getPool();
19+
return await pool.connect(async (connection) => {
20+
const draft = await connection.one(sql.type(Draft)`
21+
INSERT INTO drafts (title, summary, description, type)
22+
VALUES (${data.title}, ${data.summary}, ${data.description}, ${data.type})
23+
RETURNING *;`)
24+
25+
return draft;
26+
});
27+
}
28+
29+
async function show(id: string): Promise<Draft> {
30+
const pool = await getPool();
31+
return await pool.connect(async (connection) => {
32+
const draft = await connection.maybeOne(sql.type(Draft)`
33+
SELECT * FROM drafts WHERE id = ${id};`)
34+
35+
if (!draft) throw new Error('Draft not found');
36+
return draft;
37+
});
38+
}
39+
40+
async function update(id: string, data: DraftUpdate) {
41+
const pool = await getPool();
42+
return await pool.connect(async (connection) => {
43+
return await slonikUpdate(
44+
connection,
45+
'drafts',
46+
data,
47+
{id: parseInt(id)}
48+
)
49+
});
50+
}
51+
52+
async function destroy(id: string) {
53+
const pool = await getPool();
54+
return await pool.connect(async (connection) => {
55+
return await connection.query(sql.unsafe`
56+
DELETE FROM drafts WHERE id = ${id} RETURNING id, title;
57+
`)
58+
});
59+
}
60+
61+
export default {
62+
index,
63+
store,
64+
show,
65+
update,
66+
destroy,
67+
}

tests/drafts.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,25 @@ describe('test suite works', () => {
99
// expect(data).toEqual({ message: "Paginated list of proposals" });
1010
});
1111
})
12+
13+
14+
15+
/*
16+
TEST DRAFT
17+
*/
18+
19+
// res.status(200).json({
20+
// list: [
21+
// {
22+
// title: "Test Draft",
23+
// summary: "This is a test draft",
24+
// description: "A draft for testing",
25+
// type: "Topic",
26+
// id: "1",
27+
// created: "2024-05-12T21:40:26.157Z",
28+
// updated: "2024-05-12T21:40:26.157Z",
29+
// },
30+
// ],
31+
// });
32+
// });
33+

types/draft.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { z } from 'zod'
2+
const Draft = z.object({
3+
title: z.string().max(48),
4+
summary: z.string().max(255),
5+
description: z.string().max(2048),
6+
type: z.enum(['topic', 'project']),
7+
id: z.number().int().positive(),
8+
created: z.number().transform(val => new Date(val)),
9+
updated: z.number().transform(val => new Date(val)).nullable(),
10+
})
11+
12+
type Draft = z.infer<typeof Draft>
13+
14+
const PendingDraft = z.object({
15+
title: z.string().max(48),
16+
summary: z.string().max(255),
17+
description: z.string().max(2048),
18+
type: z.enum(['topic', 'project']),
19+
})
20+
21+
type PendingDraft = z.infer<typeof PendingDraft>
22+
23+
const DraftUpdate = PendingDraft.partial()
24+
25+
type DraftUpdate = z.infer<typeof DraftUpdate>
26+
27+
export { Draft, PendingDraft, DraftUpdate }

0 commit comments

Comments
 (0)