Skip to content

Commit

Permalink
feat: added drafts to routes, services, types, and database (#39)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
randomontherun authored May 15, 2024
1 parent 8f6100e commit d83d334
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 0 deletions.
2 changes: 2 additions & 0 deletions app.ts
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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!");
Expand Down
18 changes: 18 additions & 0 deletions database/db_init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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();
91 changes: 91 additions & 0 deletions routes/drafts.ts
Original file line number Diff line number Diff line change
@@ -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;
67 changes: 67 additions & 0 deletions services/drafts.ts
Original file line number Diff line number Diff line change
@@ -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<readonly Draft[]> {
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<Draft> {
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<Draft> {
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,
}
22 changes: 22 additions & 0 deletions tests/drafts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
// },
// ],
// });
// });

27 changes: 27 additions & 0 deletions types/draft.ts
Original file line number Diff line number Diff line change
@@ -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<typeof Draft>

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<typeof PendingDraft>

const DraftUpdate = PendingDraft.partial()

type DraftUpdate = z.infer<typeof DraftUpdate>

export { Draft, PendingDraft, DraftUpdate }

0 comments on commit d83d334

Please sign in to comment.