From d83d334624f8fa09755c9d52b53e7e6ede024b4a Mon Sep 17 00:00:00 2001 From: rbueno <133924558+randomontherun@users.noreply.github.com> Date: Tue, 14 May 2024 21:33:44 -0500 Subject: [PATCH] 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 --- app.ts | 2 + database/db_init.sql | 18 +++++++++ routes/drafts.ts | 91 ++++++++++++++++++++++++++++++++++++++++++++ services/drafts.ts | 67 ++++++++++++++++++++++++++++++++ tests/drafts.test.ts | 22 +++++++++++ types/draft.ts | 27 +++++++++++++ 6 files changed, 227 insertions(+) create mode 100644 routes/drafts.ts create mode 100644 services/drafts.ts create mode 100644 types/draft.ts diff --git a/app.ts b/app.ts index da475a6..c58dda2 100644 --- a/app.ts +++ b/app.ts @@ -1,6 +1,7 @@ import express from "express"; import ProposalsRouter from "./routes/proposals"; import TopicsRouter from "./routes/topics"; +import DraftsRouter from "./routes/drafts" import cors from 'cors'; const app = express(); @@ -9,6 +10,7 @@ app.use(express.json()); app.use("/proposals", ProposalsRouter); app.use("/topics", TopicsRouter); +app.use("/drafts", DraftsRouter); app.get("/", (req, res) => { res.send("Hello World!"); diff --git a/database/db_init.sql b/database/db_init.sql index ec9dea5..16db245 100644 --- a/database/db_init.sql +++ b/database/db_init.sql @@ -41,3 +41,21 @@ CREATE OR REPLACE TRIGGER set_updated BEFORE UPDATE ON votes FOR EACH ROW EXECUTE PROCEDURE trigger_set_updated(); + +/* +SETUP DRAFTS TABLE +*/ +CREATE TABLE if NOT EXISTS drafts ( + title varchar(100) DEFAULT '' NOT NULL, + summary TEXT DEFAULT '' NOT NULL, + description TEXT DEFAULT '' NOT NULL, + type varchar(32) DEFAULT '' NOT NULL, + id SERIAL PRIMARY KEY, + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated TIMESTAMP +); + +CREATE OR REPLACE TRIGGER set_updated +BEFORE UPDATE ON drafts +FOR EACH ROW +EXECUTE PROCEDURE trigger_set_updated(); diff --git a/routes/drafts.ts b/routes/drafts.ts new file mode 100644 index 0000000..c756600 --- /dev/null +++ b/routes/drafts.ts @@ -0,0 +1,91 @@ +import express from 'express'; +import DraftsService from '../services/drafts'; +import { SchemaValidationError } from 'slonik'; +import { formatQueryErrorResponse } from '../helpers'; +import { PendingDraft, DraftUpdate } from '../types/draft'; + +const router = express.Router(); + +router.get("/", async (req, res) => { + const { recordId } = req.query; + try { + if (recordId) { + const draft = await DraftsService.show(recordId as string); + return res.status(200).json(draft); + } else { + const drafts = await DraftsService.index(); + return res.status(200).json(drafts); + } + }catch(e){ + if (e instanceof SchemaValidationError) { + return res.status(400) + .json({message: 'Error fetching drafts: ' + formatQueryErrorResponse(e)}) + } + + console.log(e) + return res.status(500).json({message: 'Server Error'}) + } +}); + +router.post("/", async (req, res) => { + const data = req.body; + const validationResult = PendingDraft.safeParse(data); + if(!validationResult.success){ + return res.status(422).json({message: 'Invalid data', error: validationResult.error}) + } + + try{ + const draft = await DraftsService.store(data); + return res.status(201).json({ success: true, draft: JSON.stringify(draft)}); + }catch(e){ + console.log(e) + return res.status(500).json({message: 'Server Error'}) + } +}); + +router.put("/", async (req, res) => { + const { recordId } = req.query; + const data = req.body; + const validationResult = PendingDraft.safeParse(data); + if(!validationResult.success){ + return res.status(422).json({message: 'Invalid data', error: validationResult.error}) + } + + try{ + const result = await DraftsService.update(recordId as string, validationResult.data); + res.status(200).json(result); + }catch(e){ + console.log(e) + return res.status(500).json({message: 'Server Error'}) + } +}); + +router.patch("/", async (req, res) => { + const { recordId } = req.query; + const data = req.body; + const validationResult = DraftUpdate.safeParse(data); + if(!validationResult.success){ + return res.status(422).json({message: 'Invalid data', error: validationResult.error}) + } + + try{ + const result = await DraftsService.update(recordId as string, validationResult.data); + res.status(200).json(result); + }catch(e){ + console.log(e) + return res.status(500).json({message: 'Server Error'}) + } +}); + +router.delete("/", async (req, res) => { + const { recordId } = req.query; + try { + const result = await DraftsService.destroy(recordId as string); + return res.status(200).json({count: result.rowCount, rows:result.rows}); + }catch(e){ + console.log(e) + return res.status(500).json({message: 'Server Error'}) + } +}); + +export default router; diff --git a/services/drafts.ts b/services/drafts.ts new file mode 100644 index 0000000..1d4a598 --- /dev/null +++ b/services/drafts.ts @@ -0,0 +1,67 @@ +import { getPool } from '../database'; +import { sql} from 'slonik'; +import {update as slonikUpdate} from 'slonik-utilities'; +import { Draft, PendingDraft, DraftUpdate } from '../types/draft'; + +async function index(): Promise { + const pool = await getPool(); + return await pool.connect(async (connection) => { + const rows = await connection.any( + sql.type(Draft)` + SELECT * FROM drafts ORDER BY id;`) + + return rows; + }); +} + +async function store(data: PendingDraft): Promise { + const pool = await getPool(); + return await pool.connect(async (connection) => { + const draft = await connection.one(sql.type(Draft)` + INSERT INTO drafts (title, summary, description, type) + VALUES (${data.title}, ${data.summary}, ${data.description}, ${data.type}) + RETURNING *;`) + + return draft; + }); +} + +async function show(id: string): Promise { + const pool = await getPool(); + return await pool.connect(async (connection) => { + const draft = await connection.maybeOne(sql.type(Draft)` + SELECT * FROM drafts WHERE id = ${id};`) + + if (!draft) throw new Error('Draft not found'); + return draft; + }); +} + +async function update(id: string, data: DraftUpdate) { + const pool = await getPool(); + return await pool.connect(async (connection) => { + return await slonikUpdate( + connection, + 'drafts', + data, + {id: parseInt(id)} + ) + }); +} + +async function destroy(id: string) { + const pool = await getPool(); + return await pool.connect(async (connection) => { + return await connection.query(sql.unsafe` + DELETE FROM drafts WHERE id = ${id} RETURNING id, title; + `) + }); +} + +export default { + index, + store, + show, + update, + destroy, +} diff --git a/tests/drafts.test.ts b/tests/drafts.test.ts index 52e426a..8fdd55b 100644 --- a/tests/drafts.test.ts +++ b/tests/drafts.test.ts @@ -9,3 +9,25 @@ describe('test suite works', () => { // expect(data).toEqual({ message: "Paginated list of proposals" }); }); }) + + + +/* +TEST DRAFT + */ + +// res.status(200).json({ +// list: [ +// { +// title: "Test Draft", +// summary: "This is a test draft", +// description: "A draft for testing", +// type: "Topic", +// id: "1", +// created: "2024-05-12T21:40:26.157Z", +// updated: "2024-05-12T21:40:26.157Z", +// }, +// ], +// }); +// }); + diff --git a/types/draft.ts b/types/draft.ts new file mode 100644 index 0000000..71d6b92 --- /dev/null +++ b/types/draft.ts @@ -0,0 +1,27 @@ +import { z } from 'zod' +const Draft = z.object({ + title: z.string().max(48), + summary: z.string().max(255), + description: z.string().max(2048), + type: z.enum(['topic', 'project']), + id: z.number().int().positive(), + created: z.number().transform(val => new Date(val)), + updated: z.number().transform(val => new Date(val)).nullable(), +}) + +type Draft = z.infer + +const PendingDraft = z.object({ + title: z.string().max(48), + summary: z.string().max(255), + description: z.string().max(2048), + type: z.enum(['topic', 'project']), +}) + +type PendingDraft = z.infer + +const DraftUpdate = PendingDraft.partial() + +type DraftUpdate = z.infer + +export { Draft, PendingDraft, DraftUpdate }